montrose 0.10.0 → 0.12.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -26,7 +26,7 @@ module Montrose
26
26
  if @at
27
27
  times = @at.map { |hour, min, sec = 0| @time.change(hour: hour, min: min, sec: sec) }
28
28
 
29
- min_next = times.select { |t| t > @time }.min and return min_next
29
+ (min_next = times.select { |t| t > @time }.min) && (return min_next)
30
30
 
31
31
  advance_step(times.min || @time)
32
32
  else
@@ -41,7 +41,7 @@ module Montrose
41
41
  end
42
42
 
43
43
  def step
44
- @step ||= smallest_step or fail ConfigurationError, "No step for #{@options.inspect}"
44
+ (@step ||= smallest_step) || fail(ConfigurationError, "No step for #{@options.inspect}")
45
45
  end
46
46
 
47
47
  def smallest_step
@@ -76,9 +76,9 @@ module Montrose
76
76
  is_frequency = @every == unit
77
77
  if ([unit] + alternates).any? { |u| @options.key?(u) } && !is_frequency
78
78
  # smallest unit, increment by 1
79
- { step_key(unit) => 1 }
79
+ {step_key(unit) => 1}
80
80
  elsif is_frequency
81
- { step_key(unit) => @interval }
81
+ {step_key(unit) => @interval}
82
82
  end
83
83
  end
84
84
 
@@ -30,17 +30,17 @@ module Montrose
30
30
  #
31
31
  def self.from_options(opts)
32
32
  frequency = opts.fetch(:every) { fail ConfigurationError, "Please specify the :every option" }
33
- class_name = FREQUENCY_TERMS.fetch(frequency.to_s) do
33
+ class_name = FREQUENCY_TERMS.fetch(frequency.to_s) {
34
34
  fail "Don't know how to enumerate every: #{frequency}"
35
- end
35
+ }
36
36
 
37
37
  Montrose::Frequency.const_get(class_name).new(opts)
38
38
  end
39
39
 
40
40
  # @private
41
41
  def self.assert(frequency)
42
- FREQUENCY_TERMS.key?(frequency.to_s) or fail ConfigurationError,
43
- "Don't know how to enumerate every: #{frequency}"
42
+ FREQUENCY_TERMS.key?(frequency.to_s) || fail(ConfigurationError,
43
+ "Don't know how to enumerate every: #{frequency}")
44
44
 
45
45
  frequency.to_sym
46
46
  end
@@ -86,6 +86,8 @@ module Montrose
86
86
  def_option :starts
87
87
  def_option :until
88
88
  def_option :between
89
+ def_option :covering
90
+ def_option :during
89
91
  def_option :hour
90
92
  def_option :day
91
93
  def_option :mday
@@ -119,12 +121,12 @@ module Montrose
119
121
  end
120
122
 
121
123
  def to_hash
122
- hash_pairs = self.class.defined_options.flat_map do |opt_name|
124
+ hash_pairs = self.class.defined_options.flat_map { |opt_name|
123
125
  [opt_name, send(opt_name)]
124
- end
126
+ }
125
127
  Hash[*hash_pairs].reject { |_k, v| v.nil? }
126
128
  end
127
- alias to_h to_hash
129
+ alias_method :to_h, :to_hash
128
130
 
129
131
  def []=(option, val)
130
132
  send(:"#{option}=", val)
@@ -141,13 +143,13 @@ module Montrose
141
143
  self.class.new(h1.merge(h2))
142
144
  end
143
145
 
144
- def fetch(key, *args, &_block)
145
- fail ArgumentError, "wrong number of arguments (#{args.length} for 1..2)" if args.length > 1
146
+ def fetch(key, *args)
147
+ raise ArgumentError, "wrong number of arguments (#{args.length} for 1..2)" if args.length > 1
146
148
 
147
149
  found = send(key)
148
150
  return found if found
149
151
  return args.first if args.length == 1
150
- fail "Key #{key.inspect} not found" unless block_given?
152
+ raise "Key #{key.inspect} not found" unless block
151
153
 
152
154
  yield
153
155
  end
@@ -164,8 +166,8 @@ module Montrose
164
166
  @every = parsed.fetch(:every)
165
167
  end
166
168
 
167
- alias frequency every
168
- alias frequency= every=
169
+ alias_method :frequency, :every
170
+ alias_method :frequency=, :every=
169
171
 
170
172
  def starts=(time)
171
173
  @starts = normalize_time(as_time(time)) || default_starts
@@ -179,6 +181,15 @@ module Montrose
179
181
  @hour = map_arg(hours) { |h| assert_hour(h) }
180
182
  end
181
183
 
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
+ end
192
+
182
193
  def day=(days)
183
194
  @day = nested_map_arg(days) { |d| day_number!(d) }
184
195
  end
@@ -200,7 +211,9 @@ module Montrose
200
211
  end
201
212
 
202
213
  def between=(range)
203
- @between = range
214
+ if Montrose.enable_deprecated_between_masking?
215
+ @covering = range
216
+ end
204
217
  self[:starts] = range.first unless self[:starts]
205
218
  self[:until] = range.last unless self[:until]
206
219
  end
@@ -302,7 +315,7 @@ module Montrose
302
315
  result[:mday] += map_mdays(v)
303
316
  end
304
317
  else
305
- { day: map_days(arg) }
318
+ {day: map_days(arg)}
306
319
  end
307
320
  end
308
321
 
@@ -313,12 +326,12 @@ module Montrose
313
326
  day = day_number(key)
314
327
  return [:day, day] if day
315
328
 
316
- fail ConfigurationError, "Did not recognize #{key} as a month or day"
329
+ raise ConfigurationError, "Did not recognize #{key} as a month or day"
317
330
  end
318
331
 
319
332
  def assert_range_includes(range, item, absolute = false)
320
333
  test = absolute ? item.abs : item
321
- fail ConfigurationError, "Out of range: #{range.inspect} does not include #{test}" unless range.include?(test)
334
+ raise ConfigurationError, "Out of range: #{range.inspect} does not include #{test}" unless range.include?(test)
322
335
 
323
336
  item
324
337
  end
@@ -333,18 +346,18 @@ module Montrose
333
346
  def parse_frequency(input)
334
347
  if input.respond_to?(:parts)
335
348
  frequency, interval = duration_to_frequency_parts(input)
336
- { every: frequency.to_s.singularize.to_sym, interval: interval }
349
+ {every: frequency.to_s.singularize.to_sym, interval: interval}
337
350
  elsif input.is_a?(Numeric)
338
351
  frequency, interval = numeric_to_frequency_parts(input)
339
- { every: frequency, interval: interval }
352
+ {every: frequency, interval: interval}
340
353
  else
341
- { every: Frequency.assert(input) }
354
+ {every: Frequency.assert(input)}
342
355
  end
343
356
  end
344
357
 
345
358
  def numeric_to_frequency_parts(number)
346
359
  parts = nil
347
- [:year, :month, :week, :day, :hour, :minute].each do |freq|
360
+ %i[year month week day hour minute].each do |freq|
348
361
  div, mod = number.divmod(1.send(freq))
349
362
  parts = [freq, div]
350
363
  return parts if mod.zero?
@@ -355,5 +368,16 @@ module Montrose
355
368
  def duration_to_frequency_parts(duration)
356
369
  duration.parts.first
357
370
  end
371
+
372
+ def decompose_during_arg(during)
373
+ case during
374
+ when Range
375
+ [decompose_during_arg(during.first), decompose_during_arg(during.last)]
376
+ when String
377
+ during.split(/[-—–]/).map { |d| as_time_parts(d) }
378
+ else
379
+ as_time_parts(during)
380
+ end
381
+ end
358
382
  end
359
383
  end
@@ -238,7 +238,7 @@ module Montrose
238
238
  else
239
239
  fail SerializationError,
240
240
  "Object was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}"
241
- end
241
+ end
242
242
 
243
243
  JSON.dump(hash)
244
244
  end
@@ -308,7 +308,7 @@ module Montrose
308
308
  def to_hash
309
309
  default_options.to_hash
310
310
  end
311
- alias to_h to_hash
311
+ alias_method :to_h, :to_hash
312
312
 
313
313
  # Returns json string of options used to create the recurrence
314
314
  #
@@ -346,20 +346,41 @@ module Montrose
346
346
  def include?(timestamp)
347
347
  return false if earlier?(timestamp) || later?(timestamp)
348
348
 
349
- recurrence = finite? ? self : starts(timestamp)
349
+ recurrence = finite? ? self : fast_forward(timestamp)
350
350
 
351
351
  recurrence.events.lazy.each do |event|
352
352
  return true if event == timestamp
353
353
  return false if event > timestamp
354
- end or false
354
+ end || false
355
355
  end
356
356
 
357
- # Return true/false if recurrence will iterate infinitely
357
+ def fast_forward(timestamp)
358
+ return starts(timestamp) unless starts_at.present?
359
+
360
+ interval = default_options[:interval]
361
+ frequency = default_options[:every]
362
+ duration = interval.send(frequency)
363
+
364
+ # Calculate nearest earlier time ahead matching frequency * interval
365
+ jump = ((timestamp - starts_at) / duration).floor * duration
366
+
367
+ starts(starts_at + jump)
368
+ end
369
+
370
+ # Return true/false if recurrence will terminate
358
371
  #
359
- # @return [Boolean] whether or not recurrence is infinite
372
+ # @return [Boolean] returns true if recurrence has an end
360
373
  #
361
374
  def finite?
362
- ends_at || length
375
+ !infinite?
376
+ end
377
+
378
+ # Return true/false if recurrence will iterate infinitely
379
+ #
380
+ # @return [Boolean] returns true if recurrence has no end
381
+ #
382
+ def infinite?
383
+ !ends_at && !length
363
384
  end
364
385
 
365
386
  # Return true/false if given timestamp occurs before
@@ -391,7 +412,7 @@ module Montrose
391
412
  loop do
392
413
  stack.advance(clock.tick) do |time|
393
414
  yielder << time
394
- end or break
415
+ end || break
395
416
  end
396
417
  end
397
418
  end
data/lib/montrose/rule.rb CHANGED
@@ -36,10 +36,11 @@ module Montrose
36
36
  end
37
37
 
38
38
  require "montrose/rule/after"
39
- require "montrose/rule/between"
39
+ require "montrose/rule/covering"
40
40
  require "montrose/rule/day_of_month"
41
41
  require "montrose/rule/day_of_week"
42
42
  require "montrose/rule/day_of_year"
43
+ require "montrose/rule/during"
43
44
  require "montrose/rule/except"
44
45
  require "montrose/rule/hour_of_day"
45
46
  require "montrose/rule/month_of_year"
@@ -22,7 +22,7 @@ module Montrose
22
22
  end
23
23
 
24
24
  def continue?(time)
25
- @between.max > time
25
+ time < @between.max
26
26
  end
27
27
  end
28
28
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Montrose
4
+ module Rule
5
+ class Covering
6
+ include Montrose::Rule
7
+
8
+ def self.apply_options(opts)
9
+ opts[:covering].is_a?(Range) && opts[:covering]
10
+ end
11
+
12
+ # Initializes rule
13
+ #
14
+ # @param [Range] covering - timestamp range
15
+ #
16
+ def initialize(covering)
17
+ @covering = case covering.first
18
+ when Date
19
+ DateRange.new(covering)
20
+ else
21
+ covering
22
+ end
23
+ end
24
+
25
+ def include?(time)
26
+ @covering.include?(time)
27
+ end
28
+
29
+ def continue?(time)
30
+ time < @covering.last
31
+ end
32
+
33
+ class DateRange < SimpleDelegator
34
+ def include?(time)
35
+ __getobj__.include?(time.to_date)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Montrose
4
+ module Rule
5
+ class During
6
+ include Montrose::Rule
7
+
8
+ def self.apply_options(opts)
9
+ opts[:during]
10
+ end
11
+
12
+ # Initializes rule
13
+ #
14
+ # @param during [Array<Array<Fixnum>>] array of time parts arrays, e.g. [[9, 0, 0], [17, 0, 0]], i.e., "9 to 5"
15
+ #
16
+ def initialize(during)
17
+ @during = during.map { |first, last| TimeOfDayRange.new(first, last) }
18
+ end
19
+
20
+ def include?(time)
21
+ @during.any? { |range| range.include?(time) }
22
+ end
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
+ class TimeOfDayRange
37
+ def initialize(first, last, exclude_end: false)
38
+ @first = TimeOfDay.new(*first)
39
+ @last = TimeOfDay.new(*last)
40
+ @exclude_end = exclude_end
41
+ end
42
+
43
+ def include?(time)
44
+ range.include?(time.seconds_since_midnight.to_i)
45
+ end
46
+
47
+ private
48
+
49
+ def range
50
+ @range ||= Range.new(@first.seconds_since_midnight, @last.seconds_since_midnight, @exclude_end)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -8,7 +8,7 @@ module Montrose
8
8
  def self.apply_options(opts)
9
9
  return false unless opts[:until]
10
10
 
11
- { until: opts[:until], exclude_end: opts.fetch(:exclude_end, false) }
11
+ {until: opts[:until], exclude_end: opts.fetch(:exclude_end, false)}
12
12
  end
13
13
 
14
14
  def initialize(opts)
@@ -8,27 +8,56 @@ module Montrose
8
8
  # @attr_reader [Array] rules the list of recurrences
9
9
  #
10
10
  class Schedule
11
+ include Enumerable
12
+
11
13
  attr_accessor :rules
12
14
 
13
- # Instantiates a schedule and yields the instance to an optional
14
- # block for building recurrences inline
15
- #
16
- # @example Build a schedule with multiple rules added in the given block
17
- # schedule = Montrose::Schedule.build do |s|
18
- # s << { every: :day }
19
- # s << { every: :year }
20
- # end
21
- #
22
- # @return [Montrose::Schedule]
23
- #
24
- def self.build
25
- schedule = new
26
- yield schedule if block_given?
27
- schedule
15
+ class << self
16
+ # Instantiates a schedule and yields the instance to an optional
17
+ # block for building recurrences inline
18
+ #
19
+ # @example Build a schedule with multiple rules added in the given block
20
+ # schedule = Montrose::Schedule.build do |s|
21
+ # s << { every: :day }
22
+ # s << { every: :year }
23
+ # end
24
+ #
25
+ # @return [Montrose::Schedule]
26
+ #
27
+ def build
28
+ schedule = new
29
+ yield schedule if block_given?
30
+ schedule
31
+ end
32
+
33
+ def dump(obj)
34
+ return nil if obj.nil?
35
+ return dump(load(obj)) if obj.is_a?(String)
36
+
37
+ array = case obj
38
+ when Array
39
+ new(obj).to_a
40
+ when self
41
+ obj.to_a
42
+ else
43
+ fail SerializationError,
44
+ "Object was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}"
45
+ end
46
+
47
+ JSON.dump(array)
48
+ end
49
+
50
+ def load(json)
51
+ return nil if json.blank?
52
+
53
+ new JSON.parse(json)
54
+ rescue JSON::ParserError => e
55
+ fail SerializationError, "Could not parse JSON: #{e}"
56
+ end
28
57
  end
29
58
 
30
- def initialize
31
- @rules = []
59
+ def initialize(rules = [])
60
+ @rules = rules.map { |rule| Montrose::Recurrence.new(rule) }
32
61
  end
33
62
 
34
63
  # Add a recurrence rule to the schedule, either by hash or recurrence
@@ -48,7 +77,7 @@ module Montrose
48
77
 
49
78
  self
50
79
  end
51
- alias add <<
80
+ alias_method :add, :<<
52
81
 
53
82
  # Return true/false if given timestamp is included in any of the rules
54
83
  # found in the schedule
@@ -59,6 +88,31 @@ module Montrose
59
88
  @rules.any? { |r| r.include?(timestamp) }
60
89
  end
61
90
 
91
+ # Iterate over the events of a recurrence. Along with the Enumerable
92
+ # module, this makes Montrose occurrences enumerable like other Ruby
93
+ # collections
94
+ #
95
+ # @example Iterate over a finite recurrence
96
+ # schedule = Montrose::Schedule.build do |s|
97
+ # s << { every: :day }
98
+ # end
99
+ # schedule.each do |event|
100
+ # puts event
101
+ # end
102
+ #
103
+ # @example Iterate over an infinite recurrence
104
+ # schedule = Montrose::Schedule.build do |s|
105
+ # s << { every: :day }
106
+ # end
107
+ # schedule.lazy.each do |event|
108
+ # puts event
109
+ # end
110
+ #
111
+ # @return [Enumerator] an enumerator of recurrence timestamps
112
+ def each(&block)
113
+ events.each(&block)
114
+ end
115
+
62
116
  # Returns an enumerator for iterating over timestamps in the schedule
63
117
  #
64
118
  # @example Return the events
@@ -73,21 +127,55 @@ module Montrose
73
127
  enums = @rules.map { |r| r.merge(opts).events }
74
128
  Enumerator.new do |y|
75
129
  loop do
76
- enum = active_enums(enums).min_by(&:peek) or break
130
+ (enum = active_enums(enums).min_by(&:peek)) || break
77
131
  y << enum.next
78
132
  end
79
133
  end
80
134
  end
81
135
 
136
+ # Returns an array of the options used to create the recurrence
137
+ #
138
+ # @return [Array] array of hashes of recurrence options
139
+ #
140
+ def to_a
141
+ @rules.map(&:to_hash)
142
+ end
143
+
144
+ # Returns json string of options used to create the schedule
145
+ #
146
+ # @return [String] json of schedule recurrences
147
+ #
148
+ def to_json(*args)
149
+ JSON.dump(to_a, *args)
150
+ end
151
+
152
+ # Returns json array of options used to create the schedule
153
+ #
154
+ # @return [Array] json of schedule recurrence options
155
+ #
156
+ def as_json(*args)
157
+ to_a.as_json(*args)
158
+ end
159
+
160
+ # Returns options used to create the schedule recurrences in YAML format
161
+ #
162
+ # @return [String] YAML-formatted schedule recurrence options
163
+ #
164
+ def to_yaml(*args)
165
+ YAML.dump(JSON.parse(to_json(*args)))
166
+ end
167
+
168
+ def inspect
169
+ "#<#{self.class}:#{object_id.to_s(16)} #{to_a.inspect}>"
170
+ end
171
+
82
172
  private
83
173
 
84
174
  def active_enums(enums)
85
175
  enums.select do |e|
86
- begin
87
- e.peek
88
- rescue StopIteration
89
- false
90
- end
176
+ e.peek
177
+ rescue StopIteration
178
+ false
91
179
  end
92
180
  end
93
181
  end