Class: Slk::Support::TimeParser

Inherits:
Object
  • Object
show all
Defined in:
lib/slk/support/time_parser.rb

Overview

Resolves a single clock time ("1:30p", "2026-08-12 8a") to a Unix timestamp. A bare time at or before now rolls to tomorrow; an explicit YYYY-MM-DD date is honoured as given.

TimeRangeParser builds the two-sided form on the clock arithmetic here.

Constant Summary collapse

TIME =
/(\d{1,2})(?::(\d{2}))?\s*([ap]m?)?/i
PATTERN =
/\A(?:(\d{4}-\d{2}-\d{2})\s+)?#{TIME}\z/i
EXAMPLE =
'1:30p or 2026-08-12 8:00'
MINUTES_PER_DAY =
24 * 60
CLOCK_HOURS =

A bare hour of 0 or 13-23 can only be 24-hour notation, so no am/pm was omitted. Anything in 1..12 is genuinely ambiguous without one.

(1..12)

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(now: Time.now) ⇒ TimeParser

Returns a new instance of TimeParser.



29
30
31
# File 'lib/slk/support/time_parser.rb', line 29

def initialize(now: Time.now)
  @now = now
end

Class Method Details

.parse(input, now: Time.now) ⇒ Integer

Returns Unix timestamp.

Parameters:

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

    reference point for rolling bare times forward

Returns:

  • (Integer)

    Unix timestamp



27
# File 'lib/slk/support/time_parser.rb', line 27

def self.parse(input, now: Time.now) = new(now: now).parse(input)

Instance Method Details

#ambiguous?(parts) ⇒ Boolean

True when these parts could have meant either am or pm — no meridiem given, and an hour small enough that both readings are plausible.

Returns:

  • (Boolean)


80
# File 'lib/slk/support/time_parser.rb', line 80

def ambiguous?(parts) = parts[2].nil? && CLOCK_HOURS.cover?(parts[0].to_i)

#at(date, *parts) ⇒ Object

Raises:



53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/slk/support/time_parser.rb', line 53

def at(date, *parts)
  hours, minutes = to_24_hour(*parts)
  time = build(date, hours, minutes)
  # Ruby silently shifts a local time that DST skips (2:30a on a
  # spring-forward date becomes 3:30a), which would move the window
  # rather than fail. Reject it instead.
  return time if time.hour == hours && time.min == minutes

  raise TimeFormatError,
        format('%<clock>s does not exist on %<date>s (clocks skip forward for DST).',
               clock: format('%<h>02d:%<m>02d', h: hours, m: minutes), date: date)
end

#minutes(parts) ⇒ Object

Minutes since midnight, for comparing two times before either has a date.



73
74
75
76
# File 'lib/slk/support/time_parser.rb', line 73

def minutes(parts)
  hours, mins = to_24_hour(*parts)
  (hours * 60) + mins
end

#parse(input) ⇒ Object

Raises:



33
34
35
36
37
38
39
40
# File 'lib/slk/support/time_parser.rb', line 33

def parse(input)
  match = PATTERN.match(input.to_s.strip)
  raise TimeFormatError, "Invalid time: #{input}. Use #{EXAMPLE}" unless match

  parts = match.captures[1..3]
  date = match[1] ? parse_date(match[1]) : roll_forward(parts)
  at(date, *parts).to_i
end

#parse_date(text, example: EXAMPLE) ⇒ Object

Parameters:

  • example (String) (defaults to: EXAMPLE)

    format hint, so a caller with its own syntax (TimeRangeParser) does not advertise this class's example



47
48
49
50
51
# File 'lib/slk/support/time_parser.rb', line 47

def parse_date(text, example: EXAMPLE)
  Date.parse(text)
rescue Date::Error
  raise TimeFormatError, "Invalid date: #{text}. Use #{example}"
end

#place(date, *parts) ⇒ Object

Same placement without the existence check, for callers asking a question a nonexistent time still answers. at would raise, which for the roll-forward probe would reject "2:30a" outright on a spring-forward date instead of rolling it to the next day, where it does exist.



70
# File 'lib/slk/support/time_parser.rb', line 70

def place(date, *parts) = build(date, *to_24_hour(*parts))

#twenty_four_hour?(parts) ⇒ Boolean

The opposite: a bare hour too large to be a 12-hour clock reading, so the writer was plainly using 24-hour notation. TimeRangeParser treats one of these as settling how to read the other side of a range.

Returns:

  • (Boolean)


85
# File 'lib/slk/support/time_parser.rb', line 85

def twenty_four_hour?(parts) = parts[2].nil? && !CLOCK_HOURS.cover?(parts[0].to_i)