montrose 0.12.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.
@@ -88,11 +88,13 @@ module Montrose
88
88
  def_option :between
89
89
  def_option :covering
90
90
  def_option :during
91
+ def_option :minute
91
92
  def_option :hour
92
93
  def_option :day
93
94
  def_option :mday
94
95
  def_option :yday
95
96
  def_option :week
97
+ def_option :week_start
96
98
  def_option :month
97
99
  def_option :interval
98
100
  def_option :total
@@ -113,6 +115,7 @@ module Montrose
113
115
  week: nil,
114
116
  month: nil,
115
117
  total: nil,
118
+ week_start: nil,
116
119
  exclude_end: nil
117
120
  }
118
121
 
@@ -149,7 +152,7 @@ module Montrose
149
152
  found = send(key)
150
153
  return found if found
151
154
  return args.first if args.length == 1
152
- raise "Key #{key.inspect} not found" unless block
155
+ raise KeyError, "Key #{key.inspect} not found" unless block_given?
153
156
 
154
157
  yield
155
158
  end
@@ -177,37 +180,46 @@ module Montrose
177
180
  @until = normalize_time(as_time(time)) || default_until
178
181
  end
179
182
 
183
+ def minute=(minutes)
184
+ @minute = Minute.parse(minutes)
185
+ end
186
+
180
187
  def hour=(hours)
181
188
  @hour = map_arg(hours) { |h| assert_hour(h) }
182
189
  end
183
190
 
184
- def during=(during)
185
- @during = case during
186
- when Range
187
- [decompose_during_arg(during)]
188
- else
189
- map_arg(during) { |d| decompose_during_arg(d) }
190
- 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
191
203
  end
192
204
 
193
205
  def day=(days)
194
- @day = nested_map_arg(days) { |d| day_number!(d) }
206
+ @day = Day.parse(days)
195
207
  end
196
208
 
197
209
  def mday=(mdays)
198
- @mday = map_mdays(mdays)
210
+ @mday = MonthDay.parse(mdays)
199
211
  end
200
212
 
201
213
  def yday=(ydays)
202
- @yday = map_ydays(ydays)
214
+ @yday = YearDay.parse(ydays)
203
215
  end
204
216
 
205
217
  def week=(weeks)
206
- @week = map_arg(weeks) { |w| assert_week(w) }
218
+ @week = Week.parse(weeks)
207
219
  end
208
220
 
209
221
  def month=(months)
210
- @month = map_arg(months) { |d| month_number!(d) }
222
+ @month = Month.parse(months)
211
223
  end
212
224
 
213
225
  def between=(range)
@@ -219,7 +231,7 @@ module Montrose
219
231
  end
220
232
 
221
233
  def at=(time)
222
- @at = map_arg(time) { |t| as_time_parts(t) }
234
+ @at = map_arg(time) { |t| time_of_day_parse(t).parts }
223
235
  end
224
236
 
225
237
  def on=(arg)
@@ -260,51 +272,16 @@ module Montrose
260
272
  self.class.default_until
261
273
  end
262
274
 
263
- def nested_map_arg(arg, &block)
264
- case arg
265
- when Hash
266
- arg.each_with_object({}) do |(k, v), hash|
267
- hash[yield k] = [*v]
268
- end
269
- else
270
- map_arg(arg, &block)
271
- end
272
- end
273
-
274
275
  def map_arg(arg, &block)
275
276
  return nil unless arg
276
277
 
277
278
  Array(arg).map(&block)
278
279
  end
279
280
 
280
- def map_days(arg)
281
- map_arg(arg) { |d| day_number!(d) }
282
- end
283
-
284
- def map_mdays(arg)
285
- map_arg(arg) { |d| assert_mday(d) }
286
- end
287
-
288
- def map_ydays(arg)
289
- map_arg(arg) { |d| assert_yday(d) }
290
- end
291
-
292
281
  def assert_hour(hour)
293
282
  assert_range_includes(1..::Montrose::Utils::MAX_HOURS_IN_DAY, hour)
294
283
  end
295
284
 
296
- def assert_mday(mday)
297
- assert_range_includes(1..::Montrose::Utils::MAX_DAYS_IN_MONTH, mday, :absolute)
298
- end
299
-
300
- def assert_yday(yday)
301
- assert_range_includes(1..::Montrose::Utils::MAX_DAYS_IN_YEAR, yday, :absolute)
302
- end
303
-
304
- def assert_week(week)
305
- assert_range_includes(1..::Montrose::Utils::MAX_WEEKS_IN_YEAR, week, :absolute)
306
- end
307
-
308
285
  def decompose_on_arg(arg)
309
286
  case arg
310
287
  when Hash
@@ -312,18 +289,18 @@ module Montrose
312
289
  key, val = month_or_day(k)
313
290
  result[key] = val
314
291
  result[:mday] ||= []
315
- result[:mday] += map_mdays(v)
292
+ result[:mday] += Montrose::MonthDay.parse(v)
316
293
  end
317
294
  else
318
- {day: map_days(arg)}
295
+ {day: Montrose::Day.parse(arg)}
319
296
  end
320
297
  end
321
298
 
322
299
  def month_or_day(key)
323
- month = month_number(key)
300
+ month = Montrose::Month.number(key)
324
301
  return [:month, month] if month
325
302
 
326
- day = day_number(key)
303
+ day = Montrose::Day.number(key)
327
304
  return [:day, day] if day
328
305
 
329
306
  raise ConfigurationError, "Did not recognize #{key} as a month or day"
@@ -336,13 +313,6 @@ module Montrose
336
313
  item
337
314
  end
338
315
 
339
- def as_time_parts(arg)
340
- return arg if arg.is_a?(Array)
341
-
342
- time = as_time(arg)
343
- [time.hour, time.min, time.sec]
344
- end
345
-
346
316
  def parse_frequency(input)
347
317
  if input.respond_to?(:parts)
348
318
  frequency, interval = duration_to_frequency_parts(input)
@@ -369,15 +339,36 @@ module Montrose
369
339
  duration.parts.first
370
340
  end
371
341
 
372
- def decompose_during_arg(during)
373
- case during
342
+ def decompose_during_arg(during_arg)
343
+ case during_arg
374
344
  when Range
375
- [decompose_during_arg(during.first), decompose_during_arg(during.last)]
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
353
+ when Range
354
+ decompose_during_parts([during_parts.first, during_parts.last])
376
355
  when String
377
- during.split(/[-—–]/).map { |d| as_time_parts(d) }
356
+ decompose_during_parts(during_parts.split(/[-—–]/))
378
357
  else
379
- as_time_parts(during)
358
+ during_parts.map { |parts| time_of_day_parse(parts) }
380
359
  end
381
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
382
373
  end
383
374
  end
@@ -2,10 +2,6 @@
2
2
 
3
3
  require "json"
4
4
  require "yaml"
5
- require "montrose/chainable"
6
- require "montrose/errors"
7
- require "montrose/stack"
8
- require "montrose/clock"
9
5
 
10
6
  module Montrose
11
7
  # Represents the rules for a set of recurring events. Can be instantiated
@@ -250,6 +246,16 @@ module Montrose
250
246
  rescue JSON::ParserError => e
251
247
  fail SerializationError, "Could not parse JSON: #{e}"
252
248
  end
249
+
250
+ alias_method :from_json, :load
251
+
252
+ def from_yaml(yaml)
253
+ new(YAML.safe_load(yaml))
254
+ end
255
+
256
+ def from_ical(ical)
257
+ new(Montrose::ICal.parse(ical))
258
+ end
253
259
  end
254
260
 
255
261
  def initialize(opts = {})
@@ -331,7 +337,7 @@ module Montrose
331
337
  # @return [String] YAML-formatted recurrence options
332
338
  #
333
339
  def to_yaml(*args)
334
- YAML.dump(JSON.parse(to_json(*args)))
340
+ YAML.dump(as_json(*args))
335
341
  end
336
342
 
337
343
  def inspect
@@ -21,22 +21,10 @@ module Montrose
21
21
  @during.any? { |range| range.include?(time) }
22
22
  end
23
23
 
24
- class TimeOfDay
25
- def initialize(hour, min, sec)
26
- @hour = hour
27
- @min = min
28
- @sec = sec
29
- end
30
-
31
- def seconds_since_midnight
32
- @seconds_since_midnight ||= (@hour * 60 * 60) + (@min * 60) + @sec
33
- end
34
- end
35
-
36
24
  class TimeOfDayRange
37
25
  def initialize(first, last, exclude_end: false)
38
- @first = TimeOfDay.new(*first)
39
- @last = TimeOfDay.new(*last)
26
+ @first = ::Montrose::TimeOfDay.new(first)
27
+ @last = ::Montrose::TimeOfDay.new(last)
40
28
  @exclude_end = exclude_end
41
29
  end
42
30
 
@@ -47,7 +35,11 @@ module Montrose
47
35
  private
48
36
 
49
37
  def range
50
- @range ||= Range.new(@first.seconds_since_midnight, @last.seconds_since_midnight, @exclude_end)
38
+ @range ||= Range.new(
39
+ @first.seconds_since_midnight,
40
+ @last.seconds_since_midnight,
41
+ @exclude_end
42
+ )
51
43
  end
52
44
  end
53
45
  end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Montrose
4
+ module Rule
5
+ class MinuteOfHour
6
+ include Montrose::Rule
7
+
8
+ def self.apply_options(opts)
9
+ opts[:minute]
10
+ end
11
+
12
+ # Initializes rule
13
+ #
14
+ # @param minutes [Array<Fixnum>] valid minutes of hour, e.g. [0, 20, 59]
15
+ #
16
+ def initialize(minutes)
17
+ @minutes = minutes
18
+ end
19
+
20
+ def include?(time)
21
+ @minutes.include?(time.min)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "montrose/rule/nth_day_matcher"
4
-
5
3
  module Montrose
6
4
  module Rule
7
5
  class NthDayOfMonth
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "montrose/rule/nth_day_matcher"
4
-
5
3
  module Montrose
6
4
  module Rule
7
5
  class NthDayOfYear
@@ -24,7 +24,7 @@ module Montrose
24
24
  private
25
25
 
26
26
  def parts(time)
27
- [time.hour, time.min, time.sec]
27
+ ::Montrose::TimeOfDay.to_parts(time)
28
28
  end
29
29
  end
30
30
  end
data/lib/montrose/rule.rb CHANGED
@@ -3,6 +3,24 @@
3
3
  module Montrose
4
4
  # Defines the Rule duck type for recurrence rules
5
5
  module Rule
6
+ autoload :After, "montrose/rule/after"
7
+ autoload :Covering, "montrose/rule/covering"
8
+ autoload :DayOfMonth, "montrose/rule/day_of_month"
9
+ autoload :DayOfWeek, "montrose/rule/day_of_week"
10
+ autoload :DayOfYear, "montrose/rule/day_of_year"
11
+ autoload :During, "montrose/rule/during"
12
+ autoload :Except, "montrose/rule/except"
13
+ autoload :HourOfDay, "montrose/rule/hour_of_day"
14
+ autoload :MinuteOfHour, "montrose/rule/minute_of_hour"
15
+ autoload :MonthOfYear, "montrose/rule/month_of_year"
16
+ autoload :NthDayMatcher, "montrose/rule/nth_day_matcher"
17
+ autoload :NthDayOfMonth, "montrose/rule/nth_day_of_month"
18
+ autoload :NthDayOfYear, "montrose/rule/nth_day_of_year"
19
+ autoload :TimeOfDay, "montrose/rule/time_of_day"
20
+ autoload :Total, "montrose/rule/total"
21
+ autoload :Until, "montrose/rule/until"
22
+ autoload :WeekOfYear, "montrose/rule/week_of_year"
23
+
6
24
  def self.included(base)
7
25
  base.extend ClassMethods
8
26
  end
@@ -34,19 +52,3 @@ module Montrose
34
52
  end
35
53
  end
36
54
  end
37
-
38
- require "montrose/rule/after"
39
- require "montrose/rule/covering"
40
- require "montrose/rule/day_of_month"
41
- require "montrose/rule/day_of_week"
42
- require "montrose/rule/day_of_year"
43
- require "montrose/rule/during"
44
- require "montrose/rule/except"
45
- require "montrose/rule/hour_of_day"
46
- require "montrose/rule/month_of_year"
47
- require "montrose/rule/nth_day_of_month"
48
- require "montrose/rule/nth_day_of_year"
49
- require "montrose/rule/time_of_day"
50
- require "montrose/rule/total"
51
- require "montrose/rule/until"
52
- require "montrose/rule/week_of_year"
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "montrose/rule"
4
-
5
3
  module Montrose
6
4
  # Maintains stack of recurrences rules that apply to
7
5
  # an associated recurrence; manages advancing state
@@ -18,6 +16,7 @@ module Montrose
18
16
  Rule::Except,
19
17
  Rule::Total,
20
18
  Rule::TimeOfDay,
19
+ Rule::MinuteOfHour,
21
20
  Rule::HourOfDay,
22
21
  Rule::NthDayOfMonth,
23
22
  Rule::NthDayOfYear,
@@ -0,0 +1,48 @@
1
+ module Montrose
2
+ class TimeOfDay
3
+ include Comparable
4
+
5
+ attr_reader :parts, :hour, :min, :sec
6
+
7
+ def self.parse(arg)
8
+ return new(arg) if arg.is_a?(Array)
9
+
10
+ from_time(::Montrose::Utils.as_time(arg))
11
+ end
12
+
13
+ def self.from_time(time)
14
+ new(to_parts(time))
15
+ end
16
+
17
+ def self.to_parts(time)
18
+ [time.hour, time.min, time.sec]
19
+ end
20
+
21
+ def initialize(parts)
22
+ @parts = parts
23
+ @hour, @min, @sec = *parts
24
+ end
25
+
26
+ def seconds_since_midnight
27
+ @seconds_since_midnight ||= (@hour * 60 * 60) + (@min * 60) + @sec
28
+ end
29
+
30
+ def to_a
31
+ @parts
32
+ end
33
+
34
+ # def inspect
35
+ # "#<Montrose::TimeOfDay #{format_time(@hour)}:#{format_time(@min)}:#{format_time(@sec)}"
36
+ # end
37
+
38
+ def <=>(other)
39
+ to_a <=> other.to_a
40
+ end
41
+
42
+ private
43
+
44
+ def format_time(part)
45
+ format("%02d", part)
46
+ end
47
+ end
48
+ end
@@ -4,10 +4,6 @@ module Montrose
4
4
  module Utils
5
5
  module_function
6
6
 
7
- MONTHS = ::Date::MONTHNAMES
8
-
9
- DAYS = ::Date::DAYNAMES
10
-
11
7
  MAX_HOURS_IN_DAY = 24
12
8
  MAX_DAYS_IN_YEAR = 366
13
9
  MAX_WEEKS_IN_YEAR = 53
@@ -44,40 +40,6 @@ module Montrose
44
40
  ::Time.current
45
41
  end
46
42
 
47
- def month_number(name)
48
- case name
49
- when Symbol, String
50
- string = name.to_s
51
- MONTHS.index(string.titleize) || month_number(to_index(string))
52
- when 1..12
53
- name
54
- end
55
- end
56
-
57
- def month_number!(name)
58
- month_numbers = MONTHS.map.with_index { |_n, i| i.to_s }.slice(1, 12)
59
- month_number(name) || raise(ConfigurationError,
60
- "Did not recognize month #{name}, must be one of #{(MONTHS + month_numbers).inspect}")
61
- end
62
-
63
- def day_number(name)
64
- case name
65
- when 0..6
66
- name
67
- when Symbol, String
68
- string = name.to_s
69
- DAYS.index(string.titleize) || day_number(to_index(string))
70
- when Array
71
- day_number name.first
72
- end
73
- end
74
-
75
- def day_number!(name)
76
- day_numbers = DAYS.map.with_index { |_n, i| i.to_s }
77
- day_number(name) || raise(ConfigurationError,
78
- "Did not recognize day #{name}, must be one of #{(DAYS + day_numbers).inspect}")
79
- end
80
-
81
43
  def days_in_month(month, year = current_time.year)
82
44
  date = ::Date.new(year, month, 1)
83
45
  ((date >> 1) - date).to_i
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Montrose
4
- VERSION = "0.12.0"
4
+ VERSION = "0.13.0"
5
5
  end
@@ -0,0 +1,20 @@
1
+ module Montrose
2
+ class Week
3
+ class << self
4
+ NUMBERS = (-53.upto(-1).to_a + 1.upto(53).to_a)
5
+
6
+ def parse(arg)
7
+ return nil unless arg.present?
8
+
9
+ Array(arg).map { |value| assert(value.to_i) }
10
+ end
11
+
12
+ def assert(number)
13
+ test = number.abs
14
+ raise ConfigurationError, "Out of range: #{NUMBERS.inspect} does not include #{test}" unless NUMBERS.include?(number.abs)
15
+
16
+ number
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,25 @@
1
+ module Montrose
2
+ class YearDay
3
+ class << self
4
+ YDAYS = 1.upto(366).to_a
5
+
6
+ def parse(ydays)
7
+ return nil unless ydays.present?
8
+
9
+ case ydays
10
+ when String
11
+ parse(ydays.split(","))
12
+ else
13
+ Array(ydays).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: #{YDAYS.inspect} does not include #{test}" unless YDAYS.include?(number.abs)
20
+
21
+ number
22
+ end
23
+ end
24
+ end
25
+ end
data/lib/montrose.rb CHANGED
@@ -10,17 +10,29 @@ require "active_support/core_ext/numeric"
10
10
  require "active_support/core_ext/string"
11
11
  require "active_support/core_ext/time"
12
12
 
13
- require "montrose/utils"
14
- require "montrose/rule"
15
- require "montrose/clock"
16
- require "montrose/chainable"
17
- require "montrose/recurrence"
18
- require "montrose/frequency"
19
- require "montrose/schedule"
20
- require "montrose/stack"
21
13
  require "montrose/version"
14
+ require "montrose/errors"
22
15
 
23
16
  module Montrose
17
+ autoload :Chainable, "montrose/chainable"
18
+ autoload :Clock, "montrose/clock"
19
+ autoload :Day, "montrose/day"
20
+ autoload :Frequency, "montrose/frequency"
21
+ autoload :Hour, "montrose/hour"
22
+ autoload :ICal, "montrose/ical"
23
+ autoload :Minute, "montrose/minute"
24
+ autoload :Month, "montrose/month"
25
+ autoload :MonthDay, "montrose/month_day"
26
+ autoload :Options, "montrose/options"
27
+ autoload :Recurrence, "montrose/recurrence"
28
+ autoload :Rule, "montrose/rule"
29
+ autoload :TimeOfDay, "montrose/time_of_day"
30
+ autoload :Schedule, "montrose/schedule"
31
+ autoload :Stack, "montrose/stack"
32
+ autoload :Utils, "montrose/utils"
33
+ autoload :Week, "montrose/week"
34
+ autoload :YearDay, "montrose/year_day"
35
+
24
36
  extend Chainable
25
37
 
26
38
  class << self
data/montrose.gemspec CHANGED
@@ -20,9 +20,9 @@ Gem::Specification.new do |spec|
20
20
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
21
  spec.require_paths = ["lib"]
22
22
 
23
- spec.required_ruby_version = ">= 2.5.0"
23
+ spec.required_ruby_version = ">= 2.6.0"
24
24
 
25
- spec.add_dependency "activesupport", ">= 5.2", "<= 7.0"
25
+ spec.add_dependency "activesupport", ">= 5.2", "< 7.1"
26
26
 
27
27
  spec.add_development_dependency "appraisal"
28
28
  spec.add_development_dependency "m"