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.
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"