montrose 0.11.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +66 -0
  3. data/Appraisals +8 -12
  4. data/CHANGELOG.md +24 -0
  5. data/Guardfile +2 -2
  6. data/README.md +27 -15
  7. data/Rakefile +2 -4
  8. data/bin/setup +1 -0
  9. data/bin/standardrb +29 -0
  10. data/gemfiles/activesupport_5.2.gemfile +5 -1
  11. data/gemfiles/activesupport_6.0.gemfile +5 -1
  12. data/gemfiles/{activesupport_4.2.gemfile → activesupport_6.1.gemfile} +5 -1
  13. data/gemfiles/{activesupport_5.0.gemfile → activesupport_7.0.gemfile} +5 -1
  14. data/lib/montrose/chainable.rb +26 -10
  15. data/lib/montrose/clock.rb +4 -4
  16. data/lib/montrose/day.rb +83 -0
  17. data/lib/montrose/frequency.rb +58 -25
  18. data/lib/montrose/hour.rb +22 -0
  19. data/lib/montrose/ical.rb +128 -0
  20. data/lib/montrose/minute.rb +22 -0
  21. data/lib/montrose/month.rb +47 -0
  22. data/lib/montrose/month_day.rb +25 -0
  23. data/lib/montrose/options.rb +73 -79
  24. data/lib/montrose/recurrence.rb +40 -13
  25. data/lib/montrose/rule/between.rb +1 -1
  26. data/lib/montrose/rule/covering.rb +40 -0
  27. data/lib/montrose/rule/during.rb +7 -15
  28. data/lib/montrose/rule/minute_of_hour.rb +25 -0
  29. data/lib/montrose/rule/nth_day_of_month.rb +0 -2
  30. data/lib/montrose/rule/nth_day_of_year.rb +0 -2
  31. data/lib/montrose/rule/time_of_day.rb +1 -1
  32. data/lib/montrose/rule/until.rb +1 -1
  33. data/lib/montrose/rule.rb +18 -16
  34. data/lib/montrose/schedule.rb +6 -8
  35. data/lib/montrose/stack.rb +3 -4
  36. data/lib/montrose/time_of_day.rb +48 -0
  37. data/lib/montrose/utils.rb +2 -40
  38. data/lib/montrose/version.rb +1 -1
  39. data/lib/montrose/week.rb +20 -0
  40. data/lib/montrose/year_day.rb +25 -0
  41. data/lib/montrose.rb +43 -11
  42. data/montrose.gemspec +17 -17
  43. metadata +43 -36
  44. data/.rubocop.yml +0 -136
  45. data/.travis.yml +0 -33
  46. data/bin/rubocop +0 -16
  47. data/gemfiles/activesupport_4.1.gemfile +0 -12
  48. data/gemfiles/activesupport_5.1.gemfile +0 -12
@@ -1,14 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "montrose/errors"
4
- require "montrose/options"
5
-
6
3
  module Montrose
7
4
  # Abstract class for special recurrence rule required
8
5
  # in all instances of Recurrence. Frequency describes
9
6
  # the base recurrence interval.
10
7
  #
11
8
  class Frequency
9
+ autoload :Daily, "montrose/frequency/daily"
10
+ autoload :Hourly, "montrose/frequency/hourly"
11
+ autoload :Minutely, "montrose/frequency/minutely"
12
+ autoload :Monthly, "montrose/frequency/monthly"
13
+ autoload :Secondly, "montrose/frequency/secondly"
14
+ autoload :Weekly, "montrose/frequency/weekly"
15
+ autoload :Yearly, "montrose/frequency/yearly"
16
+
12
17
  include Montrose::Rule
13
18
 
14
19
  FREQUENCY_TERMS = {
@@ -25,24 +30,60 @@ module Montrose
25
30
 
26
31
  attr_reader :time, :starts
27
32
 
28
- # Factory method for instantiating the appropriate Frequency
29
- # subclass.
30
- #
31
- def self.from_options(opts)
32
- frequency = opts.fetch(:every) { fail ConfigurationError, "Please specify the :every option" }
33
- class_name = FREQUENCY_TERMS.fetch(frequency.to_s) do
34
- fail "Don't know how to enumerate every: #{frequency}"
33
+ class << self
34
+ def parse(input)
35
+ if input.respond_to?(:parts)
36
+ frequency, interval = duration_to_frequency_parts(input)
37
+ {every: frequency.to_s.singularize.to_sym, interval: interval}
38
+ elsif input.is_a?(Numeric)
39
+ frequency, interval = numeric_to_frequency_parts(input)
40
+ {every: frequency, interval: interval}
41
+ else
42
+ {every: Frequency.assert(input)}
43
+ end
35
44
  end
36
45
 
37
- Montrose::Frequency.const_get(class_name).new(opts)
38
- end
46
+ # Factory method for instantiating the appropriate Frequency
47
+ # subclass.
48
+ #
49
+ def from_options(opts)
50
+ frequency = opts.fetch(:every) { fail ConfigurationError, "Please specify the :every option" }
51
+ class_name = FREQUENCY_TERMS.fetch(frequency.to_s) {
52
+ fail "Don't know how to enumerate every: #{frequency}"
53
+ }
54
+
55
+ Montrose::Frequency.const_get(class_name).new(opts)
56
+ end
57
+
58
+ def from_term(term)
59
+ FREQUENCY_TERMS.invert.map { |k, v| [k.downcase, v] }.to_h.fetch(term.downcase) do
60
+ fail "Don't know how to convert #{term} to a Montrose frequency"
61
+ end
62
+ end
39
63
 
40
- # @private
41
- def self.assert(frequency)
42
- FREQUENCY_TERMS.key?(frequency.to_s) or fail ConfigurationError,
43
- "Don't know how to enumerate every: #{frequency}"
64
+ # @private
65
+ def assert(frequency)
66
+ FREQUENCY_TERMS.key?(frequency.to_s) || fail(ConfigurationError,
67
+ "Don't know how to enumerate every: #{frequency}")
44
68
 
45
- frequency.to_sym
69
+ frequency.to_sym
70
+ end
71
+
72
+ # @private
73
+ def numeric_to_frequency_parts(number)
74
+ parts = nil
75
+ %i[year month week day hour minute].each do |freq|
76
+ div, mod = number.divmod(1.send(freq))
77
+ parts = [freq, div]
78
+ return parts if mod.zero?
79
+ end
80
+ parts
81
+ end
82
+
83
+ # @private
84
+ def duration_to_frequency_parts(duration)
85
+ duration.parts.first
86
+ end
46
87
  end
47
88
 
48
89
  def initialize(opts = {})
@@ -67,11 +108,3 @@ module Montrose
67
108
  end
68
109
  end
69
110
  end
70
-
71
- require "montrose/frequency/daily"
72
- require "montrose/frequency/hourly"
73
- require "montrose/frequency/minutely"
74
- require "montrose/frequency/monthly"
75
- require "montrose/frequency/secondly"
76
- require "montrose/frequency/weekly"
77
- require "montrose/frequency/yearly"
@@ -0,0 +1,22 @@
1
+ module Montrose
2
+ class Hour
3
+ HOURS_IN_DAY = 1.upto(24).to_a.freeze
4
+
5
+ class << self
6
+ def parse(arg)
7
+ case arg
8
+ when String
9
+ parse(arg.split(","))
10
+ else
11
+ Array(arg).map { |h| assert(h.to_i) }.presence
12
+ end
13
+ end
14
+
15
+ def assert(hour)
16
+ raise ConfigurationError, "Out of range: #{HOURS_IN_DAY.inspect} does not include #{hour}" unless HOURS_IN_DAY.include?(hour)
17
+
18
+ hour
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Montrose
4
+ class ICal
5
+ # DTSTART;TZID=US-Eastern:19970902T090000
6
+ # RRULE:FREQ=DAILY;INTERVAL=2
7
+ def self.parse(ical)
8
+ new(ical).parse
9
+ end
10
+
11
+ def initialize(ical)
12
+ @ical = ical
13
+ end
14
+
15
+ def parse
16
+ time_zone = extract_time_zone(@ical)
17
+
18
+ Time.use_zone(time_zone) do
19
+ Hash[*parse_properties(@ical)]
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def extract_time_zone(ical_string)
26
+ _label, time_string = ical_string.split("\n").grep(/^DTSTART/).join.split(";")
27
+ time_zone_rule, _ = time_string.split(":")
28
+ _label, time_zone = (time_zone_rule || "").split("=")
29
+ time_zone
30
+ end
31
+
32
+ # First pass parsing to normalize arbitrary line breaks
33
+ def property_lines(ical_string)
34
+ ical_string.split("\n").each_with_object([]) do |line, lines|
35
+ case line
36
+ when /^(DTSTART|DTEND|EXDATE|RDATE|RRULE)/
37
+ lines << line
38
+ else
39
+ (lines.last || lines << "")
40
+ lines.last << line
41
+ end
42
+ end
43
+ end
44
+
45
+ def parse_properties(ical_string)
46
+ property_lines(ical_string).flat_map do |line|
47
+ (property, value) = line.split(":")
48
+ (property, tzid) = property.split(";")
49
+
50
+ case property
51
+ when "DTSTART"
52
+ parse_dtstart(tzid, value)
53
+ when "DTEND"
54
+ warn "DTEND not currently supported!"
55
+ when "EXDATE"
56
+ parse_exdate(value)
57
+ when "RDATE"
58
+ warn "RDATE not currently supported!"
59
+ when "RRULE"
60
+ parse_rrule(value)
61
+ end
62
+ end
63
+ end
64
+
65
+ def parse_dtstart(tzid, time)
66
+ return [] unless time.present?
67
+
68
+ @starts_at = parse_time([tzid, time].compact.join(":"))
69
+
70
+ [:starts, @starts_at]
71
+ end
72
+
73
+ def parse_timezone(time_string)
74
+ time_zone_rule, _ = time_string.split(":")
75
+ _label, time_zone = (time_zone_rule || "").split("=")
76
+ time_zone
77
+ end
78
+
79
+ def parse_time(time_string)
80
+ time_zone = parse_timezone(time_string)
81
+ Montrose::Utils.parse_time(time_string).in_time_zone(time_zone)
82
+ end
83
+
84
+ def parse_exdate(exdate)
85
+ return [] unless exdate.present?
86
+
87
+ @except = Montrose::Utils.as_date(exdate) # only currently supports dates
88
+
89
+ [:except, @except]
90
+ end
91
+
92
+ def parse_rrule(rrule)
93
+ rrule.gsub(/\s+/, "").split(";").flat_map do |rule|
94
+ prop, value = rule.split("=")
95
+ case prop
96
+ when "FREQ"
97
+ [:every, Montrose::Frequency.from_term(value)]
98
+ when "INTERVAL"
99
+ [:interval, value.to_i]
100
+ when "COUNT"
101
+ [:total, value.to_i]
102
+ when "UNTIL"
103
+ [:until, parse_time(value)]
104
+ when "BYMINUTE"
105
+ [:minute, Montrose::Minute.parse(value)]
106
+ when "BYHOUR"
107
+ [:hour, Montrose::Hour.parse(value)]
108
+ when "BYMONTH"
109
+ [:month, Montrose::Month.parse(value)]
110
+ when "BYDAY"
111
+ [:day, Montrose::Day.parse(value)]
112
+ when "BYMONTHDAY"
113
+ [:mday, Montrose::MonthDay.parse(value)]
114
+ when "BYYEARDAY"
115
+ [:yday, Montrose::YearDay.parse(value)]
116
+ when "BYWEEKNO"
117
+ [:week, Montrose::Week.parse(value)]
118
+ when "WKST"
119
+ [:week_start, value]
120
+ when "BYSETPOS"
121
+ warn "BYSETPOS not currently supported!"
122
+ else
123
+ raise "Unrecognized rrule '#{rule}'"
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,22 @@
1
+ module Montrose
2
+ class Minute
3
+ MINUTES_IN_HOUR = 0.upto(59).to_a.freeze
4
+
5
+ class << self
6
+ def parse(arg)
7
+ case arg
8
+ when String
9
+ parse(arg.split(","))
10
+ else
11
+ Array(arg).map { |m| assert(m.to_i) }.presence
12
+ end
13
+ end
14
+
15
+ def assert(minute)
16
+ raise ConfigurationError, "Out of range: #{MINUTES_IN_HOUR.inspect} does not include #{minute}" unless MINUTES_IN_HOUR.include?(minute)
17
+
18
+ minute
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,47 @@
1
+ module Montrose
2
+ class Month
3
+ extend Montrose::Utils
4
+
5
+ NAMES = ::Date::MONTHNAMES # starts with nil to match 1-12 numbering
6
+ NUMBERS = NAMES.map.with_index { |_n, i| i.to_s }.slice(1, 12)
7
+
8
+ class << self
9
+ def parse(value)
10
+ case value
11
+ when String
12
+ parse(value.split(",").compact)
13
+ when Array
14
+ value.map { |m|
15
+ Montrose::Month.number!(m)
16
+ }.presence
17
+ else
18
+ parse(Array(value))
19
+ end
20
+ end
21
+
22
+ def names
23
+ NAMES
24
+ end
25
+
26
+ def numbers
27
+ NUMBERS
28
+ end
29
+
30
+ def number(name)
31
+ case name
32
+ when Symbol, String
33
+ string = name.to_s
34
+ NAMES.index(string.titleize) || number(to_index(string))
35
+ when 1..12
36
+ name
37
+ end
38
+ end
39
+
40
+ def number!(name)
41
+ numbers = NAMES.map.with_index { |_n, i| i.to_s }.slice(1, 12)
42
+ number(name) || raise(ConfigurationError,
43
+ "Did not recognize month #{name}, must be one of #{(NAMES + numbers).inspect}")
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
1
+ module Montrose
2
+ class MonthDay
3
+ class << self
4
+ MDAYS = (-31.upto(-1).to_a + 1.upto(31).to_a)
5
+
6
+ def parse(mdays)
7
+ return nil unless mdays.present?
8
+
9
+ case mdays
10
+ when String
11
+ parse(mdays.split(","))
12
+ else
13
+ Array(mdays).map { |d| assert(d.to_i) }
14
+ end
15
+ end
16
+
17
+ def assert(number)
18
+ test = number.abs
19
+ raise ConfigurationError, "Out of range: #{MDAYS.inspect} does not include #{test}" unless MDAYS.include?(number.abs)
20
+
21
+ number
22
+ end
23
+ end
24
+ end
25
+ end
@@ -86,12 +86,15 @@ module Montrose
86
86
  def_option :starts
87
87
  def_option :until
88
88
  def_option :between
89
+ def_option :covering
89
90
  def_option :during
91
+ def_option :minute
90
92
  def_option :hour
91
93
  def_option :day
92
94
  def_option :mday
93
95
  def_option :yday
94
96
  def_option :week
97
+ def_option :week_start
95
98
  def_option :month
96
99
  def_option :interval
97
100
  def_option :total
@@ -112,6 +115,7 @@ module Montrose
112
115
  week: nil,
113
116
  month: nil,
114
117
  total: nil,
118
+ week_start: nil,
115
119
  exclude_end: nil
116
120
  }
117
121
 
@@ -120,12 +124,12 @@ module Montrose
120
124
  end
121
125
 
122
126
  def to_hash
123
- hash_pairs = self.class.defined_options.flat_map do |opt_name|
127
+ hash_pairs = self.class.defined_options.flat_map { |opt_name|
124
128
  [opt_name, send(opt_name)]
125
- end
129
+ }
126
130
  Hash[*hash_pairs].reject { |_k, v| v.nil? }
127
131
  end
128
- alias to_h to_hash
132
+ alias_method :to_h, :to_hash
129
133
 
130
134
  def []=(option, val)
131
135
  send(:"#{option}=", val)
@@ -142,13 +146,13 @@ module Montrose
142
146
  self.class.new(h1.merge(h2))
143
147
  end
144
148
 
145
- def fetch(key, *args, &_block)
146
- fail ArgumentError, "wrong number of arguments (#{args.length} for 1..2)" if args.length > 1
149
+ def fetch(key, *args)
150
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 1..2)" if args.length > 1
147
151
 
148
152
  found = send(key)
149
153
  return found if found
150
154
  return args.first if args.length == 1
151
- fail "Key #{key.inspect} not found" unless block_given?
155
+ raise KeyError, "Key #{key.inspect} not found" unless block_given?
152
156
 
153
157
  yield
154
158
  end
@@ -165,8 +169,8 @@ module Montrose
165
169
  @every = parsed.fetch(:every)
166
170
  end
167
171
 
168
- alias frequency every
169
- alias frequency= every=
172
+ alias_method :frequency, :every
173
+ alias_method :frequency=, :every=
170
174
 
171
175
  def starts=(time)
172
176
  @starts = normalize_time(as_time(time)) || default_starts
@@ -176,47 +180,58 @@ module Montrose
176
180
  @until = normalize_time(as_time(time)) || default_until
177
181
  end
178
182
 
183
+ def minute=(minutes)
184
+ @minute = Minute.parse(minutes)
185
+ end
186
+
179
187
  def hour=(hours)
180
188
  @hour = map_arg(hours) { |h| assert_hour(h) }
181
189
  end
182
190
 
183
- def during=(during)
184
- @during = case during
185
- when Range
186
- [decompose_during_arg(during)]
187
- else
188
- map_arg(during) { |d| decompose_during_arg(d) }
189
- end
191
+ def during=(during_arg)
192
+ @during = decompose_during_arg(during_arg)
193
+ .each_with_object([]) { |(time_of_day_first, time_of_day_last), all|
194
+ if time_of_day_last < time_of_day_first
195
+ all.push(
196
+ [time_of_day_first.parts, end_of_day.parts],
197
+ [beginning_of_day.parts, time_of_day_last.parts]
198
+ )
199
+ else
200
+ all.push([time_of_day_first.parts, time_of_day_last.parts])
201
+ end
202
+ }.presence
190
203
  end
191
204
 
192
205
  def day=(days)
193
- @day = nested_map_arg(days) { |d| day_number!(d) }
206
+ @day = Day.parse(days)
194
207
  end
195
208
 
196
209
  def mday=(mdays)
197
- @mday = map_mdays(mdays)
210
+ @mday = MonthDay.parse(mdays)
198
211
  end
199
212
 
200
213
  def yday=(ydays)
201
- @yday = map_ydays(ydays)
214
+ @yday = YearDay.parse(ydays)
202
215
  end
203
216
 
204
217
  def week=(weeks)
205
- @week = map_arg(weeks) { |w| assert_week(w) }
218
+ @week = Week.parse(weeks)
206
219
  end
207
220
 
208
221
  def month=(months)
209
- @month = map_arg(months) { |d| month_number!(d) }
222
+ @month = Month.parse(months)
210
223
  end
211
224
 
212
225
  def between=(range)
213
- @between = range
226
+ if Montrose.enable_deprecated_between_masking?
227
+ @covering = range
228
+ end
214
229
  self[:starts] = range.first unless self[:starts]
215
230
  self[:until] = range.last unless self[:until]
216
231
  end
217
232
 
218
233
  def at=(time)
219
- @at = map_arg(time) { |t| as_time_parts(t) }
234
+ @at = map_arg(time) { |t| time_of_day_parse(t).parts }
220
235
  end
221
236
 
222
237
  def on=(arg)
@@ -257,51 +272,16 @@ module Montrose
257
272
  self.class.default_until
258
273
  end
259
274
 
260
- def nested_map_arg(arg, &block)
261
- case arg
262
- when Hash
263
- arg.each_with_object({}) do |(k, v), hash|
264
- hash[yield k] = [*v]
265
- end
266
- else
267
- map_arg(arg, &block)
268
- end
269
- end
270
-
271
275
  def map_arg(arg, &block)
272
276
  return nil unless arg
273
277
 
274
278
  Array(arg).map(&block)
275
279
  end
276
280
 
277
- def map_days(arg)
278
- map_arg(arg) { |d| day_number!(d) }
279
- end
280
-
281
- def map_mdays(arg)
282
- map_arg(arg) { |d| assert_mday(d) }
283
- end
284
-
285
- def map_ydays(arg)
286
- map_arg(arg) { |d| assert_yday(d) }
287
- end
288
-
289
281
  def assert_hour(hour)
290
282
  assert_range_includes(1..::Montrose::Utils::MAX_HOURS_IN_DAY, hour)
291
283
  end
292
284
 
293
- def assert_mday(mday)
294
- assert_range_includes(1..::Montrose::Utils::MAX_DAYS_IN_MONTH, mday, :absolute)
295
- end
296
-
297
- def assert_yday(yday)
298
- assert_range_includes(1..::Montrose::Utils::MAX_DAYS_IN_YEAR, yday, :absolute)
299
- end
300
-
301
- def assert_week(week)
302
- assert_range_includes(1..::Montrose::Utils::MAX_WEEKS_IN_YEAR, week, :absolute)
303
- end
304
-
305
285
  def decompose_on_arg(arg)
306
286
  case arg
307
287
  when Hash
@@ -309,52 +289,45 @@ module Montrose
309
289
  key, val = month_or_day(k)
310
290
  result[key] = val
311
291
  result[:mday] ||= []
312
- result[:mday] += map_mdays(v)
292
+ result[:mday] += Montrose::MonthDay.parse(v)
313
293
  end
314
294
  else
315
- { day: map_days(arg) }
295
+ {day: Montrose::Day.parse(arg)}
316
296
  end
317
297
  end
318
298
 
319
299
  def month_or_day(key)
320
- month = month_number(key)
300
+ month = Montrose::Month.number(key)
321
301
  return [:month, month] if month
322
302
 
323
- day = day_number(key)
303
+ day = Montrose::Day.number(key)
324
304
  return [:day, day] if day
325
305
 
326
- fail ConfigurationError, "Did not recognize #{key} as a month or day"
306
+ raise ConfigurationError, "Did not recognize #{key} as a month or day"
327
307
  end
328
308
 
329
309
  def assert_range_includes(range, item, absolute = false)
330
310
  test = absolute ? item.abs : item
331
- fail ConfigurationError, "Out of range: #{range.inspect} does not include #{test}" unless range.include?(test)
311
+ raise ConfigurationError, "Out of range: #{range.inspect} does not include #{test}" unless range.include?(test)
332
312
 
333
313
  item
334
314
  end
335
315
 
336
- def as_time_parts(arg)
337
- return arg if arg.is_a?(Array)
338
-
339
- time = as_time(arg)
340
- [time.hour, time.min, time.sec]
341
- end
342
-
343
316
  def parse_frequency(input)
344
317
  if input.respond_to?(:parts)
345
318
  frequency, interval = duration_to_frequency_parts(input)
346
- { every: frequency.to_s.singularize.to_sym, interval: interval }
319
+ {every: frequency.to_s.singularize.to_sym, interval: interval}
347
320
  elsif input.is_a?(Numeric)
348
321
  frequency, interval = numeric_to_frequency_parts(input)
349
- { every: frequency, interval: interval }
322
+ {every: frequency, interval: interval}
350
323
  else
351
- { every: Frequency.assert(input) }
324
+ {every: Frequency.assert(input)}
352
325
  end
353
326
  end
354
327
 
355
328
  def numeric_to_frequency_parts(number)
356
329
  parts = nil
357
- [:year, :month, :week, :day, :hour, :minute].each do |freq|
330
+ %i[year month week day hour minute].each do |freq|
358
331
  div, mod = number.divmod(1.send(freq))
359
332
  parts = [freq, div]
360
333
  return parts if mod.zero?
@@ -366,15 +339,36 @@ module Montrose
366
339
  duration.parts.first
367
340
  end
368
341
 
369
- def decompose_during_arg(during)
370
- case during
342
+ def decompose_during_arg(during_arg)
343
+ case during_arg
344
+ when Range
345
+ [decompose_during_parts(during_arg)]
346
+ else
347
+ map_arg(during_arg) { |d| decompose_during_parts(d) } || []
348
+ end
349
+ end
350
+
351
+ def decompose_during_parts(during_parts)
352
+ case during_parts
371
353
  when Range
372
- [decompose_during_arg(during.first), decompose_during_arg(during.last)]
354
+ decompose_during_parts([during_parts.first, during_parts.last])
373
355
  when String
374
- during.split(%r{[-—–]}).map { |d| as_time_parts(d) }
356
+ decompose_during_parts(during_parts.split(/[-—–]/))
375
357
  else
376
- as_time_parts(during)
358
+ during_parts.map { |parts| time_of_day_parse(parts) }
377
359
  end
378
360
  end
361
+
362
+ def time_of_day_parse(time_parts)
363
+ ::Montrose::TimeOfDay.parse(time_parts)
364
+ end
365
+
366
+ def end_of_day
367
+ @end_of_day ||= time_of_day_parse(Time.now.end_of_day)
368
+ end
369
+
370
+ def beginning_of_day
371
+ @beginning_of_day ||= time_of_day_parse(Time.now.beginning_of_day)
372
+ end
379
373
  end
380
374
  end