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.
@@ -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