ice_cube_chosko 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 (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