cydonia
cydonia

Reputation: 1283

Convert duration to hours:minutes:seconds (or similar) in Rails 3 or Ruby

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

Upvotes: 128

Views: 105350

Answers (14)

Tristan Hill
Tristan Hill

Reputation: 181

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(':')

Upvotes: 0

Lev Lukomskyi
Lev Lukomskyi

Reputation: 6667

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"

Upvotes: 213

Luciano Ribas
Luciano Ribas

Reputation: 389

Here a simple solution using divmod and map:

    hours = 3.5456
    value = (hours*60).divmod(60).map{ |a| "%02d"%[a.floor] }.join(":")
    => "03:32"

Upvotes: 2

Sathish
Sathish

Reputation: 1475

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

 >> ActiveSupport::Duration.build(125557).inspect
 => "1 day, 10 hours, 52 minutes, and 37 seconds"

Upvotes: 13

Jon Kern
Jon Kern

Reputation: 3235

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

Upvotes: 5

Daniel
Daniel

Reputation: 7172

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.

Upvotes: 3

Cyril Duchon-Doris
Cyril Duchon-Doris

Reputation: 13929

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.

Upvotes: 5

Becca Royal-Gordon
Becca Royal-Gordon

Reputation: 17861

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

Upvotes: 14

Igor Springer
Igor Springer

Reputation: 496

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.

Upvotes: 7

IAmNaN
IAmNaN

Reputation: 10582

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.

Upvotes: 51

Sheharyar
Sheharyar

Reputation: 75740

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

Upvotes: 22

Xiao Bin
Xiao Bin

Reputation: 129

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")

Upvotes: 6

Cristobal Viedma
Cristobal Viedma

Reputation: 1020

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.

Upvotes: 27

allan
allan

Reputation: 1269

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

distance_of_time_in_words(3600)
 => "about 1 hour"

Upvotes: 94

Related Questions