Module: Heatwave::Duration

Defined in:
app/lib/heatwave/duration.rb

Overview

Human-readable duration formatting and parsing.

Vendored, modernized port of the chronic_duration gem (MIT licensed).
The gem's published RubyGems build (0.10.6) was 12 years stale, so it
was dropped in favour of this in-repo module. The natural-language
number support (the numerizer dependency — "two hours" → "2 hours")
is intentionally not ported: Duration.parse still handles digit, colon
(3:41:59) and unit-suffixed (1h 30m) forms, which is everything
Heatwave feeds it.

Examples:

Format a second count

Heatwave::Duration.humanize(3_900)                  # => "1 hr 5 min"
Heatwave::Duration.humanize(3_900, format: :short)  # => "1h 5m"

Parse an elapsed-time string

Heatwave::Duration.parse("3:41:59")  # => 13319

Constant Summary collapse

HOURS_PER_DAY =

Hours in one day, for rolling hours up into days.

24
DAYS_PER_WEEK =

Days in one week, for rolling days up into weeks.

7
SECONDS_PER =

Seconds in one of each named unit. Months are a flat 30 days and
years 365.25 days, matching the original gem.

{
  seconds: 1,
  minutes: 60,
  hours: 3600,
  days: 3600 * HOURS_PER_DAY,
  weeks: 3600 * HOURS_PER_DAY * DAYS_PER_WEEK,
  months: 3600 * HOURS_PER_DAY * 30,
  years: 31_557_600
}.freeze
UNIT_ORDER =

Units from largest to smallest — the order they appear in output.

%i[years months weeks days hours minutes seconds].freeze
FORMATS =

Per-format unit suffixes. pluralize appends an "s" to any unit
whose count is not exactly 1; joiner overrides the default space.

{
  micro:   { suffixes: { years: 'y', months: 'mo', weeks: 'w', days: 'd', hours: 'h', minutes: 'm', seconds: 's' }, joiner: '' },
  short:   { suffixes: { years: 'y', months: 'mo', weeks: 'w', days: 'd', hours: 'h', minutes: 'm', seconds: 's' } },
  default: { suffixes: { years: ' yr', months: ' mo', weeks: ' wk', days: ' day', hours: ' hr', minutes: ' min', seconds: ' sec' }, pluralize: true },
  long:    { suffixes: { years: ' year', months: ' month', weeks: ' week', days: ' day', hours: ' hour', minutes: ' minute', seconds: ' second' }, pluralize: true }
}.freeze
MAPPINGS =

Maps every recognized word/abbreviation to its canonical unit name.

{
  'seconds' => 'seconds', 'second' => 'seconds', 'secs' => 'seconds', 'sec' => 'seconds', 's' => 'seconds',
  'minutes' => 'minutes', 'minute' => 'minutes', 'mins' => 'minutes', 'min' => 'minutes', 'm' => 'minutes',
  'hours' => 'hours', 'hour' => 'hours', 'hrs' => 'hours', 'hr' => 'hours', 'h' => 'hours',
  'days' => 'days', 'day' => 'days', 'dy' => 'days', 'd' => 'days',
  'weeks' => 'weeks', 'week' => 'weeks', 'wks' => 'weeks', 'wk' => 'weeks', 'w' => 'weeks',
  'months' => 'months', 'month' => 'months', 'mos' => 'months', 'mo' => 'months',
  'years' => 'years', 'year' => 'years', 'yrs' => 'years', 'yr' => 'years', 'y' => 'years'
}.freeze
FLOAT_MATCHER =

Matches an integer or decimal number token within a parsed string.

/[0-9]*\.?[0-9]+/

Class Method Summary collapse

Class Method Details

.humanize(seconds, format: :default, units: nil, limit_to_hours: false, joiner: nil, weeks: false, keep_zero: false) ⇒ String?

Formats a number of seconds into a readable elapsed-time string.

Parameters:

  • seconds (Integer, Float, nil)

    elapsed seconds

  • format (Symbol) (defaults to: :default)

    one of :default, :short, :long, :micro

  • units (Integer, nil) (defaults to: nil)

    keep only the N largest non-zero units

  • limit_to_hours (Boolean) (defaults to: false)

    never roll hours up into days/months

  • joiner (String, nil) (defaults to: nil)

    string between units (defaults per format)

  • weeks (Boolean) (defaults to: false)

    allow a "weeks" unit in the breakdown

  • keep_zero (Boolean) (defaults to: false)

    emit a "0 sec" component instead of nil

Returns:

  • (String, nil)

    formatted duration, or nil when it is empty



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'app/lib/heatwave/duration.rb', line 77

def humanize(seconds, format: :default, units: nil, limit_to_hours: false,
             joiner: nil, weeks: false, keep_zero: false)
  return nil if seconds.nil?

  int = seconds.to_i
  seconds = int if (seconds - int).zero? # collapse a trailing ".0"
  decimal_places = seconds.is_a?(Float) ? seconds.to_s.split('.').last.length : nil

  counts = split_units(seconds, limit_to_hours: limit_to_hours, weeks: weeks)
  spec = FORMATS.fetch(format, FORMATS[:default])
  joiner ||= spec[:joiner] || ' '

  parts = UNIT_ORDER.filter_map do |unit|
    next if unit == :weeks && !weeks

    count = counts[unit]
    count = "%.#{decimal_places}f" % count if decimal_places && unit == :seconds && count.is_a?(Float)
    component(count, spec[:suffixes][unit], spec[:pluralize], keep_zero && unit == :seconds)
  end

  parts = parts.first(units) if units
  result = parts.join(joiner)
  result.empty? ? nil : result
end

.parse(string, keep_zero: false, default_unit: 'seconds') ⇒ Integer, ...

Parses a string representation of elapsed time into seconds.

Accepts digit + unit forms ("1h 30m", "45 minutes"), colon forms
("3:41:59" → hⓂ️s) and bare numbers (interpreted as default_unit).

Parameters:

  • string (String)

    elapsed-time text

  • keep_zero (Boolean) (defaults to: false)

    return 0 instead of nil for an empty result

  • default_unit (String) (defaults to: 'seconds')

    unit assumed for a trailing bare number

Returns:

  • (Integer, Float, nil)

    elapsed seconds



111
112
113
114
# File 'app/lib/heatwave/duration.rb', line 111

def parse(string, keep_zero: false, default_unit: 'seconds')
  result = sum_words(normalize(string.to_s), default_unit: default_unit)
  result.zero? && !keep_zero ? nil : result
end