Firstly, I would recommend against polluting the core Ruby class Time
. The needed methods could be housed in a custom class or possibly defined at the level of main
(as I have done below).
Nor would I make use of the classes Time
or DateTime
. It's easier to just deal with strings. Begin by constructing a regular expression that will be used to determine if each time string is valid.
VALID_TIME = /
\A # match beginning of string
(?: # begin non-capture of group
0?\d # optionally match a zero followed by a digit
| # or
1\d # match 1 followed by a digit
| # or
2[0-3] # match 2 followed by a digit 0, 1, 2 or 3
) # end non-capture group
(?: # begin a non-capture group
: # match a colon
[0-5] # match a digit 0, 1, 2, 3, 4 or 5
\d # match a digit
) # end non-capture group
{2} # execute the previous non-capture group twice
/x # free-spacing regex-definition mode
Writing regular expressions in free-spacing mode has the advantage that it makes them self-documenting. This regular expression is conventionally written as follows:
/\A(?:0?\d|1\d|2[0-3])(?::[0-5]\d){2}/
Our main method might be total_times
, to which is passed one or more strings representing twenty-four hour times.
def total_times(*time_strings)
seconds = tot_seconds(*time_strings)
days, hrs = seconds.divmod(24*3600)
hrs, mins = hrs.divmod(3600)
mins, secs = mins.divmod(60)
[days, hrs, mins, secs]
end
This first calls a method tot_seconds
, having the same arguments. That method returns the sum of the number of seconds since midnight for each of the times it is passed. Once the total number of seconds is obtained, Integer#divmod is used repeatedly to compute the equivalent numbers of days, hour, minutes and seconds.
def tot_seconds(*time_strings)
time_strings.sum { |time_str| time_str_to_seconds(time_str) }
end
This method passes each time string to the method time_str_to_seconds
, which returns the number of seconds since midnight for that time string.
def time_str_to_seconds(time_str)
raise ArgumentError, "'#{time_str}' is an invalid 24-hour time value" unless
time_str.match?(VALID_TIME)
[3600, 60, 1].zip(time_str.split(':')).
reduce(0) { |tot,(n,s)| tot + n*s.to_i }
end
This method first checks the validity of the time string. If it is found to be invalid an exception is raised. This seemed like the best place to put the validity check. If the string is found to be valid it is split into strings representing hours, minutes and seconds, which are then converted to integers and combined to obtain number of seconds.
Suppose:
time_str = "9:35:08"
Then the calculations in time_str_to_seconds
are as follows:
time_str.match?(VALID_TIME)
#=> true
a = time_str.split(':')
#=> ["9", "35", "08"]
b = [3600, 60, 1].zip(a)
#=> [[3600, "9"], [60, "35"], [1, "08"]]
b.reduce(0) { |tot,(n,s)| tot + n*s.to_i }
#=> 3600*9 + 60*35 + 1*8 => 34508
See String#match? and Array#zip.
Let's now try this with an example.
arr = total_times "2:33:41", "23:46:08"
#=> [1, 2, 19, 49]
We might use these results as follows:
puts "%d day & %d:%2d:%2d" % arr
"1 day & 2:19:49"
In some cases it might be preferable to write:
days, hrs, mins, secs = total_times "2:33:41", "23:46:08"
We can easily confirm these results.
tot_secs = 2*3600 + 33*60 + 41 +
23*3600 + 46*60 + 8
#=> 94789
days, hrs = tot_secs.divmod(24*3600)
#=> [1, 8389]
hrs, mins = hrs.divmod(3600)
#=> [2, 1189]
mins, secs = mins.divmod(60)
#=> [19, 49]
[days, hrs, mins, secs]
#=> [1, 2, 19, 49]
Two more examples:
total_times "11:23:07", "22:53:45", "0:23:23", "23:45:56"
#=> [2, 10, 26, 11]
total_times "24:01:10", "10:30:50"
#=> ArgumentError ('24:01:10' is an invalid 24-hour time value)
Note that it is not necessary to use a regular expression to determine the validity of a time string. One could write:
time_str = "9:35:08"
hr, min, sec = time_str.split(':').map(&:to_i)
#=> [9, 35, 8]
(0..23).cover?(hr) && (0..59).cover?(min) && (0..59).cover?(sec)
#=> true