montrose 0.10.1 → 0.11.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6375eeb97c6ac8c0ea36b6be0b5281b4ffeceb5fa6fa591f4aab7c801240fcc
4
- data.tar.gz: 903145e001fb0ddd6a4dff5f7cf9a9e0583e9d2242a1b00283e1a3de3d5a03ce
3
+ metadata.gz: 01fee4660a2a1e95d60de269d491675460289ae52f6c582aee5ea6be39a883cc
4
+ data.tar.gz: ece2fbd3f36909337abd6712025053385f6ceedf95695a7852b6e05b6c804906
5
5
  SHA512:
6
- metadata.gz: 9598d8bc00e29c9adbf8b1d9323dcd02a2dc7aa54ae6d6a25ccc61ab5ffdc9dc1f0c8a390732bf6a1a8181b287fbee5a11c2c4b399b6cc81871276c374391baf
7
- data.tar.gz: 74813535d8ec8248ee87e0ce2aa500a3e85722a0ad06b310b8de62d50fadc1819cd914046b04c4a0bd9fc6e76217de62510fa9a7af60ebe63d80c2b7e9fdd63b
6
+ metadata.gz: bb823d11097cd9228ec7631809bece2b1028adf728c7a4919284dfc7a6fcafa3d65c9d1ed446f60f1a16335143830ae36b4725c6d980b5cb6a152b80961f4e0b
7
+ data.tar.gz: deb0db72983faf47192095aef16e7512f928c4ff4f1f6fd20f913394850b354fc2e004dcda70455d08abe4471a2647d8b3a31832814f737cf68a09d6d9ffbaa8
@@ -1,3 +1,15 @@
1
+ ### 0.11.0 - (2019-08-16)
2
+
3
+ * enhancements
4
+ * Adds `Recurrence#during` to support recurrences within time-of-day ranges,
5
+ e.g. `Montrose.every(20.minutes).during("9am-5pm")`
6
+
7
+ ### 0.10.1 - (2019-07-22)
8
+
9
+ * enhancements
10
+ * Adds `Schedule.dump` and `Schedule.load` to support ActiveRecord column
11
+ serialization
12
+
1
13
  ### 0.10.0 - (2019-07-17)
2
14
 
3
15
  * enhancements
@@ -61,7 +73,7 @@
61
73
  * Previously, the :between option served as a shorthand for :starts to :until.
62
74
  Now, when both :starts and :between are provided, the recurrence will behave
63
75
  as if anchored by the given :starts option, but filtered through the given
64
- :betweeen option.
76
+ :between option.
65
77
  * The :exclude_end option changes the default behavior of :until--when the
66
78
  timestamp of the interval matches the :until timestamp, it will be included
67
79
  by default unless the :exclude_end option is set to true.
data/README.md CHANGED
@@ -277,6 +277,15 @@ Montrose.every(90.minutes, total: 4)
277
277
  # every 20 minutes from 9:00 AM to 4:40 PM every day
278
278
  Montrose.every(20.minutes, hour: 9..16)
279
279
 
280
+ # every 20 minutes from 9:00 AM to 4:40 PM every day with time-of-day precision
281
+ r = Montrose.every(20.minutes)
282
+ r.during("9am-4:40pm") # as semantic time-of-day range OR
283
+ r.during(time.change(hour: 9)..time.change(hour: 4: min: 40)) # as ruby time range OR
284
+ r.during([9, 0, 0], [16, 40, 0]) # as hour, min, sec tuple pairs for start, end
285
+
286
+ # every 20 minutes during multiple time-of-day ranges
287
+ Montrose.every(20.minutes).during("9am-12pm", "1pm-5pm")
288
+
280
289
  # Minutely
281
290
  Montrose.minutely
282
291
  Montrose.r(every: :minute)
@@ -366,6 +375,40 @@ r.events.take(10).each { |date| puts date.to_s }
366
375
  r.events.lazy.select { |time| time > 1.month.from_now }.take(3).each { |date| puts date.to_s }
367
376
  ```
368
377
 
378
+ ### Combining recurrences
379
+
380
+ It may be necessary to combine several recurrence rules into a single
381
+ enumeration of events. For this purpose, there is `Montrose::Schedule`. To create a schedule of multiple recurrences:
382
+
383
+ ```ruby
384
+ recurrence_1 = Montrose.monthly(day: { friday: [1] })
385
+ recurrence_2 = Montrose.weekly(on: :tuesday)
386
+
387
+ schedule = Montrose::Schedule.build do |s|
388
+ s << recurrence_1
389
+ s << recurrence_2
390
+ end
391
+
392
+ # add after building
393
+ s << Montrose.yearly
394
+ ```
395
+ The `Schedule#<<` method also accepts valid recurrence options as hashes:
396
+ ```ruby
397
+ schedule = Montrose::Schedule.build do |s|
398
+ s << { day: { friday: [1] } }
399
+ s << { on: :tuesday }
400
+ end
401
+ ```
402
+ A schedule acts like a collection of recurrence rules that also behaves as a single
403
+ stream of events:
404
+
405
+ ```ruby
406
+ schedule.events # => #<Enumerator: ...>
407
+ schedule.each do |event|
408
+ puts event
409
+ end
410
+ ```
411
+
369
412
  ### Ruby on Rails
370
413
 
371
414
  Instances of `Montrose::Recurrence` support the ActiveRecord serialization API so recurrence objects can be marshalled to and from a single database column:
@@ -374,6 +417,13 @@ Instances of `Montrose::Recurrence` support the ActiveRecord serialization API s
374
417
  class RecurringEvent < ApplicationRecord
375
418
  serialize :recurrence, Montrose::Recurrence
376
419
 
420
+ end
421
+ ```
422
+ `Montrose::Schedule` can also be serialized:
423
+ ```ruby
424
+ class RecurringEvent < ApplicationRecord
425
+ serialize :recurrence, Montrose::Schedule
426
+
377
427
  end
378
428
  ```
379
429
 
@@ -168,6 +168,23 @@ module Montrose
168
168
  merge(between: date_range)
169
169
  end
170
170
 
171
+ # Create a recurrence occurring within a time-of-day range or ranges.
172
+ # Given time ranges will parse as times-of-day and ignore given dates.
173
+ #
174
+ # @param [Range<Time>,String,Array<Array>] time-of-day range(s)
175
+ #
176
+ # @example
177
+ # Montrose.every(20.minutes).during("9am-5pm")
178
+ # Montrose.every(20.minutes).during(time.change(hour: 9)..time.change(hour: 5))
179
+ # Montrose.every(20.minutes).during([9, 0, 0], [17, 0, 0])
180
+ # Montrose.every(20.minutes).during("9am-12pm", "1pm-5pm")
181
+ #
182
+ # @return [Montrose::Recurrence]
183
+ #
184
+ def during(time_of_day, *extras)
185
+ merge(during: time_of_day.array_concat(extras))
186
+ end
187
+
171
188
  # Create a recurrence through :on option
172
189
  #
173
190
  # @param day [Hash,Symbol] weekday or day of month as hash, e.g. { friday: 13 }
@@ -86,6 +86,7 @@ module Montrose
86
86
  def_option :starts
87
87
  def_option :until
88
88
  def_option :between
89
+ def_option :during
89
90
  def_option :hour
90
91
  def_option :day
91
92
  def_option :mday
@@ -179,6 +180,15 @@ module Montrose
179
180
  @hour = map_arg(hours) { |h| assert_hour(h) }
180
181
  end
181
182
 
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
190
+ end
191
+
182
192
  def day=(days)
183
193
  @day = nested_map_arg(days) { |d| day_number!(d) }
184
194
  end
@@ -355,5 +365,16 @@ module Montrose
355
365
  def duration_to_frequency_parts(duration)
356
366
  duration.parts.first
357
367
  end
368
+
369
+ def decompose_during_arg(during)
370
+ case during
371
+ when Range
372
+ [decompose_during_arg(during.first), decompose_during_arg(during.last)]
373
+ when String
374
+ during.split(%r{[-—–]}).map { |d| as_time_parts(d) }
375
+ else
376
+ as_time_parts(during)
377
+ end
378
+ end
358
379
  end
359
380
  end
@@ -40,6 +40,7 @@ require "montrose/rule/between"
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"
@@ -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,6 +8,8 @@ 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
15
  class << self
@@ -86,6 +88,31 @@ module Montrose
86
88
  @rules.any? { |r| r.include?(timestamp) }
87
89
  end
88
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
+
89
116
  # Returns an enumerator for iterating over timestamps in the schedule
90
117
  #
91
118
  # @example Return the events
@@ -106,10 +133,42 @@ module Montrose
106
133
  end
107
134
  end
108
135
 
136
+ # Returns an array of the options used to create the recurrence
137
+ #
138
+ # @return [Array] array of hashes of recurrence options
139
+ #
109
140
  def to_a
110
141
  @rules.map(&:to_hash)
111
142
  end
112
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
+
113
172
  private
114
173
 
115
174
  def active_enums(enums)
@@ -14,6 +14,7 @@ module Montrose
14
14
  Rule::After,
15
15
  Rule::Until,
16
16
  Rule::Between,
17
+ Rule::During,
17
18
  Rule::Except,
18
19
  Rule::Total,
19
20
  Rule::TimeOfDay,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Montrose
4
- VERSION = "0.10.1"
4
+ VERSION = "0.11.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: montrose
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.1
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ross Kaffenberger
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-07-22 00:00:00.000000000 Z
11
+ date: 2019-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -169,6 +169,7 @@ files:
169
169
  - lib/montrose/rule/day_of_month.rb
170
170
  - lib/montrose/rule/day_of_week.rb
171
171
  - lib/montrose/rule/day_of_year.rb
172
+ - lib/montrose/rule/during.rb
172
173
  - lib/montrose/rule/except.rb
173
174
  - lib/montrose/rule/hour_of_day.rb
174
175
  - lib/montrose/rule/month_of_year.rb
@@ -203,7 +204,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
203
204
  - !ruby/object:Gem::Version
204
205
  version: '0'
205
206
  requirements: []
206
- rubygems_version: 3.0.3
207
+ rubyforge_project:
208
+ rubygems_version: 2.7.6
207
209
  signing_key:
208
210
  specification_version: 4
209
211
  summary: Recurring events in Ruby