ice_cube 0.6.14 → 0.7.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 (43) hide show
  1. data/lib/ice_cube.rb +63 -37
  2. data/lib/ice_cube/builders/hash_builder.rb +27 -0
  3. data/lib/ice_cube/builders/ical_builder.rb +59 -0
  4. data/lib/ice_cube/builders/string_builder.rb +74 -0
  5. data/lib/ice_cube/errors/count_exceeded.rb +7 -0
  6. data/lib/ice_cube/errors/until_exceeded.rb +7 -0
  7. data/lib/ice_cube/rule.rb +85 -147
  8. data/lib/ice_cube/rules/daily_rule.rb +5 -27
  9. data/lib/ice_cube/rules/hourly_rule.rb +6 -26
  10. data/lib/ice_cube/rules/minutely_rule.rb +5 -25
  11. data/lib/ice_cube/rules/monthly_rule.rb +6 -30
  12. data/lib/ice_cube/rules/secondly_rule.rb +5 -26
  13. data/lib/ice_cube/rules/weekly_rule.rb +5 -36
  14. data/lib/ice_cube/rules/yearly_rule.rb +8 -34
  15. data/lib/ice_cube/schedule.rb +257 -229
  16. data/lib/ice_cube/single_occurrence_rule.rb +28 -0
  17. data/lib/ice_cube/time_util.rb +202 -76
  18. data/lib/ice_cube/validated_rule.rb +107 -0
  19. data/lib/ice_cube/validations/count.rb +56 -0
  20. data/lib/ice_cube/validations/daily_interval.rb +51 -0
  21. data/lib/ice_cube/validations/day.rb +45 -31
  22. data/lib/ice_cube/validations/day_of_month.rb +44 -44
  23. data/lib/ice_cube/validations/day_of_week.rb +60 -47
  24. data/lib/ice_cube/validations/day_of_year.rb +48 -44
  25. data/lib/ice_cube/validations/hour_of_day.rb +42 -30
  26. data/lib/ice_cube/validations/hourly_interval.rb +50 -0
  27. data/lib/ice_cube/validations/lock.rb +47 -0
  28. data/lib/ice_cube/validations/minute_of_hour.rb +42 -31
  29. data/lib/ice_cube/validations/minutely_interval.rb +50 -0
  30. data/lib/ice_cube/validations/month_of_year.rb +39 -30
  31. data/lib/ice_cube/validations/monthly_interval.rb +47 -0
  32. data/lib/ice_cube/validations/schedule_lock.rb +41 -0
  33. data/lib/ice_cube/validations/second_of_minute.rb +39 -30
  34. data/lib/ice_cube/validations/secondly_interval.rb +50 -0
  35. data/lib/ice_cube/validations/until.rb +49 -0
  36. data/lib/ice_cube/validations/weekly_interval.rb +50 -0
  37. data/lib/ice_cube/validations/yearly_interval.rb +45 -0
  38. data/lib/ice_cube/version.rb +2 -2
  39. data/spec/spec_helper.rb +13 -0
  40. metadata +50 -9
  41. data/lib/ice_cube/rule_occurrence.rb +0 -94
  42. data/lib/ice_cube/validation.rb +0 -44
  43. data/lib/ice_cube/validation_types.rb +0 -137
@@ -0,0 +1,51 @@
1
+ module IceCube
2
+
3
+ module Validations::DailyInterval
4
+
5
+ # Add a new interval validation
6
+ def interval(interval)
7
+ validations_for(:interval) << Validation.new(interval)
8
+ clobber_base_validations(:wday, :day)
9
+ self
10
+ end
11
+
12
+ # A validation for checking to make sure that a time
13
+ # is inside of a certain DailyInterval
14
+ class Validation
15
+
16
+ attr_reader :interval
17
+
18
+ def initialize(interval)
19
+ @interval = interval
20
+ end
21
+
22
+ def build_s(builder)
23
+ builder.base = interval == 1 ? 'Daily' : "Every #{interval} days"
24
+ end
25
+
26
+ def build_hash(builder)
27
+ builder.validations[:interval] = interval
28
+ end
29
+
30
+ def build_ical(builder)
31
+ builder['FREQ'] << 'DAILY'
32
+ end
33
+
34
+ def type
35
+ :day
36
+ end
37
+
38
+ def validate(time, schedule)
39
+ time_date = Date.new(time.year, time.month, time.day)
40
+ start_date = Date.new(schedule.start_time.year, schedule.start_time.month, schedule.start_time.day)
41
+ days = time_date - start_date
42
+ unless days % interval === 0
43
+ interval - (days % interval)
44
+ end
45
+ end
46
+
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -1,41 +1,55 @@
1
- module IceCube
1
+ require 'date'
2
2
 
3
- class DayValidation < Validation
3
+ module IceCube
4
4
 
5
- attr_reader :days
6
-
7
- def initialize(rule)
8
- @days = rule.validations[:day]
9
- @rule = rule
10
- end
5
+ module Validations::Day
11
6
 
12
- def validate(date)
13
- return true if !@days || @days.empty?
14
- @days.include?(date.wday)
15
- end
16
-
17
- def closest(date)
18
- return nil if !@days || @days.empty?
19
- # turn days into distances
20
- days = @days.map do |d|
21
- d > date.wday ? (d - date.wday) : (7 - date.wday + d)
7
+ def day(*days)
8
+ days.each do |day|
9
+ day = TimeUtil.symbol_to_day(day) if day.is_a?(Symbol)
10
+ validations_for(:day) << Validation.new(day)
22
11
  end
23
- days.compact!
24
- # go to the closest distance away, the start of that day
25
- goal = date + days.min * IceCube::ONE_DAY
26
- self.class.adjust(goal, date)
27
- end
28
-
29
- def to_s
30
- days_dup = (@days - @rule.validations[:day_of_week].keys if @rule.validations[:day_of_week]) || @days # don't list twice
31
- 'on ' << self.class.sentence(days_dup.map { |d| Date::DAYNAMES[d] + 's' }) unless days_dup.empty?
12
+ clobber_base_validations(:wday, :day)
13
+ self
32
14
  end
33
15
 
34
- def to_ical
35
- days_dup = (@days - @rule.validations[:day_of_week].keys if @rule.validations[:day_of_week]) || @days # don't list twice
36
- 'BYDAY=' << days_dup.map { |d| IceCube::ICAL_DAYS[d] }.join(',') unless days_dup.empty?
16
+ class Validation
17
+
18
+ include Validations::Lock
19
+
20
+ attr_reader :day
21
+ alias :value :day
22
+
23
+ def initialize(day)
24
+ @day = day
25
+ end
26
+
27
+ def build_s(builder)
28
+ builder.piece(:day) << "#{Date::DAYNAMES[day]}s"
29
+ end
30
+
31
+ def build_hash(builder)
32
+ builder.validations_array(:day) << day
33
+ end
34
+
35
+ def build_ical(builder)
36
+ ical_day = IcalBuilder.fixnum_to_ical_day(day)
37
+ # Only add if there aren't others from day_of_week that override
38
+ if builder['BYDAY'].none? { |b| b.end_with?(ical_day) }
39
+ builder['BYDAY'] << ical_day
40
+ end
41
+ end
42
+
43
+ def type
44
+ :wday
45
+ end
46
+
47
+ StringBuilder.register_formatter(:day) do |segments|
48
+ "on #{StringBuilder.sentence(segments)}"
49
+ end
50
+
37
51
  end
38
-
52
+
39
53
  end
40
54
 
41
55
  end
@@ -1,52 +1,52 @@
1
1
  module IceCube
2
2
 
3
- class DayOfMonthValidation < Validation
3
+ module Validations::DayOfMonth
4
4
 
5
- attr_reader :days_of_month
6
-
7
- def initialize(rule)
8
- @days_of_month = rule.validations[:day_of_month]
9
- end
10
-
11
- def validate(date)
12
- return true if !@days_of_month || @days_of_month.empty?
13
- @days_of_month.include?(date.mday) || @days_of_month.include?(date.mday - TimeUtil.days_in_month(date) - 1)
14
- end
15
-
16
- def closest(date)
17
- return nil if !@days_of_month || @days_of_month.empty?
18
- #get some variables we need
19
- days_in_month = TimeUtil.days_in_month(date)
20
- days_left_in_this_month = days_in_month - date.mday
21
- next_month, next_year = date.month == 12 ? [1, date.year + 1] : [date.month + 1, date.year] #clean way to wrap over years
22
- days_in_next_month = TimeUtil.days_in_month(Time.utc(next_year, next_month, 1))
23
- # create a list of distances
24
- distances = []
25
- @days_of_month.each do |d|
26
- if d > 0
27
- distances << d - date.mday #today is 1, we want 20 (19)
28
- distances << days_left_in_this_month + d #(364 + 20)
29
- elsif d < 0
30
- distances << (days_in_month + d + 1) - date.mday #today is 30, we want -1
31
- distances << (days_in_next_month + d + 1) + days_left_in_this_month #today is 300, we want -70
32
- end
5
+ include Validations::Lock
6
+
7
+ def day_of_month(*days)
8
+ days.each do |day|
9
+ validations_for(:day_of_month) << Validation.new(day)
33
10
  end
34
- #return the lowest distance
35
- distances = distances.select { |d| d > 0 }
36
- return nil if distances.empty?
37
- # return the start of the proper day
38
- goal = date + distances.min * IceCube::ONE_DAY
39
- self.class.adjust(goal, date)
11
+ clobber_base_validations(:day, :wday)
12
+ self
40
13
  end
41
-
42
- def to_s
43
- 'on the ' << self.class.nice_numbers(@days_of_month) << (@days_of_month.size == 1 ? ' day' : ' days') << ' of the month' unless @days_of_month.empty?
44
- end
45
-
46
- def to_ical
47
- 'BYMONTHDAY=' << @days_of_month.join(',') unless @days_of_month.empty?
14
+
15
+ class Validation
16
+
17
+ include Validations::Lock
18
+
19
+ StringBuilder.register_formatter(:day_of_month) do |entries|
20
+ str = "on the #{StringBuilder.sentence(entries)} "
21
+ str << (entries.size == 1 ? 'day of the month' : 'days of the month')
22
+ str
23
+ end
24
+
25
+ attr_reader :day
26
+ alias :value :day
27
+
28
+ def initialize(day)
29
+ @day = day
30
+ end
31
+
32
+ def build_s(builder)
33
+ builder.piece(:day_of_month) << StringBuilder.nice_number(day)
34
+ end
35
+
36
+ def build_hash(builder)
37
+ builder.validations_array(:day_of_month) << day
38
+ end
39
+
40
+ def build_ical(builder)
41
+ builder['BYMONTHDAY'] << day
42
+ end
43
+
44
+ def type
45
+ :day
46
+ end
47
+
48
48
  end
49
-
49
+
50
50
  end
51
-
51
+
52
52
  end
@@ -1,57 +1,70 @@
1
1
  module IceCube
2
2
 
3
- class DayOfWeekValidation < Validation
3
+ module Validations::DayOfWeek
4
4
 
5
- attr_reader :days_of_week
6
-
7
- def initialize(rule)
8
- @days_of_week = rule.validations[:day_of_week]
9
- @rule = rule
10
- end
11
-
12
- def validate(date)
13
- # is it even one of the valid days?
14
- return true if !@days_of_week || @days_of_week.empty?
15
- return false unless @days_of_week.has_key?(date.wday) #shortcut
16
- # does this fall on one of the occurrences?
17
- first_occurrence = ((7 - Time.utc(date.year, date.month, 1).wday) + date.wday) % 7 + 1 #day of first occurrence of a wday in a month
18
- this_weekday_in_month_count = ((TimeUtil.days_in_month(date) - first_occurrence + 1) / 7.0).ceil #how many of these in the month
19
- nth_occurrence_of_weekday = (date.mday - first_occurrence) / 7 + 1 #what occurrence of the weekday is +date+
20
- @days_of_week[date.wday].include?(nth_occurrence_of_weekday) || @days_of_week[date.wday].include?(nth_occurrence_of_weekday - this_weekday_in_month_count - 1)
21
- end
22
-
23
-
24
- # note - temporary implementation
25
- # instead - once we know what weekday starts the month, we should be able to figure out
26
- # the rest with basic math
27
- def closest(date)
28
- return nil if !@days_of_week || @days_of_week.empty?
29
- goal = date
30
- while (next_date = goal + IceCube::ONE_DAY)
31
- # DST hack. If our day starts at midnight, when DST ends it will be pushed to 11 PM on the previous day.
32
- check_date = next_date.day == goal.day ? next_date + IceCube::ONE_HOUR : next_date
33
- return self.class.adjust(next_date, date) if validate(check_date)
34
- goal = next_date
5
+ def day_of_week(dows)
6
+ dows.each do |day, occs|
7
+ occs.each do |occ|
8
+ day = TimeUtil.symbol_to_day(day) if day.is_a?(Symbol)
9
+ validations_for(:day_of_week) << Validation.new(day, occ)
10
+ end
35
11
  end
12
+ clobber_base_validations :day, :wday
13
+ self
36
14
  end
37
15
 
38
- def to_s
39
- representation = ''
40
- representation << 'on the '
41
- representation << @days_of_week.map do |day, occ|
42
- self.class.nice_numbers(occ) << ' ' << Date::DAYNAMES[day] << (occ.size != 1 ? 's' : '') unless @days_of_week.empty?
43
- end.join(' when it is the ')
44
- representation
45
- end
46
-
47
- def to_ical
48
- representation = 'BYDAY='
49
- representation << @days_of_week.map do |day, occ|
50
- occ.map { |o| o.to_s + IceCube::ICAL_DAYS[day] }.join(',')
51
- end.join(',')
52
- representation
16
+ class Validation
17
+
18
+ attr_reader :day, :occ
19
+
20
+ StringBuilder.register_formatter(:day_of_week) do |segments|
21
+ 'on the ' + segments.join(' when it is the ')
22
+ end
23
+
24
+ def type
25
+ :day
26
+ end
27
+
28
+ def build_s(builder)
29
+ builder.piece(:day_of_week) << "#{StringBuilder.nice_number(occ)} #{Date::DAYNAMES[day]}"
30
+ end
31
+
32
+ def build_ical(builder)
33
+ ical_day = IcalBuilder.fixnum_to_ical_day(day)
34
+ # Delete any with this day and no occ first
35
+ builder['BYDAY'].delete_if { |d| d == ical_day }
36
+ builder['BYDAY'] << "#{occ}#{ical_day}"
37
+ end
38
+
39
+ def build_hash(builder)
40
+ builder.validations[:day_of_week] ||= {}
41
+ arr = (builder.validations[:day_of_week][day] ||= [])
42
+ arr << occ
43
+ end
44
+
45
+ def initialize(day, occ)
46
+ @day = day
47
+ @occ = occ
48
+ end
49
+
50
+ def validate(time, schedule)
51
+ # count the days to the weekday
52
+ sum = day >= time.wday ? day - time.wday : 7 - time.wday + day
53
+ wrapper = TimeUtil::TimeWrapper.new(time)
54
+ wrapper.add :day, sum
55
+ # and then count the week until a viable occ
56
+ loop do
57
+ which_occ, num_occ = TimeUtil.which_occurrence_in_month(wrapper.to_time, day)
58
+ this_occ = occ < 0 ? num_occ + occ + 1 : occ
59
+ break if which_occ == this_occ
60
+ sum += 7
61
+ wrapper.add :day, 7 # one week
62
+ end
63
+ sum
64
+ end
65
+
53
66
  end
54
67
 
55
68
  end
56
-
69
+
57
70
  end
@@ -1,51 +1,55 @@
1
1
  module IceCube
2
-
3
- class DayOfYearValidation < Validation
4
2
 
5
- attr_reader :days_of_year
6
-
7
- def initialize(rule)
8
- @days_of_year = rule.validations[:day_of_year]
9
- end
10
-
11
- def validate(date)
12
- return true if !@days_of_year || @days_of_year.empty?
13
- @days_of_year.include?(date.yday) || @days_of_year.include?(date.yday - TimeUtil.days_in_year(date) - 1)
14
- end
15
-
16
- def closest(date)
17
- return nil if !@days_of_year || @days_of_year.empty?
18
- #get some variables we need
19
- days_in_year = TimeUtil.days_in_year(date)
20
- days_left_in_this_year = days_in_year - date.yday
21
- days_in_next_year = TimeUtil.days_in_year(Time.utc(date.year + 1, 1, 1))
22
- # create a list of distances
23
- distances = []
24
- @days_of_year.each do |d|
25
- if d > 0
26
- distances << d - date.yday #today is 1, we want 20 (19)
27
- distances << days_left_in_this_year + d #(364 + 20)
28
- elsif d < 0
29
- distances << (days_in_year + d + 1) - date.yday #today is 300, we want -1
30
- distances << (days_in_next_year + d + 1) + days_left_in_this_year #today is 300, we want -70
31
- end
32
- end
33
- #return the lowest distance
34
- distances = distances.select { |d| d > 0 }
35
- return nil if distances.empty?
36
- # return the start of the proper day
37
- goal = date + distances.min * IceCube::ONE_DAY
38
- self.class.adjust(goal, date)
39
- end
40
-
41
- def to_s
42
- 'on the ' << self.class.nice_numbers(@days_of_year) << (@days_of_year.size == 1 ? ' day' : ' days') << ' of the year' unless @days_of_year.empty?
3
+ module Validations::DayOfYear
4
+
5
+ def day_of_year(*days)
6
+ days.each do |day|
7
+ validations_for(:day_of_year) << Validation.new(day)
8
+ end
9
+ clobber_base_validations(:month, :day, :wday)
10
+ self
43
11
  end
44
-
45
- def to_ical
46
- 'BYYEARDAY=' << @days_of_year.join(',') unless @days_of_year.empty?
12
+
13
+ class Validation
14
+
15
+ attr_reader :day
16
+
17
+ StringBuilder.register_formatter(:day_of_year) do |entries|
18
+ str = "on the #{StringBuilder.sentence(entries)} "
19
+ str << (entries.size == 1 ? 'day of the year' : 'days of the year')
20
+ str
21
+ end
22
+
23
+ def initialize(day)
24
+ @day = day
25
+ end
26
+
27
+ def type
28
+ :day
29
+ end
30
+
31
+ def build_s(builder)
32
+ builder.piece(:day_of_year) << StringBuilder.nice_number(day)
33
+ end
34
+
35
+ def build_hash(builder)
36
+ builder.validations_array(:day_of_year) << day
37
+ end
38
+
39
+ def build_ical(builder)
40
+ builder['BYYEARDAY'] << day
41
+ end
42
+
43
+ def validate(time, schedule)
44
+ days_in_year = TimeUtil.days_in_year(time)
45
+ the_day = day < 0 ? day + days_in_year : day
46
+ # compute the diff
47
+ diff = the_day - time.yday
48
+ diff >= 0 ? diff : diff + days_in_year
49
+ end
50
+
47
51
  end
48
-
52
+
49
53
  end
50
54
 
51
55
  end