ice_cube 0.6.14 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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