montrose 0.1.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 +7 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +113 -0
- data/.travis.yml +5 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/Gemfile +13 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +21 -0
- data/README.md +191 -0
- data/Rakefile +21 -0
- data/bin/_guard-core +16 -0
- data/bin/console +7 -0
- data/bin/guard +16 -0
- data/bin/m +16 -0
- data/bin/rake +16 -0
- data/bin/rubocop +16 -0
- data/bin/setup +7 -0
- data/lib/montrose.rb +20 -0
- data/lib/montrose/chainable.rb +210 -0
- data/lib/montrose/clock.rb +77 -0
- data/lib/montrose/errors.rb +5 -0
- data/lib/montrose/frequency.rb +63 -0
- data/lib/montrose/frequency/daily.rb +9 -0
- data/lib/montrose/frequency/hourly.rb +9 -0
- data/lib/montrose/frequency/minutely.rb +9 -0
- data/lib/montrose/frequency/monthly.rb +9 -0
- data/lib/montrose/frequency/weekly.rb +19 -0
- data/lib/montrose/frequency/yearly.rb +9 -0
- data/lib/montrose/options.rb +293 -0
- data/lib/montrose/recurrence.rb +67 -0
- data/lib/montrose/rule.rb +47 -0
- data/lib/montrose/rule/after.rb +27 -0
- data/lib/montrose/rule/before.rb +23 -0
- data/lib/montrose/rule/day_of_month.rb +31 -0
- data/lib/montrose/rule/day_of_week.rb +23 -0
- data/lib/montrose/rule/day_of_year.rb +37 -0
- data/lib/montrose/rule/hour_of_day.rb +23 -0
- data/lib/montrose/rule/month_of_year.rb +23 -0
- data/lib/montrose/rule/nth_day_matcher.rb +32 -0
- data/lib/montrose/rule/nth_day_of_month.rb +63 -0
- data/lib/montrose/rule/nth_day_of_year.rb +63 -0
- data/lib/montrose/rule/time_of_day.rb +33 -0
- data/lib/montrose/rule/total.rb +29 -0
- data/lib/montrose/rule/week_of_year.rb +23 -0
- data/lib/montrose/schedule.rb +42 -0
- data/lib/montrose/stack.rb +51 -0
- data/lib/montrose/utils.rb +32 -0
- data/lib/montrose/version.rb +3 -0
- data/montrose.gemspec +32 -0
- metadata +192 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module Montrose
|
2
|
+
class Frequency
|
3
|
+
class Weekly < Frequency
|
4
|
+
def include?(time)
|
5
|
+
weeks_since_start(time) % @interval == 0
|
6
|
+
end
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def weeks_since_start(time)
|
11
|
+
((time.beginning_of_week - base_date) / 1.week).round
|
12
|
+
end
|
13
|
+
|
14
|
+
def base_date
|
15
|
+
@starts.beginning_of_week
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,293 @@
|
|
1
|
+
module Montrose
|
2
|
+
class Options
|
3
|
+
@default_starts = nil
|
4
|
+
@default_until = nil
|
5
|
+
@default_every = nil
|
6
|
+
|
7
|
+
MAX_HOURS_IN_DAY = 24
|
8
|
+
MAX_DAYS_IN_YEAR = 366
|
9
|
+
MAX_WEEKS_IN_YEAR = 53
|
10
|
+
MAX_DAYS_IN_MONTH = 31
|
11
|
+
|
12
|
+
class << self
|
13
|
+
def new(options = {})
|
14
|
+
return options if options.is_a?(self)
|
15
|
+
super
|
16
|
+
end
|
17
|
+
|
18
|
+
def defined_options
|
19
|
+
@defined_options ||= []
|
20
|
+
end
|
21
|
+
|
22
|
+
def def_option(name)
|
23
|
+
defined_options << name.to_sym
|
24
|
+
attr_accessor name
|
25
|
+
protected :"#{name}="
|
26
|
+
end
|
27
|
+
|
28
|
+
attr_accessor :default_starts, :default_until, :default_every
|
29
|
+
|
30
|
+
# Return the default ending time.
|
31
|
+
#
|
32
|
+
# @example Recurrence.default_until #=> <Date>
|
33
|
+
#
|
34
|
+
def default_until
|
35
|
+
case @default_until
|
36
|
+
when String
|
37
|
+
Time.parse(@default_until)
|
38
|
+
when Proc
|
39
|
+
@default_until.call
|
40
|
+
else
|
41
|
+
@default_until
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Return the default starting time.
|
46
|
+
#
|
47
|
+
# @example Recurrence.default_starts #=> <Date>
|
48
|
+
#
|
49
|
+
def default_starts
|
50
|
+
case @default_starts
|
51
|
+
when String
|
52
|
+
Time.parse(@default_starts)
|
53
|
+
when Proc
|
54
|
+
@default_starts.call
|
55
|
+
when nil
|
56
|
+
Time.now
|
57
|
+
else
|
58
|
+
@default_starts
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def_option :every
|
64
|
+
def_option :starts
|
65
|
+
def_option :until
|
66
|
+
def_option :hour
|
67
|
+
def_option :day
|
68
|
+
def_option :mday
|
69
|
+
def_option :yday
|
70
|
+
def_option :week
|
71
|
+
def_option :month
|
72
|
+
def_option :interval
|
73
|
+
def_option :total
|
74
|
+
def_option :between
|
75
|
+
def_option :at
|
76
|
+
def_option :on
|
77
|
+
|
78
|
+
def initialize(opts = {})
|
79
|
+
defaults = {
|
80
|
+
every: self.class.default_every,
|
81
|
+
starts: self.class.default_starts,
|
82
|
+
until: self.class.default_until,
|
83
|
+
interval: 1,
|
84
|
+
day: nil,
|
85
|
+
mday: nil,
|
86
|
+
yday: nil,
|
87
|
+
week: nil,
|
88
|
+
month: nil,
|
89
|
+
total: nil
|
90
|
+
}
|
91
|
+
|
92
|
+
options = defaults.merge(opts)
|
93
|
+
options.each { |(k, v)| self[k] ||= v unless v.nil? }
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_hash
|
97
|
+
hash_pairs = self.class.defined_options.flat_map do |opt_name|
|
98
|
+
[opt_name, send(opt_name)]
|
99
|
+
end
|
100
|
+
Hash[*hash_pairs].reject { |_k, v| v.nil? }
|
101
|
+
end
|
102
|
+
|
103
|
+
def []=(option, val)
|
104
|
+
send(:"#{option}=", val)
|
105
|
+
end
|
106
|
+
|
107
|
+
def [](option)
|
108
|
+
send(:"#{option}")
|
109
|
+
end
|
110
|
+
|
111
|
+
def merge(other)
|
112
|
+
h1 = to_hash
|
113
|
+
h2 = other.to_hash
|
114
|
+
|
115
|
+
self.class.new(h1.merge(h2))
|
116
|
+
end
|
117
|
+
|
118
|
+
def fetch(key, *args, &_block)
|
119
|
+
fail ArgumentError, "wrong number of arguments (#{args.length} for 1..2)" if args.length > 1
|
120
|
+
found = send(key)
|
121
|
+
return found if found
|
122
|
+
return args.first if args.length == 1
|
123
|
+
fail "Key #{key.inspect} not found" unless block_given?
|
124
|
+
|
125
|
+
yield
|
126
|
+
end
|
127
|
+
|
128
|
+
def key?(key)
|
129
|
+
respond_to?(key) && !send(key).nil?
|
130
|
+
end
|
131
|
+
|
132
|
+
def every=(arg)
|
133
|
+
parsed = parse_frequency(arg)
|
134
|
+
|
135
|
+
self[:interval] = parsed[:interval] if parsed[:interval]
|
136
|
+
|
137
|
+
@every = parsed.fetch(:every)
|
138
|
+
end
|
139
|
+
|
140
|
+
def starts=(time)
|
141
|
+
@starts = as_time(time) || self.class.default_starts
|
142
|
+
end
|
143
|
+
|
144
|
+
def until=(time)
|
145
|
+
@until = as_time(time) || self.class.default_until
|
146
|
+
end
|
147
|
+
|
148
|
+
def hour=(hours)
|
149
|
+
@hour = map_arg(hours) { |h| assert_hour(h) }
|
150
|
+
end
|
151
|
+
|
152
|
+
def day=(days)
|
153
|
+
@day = nested_map_arg(days) { |d| Montrose::Utils.day_number(d) }
|
154
|
+
end
|
155
|
+
|
156
|
+
def mday=(mdays)
|
157
|
+
@mday = map_mdays(mdays)
|
158
|
+
end
|
159
|
+
|
160
|
+
def yday=(ydays)
|
161
|
+
@yday = map_ydays(ydays)
|
162
|
+
end
|
163
|
+
|
164
|
+
def week=(weeks)
|
165
|
+
@week = map_arg(weeks) { |w| assert_week(w) }
|
166
|
+
end
|
167
|
+
|
168
|
+
def month=(months)
|
169
|
+
@month = map_arg(months) { |d| Montrose::Utils.month_number(d) }
|
170
|
+
end
|
171
|
+
|
172
|
+
def between=(range)
|
173
|
+
self[:starts] = range.first
|
174
|
+
self[:until] = range.last
|
175
|
+
end
|
176
|
+
|
177
|
+
def between
|
178
|
+
return nil unless self[:starts] && self[:until]
|
179
|
+
|
180
|
+
(self[:starts]..self[:until])
|
181
|
+
end
|
182
|
+
|
183
|
+
def at=(time)
|
184
|
+
times = map_arg(time) { |t| as_time(t) }
|
185
|
+
now = Time.now
|
186
|
+
first = times.map { |t| t < now ? t + 24.hours : t }.min
|
187
|
+
self[:starts] = first if first
|
188
|
+
@at = times
|
189
|
+
end
|
190
|
+
|
191
|
+
def on=(arg)
|
192
|
+
wday, mday = assert_wday_mday(arg)
|
193
|
+
self[:day] = wday
|
194
|
+
self[:mday] = mday if mday
|
195
|
+
@on = arg
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def nested_map_arg(arg, &block)
|
201
|
+
case arg
|
202
|
+
when Hash
|
203
|
+
arg.each_with_object({}) do |(k, v), hash|
|
204
|
+
hash[yield k] = [*v]
|
205
|
+
end
|
206
|
+
else
|
207
|
+
map_arg(arg, &block)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def map_arg(arg, &block)
|
212
|
+
return nil unless arg
|
213
|
+
|
214
|
+
Array(arg).map(&block)
|
215
|
+
end
|
216
|
+
|
217
|
+
def map_days(arg)
|
218
|
+
map_arg(arg) { |d| Montrose::Utils.day_number(d) }
|
219
|
+
end
|
220
|
+
|
221
|
+
def map_mdays(arg)
|
222
|
+
map_arg(arg) { |d| assert_mday(d) }
|
223
|
+
end
|
224
|
+
|
225
|
+
def map_ydays(arg)
|
226
|
+
map_arg(arg) { |d| assert_yday(d) }
|
227
|
+
end
|
228
|
+
|
229
|
+
def assert_hour(hour)
|
230
|
+
assert_range_includes(1..MAX_HOURS_IN_DAY, hour)
|
231
|
+
end
|
232
|
+
|
233
|
+
def assert_mday(mday)
|
234
|
+
assert_range_includes(1..MAX_DAYS_IN_MONTH, mday, :absolute)
|
235
|
+
end
|
236
|
+
|
237
|
+
def assert_yday(yday)
|
238
|
+
assert_range_includes(1..MAX_DAYS_IN_YEAR, yday, :absolute)
|
239
|
+
end
|
240
|
+
|
241
|
+
def assert_week(week)
|
242
|
+
assert_range_includes(1..MAX_WEEKS_IN_YEAR, week, :absolute)
|
243
|
+
end
|
244
|
+
|
245
|
+
def assert_wday_mday(arg)
|
246
|
+
case arg
|
247
|
+
when Hash
|
248
|
+
[map_days(arg.keys), map_mdays(*arg.values)]
|
249
|
+
else
|
250
|
+
map_days(arg)
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def assert_range_includes(range, item, absolute = false)
|
255
|
+
test = absolute ? item.abs : item
|
256
|
+
fail ConfigurationError, "Out of range" unless range.include?(test)
|
257
|
+
|
258
|
+
item
|
259
|
+
end
|
260
|
+
|
261
|
+
def as_time(time)
|
262
|
+
return nil unless time
|
263
|
+
|
264
|
+
case
|
265
|
+
when time.is_a?(String)
|
266
|
+
Time.parse(time)
|
267
|
+
when time.respond_to?(:to_time)
|
268
|
+
time.to_time
|
269
|
+
else
|
270
|
+
Array(time).flat_map { |d| as_time(d) }
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def parse_frequency(input)
|
275
|
+
if input.is_a?(Numeric)
|
276
|
+
frequency, interval = duration_to_frequency_parts(input)
|
277
|
+
{ every: frequency, interval: interval }
|
278
|
+
else
|
279
|
+
{ every: Frequency.assert(input) }
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def duration_to_frequency_parts(duration)
|
284
|
+
parts = nil
|
285
|
+
[:year, :month, :week, :day, :hour, :minute].each do |freq|
|
286
|
+
div, mod = duration.divmod(1.send(freq))
|
287
|
+
parts = [freq, div]
|
288
|
+
return parts if mod.zero?
|
289
|
+
end
|
290
|
+
parts
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require "json"
|
2
|
+
require "montrose/chainable"
|
3
|
+
require "montrose/errors"
|
4
|
+
require "montrose/stack"
|
5
|
+
require "montrose/clock"
|
6
|
+
|
7
|
+
module Montrose
|
8
|
+
class Recurrence
|
9
|
+
include Chainable
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
attr_reader :default_options
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def new(options = {})
|
16
|
+
return options if options.is_a?(self)
|
17
|
+
super
|
18
|
+
end
|
19
|
+
|
20
|
+
def dump(obj)
|
21
|
+
return nil if obj.nil?
|
22
|
+
unless obj.is_a?(self)
|
23
|
+
fail SerializationError,
|
24
|
+
"Object was supposed to be a #{self}, but was a #{obj.class}. -- #{obj.inspect}"
|
25
|
+
end
|
26
|
+
|
27
|
+
JSON.dump(obj.to_hash)
|
28
|
+
end
|
29
|
+
|
30
|
+
def load(json)
|
31
|
+
new JSON.load(json)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(opts = {})
|
36
|
+
@default_options = Montrose::Options.new(opts)
|
37
|
+
end
|
38
|
+
|
39
|
+
def events
|
40
|
+
event_enum
|
41
|
+
end
|
42
|
+
|
43
|
+
def each(&block)
|
44
|
+
events.each(&block)
|
45
|
+
end
|
46
|
+
|
47
|
+
def to_hash
|
48
|
+
default_options.to_hash
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def event_enum
|
54
|
+
opts = @default_options
|
55
|
+
stack = Stack.new(opts)
|
56
|
+
clock = Clock.new(opts)
|
57
|
+
|
58
|
+
Enumerator.new do |yielder|
|
59
|
+
loop do
|
60
|
+
stack.advance(clock.tick) do |time|
|
61
|
+
yielder << time
|
62
|
+
end or break
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Montrose
|
2
|
+
# Defines the Rule duck type for recurrence rules
|
3
|
+
module Rule
|
4
|
+
def self.included(base)
|
5
|
+
base.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
def include?(_time)
|
9
|
+
fail "Class must implement #{__method__}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def advance!(_time)
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def continue?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
def apply_option(_opts)
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def apply_options?(opts)
|
26
|
+
apply_options(opts)
|
27
|
+
end
|
28
|
+
|
29
|
+
def from_options(opts)
|
30
|
+
new(apply_options(opts)) if apply_options?(opts)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
require "montrose/rule/after"
|
37
|
+
require "montrose/rule/before"
|
38
|
+
require "montrose/rule/day_of_month"
|
39
|
+
require "montrose/rule/day_of_week"
|
40
|
+
require "montrose/rule/day_of_year"
|
41
|
+
require "montrose/rule/hour_of_day"
|
42
|
+
require "montrose/rule/month_of_year"
|
43
|
+
require "montrose/rule/nth_day_of_month"
|
44
|
+
require "montrose/rule/nth_day_of_year"
|
45
|
+
require "montrose/rule/time_of_day"
|
46
|
+
require "montrose/rule/total"
|
47
|
+
require "montrose/rule/week_of_year"
|