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.
- checksums.yaml +7 -0
- data/config/locales/en.yml +178 -0
- data/config/locales/es.yml +176 -0
- data/config/locales/ja.yml +107 -0
- data/lib/ice_cube.rb +92 -0
- data/lib/ice_cube/builders/hash_builder.rb +27 -0
- data/lib/ice_cube/builders/ical_builder.rb +59 -0
- data/lib/ice_cube/builders/string_builder.rb +76 -0
- data/lib/ice_cube/deprecated.rb +36 -0
- data/lib/ice_cube/errors/count_exceeded.rb +7 -0
- data/lib/ice_cube/errors/until_exceeded.rb +7 -0
- data/lib/ice_cube/flexible_hash.rb +40 -0
- data/lib/ice_cube/i18n.rb +24 -0
- data/lib/ice_cube/null_i18n.rb +28 -0
- data/lib/ice_cube/occurrence.rb +101 -0
- data/lib/ice_cube/parsers/hash_parser.rb +91 -0
- data/lib/ice_cube/parsers/ical_parser.rb +91 -0
- data/lib/ice_cube/parsers/yaml_parser.rb +19 -0
- data/lib/ice_cube/rule.rb +123 -0
- data/lib/ice_cube/rules/daily_rule.rb +16 -0
- data/lib/ice_cube/rules/hourly_rule.rb +16 -0
- data/lib/ice_cube/rules/minutely_rule.rb +16 -0
- data/lib/ice_cube/rules/monthly_rule.rb +16 -0
- data/lib/ice_cube/rules/secondly_rule.rb +15 -0
- data/lib/ice_cube/rules/weekly_rule.rb +16 -0
- data/lib/ice_cube/rules/yearly_rule.rb +16 -0
- data/lib/ice_cube/schedule.rb +529 -0
- data/lib/ice_cube/single_occurrence_rule.rb +28 -0
- data/lib/ice_cube/time_util.rb +328 -0
- data/lib/ice_cube/validated_rule.rb +184 -0
- data/lib/ice_cube/validations/count.rb +61 -0
- data/lib/ice_cube/validations/daily_interval.rb +54 -0
- data/lib/ice_cube/validations/day.rb +71 -0
- data/lib/ice_cube/validations/day_of_month.rb +55 -0
- data/lib/ice_cube/validations/day_of_week.rb +77 -0
- data/lib/ice_cube/validations/day_of_year.rb +61 -0
- data/lib/ice_cube/validations/fixed_value.rb +95 -0
- data/lib/ice_cube/validations/hour_of_day.rb +55 -0
- data/lib/ice_cube/validations/hourly_interval.rb +54 -0
- data/lib/ice_cube/validations/lock.rb +95 -0
- data/lib/ice_cube/validations/minute_of_hour.rb +54 -0
- data/lib/ice_cube/validations/minutely_interval.rb +54 -0
- data/lib/ice_cube/validations/month_of_year.rb +54 -0
- data/lib/ice_cube/validations/monthly_interval.rb +53 -0
- data/lib/ice_cube/validations/schedule_lock.rb +46 -0
- data/lib/ice_cube/validations/second_of_minute.rb +54 -0
- data/lib/ice_cube/validations/secondly_interval.rb +51 -0
- data/lib/ice_cube/validations/until.rb +57 -0
- data/lib/ice_cube/validations/weekly_interval.rb +67 -0
- data/lib/ice_cube/validations/yearly_interval.rb +53 -0
- data/lib/ice_cube/version.rb +5 -0
- data/spec/spec_helper.rb +64 -0
- 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
|