ice_cube_chosko 0.1.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.
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