127

I have a feeling there is a simple/built-in way to do this but I can't find it.

I have a duration (in seconds) in an integer and I want to display it in a friendly format.

e.g. 3600 would be displayed as "01:00:00" or "1 hour" or something.

I can do it with time_ago_in_words(Time.zone.now+3600) but that feels like a bit of a hack, there is no reason to add/subtract from the current time just to format this value. Is there a duration_in_words() or something?

Thanks

cydonia
  • 1,273
  • 2
  • 9
  • 4

14 Answers14

212

Summing up:

assuming that total_seconds = 3600

Option 1:

distance_of_time_in_words(total_seconds) #=> "about 1 hour"

Option 2:

Time.at(total_seconds).utc.strftime("%H:%M:%S") #=> "01:00:00"

Note: it overflows, eg. for total_seconds = 25.hours.to_i it'll return "01:00:00" also

Option 3:

seconds = total_seconds % 60
minutes = (total_seconds / 60) % 60
hours = total_seconds / (60 * 60)

format("%02d:%02d:%02d", hours, minutes, seconds) #=> "01:00:00"

Option 4:

ActiveSupport::Duration.build(total_seconds).inspect #=> "1 hour"

# OR

parts = ActiveSupport::Duration.build(total_seconds).parts
"%02d:%02d:%02d" % [parts.fetch(:hours, 0),
                    parts.fetch(:minutes, 0),
                    parts.fetch(:seconds, 0)] #=> "01:00:00"
Lev Lukomsky
  • 6,346
  • 4
  • 34
  • 24
  • 1
    The distance_of_time_in_words method is an ActionView Helper, thus needs to be called from the View (not the controller). http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html – msdundar Jun 29 '14 at 13:15
  • 14
    Note that the strftime option will overflow at 24 hours. If your duration is 25 hours, it will display `01:00:00`. – Gabe Martin-Dempesy Feb 05 '15 at 17:00
  • 3
    this does not work for negative times, for example -1800 returns -1h 30m instead of -30m – Arnold Roa Feb 07 '17 at 16:00
  • Note that option 4 only works with `Duration.build`; if you try it with `3600.seconds` you'll get `"00:00:3600"`. – David Moles Sep 08 '21 at 23:15
94

See: http://api.rubyonrails.org/classes/ActionView/Helpers/DateHelper.html

distance_of_time_in_words(3600)
 => "about 1 hour"
allan
  • 1,269
  • 9
  • 7
  • 1
    thanks i looked at that but thought i had to provide two times.. what a nub! – cydonia Nov 14 '10 at 02:13
  • 1
    yeah, those examples are daft – allan Nov 14 '10 at 02:18
  • 1
    distance_of_time_in_words(from_time, to_time, ...) – boulder_ruby Aug 12 '12 at 02:45
  • 1
    Also, if you want this to be very specific and not "round" the duration, check out Radar's gem: https://github.com/radar/distance_of_time_in_words. Drop-in replacement for `distance_of_time_in_words` and you can get the rounded number by passing `vague: true` as an option. – Joshua Pinter May 30 '19 at 14:43
51

Ruby's string % operator is too unappreciated and oft forgotten.

"%02d:%02d:%02d:%02d" % [t/86400, t/3600%24, t/60%60, t%60]

Given t is a duration in seconds, this emits a zero-padded colon-separated string including days. Example:

t = 123456
"%02d:%02d:%02d:%02d" % [t/86400, t/3600%24, t/60%60, t%60]
=> "01:10:17:36"

Lovely.

IAmNaN
  • 10,305
  • 3
  • 53
  • 51
  • 29
    That's an odd definition of "lovely". – Marc Bollinger May 12 '15 at 22:59
  • 12
    The `%` says, "Take the array of parameters from my right and insert them into the format string on my left." In that respect, it is similar to `printf` in C\C++ only more concise and can be used in an assignment. Yes, I call that lovely. And powerful. There are examples that would demonstrate this eloquence better, but they wouldn't answer the question. Your snark is not appreciated, btw. – IAmNaN May 13 '15 at 17:54
27

I guess you could do also something like:

(Time.mktime(0)+3600).strftime("%H:%M:%S")

To format it as you wish.

BTW, originally I thought of using Time.at() but seems that EPOCH time on my Ubuntu is Thu Jan 01 01:00:00 +0100 1970 and not 00:00:00 hours as I expected, and therefore if I do:

Time.at(3600).strftime("%H:%M:%S")

Gives me 1 hour more than wanted.

Cristobal Viedma
  • 990
  • 1
  • 8
  • 20
22

I use this to show time durations in my Rails Project:

  1. Add a custom method to the Integer class. You can create a new file called pretty_duration.rb in the initializers folder:

    class Integer
        def pretty_duration
            parse_string = 
                if self < 3600
                    '%M:%S'
                else
                    '%H:%M:%S'
                end
    
            Time.at(self).utc.strftime(parse_string)
        end
    end
    
  2. Call seconds.pretty_duration anywhere in your project:

    275.pretty_duration     # => "04:35"
    9823.pretty_duration    # => "02:43:43"
    

This answer builds up on Lev Lukomsky's Code

Community
  • 1
  • 1
Sheharyar
  • 73,588
  • 21
  • 168
  • 215
14

This one uses the obscure divmod method to divide and modulo at the same time, so it handles Float seconds properly:

def duration(seconds)
  minutes, seconds = seconds.divmod(60)
  hours, minutes = minutes.divmod(60)
  days, hours = hours.divmod(24)

  "#{days.to_s.rjust(3)}d #{hours.to_s.rjust(2)}h #{minutes.to_s.rjust(2)}m #{seconds}s"
end
Becca Royal-Gordon
  • 17,541
  • 7
  • 56
  • 91
12

ActiveSupport::Duration.build + inspect gives you valid results

 >> ActiveSupport::Duration.build(125557).inspect
 => "1 day, 10 hours, 52 minutes, and 37 seconds"
Sathish
  • 1,245
  • 2
  • 15
  • 22
  • elegant! works well for me. result for value `75`: `1 minute and 15 seconds ` – Yshmarov Feb 04 '21 at 17:36
  • It can even be used to extract out individual parts to build a custom string. e.g. `ActiveSupport::Duration.build(125557).parts` will give `{ days: 1, hours: 10, minutes: 52, seconds: 37 }`. – Waseem Aug 25 '21 at 15:20
7

Using Time.utc.strftime works only for values when total number of hours is less then 24:

2.2.2 :004 > Time.at(60 * 60).utc.strftime('%H h %M m')
=> "01 h 00 m"

For greater values it returns incorrect results:

2.2.2 :006 > Time.at(60 * 60 * 24).utc.strftime('%H h %M m')
 => "00 h 00 m"

I suggest using 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.

Igor Springer
  • 498
  • 5
  • 7
6

Be careful with the duration longer than one day.

(timing/3600).to_i.to_s.rjust(2,'0') + ":"+Time.at(timing).utc.strftime("%M:%S")
Xiao Bin
  • 129
  • 1
  • 5
5

An answer inspired from Lev Lukomsky's one taking advantage of ActiveSupport::Duration, and handling milliseconds (useful to benchmark code)

# duration in ms modulus number of ms in one second
milliseconds = duration.in_milliseconds % 1.second.in_milliseconds

# duration in seconds modulus number of seconds in one minute
seconds = (duration / 1.second) % (1.minute / 1.second)

# duration in minutes modulus number of minutes in one hour
minutes = (duration / 1.minute) % (1.hour / 1.minute)

# duration in hours modulus number of hours in one day
hours = (duration / 1.hour) % (1.day / 1.hour)

format("%02d:%02d:%02d:%03d", hours, minutes, seconds, milliseconds) #=> "12:05:00:001"

Of course you can extend this easily with days, months, years, etc using related ActiveSupport methods and repeating the same structure.

Keep in mind that for too long durations, this may be inaccurate since the duration of 1 month is not fixed in number of days, and I'm not sure how AS:Duration deals with that.

Cyril Duchon-Doris
  • 12,964
  • 9
  • 77
  • 164
5

Shout out to @joshuapinter who gave the best answer (in the form of a comment).

Use the drop-in replacement dotiw gem to gain more control over the accuracy of the output to suit different needs:

https://github.com/radar/distance_of_time_in_words

Sample view code:

%label
  Logoff after:
  - expire_in = distance_of_time_in_words(Time.now, Time.now + user.custom_timeout.minutes, :only => [:minutes, :hours, :days])
  = expire_in

Resulting in something like this:

Logoff after: 1 day, 13 hours, and 20 minutes
Joshua Pinter
  • 45,245
  • 23
  • 243
  • 245
Jon Kern
  • 3,186
  • 32
  • 34
  • Note that you can also use `dotiw` to generate other formats, e.g.: `DOTIW::TimeHash.new(1.days + 2.hours + 3.minutes + 4.seconds, nil).to_hash.each_value.map { |v| format('%02d', v) }.join(':')` produces `01:02:03:04`. (This particular format gets a bit unreadable beyond days, though.) – David Moles Sep 08 '21 at 23:24
3

Just to throw in my 2 cents:

Time.at(i).utc.strftime((i < 3600) ? '%-M minutes and %-S seconds' : '%-H hours, %-M minutes, and %-S seconds')

Built off of Xiao Bin's answer.

Daniel
  • 7,006
  • 7
  • 43
  • 49
2

Here a simple solution using divmod and map:

    hours = 3.5456
    value = (hours*60).divmod(60).map{ |a| "%02d"%[a.floor] }.join(":")
    => "03:32"
Luciano Ribas
  • 309
  • 2
  • 4
0

Similar to option 4 in this answer but deals with duration going over a day

duration = ActiveSupport::Duration.build(total_seconds)
%i(hours minutes seconds).map { |part| "%02d" % (duration.parts.fetch(part, 0) + (part == :hours ? duration.in_days.to_i * 24 : 0)) }.join(':')
Tristan Hill
  • 171
  • 1
  • 3