how to convert 270921sec into days + hours + minutes + sec ? (ruby)

Solution 1:

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.

Solution 2:

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.

Solution 3:

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]