ice_cube_conrad 0.8.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 (42) hide show
  1. data/lib/ice_cube.rb +80 -0
  2. data/lib/ice_cube/builders/hash_builder.rb +27 -0
  3. data/lib/ice_cube/builders/ical_builder.rb +59 -0
  4. data/lib/ice_cube/builders/string_builder.rb +74 -0
  5. data/lib/ice_cube/deprecated.rb +28 -0
  6. data/lib/ice_cube/errors/count_exceeded.rb +7 -0
  7. data/lib/ice_cube/errors/until_exceeded.rb +7 -0
  8. data/lib/ice_cube/errors/zero_interval.rb +7 -0
  9. data/lib/ice_cube/rule.rb +182 -0
  10. data/lib/ice_cube/rules/daily_rule.rb +14 -0
  11. data/lib/ice_cube/rules/hourly_rule.rb +14 -0
  12. data/lib/ice_cube/rules/minutely_rule.rb +14 -0
  13. data/lib/ice_cube/rules/monthly_rule.rb +14 -0
  14. data/lib/ice_cube/rules/secondly_rule.rb +13 -0
  15. data/lib/ice_cube/rules/weekly_rule.rb +14 -0
  16. data/lib/ice_cube/rules/yearly_rule.rb +14 -0
  17. data/lib/ice_cube/schedule.rb +414 -0
  18. data/lib/ice_cube/single_occurrence_rule.rb +28 -0
  19. data/lib/ice_cube/time_util.rb +250 -0
  20. data/lib/ice_cube/validated_rule.rb +108 -0
  21. data/lib/ice_cube/validations/count.rb +56 -0
  22. data/lib/ice_cube/validations/daily_interval.rb +55 -0
  23. data/lib/ice_cube/validations/day.rb +65 -0
  24. data/lib/ice_cube/validations/day_of_month.rb +52 -0
  25. data/lib/ice_cube/validations/day_of_week.rb +70 -0
  26. data/lib/ice_cube/validations/day_of_year.rb +55 -0
  27. data/lib/ice_cube/validations/hour_of_day.rb +52 -0
  28. data/lib/ice_cube/validations/hourly_interval.rb +57 -0
  29. data/lib/ice_cube/validations/lock.rb +47 -0
  30. data/lib/ice_cube/validations/minute_of_hour.rb +51 -0
  31. data/lib/ice_cube/validations/minutely_interval.rb +57 -0
  32. data/lib/ice_cube/validations/month_of_year.rb +49 -0
  33. data/lib/ice_cube/validations/monthly_interval.rb +51 -0
  34. data/lib/ice_cube/validations/schedule_lock.rb +41 -0
  35. data/lib/ice_cube/validations/second_of_minute.rb +49 -0
  36. data/lib/ice_cube/validations/secondly_interval.rb +54 -0
  37. data/lib/ice_cube/validations/until.rb +51 -0
  38. data/lib/ice_cube/validations/weekly_interval.rb +60 -0
  39. data/lib/ice_cube/validations/yearly_interval.rb +49 -0
  40. data/lib/ice_cube/version.rb +5 -0
  41. data/spec/spec_helper.rb +11 -0
  42. metadata +120 -0
@@ -0,0 +1,14 @@
1
+ module IceCube
2
+
3
+ class DailyRule < ValidatedRule
4
+
5
+ include Validations::DailyInterval
6
+
7
+ def initialize(interval = 1)
8
+ interval(interval)
9
+ schedule_lock(:hour, :min, :sec)
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,14 @@
1
+ module IceCube
2
+
3
+ class HourlyRule < ValidatedRule
4
+
5
+ include Validations::HourlyInterval
6
+
7
+ def initialize(interval = 1)
8
+ interval(interval)
9
+ schedule_lock(:min, :sec)
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,14 @@
1
+ module IceCube
2
+
3
+ class MinutelyRule < ValidatedRule
4
+
5
+ include Validations::MinutelyInterval
6
+
7
+ def initialize(interval = 1)
8
+ interval(interval)
9
+ schedule_lock(:sec)
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,14 @@
1
+ module IceCube
2
+
3
+ class MonthlyRule < ValidatedRule
4
+
5
+ include Validations::MonthlyInterval
6
+
7
+ def initialize(interval = 1)
8
+ interval(interval)
9
+ schedule_lock(:day, :hour, :min, :sec)
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,13 @@
1
+ module IceCube
2
+
3
+ class SecondlyRule < ValidatedRule
4
+
5
+ include Validations::SecondlyInterval
6
+
7
+ def initialize(interval = 1)
8
+ interval(interval)
9
+ end
10
+
11
+ end
12
+
13
+ end
@@ -0,0 +1,14 @@
1
+ module IceCube
2
+
3
+ class WeeklyRule < ValidatedRule
4
+
5
+ include Validations::WeeklyInterval
6
+
7
+ def initialize(interval = 1, week_start = :sunday)
8
+ interval(interval, week_start)
9
+ schedule_lock(:wday, :hour, :min, :sec)
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,14 @@
1
+ module IceCube
2
+
3
+ class YearlyRule < ValidatedRule
4
+
5
+ include Validations::YearlyInterval
6
+
7
+ def initialize(interval = 1)
8
+ interval(interval)
9
+ schedule_lock(:month, :day, :hour, :min, :sec)
10
+ end
11
+
12
+ end
13
+
14
+ end
@@ -0,0 +1,414 @@
1
+ require 'yaml'
2
+
3
+ module IceCube
4
+
5
+ class Schedule
6
+
7
+ extend ::Deprecated
8
+
9
+ # Get the start time
10
+ attr_accessor :start_time
11
+ deprecated_alias :start_date, :start_time
12
+ deprecated_alias :start_date=, :start_time=
13
+
14
+ # Get the duration
15
+ attr_accessor :duration
16
+
17
+ # Get the end time
18
+ attr_accessor :end_time
19
+ deprecated_alias :end_date, :end_time
20
+ deprecated_alias :end_date=, :end_time=
21
+
22
+ # Create a new schedule
23
+ def initialize(start_time = nil, options = {})
24
+ @start_time = start_time || Time.now
25
+ @end_time = options[:end_time]
26
+ @duration = options[:duration]
27
+ @all_recurrence_rules = []
28
+ @all_exception_rules = []
29
+ end
30
+
31
+ # Add a recurrence time to the schedule
32
+ def add_recurrence_time(time)
33
+ return nil if time.nil?
34
+ rule = SingleOccurrenceRule.new(time)
35
+ add_recurrence_rule rule
36
+ time
37
+ end
38
+ alias :rtime :add_recurrence_time
39
+ deprecated_alias :rdate, :rtime
40
+ deprecated_alias :add_recurrence_date, :add_recurrence_time
41
+
42
+ # Add an exception time to the schedule
43
+ def add_exception_time(time)
44
+ return nil if time.nil?
45
+ rule = SingleOccurrenceRule.new(time)
46
+ add_exception_rule rule
47
+ time
48
+ end
49
+ alias :extime :add_exception_time
50
+ deprecated_alias :exdate, :extime
51
+ deprecated_alias :add_exception_date, :add_exception_time
52
+
53
+ # Add a recurrence rule to the schedule
54
+ def add_recurrence_rule(rule)
55
+ @all_recurrence_rules << rule unless @all_recurrence_rules.include?(rule)
56
+ end
57
+ alias :rrule :add_recurrence_rule
58
+
59
+ # Remove a recurrence rule
60
+ def remove_recurrence_rule(rule)
61
+ res = @all_recurrence_rules.delete(rule)
62
+ res.nil? ? [] : [res]
63
+ end
64
+
65
+ # Add an exception rule to the schedule
66
+ def add_exception_rule(rule)
67
+ @all_exception_rules << rule unless @all_exception_rules.include?(rule)
68
+ end
69
+ alias :exrule :add_exception_rule
70
+
71
+ # Remove an exception rule
72
+ def remove_exception_rule(rule)
73
+ res = @all_exception_rules.delete(rule)
74
+ res.nil? ? [] : [res]
75
+ end
76
+
77
+ # Get the recurrence rules
78
+ def recurrence_rules
79
+ @all_recurrence_rules.reject { |r| r.is_a?(SingleOccurrenceRule) }
80
+ end
81
+ alias :rrules :recurrence_rules
82
+
83
+ # Get the exception rules
84
+ def exception_rules
85
+ @all_exception_rules.reject { |r| r.is_a?(SingleOccurrenceRule) }
86
+ end
87
+ alias :exrules :exception_rules
88
+
89
+ # Get the recurrence times that are on the schedule
90
+ def recurrence_times
91
+ @all_recurrence_rules.select { |r| r.is_a?(SingleOccurrenceRule) }.map(&:time)
92
+ end
93
+ alias :rtimes :recurrence_times
94
+ deprecated_alias :rdates, :rtimes
95
+ deprecated_alias :recurrence_dates, :recurrence_times
96
+
97
+ # Remove a recurrence time
98
+ def remove_recurrence_time(time)
99
+ found = false
100
+ @all_recurrence_rules.delete_if do |rule|
101
+ found = true if rule.is_a?(SingleOccurrenceRule) && rule.time == time
102
+ end
103
+ time if found
104
+ end
105
+ alias :remove_rtime :remove_recurrence_time
106
+ deprecated_alias :remove_recurrence_date, :remove_recurrence_time
107
+ deprecated_alias :remove_rdate, :remove_rtime
108
+
109
+ # Get the exception times that are on the schedule
110
+ def exception_times
111
+ @all_exception_rules.select { |r| r.is_a?(SingleOccurrenceRule) }.map(&:time)
112
+ end
113
+ alias :extimes :exception_times
114
+ deprecated_alias :exdates, :extimes
115
+ deprecated_alias :exception_dates, :exception_times
116
+
117
+ # Remove an exception time
118
+ def remove_exception_time(time)
119
+ found = false
120
+ @all_exception_rules.delete_if do |rule|
121
+ found = true if rule.is_a?(SingleOccurrenceRule) && rule.time == time
122
+ end
123
+ time if found
124
+ end
125
+ alias :remove_extime :remove_exception_time
126
+ deprecated_alias :remove_exception_date, :remove_exception_time
127
+ deprecated_alias :remove_exdate, :remove_extime
128
+
129
+ # Get all of the occurrences from the start_time up until a
130
+ # given Time
131
+ def occurrences(closing_time)
132
+ find_occurrences(start_time, closing_time)
133
+ end
134
+
135
+ # All of the occurrences
136
+ def all_occurrences
137
+ raise ArgumentError.new('Rule must specify either an until date or a count to use #all_occurrences') unless terminating?
138
+ find_occurrences(start_time)
139
+ end
140
+
141
+ # Iterate forever
142
+ def each_occurrence(&block)
143
+ find_occurrences(start_time, &block)
144
+ self
145
+ end
146
+
147
+ # The next n occurrences after now
148
+ def next_occurrences(num, from = Time.now)
149
+ find_occurrences(from + 1, nil, num)
150
+ end
151
+
152
+ # The next occurrence after now (overridable)
153
+ def next_occurrence(from = Time.now)
154
+ find_occurrences(from + 1, nil, 1).first
155
+ end
156
+
157
+ # The remaining occurrences (same requirements as all_occurrences)
158
+ def remaining_occurrences(from = Time.now)
159
+ find_occurrences(from)
160
+ end
161
+
162
+ # Occurrences between two times
163
+ def occurrences_between(begin_time, closing_time)
164
+ find_occurrences(begin_time, closing_time)
165
+ end
166
+
167
+ # Return a boolean indicating if an occurrence falls between
168
+ # two times
169
+ def occurs_between?(begin_time, closing_time)
170
+ !find_occurrences(begin_time, closing_time, 1).empty?
171
+ end
172
+
173
+ # Return a boolean indicating if an occurrence falls on a certain date
174
+ def occurs_on?(date)
175
+ begin_time = TimeUtil.beginning_of_date(date)
176
+ closing_time = TimeUtil.end_of_date(date)
177
+ occurs_between?(begin_time, closing_time)
178
+ end
179
+
180
+ # Determine if the schedule is occurring at a given time
181
+ def occurring_at?(time)
182
+ time = time.to_time
183
+ if duration
184
+ return false if exception_time?(time)
185
+ occurs_between?(time - duration + 1, time)
186
+ else
187
+ occurs_at?(time)
188
+ end
189
+ end
190
+
191
+ # Determine if this schedule conflicts with another schedule
192
+ # @param [IceCube::Schedule] other_schedule - The schedule to compare to
193
+ # @param [Time] closing_time - the last time to consider
194
+ # @return [Boolean] whether or not the schedules conflict at all
195
+ def conflicts_with?(other_schedule, closing_time = nil)
196
+ unless terminating? || other_schedule.terminating? || closing_time
197
+ raise ArgumentError.new 'At least one schedule must be terminating to use #conflicts_with?'
198
+ end
199
+ # Pick the terminating schedule, and other schedule
200
+ # No need to reverse if terminating? or there is a closing time
201
+ terminating_schedule = self
202
+ unless terminating? || closing_time
203
+ terminating_schedule, other_schedule = other_schedule, terminating_schedule
204
+ end
205
+ # Go through each occurrence of the terminating schedule and determine
206
+ # if the other occurs at that time
207
+ last_time = nil
208
+ terminating_schedule.each_occurrence do |time|
209
+ if closing_time && time > closing_time
210
+ last_time = closing_time
211
+ break
212
+ end
213
+ last_time = time
214
+ return true if other_schedule.occurring_at?(time)
215
+ end
216
+ # Due to durations, we need to walk up to the end time, and verify in the
217
+ # other direction
218
+ if last_time
219
+ last_time = terminating_schedule.duration ? last_time + terminating_schedule.duration : last_time
220
+ other_schedule.each_occurrence do |time|
221
+ break if time > last_time
222
+ return true if terminating_schedule.occurring_at?(time)
223
+ end
224
+ end
225
+ # No conflict, return false
226
+ false
227
+ end
228
+
229
+ # Determine if the schedule occurs at a specific time
230
+ def occurs_at?(time)
231
+ occurs_between?(time, time)
232
+ end
233
+
234
+ # Get the first n occurrences, or the first occurrence if n is skipped
235
+ def first(n = nil)
236
+ occurrences = find_occurrences start_time, nil, n || 1
237
+ n.nil? ? occurrences.first : occurrences
238
+ end
239
+
240
+ # String serialization
241
+ def to_s
242
+ pieces = []
243
+ ed = extimes; rd = rtimes - ed
244
+ pieces.concat rd.sort.map { |t| t.strftime(TO_S_TIME_FORMAT) }
245
+ pieces.concat rrules.map { |t| t.to_s }
246
+ pieces.concat exrules.map { |t| "not #{t.to_s}" }
247
+ pieces.concat ed.sort.map { |t| "not on #{t.strftime(TO_S_TIME_FORMAT)}" }
248
+ pieces << "until #{end_time.strftime(TO_S_TIME_FORMAT)}" if end_time
249
+ pieces.join(' / ')
250
+ end
251
+
252
+ # Serialize this schedule to_ical
253
+ def to_ical(force_utc = false)
254
+ pieces = []
255
+ pieces << "DTSTART#{IcalBuilder.ical_format(start_time, force_utc)}"
256
+ pieces << "DURATION:#{IcalBuilder.ical_duration(duration)}" if duration
257
+ pieces.concat recurrence_rules.map { |r| "RRULE:#{r.to_ical}" }
258
+ pieces.concat exception_rules.map { |r| "EXRULE:#{r.to_ical}" }
259
+ pieces.concat recurrence_times.map { |t| "RDATE#{IcalBuilder.ical_format(t, force_utc)}" }
260
+ pieces.concat exception_times.map { |t| "EXDATE#{IcalBuilder.ical_format(t, force_utc)}" }
261
+ pieces << "DTEND#{IcalBuilder.ical_format(end_time, force_utc)}" if end_time
262
+ pieces.join("\n")
263
+ end
264
+
265
+ # Convert the schedule to yaml
266
+ def to_yaml(*args)
267
+ IceCube::use_psych? ? Psych::dump(to_hash, *args) : YAML::dump(to_hash, *args)
268
+ end
269
+
270
+ # Load the schedule from yaml
271
+ def self.from_yaml(yaml, options = {})
272
+ from_hash IceCube::use_psych? ? Psych::load(yaml) : YAML::load(yaml), options
273
+ end
274
+
275
+ # Convert the schedule to a hash
276
+ def to_hash
277
+ data = {}
278
+ data[:start_date] = TimeUtil.serialize_time(start_time)
279
+ data[:end_time] = TimeUtil.serialize_time(end_time) if end_time
280
+ data[:duration] = duration if duration
281
+ data[:rrules] = recurrence_rules.map(&:to_hash)
282
+ data[:exrules] = exception_rules.map(&:to_hash)
283
+ data[:rtimes] = recurrence_times.map do |rt|
284
+ TimeUtil.serialize_time(rt)
285
+ end
286
+ data[:extimes] = exception_times.map do |et|
287
+ TimeUtil.serialize_time(et)
288
+ end
289
+ data
290
+ end
291
+
292
+ # Load the schedule from a hash
293
+ def self.from_hash(data, options = {})
294
+ data[:start_date] = options[:start_date_override] if options[:start_date_override]
295
+ # And then deserialize
296
+ schedule = IceCube::Schedule.new TimeUtil.deserialize_time(data[:start_date])
297
+ schedule.duration = data[:duration] if data[:duration]
298
+ schedule.end_time = TimeUtil.deserialize_time(data[:end_time]) if data[:end_time]
299
+ data[:rrules] && data[:rrules].each { |h| schedule.rrule(IceCube::Rule.from_hash(h)) }
300
+ data[:exrules] && data[:exrules].each { |h| schedule.exrule(IceCube::Rule.from_hash(h)) }
301
+ data[:rtimes] && data[:rtimes].each do |t|
302
+ schedule.add_recurrence_time TimeUtil.deserialize_time(t)
303
+ end
304
+ data[:extimes] && data[:extimes].each do |t|
305
+ schedule.add_exception_time TimeUtil.deserialize_time(t)
306
+ end
307
+ # Also serialize old format for backward compat
308
+ data[:rdates] && data[:rdates].each do |t|
309
+ schedule.add_recurrence_time TimeUtil.deserialize_time(t)
310
+ end
311
+ data[:exdates] && data[:exdates].each do |t|
312
+ schedule.add_exception_time TimeUtil.deserialize_time(t)
313
+ end
314
+ schedule
315
+ end
316
+
317
+ # Determine if the schedule will end
318
+ # @return [Boolean] true if ending, false if repeating forever
319
+ def terminating?
320
+ end_time || recurrence_rules.all?(&:terminating?)
321
+ end
322
+
323
+ def self.dump(schedule)
324
+ schedule.to_yaml
325
+ end
326
+
327
+ def self.load(yaml)
328
+ from_yaml(yaml) unless yaml.nil? || yaml.empty?
329
+ end
330
+
331
+ private
332
+
333
+ # Reset all rules for another run
334
+ def reset
335
+ @all_recurrence_rules.each(&:reset)
336
+ @all_exception_rules.each(&:reset)
337
+ end
338
+
339
+ # Find all of the occurrences for the schedule between opening_time
340
+ # and closing_time
341
+ def find_occurrences(opening_time, closing_time = nil, limit = nil, &block)
342
+ reset
343
+ answers = []
344
+ # ensure the bounds are proper
345
+ if end_time
346
+ closing_time = end_time unless closing_time && closing_time < end_time
347
+ end
348
+ opening_time = start_time if opening_time < start_time
349
+ # walk up to the opening time - and off we go
350
+ # If we have rules with counts, we need to walk from the beginning of time,
351
+ # otherwise opening_time
352
+ time = full_required? ? start_time : opening_time
353
+ loop do
354
+ res = next_time(time, closing_time)
355
+ break unless res
356
+ break if closing_time && res > closing_time
357
+ if res >= opening_time
358
+ block_given? ? block.call(res) : (answers << res)
359
+ break if limit && answers.length == limit
360
+ end
361
+ time = res + 1
362
+ end
363
+ # and return our answers
364
+ answers
365
+ end
366
+
367
+ # Get the next time after (or including) a specific time
368
+ def next_time(time, closing_time)
369
+ min_time = nil
370
+ loop do
371
+ @all_recurrence_rules.each do |rule|
372
+ begin
373
+ if res = rule.next_time(time, self, closing_time)
374
+ if min_time.nil? || res < min_time
375
+ min_time = res
376
+ end
377
+ end
378
+ # Certain exceptions mean this rule no longer wants to play
379
+ rescue CountExceeded, UntilExceeded
380
+ next
381
+ end
382
+ end
383
+ # If there is no match, return nil
384
+ return nil unless min_time
385
+ # Now make sure that its not an exception_time, and if it is
386
+ # then keep looking
387
+ if exception_time?(min_time)
388
+ time = min_time + 1
389
+ min_time = nil
390
+ next
391
+ end
392
+ # Break, we're done
393
+ break
394
+ end
395
+ min_time
396
+ end
397
+
398
+ # Return a boolean indicating if any rule needs to be run from the start of time
399
+ def full_required?
400
+ @all_recurrence_rules.any?(&:full_required?) ||
401
+ @all_exception_rules.any?(&:full_required?)
402
+ end
403
+
404
+ # Return a boolean indicating whether or not a specific time
405
+ # is excluded from the schedule
406
+ def exception_time?(time)
407
+ @all_exception_rules.any? do |rule|
408
+ rule.on?(time, self)
409
+ end
410
+ end
411
+
412
+ end
413
+
414
+ end