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