ice_cube_chosko 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/config/locales/en.yml +178 -0
  3. data/config/locales/es.yml +176 -0
  4. data/config/locales/ja.yml +107 -0
  5. data/lib/ice_cube.rb +92 -0
  6. data/lib/ice_cube/builders/hash_builder.rb +27 -0
  7. data/lib/ice_cube/builders/ical_builder.rb +59 -0
  8. data/lib/ice_cube/builders/string_builder.rb +76 -0
  9. data/lib/ice_cube/deprecated.rb +36 -0
  10. data/lib/ice_cube/errors/count_exceeded.rb +7 -0
  11. data/lib/ice_cube/errors/until_exceeded.rb +7 -0
  12. data/lib/ice_cube/flexible_hash.rb +40 -0
  13. data/lib/ice_cube/i18n.rb +24 -0
  14. data/lib/ice_cube/null_i18n.rb +28 -0
  15. data/lib/ice_cube/occurrence.rb +101 -0
  16. data/lib/ice_cube/parsers/hash_parser.rb +91 -0
  17. data/lib/ice_cube/parsers/ical_parser.rb +91 -0
  18. data/lib/ice_cube/parsers/yaml_parser.rb +19 -0
  19. data/lib/ice_cube/rule.rb +123 -0
  20. data/lib/ice_cube/rules/daily_rule.rb +16 -0
  21. data/lib/ice_cube/rules/hourly_rule.rb +16 -0
  22. data/lib/ice_cube/rules/minutely_rule.rb +16 -0
  23. data/lib/ice_cube/rules/monthly_rule.rb +16 -0
  24. data/lib/ice_cube/rules/secondly_rule.rb +15 -0
  25. data/lib/ice_cube/rules/weekly_rule.rb +16 -0
  26. data/lib/ice_cube/rules/yearly_rule.rb +16 -0
  27. data/lib/ice_cube/schedule.rb +529 -0
  28. data/lib/ice_cube/single_occurrence_rule.rb +28 -0
  29. data/lib/ice_cube/time_util.rb +328 -0
  30. data/lib/ice_cube/validated_rule.rb +184 -0
  31. data/lib/ice_cube/validations/count.rb +61 -0
  32. data/lib/ice_cube/validations/daily_interval.rb +54 -0
  33. data/lib/ice_cube/validations/day.rb +71 -0
  34. data/lib/ice_cube/validations/day_of_month.rb +55 -0
  35. data/lib/ice_cube/validations/day_of_week.rb +77 -0
  36. data/lib/ice_cube/validations/day_of_year.rb +61 -0
  37. data/lib/ice_cube/validations/fixed_value.rb +95 -0
  38. data/lib/ice_cube/validations/hour_of_day.rb +55 -0
  39. data/lib/ice_cube/validations/hourly_interval.rb +54 -0
  40. data/lib/ice_cube/validations/lock.rb +95 -0
  41. data/lib/ice_cube/validations/minute_of_hour.rb +54 -0
  42. data/lib/ice_cube/validations/minutely_interval.rb +54 -0
  43. data/lib/ice_cube/validations/month_of_year.rb +54 -0
  44. data/lib/ice_cube/validations/monthly_interval.rb +53 -0
  45. data/lib/ice_cube/validations/schedule_lock.rb +46 -0
  46. data/lib/ice_cube/validations/second_of_minute.rb +54 -0
  47. data/lib/ice_cube/validations/secondly_interval.rb +51 -0
  48. data/lib/ice_cube/validations/until.rb +57 -0
  49. data/lib/ice_cube/validations/weekly_interval.rb +67 -0
  50. data/lib/ice_cube/validations/yearly_interval.rb +53 -0
  51. data/lib/ice_cube/version.rb +5 -0
  52. data/spec/spec_helper.rb +64 -0
  53. metadata +166 -0
@@ -0,0 +1,28 @@
1
+ module IceCube
2
+
3
+ class SingleOccurrenceRule < Rule
4
+
5
+ attr_reader :time
6
+
7
+ def initialize(time)
8
+ @time = TimeUtil.ensure_time time
9
+ end
10
+
11
+ # Always terminating
12
+ def terminating?
13
+ true
14
+ end
15
+
16
+ def next_time(t, schedule, closing_time)
17
+ unless closing_time && closing_time < t
18
+ time if time.to_i >= t.to_i
19
+ end
20
+ end
21
+
22
+ def to_hash
23
+ { :time => time }
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,328 @@
1
+ require 'date'
2
+ require 'time'
3
+
4
+ module IceCube
5
+ module TimeUtil
6
+
7
+ extend Deprecated
8
+
9
+ DAYS = {
10
+ :sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3,
11
+ :thursday => 4, :friday => 5, :saturday => 6
12
+ }
13
+
14
+ ICAL_DAYS = {
15
+ 'SU' => :sunday, 'MO' => :monday, 'TU' => :tuesday, 'WE' => :wednesday,
16
+ 'TH' => :thursday, 'FR' => :friday, 'SA' => :saturday
17
+ }
18
+
19
+ MONTHS = {
20
+ :january => 1, :february => 2, :march => 3, :april => 4, :may => 5,
21
+ :june => 6, :july => 7, :august => 8, :september => 9, :october => 10,
22
+ :november => 11, :december => 12
23
+ }
24
+
25
+ CLOCK_VALUES = [:year, :month, :day, :hour, :min, :sec]
26
+
27
+ # Provides a Time.now without the usec, in the reference zone or utc offset
28
+ def self.now(reference=Time.now)
29
+ match_zone(Time.at(Time.now.to_i), reference)
30
+ end
31
+
32
+ def self.build_in_zone(args, reference)
33
+ if reference.respond_to?(:time_zone)
34
+ reference.time_zone.local(*args)
35
+ elsif reference.utc?
36
+ Time.utc(*args)
37
+ elsif reference.zone
38
+ Time.local(*args)
39
+ else
40
+ Time.new(*args << reference.utc_offset)
41
+ end
42
+ end
43
+
44
+ def self.match_zone(input_time, reference)
45
+ return unless time = ensure_time(input_time)
46
+ time = if reference.respond_to? :time_zone
47
+ time.in_time_zone(reference.time_zone)
48
+ else
49
+ if reference.utc?
50
+ time.utc
51
+ elsif reference.zone
52
+ time.getlocal
53
+ else
54
+ time.getlocal(reference.utc_offset)
55
+ end
56
+ end
57
+ (Date === input_time) ? beginning_of_date(time, reference) : time
58
+ end
59
+
60
+ # Ensure that this is either nil, or a time
61
+ def self.ensure_time(time, date_eod = false)
62
+ case time
63
+ when DateTime
64
+ warn "IceCube: DateTime support is deprecated (please use Time) at: #{ caller[2] }"
65
+ Time.local(time.year, time.month, time.day, time.hour, time.min, time.sec)
66
+ when Date
67
+ date_eod ? end_of_date(time) : time.to_time
68
+ else
69
+ time
70
+ end
71
+ end
72
+
73
+ # Ensure that this is either nil, or a date
74
+ def self.ensure_date(date)
75
+ case date
76
+ when Date then date
77
+ else
78
+ return Date.new(date.year, date.month, date.day)
79
+ end
80
+ end
81
+
82
+ # Serialize a time appropriate for storing
83
+ def self.serialize_time(time)
84
+ if time.respond_to?(:time_zone)
85
+ {:time => time.utc, :zone => time.time_zone.name}
86
+ elsif time.is_a?(Time)
87
+ time
88
+ end
89
+ end
90
+
91
+ # Deserialize a time serialized with serialize_time or in ISO8601 string format
92
+ def self.deserialize_time(time_or_hash)
93
+ case time_or_hash
94
+ when Time
95
+ time_or_hash
96
+ when Hash
97
+ hash = FlexibleHash.new(time_or_hash)
98
+ hash[:time].in_time_zone(hash[:zone])
99
+ when String
100
+ Time.parse(time_or_hash)
101
+ end
102
+ end
103
+
104
+ # Check the deserialized time offset string against actual local time
105
+ # offset to try and preserve the original offset for plain Ruby Time. If
106
+ # the offset is the same as local we can assume the same original zone and
107
+ # keep it. If it was serialized with a different offset than local TZ it
108
+ # will lose the zone and not support DST.
109
+ def self.restore_deserialized_offset(time, orig_offset_str)
110
+ return time if time.respond_to?(:time_zone) ||
111
+ time.getlocal(orig_offset_str).utc_offset == time.utc_offset
112
+ warn "IceCube: parsed Time from nonlocal TZ. Use ActiveSupport to fix DST at: #{ caller[0] }"
113
+ time.localtime(orig_offset_str)
114
+ end
115
+
116
+ # Get the beginning of a date
117
+ def self.beginning_of_date(date, reference=Time.now)
118
+ build_in_zone([date.year, date.month, date.day, 0, 0, 0], reference)
119
+ end
120
+
121
+ # Get the end of a date
122
+ def self.end_of_date(date, reference=Time.now)
123
+ build_in_zone([date.year, date.month, date.day, 23, 59, 59], reference)
124
+ end
125
+
126
+ # Convert a symbol to a numeric month
127
+ def self.sym_to_month(sym)
128
+ MONTHS.fetch(sym) do |k|
129
+ MONTHS.values.detect { |i| i.to_s == k.to_s } or
130
+ raise ArgumentError, "Expecting Fixnum or Symbol value for month. " \
131
+ "No such month: #{k.inspect}"
132
+ end
133
+ end
134
+ deprecated_alias :symbol_to_month, :sym_to_month
135
+
136
+ # Convert a symbol to a wday number
137
+ def self.sym_to_wday(sym)
138
+ DAYS.fetch(sym) do |k|
139
+ DAYS.values.detect { |i| i.to_s == k.to_s } or
140
+ raise ArgumentError, "Expecting Fixnum or Symbol value for weekday. " \
141
+ "No such weekday: #{k.inspect}"
142
+ end
143
+ end
144
+ deprecated_alias :symbol_to_day, :sym_to_wday
145
+
146
+ # Convert wday number to day symbol
147
+ def self.wday_to_sym(wday)
148
+ return sym = wday if DAYS.keys.include? wday
149
+ DAYS.invert.fetch(wday) do |i|
150
+ raise ArgumentError, "Expecting Fixnum value for weekday. " \
151
+ "No such wday number: #{i.inspect}"
152
+ end
153
+ end
154
+
155
+ # Convert a symbol to an ical day (SU, MO)
156
+ def self.week_start(sym)
157
+ raise ArgumentError, "Invalid day: #{str}" unless DAYS.keys.include?(sym)
158
+ day = sym.to_s.upcase[0..1]
159
+ day
160
+ end
161
+
162
+ # Convert weekday from base sunday to the schedule's week start.
163
+ def self.normalize_wday(wday, week_start)
164
+ (wday - sym_to_wday(week_start)) % 7
165
+ end
166
+ deprecated_alias :normalize_weekday, :normalize_wday
167
+
168
+ def self.ical_day_to_symbol(str)
169
+ day = ICAL_DAYS[str]
170
+ raise ArgumentError, "Invalid day: #{str}" if day.nil?
171
+ day
172
+ end
173
+
174
+ # Return the count of the number of times wday appears in the month,
175
+ # and which of those time falls on
176
+ def self.which_occurrence_in_month(time, wday)
177
+ first_occurrence = ((7 - Time.utc(time.year, time.month, 1).wday) + time.wday) % 7 + 1
178
+ this_weekday_in_month_count = ((days_in_month(time) - first_occurrence + 1) / 7.0).ceil
179
+ nth_occurrence_of_weekday = (time.mday - first_occurrence) / 7 + 1
180
+ [nth_occurrence_of_weekday, this_weekday_in_month_count]
181
+ end
182
+
183
+ # Get the days in the month for +time
184
+ def self.days_in_month(time)
185
+ date = Date.new(time.year, time.month, 1)
186
+ ((date >> 1) - date).to_i
187
+ end
188
+
189
+ # Get the days in the following month for +time
190
+ def self.days_in_next_month(time)
191
+ date = Date.new(time.year, time.month, 1) >> 1
192
+ ((date >> 1) - date).to_i
193
+ end
194
+
195
+ # Count the number of days to the same day of the next month without
196
+ # overflowing shorter months
197
+ def self.days_to_next_month(time)
198
+ date = Date.new(time.year, time.month, time.day)
199
+ ((date >> 1) - date).to_i
200
+ end
201
+
202
+ # Get a day of the month in the month of a given time without overflowing
203
+ # into the next month. Accepts days from positive (start of month forward) or
204
+ # negative (from end of month)
205
+ def self.day_of_month(value, date)
206
+ if value.to_i > 0
207
+ [value, days_in_month(date)].min
208
+ else
209
+ [1 + days_in_month(date) + value, 1].max
210
+ end
211
+ end
212
+
213
+ # Number of days in a year
214
+ def self.days_in_year(time)
215
+ date = Date.new(time.year, 1, 1)
216
+ ((date >> 12) - date).to_i
217
+ end
218
+
219
+ # Number of days to n years
220
+ def self.days_in_n_years(time, year_distance)
221
+ date = Date.new(time.year, time.month, time.day)
222
+ ((date >> year_distance * 12) - date).to_i
223
+ end
224
+
225
+ # The number of days in n months
226
+ def self.days_in_n_months(time, month_distance)
227
+ date = Date.new(time.year, time.month, time.day)
228
+ ((date >> month_distance) - date).to_i
229
+ end
230
+
231
+ def self.dst_change(time)
232
+ one_hour_ago = time - ONE_HOUR
233
+ if time.dst? ^ one_hour_ago.dst?
234
+ (time.utc_offset - one_hour_ago.utc_offset) / ONE_HOUR
235
+ end
236
+ end
237
+
238
+ def self.same_clock?(t1, t2)
239
+ CLOCK_VALUES.all? { |i| t1.send(i) == t2.send(i) }
240
+ end
241
+
242
+ # A utility class for safely moving time around
243
+ class TimeWrapper
244
+
245
+ def initialize(time, dst_adjust = true)
246
+ @dst_adjust = dst_adjust
247
+ @time = time
248
+ end
249
+
250
+ # Get the wrapper time back
251
+ def to_time
252
+ @time
253
+ end
254
+
255
+ # DST-safely add an interval of time to the wrapped time
256
+ def add(type, val)
257
+ type = :day if type == :wday
258
+ adjust do
259
+ @time += case type
260
+ when :year then TimeUtil.days_in_n_years(@time, val) * ONE_DAY
261
+ when :month then TimeUtil.days_in_n_months(@time, val) * ONE_DAY
262
+ when :day then val * ONE_DAY
263
+ when :hour then val * ONE_HOUR
264
+ when :min then val * ONE_MINUTE
265
+ when :sec then val
266
+ end
267
+ end
268
+ end
269
+
270
+ # Clear everything below a certain type
271
+ CLEAR_ORDER = [:sec, :min, :hour, :day, :month, :year]
272
+ def clear_below(type)
273
+ type = :day if type == :wday
274
+ CLEAR_ORDER.each do |ptype|
275
+ break if ptype == type
276
+ adjust do
277
+ send(:"clear_#{ptype}")
278
+ end
279
+ end
280
+ end
281
+
282
+ private
283
+
284
+ def adjust(&block)
285
+ if @dst_adjust
286
+ off = @time.utc_offset
287
+ yield
288
+ diff = off - @time.utc_offset
289
+ @time += diff if diff != 0
290
+ else
291
+ yield
292
+ end
293
+ end
294
+
295
+ def clear_sec
296
+ @time.sec > 0 ? @time -= @time.sec : @time
297
+ end
298
+
299
+ def clear_min
300
+ @time.min > 0 ? @time -= (@time.min * ONE_MINUTE) : @time
301
+ end
302
+
303
+ def clear_hour
304
+ @time.hour > 0 ? @time -= (@time.hour * ONE_HOUR) : @time
305
+ end
306
+
307
+ # Move to the first of the month, 0 hours
308
+ def clear_day
309
+ @time.day > 1 ? @time -= (@time.day - 1) * ONE_DAY : @time
310
+ end
311
+
312
+ # Clear to january 1st
313
+ def clear_month
314
+ @time -= ONE_DAY
315
+ until @time.month == 12
316
+ @time -= TimeUtil.days_in_month(@time) * ONE_DAY
317
+ end
318
+ @time += ONE_DAY
319
+ end
320
+
321
+ def clear_year
322
+ @time
323
+ end
324
+
325
+ end
326
+
327
+ end
328
+ end
@@ -0,0 +1,184 @@
1
+ module IceCube
2
+
3
+ class ValidatedRule < Rule
4
+
5
+ include Validations::ScheduleLock
6
+
7
+ include Validations::HourOfDay
8
+ include Validations::MinuteOfHour
9
+ include Validations::SecondOfMinute
10
+ include Validations::DayOfMonth
11
+ include Validations::DayOfWeek
12
+ include Validations::Day
13
+ include Validations::MonthOfYear
14
+ include Validations::DayOfYear
15
+
16
+ include Validations::Count
17
+ include Validations::Until
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
+
33
+ attr_reader :validations
34
+
35
+ def initialize(interval = 1, *)
36
+ @validations = Hash.new
37
+ end
38
+
39
+ def base_interval_validation
40
+ @validations[:interval].first
41
+ end
42
+
43
+ def other_interval_validations
44
+ Array(@validations[base_interval_validation.type])
45
+ end
46
+
47
+ def base_interval_type
48
+ base_interval_validation.type
49
+ end
50
+
51
+ # Compute the next time after (or including) the specified time in respect
52
+ # to the given schedule
53
+ def next_time(time, schedule, closing_time)
54
+ @time = time
55
+ @schedule = schedule
56
+
57
+ return nil unless find_acceptable_time_before(closing_time)
58
+
59
+ @uses += 1 if @time
60
+ @time
61
+ end
62
+
63
+ def skipped_for_dst
64
+ @uses -= 1 if @uses > 0
65
+ end
66
+
67
+ def dst_adjust?
68
+ @validations[:interval].any? &:dst_adjust?
69
+ end
70
+
71
+ def to_s
72
+ builder = StringBuilder.new
73
+ @validations.each do |name, validations|
74
+ validations.each do |validation|
75
+ validation.build_s(builder)
76
+ end
77
+ end
78
+ builder.to_s
79
+ end
80
+
81
+ def to_hash
82
+ builder = HashBuilder.new(self)
83
+ @validations.each do |name, validations|
84
+ validations.each do |validation|
85
+ validation.build_hash(builder)
86
+ end
87
+ end
88
+ builder.to_hash
89
+ end
90
+
91
+ def to_ical
92
+ builder = IcalBuilder.new
93
+ @validations.each do |name, validations|
94
+ validations.each do |validation|
95
+ validation.build_ical(builder)
96
+ end
97
+ end
98
+ builder.to_s
99
+ end
100
+
101
+ # Get the collection that contains validations of a certain type
102
+ def validations_for(key)
103
+ @validations[key] ||= []
104
+ end
105
+
106
+ # Fully replace validations
107
+ def replace_validations_for(key, arr)
108
+ if arr.nil?
109
+ @validations.delete(key)
110
+ else
111
+ @validations[key] = arr
112
+ end
113
+ end
114
+
115
+ # Remove the specified base validations
116
+ def clobber_base_validations(*types)
117
+ types.each do |type|
118
+ @validations.delete(:"base_#{type}")
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ def normalized_interval(interval)
125
+ int = interval.to_i
126
+ raise ArgumentError, "'#{interval}' is not a valid input for interval. Please pass an integer." unless int > 0
127
+ int
128
+ end
129
+
130
+ def finds_acceptable_time?
131
+ validation_names.all? do |type|
132
+ validation_accepts_or_updates_time?(@validations[type])
133
+ end
134
+ end
135
+
136
+ def find_acceptable_time_before(boundary)
137
+ until finds_acceptable_time?
138
+ return false if past_closing_time?(boundary)
139
+ end
140
+ true
141
+ end
142
+
143
+ # Returns true if all validations for the current rule match
144
+ # otherwise false and shifts to the first (largest) unmatched offset
145
+ #
146
+ def validation_accepts_or_updates_time?(validations_for_type)
147
+ res = validations_for_type.each_with_object([]) do |validation, offsets|
148
+ r = validation.validate(@time, @schedule)
149
+ return true if r.nil? || r == 0
150
+ offsets << r
151
+ end
152
+ shift_time_by_validation(res, validations_for_type.first)
153
+ false
154
+ end
155
+
156
+ def shift_time_by_validation(res, validation)
157
+ return unless (interval = res.min)
158
+ wrapper = TimeUtil::TimeWrapper.new(@time, validation.dst_adjust?)
159
+ wrapper.add(validation.type, interval)
160
+ wrapper.clear_below(validation.type)
161
+
162
+ # Move over DST if blocked, no adjustments
163
+ if wrapper.to_time <= @time
164
+ wrapper = TimeUtil::TimeWrapper.new(wrapper.to_time, false)
165
+ until wrapper.to_time > @time
166
+ wrapper.add(:min, 10) # smallest interval
167
+ end
168
+ end
169
+
170
+ # And then get the correct time out
171
+ @time = wrapper.to_time
172
+ end
173
+
174
+ def past_closing_time?(closing_time)
175
+ closing_time && @time > closing_time
176
+ end
177
+
178
+ def validation_names
179
+ VALIDATION_ORDER & @validations.keys
180
+ end
181
+
182
+ end
183
+
184
+ end