montrose 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +11 -0
  3. data/.rubocop.yml +113 -0
  4. data/.travis.yml +5 -0
  5. data/CODE_OF_CONDUCT.md +13 -0
  6. data/Gemfile +13 -0
  7. data/Guardfile +34 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +191 -0
  10. data/Rakefile +21 -0
  11. data/bin/_guard-core +16 -0
  12. data/bin/console +7 -0
  13. data/bin/guard +16 -0
  14. data/bin/m +16 -0
  15. data/bin/rake +16 -0
  16. data/bin/rubocop +16 -0
  17. data/bin/setup +7 -0
  18. data/lib/montrose.rb +20 -0
  19. data/lib/montrose/chainable.rb +210 -0
  20. data/lib/montrose/clock.rb +77 -0
  21. data/lib/montrose/errors.rb +5 -0
  22. data/lib/montrose/frequency.rb +63 -0
  23. data/lib/montrose/frequency/daily.rb +9 -0
  24. data/lib/montrose/frequency/hourly.rb +9 -0
  25. data/lib/montrose/frequency/minutely.rb +9 -0
  26. data/lib/montrose/frequency/monthly.rb +9 -0
  27. data/lib/montrose/frequency/weekly.rb +19 -0
  28. data/lib/montrose/frequency/yearly.rb +9 -0
  29. data/lib/montrose/options.rb +293 -0
  30. data/lib/montrose/recurrence.rb +67 -0
  31. data/lib/montrose/rule.rb +47 -0
  32. data/lib/montrose/rule/after.rb +27 -0
  33. data/lib/montrose/rule/before.rb +23 -0
  34. data/lib/montrose/rule/day_of_month.rb +31 -0
  35. data/lib/montrose/rule/day_of_week.rb +23 -0
  36. data/lib/montrose/rule/day_of_year.rb +37 -0
  37. data/lib/montrose/rule/hour_of_day.rb +23 -0
  38. data/lib/montrose/rule/month_of_year.rb +23 -0
  39. data/lib/montrose/rule/nth_day_matcher.rb +32 -0
  40. data/lib/montrose/rule/nth_day_of_month.rb +63 -0
  41. data/lib/montrose/rule/nth_day_of_year.rb +63 -0
  42. data/lib/montrose/rule/time_of_day.rb +33 -0
  43. data/lib/montrose/rule/total.rb +29 -0
  44. data/lib/montrose/rule/week_of_year.rb +23 -0
  45. data/lib/montrose/schedule.rb +42 -0
  46. data/lib/montrose/stack.rb +51 -0
  47. data/lib/montrose/utils.rb +32 -0
  48. data/lib/montrose/version.rb +3 -0
  49. data/montrose.gemspec +32 -0
  50. metadata +192 -0
@@ -0,0 +1,9 @@
1
+ module Montrose
2
+ class Frequency
3
+ class Daily < Frequency
4
+ def include?(time)
5
+ matches_interval? time.to_date - @starts.to_date
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Montrose
2
+ class Frequency
3
+ class Hourly < Frequency
4
+ def include?(time)
5
+ matches_interval?((time - @starts) / 1.hour)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Montrose
2
+ class Frequency
3
+ class Minutely < Frequency
4
+ def include?(time)
5
+ matches_interval?((time - @starts) / 1.minute)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Montrose
2
+ class Frequency
3
+ class Monthly < Frequency
4
+ def include?(time)
5
+ matches_interval?((time.month - @starts.month) + (time.year - @starts.year) * 12)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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,9 @@
1
+ module Montrose
2
+ class Frequency
3
+ class Yearly < Frequency
4
+ def include?(time)
5
+ matches_interval? time.year - @starts.year
6
+ end
7
+ end
8
+ end
9
+ 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"