Extracting numbers from a string - ruby-on-rails

Consider i have a string like this:
"1 hour 7 mins"
I need to extract number of hour (1) and min (7). the problem is either hour or mins can be nill so in this case the string would be 1 hour ot just 7 mins
I am mostly interested in regular expression. I have already seen this and run this code
result = duration.gsub(/[^\d]/, '')
result[0]!= nil ? hour=result[0] : hour=0
result[1]!=nil ? mins=result[1] : mins=0
the problem is, when i have only 5 mins it gives me 5 and i do not know it is mins or hour
So how can i do it?

What do you think about something like this:
hours = duration.match(/[\d]* hour/).to_s.gsub(/[^\d]/, '')
minutes = duration.match(/[\d]* mins/).to_s.gsub(/[^\d]/, '')

You could do that :
a = duration[/(\d*)(\s*hour)?s?\s*(\d*)(\s*min)?s?/][0]
if a.include?("hour")
hour = a[0]
min = a[2]
else
min = a[0]
end
Improved, this is what I wanted :
capture = duration.match(/^((\d*) ?hour)?s? ?((\d*) ?min)?s?/)
hour = capture[2]
min = capture[4]
You can try the regex here :
http://rubular.com/r/ACwfzUIHBo

I couldn't resist a bit of code golf:
You can do:
hours,_,mins = (duration.match /^([\d]* h)?([^\d]*)?([\d]* m)?/)[1..3].map(&:to_i)
Explanation:
matches number then 'h', then anything not a number, then number then 'm'. Then gets the match data and does .to_i (which in ruby if it starts with a number uses this number). It then assigns 1st and third match to hours and minutes respectively:
Output:
2.2.1 :001 > duration = "5 hours 26 min"
=> "5 hours 26 min"
2.2.1 :002 > hours,_,mins = (duration.match /^([\d]* h)?([^\d]*)?([\d]* m)?/)[1..3].map(&:to_i)
=> [5, 0, 26]
2.2.1 :003 > hours
=> 5
2.2.1 :004 > mins
=> 26
2.2.1 :005 > duration = "5 hours"
=> "5 hours"
2.2.1 :006 > hours,_,mins = (duration.match /^([\d]* h)?([^\d]*)?([\d]* m)?/)[1..3].map(&:to_i)
=> [5, 0, 0]
2.2.1 :007 > duration = "54 mins"
=> "54 mins"
2.2.1 :008 > hours,_,mins = (duration.match /^([\d]* h)?([^\d]*)?([\d]* m)?/)[1..3].map(&:to_i)
=> [0, 0, 54]
2.2.1 :009 >

Related

how to convert integer(months) into human friendly string?

I need to translate a number of months, integer based, into a human friendly string containing information about years. its not easy to explain so I will just provide examples. inputs and outputs I want are:
input: 19
output: "1 year, 7 months"
input: 24
output: "2 years"
input: 26
output: "2 years, 2 months"
do you know any out of the box solutions? if not, how would you implement it yourself?
input = 26
year, month = input.divmod(12)
if month.eql? 0 and year > 1
puts "#{year} years"
elsif month.eql? 0 and year.eql? 1
puts "#{year} year"
elsif year > 1
puts "#{year} years, #{month} month"
else
puts "#{year} year, #{month} month"
end
Output
2 years, 2 month
Since this question is tagged with Ruby on Rails, ActiveSupport extensions are available, so this works too:
number_of_months = 19 # for example
ActiveSupport::Duration.build(number_of_months.months).inspect.gsub(' and',', ')
Edit
I just noticed that there was a bug in ActiveSupport::Duration version 6.0.2.2 that was fixed sometime prior to version 6.1.0.alpha that caused rounding errors for certain values of number_of_months.
Just for fun:
num.divmod(12).then { |y, m| [y, m].zip ['years', 'months'] }
.reject{ |e| e.first.zero? }
.each{ |e| e.last.delete_suffix!('s') if e.first == 1 }
.join(' ')
.tap{ |res| res.replace('0 months') if res.empty? }
Samples:
[11, 12, 13, 23, 24, 25, 26].each { |n| p [n, n.divmod(12).then.......] } # pseudo
[11, "11 months"]
[12, "1 year"]
[13, "1 year 1 month"]
[23, "1 year 11 months"]
[24, "2 years"]
[25, "2 years 1 month"]
[26, "2 years 2 months"]
def pluralize(num, string)
[num, (num == 1 ? string : "#{string}s")] * ' '
end
def humanize_months(months)
months = input % 12
years = input / 12
text = []
text << pluralize(years, 'year') if years > 0
text << pluralize(months, 'month') if months > 0
text * ', '
end

ActiveSupport::Duration incorrectly calculates interval?

I have a very strange feeling that I am getting incorrect duration calculated by ActiveSupport::Duration. Here is the essence of the code I have
require 'time'
require 'active_support/duration'
require 'active_support/gem_version'
a = Time.parse('2044-11-18 01:00:00 -0600')
b = Time.parse('2045-03-05 04:00:00 -0600')
ActiveSupport::Duration.build(b - a).inspect
ActiveSupport.gem_version
And here is what I get
[30] pry(main)> require 'time'
=> false
[31] pry(main)> require 'active_support/duration'
=> false
[32] pry(main)> require 'active_support/gem_version'
=> false
[33] pry(main)> a = Time.parse('2044-11-18 01:00:00 -0600')
=> 2044-11-18 01:00:00 -0600
[34] pry(main)> b = Time.parse('2045-03-05 04:00:00 -0600')
=> 2045-03-05 04:00:00 -0600
[35] pry(main)> ActiveSupport::Duration.build(b - a).inspect
=> "3 months, 2 weeks, 1 day, 19 hours, 32 minutes, and 42.0 seconds"
[36] pry(main)> ActiveSupport.gem_version
=> Gem::Version.new("6.0.1")
I cross-checked the result with PostgreSQL
select justify_interval('2045-03-05 04:00:00 -0600'::timestamp - '2044-11-18 01:00:00 -0600'::timestamp)
and got 3 mons 17 days 03:00:00 (or 107 days and 3 hours). Also there is a web site that gives result consistent with PostgreSQL (although web page says 107 days are 3 months and 15 days).
Am I missing something? Where minutes and seconds are coming from? Is there a better interval calculator for Ruby/Rails?
Update
distance_of_time_in_words returns 4 months!
Update 2
I ended up with slightly modified Wizard's solution wrapped up to produce text
def nice_duration(seconds)
parts = duration_in_whms(seconds)
out = []
I18n.with_options(scope: 'datetime.distance_in_words') do |locale|
out.push locale.t(:x_days, count: parts[:days]) if parts.key?(:days)
out.push locale.t(:x_hours, count: parts[:hours]) if parts.key?(:hours)
out.push locale.t(:x_minutes, count: parts[:minutes]) if parts.key?(:minutes)
end
out.join ' '
end
private
def duration_in_whms(seconds)
parts_and_seconds_in_part = {:days => 86400, :hours => 3600, :minutes => 60}
result = {}
remainder = seconds
parts_and_seconds_in_part.each do |k, v|
out = (remainder / v).to_i
result[k] = out if out.positive?
remainder -= out * v
end
result.merge(seconds: remainder)
end
Apparently localization from Action View does not have hours without about. So I also had to add corresponding translation into my locales
en:
datetime:
distance_in_words:
x_hours:
one: "1 hour"
other: "%{count} hours"
ActiveSupport::Duration calculates its value using the following constants and algorithm (I have added the explanation on what it's doing below but here is a link to the source). As you can see below, the SECONDS_PER_YEAR constant is the average number of seconds in the gregorian calendar (which is then used to define SECONDS_PER_MONTH). It is because of this, "average definition" of SECONDS_PER_YEAR and SECONDS_PER_MONTH that you are getting the unexpected hours, minutes and seconds. It is defined as an average because a month and year is not a standard fixed amount of time.
SECONDS_PER_MINUTE = 60
SECONDS_PER_HOUR = 3600
SECONDS_PER_DAY = 86400
SECONDS_PER_WEEK = 604800
SECONDS_PER_MONTH = 2629746 # This is 1/12 of a Gregorian year
SECONDS_PER_YEAR = 31556952 # The length of a Gregorian year = 365.2425 days
# You pass ActiveSupport::Duration the number of seconds (b-a) = 9255600.0 seconds
remainder_seconds = 9255600.0
# Figure out how many years fit into the seconds using integer division.
years = (remainder_seconds/SECONDS_PER_YEAR).to_i # => 0
# Subtract the amount of years from the remaining_seconds
remainder_seconds -= years * SECONDS_PER_YEAR # => 9255600.0
months = (remainder_seconds/SECONDS_PER_MONTH).to_i # => 3
remainder_seconds -= months * SECONDS_PER_MONTH # => 1366362.0
weeks = (remainder_seconds/SECONDS_PER_WEEK).to_i # => 2
remainder_seconds -= weeks * SECONDS_PER_WEEK # => 156762.0
days = (remainder_seconds/SECONDS_PER_DAY).to_i # => 1
remainder_seconds -= days * SECONDS_PER_DAY # => 70362.0
hours = (remainder_seconds/SECONDS_PER_HOUR).to_i # => 19
remainder_seconds -= hours * SECONDS_PER_HOUR # => 1962.0
minutes = (remainder_seconds/SECONDS_PER_MINUTE).to_i # => 32
remainder_seconds -= minutes * SECONDS_PER_MINUTE # => 42
seconds = remainder_seconds # => 42
puts "#{years} years, #{months} months, #{weeks} weeks, #{days} days, #{hours} hours, #{minutes} minutes, #{seconds} seconds"
# 0 years, 3 months, 2 weeks, 1 days, 19 hours, 32 minutes, 42.0 seconds
To avoid the issue you are having, I would suggest to just represent the time in week, days, hours, minutes and seconds (basically anything excluding month & year).
The number of seconds in a month is complicated if you don't use an average since you will need to account for 28, 29, 30 and 31 days for each separate month. Similarly, for the year, you will need to account for leap/non-leap if you don't use the average.
I am not sure of any gems around which do this for you, however I can provide a function which can help you calculate the duration in days, hours, minutes and seconds below.
def duration_in_whms(seconds)
parts_and_seconds_in_part = {:weeks => 604800, :days => 86400, :hours => 3600, :minutes => 60}
result = {}
remainder = seconds
parts_and_seconds_in_part.each do |k, v|
result[k] = (remainder/v).to_i
remainder -= result[k]*v
end
result.merge(seconds: remainder)
end
duration_in_whms(9255600) => # {:weeks=>15, :days=>2, :hours=>3, :minutes=>0, :seconds=>0.0}

Ruby: Calculate time difference between 2 times

I want to calculate the difference between 2 times.
start_time: 22:00 (Rails interprets this as 2015-12-31 22:00:00 +0100)
second_time: 02:00 (Rails interprets this as 2015-12-31 02:00:00 +0100). The second time is 4 hours later, so in the next day. Is there a way to calculate this difference?
I can not simply do this: second_time - first_time, because this gives me a difference of 22 hours instead of 4 hours.
Edit:
Some background information:
A job is starting at 22:00 and ending the next day at 02:00. Because i fill in the form of this job only times, this times for the above 2 values are 2015-12-31 22:00:00 +0100 and 2015-12-31 02:00:00 +0100. I don't want the user to fill in the time including the date. The real difference between the times should be 4 hours.
So what i actually want is calculate the difference between 22:00 and 02:00 (in the next day).
I don't understand why you think it should return 4 hours or why it does return 22 hours. 20 hours would be correct for your example:
require 'time'
a = Time.parse('2015-12-31 22:00:00 +0100')
b = Time.parse('2015-12-31 02:00:00 +0100')
a - b
#=> 72000.0 # difference in seconds
(a - b) / 3600
#=> 20.0 # difference in hours
Update: It seems like you are dealing only with the time portion and not with the actual date. And I assume the maximum difference you will have to deal with is 24 hours:
def time_difference(time_a, time_b)
difference = time_b - time_a
if difference > 0
difference
else
24 * 3600 + difference
end
end
a = Time.parse('2015-12-31 22:00:00 +0100')
b = Time.parse('2015-12-31 02:00:00 +0100')
time_difference(a, b) / 3600
# => 4 # hours
a = Time.parse('2015-12-31 02:00:00 +0100')
b = Time.parse('2015-12-31 22:00:00 +0100')
time_difference(a, b) / 3600
# => 20 # hours
Old question but I did a nice method to deal with it:
def time(start,ending)
if start != ending
medidas=["year","month","day","hour","minute","second"]
array=[1970,1,1,0,0,0]
text = ""
Time.at(ending-start).utc.to_a.take(6).reverse.each_with_index do |k,i|
text = "#{text} #{I18n.translate medidas[i].to_sym, count: k-array[i]}"
end
text = text.strip.squish
pos = text.rindex(" ",(text.rindex(" ")-1))
unless pos.nil?
text = text.insert(pos," and")
end
text = text.strip.squish #This shouldn't be needed but just in case
else
"0 seconds"
end
end
Then in config/locales/en.yml I added:
en:
año:
zero: ''
one: '1 year'
other: '%{count} years'
mes:
zero: ''
one: '1 month'
other: '%{count} months'
dia:
zero: ''
one: '1 day'
other: '%{count} days'
hora:
zero: ''
one: '1 hour'
other: '%{count} hours'
minuto:
zero: ''
one: '1 minute'
other: '%{count} minutes'
segundo:
zero: ''
one: '1 second'
other: '%{count} seconds'
So for example when you call:
start = Time.now
ending = start + (60*60)
time(start,ending)
=> "1 hour"
ending = start + (60*60*28)
time(start,ending)
=> "1 day and 4 hours"
ending = start + (53*60*5874)
time(start,ending)
=> "7 months 4 days 4 hours and 42 minutes"
Hope it's useful
I'd write it thusly (before adding data checks), in an attempt to make it self-documenting:
require 'time'
DT_FMT = '%Y-%m-%d %H:%M:%S %z'
SECONDS_PER_DAY = 24*60*60
def hours_elapsed(start_str, finish_str)
start = DateTime.strptime(start_str, DT_FMT).to_time
finish = DateTime.strptime(finish_str, DT_FMT).to_time
finish = same_time_tomorrow(finish) if finish < start
(finish-start)/3600
end
def same_time_tomorrow(time)
time + SECONDS_PER_DAY
end
hours_elapsed '2015-12-31 22:00:00 +0100',
'2015-12-31 02:00:00 +0100'
#=> 4.0
hours_elapsed '2015-12-31 02:00:00 +0100',
'2015-12-31 22:00:00 +0100'
#=> 20.0
It may be better for the arguments of hours_elapsed to be strings containing hours and minutes only, in which case we might rename the method as well. time_elapsed("18:00", "2:30") is an example of how this method might be invoked.
MINUTES_PER_DAY = 24*60
def time_elapsed(start_str, finish_str)
start_mins = time_str_to_minutes(start_str)
finish_mins = time_str_to_minutes(finish_str)
finish_mins += MINUTES_PER_DAY if
finish_mins < start_mins
(finish_mins-start_mins).divmod(60)
end
def time_str_to_minutes(str)
hrs, mins = str.split(':').map(&:to_i)
60 * hrs + mins
end
time_elapsed("8:00", "17:30")
#=> [9, 30]
time_elapsed("18:00", "2:30")
#=> [8, 30]

Map integers 0-6 to weekday names

I have a class with two attributes saving weekdays with numeric values. I had hoped to be able to use Enum, but appearantly you can not use the same value for two attributes with Enum.
How could I represent the integer value of an attribute to the corresponding weekday?
0 => "monday"
4 => "friday"
Date::DAYNAMES[(i + 1) % 7]
where i is your integer
2.0.0-p247 :001 > Date::DAYNAMES[(0 + 1) % 7]
=> "Monday"
2.0.0-p247 :002 > Date::DAYNAMES[(4 + 1) % 7]
=> "Friday"
2.0.0-p247 :003 > Date::DAYNAMES[(6 + 1) % 7]
=> "Sunday"
You cannot do a straight look up on the index because in DAYNAMES 0 is Sunday and you want 0 to be Monday.

Float numbers in ERB/Rails

I'm pretty new to Rails, I'm passing 2 variables to a view, in my controller have them defined like
#correct = 5
#total = 40
in my view I'm trying to mark them up like:
Score = <%=#score%>/<%=#total%> = <%=(#score/#total)%>
It outputs 0 for the division. Do I need to explicitly define that equation to output a float or something? How do I get it to output 0.125 instead of 0
Thanks guys
Decide whether you want to use #correct or #score. Also to use double division instead of integer multiply #score by 1.0:
<%=(1.0 * #score/#total)%>
Or alternatively cast #score to float:
<%=(#score.to_f/#total)%>
You need to explicitly convert your integers into floats:
1.9.3p0 :001 > a = 5
=> 5
1.9.3p0 :002 > b = 40
=> 40
1.9.3p0 :003 > a / b
=> 0
1.9.3p0 :005 > (a / b).to_f
=> 0.0
1.9.3p0 :006 > a.to_f / b.to_f
=> 0.125
In your case:
Score = <%=#score%>/<%=#total%> = <%=(#score.to_f/#total.to_f)%>

Resources