I have a a Track model that has a duration attribute. The attribute is string based, and reads in minutes:seconds format. I was wondering what the best way would be to take these string-based values and add them together. For example, if there are duration values like this:
Duration 1: "1:00"
Duration 2: "1:30"
how could I get it to output "2:30"?
Most of the questions I found related to this issue start with an integer based value. What's the best way to get this done?
My suggestion is to store/manipulate them as seconds.
It's definitely easier to store them as the integer number of seconds, and apply a function to parse/format the value into the proper string representation.
Storing them as integer will make it very easy to sum and subtract them.
Here is one way this can be done:
class Track
attr_accessor :duration
def initialize(duration)
#duration = duration
end
end
arr = [Track.new("1:00"), Track.new("1:30")]
total_seconds = arr.reduce(0) do |a, i|
min, sec = i.duration.split(":").map(&:to_i)
a + min * 60 + sec
end
p total_duration = '%d:%02d' % total_seconds.divmod(60)
#=> "2:30"
Edit: I missed #Wand's earlier answer, which is the same as mine. I'll leave mine just for the way I've organized the calculations.
arr = %w| 1:30 3:07 12:53 |
#=> ["1:30", "3:07", "12:53"]
"%d:%2d" % arr.reduce(0) do |tot,str|
m,s = str.split(':')
tot + 60*m.to_i + s.to_i
end.divmod(60)
#=> "17:30"
I just had to implement something like this in a recent project. Here is a simple start. If you are sure you will always have this format 'H:S', you will not need to convert your duration to time objects:
entries = ["01:00", "1:30", "1:45"]
hours = 0
minutes = 0
entries.each do |e|
entry_hours, entry_minutes = e.split(':', 2)
hours += entry_hours.to_i
minutes += entry_minutes.to_i
end
hours += (minutes/60)
minutes = (minutes%60)
p "#{hours}:#{minutes}"
#=> "4:15"
I agree with #SimoneCarletti: store them as an integer number of seconds. However, you could wrap them in a duration value class that can output itself as a nicely formatted string.
class Duration
attr_accessor :seconds
def initialize(string)
minutes, seconds = string.split(':', 2)
#seconds = minutes.to_i * 60 + seconds.to_i
end
def to_s
sprintf("%d:%02d", #seconds / 60, #seconds % 60)
end
def self.sum(*durations)
Duration.new(durations.reduce(0) { |t,d| t + d.seconds })
end
end
EDIT: Added a sum method similar to that suggested by #CarySwoveland below. You can use this as follows:
durations = ["1:30", "2:15"].map { |str| Duration.new(str) }
total = Duration.sum *durations
Related
I'm trying to get the minutes to show two digits even when a single digit answer is assigned to the min_side variable. It's probably a simple answer but I cannot seem to get it to work.
def time_conversion(minutes)
hour = minutes/60
min_side = minutes%60
min_side = %02d
time = "#{hour}:#{min_side}"
return time
end
puts time_conversion(360)
You can use sprintf:
def time_conversion(minutes)
hour = minutes / 60
min_side = minutes % 60
sprintf("%d:%02d", hour, min_side)
end
you can use rjust to add zeros
minutes.to_s.rjust(2, '0')
You could do it like so:
def time_conversion(minutes)
"%d:%02d" % minutes.divmod(60)
end
puts time_conversion(360)
#=> "6:00"
def time_conversion(minutes)
hour = minutes/60
min_side = minutes%60
"#{hour}:#{min_side < 10 ? "0#{min_side}" : min_side}"
end
By the way, the latter part of the last line is not a comment. It's a nested string interpolation that a code formatting here cannot recognize
Low readability, in-place perl'ish way :)
time = "#{hour}:#{"0#{min_side}"[-2,2]}"
Adds zero and gets last two chars of the result.
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
I solved TestFirst.org question 09_timer for Ruby Rspec testing. My code works but I do not like it. It is very long. Any comments and/or suggestions for improving it would be greatly appreciated. Please include an explanation to clarify any suggestions. The goal was to create the Timer with an #seconds instance variable initialized to 0, and then return all values as a string with hours, minutes, seconds format: 00:00:00. So 12 seconds => 00:00:12; 66 seconds => 00:01:06; and 4000 seconds => 01:06:40.
Thank you. Code below.
class Timer
attr_accessor :seconds
def initialize
#seconds = 0
end
def padded(n)
"0#{n}"
end
def time_string
hours = #seconds/3600
h_minutes = ((#seconds%3600)/60)
minutes = #seconds/60
m_seconds = #seconds%60
second = #seconds
seconds = ""
if #seconds < 60
if second < 10
second = padded(second)
end
seconds << "00:00:#{second}"
elsif #seconds > 3600
if hours < 10
hours = padded(hours)
end
if h_minutes < 10
h_minutes = padded(h_minutes)
end
if m_seconds < 10
m_seconds = padded(m_seconds)
end
seconds << "#{hours}:#{h_minutes}:#{m_seconds}"
else
if minutes < 10
minutes = padded(minutes)
end
if m_seconds < 10
m_seconds = padded(m_seconds)
end
seconds << "00:#{minutes}:#{m_seconds}"
end
#seconds = seconds
end
end
There are several little things you can do to simplify your class, and a few large organizational changes.
1) Use String#rjust to pad the numbers:
def padded(n)
"#{n}".rjust(2, '0')
end
This lets you apply it to every number, regardless or whether or not it already has two digits. As a consequence, you can get rid of all of the single-digit checks (if h_minutes < 10, etc).
2) Get rid of everything starting from the first if statement, as none of it is necessary. Just a few lines before, you have hours = #seconds / 3600, h_minutes = ((#seconds%3600)/60), and m_seconds = #seconds%60, which are the only three values you need. Apply a simple map (for padding), and join with ":" to arrive at your final string.
3) If you want an object-oriented approach, each of your hours/minutes/seconds variables could be a method, so you end up with something more like this:
class Timer
attr_accessor :seconds
def initialize
#seconds = 0
end
def time_string
[hours, minutes, m_seconds].map(&method(:padded)).join(":")
end
def hours
seconds / 3600
end
def minutes
(seconds % 3600)/60
end
def m_seconds
(seconds % 60)
end
def padded(n)
"#{n}".rjust(2, '0')
end
end
Say it took someone 3 minutes and 45 seconds to complete a task.
I'd represent that as 3:45.
But what I need to do is, assuming I'm given 3:45, convert that to the number of seconds it took.
So when given 3:45, I want to convert that to 225.
This would need to work with Ruby 1.8.7.
You could use something like Dave suggested, or if you need more stuff, there's a duration library that does this stuff.
It would look like:
001:0> ChronicDuration.parse("3:45")
225
I'd be careful about reinventing the wheel here. While you may assume you'll have only minutes and seconds, and that you'll always have the same format, it's safer to have something more robust.
Check out chronic_duration, a gem for parsing elapsed time.
def time_to_seconds(str)
time_in = []
time_in = str.split(":").reverse
asec = 0
secs = [1, 60, 60*60, 60*60*24]
time_in.size.times do {|i|
asec += secs[i].to_i * time_in[i].to_i
end
sec
end
class String
def to_secs
split(':').reverse.zip([1, 60, 60*60]).inject(0) { |m, e| m += e[0].to_i * e[1] }
end
end
puts '3:45'.to_secs # 225
puts '1:03:45'.to_secs # 3825
pry(main)> a = "3:45".split(":")
=> ["3", "45"]
pry(main)> a[0].to_i*60 + a[1].to_i
=> 225
(Wrapped up in a method, of course.)
I have a number of seconds. Let's say 270921. How can I display that number saying it is xx days, yy hours, zz minutes, ww seconds?
It can be done pretty concisely using divmod:
t = 270921
mm, ss = t.divmod(60) #=> [4515, 21]
hh, mm = mm.divmod(60) #=> [75, 15]
dd, hh = hh.divmod(24) #=> [3, 3]
puts "%d days, %d hours, %d minutes and %d seconds" % [dd, hh, mm, ss]
#=> 3 days, 3 hours, 15 minutes and 21 seconds
You could probably DRY it further by getting creative with collect, or maybe inject, but when the core logic is three lines it may be overkill.
I was hoping there would be an easier way than using divmod, but this is the most DRY and reusable way I found to do it:
def seconds_to_units(seconds)
'%d days, %d hours, %d minutes, %d seconds' %
# the .reverse lets us put the larger units first for readability
[24,60,60].reverse.inject([seconds]) {|result, unitsize|
result[0,0] = result.shift.divmod(unitsize)
result
}
end
The method is easily adjusted by changing the format string and the first inline array (ie the [24,60,60]).
Enhanced version
class TieredUnitFormatter
# if you set this, '%d' must appear as many times as there are units
attr_accessor :format_string
def initialize(unit_names=%w(days hours minutes seconds), conversion_factors=[24, 60, 60])
#unit_names = unit_names
#factors = conversion_factors
#format_string = unit_names.map {|name| "%d #{name}" }.join(', ')
# the .reverse helps us iterate more effectively
#reversed_factors = #factors.reverse
end
# e.g. seconds
def format(smallest_unit_amount)
parts = split(smallest_unit_amount)
#format_string % parts
end
def split(smallest_unit_amount)
# go from smallest to largest unit
#reversed_factors.inject([smallest_unit_amount]) {|result, unitsize|
# Remove the most significant item (left side), convert it, then
# add the 2-element array to the left side of the result.
result[0,0] = result.shift.divmod(unitsize)
result
}
end
end
Examples:
fmt = TieredUnitFormatter.new
fmt.format(270921) # => "3 days, 3 hours, 15 minutes, 21 seconds"
fmt = TieredUnitFormatter.new(%w(minutes seconds), [60])
fmt.format(5454) # => "90 minutes, 54 seconds"
fmt.format_string = '%d:%d'
fmt.format(5454) # => "90:54"
Note that format_string won't let you change the order of the parts (it's always the most significant value to least). For finer grained control, you can use split and manipulate the values yourself.
Needed a break. Golfed this up:
s = 270921
dhms = [60,60,24].reduce([s]) { |m,o| m.unshift(m.shift.divmod(o)).flatten }
# => [3, 3, 15, 21]
Rails has an helper which converts distance of time in words.
You can look its implementation: distance_of_time_in_words
If you're using Rails, there is an easy way if you don't need the precision:
time_ago_in_words 270921.seconds.from_now
# => 3 days
You can use the simplest method I found for this problem:
def formatted_duration total_seconds
hours = total_seconds / (60 * 60)
minutes = (total_seconds / 60) % 60
seconds = total_seconds % 60
"#{ hours } h #{ minutes } m #{ seconds } s"
end
You can always adjust returned value to your needs.
2.2.2 :062 > formatted_duration 3661
=> "1 h 1 m 1 s"
I modified the answer given by #Mike to add dynamic formatting based on the size of the result
def formatted_duration(total_seconds)
dhms = [60, 60, 24].reduce([total_seconds]) { |m,o| m.unshift(m.shift.divmod(o)).flatten }
return "%d days %d hours %d minutes %d seconds" % dhms unless dhms[0].zero?
return "%d hours %d minutes %d seconds" % dhms[1..3] unless dhms[1].zero?
return "%d minutes %d seconds" % dhms[2..3] unless dhms[2].zero?
"%d seconds" % dhms[3]
end
I just start writing ruby. i guess this is only for 1.9.3
def dateBeautify(t)
cute_date=Array.new
tables=[ ["day", 24*60*60], ["hour", 60*60], ["minute", 60], ["sec", 1] ]
tables.each do |unit, value|
o = t.divmod(value)
p_unit = o[0] > 1 ? unit.pluralize : unit
cute_date.push("#{o[0]} #{unit}") unless o[0] == 0
t = o[1]
end
return cute_date.join(', ')
end
Number of days = 270921/86400 (Number of seconds in day) = 3 days this is the absolute number
seconds remaining (t) = 270921 - 3*86400 = 11721
3.to_s + Time.at(t).utc.strftime(":%H:%M:%S")
Which will produce something like 3:03:15:21
Not a direct answer to the OP but it might help someone who lands here.
I had this string
"Sorry, you cannot change team leader in the last #{freeze_period_time} of a #{competition.kind}"
freeze_period_time resolved to 5 days inside irb, but inside the string, it resolved to time in seconds eg 47200, so the string became something ugly
"Sorry, you cannot change team leader in the last 47200 of a hackathon"
To fix it, I had to use .inspect on the freeze_period_time object.
So the following made it work
"Sorry, you cannot change team leader in the last #{freeze_period_time.inspect} of a #{competition.kind}"
Which returned the correct sentence
"Sorry, you cannot change team leader in the last 5 days of a hackathon"
TLDR
You might need time.inspect - https://www.geeksforgeeks.org/ruby-time-inspect-function/