hiccup 0.0.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Rakefile +9 -1
- data/hiccup.gemspec +8 -2
- data/lib/hiccup.rb +69 -1
- data/lib/hiccup/convenience.rb +28 -0
- data/lib/hiccup/core_ext/date.rb +29 -0
- data/lib/hiccup/core_ext/duration.rb +25 -0
- data/lib/hiccup/core_ext/fixnum.rb +37 -0
- data/lib/hiccup/enumerable.rb +230 -0
- data/lib/hiccup/humanizable.rb +68 -0
- data/lib/hiccup/schedule.rb +42 -0
- data/lib/hiccup/serializable/ical.rb +39 -0
- data/lib/hiccup/serializers/ical.rb +189 -0
- data/lib/hiccup/validatable.rb +110 -0
- data/lib/hiccup/version.rb +1 -1
- data/test/enumerable_test.rb +283 -0
- data/test/humanizable_test.rb +70 -0
- data/test/ical_serializable_test.rb +92 -0
- data/test/test_helper.rb +6 -0
- data/test/validatable_test.rb +81 -0
- metadata +68 -6
data/Rakefile
CHANGED
data/hiccup.gemspec
CHANGED
@@ -10,9 +10,15 @@ Gem::Specification.new do |s|
|
|
10
10
|
s.homepage = "http://boblail.github.com/hiccup/"
|
11
11
|
s.summary = %q{Recurrence features a-la-cart}
|
12
12
|
s.description = %q{Recurrence features a-la-cart}
|
13
|
-
|
13
|
+
|
14
14
|
s.rubyforge_project = "hiccup"
|
15
|
-
|
15
|
+
|
16
|
+
s.add_dependency "activesupport"
|
17
|
+
|
18
|
+
s.add_development_dependency "ri_cal"
|
19
|
+
s.add_development_dependency "rails"
|
20
|
+
s.add_development_dependency "turn"
|
21
|
+
|
16
22
|
s.files = `git ls-files`.split("\n")
|
17
23
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
24
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
data/lib/hiccup.rb
CHANGED
@@ -1,5 +1,73 @@
|
|
1
1
|
require "hiccup/version"
|
2
2
|
|
3
|
+
|
4
|
+
# =======================================================
|
5
|
+
# Hiccup
|
6
|
+
# =======================================================
|
7
|
+
#
|
8
|
+
# This module contains mixins that can apply, serialize,
|
9
|
+
# validate, and humanize an object that models a recurrence
|
10
|
+
# pattern and which exposes the following properties:
|
11
|
+
#
|
12
|
+
# * kind - One of :never, :weekly, :monthly, :annually # <== change to :none and :yearly
|
13
|
+
# * start_date - The date when the recurrence pattern
|
14
|
+
# should start
|
15
|
+
# * ends - true or false indicating whether the recurrence
|
16
|
+
# ever ends
|
17
|
+
# * end_date - The date when the recurrence pattern ends
|
18
|
+
# * skip - The number of instances to skip # <== change this to :interval
|
19
|
+
# * pattern - An array of recurrence rules
|
20
|
+
#
|
21
|
+
# Examples:
|
22
|
+
#
|
23
|
+
# Every other Monday
|
24
|
+
# :kind => :weekly, :pattern => ["Monday"]
|
25
|
+
#
|
26
|
+
# Every year on June 21 (starting in 1999)
|
27
|
+
# :kind => :yearly, :start_date => Date.new(1999, 6, 21)
|
28
|
+
#
|
29
|
+
# The second and fourth Sundays of the month
|
30
|
+
# :kind => :monthly, :pattern => [[2, "Sunday"], [4, "Sunday"]]
|
31
|
+
#
|
32
|
+
#
|
3
33
|
module Hiccup
|
4
|
-
|
34
|
+
|
35
|
+
|
36
|
+
def hiccup(*modules)
|
37
|
+
options = modules.extract_options!
|
38
|
+
add_hiccup_modules(modules)
|
39
|
+
add_hiccup_serialization_formats(options[:serializable])
|
40
|
+
end
|
41
|
+
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
|
46
|
+
def add_hiccup_modules(modules)
|
47
|
+
(modules||[]).each {|name| add_hiccup_module(name)}
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_hiccup_module(symbol)
|
51
|
+
include_hiccup_module "hiccup/#{symbol}"
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
def add_hiccup_serialization_formats(formats)
|
56
|
+
(formats||[]).each {|format| add_hiccup_serialization_format(format)}
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_hiccup_serialization_format(format)
|
60
|
+
include_hiccup_module "hiccup/serializable/#{format}"
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
def include_hiccup_module(module_path)
|
65
|
+
require module_path
|
66
|
+
include module_path.classify.constantize
|
67
|
+
end
|
68
|
+
|
69
|
+
|
5
70
|
end
|
71
|
+
|
72
|
+
|
73
|
+
ActiveRecord::Base.extend(Hiccup) if defined?(ActiveRecord::Base)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Hiccup
|
2
|
+
module Convenience
|
3
|
+
|
4
|
+
|
5
|
+
def never?
|
6
|
+
kind == :never
|
7
|
+
end
|
8
|
+
|
9
|
+
def weekly?
|
10
|
+
kind == :weekly
|
11
|
+
end
|
12
|
+
|
13
|
+
def monthly?
|
14
|
+
kind == :monthly
|
15
|
+
end
|
16
|
+
|
17
|
+
def annually?
|
18
|
+
kind == :annually
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def ends?
|
23
|
+
(ends == true) || %w{true 1 t}.member?(ends)
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Hiccup
|
2
|
+
module CoreExtensions
|
3
|
+
module Date
|
4
|
+
|
5
|
+
|
6
|
+
def get_months_since(earlier_date)
|
7
|
+
((self.year - earlier_date.year) * 12) + (self.month - earlier_date.month).to_int
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_years_since(earlier_date)
|
11
|
+
(self.year - earlier_date.year)
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
def get_months_until(later_date)
|
16
|
+
later_date.months_since(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get_years_until(later_date)
|
20
|
+
later_date.years_since(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
Date.send :include, Hiccup::CoreExtensions::Date
|
29
|
+
DateTime.send :include, Hiccup::CoreExtensions::Date
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require "active_support/all"
|
2
|
+
|
3
|
+
|
4
|
+
module Hiccup
|
5
|
+
module CoreExt
|
6
|
+
module DurationExtensions
|
7
|
+
|
8
|
+
def from(*args)
|
9
|
+
since(*args)
|
10
|
+
end
|
11
|
+
|
12
|
+
def after(*args)
|
13
|
+
since(*args)
|
14
|
+
end
|
15
|
+
|
16
|
+
def before(*args)
|
17
|
+
ago(*args)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
ActiveSupport::Duration.send(:include, Hiccup::CoreExt::DurationExtensions)
|
@@ -0,0 +1,37 @@
|
|
1
|
+
class Fixnum
|
2
|
+
|
3
|
+
# todo: complete
|
4
|
+
def human_ordinalize(map={})
|
5
|
+
map.key?(self) ? map[self] : (begin
|
6
|
+
if self < -1
|
7
|
+
"#{(-self).human_ordinalize} to last"
|
8
|
+
else
|
9
|
+
case self
|
10
|
+
when -1; "last"
|
11
|
+
when 1; "first"
|
12
|
+
when 2; "second"
|
13
|
+
when 3; "third"
|
14
|
+
when 4; "fourth"
|
15
|
+
when 5; "fifth"
|
16
|
+
when 6; "sixth"
|
17
|
+
when 7; "seventh"
|
18
|
+
when 8; "eighth"
|
19
|
+
when 9; "ninth"
|
20
|
+
when 10; "tenth"
|
21
|
+
when 11; "eleventh"
|
22
|
+
when 12; "twelfth"
|
23
|
+
when 13; "thirteenth"
|
24
|
+
when 14; "fourteenth"
|
25
|
+
when 15; "fifteenth"
|
26
|
+
when 16; "sixteenth"
|
27
|
+
when 17; "seventeeth"
|
28
|
+
when 18; "eighteenth"
|
29
|
+
when 19; "nineteenth"
|
30
|
+
when 20; "twentieth"
|
31
|
+
else; self.ordinalize
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,230 @@
|
|
1
|
+
require "hiccup/convenience"
|
2
|
+
require "hiccup/core_ext/date"
|
3
|
+
require "hiccup/core_ext/duration"
|
4
|
+
|
5
|
+
|
6
|
+
module Hiccup
|
7
|
+
module Enumerable
|
8
|
+
include Convenience
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
def occurrences_during_month(year, month)
|
13
|
+
date1 = Date.new(year, month, 1)
|
14
|
+
date2 = date1.at_end_of_month
|
15
|
+
occurrences_between(date1, date2)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
def occurrences_between(earlier_date, later_date)
|
21
|
+
[].tap do |occurrences|
|
22
|
+
if (!ends? || earlier_date <= end_date) && (later_date >= start_date)
|
23
|
+
earlier_date = start_date if (earlier_date < start_date)
|
24
|
+
later_date = end_date if ends? && (later_date > end_date)
|
25
|
+
occurrence = first_occurrence_on_or_after(earlier_date)
|
26
|
+
while occurrence && (occurrence <= later_date)
|
27
|
+
occurrences << occurrence
|
28
|
+
occurrence = next_occurrence_after(occurrence)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
|
36
|
+
def first_occurrence_on_or_after(date)
|
37
|
+
date = date.to_date unless date.is_a?(Date) || !date.respond_to?(:to_date)
|
38
|
+
date = self.start_date if (date < self.start_date)
|
39
|
+
|
40
|
+
result = nil
|
41
|
+
case kind
|
42
|
+
when :never
|
43
|
+
result = date if date == self.start_date # (date > self.start_date) ? nil : self.start_date
|
44
|
+
|
45
|
+
when :weekly
|
46
|
+
wday = date.wday
|
47
|
+
pattern.each do |weekday|
|
48
|
+
wd = Date::DAYNAMES.index(weekday)
|
49
|
+
wd = wd + 7 if wd < wday
|
50
|
+
days_in_the_future = wd - wday
|
51
|
+
temp = days_in_the_future.days.after(date)
|
52
|
+
|
53
|
+
remainder = ((temp - start_date) / 7).to_i % skip
|
54
|
+
temp = (skip - remainder).weeks.after(temp) if (remainder > 0)
|
55
|
+
|
56
|
+
result = temp if !result || (temp < result)
|
57
|
+
end
|
58
|
+
|
59
|
+
when :monthly
|
60
|
+
pattern.each do |occurrence|
|
61
|
+
temp = nil
|
62
|
+
(0...30).each do |i| # If an occurrence doesn't occur this month, try up to 30 months in the future
|
63
|
+
temp = monthly_occurrence_to_date(occurrence, i.months.after(date))
|
64
|
+
break if temp && (temp >= date)
|
65
|
+
end
|
66
|
+
next unless temp
|
67
|
+
|
68
|
+
remainder = months_between(temp, start_date) % skip
|
69
|
+
temp = monthly_occurrence_to_date(occurrence, (skip - remainder).months.after(temp)) if (remainder > 0)
|
70
|
+
|
71
|
+
result = temp if !result || (temp < result)
|
72
|
+
end
|
73
|
+
|
74
|
+
when :annually
|
75
|
+
result, try_to_use_2_29 = if((start_date.month == 2) && (start_date.day == 29))
|
76
|
+
[Date.new(date.year, 2, 28), true]
|
77
|
+
else
|
78
|
+
[Date.new(date.year, start_date.month, start_date.day), false]
|
79
|
+
end
|
80
|
+
result = 1.year.after(result) if (result < date)
|
81
|
+
|
82
|
+
remainder = years_between(result, start_date) % skip
|
83
|
+
result = (skip - remainder).years.after(result) if (remainder > 0)
|
84
|
+
|
85
|
+
if try_to_use_2_29
|
86
|
+
begin
|
87
|
+
date = Date.new(result.year, 2, 29)
|
88
|
+
rescue
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
result = nil if (self.ends? && result && result > self.end_date)
|
94
|
+
result
|
95
|
+
end
|
96
|
+
|
97
|
+
|
98
|
+
|
99
|
+
def next_occurrence_after(date)
|
100
|
+
first_occurrence_on_or_after(1.day.after(date))
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
def first_occurrence_on_or_before(date)
|
106
|
+
date = date.to_date unless date.is_a?(Date) || !date.respond_to?(:to_date)
|
107
|
+
date = self.end_date if (self.ends? && date > self.end_date)
|
108
|
+
|
109
|
+
result = nil
|
110
|
+
case kind
|
111
|
+
when :never
|
112
|
+
result = date # (date > self.start_date) ? nil : self.start_date
|
113
|
+
|
114
|
+
when :weekly
|
115
|
+
wday = date.wday
|
116
|
+
pattern.each do |weekday|
|
117
|
+
wd = Date::DAYNAMES.index(weekday)
|
118
|
+
wd = wd - 7 if wd > wday
|
119
|
+
days_in_the_past = wday - wd
|
120
|
+
temp = days_in_the_past.days.before(date)
|
121
|
+
|
122
|
+
remainder = ((temp - start_date) / 7).to_i % skip
|
123
|
+
temp = remainder.weeks.before(temp) if (remainder > 0)
|
124
|
+
|
125
|
+
result = temp if !result || (temp > result)
|
126
|
+
end
|
127
|
+
|
128
|
+
when :monthly
|
129
|
+
pattern.each do |occurrence|
|
130
|
+
temp = nil
|
131
|
+
(0...30).each do |i| # If an occurrence doesn't occur this month, try up to 30 months in the past
|
132
|
+
temp = monthly_occurrence_to_date(occurrence, i.months.before(date))
|
133
|
+
break if temp && (temp <= date)
|
134
|
+
end
|
135
|
+
next unless temp
|
136
|
+
|
137
|
+
remainder = months_between(temp, start_date) % skip
|
138
|
+
temp = monthly_occurrence_to_date(occurrence, remainder.months.before(temp)) if (remainder > 0)
|
139
|
+
|
140
|
+
result = temp if !result || (temp > result)
|
141
|
+
end
|
142
|
+
|
143
|
+
when :annually
|
144
|
+
result, try_to_use_2_29 = if((start_date.month == 2) && (start_date.day == 29))
|
145
|
+
[Date.new(date.year, 2, 28), true]
|
146
|
+
else
|
147
|
+
[Date.new(date.year, start_date.month, start_date.day), false]
|
148
|
+
end
|
149
|
+
result = 1.year.before(result) if (result < date)
|
150
|
+
|
151
|
+
remainder = years_between(result, start_date) % skip
|
152
|
+
result = remainder.years.before(result) if (remainder > 0)
|
153
|
+
if try_to_use_2_29
|
154
|
+
begin
|
155
|
+
date = Date.new(result.year, 2, 29)
|
156
|
+
rescue
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
result = nil if (result && result < self.start_date)
|
162
|
+
result
|
163
|
+
end
|
164
|
+
|
165
|
+
|
166
|
+
|
167
|
+
def first_occurrence_before(date)
|
168
|
+
first_occurrence_on_or_before(1.day.before(date))
|
169
|
+
end
|
170
|
+
|
171
|
+
|
172
|
+
|
173
|
+
def occurs_on(date)
|
174
|
+
date = date.to_date
|
175
|
+
first_occurrence_on_or_after(date).eql?(date)
|
176
|
+
end
|
177
|
+
alias :contains? :occurs_on
|
178
|
+
|
179
|
+
|
180
|
+
|
181
|
+
def monthly_occurrence_to_date(occurrence, date)
|
182
|
+
year, month = date.year, date.month
|
183
|
+
|
184
|
+
day = begin
|
185
|
+
if occurrence.is_a?(Array)
|
186
|
+
ordinal, weekday = occurrence
|
187
|
+
wday_of_first_of_month = Date.new(year, month, 1).wday
|
188
|
+
wday = Date::DAYNAMES.index(weekday)
|
189
|
+
day = wday
|
190
|
+
day = day + 7 if (wday < wday_of_first_of_month)
|
191
|
+
day = day - wday_of_first_of_month
|
192
|
+
day = day + (ordinal * 7) - 6
|
193
|
+
else
|
194
|
+
occurrence
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
last_day_of_month = Date.new(year, month, -1).day
|
199
|
+
(day > last_day_of_month) ? nil : Date.new(year, month, day)
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
|
204
|
+
private
|
205
|
+
|
206
|
+
|
207
|
+
|
208
|
+
def months_between(date1, date2)
|
209
|
+
later_date, earlier_date = sort_dates(date1, date2)
|
210
|
+
later_date.get_months_since(earlier_date)
|
211
|
+
end
|
212
|
+
|
213
|
+
def weeks_between(date1, date2)
|
214
|
+
later_date, earlier_date = sort_dates(date1, date2)
|
215
|
+
later_date.get_weeks_since(earlier_date)
|
216
|
+
end
|
217
|
+
|
218
|
+
def years_between(date1, date2)
|
219
|
+
later_date, earlier_date = sort_dates(date1, date2)
|
220
|
+
later_date.get_years_since(earlier_date)
|
221
|
+
end
|
222
|
+
|
223
|
+
def sort_dates(date1, date2)
|
224
|
+
(date1 > date2) ? [date1, date2] : [date2, date1]
|
225
|
+
end
|
226
|
+
|
227
|
+
|
228
|
+
|
229
|
+
end
|
230
|
+
end
|