16

I am using Rails 4, Ruby 2.1 with PostgreSQL.

I have a database field called duration which is an interval data type.

When pulling out the data in this column it returns in the format of hh:mm:ss, e.g. 01:30:00.

I am trying to figure out a way to display this as 1 hour, 30 minutes.

Other examples:

  • 02:00:00 to 2 hours
  • 02:15:00 to 2 hours, 15 minutes
  • 02:01:00 to 2 hours, 1 minute
nik7
  • 806
  • 3
  • 12
  • 20
patrick
  • 9,837
  • 3
  • 20
  • 27
  • 1
    What have you tried to solve this yourself? Note: Stack Overflow is brimming with helpful people who are willing to help *you to write it yourself* (ie we won't do it all for you)... so you need to have a go at writing it yourself first. :) Have you googled the ruby time-methods? the ruby number-based methods? Do you have a rough idea of what algorithm you might start with? Have a bash and see what you can come up with... – Taryn East Sep 25 '14 at 01:55

7 Answers7

23

Just use duration + inspect

seconds = 86400 + 3600 + 15
ActiveSupport::Duration.build(seconds).inspect
=> "1 day, 1 hour, and 15.0 seconds"

Or a it can be a little be customized

ActiveSupport::Duration.build(seconds).parts.map do |key, value|
  [value.to_i, key].join 
end.join(' ')
=> "1days 1hours 15seconds"

P.S.

You can get seconds with

1.day.to_i
=> 86400

Time can be parsed only in ISO8601 format

ActiveSupport::Duration.parse("PT2H15M").inspect
=> "2 hours and 15 minutes"
itsnikolay
  • 17,415
  • 4
  • 65
  • 64
9

I would start with something like this:

def duration_of_interval_in_words(interval)
  hours, minutes, seconds = interval.split(':').map(&:to_i)

  [].tap do |parts|
    parts << "#{hours} hour".pluralize(hours)       unless hours.zero?
    parts << "#{minutes} minute".pluralize(minutes) unless minutes.zero?
    parts << "#{seconds} hour".pluralize(seconds)   unless seconds.zero?
  end.join(', ')
end

duration_of_interval_in_words('02:00:00')
# => '2 hours'

duration_of_interval_in_words('02:01:00')
# => '2 hours, 1 minute'

duration_of_interval_in_words('02:15:00')
# => '2 hours, 15 minutes'
spickermann
  • 100,941
  • 9
  • 101
  • 131
2

See also ActionView::Helpers::DateHelper distance_of_time_in_words (and related)

e.g.

0 <-> 29 secs # => less than a minute
30 secs <-> 1 min, 29 secs # => 1 minute
1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
... etc

https://apidock.com/rails/ActionView/Helpers/DateHelper/distance_of_time_in_words

Perhaps not appropriate to include in a model validation error? (which is my use case)

Justin Maxwell
  • 159
  • 2
  • 9
2

Here is a locale-aware helper method which builds upon MasonMc's answer.

# app/helpers/date_time_helper.rb
module DateTimeHelper
  def humanized_duration(duration)
    ActiveSupport::Duration.build(duration).parts.except(:seconds).collect do |key, val|
      t(:"datetime.distance_in_words.x_#{key}", count: val)
    end.join(', ')
  end
end

You can also replace join(', ') with to_sentence if it reads better, or get fancy and allow passing a locale, like distance_of_time_in_words.

Gotcha

Rather counter-intuitively, x_hours is absent from Rails' default locale file because distance_of_time_in_words doesn't use it. You'll need to add it yourself, even if using the rails-i18n gem.

# config/locales/en.datetime.yml
en:
  datetime:
    distance_in_words:
      x_hours:
        one: "one hour"
        other: "%{count} hours"

Here's the output:

humanized_duration(100)
# => '1 minute'
humanized_duration(12.34.hours)
# => '12 hours, 20 minutes'
humanized_duration(42.hours)
# => '1 day, 18 hours, 25 minutes'
Goulven
  • 777
  • 9
  • 20
1

You can try following method to display such as:

  • minutes_to_human(45) # 45 minutes
  • minutes_to_human(120) # 2 hours
  • minutes_to_human(75) # 2.5 hours
  • minutes_to_human(75) # 1.15 hours

def minutes_to_human(minutes)
  result = {}

  hours = minutes / 60
  result[:hours] = hours if hours.positive?
  result[:minutes] = ((minutes * 60) - (hours * 60 * 60)) / 60 if minutes % 60 != 0

  result[:minutes] /= 60.0 if result.key?(:hours) && result.key?(:minutes)

  return I18n.t('helper.minutes_to_human.hours_minutes', time: (result[:hours] + result[:minutes]).round(2)) if result.key?(:hours) && result.key?(:minutes)
  return I18n.t('helper.minutes_to_human.hours', count: result[:hours]) if result.key?(:hours)
  return I18n.t('helper.minutes_to_human.minutes', count: result[:minutes].round) if result.key?(:minutes)
  ''
end

Translations:

en:
  helper:
    minutes_to_human:
      minutes:
        zero: '%{count} minute'
        one: '%{count} minute'
        other: '%{count} minutes'
      hours:
        one: '%{count} hour'
        other: '%{count} hours'
      hours_minutes: '%{time} hours'
Saiqul Haq
  • 2,287
  • 16
  • 18
1

This worked for me:

irb(main):030:0> def humanized_duration(seconds)
irb(main):031:1>   ActiveSupport::Duration.build(seconds).parts.except(:seconds).reduce("") do |output, (key, val)|
irb(main):032:2*       output+= "#{val}#{key.to_s.first} "
irb(main):033:2>     end.strip
irb(main):034:1>   end
=> :humanized_duration
irb(main):035:0> humanized_duration(920)
=> "15m"
irb(main):036:0> humanized_duration(3920)
=> "1h 5m"
irb(main):037:0> humanized_duration(6920)
=> "1h 55m"
irb(main):038:0> humanized_duration(10800)
=> "3h"

You can change the format you want the resulting string to be inside the reduce. I like the 'h' and 'm' for hours and minutes. And I excluded the seconds from the duration parts since that wasn't important for my usage of it.

MasonMc
  • 23
  • 4
-3

I find that duration.inspect serve the purpose pretty well.

> 10.years.inspect
=> "10 years"
TerryYin
  • 174
  • 2
  • 6
  • `inspect` method should not be used into views. You should use a proper formatter. – Antoine Apr 19 '16 at 16:22
  • `10.weeks.inspect` => `"70 days"` and `2.hours.inspect` => `"7200 seconds"` – spejamchr Apr 29 '16 at 20:50
  • 1
    `10.weeks.inspect` => `"10 weeks"` and `2.hours.inspect` => `"2 hours"`. It's too bad they can't just copy this inspect to another name, because people wouldn't object to it if it were called anything else, and otherwise seems perfect. Also handles the multiple case nicely: `(1.month + 10.weeks + 2.hours).inspect` => `"1 month, 10 weeks, and 2 hours"`. I mean, I would do it without the last comma, but it's good enough. – Hakanai Feb 03 '17 at 04:43
  • Wow - this is great. Agree that it deserves to be replicated as something other than .inspect - daft to rewrite something built in that works. HOWEVER the Oxford Comma is to be respected, this denigration of it will not stand! – Justin Maxwell Aug 26 '17 at 19:55