ice_cube_chosko 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/config/locales/en.yml +178 -0
  3. data/config/locales/es.yml +176 -0
  4. data/config/locales/ja.yml +107 -0
  5. data/lib/ice_cube.rb +92 -0
  6. data/lib/ice_cube/builders/hash_builder.rb +27 -0
  7. data/lib/ice_cube/builders/ical_builder.rb +59 -0
  8. data/lib/ice_cube/builders/string_builder.rb +76 -0
  9. data/lib/ice_cube/deprecated.rb +36 -0
  10. data/lib/ice_cube/errors/count_exceeded.rb +7 -0
  11. data/lib/ice_cube/errors/until_exceeded.rb +7 -0
  12. data/lib/ice_cube/flexible_hash.rb +40 -0
  13. data/lib/ice_cube/i18n.rb +24 -0
  14. data/lib/ice_cube/null_i18n.rb +28 -0
  15. data/lib/ice_cube/occurrence.rb +101 -0
  16. data/lib/ice_cube/parsers/hash_parser.rb +91 -0
  17. data/lib/ice_cube/parsers/ical_parser.rb +91 -0
  18. data/lib/ice_cube/parsers/yaml_parser.rb +19 -0
  19. data/lib/ice_cube/rule.rb +123 -0
  20. data/lib/ice_cube/rules/daily_rule.rb +16 -0
  21. data/lib/ice_cube/rules/hourly_rule.rb +16 -0
  22. data/lib/ice_cube/rules/minutely_rule.rb +16 -0
  23. data/lib/ice_cube/rules/monthly_rule.rb +16 -0
  24. data/lib/ice_cube/rules/secondly_rule.rb +15 -0
  25. data/lib/ice_cube/rules/weekly_rule.rb +16 -0
  26. data/lib/ice_cube/rules/yearly_rule.rb +16 -0
  27. data/lib/ice_cube/schedule.rb +529 -0
  28. data/lib/ice_cube/single_occurrence_rule.rb +28 -0
  29. data/lib/ice_cube/time_util.rb +328 -0
  30. data/lib/ice_cube/validated_rule.rb +184 -0
  31. data/lib/ice_cube/validations/count.rb +61 -0
  32. data/lib/ice_cube/validations/daily_interval.rb +54 -0
  33. data/lib/ice_cube/validations/day.rb +71 -0
  34. data/lib/ice_cube/validations/day_of_month.rb +55 -0
  35. data/lib/ice_cube/validations/day_of_week.rb +77 -0
  36. data/lib/ice_cube/validations/day_of_year.rb +61 -0
  37. data/lib/ice_cube/validations/fixed_value.rb +95 -0
  38. data/lib/ice_cube/validations/hour_of_day.rb +55 -0
  39. data/lib/ice_cube/validations/hourly_interval.rb +54 -0
  40. data/lib/ice_cube/validations/lock.rb +95 -0
  41. data/lib/ice_cube/validations/minute_of_hour.rb +54 -0
  42. data/lib/ice_cube/validations/minutely_interval.rb +54 -0
  43. data/lib/ice_cube/validations/month_of_year.rb +54 -0
  44. data/lib/ice_cube/validations/monthly_interval.rb +53 -0
  45. data/lib/ice_cube/validations/schedule_lock.rb +46 -0
  46. data/lib/ice_cube/validations/second_of_minute.rb +54 -0
  47. data/lib/ice_cube/validations/secondly_interval.rb +51 -0
  48. data/lib/ice_cube/validations/until.rb +57 -0
  49. data/lib/ice_cube/validations/weekly_interval.rb +67 -0
  50. data/lib/ice_cube/validations/yearly_interval.rb +53 -0
  51. data/lib/ice_cube/version.rb +5 -0
  52. data/spec/spec_helper.rb +64 -0
  53. metadata +166 -0
@@ -0,0 +1,529 @@
1
+ require 'yaml'
2
+
3
+ module IceCube
4
+
5
+ class Schedule
6
+
7
+ extend Deprecated
8
+
9
+ # Get the start time
10
+ attr_reader :start_time
11
+ deprecated_alias :start_date, :start_time
12
+
13
+ # Get the end time
14
+ attr_reader :end_time
15
+ deprecated_alias :end_date, :end_time
16
+
17
+ # Create a new schedule
18
+ def initialize(start_time = nil, options = {})
19
+ self.start_time = start_time || TimeUtil.now
20
+ self.end_time = self.start_time + options[:duration] if options[:duration]
21
+ self.end_time = options[:end_time] if options[:end_time]
22
+ @all_recurrence_rules = []
23
+ @all_exception_rules = []
24
+ yield self if block_given?
25
+ end
26
+
27
+ # Set start_time
28
+ def start_time=(start_time)
29
+ @start_time = TimeUtil.ensure_time start_time
30
+ end
31
+ deprecated_alias :start_date=, :start_time=
32
+
33
+ # Set end_time
34
+ def end_time=(end_time)
35
+ @end_time = TimeUtil.ensure_time end_time
36
+ end
37
+ deprecated_alias :end_date=, :end_time=
38
+
39
+ def duration
40
+ end_time ? end_time - start_time : 0
41
+ end
42
+
43
+ def duration=(seconds)
44
+ @end_time = start_time + seconds
45
+ end
46
+
47
+ # Add a recurrence time to the schedule
48
+ def add_recurrence_time(time)
49
+ return nil if time.nil?
50
+ rule = SingleOccurrenceRule.new(time)
51
+ add_recurrence_rule rule
52
+ time
53
+ end
54
+ alias :rtime :add_recurrence_time
55
+ deprecated_alias :rdate, :rtime
56
+ deprecated_alias :add_recurrence_date, :add_recurrence_time
57
+
58
+ # Add an exception time to the schedule
59
+ def add_exception_time(time)
60
+ return nil if time.nil?
61
+ rule = SingleOccurrenceRule.new(time)
62
+ add_exception_rule rule
63
+ time
64
+ end
65
+ alias :extime :add_exception_time
66
+ deprecated_alias :exdate, :extime
67
+ deprecated_alias :add_exception_date, :add_exception_time
68
+
69
+ # Add a recurrence rule to the schedule
70
+ def add_recurrence_rule(rule)
71
+ @all_recurrence_rules << rule unless @all_recurrence_rules.include?(rule)
72
+ end
73
+ alias :rrule :add_recurrence_rule
74
+
75
+ # Remove a recurrence rule
76
+ def remove_recurrence_rule(rule)
77
+ res = @all_recurrence_rules.delete(rule)
78
+ res.nil? ? [] : [res]
79
+ end
80
+
81
+ # Add an exception rule to the schedule
82
+ def add_exception_rule(rule)
83
+ @all_exception_rules << rule unless @all_exception_rules.include?(rule)
84
+ end
85
+ alias :exrule :add_exception_rule
86
+
87
+ # Remove an exception rule
88
+ def remove_exception_rule(rule)
89
+ res = @all_exception_rules.delete(rule)
90
+ res.nil? ? [] : [res]
91
+ end
92
+
93
+ # Get the recurrence rules
94
+ def recurrence_rules
95
+ @all_recurrence_rules.reject { |r| r.is_a?(SingleOccurrenceRule) }
96
+ end
97
+ alias :rrules :recurrence_rules
98
+
99
+ # Get the exception rules
100
+ def exception_rules
101
+ @all_exception_rules.reject { |r| r.is_a?(SingleOccurrenceRule) }
102
+ end
103
+ alias :exrules :exception_rules
104
+
105
+ # Get the recurrence times that are on the schedule
106
+ def recurrence_times
107
+ @all_recurrence_rules.select { |r| r.is_a?(SingleOccurrenceRule) }.map(&:time)
108
+ end
109
+ alias :rtimes :recurrence_times
110
+ deprecated_alias :rdates, :rtimes
111
+ deprecated_alias :recurrence_dates, :recurrence_times
112
+
113
+ # Remove a recurrence time
114
+ def remove_recurrence_time(time)
115
+ found = false
116
+ @all_recurrence_rules.delete_if do |rule|
117
+ found = true if rule.is_a?(SingleOccurrenceRule) && rule.time == time
118
+ end
119
+ time if found
120
+ end
121
+ alias :remove_rtime :remove_recurrence_time
122
+ deprecated_alias :remove_recurrence_date, :remove_recurrence_time
123
+ deprecated_alias :remove_rdate, :remove_rtime
124
+
125
+ # Get the exception times that are on the schedule
126
+ def exception_times
127
+ @all_exception_rules.select { |r| r.is_a?(SingleOccurrenceRule) }.map(&:time)
128
+ end
129
+ alias :extimes :exception_times
130
+ deprecated_alias :exdates, :extimes
131
+ deprecated_alias :exception_dates, :exception_times
132
+
133
+ # Remove an exception time
134
+ def remove_exception_time(time)
135
+ found = false
136
+ @all_exception_rules.delete_if do |rule|
137
+ found = true if rule.is_a?(SingleOccurrenceRule) && rule.time == time
138
+ end
139
+ time if found
140
+ end
141
+ alias :remove_extime :remove_exception_time
142
+ deprecated_alias :remove_exception_date, :remove_exception_time
143
+ deprecated_alias :remove_exdate, :remove_extime
144
+
145
+ # Get all of the occurrences from the start_time up until a
146
+ # given Time
147
+ def occurrences(closing_time)
148
+ enumerate_occurrences(start_time, closing_time).to_a
149
+ end
150
+
151
+ # All of the occurrences
152
+ def all_occurrences
153
+ require_terminating_rules
154
+ enumerate_occurrences(start_time).to_a
155
+ end
156
+
157
+ # Emit an enumerator based on the start time
158
+ def all_occurrences_enumerator
159
+ enumerate_occurrences(start_time)
160
+ end
161
+
162
+ # Iterate forever
163
+ def each_occurrence(&block)
164
+ enumerate_occurrences(start_time, &block).to_a
165
+ self
166
+ end
167
+
168
+ # The next n occurrences after now
169
+ def next_occurrences(num, from = nil, spans = false)
170
+ from = TimeUtil.match_zone(from, start_time) || TimeUtil.now(start_time)
171
+ enumerate_occurrences(from + 1, nil, spans).take(num)
172
+ end
173
+
174
+ # The next occurrence after now (overridable)
175
+ def next_occurrence(from = nil, spans = false)
176
+ from = TimeUtil.match_zone(from, start_time) || TimeUtil.now(start_time)
177
+ enumerate_occurrences(from + 1, nil, spans).next
178
+ rescue StopIteration
179
+ nil
180
+ end
181
+
182
+ # The previous occurrence from a given time
183
+ def previous_occurrence(from)
184
+ from = TimeUtil.match_zone(from, start_time) or raise ArgumentError, "Time required, got #{time.inspect}"
185
+ return nil if from <= start_time
186
+ enumerate_occurrences(start_time, from - 1).to_a.last
187
+ end
188
+
189
+ # The previous n occurrences before a given time
190
+ def previous_occurrences(num, from)
191
+ from = TimeUtil.match_zone(from, start_time) or raise ArgumentError, "Time required, got #{time.inspect}"
192
+ return [] if from <= start_time
193
+ a = enumerate_occurrences(start_time, from - 1).to_a
194
+ a.size > num ? a[-1*num,a.size] : a
195
+ end
196
+
197
+ # The remaining occurrences (same requirements as all_occurrences)
198
+ def remaining_occurrences(from = nil, spans = false)
199
+ require_terminating_rules
200
+ from ||= TimeUtil.now(@start_time)
201
+ enumerate_occurrences(from, nil, spans).to_a
202
+ end
203
+
204
+ # Returns an enumerator for all remaining occurrences
205
+ def remaining_occurrences_enumerator(from = nil, spans = false)
206
+ from ||= TimeUtil.now(@start_time)
207
+ enumerate_occurrences(from, nil, spans)
208
+ end
209
+
210
+ # Occurrences between two times
211
+ def occurrences_between(begin_time, closing_time, spans = false)
212
+ enumerate_occurrences(begin_time, closing_time, spans).to_a
213
+ end
214
+
215
+ # Return a boolean indicating if an occurrence falls between two times
216
+ def occurs_between?(begin_time, closing_time, spans = false)
217
+ enumerate_occurrences(begin_time, closing_time, spans).next
218
+ true
219
+ rescue StopIteration
220
+ false
221
+ end
222
+
223
+ # Return a boolean indicating if an occurrence is occurring between two
224
+ # times, inclusive of its duration. This counts zero-length occurrences
225
+ # that intersect the start of the range and within the range, but not
226
+ # occurrences at the end of the range since none of their duration
227
+ # intersects the range.
228
+ def occurring_between?(opening_time, closing_time)
229
+ occurs_between?(opening_time, closing_time, true)
230
+ end
231
+
232
+ # Return a boolean indicating if an occurrence falls on a certain date
233
+ def occurs_on?(date)
234
+ date = TimeUtil.ensure_date(date)
235
+ begin_time = TimeUtil.beginning_of_date(date, start_time)
236
+ closing_time = TimeUtil.end_of_date(date, start_time)
237
+ occurs_between?(begin_time, closing_time)
238
+ end
239
+
240
+ # Determine if the schedule is occurring at a given time
241
+ def occurring_at?(time)
242
+ time = TimeUtil.match_zone(time, start_time) or raise ArgumentError, "Time required, got #{time.inspect}"
243
+ if duration > 0
244
+ return false if exception_time?(time)
245
+ occurs_between?(time - duration + 1, time)
246
+ else
247
+ occurs_at?(time)
248
+ end
249
+ end
250
+
251
+ # Determine if this schedule conflicts with another schedule
252
+ # @param [IceCube::Schedule] other_schedule - The schedule to compare to
253
+ # @param [Time] closing_time - the last time to consider
254
+ # @return [Boolean] whether or not the schedules conflict at all
255
+ def conflicts_with?(other_schedule, closing_time = nil)
256
+ closing_time = TimeUtil.ensure_time(closing_time)
257
+ unless terminating? || other_schedule.terminating? || closing_time
258
+ raise ArgumentError, "One or both schedules must be terminating to use #conflicts_with?"
259
+ end
260
+ # Pick the terminating schedule, and other schedule
261
+ # No need to reverse if terminating? or there is a closing time
262
+ terminating_schedule = self
263
+ unless terminating? || closing_time
264
+ terminating_schedule, other_schedule = other_schedule, terminating_schedule
265
+ end
266
+ # Go through each occurrence of the terminating schedule and determine
267
+ # if the other occurs at that time
268
+ #
269
+ last_time = nil
270
+ terminating_schedule.each_occurrence do |time|
271
+ if closing_time && time > closing_time
272
+ last_time = closing_time
273
+ break
274
+ end
275
+ last_time = time
276
+ return true if other_schedule.occurring_at?(time)
277
+ end
278
+ # Due to durations, we need to walk up to the end time, and verify in the
279
+ # other direction
280
+ if last_time
281
+ last_time += terminating_schedule.duration
282
+ other_schedule.each_occurrence do |time|
283
+ break if time > last_time
284
+ return true if terminating_schedule.occurring_at?(time)
285
+ end
286
+ end
287
+ # No conflict, return false
288
+ false
289
+ end
290
+
291
+ # Determine if the schedule occurs at a specific time
292
+ def occurs_at?(time)
293
+ occurs_between?(time, time)
294
+ end
295
+
296
+ # Get the first n occurrences, or the first occurrence if n is skipped
297
+ def first(n = nil)
298
+ occurrences = enumerate_occurrences(start_time).take(n || 1)
299
+ n.nil? ? occurrences.first : occurrences
300
+ end
301
+
302
+ # Get the final n occurrences of a terminating schedule
303
+ # or the final one if no n is given
304
+ def last(n = nil)
305
+ require_terminating_rules
306
+ occurrences = enumerate_occurrences(start_time).to_a
307
+ n.nil? ? occurrences.last : occurrences[-n..-1]
308
+ end
309
+
310
+ # String serialization
311
+ def to_s
312
+ pieces = []
313
+ rd = recurrence_times_with_start_time - extimes
314
+ pieces.concat rd.sort.map { |t| IceCube::I18n.l(t, format: IceCube.to_s_time_format) }
315
+ pieces.concat rrules.map { |t| t.to_s }
316
+ pieces.concat exrules.map { |t| IceCube::I18n.t('ice_cube.not', target: t.to_s) }
317
+ pieces.concat extimes.sort.map { |t|
318
+ target = IceCube::I18n.l(t, format: IceCube.to_s_time_format)
319
+ IceCube::I18n.t('ice_cube.not_on', target: target)
320
+ }
321
+ pieces.join(IceCube::I18n.t('ice_cube.pieces_connector'))
322
+ end
323
+
324
+ # Serialize this schedule to_ical
325
+ def to_ical(force_utc = false)
326
+ pieces = []
327
+ pieces << "DTSTART#{IcalBuilder.ical_format(start_time, force_utc)}"
328
+ pieces.concat recurrence_rules.map { |r| "RRULE:#{r.to_ical}" }
329
+ pieces.concat exception_rules.map { |r| "EXRULE:#{r.to_ical}" }
330
+ pieces.concat recurrence_times_without_start_time.map { |t| "RDATE#{IcalBuilder.ical_format(t, force_utc)}" }
331
+ pieces.concat exception_times.map { |t| "EXDATE#{IcalBuilder.ical_format(t, force_utc)}" }
332
+ pieces << "DTEND#{IcalBuilder.ical_format(end_time, force_utc)}" if end_time
333
+ pieces.join("\n")
334
+ end
335
+
336
+ # Load the schedule from ical
337
+ def self.from_ical(ical, options = {})
338
+ IcalParser.schedule_from_ical(ical, options)
339
+ end
340
+
341
+ # Convert the schedule to yaml
342
+ def to_yaml(*args)
343
+ YAML::dump(to_hash, *args)
344
+ end
345
+
346
+ # Load the schedule from yaml
347
+ def self.from_yaml(yaml, options = {})
348
+ YamlParser.new(yaml).to_schedule do |schedule|
349
+ Deprecated.schedule_options(schedule, options)
350
+ yield schedule if block_given?
351
+ end
352
+ end
353
+
354
+ # Convert the schedule to a hash
355
+ def to_hash
356
+ data = {}
357
+ data[:start_time] = TimeUtil.serialize_time(start_time)
358
+ data[:start_date] = data[:start_time] if IceCube.compatibility <= 11
359
+ data[:end_time] = TimeUtil.serialize_time(end_time) if end_time
360
+ data[:rrules] = recurrence_rules.map(&:to_hash)
361
+ if IceCube.compatibility <= 11 && exception_rules.any?
362
+ data[:exrules] = exception_rules.map(&:to_hash)
363
+ end
364
+ data[:rtimes] = recurrence_times.map do |rt|
365
+ TimeUtil.serialize_time(rt)
366
+ end
367
+ data[:extimes] = exception_times.map do |et|
368
+ TimeUtil.serialize_time(et)
369
+ end
370
+ data
371
+ end
372
+
373
+ # Load the schedule from a hash
374
+ def self.from_hash(original_hash, options = {})
375
+ HashParser.new(original_hash).to_schedule do |schedule|
376
+ Deprecated.schedule_options(schedule, options)
377
+ yield schedule if block_given?
378
+ end
379
+ end
380
+
381
+ # Determine if the schedule will end
382
+ # @return [Boolean] true if ending, false if repeating forever
383
+ def terminating?
384
+ recurrence_rules.empty? || recurrence_rules.all?(&:terminating?)
385
+ end
386
+
387
+ def self.dump(schedule)
388
+ return schedule if schedule.nil? || schedule == ""
389
+ schedule.to_yaml
390
+ end
391
+
392
+ def self.load(yaml)
393
+ return yaml if yaml.nil? || yaml == ""
394
+ from_yaml(yaml)
395
+ end
396
+
397
+ private
398
+
399
+ # Reset all rules for another run
400
+ def reset
401
+ @all_recurrence_rules.each(&:reset)
402
+ @all_exception_rules.each(&:reset)
403
+ end
404
+
405
+ # Find all of the occurrences for the schedule between opening_time
406
+ # and closing_time
407
+ # Iteration is unrolled in pairs to skip duplicate times in end of DST
408
+ def enumerate_occurrences(opening_time, closing_time = nil, spans = false, &block)
409
+ opening_time = TimeUtil.match_zone(opening_time, start_time)
410
+ closing_time = TimeUtil.match_zone(closing_time, start_time)
411
+ opening_time += start_time.subsec - opening_time.subsec rescue 0
412
+ opening_time = start_time if opening_time < start_time
413
+ spans = false if duration == 0
414
+ Enumerator.new do |yielder|
415
+ reset
416
+ t1 = full_required? ? start_time : realign((spans ? opening_time - duration : opening_time))
417
+ loop do
418
+ break unless (t0 = next_time(t1, closing_time))
419
+ break if closing_time && t0 > closing_time
420
+ if (spans ? (t0.end_time > opening_time) : (t0 >= opening_time))
421
+ yielder << (block_given? ? block.call(t0) : t0)
422
+ end
423
+ break unless (t1 = next_time(t0 + 1, closing_time))
424
+ break if closing_time && t1 > closing_time
425
+ if TimeUtil.same_clock?(t0, t1) && recurrence_rules.any?(&:dst_adjust?)
426
+ wind_back_dst
427
+ next (t1 += 1)
428
+ end
429
+ if (spans ? (t1.end_time > opening_time) : (t1 >= opening_time))
430
+ yielder << (block_given? ? block.call(t1) : t1)
431
+ end
432
+ next (t1 += 1)
433
+ end
434
+ end
435
+ end
436
+
437
+ # Get the next time after (or including) a specific time
438
+ def next_time(time, closing_time)
439
+ loop do
440
+ min_time = recurrence_rules_with_implicit_start_occurrence.reduce(nil) do |min_time, rule|
441
+ begin
442
+ new_time = rule.next_time(time, self, min_time || closing_time)
443
+ [min_time, new_time].compact.min
444
+ rescue StopIteration
445
+ min_time
446
+ end
447
+ end
448
+ break nil unless min_time
449
+ next (time = min_time + 1) if exception_time?(min_time)
450
+ break Occurrence.new(min_time, min_time + duration)
451
+ end
452
+ end
453
+
454
+ # Indicate if any rule needs to be run from the start of time
455
+ # If we have rules with counts, we need to walk from the beginning of time
456
+ def full_required?
457
+ @all_recurrence_rules.any?(&:full_required?) ||
458
+ @all_exception_rules.any?(&:full_required?)
459
+ end
460
+
461
+ # Return a boolean indicating whether or not a specific time
462
+ # is excluded from the schedule
463
+ def exception_time?(time)
464
+ @all_exception_rules.any? do |rule|
465
+ rule.on?(time, self)
466
+ end
467
+ end
468
+
469
+ def require_terminating_rules
470
+ return true if terminating?
471
+ method_name = caller[0].split(' ').last
472
+ raise ArgumentError, "All recurrence rules must specify .until or .count to use #{method_name}"
473
+ end
474
+
475
+ def implicit_start_occurrence_rule
476
+ SingleOccurrenceRule.new(start_time)
477
+ end
478
+
479
+ def recurrence_times_without_start_time
480
+ recurrence_times.reject { |t| t == start_time }
481
+ end
482
+
483
+ def recurrence_times_with_start_time
484
+ if recurrence_rules.empty?
485
+ [start_time].concat recurrence_times_without_start_time
486
+ else
487
+ recurrence_times
488
+ end
489
+ end
490
+
491
+ def recurrence_rules_with_implicit_start_occurrence
492
+ if recurrence_rules.empty?
493
+ [implicit_start_occurrence_rule].concat @all_recurrence_rules
494
+ else
495
+ @all_recurrence_rules
496
+ end
497
+ end
498
+
499
+ def wind_back_dst
500
+ recurrence_rules.each do |rule|
501
+ rule.skipped_for_dst
502
+ end
503
+ end
504
+
505
+ # If any rule has validations for values within the period, (overriding the
506
+ # interval from start time, e.g. `day[_of_week]`), and the opening time is
507
+ # offset from the interval multiplier such that it might miss the first
508
+ # correct occurrence (e.g. repeat is every N weeks, but selecting from end
509
+ # of week N-1, the first jump would go to end of week N and miss any
510
+ # earlier validations in the week). This realigns the opening time to
511
+ # the start of the interval's correct period (e.g. move to start of week N)
512
+ # TODO: check if this is needed for validations other than `:wday`
513
+ #
514
+ def realign(opening_time)
515
+ time = TimeUtil::TimeWrapper.new(opening_time)
516
+ recurrence_rules.each do |rule|
517
+ wday_validations = rule.other_interval_validations.select { |v| v.type == :wday } or next
518
+ interval = rule.base_interval_validation.validate(opening_time, self).to_i
519
+ offset = wday_validations
520
+ .map { |v| v.validate(opening_time, self).to_i }
521
+ .reduce(0) { |least, i| i > 0 && i <= interval && (i < least || least == 0) ? i : least }
522
+ time.add(rule.base_interval_type, 7 - time.to_time.wday) if offset > 0
523
+ end
524
+ time.to_time
525
+ end
526
+
527
+ end
528
+
529
+ end