hiccup 0.0.0 → 0.2.0
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/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
|