ice_cube 0.2.2

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.
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
+