ice_cube 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
data/lib/ice_cube.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'yaml.rb'
2
+ require 'set.rb'
3
+
4
+ require 'ice_cube/time_util'
5
+
6
+ require 'ice_cube/validations/month_of_year'
7
+ require 'ice_cube/validations/day_of_year'
8
+ require 'ice_cube/validations/day_of_month'
9
+ require 'ice_cube/validations/day_of_week'
10
+ require 'ice_cube/validations/day'
11
+ require 'ice_cube/validations/hour_of_day'
12
+ require 'ice_cube/validations/minute_of_hour'
13
+ require 'ice_cube/validations/second_of_minute'
14
+
15
+ require 'ice_cube/rule'
16
+ require 'ice_cube/schedule'
17
+ require 'ice_cube/rule_occurrence'
18
+
19
+ module IceCube
20
+
21
+ autoload :DailyRule, 'ice_cube/rules/daily_rule'
22
+ autoload :WeeklyRule, 'ice_cube/rules/weekly_rule'
23
+ autoload :MonthlyRule, 'ice_cube/rules/monthly_rule'
24
+ autoload :YearlyRule, 'ice_cube/rules/yearly_rule'
25
+
26
+ autoload :HourlyRule, 'ice_cube/rules/hourly_rule'
27
+ autoload :MinutelyRule, 'ice_cube/rules/minutely_rule'
28
+ autoload :SecondlyRule, 'ice_cube/rules/secondly_rule'
29
+
30
+ VERSION = '0.2.2'
31
+
32
+ ONE_DAY = 24 * 60 * 60
33
+ ONE_HOUR = 60 * 60
34
+ ONE_MINUTE = 60
35
+ ONE_SECOND = 1
36
+
37
+ ICAL_DAYS = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA']
38
+ DAYS = { :sunday => 0, :monday => 1, :tuesday => 2, :wednesday => 3, :thursday => 4, :friday => 5, :saturday => 6 }
39
+ MONTHS = { :january => 1, :february => 2, :march => 3, :april => 4, :may => 5, :june => 6, :july => 7, :august => 8,
40
+ :september => 9, :october => 10, :november => 11, :december => 12 }
41
+
42
+ include TimeUtil
43
+
44
+ end
@@ -0,0 +1,169 @@
1
+ module IceCube
2
+
3
+ class Rule
4
+
5
+ attr_reader :occurrence_count, :until_date
6
+
7
+ SuggestionTypes = []
8
+ include MonthOfYearValidation, DayOfYearValidation, DayOfMonthValidation, DayOfWeekValidation, DayValidation
9
+ include HourOfDayValidation, MinuteOfHourValidation, SecondOfMinuteValidation
10
+
11
+ def to_hash
12
+ hash = Hash.new
13
+ hash[:rule_type] = self.class.name
14
+ hash[:interval] = @interval
15
+ hash[:until] = @until_date
16
+ hash[:count] = @occurrence_count
17
+ hash[:validations] = @validations
18
+ hash
19
+ end
20
+
21
+ def self.from_hash(hash)
22
+ rule = hash[:rule_type].split('::').inject(Object) { |namespace, const_name| namespace.const_get(const_name) }.new(hash[:interval])
23
+ rule.count(hash[:count]) if hash[:count]
24
+ rule.until(hash[:until]) if hash[:until]
25
+ rule.validations = hash[:validations]
26
+ rule
27
+ end
28
+
29
+ # create a new daily rule
30
+ def self.daily(interval = 1)
31
+ DailyRule.new(interval)
32
+ end
33
+
34
+ # create a new weekly rule
35
+ def self.weekly(interval = 1)
36
+ WeeklyRule.new(interval)
37
+ end
38
+
39
+ # create a new monthly rule
40
+ def self.monthly(interval = 1)
41
+ MonthlyRule.new(interval)
42
+ end
43
+
44
+ # create a new yearly rule
45
+ def self.yearly(interval = 1)
46
+ YearlyRule.new(interval)
47
+ end
48
+
49
+ # create a new hourly rule
50
+ def self.hourly(interval = 1)
51
+ HourlyRule.new(interval)
52
+ end
53
+
54
+ # create a new minutely rule
55
+ def self.minutely(interval = 1)
56
+ MinutelyRule.new(interval)
57
+ end
58
+
59
+ # create a new secondly rule
60
+ def self.secondly(interval = 1)
61
+ SecondlyRule.new(interval)
62
+ end
63
+
64
+ # Set the time when this rule will no longer be effective
65
+ def until(until_date)
66
+ raise ArgumentError.new('Cannot specify until and count on the same rule') if @count #as per rfc
67
+ raise ArgumentError.new('Argument must be a valid Time') unless until_date.class == Time
68
+ @until_date = until_date
69
+ self
70
+ end
71
+
72
+ # set the number of occurrences after which this rule is no longer effective
73
+ def count(count)
74
+ raise ArgumentError.new('Argument must be a positive integer') unless Integer(count) && count >= 0
75
+ @occurrence_count = count
76
+ self
77
+ end
78
+
79
+ def validate_single_date(date)
80
+ SuggestionTypes.all? do |s|
81
+ response = send("validate_#{s}", date)
82
+ response.nil? || response
83
+ end
84
+ end
85
+
86
+ # The key - extremely educated guesses
87
+ # This spidering behavior will go through look for the next suggestion
88
+ # by constantly moving the farthest back value forward
89
+ def next_suggestion(date)
90
+ # get the next date recommendation set
91
+ suggestions = SuggestionTypes.map { |r| send("closest_#{r}", date) }
92
+ compact_suggestions = suggestions.compact
93
+ # find the next date to go to
94
+ if compact_suggestions.empty?
95
+ next_date = date
96
+ loop do
97
+ # keep going through rule suggestions
98
+ next_date = self.default_jump(next_date)
99
+ return next_date if validate_single_date(next_date)
100
+ end
101
+ else
102
+ loop do
103
+ compact_suggestions = suggestions.compact
104
+ min_suggestion = compact_suggestions.min
105
+ # validate all against the minimum
106
+ return min_suggestion if validate_single_date(min_suggestion)
107
+ # move anything that is the minimum to its next closest
108
+ SuggestionTypes.each_with_index do |r, index|
109
+ suggestions[index] = send("closest_#{r}", min_suggestion) if min_suggestion == suggestions[index]
110
+ end
111
+ end
112
+ end
113
+ end
114
+
115
+ def to_s
116
+ to_ical
117
+ end
118
+
119
+ attr_accessor :validations
120
+
121
+ private
122
+
123
+ def adjust(goal, date)
124
+ return goal if goal.utc_offset == date.utc_offset
125
+ goal - goal.utc_offset + date.utc_offset
126
+ end
127
+
128
+ #TODO - until date formatting is not iCalendar here
129
+ #get the icalendar representation of this rule logic
130
+ def to_ical_base
131
+ representation = ''
132
+ representation << ";INTERVAL=#{@interval}" if @interval > 1
133
+ representation << ';BYMONTH=' << @validations[:month_of_year].join(',') if @validations[:month_of_year]
134
+ representation << ';BYYEARDAY=' << @validations[:day_of_year].join(',') if @validations[:day_of_year]
135
+ representation << ';BYMONTHDAY=' << @validations[:day_of_month].join(',') if @validations[:day_of_month]
136
+ if @validations[:day] || @validations[:day_of_week]
137
+ representation << ';BYDAY='
138
+ days_dedup = @validations[:day].dup if @validations[:day]
139
+ #put days on the string, remove all occurrences in days from days_of_week
140
+ if days_dedup
141
+ @validations[:day_of_week].keys.each { |day| days_dedup.delete(day) } if @validations[:day_of_week]
142
+ representation << (days_dedup.map { |d| ICAL_DAYS[d]} ).join(',')
143
+ end
144
+ representation << ',' if days_dedup && @validations[:day_of_week]
145
+ #put days_of_week on string representation
146
+ representation << @validations[:day_of_week].inject([]) do |day_rules, pair|
147
+ day, occ = *pair
148
+ day_rules.concat(occ.map {|v| v.to_s + ICAL_DAYS[day]})
149
+ end.flatten.join(',') if @validations[:day_of_week]
150
+ end
151
+ representation << ';BYHOUR=' << @validations[:hour_of_day].join(',') if @validations[:hour_of_day]
152
+ representation << ';BYMINUTE=' << @validations[:minute_of_hour].join(',') if @validations[:minute_of_hour]
153
+ representation << ';BYSECOND=' << @validations[:second_of_minute].join(',') if @validations[:second_of_minute]
154
+ representation << ";COUNT=#{@occurrence_count}" if @occurrence_count
155
+ representation << ";UNTIL=#{@until_date}" if @until_date
156
+ representation
157
+ end
158
+
159
+ # Set the interval for the rule. Depending on the type of rule,
160
+ # interval means every (n) weeks, months, etc. starting on the start_date's
161
+ def initialize(interval = 1)
162
+ throw ArgumentError.new('Interval must be > 0') unless interval > 0
163
+ @validations = {}
164
+ @interval = interval
165
+ end
166
+
167
+ end
168
+
169
+ end
@@ -0,0 +1,73 @@
1
+ module IceCube
2
+
3
+ class RuleOccurrence
4
+
5
+ include Comparable
6
+
7
+ #allow to be compared to dates
8
+ def <=>(other)
9
+ to_time <=> other
10
+ end
11
+
12
+ def to_time
13
+ @date
14
+ end
15
+
16
+ def all_occurrences
17
+ raise ArgumentError.new("Rule must specify either an until date or a count to use 'all_occurrences'") unless @rule.occurrence_count || @rule.until_date
18
+ find_occurrences { |roc| false }
19
+ end
20
+
21
+ def upto(end_date)
22
+ find_occurrences { |roc| roc > end_date }
23
+ end
24
+
25
+ def first(n)
26
+ count = 0
27
+ find_occurrences { |roc| count += 1; count > n }
28
+ end
29
+
30
+ #get the next occurrence of this rule
31
+ def succ
32
+ return nil if @rule.occurrence_count && @index >= @rule.occurrence_count # count check
33
+ # get the next date to walk to
34
+ if @date.nil?
35
+ date = @start_date if @rule.validate_single_date(@start_date)
36
+ date = @rule.next_suggestion(@start_date) unless date
37
+ else
38
+ date = @rule.next_suggestion(@date)
39
+ end
40
+ #walk through all of the successive dates, looking for the next occurrence (interval-valid), then return it.
41
+ begin
42
+ return nil if @rule.until_date && date > @rule.until_date # until check
43
+ return RuleOccurrence.new(@rule, @start_date, date, @index + 1) if @rule.in_interval?(date, @start_date)
44
+ end while date = @rule.next_suggestion(date)
45
+ end
46
+
47
+ attr_reader :rule
48
+
49
+ private
50
+
51
+ def find_occurrences
52
+ include_dates = []
53
+ roc = self
54
+ begin
55
+ break if roc.nil? #go until we run out of dates
56
+ next if roc.to_time.nil? #handle the case where start_date is not a valid occurrence
57
+ break if yield(roc) #recurrence condition
58
+ include_dates << roc.to_time
59
+ end while roc = roc.succ
60
+ include_dates
61
+ end
62
+
63
+ def initialize(rule, start_date, date = nil, index = 0)
64
+ #set some variables
65
+ @rule = rule
66
+ @date = date
67
+ @start_date = start_date
68
+ @index = index
69
+ end
70
+
71
+ end
72
+
73
+ end
@@ -0,0 +1,31 @@
1
+ module IceCube
2
+
3
+ class DailyRule < Rule
4
+
5
+ # Determine whether this rule occurs on a give date.
6
+ def in_interval?(date, start_date)
7
+ #make sure we're in a proper interval
8
+ day_count = ((date - start_date) / ONE_DAY).to_i
9
+ day_count % @interval == 0
10
+ end
11
+
12
+ def to_ical
13
+ 'FREQ=DAILY' << to_ical_base
14
+ end
15
+
16
+ protected
17
+
18
+ def default_jump(date)
19
+ goal = date + ONE_DAY * @interval
20
+ adjust(goal, date)
21
+ end
22
+
23
+ private
24
+
25
+ def initialize(interval)
26
+ super(interval)
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,30 @@
1
+ module IceCube
2
+
3
+ class HourlyRule < Rule
4
+
5
+ # Determine whether this rule occurs on a give date.
6
+ def in_interval?(date, start_date)
7
+ #make sure we're in a proper interval
8
+ day_count = ((date - start_date) / ONE_HOUR).to_i
9
+ day_count % @interval == 0
10
+ end
11
+
12
+ def to_ical
13
+ 'FREQ=HOURLY' << to_ical_base
14
+ end
15
+
16
+ protected
17
+
18
+ def default_jump(date)
19
+ date + ONE_HOUR * @interval
20
+ end
21
+
22
+ private
23
+
24
+ def initialize(interval)
25
+ super(interval)
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,30 @@
1
+ module IceCube
2
+
3
+ class MinutelyRule < Rule
4
+
5
+ # Determine whether this rule occurs on a give date.
6
+ def in_interval?(date, start_date)
7
+ #make sure we're in a proper interval
8
+ day_count = ((date - start_date) / ONE_MINUTE).to_i
9
+ day_count % @interval == 0
10
+ end
11
+
12
+ def to_ical
13
+ 'FREQ=MINUTELY' << to_ical_base
14
+ end
15
+
16
+ protected
17
+
18
+ def default_jump(date)
19
+ date + ONE_MINUTE
20
+ end
21
+
22
+ private
23
+
24
+ def initialize(interval)
25
+ super(interval)
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,36 @@
1
+ module IceCube
2
+
3
+ class MonthlyRule < Rule
4
+
5
+ # Determine for a given date/start_date if this rule occurs or not.
6
+ # Month rules occur if we're in a valid interval
7
+ # and either (1) we're on a valid day of the week (ie: first sunday of the month)
8
+ # or we're on a valid day of the month (1, 15, -1)
9
+ # Note: Rollover is not implemented, so the 35th day of the month is invalid.
10
+ def in_interval?(date, start_date)
11
+ #make sure we're in the proper interval
12
+ months_to_start_date = (date.month - start_date.month) + (date.year - start_date.year) * 12
13
+ months_to_start_date % @interval == 0
14
+ end
15
+
16
+ def to_ical
17
+ 'FREQ=MONTHLY' << to_ical_base
18
+ end
19
+
20
+ protected
21
+
22
+ def default_jump(date)
23
+ date_type = date.utc? ? :utc : :local
24
+ next_month = date.month + @interval
25
+ Time.send(date_type, date.year + next_month / 12, (next_month - 1) % 12 + 1, date.day, date.hour, date.min, date.sec)
26
+ end
27
+
28
+ private
29
+
30
+ def initialize(interval)
31
+ super(interval)
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,30 @@
1
+ module IceCube
2
+
3
+ class SecondlyRule < Rule
4
+
5
+ # Determine whether this rule occurs on a give date.
6
+ def in_interval?(date, start_date)
7
+ #make sure we're in a proper interval
8
+ day_count = date - start_date
9
+ day_count % @interval == 0
10
+ end
11
+
12
+ def to_ical
13
+ 'FREQ=SECONDLY' << to_ical_base
14
+ end
15
+
16
+ protected
17
+
18
+ def default_jump(date)
19
+ date + 1
20
+ end
21
+
22
+ private
23
+
24
+ def initialize(interval)
25
+ super(interval)
26
+ end
27
+
28
+ end
29
+
30
+ end
@@ -0,0 +1,33 @@
1
+ module IceCube
2
+
3
+ class WeeklyRule < Rule
4
+
5
+ # Determine whether or not this rule occurs on a given date.
6
+ # Weekly rules occurs if we're in one of the interval weeks,
7
+ # and we're in a valid day of the week.
8
+ def in_interval?(date, start_date)
9
+ #make sure we're in the right interval
10
+ week_of_year = Date.civil(date.year, date.month, date.day).cweek
11
+ week_of_year % @interval == 0
12
+ end
13
+
14
+ def to_ical
15
+ 'FREQ=WEEKLY' << to_ical_base
16
+ end
17
+
18
+ protected
19
+
20
+ def default_jump(date)
21
+ goal = date + 7 * ONE_DAY * @interval
22
+ adjust(goal, date)
23
+ end
24
+
25
+ private
26
+
27
+ def initialize(interval)
28
+ super(interval)
29
+ end
30
+
31
+ end
32
+
33
+ end
@@ -0,0 +1,36 @@
1
+ module IceCube
2
+
3
+ class YearlyRule < Rule
4
+
5
+ # Determine whether or not the rule, given a start_date,
6
+ # occurs on a given date.
7
+ # Yearly occurs if we're in a proper interval
8
+ # and either (1) we're on a day of the year, or (2) we're on a month of the year as specified
9
+ # Note: rollover dates don't work, so you can't ask for the 400th day of a year
10
+ # and expect to roll into the next year (this might be a possible direction in the future)
11
+ def in_interval?(date, start_date)
12
+ #make sure we're in the proper interval
13
+ (date.year - start_date.year) % @interval == 0
14
+ end
15
+
16
+ def to_ical
17
+ 'FREQ=YEARLY' << to_ical_base
18
+ end
19
+
20
+ protected
21
+
22
+ # one year from now, the same month and day of the year
23
+ def default_jump(date)
24
+ date_type = date.utc? ? :utc : :local
25
+ Time.send(date_type, date.year + @interval, date.month, date.day, date.hour, date.min, date.sec)
26
+ end
27
+
28
+ private
29
+
30
+ def initialize(interval)
31
+ super(interval)
32
+ end
33
+
34
+ end
35
+
36
+ end
@@ -0,0 +1,108 @@
1
+ module IceCube
2
+
3
+ class Schedule
4
+
5
+ def initialize(start_date)
6
+ @rrule_occurrence_heads = []
7
+ @exrule_occurrence_heads = []
8
+ @rdates = []
9
+ @exdates = []
10
+ @start_date = start_date
11
+ end
12
+
13
+ def to_hash
14
+ hash = Hash.new
15
+ hash[:start_date] = @start_date
16
+ hash[:rrules] = @rrule_occurrence_heads.map { |rr| rr.rule.to_hash }
17
+ hash[:exrules] = @exrule_occurrence_heads.map { |ex| ex.rule.to_hash }
18
+ hash[:rdates] = @rdates
19
+ hash[:exdates] = @exdates
20
+ hash
21
+ end
22
+
23
+ def to_yaml
24
+ to_hash.to_yaml
25
+ end
26
+
27
+ def self.from_hash(hash)
28
+ schedule = Schedule.new(hash[:start_date])
29
+ hash[:rrules].each { |rr| schedule.add_recurrence_rule Rule.from_hash(rr) }
30
+ hash[:exrules].each { |ex| schedule.add_exception_rule Rule.from_hash(ex) }
31
+ hash[:rdates].each { |rd| schedule.add_recurrence_date rd }
32
+ hash[:exdates].each { |ed| schedule.add_exception_date ed }
33
+ schedule
34
+ end
35
+
36
+ def self.from_yaml(str)
37
+ from_hash(YAML::load(str))
38
+ end
39
+
40
+ # Determine whether a given date adheres to the ruleset of this schedule.
41
+ def occurs_on?(date)
42
+ dates = occurrences(date)
43
+ dates.last == date
44
+ end
45
+
46
+ # Return all possible occurrences
47
+ # In order to make this call, all rules in the schedule must have
48
+ # either an until date or an occurrence count
49
+ def all_occurrences
50
+ find_occurrences { |head| head.all_occurrences }
51
+ end
52
+
53
+ # Find all occurrences until a certain date
54
+ def occurrences(end_date)
55
+ find_occurrences { |head| head.upto(end_date) }
56
+ end
57
+
58
+ def first(n)
59
+ dates = find_occurrences { |head| head.first(n) }
60
+ dates.slice(0, n)
61
+ end
62
+
63
+ # Add a rule of any type as an recurrence in this schedule
64
+ def add_recurrence_rule(rule)
65
+ raise ArgumentError.new('Argument must be a valid rule') unless rule.class < Rule
66
+ @rrule_occurrence_heads << RuleOccurrence.new(rule, @start_date)
67
+ end
68
+
69
+ # Add a rule of any type as an exception to this schedule
70
+ def add_exception_rule(rule)
71
+ raise ArgumentError.new('Argument must be a valid rule') unless rule.class < Rule
72
+ @exrule_occurrence_heads << RuleOccurrence.new(rule, @start_date)
73
+ end
74
+
75
+ # Add an individual date to this schedule
76
+ def add_recurrence_date(date)
77
+ raise ArgumentError.new('Argument must be a valid Time') unless date.class == Time
78
+ @rdates << date
79
+ end
80
+
81
+ # Add an individual date exception to this schedule
82
+ def add_exception_date(date)
83
+ raise ArgumentError.new('Argument must be a valid Time') unless date.class == Time
84
+ @exdates << date
85
+ end
86
+
87
+ private
88
+
89
+ # Find all occurrences (following rules and exceptions) from the schedule's start date to end_date.
90
+ # Use custom methods to say when to end
91
+ def find_occurrences
92
+ exclude_dates, include_dates = Set.new(@exdates), SortedSet.new(@rdates)
93
+ # walk through each rule, adding it to dates
94
+ @rrule_occurrence_heads.each do |rrule_occurrence_head|
95
+ include_dates.merge(yield(rrule_occurrence_head))
96
+ end
97
+ # walk through each exrule, removing it from dates
98
+ @exrule_occurrence_heads.each do |exrule_occurrence_head|
99
+ exclude_dates.merge(yield(exrule_occurrence_head))
100
+ end
101
+ #return a unique list of dates
102
+ include_dates.reject! { |date| exclude_dates.include?(date) }
103
+ include_dates.to_a
104
+ end
105
+
106
+ end
107
+
108
+ end
@@ -0,0 +1,18 @@
1
+ module TimeUtil
2
+
3
+ LeapYearMonthDays = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
4
+ CommonYearMonthDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
5
+
6
+ def self.is_leap?(date)
7
+ (date.year % 4 == 0 && date.year % 100 != 0) || (date.year % 400 == 0)
8
+ end
9
+
10
+ def self.days_in_year(date)
11
+ is_leap?(date) ? 366 : 365
12
+ end
13
+
14
+ def self.days_in_month(date)
15
+ is_leap?(date) ? LeapYearMonthDays[date.month - 1] : CommonYearMonthDays[date.month - 1]
16
+ end
17
+
18
+ end
@@ -0,0 +1,36 @@
1
+ module DayValidation
2
+
3
+ def self.included(base)
4
+ base::SuggestionTypes << :day
5
+ end
6
+
7
+ # Specify what days of the week this rule should occur on.
8
+ # ie: Schedule.weekly.day_of_week(:monday) would create a rule that
9
+ # occurs every monday.
10
+ def day(*days)
11
+ @validations[:day] ||= []
12
+ days.each do |day|
13
+ raise ArgumentError.new('Argument must be a valid day of the week') unless DAYS.has_key?(day)
14
+ @validations[:day] << DAYS[day]
15
+ end
16
+ self
17
+ end
18
+
19
+ def validate_day(date)
20
+ return true if !@validations[:day] || @validations[:day].empty?
21
+ @validations[:day].include?(date.wday)
22
+ end
23
+
24
+ def closest_day(date)
25
+ return nil if !@validations[:day] || @validations[:day].empty?
26
+ # turn days into distances
27
+ days = @validations[:day].map do |d|
28
+ d > date.wday ? (d - date.wday) : (7 - date.wday + d)
29
+ end
30
+ days.compact!
31
+ # go to the closest distance away, the start of that day
32
+ goal = date + days.min * ONE_DAY
33
+ adjust(goal, date)
34
+ end
35
+
36
+ end
@@ -0,0 +1,51 @@
1
+ module DayOfMonthValidation
2
+
3
+ def self.included(base)
4
+ base::SuggestionTypes << :day_of_month
5
+ end
6
+
7
+ # Specify the days of the month that this rule should
8
+ # occur on. ie: rule.day_of_month(1, -1) would mean that
9
+ # this rule should occur on the first and last day of every month.
10
+ def day_of_month(*days)
11
+ @validations[:day_of_month] ||= []
12
+ days.each do |day|
13
+ raise ArgumentError.new('Argument must be a valid date') if day.abs > 31
14
+ raise ArgumentError.new('Argument must be non-zero') if day == 0
15
+ @validations[:day_of_month] << day
16
+ end
17
+ self
18
+ end
19
+
20
+ def validate_day_of_month(date)
21
+ return true if !@validations[:day_of_month] || @validations[:day_of_month].empty?
22
+ @validations[:day_of_month].include?(date.mday) || @validations[:day_of_month].include?(date.mday - TimeUtil.days_in_month(date) - 1)
23
+ end
24
+
25
+ def closest_day_of_month(date)
26
+ return nil if !@validations[:day_of_month] || @validations[:day_of_month].empty?
27
+ #get some variables we need
28
+ days_in_month = TimeUtil.days_in_month(date)
29
+ days_left_in_this_month = days_in_month - date.mday
30
+ next_month, next_year = date.month == 12 ? [1, date.year + 1] : [date.month + 1, date.year] #clean way to wrap over years
31
+ days_in_next_month = TimeUtil.days_in_month(Time.utc(next_year, next_month, 1))
32
+ # create a list of distances
33
+ distances = []
34
+ @validations[:day_of_month].each do |d|
35
+ if d > 0
36
+ distances << d - date.mday #today is 1, we want 20 (19)
37
+ distances << days_left_in_this_month + d #(364 + 20)
38
+ elsif d < 0
39
+ distances << (days_in_month + d + 1) - date.mday #today is 30, we want -1
40
+ distances << (days_in_next_month + d + 1) + days_left_in_this_month #today is 300, we want -70
41
+ end
42
+ end
43
+ #return the lowest distance
44
+ distances = distances.select { |d| d > 0 }
45
+ return nil if distances.empty?
46
+ # return the start of the proper day
47
+ goal = date + distances.min * ONE_DAY
48
+ adjust(goal, date)
49
+ end
50
+
51
+ end
@@ -0,0 +1,41 @@
1
+ module DayOfWeekValidation
2
+
3
+ def self.included(base)
4
+ base::SuggestionTypes << :day_of_week
5
+ end
6
+
7
+ # Specify the day(s) of the week that this rule should occur
8
+ # on. ie: rule.day_of_week(:monday => [1, -1]) would mean
9
+ # that this rule should occur on the first and last mondays of each month.
10
+ def day_of_week(days)
11
+ @validations[:day_of_week] ||= {}
12
+ days.each do |day, occurrences|
13
+ raise ArgumentError.new('Argument must be a valid day') unless DAYS.has_key?(day)
14
+ @validations[:day_of_week][DAYS[day]] ||= []
15
+ @validations[:day_of_week][DAYS[day]].concat(occurrences)
16
+ end
17
+ self
18
+ end
19
+
20
+ def validate_day_of_week(date)
21
+ # is it even one of the valid days?
22
+ return true if !@validations[:day_of_week] || @validations[:day_of_week].empty?
23
+ return false unless @validations[:day_of_week].has_key?(date.wday) #shortcut
24
+ # does this fall on one of the occurrences?
25
+ 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
26
+ this_weekday_in_month_count = ((TimeUtil.days_in_month(date) - first_occurrence + 1) / 7.0).ceil #how many of these in the month
27
+ nth_occurrence_of_weekday = (date.mday - first_occurrence) / 7 + 1 #what occurrence of the weekday is +date+
28
+ @validations[:day_of_week][date.wday].include?(nth_occurrence_of_weekday) || @validations[:day_of_week][date.wday].include?(nth_occurrence_of_weekday - this_weekday_in_month_count - 1)
29
+ end
30
+
31
+ #note - temporary implementation
32
+ def closest_day_of_week(date)
33
+ return nil if !@validations[:day_of_week] || @validations[:day_of_week].empty?
34
+ goal = date
35
+ while goal += ONE_DAY
36
+ test = adjust(goal, date)
37
+ return test if validate_day_of_week(test)
38
+ end
39
+ end
40
+
41
+ end
@@ -0,0 +1,51 @@
1
+ module DayOfYearValidation
2
+
3
+ def self.included(base)
4
+ base::SuggestionTypes << :day_of_year
5
+ end
6
+
7
+ # Specify what days of the year this rule applies to.
8
+ # ie: Schedule.yearly(2).days_of_year(17, -1) would create a
9
+ # rule which occurs every 17th and last day of every other year.
10
+ # Note: you cannot combine month_of_year and day_of_year in the same rule.
11
+ def day_of_year(*days)
12
+ @validations[:day_of_year] ||= []
13
+ days.each do |day|
14
+ raise ArgumentError.new('Argument must be a valid day') if day.abs > 366
15
+ raise ArgumentError.new('Argument must be non-zero') if day == 0
16
+ @validations[:day_of_year] << day
17
+ end
18
+ self
19
+ end
20
+
21
+ def validate_day_of_year(date)
22
+ return true if !@validations[:day_of_year] || @validations[:day_of_year].empty?
23
+ @validations[:day_of_year].include?(date.yday) || @validations[:day_of_year].include?(date.yday - TimeUtil.days_in_year(date) - 1)
24
+ end
25
+
26
+ def closest_day_of_year(date)
27
+ return nil if !@validations[:day_of_year] || @validations[:day_of_year].empty?
28
+ #get some variables we need
29
+ days_in_year = TimeUtil.days_in_year(date)
30
+ days_left_in_this_year = days_in_year - date.yday
31
+ days_in_next_year = TimeUtil.days_in_year(Time.utc(date.year + 1, 1, 1))
32
+ # create a list of distances
33
+ distances = []
34
+ @validations[:day_of_year].each do |d|
35
+ if d > 0
36
+ distances << d - date.yday #today is 1, we want 20 (19)
37
+ distances << days_left_in_this_year + d #(364 + 20)
38
+ elsif d < 0
39
+ distances << (days_in_year + d + 1) - date.yday #today is 300, we want -1
40
+ distances << (days_in_next_year + d + 1) + days_left_in_this_year #today is 300, we want -70
41
+ end
42
+ end
43
+ #return the lowest distance
44
+ distances = distances.select { |d| d > 0 }
45
+ return nil if distances.empty?
46
+ # return the start of the proper day
47
+ goal = date + distances.min * ONE_DAY
48
+ adjust(goal, date)
49
+ end
50
+
51
+ end
@@ -0,0 +1,35 @@
1
+ module HourOfDayValidation
2
+
3
+ def self.included(base)
4
+ base::SuggestionTypes << :hour_of_day
5
+ end
6
+
7
+ def hour_of_day(*hours)
8
+ @validations[:hour_of_day] ||= []
9
+ hours.each do |hour|
10
+ raise ArgumentError.new('Argument must be a valid hour') unless hour < 24 && hour >= 0
11
+ @validations[:hour_of_day] << hour
12
+ end
13
+ self
14
+ end
15
+
16
+ def validate_hour_of_day(date)
17
+ return true if !@validations[:hour_of_day] || @validations[:hour_of_day].empty?
18
+ @validations[:hour_of_day].include?(date.hour)
19
+ end
20
+
21
+ def closest_hour_of_day(date)
22
+ return nil if !@validations[:hour_of_day] || @validations[:hour_of_day].empty?
23
+ # turn hours into hour of day
24
+ # hour >= 24 should fall into the next day
25
+ hours = @validations[:hour_of_day].map do |h|
26
+ h > date.hour ? h - date.hour : 24 - date.hour + h
27
+ end
28
+ hours.compact!
29
+ # go to the closest distance away, the start of that hour
30
+ closest_hour = hours.min
31
+ goal = date + ONE_HOUR * closest_hour
32
+ adjust(goal, date)
33
+ end
34
+
35
+ end
@@ -0,0 +1,35 @@
1
+ module MinuteOfHourValidation
2
+
3
+ def self.included(base)
4
+ base::SuggestionTypes << :minute_of_hour
5
+ end
6
+
7
+ def minute_of_hour(*minutes)
8
+ @validations[:minute_of_hour] ||= []
9
+ minutes.each do |minute|
10
+ raise ArgumentError.new('Argument must be a valid minute') unless minute < 60 && minute >= 0
11
+ @validations[:minute_of_hour] << minute
12
+ end
13
+ self
14
+ end
15
+
16
+ def validate_minute_of_hour(date)
17
+ return true if !@validations[:minute_of_hour] || @validations[:minute_of_hour].empty?
18
+ @validations[:minute_of_hour].include?(date.min)
19
+ end
20
+
21
+ def closest_minute_of_hour(date)
22
+ return nil if !@validations[:minute_of_hour] || @validations[:minute_of_hour].empty?
23
+ # turn minutes into minutes of hour
24
+ # minute >= 60 should fall into the next hour
25
+ minutes = @validations[:minute_of_hour].map do |m|
26
+ m > date.min ? m - date.min : 60 - date.min + m
27
+ end
28
+ minutes.compact!
29
+ # go to the closest distance away, the beginning of that minute
30
+ closest_minute = minutes.min
31
+ goal = date + closest_minute * ONE_MINUTE
32
+ adjust(goal, date)
33
+ end
34
+
35
+ end
@@ -0,0 +1,39 @@
1
+ module MonthOfYearValidation
2
+
3
+ def self.included(base)
4
+ base::SuggestionTypes << :month_of_year
5
+ end
6
+
7
+ # Specify what months of the year this rule applies to.
8
+ # ie: Schedule.yearly(2).month_of_year(:january, :march) would create a
9
+ # rule which occurs every january and march, every other year
10
+ # Note: you cannot combine day_of_year and month_of_year in the same rule.
11
+ def month_of_year(*months)
12
+ @validations[:month_of_year] ||= []
13
+ months.each do |month|
14
+ raise ArgumentError.new('Argument must be a valid month') unless MONTHS.has_key?(month)
15
+ @validations[:month_of_year] << MONTHS[month]
16
+ end
17
+ self
18
+ end
19
+
20
+ def validate_month_of_year(date)
21
+ return true if !@validations[:month_of_year] || @validations[:month_of_year].empty?
22
+ @validations[:month_of_year].include?(date.month)
23
+ end
24
+
25
+ def closest_month_of_year(date)
26
+ return nil if !@validations[:month_of_year] || @validations[:month_of_year].empty?
27
+ # turn months into month of year
28
+ # month > 12 should fall into the next year
29
+ months = @validations[:month_of_year].map do |m|
30
+ m > date.month ? m - date.month : 12 - date.month + m
31
+ end
32
+ months.compact!
33
+ # go to the closest distance away
34
+ goal = date
35
+ months.min.times { goal += TimeUtil.days_in_month(goal) * ONE_DAY }
36
+ adjust(goal, date)
37
+ end
38
+
39
+ end
@@ -0,0 +1,35 @@
1
+ module SecondOfMinuteValidation
2
+
3
+ def self.included(base)
4
+ base::SuggestionTypes << :second_of_minute
5
+ end
6
+
7
+ def second_of_minute(*seconds)
8
+ @validations[:second_of_minute] ||= []
9
+ seconds.each do |second|
10
+ raise ArgumentError.new('Argument must be a valid second') unless second < 60 && second >= 0
11
+ @validations[:second_of_minute] << second
12
+ end
13
+ self
14
+ end
15
+
16
+ def validate_second_of_minute(date)
17
+ return true if !@validations[:second_of_minute] || @validations[:second_of_minute].empty?
18
+ @validations[:second_of_minute].include?(date.sec)
19
+ end
20
+
21
+ def closest_second_of_minute(date)
22
+ return nil if !@validations[:second_of_minute] || @validations[:second_of_minute].empty?
23
+ # turn seconds into seconds of minute
24
+ # second >= 60 should fall into the next minute
25
+ seconds = @validations[:second_of_minute].map do |s|
26
+ s > date.sec ? s - date.sec : 60 - date.sec + s
27
+ end
28
+ seconds.compact!
29
+ # go to the closest distance away
30
+ closest_second = seconds.min
31
+ goal = date + closest_second
32
+ adjust(goal, date)
33
+ end
34
+
35
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ice_cube
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - John Crepezzi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-04-01 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: ice_cube is a recurring date library for Ruby. It allows for quick, programatic expansion of recurring date rules.
26
+ email: john@crepezzi.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files: []
32
+
33
+ files:
34
+ - lib/ice_cube/rule.rb
35
+ - lib/ice_cube/rule_occurrence.rb
36
+ - lib/ice_cube/rules/daily_rule.rb
37
+ - lib/ice_cube/rules/hourly_rule.rb
38
+ - lib/ice_cube/rules/minutely_rule.rb
39
+ - lib/ice_cube/rules/monthly_rule.rb
40
+ - lib/ice_cube/rules/secondly_rule.rb
41
+ - lib/ice_cube/rules/weekly_rule.rb
42
+ - lib/ice_cube/rules/yearly_rule.rb
43
+ - lib/ice_cube/schedule.rb
44
+ - lib/ice_cube/time_util.rb
45
+ - lib/ice_cube/validations/day.rb
46
+ - lib/ice_cube/validations/day_of_month.rb
47
+ - lib/ice_cube/validations/day_of_week.rb
48
+ - lib/ice_cube/validations/day_of_year.rb
49
+ - lib/ice_cube/validations/hour_of_day.rb
50
+ - lib/ice_cube/validations/minute_of_hour.rb
51
+ - lib/ice_cube/validations/month_of_year.rb
52
+ - lib/ice_cube/validations/second_of_minute.rb
53
+ - lib/ice_cube.rb
54
+ has_rdoc: true
55
+ homepage: http://github.com/seejohnrun/ice_cube
56
+ licenses: []
57
+
58
+ post_install_message:
59
+ rdoc_options: []
60
+
61
+ require_paths:
62
+ - lib
63
+ required_ruby_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: "0"
74
+ version:
75
+ requirements: []
76
+
77
+ rubyforge_project:
78
+ rubygems_version: 1.3.5
79
+ signing_key:
80
+ specification_version: 3
81
+ summary: Ruby Date Recurrence Library
82
+ test_files: []
83
+