ice_cube 0.9.3 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/ice_cube.rb +1 -0
- data/lib/ice_cube/occurrence.rb +90 -0
- data/lib/ice_cube/rule.rb +1 -0
- data/lib/ice_cube/schedule.rb +53 -34
- data/lib/ice_cube/time_step.rb +30 -0
- data/lib/ice_cube/time_util.rb +80 -75
- data/lib/ice_cube/validated_rule.rb +32 -12
- data/lib/ice_cube/validations/day.rb +1 -1
- data/lib/ice_cube/validations/day_of_week.rb +2 -2
- data/lib/ice_cube/validations/lock.rb +49 -22
- data/lib/ice_cube/validations/month_of_year.rb +1 -1
- data/lib/ice_cube/validations/schedule_lock.rb +6 -0
- data/lib/ice_cube/validations/weekly_interval.rb +9 -3
- data/lib/ice_cube/version.rb +1 -1
- data/spec/spec_helper.rb +40 -0
- metadata +19 -26
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0cc5c669bd801317d8256d7fc8b77841e80dae6e
|
4
|
+
data.tar.gz: 6bda80589b1d0a91d54ea2d7b4194d320e26b2ac
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d3a10207a9dc7368b3cc6f045160c2b3f6651e1a95570a8d6c58af689eb616deb47291485b514b4ec58b1275595dce7f7c21f1606341d624449edfc5c842648d
|
7
|
+
data.tar.gz: 0abec738ddd565630a8a92468a8db708c2c18e2601abe3824ba10b766ce952341d7ff90da50a7ac6f4349d079661d66e8b131f177cbf4ee2853b4b046d4389a6
|
data/lib/ice_cube.rb
CHANGED
@@ -17,6 +17,7 @@ module IceCube
|
|
17
17
|
|
18
18
|
autoload :Rule, 'ice_cube/rule'
|
19
19
|
autoload :Schedule, 'ice_cube/schedule'
|
20
|
+
autoload :Occurrence, 'ice_cube/occurrence'
|
20
21
|
|
21
22
|
autoload :IcalBuilder, 'ice_cube/builders/ical_builder'
|
22
23
|
autoload :HashBuilder, 'ice_cube/builders/hash_builder'
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
require 'delegate'
|
3
|
+
|
4
|
+
module IceCube
|
5
|
+
|
6
|
+
# Wraps start_time and end_time in a single concept concerning the duration.
|
7
|
+
# This delegates to the enclosed start_time so it behaves like a normal Time
|
8
|
+
# in almost all situations, however:
|
9
|
+
#
|
10
|
+
# Without ActiveSupport, it's necessary to cast the occurrence using
|
11
|
+
# +#to_time+ before doing arithmetic, else Time will try to subtract it
|
12
|
+
# using +#to_i+ and return a new time instead.
|
13
|
+
#
|
14
|
+
# Time.now - Occurrence.new(start_time) # => 1970-01-01 01:00:00
|
15
|
+
# Time.now - Occurrence.new(start_time).to_time # => 3600
|
16
|
+
#
|
17
|
+
# When ActiveSupport::Time core extensions are loaded, it's possible to
|
18
|
+
# subtract an Occurrence object directly from a Time to get the difference:
|
19
|
+
#
|
20
|
+
# Time.now - Occurrence.new(start_time) # => 3600
|
21
|
+
#
|
22
|
+
class Occurrence < SimpleDelegator
|
23
|
+
|
24
|
+
# Optimize for common methods to avoid method_missing
|
25
|
+
extend Forwardable
|
26
|
+
def_delegators :start_time, :to_s, :to_i, :<=>, :==
|
27
|
+
def_delegators :to_range, :cover?, :include?, :each, :first, :last
|
28
|
+
|
29
|
+
attr_reader :start_time, :end_time
|
30
|
+
|
31
|
+
def initialize(start_time, end_time=nil)
|
32
|
+
@start_time = start_time
|
33
|
+
@end_time = end_time || start_time
|
34
|
+
__setobj__ @start_time
|
35
|
+
end
|
36
|
+
|
37
|
+
def is_a?(klass)
|
38
|
+
klass == ::Time || super
|
39
|
+
end
|
40
|
+
alias_method :kind_of?, :is_a?
|
41
|
+
|
42
|
+
def intersects? other
|
43
|
+
if other.is_a?(Occurrence) || other.is_a?(Range)
|
44
|
+
lower_bound_1 = first + 1
|
45
|
+
upper_bound_1 = last # exclude end
|
46
|
+
lower_bound_2 = other.first + 1
|
47
|
+
upper_bound_2 = other.last + 1
|
48
|
+
if (lower_bound_2 <=> upper_bound_2) > 0
|
49
|
+
false
|
50
|
+
elsif (lower_bound_1 <=> upper_bound_1) > 0
|
51
|
+
false
|
52
|
+
else
|
53
|
+
(upper_bound_1 <=> lower_bound_2) >= 0 and
|
54
|
+
(upper_bound_2 <=> lower_bound_1) >= 0
|
55
|
+
end
|
56
|
+
else
|
57
|
+
cover? other
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def comparable_time
|
62
|
+
start_time
|
63
|
+
end
|
64
|
+
|
65
|
+
def duration
|
66
|
+
end_time - start_time
|
67
|
+
end
|
68
|
+
|
69
|
+
def to_range
|
70
|
+
start_time..end_time
|
71
|
+
end
|
72
|
+
|
73
|
+
def to_time
|
74
|
+
start_time
|
75
|
+
end
|
76
|
+
|
77
|
+
def to_s
|
78
|
+
if duration > 0
|
79
|
+
"#{start_time} - #{end_time}"
|
80
|
+
else
|
81
|
+
"#{start_time}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def overnight?
|
86
|
+
midnight = Time.new(start_time.year, start_time.month, start_time.day + 1)
|
87
|
+
midnight < end_time
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
data/lib/ice_cube/rule.rb
CHANGED
@@ -48,6 +48,7 @@ module IceCube
|
|
48
48
|
hash = IceCube::FlexibleHash.new original_hash
|
49
49
|
return nil unless match = hash[:rule_type].match(/\:\:(.+?)Rule/)
|
50
50
|
rule = IceCube::Rule.send(match[1].downcase.to_sym, hash[:interval] || 1)
|
51
|
+
rule.interval(hash[:interval] || 1, TimeUtil.wday_to_sym(hash[:week_start] || 0)) if match[1] == "Weekly"
|
51
52
|
rule.until(TimeUtil.deserialize_time(hash[:until])) if hash[:until]
|
52
53
|
rule.count(hash[:count]) if hash[:count]
|
53
54
|
hash[:validations] && hash[:validations].each do |key, value|
|
data/lib/ice_cube/schedule.rb
CHANGED
@@ -21,6 +21,7 @@ module IceCube
|
|
21
21
|
self.end_time = options[:end_time] if options[:end_time]
|
22
22
|
@all_recurrence_rules = []
|
23
23
|
@all_exception_rules = []
|
24
|
+
yield self if block_given?
|
24
25
|
end
|
25
26
|
|
26
27
|
# Set start_time
|
@@ -182,16 +183,20 @@ module IceCube
|
|
182
183
|
find_occurrences(begin_time, closing_time)
|
183
184
|
end
|
184
185
|
|
185
|
-
# Return a boolean indicating if an occurrence falls between
|
186
|
-
# two times
|
186
|
+
# Return a boolean indicating if an occurrence falls between two times
|
187
187
|
def occurs_between?(begin_time, closing_time)
|
188
188
|
!find_occurrences(begin_time, closing_time, 1).empty?
|
189
189
|
end
|
190
190
|
|
191
|
-
# Return a boolean indicating if an occurrence is occurring between
|
192
|
-
#
|
193
|
-
|
194
|
-
|
191
|
+
# Return a boolean indicating if an occurrence is occurring between two
|
192
|
+
# times, inclusive of its duration. This counts zero-length occurrences
|
193
|
+
# that intersect the start of the range and within the range, but not
|
194
|
+
# occurrences at the end of the range since none of their duration
|
195
|
+
# intersects the range.
|
196
|
+
def occurring_between?(opening_time, closing_time)
|
197
|
+
opening_time = opening_time - duration
|
198
|
+
closing_time = closing_time - 1 if duration > 0
|
199
|
+
occurs_between?(opening_time, closing_time)
|
195
200
|
end
|
196
201
|
|
197
202
|
# Return a boolean indicating if an occurrence falls on a certain date
|
@@ -265,11 +270,11 @@ module IceCube
|
|
265
270
|
# String serialization
|
266
271
|
def to_s
|
267
272
|
pieces = []
|
268
|
-
|
273
|
+
rd = recurrence_times_with_start_time - extimes
|
269
274
|
pieces.concat rd.sort.map { |t| t.strftime(IceCube.to_s_time_format) }
|
270
|
-
pieces.concat rrules.map
|
275
|
+
pieces.concat rrules.map { |t| t.to_s }
|
271
276
|
pieces.concat exrules.map { |t| "not #{t.to_s}" }
|
272
|
-
pieces.concat
|
277
|
+
pieces.concat extimes.sort.map { |t| "not on #{t.strftime(IceCube.to_s_time_format)}" }
|
273
278
|
pieces.join(' / ')
|
274
279
|
end
|
275
280
|
|
@@ -278,9 +283,9 @@ module IceCube
|
|
278
283
|
pieces = []
|
279
284
|
pieces << "DTSTART#{IcalBuilder.ical_format(start_time, force_utc)}"
|
280
285
|
pieces.concat recurrence_rules.map { |r| "RRULE:#{r.to_ical}" }
|
281
|
-
pieces.concat exception_rules.map
|
282
|
-
pieces.concat
|
283
|
-
pieces.concat exception_times.map
|
286
|
+
pieces.concat exception_rules.map { |r| "EXRULE:#{r.to_ical}" }
|
287
|
+
pieces.concat recurrence_times_without_start_time.map { |t| "RDATE#{IcalBuilder.ical_format(t, force_utc)}" }
|
288
|
+
pieces.concat exception_times.map { |t| "EXDATE#{IcalBuilder.ical_format(t, force_utc)}" }
|
284
289
|
pieces << "DTEND#{IcalBuilder.ical_format(end_time, force_utc)}" if end_time
|
285
290
|
pieces.join("\n")
|
286
291
|
end
|
@@ -292,7 +297,11 @@ module IceCube
|
|
292
297
|
|
293
298
|
# Load the schedule from yaml
|
294
299
|
def self.from_yaml(yaml, options = {})
|
295
|
-
|
300
|
+
hash = IceCube::use_psych? ? Psych::load(yaml) : YAML::load(yaml)
|
301
|
+
if match = yaml.match(/start_date: .+((?:-|\+)\d{2}:\d{2})$/)
|
302
|
+
TimeUtil.restore_deserialized_offset(hash[:start_date], match[1])
|
303
|
+
end
|
304
|
+
from_hash hash, options
|
296
305
|
end
|
297
306
|
|
298
307
|
# Convert the schedule to a hash
|
@@ -388,33 +397,19 @@ module IceCube
|
|
388
397
|
|
389
398
|
# Get the next time after (or including) a specific time
|
390
399
|
def next_time(time, closing_time)
|
391
|
-
min_time = nil
|
392
400
|
loop do
|
393
|
-
|
401
|
+
min_time = recurrence_rules_with_implicit_start_occurrence.reduce(nil) do |min_time, rule|
|
394
402
|
begin
|
395
|
-
|
396
|
-
|
397
|
-
min_time = res
|
398
|
-
end
|
399
|
-
end
|
400
|
-
# Certain exceptions mean this rule no longer wants to play
|
403
|
+
new_time = rule.next_time(time, self, min_time || closing_time)
|
404
|
+
[min_time, new_time].compact.min
|
401
405
|
rescue CountExceeded, UntilExceeded
|
402
|
-
|
406
|
+
min_time
|
403
407
|
end
|
404
408
|
end
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
# then keep looking
|
409
|
-
if exception_time?(min_time)
|
410
|
-
time = min_time + 1
|
411
|
-
min_time = nil
|
412
|
-
next
|
413
|
-
end
|
414
|
-
# Break, we're done
|
415
|
-
break
|
409
|
+
break nil unless min_time
|
410
|
+
next(time = min_time + 1) if exception_time?(min_time)
|
411
|
+
break Occurrence.new(min_time, min_time + duration)
|
416
412
|
end
|
417
|
-
min_time
|
418
413
|
end
|
419
414
|
|
420
415
|
# Return a boolean indicating if any rule needs to be run from the start of time
|
@@ -431,6 +426,30 @@ module IceCube
|
|
431
426
|
end
|
432
427
|
end
|
433
428
|
|
429
|
+
def implicit_start_occurrence
|
430
|
+
SingleOccurrenceRule.new(start_time)
|
431
|
+
end
|
432
|
+
|
433
|
+
def recurrence_times_without_start_time
|
434
|
+
recurrence_times.reject { |t| t == start_time }
|
435
|
+
end
|
436
|
+
|
437
|
+
def recurrence_times_with_start_time
|
438
|
+
if (recurrence_rules).empty?
|
439
|
+
[start_time] + recurrence_times_without_start_time
|
440
|
+
else
|
441
|
+
recurrence_times
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
def recurrence_rules_with_implicit_start_occurrence
|
446
|
+
if recurrence_rules.empty?
|
447
|
+
[implicit_start_occurrence] + @all_recurrence_rules
|
448
|
+
else
|
449
|
+
@all_recurrence_rules
|
450
|
+
end
|
451
|
+
end
|
452
|
+
|
434
453
|
end
|
435
454
|
|
436
455
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module IceCube
|
2
|
+
class TimeStep
|
3
|
+
SECS = 1
|
4
|
+
MINS = 60
|
5
|
+
HOURS = MINS * 60
|
6
|
+
DAYS = HOURS * 24
|
7
|
+
WEEKS = DAYS * 7
|
8
|
+
MONTHS = {
|
9
|
+
1 => [ 28, 29, 30, 31].map { |m| m * DAYS },
|
10
|
+
2 => [ 59, 60, 61, 62].map { |m| m * DAYS },
|
11
|
+
3 => [ 89, 90, 91, 92].map { |m| m * DAYS },
|
12
|
+
4 => [120, 121, 122, 123].map { |m| m * DAYS },
|
13
|
+
5 => [150, 151, 152, 153].map { |m| m * DAYS },
|
14
|
+
6 => [181, 182, 183, 184].map { |m| m * DAYS },
|
15
|
+
7 => [212, 213, 214, 215].map { |m| m * DAYS },
|
16
|
+
8 => [242, 243, 244, 245].map { |m| m * DAYS },
|
17
|
+
9 => [273, 274, 275, 276].map { |m| m * DAYS },
|
18
|
+
10 => [303, 304, 305, 306].map { |m| m * DAYS },
|
19
|
+
11 => [334, 335, 336, 337].map { |m| m * DAYS },
|
20
|
+
12 => [365, 366] .map { |m| m * DAYS }
|
21
|
+
}
|
22
|
+
YEARS = [365, 366]
|
23
|
+
|
24
|
+
def next(base, validations)
|
25
|
+
end
|
26
|
+
|
27
|
+
def prev(base, validations)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/lib/ice_cube/time_util.rb
CHANGED
@@ -1,11 +1,9 @@
|
|
1
1
|
require 'date'
|
2
2
|
|
3
3
|
module IceCube
|
4
|
-
|
5
4
|
module TimeUtil
|
6
5
|
|
7
|
-
|
8
|
-
COMMON_YEAR_MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
6
|
+
extend ::Deprecated
|
9
7
|
|
10
8
|
DAYS = {
|
11
9
|
:sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3,
|
@@ -40,9 +38,13 @@ module IceCube
|
|
40
38
|
# Ensure that this is either nil, or a time
|
41
39
|
def self.ensure_time(time, date_eod = false)
|
42
40
|
case time
|
43
|
-
when DateTime
|
44
|
-
|
45
|
-
|
41
|
+
when DateTime
|
42
|
+
warn "IceCube: DateTime support is deprecated (please use Time)"
|
43
|
+
Time.local(time.year, time.month, time.day, time.hour, time.min, time.sec)
|
44
|
+
when Date
|
45
|
+
date_eod ? end_of_date(time) : time.to_time
|
46
|
+
else
|
47
|
+
time
|
46
48
|
end
|
47
49
|
end
|
48
50
|
|
@@ -51,15 +53,14 @@ module IceCube
|
|
51
53
|
case date
|
52
54
|
when Date then date
|
53
55
|
else
|
54
|
-
return date.
|
55
|
-
return date.to_time.to_date if date.respond_to? :to_time
|
56
|
+
return Date.new(date.year, date.month, date.day)
|
56
57
|
end
|
57
58
|
end
|
58
59
|
|
59
60
|
# Serialize a time appropriate for storing
|
60
61
|
def self.serialize_time(time)
|
61
|
-
if
|
62
|
-
{
|
62
|
+
if time.respond_to?(:time_zone)
|
63
|
+
{:time => time.utc, :zone => time.time_zone.name}
|
63
64
|
elsif time.is_a?(Time)
|
64
65
|
time
|
65
66
|
end
|
@@ -74,9 +75,22 @@ module IceCube
|
|
74
75
|
end
|
75
76
|
end
|
76
77
|
|
78
|
+
# Check the deserialized time offset string against actual local time
|
79
|
+
# offset to try and preserve the original offset for plain Ruby Time. If
|
80
|
+
# the offset is the same as local we can assume the same original zone and
|
81
|
+
# keep it. If it was serialized with a different offset than local TZ it
|
82
|
+
# will lose the zone and not support DST.
|
83
|
+
def self.restore_deserialized_offset(time, orig_offset_str)
|
84
|
+
return time if time.respond_to?(:time_zone) or
|
85
|
+
time.getlocal(orig_offset_str).utc_offset == time.utc_offset
|
86
|
+
warn 'IceCube: parsed Time from nonlocal TZ. Use ActiveSupport to fix DST'
|
87
|
+
time.localtime(orig_offset_str)
|
88
|
+
end
|
89
|
+
|
77
90
|
# Get the beginning of a date
|
78
|
-
def self.beginning_of_date(date, reference=
|
91
|
+
def self.beginning_of_date(date, reference=nil)
|
79
92
|
args = [date.year, date.month, date.day, 0, 0, 0]
|
93
|
+
reference ||= Time.local(*args)
|
80
94
|
if reference.respond_to?(:time_zone) && reference.time_zone
|
81
95
|
reference.time_zone.local(*args)
|
82
96
|
else
|
@@ -85,8 +99,9 @@ module IceCube
|
|
85
99
|
end
|
86
100
|
|
87
101
|
# Get the end of a date
|
88
|
-
def self.end_of_date(date, reference=
|
102
|
+
def self.end_of_date(date, reference=nil)
|
89
103
|
args = [date.year, date.month, date.day, 23, 59, 59]
|
104
|
+
reference ||= Time.local(*args)
|
90
105
|
if reference.respond_to?(:time_zone) && reference.time_zone
|
91
106
|
reference.time_zone.local(*args)
|
92
107
|
else
|
@@ -95,30 +110,30 @@ module IceCube
|
|
95
110
|
end
|
96
111
|
|
97
112
|
# Convert a symbol to a numeric month
|
98
|
-
def self.
|
99
|
-
|
100
|
-
raise "No such month: #{
|
101
|
-
month
|
113
|
+
def self.sym_to_month(sym)
|
114
|
+
return wday = sym if (1..12).include? sym
|
115
|
+
MONTHS.fetch(sym) { |k| raise KeyError, "No such month: #{k}" }
|
102
116
|
end
|
117
|
+
deprecated_alias :symbol_to_month, :sym_to_month
|
103
118
|
|
104
|
-
# Convert a symbol to a
|
105
|
-
def self.
|
106
|
-
|
107
|
-
raise "No such
|
108
|
-
day
|
119
|
+
# Convert a symbol to a wday number
|
120
|
+
def self.sym_to_wday(sym)
|
121
|
+
return sym if (0..6).include? sym
|
122
|
+
DAYS.fetch(sym) { |k| raise KeyError, "No such weekday: #{k}" }
|
109
123
|
end
|
124
|
+
deprecated_alias :symbol_to_day, :sym_to_wday
|
110
125
|
|
111
|
-
# Convert
|
112
|
-
def self.
|
113
|
-
|
114
|
-
|
115
|
-
day
|
126
|
+
# Convert wday number to day symbol
|
127
|
+
def self.wday_to_sym(wday)
|
128
|
+
return sym = wday if DAYS.keys.include? wday
|
129
|
+
DAYS.invert.fetch(wday) { |i| raise KeyError, "No such wday number: #{i}" }
|
116
130
|
end
|
117
131
|
|
118
132
|
# Convert weekday from base sunday to the schedule's week start.
|
119
|
-
def self.
|
120
|
-
(
|
133
|
+
def self.normalize_wday(wday, week_start)
|
134
|
+
(wday - sym_to_wday(week_start)) % 7
|
121
135
|
end
|
136
|
+
deprecated_alias :normalize_weekday, :normalize_wday
|
122
137
|
|
123
138
|
# Return the count of the number of times wday appears in the month,
|
124
139
|
# and which of those time falls on
|
@@ -131,67 +146,57 @@ module IceCube
|
|
131
146
|
|
132
147
|
# Get the days in the month for +time
|
133
148
|
def self.days_in_month(time)
|
134
|
-
|
149
|
+
date = Date.new(time.year, time.month, 1)
|
150
|
+
((date >> 1) - date).to_i
|
135
151
|
end
|
136
152
|
|
153
|
+
# Get the days in the following month for +time
|
137
154
|
def self.days_in_next_month(time)
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
155
|
+
date = Date.new(time.year, time.month, 1) >> 1
|
156
|
+
((date >> 1) - date).to_i
|
157
|
+
end
|
158
|
+
|
159
|
+
# Count the number of days to the same day of the next month without
|
160
|
+
# overflowing shorter months
|
161
|
+
def self.days_to_next_month(time)
|
162
|
+
date = Date.new(time.year, time.month, time.day)
|
163
|
+
(date >> 1) - date
|
147
164
|
end
|
148
165
|
|
149
|
-
|
150
|
-
|
166
|
+
# Get a day of the month in the month of a given time without overflowing
|
167
|
+
# into the next month. Accepts days from positive (start of month forward) or
|
168
|
+
# negative (from end of month)
|
169
|
+
def self.day_of_month(value, date)
|
170
|
+
if value.to_i > 0
|
171
|
+
[value, days_in_month(date)].min
|
172
|
+
else
|
173
|
+
[1 + days_in_month(date) + value, 1].max
|
174
|
+
end
|
151
175
|
end
|
152
176
|
|
153
177
|
# Number of days in a year
|
154
178
|
def self.days_in_year(time)
|
155
|
-
|
179
|
+
date = Date.new(time.year, 1, 1)
|
180
|
+
((date >> 12) - date).to_i
|
156
181
|
end
|
157
182
|
|
158
183
|
# Number of days to n years
|
159
184
|
def self.days_in_n_years(time, year_distance)
|
160
|
-
|
161
|
-
|
162
|
-
year_distance.times do
|
163
|
-
diy = days_in_year(wrapper.to_time)
|
164
|
-
sum += diy
|
165
|
-
wrapper.add(:day, diy)
|
166
|
-
end
|
167
|
-
sum
|
185
|
+
date = Date.new(time.year, time.month, time.day)
|
186
|
+
((date >> year_distance * 12) - date).to_i
|
168
187
|
end
|
169
188
|
|
170
189
|
# The number of days in n months
|
171
190
|
def self.days_in_n_months(time, month_distance)
|
172
|
-
|
173
|
-
|
174
|
-
time -= IceCube::ONE_DAY * (time.day - 27) if time.day >= 28
|
175
|
-
# move n months ahead
|
176
|
-
sum = 0
|
177
|
-
wrapper = TimeWrapper.new(time)
|
178
|
-
month_distance.times do
|
179
|
-
dim = days_in_month(wrapper.to_time)
|
180
|
-
sum += dim
|
181
|
-
wrapper.add(:day, dim)
|
182
|
-
end
|
183
|
-
# now we can move to the desired day
|
184
|
-
dim = days_in_month(wrapper.to_time)
|
185
|
-
if desired_day > dim
|
186
|
-
sum -= desired_day - dim
|
187
|
-
end
|
188
|
-
sum
|
191
|
+
date = Date.new(time.year, time.month, time.day)
|
192
|
+
((date >> month_distance) - date).to_i
|
189
193
|
end
|
190
194
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
+
def self.dst_change(time)
|
196
|
+
one_hour_ago = time - ONE_HOUR
|
197
|
+
if time.dst? ^ one_hour_ago.dst?
|
198
|
+
(time.utc_offset - one_hour_ago.utc_offset) / ONE_HOUR
|
199
|
+
end
|
195
200
|
end
|
196
201
|
|
197
202
|
# A utility class for safely moving time around
|
@@ -248,20 +253,20 @@ module IceCube
|
|
248
253
|
end
|
249
254
|
|
250
255
|
def clear_sec
|
251
|
-
@time -= @time.sec
|
256
|
+
@time.sec > 0 ? @time -= @time.sec : @time
|
252
257
|
end
|
253
258
|
|
254
259
|
def clear_min
|
255
|
-
@time -= (@time.min * ONE_MINUTE)
|
260
|
+
@time.min > 0 ? @time -= (@time.min * ONE_MINUTE) : @time
|
256
261
|
end
|
257
262
|
|
258
263
|
def clear_hour
|
259
|
-
@time -= (@time.hour * ONE_HOUR)
|
264
|
+
@time.hour > 0 ? @time -= (@time.hour * ONE_HOUR) : @time
|
260
265
|
end
|
261
266
|
|
262
267
|
# Move to the first of the month, 0 hours
|
263
268
|
def clear_day
|
264
|
-
@time -= (@time.day - 1) *
|
269
|
+
@time.day > 1 ? @time -= (@time.day - 1) * ONE_DAY : @time
|
265
270
|
end
|
266
271
|
|
267
272
|
# Clear to january 1st
|
@@ -274,10 +279,10 @@ module IceCube
|
|
274
279
|
end
|
275
280
|
|
276
281
|
def clear_year
|
282
|
+
@time
|
277
283
|
end
|
278
284
|
|
279
285
|
end
|
280
286
|
|
281
287
|
end
|
282
|
-
|
283
288
|
end
|
@@ -16,21 +16,28 @@ module IceCube
|
|
16
16
|
include Validations::Count
|
17
17
|
include Validations::Until
|
18
18
|
|
19
|
+
# Validations ordered for efficiency in sequence of:
|
20
|
+
# * descending intervals
|
21
|
+
# * boundary limits
|
22
|
+
# * base values by cardinality (n = 60, 60, 31, 24, 12, 7)
|
23
|
+
# * locks by cardinality (n = 365, 60, 60, 31, 24, 12, 7)
|
24
|
+
# * interval multiplier
|
25
|
+
VALIDATION_ORDER = [
|
26
|
+
:year, :month, :day, :wday, :hour, :min, :sec, :count, :until,
|
27
|
+
:base_sec, :base_min, :base_day, :base_hour, :base_month, :base_wday,
|
28
|
+
:day_of_year, :second_of_minute, :minute_of_hour, :day_of_month,
|
29
|
+
:hour_of_day, :month_of_year, :day_of_week,
|
30
|
+
:interval
|
31
|
+
]
|
32
|
+
|
19
33
|
# Compute the next time after (or including) the specified time in respect
|
20
34
|
# to the given schedule
|
21
35
|
def next_time(time, schedule, closing_time)
|
22
36
|
@time = time
|
23
37
|
@schedule = schedule
|
24
38
|
|
25
|
-
|
26
|
-
# Prevent a non-matching infinite loop
|
27
|
-
return nil if closing_time && @time.to_i > closing_time.to_i
|
28
|
-
end
|
39
|
+
return nil unless find_acceptable_time_before(closing_time)
|
29
40
|
|
30
|
-
# NOTE Uses may be 1 higher than proper here since end_time isn't
|
31
|
-
# validated in this class. This is okay now, since we never expose it -
|
32
|
-
# but if we ever do - we should check that above this line, and return
|
33
|
-
# nil if end_time is past
|
34
41
|
@uses += 1 if @time
|
35
42
|
@time
|
36
43
|
end
|
@@ -89,14 +96,19 @@ module IceCube
|
|
89
96
|
|
90
97
|
private
|
91
98
|
|
92
|
-
# NOTE: optimization target, sort the rules by their type, year first
|
93
|
-
# so we can make bigger jumps more often
|
94
99
|
def finds_acceptable_time?
|
95
|
-
|
96
|
-
validation_accepts_or_updates_time?(
|
100
|
+
validation_names.all? do |type|
|
101
|
+
validation_accepts_or_updates_time?(@validations[type])
|
97
102
|
end
|
98
103
|
end
|
99
104
|
|
105
|
+
def find_acceptable_time_before(boundary)
|
106
|
+
until finds_acceptable_time?
|
107
|
+
return false if past_closing_time?(boundary)
|
108
|
+
end
|
109
|
+
true
|
110
|
+
end
|
111
|
+
|
100
112
|
def validation_accepts_or_updates_time?(validations_for_type)
|
101
113
|
res = validated_results(validations_for_type)
|
102
114
|
# If there is any nil, then we're set - otherwise choose the lowest
|
@@ -136,6 +148,14 @@ module IceCube
|
|
136
148
|
@time = wrapper.to_time
|
137
149
|
end
|
138
150
|
|
151
|
+
def past_closing_time?(closing_time)
|
152
|
+
closing_time && @time > closing_time
|
153
|
+
end
|
154
|
+
|
155
|
+
def validation_names
|
156
|
+
VALIDATION_ORDER & @validations.keys
|
157
|
+
end
|
158
|
+
|
139
159
|
end
|
140
160
|
|
141
161
|
end
|
@@ -5,7 +5,7 @@ module IceCube
|
|
5
5
|
def day_of_week(dows)
|
6
6
|
dows.each do |day, occs|
|
7
7
|
occs.each do |occ|
|
8
|
-
day = TimeUtil.
|
8
|
+
day = TimeUtil.sym_to_wday(day)
|
9
9
|
validations_for(:day_of_week) << Validation.new(day, occ)
|
10
10
|
end
|
11
11
|
end
|
@@ -18,7 +18,7 @@ module IceCube
|
|
18
18
|
attr_reader :day, :occ
|
19
19
|
|
20
20
|
StringBuilder.register_formatter(:day_of_week) do |segments|
|
21
|
-
'on the ' + segments.join('
|
21
|
+
'on the ' + segments.join(' and ')
|
22
22
|
end
|
23
23
|
|
24
24
|
def type
|
@@ -5,7 +5,8 @@ module IceCube
|
|
5
5
|
|
6
6
|
module Validations::Lock
|
7
7
|
|
8
|
-
INTERVALS = {
|
8
|
+
INTERVALS = {:min => 60, :sec => 60, :month => 12, :wday => 7}
|
9
|
+
|
9
10
|
def validate(time, schedule)
|
10
11
|
return send(:"validate_#{type}_lock", time, schedule) unless INTERVALS[type]
|
11
12
|
start = value || schedule.start_time.send(type)
|
@@ -15,31 +16,57 @@ module IceCube
|
|
15
16
|
|
16
17
|
private
|
17
18
|
|
18
|
-
#
|
19
|
-
#
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
if
|
25
|
-
|
19
|
+
# Lock the hour if explicitly set by hour_of_day, but allow for the nearest
|
20
|
+
# hour during DST start to keep the correct interval.
|
21
|
+
#
|
22
|
+
def validate_hour_lock(time, schedule)
|
23
|
+
hour = value || schedule.start_time.send(type)
|
24
|
+
hour = 24 + hour if hour < 0
|
25
|
+
if hour >= time.hour
|
26
|
+
hour - time.hour
|
27
|
+
else
|
28
|
+
if dst_offset = TimeUtil.dst_change(time)
|
29
|
+
hour - time.hour + dst_offset
|
30
|
+
else
|
31
|
+
24 - time.hour + hour
|
32
|
+
end
|
26
33
|
end
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
34
|
+
end
|
35
|
+
|
36
|
+
# For monthly rules that have no specified day value, the validation relies
|
37
|
+
# on the schedule start time and jumps to include every month even if it
|
38
|
+
# has fewer days than the schedule's start day.
|
39
|
+
#
|
40
|
+
# Negative day values (from month end) also include all months.
|
41
|
+
#
|
42
|
+
# Positive day values are taken literally so months with fewer days will
|
43
|
+
# be skipped.
|
44
|
+
#
|
45
|
+
def validate_day_lock(time, schedule)
|
46
|
+
days_in_month = TimeUtil.days_in_month(time)
|
47
|
+
date = Date.new(time.year, time.month, time.day)
|
48
|
+
|
49
|
+
if value && value < 0
|
50
|
+
start = TimeUtil.day_of_month(value, date)
|
51
|
+
month_overflow = days_in_month - TimeUtil.days_in_next_month(time)
|
52
|
+
elsif value && value > 0
|
53
|
+
start = value
|
54
|
+
month_overflow = 0
|
55
|
+
else
|
56
|
+
start = TimeUtil.day_of_month(schedule.start_time.day, date)
|
57
|
+
month_overflow = 0
|
33
58
|
end
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
days_in_this_month + start_two - time.day
|
59
|
+
|
60
|
+
sleeps = start - date.day
|
61
|
+
|
62
|
+
if value && value > 0
|
63
|
+
until_next_month = days_in_month + sleeps
|
40
64
|
else
|
41
|
-
|
65
|
+
until_next_month = TimeUtil.days_to_next_month(date) + sleeps
|
66
|
+
until_next_month -= month_overflow
|
42
67
|
end
|
68
|
+
|
69
|
+
sleeps >= 0 ? sleeps : until_next_month
|
43
70
|
end
|
44
71
|
|
45
72
|
end
|
@@ -4,7 +4,7 @@ module IceCube
|
|
4
4
|
|
5
5
|
def month_of_year(*months)
|
6
6
|
months.each do |month|
|
7
|
-
month = TimeUtil.
|
7
|
+
month = TimeUtil.sym_to_month(month)
|
8
8
|
validations_for(:month_of_year) << Validation.new(month)
|
9
9
|
end
|
10
10
|
clobber_base_validations :month
|
@@ -6,11 +6,16 @@ module IceCube
|
|
6
6
|
|
7
7
|
def interval(interval, week_start = :sunday)
|
8
8
|
@interval = interval
|
9
|
+
@week_start = TimeUtil.wday_to_sym(week_start)
|
9
10
|
validations_for(:interval) << Validation.new(interval, week_start)
|
10
11
|
clobber_base_validations(:day)
|
11
12
|
self
|
12
13
|
end
|
13
14
|
|
15
|
+
def week_start
|
16
|
+
@week_start
|
17
|
+
end
|
18
|
+
|
14
19
|
class Validation
|
15
20
|
|
16
21
|
attr_reader :interval, :week_start
|
@@ -27,12 +32,13 @@ module IceCube
|
|
27
32
|
builder['FREQ'] << 'WEEKLY'
|
28
33
|
unless interval == 1
|
29
34
|
builder['INTERVAL'] << interval
|
30
|
-
builder['WKST'] <<
|
35
|
+
builder['WKST'] << week_start.to_s.upcase[0..1]
|
31
36
|
end
|
32
37
|
end
|
33
38
|
|
34
39
|
def build_hash(builder)
|
35
40
|
builder[:interval] = interval
|
41
|
+
builder[:week_start] = TimeUtil.sym_to_wday(week_start)
|
36
42
|
end
|
37
43
|
|
38
44
|
def initialize(interval, week_start)
|
@@ -45,8 +51,8 @@ module IceCube
|
|
45
51
|
st = schedule.start_time
|
46
52
|
start_date = Date.new(st.year, st.month, st.day)
|
47
53
|
weeks = (
|
48
|
-
(date - TimeUtil.
|
49
|
-
(start_date - TimeUtil.
|
54
|
+
(date - TimeUtil.normalize_wday(date.wday, week_start)) -
|
55
|
+
(start_date - TimeUtil.normalize_wday(start_date.wday, week_start))
|
50
56
|
) / 7
|
51
57
|
unless weeks % interval == 0
|
52
58
|
(interval - (weeks % interval)) * 7
|
data/lib/ice_cube/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -9,3 +9,43 @@ require File.dirname(__FILE__) + '/../lib/ice_cube'
|
|
9
9
|
|
10
10
|
DAY = Time.utc(2010, 3, 1)
|
11
11
|
WEDNESDAY = Time.utc(2010, 6, 23, 5, 0, 0)
|
12
|
+
WORLD_TIME_ZONES = [
|
13
|
+
'America/Anchorage', # -1000 / -0900
|
14
|
+
'Europe/London', # +0000 / +0100
|
15
|
+
'Pacific/Auckland', # +1200 / +1300
|
16
|
+
]
|
17
|
+
|
18
|
+
RSpec.configure do |config|
|
19
|
+
|
20
|
+
config.around :each, :if_active_support_time => true do |example|
|
21
|
+
example.run if defined? ActiveSupport
|
22
|
+
end
|
23
|
+
|
24
|
+
config.around :each, :if_active_support_time => false do |example|
|
25
|
+
unless defined? ActiveSupport
|
26
|
+
stubbed_active_support = ::ActiveSupport = Module.new
|
27
|
+
example.run
|
28
|
+
Object.send :remove_const, :ActiveSupport
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
config.around :each do |example|
|
33
|
+
if zone = example.metadata[:system_time_zone]
|
34
|
+
@orig_zone = ENV['TZ']
|
35
|
+
ENV['TZ'] = zone
|
36
|
+
example.run
|
37
|
+
ENV['TZ'] = @orig_zone
|
38
|
+
else
|
39
|
+
example.run
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
config.before :each do
|
44
|
+
if time_args = @example.metadata[:system_time]
|
45
|
+
case time_args
|
46
|
+
when Array then Time.stub!(:now).and_return Time.local(*time_args)
|
47
|
+
when Time then Time.stub!(:now).and_return time_args
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
metadata
CHANGED
@@ -1,78 +1,69 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ice_cube
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
5
|
-
prerelease:
|
4
|
+
version: 0.10.0
|
6
5
|
platform: ruby
|
7
6
|
authors:
|
8
7
|
- John Crepezzi
|
9
8
|
autorequire:
|
10
9
|
bindir: bin
|
11
10
|
cert_chain: []
|
12
|
-
date: 2013-
|
11
|
+
date: 2013-02-25 00:00:00.000000000 Z
|
13
12
|
dependencies:
|
14
13
|
- !ruby/object:Gem::Dependency
|
15
14
|
name: rake
|
16
15
|
requirement: !ruby/object:Gem::Requirement
|
17
|
-
none: false
|
18
16
|
requirements:
|
19
|
-
- -
|
17
|
+
- - '>='
|
20
18
|
- !ruby/object:Gem::Version
|
21
19
|
version: '0'
|
22
20
|
type: :development
|
23
21
|
prerelease: false
|
24
22
|
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
none: false
|
26
23
|
requirements:
|
27
|
-
- -
|
24
|
+
- - '>='
|
28
25
|
- !ruby/object:Gem::Version
|
29
26
|
version: '0'
|
30
27
|
- !ruby/object:Gem::Dependency
|
31
28
|
name: rspec
|
32
29
|
requirement: !ruby/object:Gem::Requirement
|
33
|
-
none: false
|
34
30
|
requirements:
|
35
|
-
- -
|
31
|
+
- - '>='
|
36
32
|
- !ruby/object:Gem::Version
|
37
33
|
version: '0'
|
38
34
|
type: :development
|
39
35
|
prerelease: false
|
40
36
|
version_requirements: !ruby/object:Gem::Requirement
|
41
|
-
none: false
|
42
37
|
requirements:
|
43
|
-
- -
|
38
|
+
- - '>='
|
44
39
|
- !ruby/object:Gem::Version
|
45
40
|
version: '0'
|
46
41
|
- !ruby/object:Gem::Dependency
|
47
42
|
name: active_support
|
48
43
|
requirement: !ruby/object:Gem::Requirement
|
49
|
-
none: false
|
50
44
|
requirements:
|
51
|
-
- -
|
45
|
+
- - '>='
|
52
46
|
- !ruby/object:Gem::Version
|
53
47
|
version: 3.0.0
|
54
48
|
type: :development
|
55
49
|
prerelease: false
|
56
50
|
version_requirements: !ruby/object:Gem::Requirement
|
57
|
-
none: false
|
58
51
|
requirements:
|
59
|
-
- -
|
52
|
+
- - '>='
|
60
53
|
- !ruby/object:Gem::Version
|
61
54
|
version: 3.0.0
|
62
55
|
- !ruby/object:Gem::Dependency
|
63
56
|
name: tzinfo
|
64
57
|
requirement: !ruby/object:Gem::Requirement
|
65
|
-
none: false
|
66
58
|
requirements:
|
67
|
-
- -
|
59
|
+
- - '>='
|
68
60
|
- !ruby/object:Gem::Version
|
69
61
|
version: '0'
|
70
62
|
type: :development
|
71
63
|
prerelease: false
|
72
64
|
version_requirements: !ruby/object:Gem::Requirement
|
73
|
-
none: false
|
74
65
|
requirements:
|
75
|
-
- -
|
66
|
+
- - '>='
|
76
67
|
- !ruby/object:Gem::Version
|
77
68
|
version: '0'
|
78
69
|
description: ice_cube is a recurring date library for Ruby. It allows for quick,
|
@@ -89,6 +80,7 @@ files:
|
|
89
80
|
- lib/ice_cube/errors/count_exceeded.rb
|
90
81
|
- lib/ice_cube/errors/until_exceeded.rb
|
91
82
|
- lib/ice_cube/flexible_hash.rb
|
83
|
+
- lib/ice_cube/occurrence.rb
|
92
84
|
- lib/ice_cube/rule.rb
|
93
85
|
- lib/ice_cube/rules/daily_rule.rb
|
94
86
|
- lib/ice_cube/rules/hourly_rule.rb
|
@@ -99,6 +91,7 @@ files:
|
|
99
91
|
- lib/ice_cube/rules/yearly_rule.rb
|
100
92
|
- lib/ice_cube/schedule.rb
|
101
93
|
- lib/ice_cube/single_occurrence_rule.rb
|
94
|
+
- lib/ice_cube/time_step.rb
|
102
95
|
- lib/ice_cube/time_util.rb
|
103
96
|
- lib/ice_cube/validated_rule.rb
|
104
97
|
- lib/ice_cube/validations/count.rb
|
@@ -124,28 +117,28 @@ files:
|
|
124
117
|
- lib/ice_cube.rb
|
125
118
|
- spec/spec_helper.rb
|
126
119
|
homepage: http://seejohnrun.github.com/ice_cube/
|
127
|
-
licenses:
|
120
|
+
licenses:
|
121
|
+
- MIT
|
122
|
+
metadata: {}
|
128
123
|
post_install_message:
|
129
124
|
rdoc_options: []
|
130
125
|
require_paths:
|
131
126
|
- lib
|
132
127
|
required_ruby_version: !ruby/object:Gem::Requirement
|
133
|
-
none: false
|
134
128
|
requirements:
|
135
|
-
- -
|
129
|
+
- - '>='
|
136
130
|
- !ruby/object:Gem::Version
|
137
131
|
version: '0'
|
138
132
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
139
|
-
none: false
|
140
133
|
requirements:
|
141
|
-
- -
|
134
|
+
- - '>='
|
142
135
|
- !ruby/object:Gem::Version
|
143
136
|
version: '0'
|
144
137
|
requirements: []
|
145
138
|
rubyforge_project: ice-cube
|
146
|
-
rubygems_version:
|
139
|
+
rubygems_version: 2.0.0
|
147
140
|
signing_key:
|
148
|
-
specification_version:
|
141
|
+
specification_version: 4
|
149
142
|
summary: Ruby Date Recurrence Library
|
150
143
|
test_files:
|
151
144
|
- spec/spec_helper.rb
|