Count working hours between two datetimes in Ruby - ruby-on-rails

I want to count "morning" and "evening" working hours between two datetimes in Ruby.
"Morning" working hours starts at 06:00:00 and ends at 21:59:59
"Evening" working hours starts at 22:00:00 and ends at 05:59:59
For example, employee started work at 2014-06-04 21:45 and completed at 2014-06-05 05:45, it means, in summary should be:
0.25 "morning" working hours
7.75 "evening" working hours
Total 8 working hours

This could be done using the biz gem:
morning_schedule = Biz::Schedule.new do |config|
config.hours = {
mon: {'06:00' => '22:00'},
tue: {'06:00' => '22:00'},
wed: {'06:00' => '22:00'},
thu: {'06:00' => '22:00'},
fri: {'06:00' => '22:00'}
}
end
evening_schedule = Biz::Schedule.new do |config|
config.hours = {
mon: {'00:00' => '06:00', '22:00' => '24:00'},
tue: {'00:00' => '06:00', '22:00' => '24:00'},
wed: {'00:00' => '06:00', '22:00' => '24:00'},
thu: {'00:00' => '06:00', '22:00' => '24:00'},
fri: {'00:00' => '06:00', '22:00' => '24:00'}
}
end
# morning hours worked
morning_schedule.within(
Time.new(2014, 6, 4, 21, 45),
Time.new(2014, 6, 5, 5, 45)
)
# evening hours worked
evening_schedule.within(
Time.new(2014, 6, 4, 21, 45),
Time.new(2014, 6, 5, 5, 45)
)
With the duration of each working time type determined, subsequent calculations, such as with salary coefficients, can be performed.

You can use the working_hours gem that does this kind of computing : https://github.com/intrepidd/working_hours

Here's an ugly implementation (should work for any range of time):
def time_test(tstr1, tstr2)
t1 = Time.parse(tstr1)
t2 = Time.parse(tstr2)
morning_seconds = 0
evening_seconds = 0
while t1 < t2 do
# if we are starting during morning hours...
if t1.hour >= 6 && t1.hour < 22
# this sets the end of morning hours for the current day...
tend = t1.change(hour: 22)
# but our actual morning_end should be the lesser of the range end time
# and the end of current days' working hours
morning_end = tend < t2 ? tend : t2
# increment our seconds with the difference between start time and morning end
morning_seconds += morning_end - t1
# set our start time to morning_end. if morning_end == t2, then we'll exit
# the while loop, else we'll process some evening hours
t1 = morning_end
# if we are starting during evening hours (at end of day)
elsif t1.hour >= 22
# same as above, except that the potential end time needs to increment the day
# as well as the hour
tend = t1.tomorrow.change(hour: 6)
evening_end = tend < t2 ? tend : t2
evening_seconds += evening_end - t1
t1 = evening_end
# if we are starting during evening hours (at beginning of day)
elsif t1.hour < 6
tend = t1.change(hour: 6)
evening_end = tend < t2 ? tend : t2
evening_seconds += evening_end - t1
t1 = evening_end
end
end
{
morning: morning_seconds / 3600,
evening: evening_seconds / 3600,
total: (morning_seconds + evening_seconds) / 3600
}
end
time_test('2014-06-04 21:45', '2014-06-05 05:45')
# => {:morning=>0.25, :evening=>7.75, :total=>8.0}
And here is a slightly prettier recursive way:
def time_test(tstr1, tstr2)
_t1 = Time.parse(tstr1)
t2 = Time.parse(tstr2)
runner = lambda do |t1, ms, es|
return {
morning: ms / 3600, evening: es / 3600, total: (ms + es) / 3600
} if t1 >= t2
if t1.hour >= 6 && t1.hour < 22
tend = t1.change(hour: 22)
morning_end = tend < t2 ? tend : t2
runner.call(morning_end, ms + (morning_end - t1), es)
elsif t1.hour >= 22
tend = t1.tomorrow.change(hour: 6)
evening_end = tend < t2 ? tend : t2
runner.call(evening_end, ms, es + (evening_end - t1))
elsif t1.hour < 6
tend = t1.change(hour: 6)
evening_end = tend < t2 ? tend : t2
runner.call(evening_end, ms, es + (evening_end - t1))
end
end
runner.call(_t1, 0, 0)
end

Related

Calculate how many n.months between two dates

I want to calculate the distance between two dates by month:
Q: start_date + n.months >= end_dates, what is the variable n?
start_date = Date.parse('2021-01-31')
end_date = Date.parse('2021-02-28')
## start_date + 1.months = 2021-02-28
## The answer 1 month, which is more clearable for human using
## Don't want to find the answer like 28 days or 0.93 month. (30 day a month)
First I tried to let each month is 30.days, but the answer will have some bias on some special dates. Each month's date is different, and the date on End of Feb month is always the problem.
Then I tried to install gems like date_diff, time_difference..., but no methods can do this, most of the output is 28 days but not 1 month.
For simple way, I can easily do the iterated loop to find the n, like:
start_date = Date.parse('2021-01-31')
end_date = Date.parse('2021-02-28')
def calc_diff(start_date, end_date)
n = 0
while start_date + n.months < end_date
n += 1
end
n
end
Is there any better way to find the n months between two dates instead, but not use a loop?
Thank you.
My understanding of the question is consistent with the examples below. I have computed the difference between Date objects date1 and date2, where date2 >= date1.
require 'date'
def months_between(date1, date2)
12*(date2.yr - date1.yr) + date2.mon - date1.mon + date2.day > date1.day ? 1 : 0
end
months_between Date.new(2020, 1, 22), Date.new(2020, 3, 21) #=> 2
months_between Date.new(2020, 1, 22), Date.new(2020, 3, 22) #=> 2
months_between Date.new(2020, 1, 22), Date.new(2020, 3, 23) #=> 3
months_between Date.new(2020, 1, 22), Date.new(2021, 3, 21) #=> 14
months_between Date.new(2020, 1, 22), Date.new(2021, 3, 22) #=> 14
months_between Date.new(2020, 1, 22), Date.new(2021, 3, 23) #=> 15
# find minimum n so that `start_date + n.months >= end_dates`
def calc_diff(start_date, end_date)
diff = (end_date.yday - start_date.yday) / 30
return diff if start_date + diff.months >= end_date
diff + 1
end
calc_diff(Date.parse('2021-01-31'), Date.parse('2021-02-28')) # 1
calc_diff(Date.parse('2021-01-31'), Date.parse('2021-04-30')) # 3
calc_diff(Date.parse('2021-01-31'), Date.parse('2021-05-31')) # 4
calc_diff(Date.parse('2021-02-01'), Date.parse('2021-06-01')) # 4
calc_diff(Date.parse('2021-02-01'), Date.parse('2021-06-02')) # 5
Thanks for #Cary's and #Lam's answer.
Here is my answer to find the n month.
# try to find the minimum n month between start_date and target_date
def calc_diff(start_date, target_date)
months_diff = (target_date.year * 12 + target_date.month) - (start_date.year * 12 + start_date.month)
## need to check the end of month because some special case
## start date: 2020-01-31 ; end date 2020-06-30
## the minimum n month must be 5
## another special case of Feb must consider (test case 15)
if start_date.day > target_date.day && !((start_date == start_date.end_of_month || target_date.month == 2) && (target_date == target_date.end_of_month))
months_diff = months_diff - 1
end
puts months_diff # it will show the minimum whole n month
# the target_date will between inside
# (start_date + months_diff.months) <= target_date < (start_date + (months_diff + 1).months)
(start_date + months_diff.months)..(start_date + (months_diff + 1).months)
end
The Test Cases:
## test case 1
## 6/15 - 7/15 => n = 5
calc_diff(Date.parse('2020-01-15'), Date.parse('2020-06-19'))
## test case 2
## 7/15 - 8/15 => n = 6
calc_diff(Date.parse('2020-01-15'), Date.parse('2020-07-15'))
## test case 3
## 5/15 - 6/15 => n = 4
calc_diff(Date.parse('2020-01-15'), Date.parse('2020-06-01'))
## test case 4 (special case)
## 6/30 - 7/31 => n = 5
calc_diff(Date.parse('2020-01-31'), Date.parse('2020-06-30'))
## test case 5
## 7/30 - 8/30 => n = 4
calc_diff(Date.parse('2020-04-30'), Date.parse('2020-07-31'))
## test case 6
## 6/30 - 7/30 => n = 2
calc_diff(Date.parse('2020-04-30'), Date.parse('2020-06-30'))
## test case 7
## 5/31 - 6/30 => n = 4
calc_diff(Date.parse('2020-01-31'), Date.parse('2020-05-31'))
## test case 8
## 2/29 - 3/31 => n = 1
calc_diff(Date.parse('2020-01-31'), Date.parse('2020-02-29'))
## test case 9
## 6/29 - 7/29 => n = 4
calc_diff(Date.parse('2020-02-29'), Date.parse('2020-06-30'))
## test case 10
## 7/29 - 8/29 => n = 5
calc_diff(Date.parse('2020-02-29'), Date.parse('2020-07-31'))
## test case 11
## 1/31 - 2/29 => n = 0
calc_diff(Date.parse('2020-01-31'), Date.parse('2020-02-28'))
## test case 12
## 2/29 - 3/31 => n = 1
calc_diff(Date.parse('2020-01-31'), Date.parse('2020-03-01'))
## test case 13
## 1/17 - 2/17 => n = 0
calc_diff(Date.parse('2020-01-17'), Date.parse('2020-01-17'))
## test case 14
## 1/17 - 2/17 => n = 0
calc_diff(Date.parse('2020-01-17'), Date.parse('2020-01-18'))
## test case 15 (special case)
## 1/30 - 2/29 => n = 1
calc_diff(Date.parse('2019-12-30'), Date.parse('2020-02-28'))
## test case 16
## 2/29 - 3/30 => n = 2
calc_diff(Date.parse('2019-12-30'), Date.parse('2020-02-29'))

Shortening a long if else statement

I have a long if else statement:
rnd = rand(1..1000)
if rnd >= 600
0
elsif rnd < 600 && rnd >= 350
1
elsif rnd < 350 && rnd >= 270
2
elsif rnd < 270 && rnd >= 200
3
elsif rnd < 200 && rnd >= 150
4
elsif rnd < 150 && rnd >= 100
5
elsif rnd < 100 && rnd >= 80
6
elsif rnd < 80 && rnd >= 50
7
elsif rnd < 50 && rnd >= 30
8
else
9
end
I would like to shorten it. Is it possible?
My rubocop swears at this long method.
I would start with something like this:
RANGES = {
(0...30) => 9,
(30...50) => 8,
(50...80) => 7,
# ...
(350...600) => 1,
(600...1000) => 0
}
rnd = rand(1..1000)
RANGES.find { |k, _| k.cover?(rnd) }.last
Great answers already! Just chiming in since I had a suspicion that ruby could handle this with a case statement, and it appears to be able to do so:
rnd = rand(1..1000)
case rnd
when 600.. then 0
when 350...600 then 1
when 270...350 then 2
...
else 9
end
Regardless of the approach taken, you're going to have to specify the ranges somewhere, so I think using something like a case statement is appropriate here (sorry! It doesn't shorten the code more than a few lines). Using a hash would also be a great approach (and might allow you to move the hash elsewhere), as other commenters have already shown.
It's worth mentioning, with ruby ranges, .. means that the range is inclusive and includes the last value (1..10 includes the number 10), and ... means the range is exclusive where it does not include the last value.
The top case 600.. is an endless range, which means it will match anything greater than 600. (That functionality was added in ruby 2.6)
You can simplify your conditions by using only the lower bound.
And you can avoid repeting elsif because it is cumbersome
rnd = rand(1..1000)
lower_bounds = {
600 => 0,
350 => 1,
270 => 2,
200 => 3,
150 => 4,
100 => 5,
80 => 6,
50 => 7,
30 => 8,
0 => 9,
}
lower_bounds.find { |k, _| k <= rnd }.last
MX = 1000
LIMITS = [600, 350, 270, 200, 150, 100, 80, 50, 30, 0]
The required index can be computed as follows.
def doit
rnd = rand(1..MX)
LIMITS.index { |n| n <= rnd }
end
doit
#=> 5
In this example rnd #=> 117.
If this must be repeated many times, and speed is paramount, you could do the following.
LOOK_UP = (1..MX).each_with_object({}) do |m,h|
h[m] = LIMITS.index { |n| n <= m }
end
#=> {1=>9, 2=>9,..., 29=>9,
# 30=>8, 31=>8,..., 49=>8,
# ...
# 600=>0, 601=>0,..., 1000=>0}
Then simply
def doit
LOOK_UP[rand(1..MX)]
end
doit
#=> 3
In this example rand(1..MX) #=> 262.
If speed were paramount but MX were so large that the previous approach would require excessive memory, you could use a binary search.
def doit
rnd = rand(1..MX)
LIMITS.bsearch_index { |n| n <= rnd }
end
doit
#=> 5
In this example rnd #=> 174.
See Array#bsearch_index. bsearch_index returns the correct index in O(log n), n being LIMITS.size). bsearch_index requires the array on which it operates to be ordered.

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}

Adding BigDecimal hours to DateTime is wrong by 1 second

This happens with the update to ActiveSupport 6
start_time = DateTime.now.beginning_of_day
start_time + BigDecimal(2).hours #=> Wed, 11 Sep 2019 01:59:59 +0000
Oddly enough this works fine with Time
start_time = Time.now.beginning_of_day
start_time + BigDecimal(2).hours #=> 2019-09-11 02:00:00 +0000
Can anybody explain why?
Ultimately, it boils down to floating point errors in some of the math that ActiveSupport does internally.
Notice that using Rational instead of BigDecimal works:
DateTime.now.beginning_of_day + Rational(2, 1).hours
# => Mon, 02 Dec 2019 02:00:00 -0800
Time.now.beginning_of_day + Rational(2, 1).hours
# => 2019-12-02 02:00:00 -0800
Here's the relevant code from Time/DateTime/ActiveSupport:
class DateTime
def since(seconds)
self + Rational(seconds, 86400)
end
def plus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
other.since(self)
else
plus_without_duration(other)
end
end
end
class Time
def since(seconds)
self + seconds
rescue
to_datetime.since(seconds)
end
def plus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
other.since(self)
else
plus_without_duration(other)
end
end
def advance(options)
unless options[:weeks].nil?
options[:weeks], partial_weeks = options[:weeks].divmod(1)
options[:days] = options.fetch(:days, 0) + 7 * partial_weeks
end
unless options[:days].nil?
options[:days], partial_days = options[:days].divmod(1)
options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
end
d = to_date.gregorian.advance(options)
time_advanced_by_date = change(year: d.year, month: d.month, day: d.day)
seconds_to_advance = \
options.fetch(:seconds, 0) +
options.fetch(:minutes, 0) * 60 +
options.fetch(:hours, 0) * 3600
if seconds_to_advance.zero?
time_advanced_by_date
else
time_advanced_by_date.since(seconds_to_advance)
end
end
end
class ActiveSupport::Duration
def since(time = ::Time.current)
sum(1, time)
end
def sum(sign, time = ::Time.current)
parts.inject(time) do |t, (type, number)|
if t.acts_like?(:time) || t.acts_like?(:date)
if type == :seconds
t.since(sign * number)
elsif type == :minutes
t.since(sign * number * 60)
elsif type == :hours
t.since(sign * number * 3600)
else
t.advance(type => sign * number)
end
else
raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
end
end
end
end
What's happening in your case is on the t.since(sign * number * 3600) line, number is BigDecimal(2), and DateTime.since does Rational(seconds, 86400). So the whole expression when using DateTime is Rational(1 * BigDecimal(2) * 3600, 86400).
Since a BigDecimal is used as an argument to Rational, the result isn't rational at all:
Rational(1 * BigDecimal(2) * 3600, 86400)
# => 0.83333333333333333e-1 # Since there's no obvious way to coerce a BigDecimal into a Rational, this returns a BigDecimal
Rational(1 * 2 * 3600, 86400)
# => (1/12) # A rational, as expected
This value makes it back to Time#advance. Here are the results of the calculations it makes:
options[:days], partial_days = options[:days].divmod(1)
# => [0.0, 0.83333333333333333e-1] # 0 days, 2 hours
options[:hours] = options.fetch(:hours, 0) + 24 * partial_days
# => 0.1999999999999999992e1 # juuuust under 2 hours.
And finally, 0.1999999999999999992e1 * 3600 = 7199.9999999999999712, which gets floored when it's finally converted back to a time/datetime.
This doesn't happen with Time, since Time doesn't ever need to pass the duration's value into a Rational.
I don't think this should be considered a bug, since if you're passing a BigDecimal then is how you should expect the code to treat your data: As a number with a decimal component, rather than as a ratio. That is, when you use BigDecimals you open yourself up to floating point errors.
It's off by one second, not milisecond. Why not use 2.hours instead of BigDecimal(2).hours?

How do I convert a string into a Time object?

I searched for my problem and got a lot of solutions, but unfortunately none satisfy my need.
My problem is, I have two or more strings, and I want to convert those strings into times, and add them:
time1 = "10min 43s"
time2 = "32min 30s"
The output will be: 43min 13s
My attempted solution is:
time1 = "10min 43s"
d1=DateTime.strptime(time1, '%M ')
# Sat, 02 Nov 2013 00:10:00 +0000
time2 = "32min 30s"
d2=DateTime.strptime(time2, '%M ')
# Sat, 02 Nov 2013 00:32:00 +0000
Then I can't progress.
There are many ways to do this. Here's another:
time1 = "10min 43s"
time2 = "32min 30s"
def get_mins_and_secs(time_str)
time_str.scan(/\d+/).map(&:to_i)
#=> [10, 43] for time_str = time1, [32, 30] for time_str = time2
end
min, sec = get_mins_and_secs(time1)
min2, sec2 = get_mins_and_secs(time2)
min += min2
sec += sec2
if sec > 59
min += 1
sec -= 60
end
puts "#{min}min #{sec}sec"
Let's consider what's happening here. Firstly, you need to extract the minutes and seconds from the time strings. I made a method to do that:
def get_mins_and_secs(time_str)
time_str.scan(/\d+/).map(&:to_i)
#=> [10, 43] for time_str = time1, [32, 30] for time_str = time2
end
For time_str = "10min 43s", we apply the String#scan method to extract the two numbers as strings:
"10min 43s".scan(/\d+/) # => ["10", "43"]
Array#map is then used to convert these two strings to integers
["10", "43"].map {|e| e.to_i} # => [10, 43]
This can be written more succinctly as
["10", "43"].map(&:to_i} # => [10, 43]
By chaining map to to scan we obtain
"10min 43s".scan(/\d+/).map(&:to_i} # => [10, 43]
The array [10, 43] is returned and received (deconstructed) by the variables min and sec:
min, sec = get_mins_and_secs(time_str)
The rest is straightforward.
Here's a simple solution assuming that the format stays the same:
time1 = "10min 43s"
time2 = "32min 30s"
strings = [time1, time2]
total_time = strings.inject(0) do |sum, entry|
minutes, seconds = entry.split(' ')
minutes = minutes.gsub("min", "").to_i.send(:minutes)
seconds = seconds.gsub("s", "").to_i.send(:seconds)
sum + minutes + seconds
end
puts "#{total_time/60}min #{total_time%60}s"
Something like the following should do the trick:
# split the string on all the integers in the string
def to_seconds(time_string)
min, sec = time_string.gsub(/\d+/).map(&:to_i)
min.minutes + sec.seconds
end
# Divide the seconds with 60 to get minutes and format the output.
def to_time_str(seconds)
minutes = seconds / 60
seconds = seconds % 60
format("%02dmin %02dsec", minutes, seconds)
end
time_in_seconds1 = to_seconds("10min 43s")
time_in_seconds2 = to_seconds("32min 30s")
to_time_str(time_in_seconds1 + time_in_seconds2)
My solution that takes any number of time strings and return the sum in the same format:
def add_times(*times)
digits = /\d+/
total_time = times.inject(0){|sum, entry|
m, s = entry.scan(digits).map(&:to_i)
sum + m*60 + s
}.divmod(60)
times.first.gsub(digits){total_time.shift}
end
p add_times("10min 43s", "32min 55s", "1min 2s") #=> "44min, 40s"
p add_times("10:43", "32:55") #=> "38:43"

Resources