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.
@@ -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
@@ -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
@@ -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|
@@ -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
- # two times, inclusive
193
- def occurring_between?(begin_time, closing_time)
194
- occurs_between?(begin_time - duration + 1, closing_time + duration - 1)
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
- ed = extimes; rd = rtimes - ed
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 { |t| t.to_s }
275
+ pieces.concat rrules.map { |t| t.to_s }
271
276
  pieces.concat exrules.map { |t| "not #{t.to_s}" }
272
- pieces.concat ed.sort.map { |t| "not on #{t.strftime(IceCube.to_s_time_format)}" }
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 { |r| "EXRULE:#{r.to_ical}" }
282
- pieces.concat recurrence_times.map { |t| "RDATE#{IcalBuilder.ical_format(t, force_utc)}" }
283
- pieces.concat exception_times.map { |t| "EXDATE#{IcalBuilder.ical_format(t, force_utc)}" }
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
- from_hash IceCube::use_psych? ? Psych::load(yaml) : YAML::load(yaml), options
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
- @all_recurrence_rules.each do |rule|
401
+ min_time = recurrence_rules_with_implicit_start_occurrence.reduce(nil) do |min_time, rule|
394
402
  begin
395
- if res = rule.next_time(time, self, closing_time)
396
- if min_time.nil? || res < min_time
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
- next
406
+ min_time
403
407
  end
404
408
  end
405
- # If there is no match, return nil
406
- return nil unless min_time
407
- # Now make sure that its not an exception_time, and if it is
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
@@ -1,11 +1,9 @@
1
1
  require 'date'
2
2
 
3
3
  module IceCube
4
-
5
4
  module TimeUtil
6
5
 
7
- LEAP_YEAR_MONTH_DAYS = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
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 then time.to_time
44
- when Date then date_eod ? time.to_time.end_of_day : time.to_time
45
- else time
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.to_date if date.respond_to? :to_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 defined?(:ActiveSupport) && const_defined?(:ActiveSupport) && time.is_a?(ActiveSupport::TimeWithZone)
62
- { :time => time.utc, :zone => time.time_zone.name }
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=Time.now)
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=Time.now)
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.symbol_to_month(sym)
99
- month = MONTHS[sym]
100
- raise "No such month: #{sym}" unless 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 numeric day
105
- def self.symbol_to_day(sym)
106
- day = DAYS[sym]
107
- raise "No such day: #{sym}" unless day
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 a symbol to an ical day (SU, MO)
112
- def self.week_start(sym)
113
- raise "No such day: #{sym}" unless DAYS.keys.include?(sym)
114
- day = sym.to_s.upcase[0..1]
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.normalize_weekday(daynum, week_start)
120
- (daynum - symbol_to_day(week_start)) % 7
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
- days_in_month_year(time.month, time.year)
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
- # Get the next month
139
- year = time.year
140
- month = time.month + 1
141
- if month > 12
142
- month %= 12
143
- year += 1
144
- end
145
- # And then determine
146
- days_in_month_year(month, year)
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
- def self.days_in_month_year(month, year)
150
- is_leap?(year) ? LEAP_YEAR_MONTH_DAYS[month - 1] : COMMON_YEAR_MONTH_DAYS[month - 1]
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
- is_leap?(time.year) ? 366 : 365
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
- sum = 0
161
- wrapper = TimeWrapper.new(time)
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
- # move to a safe spot in the month to make this computation
173
- desired_day = time.day
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
- # Given a year, return a boolean indicating whether it is
192
- # a leap year or not
193
- def self.is_leap?(year)
194
- (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0)
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) * IceCube::ONE_DAY
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
- until finds_acceptable_time?
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
- @validations.all? do |name, validations_for_type|
96
- validation_accepts_or_updates_time?(validations_for_type)
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
@@ -6,7 +6,7 @@ module IceCube
6
6
 
7
7
  def day(*days)
8
8
  days.each do |day|
9
- day = TimeUtil.symbol_to_day(day) if day.is_a?(Symbol)
9
+ day = TimeUtil.sym_to_wday(day)
10
10
  validations_for(:day) << Validation.new(day)
11
11
  end
12
12
  clobber_base_validations(:wday, :day)
@@ -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.symbol_to_day(day) if day.is_a?(Symbol)
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(' when it is the ')
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 = { :hour => 24, :min => 60, :sec => 60, :month => 12, :wday => 7 }
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
- # Needs to be custom since we don't know the days in the month
19
- # (meaning, its not a fixed interval)
20
- def validate_day_lock(time, schedule)
21
- start = value || schedule.start_time.day
22
- days_in_this_month = TimeUtil.days_in_month(time)
23
- # If this number is positive, then follow our normal procedure
24
- if start > 0
25
- return start >= time.day ? start - time.day : days_in_this_month - time.day + start
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
- # If the number is negative, and it resolved against the current month
28
- # puts it in the future, just return the difference
29
- days_in_this_month = TimeUtil.days_in_month(time)
30
- start_one = days_in_this_month + start + 1
31
- if start_one >= time.day
32
- return start_one - time.day
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
- # Otherwise, we need to figure out the meaning of the value
35
- # in the next month, and then figure out how to get there
36
- days_in_next_month = TimeUtil.days_in_next_month(time)
37
- start_two = days_in_next_month + start + 1
38
- if start_two >= time.day
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
- days_in_next_month + start_two - time.day
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.symbol_to_month(month) if month.is_a?(Symbol)
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
@@ -34,6 +34,12 @@ module IceCube
34
34
  def build_hash(builder)
35
35
  end
36
36
 
37
+ def dst_adjust?
38
+ case @type
39
+ when :sec, :min then false
40
+ else true
41
+ end
42
+ end
37
43
  end
38
44
 
39
45
  end
@@ -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'] << TimeUtil.week_start(week_start)
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.normalize_weekday(date.wday, week_start)) -
49
- (start_date - TimeUtil.normalize_weekday(start_date.wday, week_start))
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
@@ -1,5 +1,5 @@
1
1
  module IceCube
2
2
 
3
- VERSION = '0.9.3'
3
+ VERSION = '0.10.0'
4
4
 
5
5
  end
@@ -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.9.3
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-01-03 00:00:00.000000000 Z
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: 1.8.24
139
+ rubygems_version: 2.0.0
147
140
  signing_key:
148
- specification_version: 3
141
+ specification_version: 4
149
142
  summary: Ruby Date Recurrence Library
150
143
  test_files:
151
144
  - spec/spec_helper.rb