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 +44 -0
- data/lib/ice_cube/rule.rb +169 -0
- data/lib/ice_cube/rule_occurrence.rb +73 -0
- data/lib/ice_cube/rules/daily_rule.rb +31 -0
- data/lib/ice_cube/rules/hourly_rule.rb +30 -0
- data/lib/ice_cube/rules/minutely_rule.rb +30 -0
- data/lib/ice_cube/rules/monthly_rule.rb +36 -0
- data/lib/ice_cube/rules/secondly_rule.rb +30 -0
- data/lib/ice_cube/rules/weekly_rule.rb +33 -0
- data/lib/ice_cube/rules/yearly_rule.rb +36 -0
- data/lib/ice_cube/schedule.rb +108 -0
- data/lib/ice_cube/time_util.rb +18 -0
- data/lib/ice_cube/validations/day.rb +36 -0
- data/lib/ice_cube/validations/day_of_month.rb +51 -0
- data/lib/ice_cube/validations/day_of_week.rb +41 -0
- data/lib/ice_cube/validations/day_of_year.rb +51 -0
- data/lib/ice_cube/validations/hour_of_day.rb +35 -0
- data/lib/ice_cube/validations/minute_of_hour.rb +35 -0
- data/lib/ice_cube/validations/month_of_year.rb +39 -0
- data/lib/ice_cube/validations/second_of_minute.rb +35 -0
- metadata +83 -0
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
|
+
|