Class: AIXM::Schedule::Time

Inherits:
Object show all
Extended by:
Forwardable
Includes:
Concerns::HashEquality
Defined in:
lib/aixm/schedule/time.rb

Overview

Note:

The DATELESS_DATE is used to mark the date of the internal Time“ object irrelevant. However, Ruby does not persist end of days as 24:00, therefore {DATELESS_DATE} 1 marks this case.

Times suitable for schedules

This class implements the bare minimum of stdlib Time and adds some extensions:

  • converts to UTC

  • date, seconds and milliseconds are ignored

  • #covered_by? to check whether schedule time falls within range of times

Shortcuts:

  • AIXM::BEGINNING_OF_DAY - midnight expressed as “00:00”

  • AIXM::END_OF_DAY - midnight expressed as “24:00”

Examples:

time = AIXM.time('21:30')                                  # => 21:30
time.covered_by?(AIXM.time('20:00')..AIXM.time('02:00'))   # => true

Constant Summary collapse

EVENTS =
{ sunrise: :up, sunset: :down }.freeze
PRECEDENCES =
{ first: :min, last: :max }.freeze
DATELESS_DATE =
::Date.parse('0001-01-01').freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Concerns::HashEquality

#eql?, #hash

Constructor Details

#initialize(time_or_event, or: nil, plus: 0, minus: 0, whichever_comes: :first) ⇒ Time

Note:

Unlike its twin from the stdlib, this class differs between AIXM.time(‘00:00’) (beginning of day) and AIXM.time(‘24:00’) (end of day).

Parse the given representation of time.

Examples:

AIXM.time('08:00')
AIXM.time(:sunrise)
AIXM.time(:sunrise, plus: 30)
AIXM.time('08:00', or: :sunrise)
AIXM.time('08:00', or: :sunrise, plus: 30)
AIXM.time('08:00', or: :sunrise, minus: 15)
AIXM.time('08:00', or: :sunrise, whichever_comes: :last)

Parameters:

  • time_or_event (Time, DateTime, String, Symbol)

    either time as stdlib Time or DateTime, “HH:MM” (implicitly UTC), “HH:MM [+-]00:00”, “HH:MM UTC” or any key from EVENTS

  • or (Symbol) (defaults to: nil)

    alternative event, any key from EVENTS

  • plus (Integer) (defaults to: 0)

    minutes added to event

  • minus (Integer) (defaults to: 0)

    minutes subtracted from event

  • whichever_comes (Symbol) (defaults to: :first)

    any key from PRECEDENCES



74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/aixm/schedule/time.rb', line 74

def initialize(time_or_event, or: nil, plus: 0, minus: 0, whichever_comes: :first)
  alternative_event = binding.local_variable_get(:or)   # necessary since "or" is a keyword
  @time = @event = @precedence = nil
  case time_or_event
  when Symbol
    self.event = time_or_event
  when ::Time, ::DateTime
    time_or_event = time_or_event.to_time
    set_time(time_or_event.hour, time_or_event.min, time_or_event.utc_offset)
  when /\A(\d{2}):?(\d{2}) ?([+-]\d{2}:?\d{2}|UTC)?\z/
    set_time($1, $2, $3)
  else
    fail(ArgumentError, "time or event not recognized")
  end
  fail(ArgumentError, "only one event allowed") if event && alternative_event
  self.event ||= alternative_event
  @delta = event ? plus - minus : 0
  if @time && event
    self.precedence = whichever_comes
    fail(ArgumentError, "mandatory precedence missing") unless precedence
  end
end

Instance Attribute Details

#deltaInteger? (readonly)

Minutes added or subtracted from event

Returns:

  • (Integer, nil)


45
46
47
# File 'lib/aixm/schedule/time.rb', line 45

def delta
  @delta
end

#eventSymbol?

Event or alternative to time

Returns:

  • (Symbol, nil)

    any key from EVENTS



40
41
42
# File 'lib/aixm/schedule/time.rb', line 40

def event
  @event
end

#precedenceSymbol?

Precedence of time vs. event

Returns:



50
51
52
# File 'lib/aixm/schedule/time.rb', line 50

def precedence
  @precedence
end

#timeObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



35
36
37
# File 'lib/aixm/schedule/time.rb', line 35

def time
  @time
end

Instance Method Details

#==(other) ⇒ Boolean

Whether two times are equal.

Returns:

  • (Boolean)


215
216
217
# File 'lib/aixm/schedule/time.rb', line 215

def ==(other)
  to_s == other.to_s
end

#at(hour: nil, min: nil, wrap: false) ⇒ AIXM::Schedule::Date

Creates a new time with the given parts altered.

Examples:

time = AIXM.time('22:12')
time.at(min: 0)               # => 22:00
time.at(min: 0 wrap: true)   # => 2021-01-22 (year incremented)

Parameters:

  • hour (Integer) (defaults to: nil)

    new hour

  • min (Integer) (defaults to: nil)

    new minutes

  • wrap (Boolean) (defaults to: false)

    whether to increment hour when crossing minutes boundary

Returns:



134
135
136
137
138
139
140
141
# File 'lib/aixm/schedule/time.rb', line 134

def at(hour: nil, min: nil, wrap: false)
  return self unless hour || min
  min ||= time.min
  hour ||= time.hour
  hour = hour + 1 if wrap && min < time.min
  hour = hour % 24 unless min.zero?
  self.class.new("%02d:%02d" % [hour, min])
end

#covered_by?(other) ⇒ Boolean

Whether this schedule time falls within the given range of schedule times.

Parameters:

Returns:

  • (Boolean)

Raises:

  • RuntimeError if either self is or the range contains an unsortable time with event



234
235
236
237
238
239
240
241
242
243
244
# File 'lib/aixm/schedule/time.rb', line 234

def covered_by?(other)
  range = Range.from(other)
  case
  when !sortable? || !range.first.sortable? || !range.last.sortable?
    fail "includes unsortables"
  when range.min
    range.first.to_s <= self.to_s && self.to_s <= range.last.to_s
  else
    range.first.to_s <= self.to_s || self.to_s <= range.last.to_s
  end
end

#hourInteger

Hour from 0 (beginning of day) to 24 (end of day)

Returns:

  • (Integer)


204
205
206
# File 'lib/aixm/schedule/time.rb', line 204

def hour
  @time.hour + (end_of_day? ? 24 : 0)
end

#inspectObject



118
119
120
# File 'lib/aixm/schedule/time.rb', line 118

def inspect
  %Q(#<#{self.class} #{to_s}>)
end

#minInteger

Returns:

  • (Integer)


210
# File 'lib/aixm/schedule/time.rb', line 210

def_delegators :@time, :min

#resolve(on:, xy:, round: nil) ⇒ AIXM::Schedule::Time, self

Resolve event to simple time

  • If self doesn’t have any event, self is returned.

  • Otherwise a new time is created with the event resolved for the given date and geographical location.

Examples:

time = AIXM.time('21:00', or: :sunset, minus: 30, whichever_cones: first)
time.resolve(on: AIXM.date('2000-08-01'), at: AIXM.xy(lat: 48.8584, long: 2.2945))
# => 20:50

Parameters:

  • on (AIXM::Date)

    defaults to today

  • xy (AIXM::XY)
  • round (Integer, nil) (defaults to: nil)

    round up (sunrise) or down (sunset) to the given minutes or nil in order not to round round

Returns:



159
160
161
162
163
164
165
166
167
168
# File 'lib/aixm/schedule/time.rb', line 159

def resolve(on:, xy:, round: nil)
  if resolved?
    self
  else
    sun_time = self.class.new(Sun.send(event, on.to_date, xy.lat, xy.long).utc + (delta * 60))
    sun_time = self.class.new([sun_time.time, self.time].send(PRECEDENCES.fetch(precedence))) if time
    sun_time = sun_time.round(EVENTS.fetch(event) => round) if round
    sun_time
  end
end

#resolved?Boolean

Whether this time is resolved and doesn’t contain an event (anymore).

Returns:

  • (Boolean)


173
174
175
# File 'lib/aixm/schedule/time.rb', line 173

def resolved?
  !event
end

#round(up: nil, down: nil) ⇒ AIXM::Schedule::Time, self

Round this time up or down.

Parameters:

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

    round up to the next given minutes

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

    round down to the next given minutes

Returns:



182
183
184
185
186
187
188
189
190
191
# File 'lib/aixm/schedule/time.rb', line 182

def round(up: nil, down: nil)
  step = up || down || fail(ArgumentError, "either up or down is mandatory")
  rounded_min = min / step * step
  if rounded_min == min
    self
  else
    rounded_min = (rounded_min + step) % 60 if up
    at(min: rounded_min, wrap: !!up)
  end
end

#sortable?Boolean

Whether this schedule time is sortable.

Returns:

  • (Boolean)


222
223
224
# File 'lib/aixm/schedule/time.rb', line 222

def sortable?
  !event
end

#to_s(format = '%R %z %o %E %P') ⇒ String

Human readable representation

The format recognises does the following interpolations:

  • %R - “HH:MM” in UTC if time is present, “” otherwise

  • %z - “UTC” if time is present, “” otherwise

  • %o - “or” if both time and event are present, “” otherwise

  • %E - “sunrise-15min” if no event is present, “” otherwise

  • %P - “whichever comes first” if precedence is present, “” otherwise

Parameters:

  • format (String) (defaults to: '%R %z %o %E %P')

Returns:

  • (String)


108
109
110
111
112
113
114
115
116
# File 'lib/aixm/schedule/time.rb', line 108

def to_s(format='%R %z %o %E %P')
  format.gsub(/%[RzoEP]/,
    '%R' => (sprintf("%02d:%02d", hour, min) if @time),
    '%z' => ('UTC' if @time),
    '%o' => ('or' if @time && event),
    '%E' => "#{event}#{sprintf("%+dmin", delta) unless delta.zero?}",
    '%P' => ("whichever comes #{precedence}" if precedence)
  ).compact
end

#to_timeTime

Stdlib Time equivalent using the value of DATELESS_DATE to represent a time only.

Returns:



197
198
199
# File 'lib/aixm/schedule/time.rb', line 197

def to_time
  @time
end