montrose 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"