ice_cube 0.9.3 → 0.10.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/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
|