How do I parse a simple time string into a time period? - ruby

In my Ruby app, I have an Investment entity that has a term attribute.
I need this class to accept a String from user input in the form of 3 Years or 36 months. What I want is to then convert the input into number of months, set the term attribute to this period and figure out the maturity date.
So far I have tried using Active Support and Chronic but the APIs do not support this.
This getter works:
def term
if term =~ /year[s]?/i
term = term.to_i * 12
else term =~ /month[s]?/i
term = term.to_i
end
end
But is there a more elegant way to do this in Ruby?

Ruby doesn't have anything built-in that represents a "time-span" (some other languages do). However, there is a library for it (timespan), although it may be a bit overkill for your situation.
You mentioned that chronic doesn't support this. But why not just calculate the time difference yourself?
require 'chronic'
input = '2 years'
then = Chronic.parse(input + ' ago')
now = Time.now
# Now we just calculate the number of months
term = (now.year * 12 + now.month) - (then.year * 12 + then.month)
That way, you get the flexibility of chronic's parsing, and you still don't need much code.
Or just go ahead and use the timespan library.
require 'timespan'
term = Timespan.new('2 years').to_months
# Boom.

If we can assume that the input string will always contain one or more digits followed a unit ("years", "Year", "months", etc.), this is pretty straightforward. Just write a regular expression that captures the digits and the unit, convert the digits to a number and normalize the unit, and do the math.
def to_months(str)
if str =~ /(\d+)\s*(month|year)s?/i
num = $1.to_i # => 3
unit = $2.downcase # => year
num *= 12 if unit == "year"
return num
end
raise ArgumentError, "Invalid input"
end
puts to_months("3 Years") # => 36
puts to_months("1 month") # => 1
puts to_months("6months") # => 6
It's not a whole lot more elegant than your method, but perhaps it'll give you an idea or two.

Related

How do I destructure a range in Ruby?

Is it possible to use destructuring in ruby to extract the end and beginning from a range?
module PriceHelper
def price_range_human( range )
"$%s to $%s" % [range.begin, range.end].map(:number_to_currency)
end
end
I know that I can use array coercion as a really bad hack:
first, *center, last = *rng
"$%s to $%s" % [first, last].map(:number_to_currency)
But is there a syntactical way to get begin and end without actually manually creating an array?
min, max = (1..10)
Would have been awesome.
You can use minmax to destructure ranges:
min, max = (1..10).minmax
min # => 1
max # => 10
If you are using Ruby before 2.7, avoid using this on large ranges.
The beginning and end? I'd use:
foo = 1..2
foo.min # => 1
foo.max # => 2
Trying to use destructuring for a range is a bad idea. Imagine the sizes of the array that could be generated then thrown away, wasting CPU time and memory. It's actually a great way to DOS your own code if your range ends with Float::INFINITY.
end is not the same as max: in 1...10, end is 10, but max is 9
That's because start_val ... end_val is equivalent to start_val .. (end_val - 1):
start_value = 1
end_value = 2
foo = start_value...end_value
foo.end # => 2
foo.max # => 1
foo = start_value..(end_value - 1)
foo.end # => 1
foo.max # => 1
max reflects the reality of the values actually used by Ruby when iterating over the range or testing for inclusion in the range.
In my opinion, end should reflect the actual maximum value that will be considered inside the range, not the value used at the end of the definition of the range, but I doubt that'll change otherwise it'd affect existing code.
... is more confusing and leads to increased maintenance problems so its use is not recommended.
No, Until I am proven incorrect by Cary Swoveland, Weekly World News or another tabloid, I'll continue believing without any evidence that the answer is "no"; but it's easy enough to make.
module RangeWithBounds
refine Range do
def bounds
[self.begin, self.end]
end
end
end
module Test
using RangeWithBounds
r = (1..10)
b, e = *r.bounds
puts "#{b}..#{e}"
end
Then again, I'd just write "#{r.begin.number_to_currency}..#{r.end.number_to_currency}" in the first place.
Amadan's answer is fine. you just need to remove the splat (*) when using it since it is not needed
eg,
> "%s to %s" % (1..3).bounds.map{|x| number_to_currency(x)}
=> "$1.00 to $3.00"

(Ruby) Padding single digits when it comes to time

I have 2 methods in a Timer class I'm creating. One method is where hours, minutes, and seconds are calculated from any given amount of seconds and the other method will pad any single digits with a "0". Every things I've look up so far isn't work for me. Below is my code:
class Timer
attr_accessor :seconds=(time), :time_string
def initialize(seconds = 00)
#seconds = seconds
end
def time_string
hours = padded((#seconds/3600)
minutes = padded(#seconds/60 - hours * 60)
seconds = padded(#seconds%60)
puts '#{hours):#{minutes}:#{seconds}'
end
def padded(x)
if x.length == 1
"0"+x
end
end
end
so if I put in 7683, the output I want to get is "02:08:03". but when I execute it, I get the following error message:
(eval):6: (eval):6:in `-': String can't be coerced into Fixnum (TypeError)
from (eval):6:in `time'
from (eval):19
I'm confused where this is erroring out.
To answer your question as to why your code is not working, you have got couple of conversion issues between FixNum and String throughout your code, you can fix it as follows:
def time_string(seconds)
hours = seconds/3600
minutes = seconds/60 - (hours * 60)
seconds = seconds%60
puts padded(hours)+':'+padded(minutes)+':'+padded(seconds)
end
You use the hours variable in the second statement, but because its already converted to string, it crashes, therefore its better to do all the calculations first, and only later use the padded method which returns the padded digits in string format. The padded method must also be modified to be consistent:
def padded(x)
if x.to_s.length == 1
return "0"+x.to_s
else
return x.to_s
end
end
Just keep in mind that the combination of the two methods will work only for numbers up to 86399, which will return 23:59:59. Any number passed to time_string bigger than that will pass the 24 hour mark and will return something like: 26:00:00
There is a brilliant method for padding, why not use it?
3.to_s.rjust(10,"*") #=> "*********3"
4.to_s.rjust(2,"0") #=> "04"
44.to_s.rjust(2,"0") #=> "44"
If you want a better solution than writing your own class, use at
Time.at(7683).strftime("%H:%M:%S") #=> "02:08:03"
There's no need to reinvent the wheel.
t = 7683 # seconds
Time.at(t).strftime("%H:%M:%S")
Time.at(seconds) converts your seconds into a time object, which then you can format with strftime. From the strftime documentation you can see you can get the parameters you want non padded, white padded or zero padded.
I tend to use something like this
"%02d" % 2 #=> 02
"%02d" % 13 #=> 13
It's part of the Kernel module: http://ruby-doc.org/core-2.1.3/Kernel.html#M001433

Multiplying variables in Ruby?

I'm trying to create a simple pay predictor program.
I am capturing two variables, then want to multiply those variables. I have tried doing
pay = payrate * hourrate
but it seems not to work. I was told to put pay in a definition, then I tried to display the variable but it still didn't work. When I looked at the documentation, it seems that I am using the correct operator. Any guesses?
# Simple Budgeting Program
puts "What is your budget type?\nYou can say 'Monthly' 'Weekly' or 'Fortnightly'"
payperiod = gets.chomp
case payperiod
when "Monthly"
puts "You are paid monthly"
when "Weekly"
puts "You are paid weekly"
when "Fortnightly"
puts "You are paid every two weeks."
else
puts "You did not input a correct answer."
end
puts "What is your hourly rate?\n"
payrate = gets.chomp
puts "How many hours have you worked during your pay period?\n"
hourrate = gets.chomp
def pay
payrate * hourrate
end
puts "#{pay}"
preventclose = gets.chomp
The def has nothing to do with it. payrate and hourrate are strings and * means a very different thing to strings. You need to convert them to numbers first with to_i or to_f.
payrate.to_f * hourrate.to_f
You declared pay rate and hour rate as strings. In Ruby, you cannot multiply strings by other strings. However, in Ruby there are type conversions. Ruby's string class offers a method of converting strings to integers.
string = "4"
string.to_i => 4
In your case, you first need to convert BOTH strings to an integer.
def pay
payrate.to_i * hourrate.to_i
end
Here's some great information about strings.
http://www.ruby-doc.org/core-2.1.2/String.html
Hope this helps
Coercing Strings into Numbers, and Back Again
Kernel#gets generally returns a string, not an integer or floating point number. In order to perform numeric calculations, you need to coerce the string into a number first. There are a number of ways to do this, but the Kernel#Float method is often safer than String#to_i because the Kernel method will raise an exception if a string can't be coerced. For example:
print 'Pay Rate: '
rate = Float(gets.chomp)
print 'Hours Worked: '
print hours = Float(gets.chomp)
Of course, operations on floating point numbers can be inaccurate, so you might want to consider using Kernel#Rational and then converting to floating point for your output. For example, consider:
# Return a float with two decimal places as a string.
def pay rate, hours
sprintf '%.2f', Rational(rate) * Rational(hours)
end
p pay 10, 39.33
"393.30"
#=> "393.30"

Time-of-day range in Ruby?

I want to know if a time belongs to an schedule or another.
In my case is for calculate if the time is in night schedule or normal schedule.
I have arrived to this solution:
NIGHT = ["21:00", "06:00"]
def night?( date )
date_str = date.strftime( "%H:%M" )
date_str > NIGHT[0] || date_str < NIGHT[1]
end
But I think is not very elegant and also only works for this concrete case and not every time range.
(I've found several similar question is SO but all of them make reference to Date ranges no Time ranges)
Updated
Solution has to work for random time ranges not only for this concrete one. Let's say:
"05:00"-"10:00"
"23:00"-"01:00"
"01:00"-"01:10"
This is actually more or less how I would do it, except maybe a bit more concise:
def night?( date )
!("06:00"..."21:00").include?(date.strftime("%H:%M"))
end
or, if your schedule boundaries can remain on the hour:
def night?(date)
!((6...21).include? date.hour)
end
Note the ... - that means, basically, "day time is hour 6 to hour 21 but not including hour 21".
edit: here is a generic (and sadly much less pithy) solution:
class TimeRange
private
def coerce(time)
time.is_a? String and return time
return time.strftime("%H:%M")
end
public
def initialize(start,finish)
#start = coerce(start)
#finish = coerce(finish)
end
def include?(time)
time = coerce(time)
#start < #finish and return (#start..#finish).include?(time)
return !(#finish..#start).include?(time)
end
end
You can use it almost like a normal Range:
irb(main):013:0> TimeRange.new("02:00","01:00").include?(Time.mktime(2010,04,01,02,30))
=> true
irb(main):014:0> TimeRange.new("02:00","01:00").include?(Time.mktime(2010,04,01,01,30))
=> false
irb(main):015:0> TimeRange.new("01:00","02:00").include?(Time.mktime(2010,04,01,01,30))
=> true
irb(main):016:0> TimeRange.new("01:00","02:00").include?(Time.mktime(2010,04,01,02,30))
=> false
Note, the above class is ignorant about time zones.
In Rails 3.2 it has added Time.all_day and similars as a way of generating date ranges. I think you must see how it works. It may be useful.

How do I generate a random 10 digit number in ruby?

Additionally, how can I format it as a string padded with zeros?
To generate the number call rand with the result of the expression "10 to the power of 10"
rand(10 ** 10)
To pad the number with zeros you can use the string format operator
'%010d' % rand(10 ** 10)
or the rjust method of string
rand(10 ** 10).to_s.rjust(10,'0')
I would like to contribute probably a simplest solution I know, which is a quite a good trick.
rand.to_s[2..11]
=> "5950281724"
This is a fast way to generate a 10-sized string of digits:
10.times.map{rand(10)}.join # => "3401487670"
The most straightforward answer would probably be
rand(1e9...1e10).to_i
The to_i part is needed because 1e9 and 1e10 are actually floats:
irb(main)> 1e9.class
=> Float
DON'T USE rand.to_s[2..11].to_i
Why? Because here's what you can get:
rand.to_s[2..9] #=> "04890612"
and then:
"04890612".to_i #=> 4890612
Note that:
4890612.to_s.length #=> 7
Which is not what you've expected!
To check that error in your own code, instead of .to_i you may wrap it like this:
Integer(rand.to_s[2..9])
and very soon it will turn out that:
ArgumentError: invalid value for Integer(): "02939053"
So it's always better to stick to .center, but keep in mind that:
rand(9)
sometimes may give you 0.
To prevent that:
rand(1..9)
which will always return something withing 1..9 range.
I'm glad that I had good tests and I hope you will avoid breaking your system.
Random number generation
Use Kernel#rand method:
rand(1_000_000_000..9_999_999_999) # => random 10-digits number
Random string generation
Use times + map + join combination:
10.times.map { rand(0..9) }.join # => random 10-digit string (may start with 0!)
Number to string conversion with padding
Use String#% method:
"%010d" % 123348 # => "0000123348"
Password generation
Use KeePass password generator library, it supports different patterns for generating random password:
KeePass::Password.generate("d{10}") # => random 10-digit string (may start with 0!)
A documentation for KeePass patterns can be found here.
Just because it wasn't mentioned, the Kernel#sprintf method (or it's alias Kernel#format in the Powerpack Library) is generally preferred over the String#% method, as mentioned in the Ruby Community Style Guide.
Of course this is highly debatable, but to provide insight:
The syntax of #quackingduck's answer would be
# considered bad
'%010d' % rand(10**10)
# considered good
sprintf('%010d', rand(10**10))
The nature of this preference is primarily due to the cryptic nature of %. It's not very semantic by itself and without any additional context it can be confused with the % modulo operator.
Examples from the Style Guide:
# bad
'%d %d' % [20, 10]
# => '20 10'
# good
sprintf('%d %d', 20, 10)
# => '20 10'
# good
sprintf('%{first} %{second}', first: 20, second: 10)
# => '20 10'
format('%d %d', 20, 10)
# => '20 10'
# good
format('%{first} %{second}', first: 20, second: 10)
# => '20 10'
To make justice for String#%, I personally really like using operator-like syntaxes instead of commands, the same way you would do your_array << 'foo' over your_array.push('123').
This just illustrates a tendency in the community, what's "best" is up to you.
More info in this blogpost.
I ended up with using Ruby kernel srand
srand.to_s.last(10)
Docs here: Kernel#srand
Here is an expression that will use one fewer method call than quackingduck's example.
'%011d' % rand(1e10)
One caveat, 1e10 is a Float, and Kernel#rand ends up calling to_i on it, so for some higher values you might have some inconsistencies. To be more precise with a literal, you could also do:
'%011d' % rand(10_000_000_000) # Note that underscores are ignored in integer literals
('%010d' % rand(0..9999999999)).to_s
or
"#{'%010d' % rand(0..9999999999)}"
I just want to modify first answer. rand (10**10) may generate 9 digit random no if 0 is in first place. For ensuring 10 exact digit just modify
code = rand(10**10)
while code.to_s.length != 10
code = rand(11**11)
end
Try using the SecureRandom ruby library.
It generates random numbers but the length is not specific.
Go through this link for more information: http://ruby-doc.org/stdlib-2.1.2/libdoc/securerandom/rdoc/SecureRandom.html
Simplest way to generate n digit random number -
Random.new.rand((10**(n - 1))..(10**n))
generate 10 digit number number -
Random.new.rand((10**(10 - 1))..(10**10))
This technique works for any "alphabet"
(1..10).map{"0123456789".chars.to_a.sample}.join
=> "6383411680"
Just use straightforward below.
rand(10 ** 9...10 ** 10)
Just test it on IRB with below.
(1..1000).each { puts rand(10 ** 9...10 ** 10) }
To generate a random, 10-digit string:
# This generates a 10-digit string, where the
# minimum possible value is "0000000000", and the
# maximum possible value is "9999999999"
SecureRandom.random_number(10**10).to_s.rjust(10, '0')
Here's more detail of what's happening, shown by breaking the single line into multiple lines with explaining variables:
# Calculate the upper bound for the random number generator
# upper_bound = 10,000,000,000
upper_bound = 10**10
# n will be an integer with a minimum possible value of 0,
# and a maximum possible value of 9,999,999,999
n = SecureRandom.random_number(upper_bound)
# Convert the integer n to a string
# unpadded_str will be "0" if n == 0
# unpadded_str will be "9999999999" if n == 9_999_999_999
unpadded_str = n.to_s
# Pad the string with leading zeroes if it is less than
# 10 digits long.
# "0" would be padded to "0000000000"
# "123" would be padded to "0000000123"
# "9999999999" would not be padded, and remains unchanged as "9999999999"
padded_str = unpadded_str.rjust(10, '0')
rand(9999999999).to_s.center(10, rand(9).to_s).to_i
is faster than
rand.to_s[2..11].to_i
You can use:
puts Benchmark.measure{(1..1000000).map{rand(9999999999).to_s.center(10, rand(9).to_s).to_i}}
and
puts Benchmark.measure{(1..1000000).map{rand.to_s[2..11].to_i}}
in Rails console to confirm that.
An alternative answer, using the regexp-examples ruby gem:
require 'regexp-examples'
/\d{10}/.random_example # => "0826423747"
There's no need to "pad with zeros" with this approach, since you are immediately generating a String.
This will work even on ruby 1.8.7:
rand(9999999999).to_s.center(10, rand(9).to_s).to_i
A better approach is use Array.new() instead of .times.map. Rubocop recommends it.
Example:
string_size = 9
Array.new(string_size) do
rand(10).to_s
end
Rubucop, TimesMap:
https://www.rubydoc.info/gems/rubocop/RuboCop/Cop/Performance/TimesMap
In my case number must be unique in my models, so I added checking block.
module StringUtil
refine String.singleton_class do
def generate_random_digits(size:)
proc = lambda{ rand.to_s[2...(2 + size)] }
if block_given?
loop do
generated = proc.call
break generated if yield(generated) # check generated num meets condition
end
else
proc.call
end
end
end
end
using StringUtil
String.generate_random_digits(3) => "763"
String.generate_random_digits(3) do |num|
User.find_by(code: num).nil?
end => "689"(This is unique in Users code)
I did something like this
x = 10 #Number of digit
(rand(10 ** x) + 10**x).to_s[0..x-1]
Random 10 numbers:
require 'string_pattern'
puts "10:N".gen

Resources