Module: Philiprehberger::TimeAgo

Defined in:
lib/philiprehberger/time_ago.rb,
lib/philiprehberger/time_ago/version.rb

Defined Under Namespace

Classes: Error

Constant Summary collapse

SECONDS_PER_MINUTE =
60
SECONDS_PER_HOUR =
3600
SECONDS_PER_DAY =
86_400
SECONDS_PER_WEEK =
604_800
SECONDS_PER_MONTH =
2_592_000
SECONDS_PER_YEAR =
31_536_000
UNITS =
{
  year: SECONDS_PER_YEAR,
  month: SECONDS_PER_MONTH,
  week: SECONDS_PER_WEEK,
  day: SECONDS_PER_DAY,
  hour: SECONDS_PER_HOUR,
  minute: SECONDS_PER_MINUTE,
  second: 1
}.freeze
SHORT_LABELS =
{
  year: 'y',
  month: 'mo',
  week: 'w',
  day: 'd',
  hour: 'h',
  minute: 'm',
  second: 's'
}.freeze
PRECISION_ORDER =
%i[year month week day hour minute second].freeze
DEFAULT_CONFIG =
{
  just_now: 30
}.freeze
VERSION =
'0.6.0'

Class Method Summary collapse

Class Method Details

.auto(time, threshold: 86_400, format: '%b %d, %Y', relative_to: Time.now) ⇒ String

Return relative time if within threshold, otherwise formatted absolute date

Parameters:

  • time (Time)

    the timestamp

  • threshold (Integer) (defaults to: 86_400)

    seconds threshold for relative display (default 86400)

  • format (String) (defaults to: '%b %d, %Y')

    strftime format for absolute date (default ‘%b %d, %Y’)

  • relative_to (Time) (defaults to: Time.now)

    reference time (default: Time.now)

Returns:

  • (String)

    relative or absolute time string

Raises:



157
158
159
160
161
162
163
164
165
166
167
# File 'lib/philiprehberger/time_ago.rb', line 157

def self.auto(time, threshold: 86_400, format: '%b %d, %Y', relative_to: Time.now)
  raise Error, 'Expected a Time object' unless time.is_a?(Time)

  diff = (relative_to - time).abs

  if diff < threshold
    self.format(time, relative_to: relative_to)
  else
    time.strftime(format)
  end
end

.configHash

Return the current configuration

Returns:

  • (Hash)

    current configuration



61
62
63
# File 'lib/philiprehberger/time_ago.rb', line 61

def self.config
  @config.dup
end

.configure(**options) ⇒ Hash

Configure module-level thresholds

Parameters:

  • options (Hash)

    configuration options

Options Hash (**options):

  • :just_now (Integer)

    seconds threshold for “just now” (default 30)

Returns:

  • (Hash)

    current configuration



49
50
51
52
53
54
55
56
# File 'lib/philiprehberger/time_ago.rb', line 49

def self.configure(**options)
  options.each do |key, value|
    raise Error, "Unknown config option: #{key}" unless DEFAULT_CONFIG.key?(key)

    @config[key] = value
  end
  @config.dup
end

.duration_between(time1, time2) ⇒ Hash

Return a structured hash of time components between two times

Parameters:

  • time1 (Time)

    first timestamp

  • time2 (Time)

    second timestamp

Returns:

  • (Hash)

    { days:, hours:, minutes:, seconds: }

Raises:



192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/philiprehberger/time_ago.rb', line 192

def self.duration_between(time1, time2)
  raise Error, 'Expected Time objects' unless time1.is_a?(Time) && time2.is_a?(Time)

  total = (time2 - time1).abs.to_i
  days = total / SECONDS_PER_DAY
  remaining = total % SECONDS_PER_DAY
  hours = remaining / SECONDS_PER_HOUR
  remaining %= SECONDS_PER_HOUR
  minutes = remaining / SECONDS_PER_MINUTE
  seconds = remaining % SECONDS_PER_MINUTE

  { days: days, hours: hours, minutes: minutes, seconds: seconds }
end

.format(time, style: :long, relative_to: Time.now, max_days: nil, precision: nil, max_units: nil, compound: false, approximate: false) ⇒ String

Format a time as a human-readable relative string

Parameters:

  • time (Time)

    the timestamp to format

  • style (Symbol) (defaults to: :long)

    :long (default) or :short

  • relative_to (Time) (defaults to: Time.now)

    reference time (default: Time.now)

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

    fallback to absolute date after this many days

  • precision (Symbol, nil) (defaults to: nil)

    smallest unit to show (:year, :month, :week, :day, :hour, :minute, :second)

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

    maximum number of time components to show

  • compound (Boolean) (defaults to: false)

    show two units (e.g., “1 hour and 30 minutes ago”)

  • approximate (Boolean) (defaults to: false)

    prefix with “about” (e.g., “about 2 hours ago”)

Returns:

  • (String)

    relative time string

Raises:

  • (Error)

    if time is not a Time object



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/philiprehberger/time_ago.rb', line 84

def self.format(time, style: :long, relative_to: Time.now, max_days: nil, precision: nil, max_units: nil,
                compound: false, approximate: false)
  raise Error, 'Expected a Time object' unless time.is_a?(Time)
  raise Error, 'Expected relative_to to be a Time object' unless relative_to.is_a?(Time)

  diff = relative_to - time
  absolute_diff = diff.abs
  past = diff.positive?

  return time.strftime('%b %-d, %Y') if max_days && absolute_diff >= max_days * SECONDS_PER_DAY

  effective_max_units = compound ? 2 : max_units

  result = case style
           when :long  then format_long(absolute_diff, past, precision: precision, max_units: effective_max_units, compound: compound)
           when :short then format_short(absolute_diff, past, precision: precision, max_units: effective_max_units)
           else raise Error, "Unknown style: #{style}"
           end

  approximate ? add_approximate(result) : result
end

.format_duration(seconds, style: :long, precision: nil, max_units: 2, compound: true, approximate: false) ⇒ String

Format a raw number of seconds as a human-readable duration string

Unlike ‘format`, this method produces a directionless duration (no “ago” / “in”). Unlike `in_words`, it supports all formatting options (style, precision, etc.).

Parameters:

  • seconds (Numeric)

    number of seconds to format

  • style (Symbol) (defaults to: :long)

    :long (default) or :short

  • precision (Symbol, nil) (defaults to: nil)

    smallest unit to show (:year, :month, :week, :day, :hour, :minute, :second)

  • max_units (Integer) (defaults to: 2)

    maximum number of time components to show (default 2)

  • compound (Boolean) (defaults to: true)

    join units with “and” (default true)

  • approximate (Boolean) (defaults to: false)

    prefix with “about” (default false)

Returns:

  • (String)

    duration string (e.g., “1 hour and 30 minutes”)

Raises:

  • (Error)

    if seconds is not Numeric



305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
# File 'lib/philiprehberger/time_ago.rb', line 305

def self.format_duration(seconds, style: :long, precision: nil, max_units: 2, compound: true, approximate: false)
  raise Error, 'Expected a Numeric value' unless seconds.is_a?(Numeric)

  absolute = seconds.abs

  if absolute < @config[:just_now]
    count = absolute.floor
    result = case style
             when :long  then count == 1 ? '1 second' : "#{count} seconds"
             when :short then "#{count}s"
             else raise Error, "Unknown style: #{style}"
             end
    return approximate ? "about #{result}" : result
  end

  parts = decompose(absolute, precision: precision, max_units: max_units)

  result = case style
           when :long  then format_duration_long(parts, compound: compound)
           when :short then parts.map { |unit, count| "#{count}#{SHORT_LABELS[unit]}" }.join(' ')
           else raise Error, "Unknown style: #{style}"
           end

  approximate ? "about #{result}" : result
end

.future?(time, relative_to: Time.now) ⇒ Boolean

True when ‘time` is strictly after `relative_to`.

Parameters:

  • time (Time, DateTime, Integer)

    timestamp (Integer is epoch seconds)

  • relative_to (Time) (defaults to: Time.now)

    reference time, defaults to now

Returns:

  • (Boolean)


174
175
176
# File 'lib/philiprehberger/time_ago.rb', line 174

def self.future?(time, relative_to: Time.now)
  coerce_time(time) > relative_to
end

.in_words(seconds) ⇒ String

Format a raw number of seconds as duration words without “ago”/“from now”

Parameters:

  • seconds (Numeric)

    number of seconds to format

  • compound (Boolean)

    show two units (default false)

Returns:

  • (String)

    duration string (e.g., “5 minutes”, “2 hours and 30 minutes”)

Raises:



130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/philiprehberger/time_ago.rb', line 130

def self.in_words(seconds)
  raise Error, 'Expected a Numeric value' unless seconds.is_a?(Numeric)

  absolute = seconds.abs

  if absolute < @config[:just_now]
    count = absolute.floor
    return count == 1 ? '1 second' : "#{count} seconds"
  end

  parts = decompose(absolute, precision: nil, max_units: 2)
  if parts.length > 1
    labels = parts.map { |unit, count| "#{count} #{count == 1 ? unit : "#{unit}s"}" }
    "#{labels[0]} and #{labels[1]}"
  else
    unit, count = parts.first
    "#{count} #{count == 1 ? unit : "#{unit}s"}"
  end
end

.past?(time, relative_to: Time.now) ⇒ Boolean

True when ‘time` is strictly before `relative_to`.

Parameters:

  • time (Time, DateTime, Integer)

    timestamp (Integer is epoch seconds)

  • relative_to (Time) (defaults to: Time.now)

    reference time, defaults to now

Returns:

  • (Boolean)


183
184
185
# File 'lib/philiprehberger/time_ago.rb', line 183

def self.past?(time, relative_to: Time.now)
  coerce_time(time) < relative_to
end

.reset_config!Hash

Reset configuration to defaults

Returns:

  • (Hash)

    default configuration



68
69
70
# File 'lib/philiprehberger/time_ago.rb', line 68

def self.reset_config!
  @config = DEFAULT_CONFIG.dup
end

.until(time, relative_to: Time.now) ⇒ String

Format a future time as a human-readable relative string (e.g., “in 3 minutes”)

This is the inverse of ‘format` for past times. While `format` with a past timestamp returns “3 minutes ago”, `until` with a future timestamp returns “in 3 minutes”.

Parameters:

  • time (Time)

    the future timestamp to format

  • relative_to (Time) (defaults to: Time.now)

    reference time (default: Time.now)

Returns:

  • (String)

    relative future time string (e.g., “in 2 hours”)

Raises:

  • (Error)

    if time is not a Time object

  • (Error)

    if time is not in the future relative to relative_to



117
118
119
120
121
122
123
# File 'lib/philiprehberger/time_ago.rb', line 117

def self.until(time, relative_to: Time.now)
  raise Error, 'Expected a Time object' unless time.is_a?(Time)
  raise Error, 'Expected relative_to to be a Time object' unless relative_to.is_a?(Time)
  raise Error, 'Expected a future time' unless time > relative_to

  format(time, relative_to: relative_to)
end