hiccup 0.2.1 → 0.3.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/README.mdown +82 -0
- data/hiccup.gemspec +6 -4
- data/lib/hiccup/core_ext/date.rb +11 -0
- data/lib/hiccup/core_ext/enumerable.rb +17 -0
- data/lib/hiccup/core_ext/hash.rb +16 -0
- data/lib/hiccup/enumerable.rb +20 -2
- data/lib/hiccup/humanizable.rb +1 -1
- data/lib/hiccup/inferrable.rb +249 -0
- data/lib/hiccup/schedule.rb +1 -0
- data/lib/hiccup/serializers/ical.rb +1 -1
- data/lib/hiccup/version.rb +1 -1
- data/test/core_ext_date_test.rb +32 -0
- data/test/enumerable_test.rb +50 -1
- data/test/humanizable_test.rb +20 -6
- data/test/ical_serializable_test.rb +25 -0
- data/test/inferrable_test.rb +244 -0
- data/test/test_helper.rb +1 -0
- metadata +133 -70
data/README.mdown
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Hiccup
|
2
|
+
|
3
|
+
Hiccup mixes a-la-cart recurrence features into your recurring model. It doesn't dictate the data structure of your model, just the interface. It works just like Devise does for authenticatable models.
|
4
|
+
|
5
|
+
Hiccup does provide an extremely lightweight `Schedule` class that mixes in all of Hiccup's feature, but you don't have to use Hiccup's Schedul if you don't want to.
|
6
|
+
|
7
|
+
### Usage
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class Schedule
|
11
|
+
extend Hiccup
|
12
|
+
|
13
|
+
hiccup :enumerable,
|
14
|
+
:validatable,
|
15
|
+
:humanizable,
|
16
|
+
:inferrable,
|
17
|
+
:serializable => [:ical]
|
18
|
+
|
19
|
+
end
|
20
|
+
```
|
21
|
+
|
22
|
+
### Interface
|
23
|
+
|
24
|
+
Hiccup requires that a recurring object expose the following properties
|
25
|
+
|
26
|
+
- **kind**: one of `:never`, `:weekly`, `:monthly`, `:annually`
|
27
|
+
- **start_date**: the date when the recurrence should start
|
28
|
+
- **ends**: either `true` or `false`, indicating whether the recurrence has an end date
|
29
|
+
- **end_date**: the date when the recurrence should end
|
30
|
+
- **skip**: the number of instances to skip (You'd set `skip` to 2 to specify an event that occurs every _other_ week, for example)
|
31
|
+
- **weekly_pattern**: an array of weekdays on which a weekly event should recur
|
32
|
+
- **monthly_pattern**: an array of recurrence rules for a monthly event
|
33
|
+
|
34
|
+
### Examples
|
35
|
+
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
# Every other Monday
|
39
|
+
Schedule.new(:kind => :weekly, :weekly_pattern => ["Monday"])
|
40
|
+
|
41
|
+
# Every year on June 21 (starting in 1999)
|
42
|
+
Schedule.new(:kind => :yearly, :start_date => Date.new(1999, 6, 21))
|
43
|
+
|
44
|
+
# The second and fourth Sundays of the month
|
45
|
+
Schedule.new(:kind => :monthly, :monthly_pattern => [[2, "Sunday"], [4, "Sunday"]])
|
46
|
+
```
|
47
|
+
|
48
|
+
|
49
|
+
# Modules
|
50
|
+
|
51
|
+
### Enumerable
|
52
|
+
|
53
|
+
Supplies methods for creating instances of a recurring pattern
|
54
|
+
|
55
|
+
### Validatable
|
56
|
+
|
57
|
+
Mixes in ActiveModel validations for recurrence models
|
58
|
+
|
59
|
+
### Humanizable
|
60
|
+
|
61
|
+
Represents a recurring pattern in a human-readable string
|
62
|
+
|
63
|
+
### Inferrable
|
64
|
+
|
65
|
+
Infers a schedule from an array of dates
|
66
|
+
|
67
|
+
Examples:
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
schedule = Schedule.infer %w{2012-7-9 2012-8-13 2012-9-10}
|
71
|
+
schedule.humanize # => "The second Monday of every month"
|
72
|
+
|
73
|
+
schedule = Schedule.infer %w{2010-3-4 2011-3-4 2012-3-4}
|
74
|
+
schedule.humanize # => "Every year on March 4"
|
75
|
+
|
76
|
+
schedule = Schedule.infer %w{2012-3-6 2012-3-8 2012-3-15 2012-3-20 2012-3-27 2012-3-29}
|
77
|
+
schedule.humanize # => "Every Tuesday and Thursday"
|
78
|
+
```
|
79
|
+
|
80
|
+
### Serializable
|
81
|
+
|
82
|
+
Supports serializing and deserializing a recurrence pattern to an array of formats
|
data/hiccup.gemspec
CHANGED
@@ -8,17 +8,19 @@ Gem::Specification.new do |s|
|
|
8
8
|
s.authors = ["Bob Lail"]
|
9
9
|
s.email = ["bob.lailfamily@gmail.com"]
|
10
10
|
s.homepage = "http://boblail.github.com/hiccup/"
|
11
|
-
s.summary = %q{
|
12
|
-
s.description = %q{
|
11
|
+
s.summary = %q{A library for working with things that recur}
|
12
|
+
s.description = %q{Hiccup mixes a-la-cart recurrence features into your data structure. It doesn't dictate the data structure, just the interface.}
|
13
13
|
|
14
14
|
s.rubyforge_project = "hiccup"
|
15
15
|
|
16
|
-
s.add_dependency "activesupport"
|
16
|
+
s.add_dependency "activesupport", "~> 3.2.8"
|
17
|
+
s.add_dependency "builder"
|
17
18
|
|
18
19
|
s.add_development_dependency "ri_cal"
|
19
|
-
s.add_development_dependency "rails"
|
20
|
+
s.add_development_dependency "rails", "~> 3.2.8"
|
20
21
|
s.add_development_dependency "turn"
|
21
22
|
s.add_development_dependency "simplecov"
|
23
|
+
s.add_development_dependency "pry"
|
22
24
|
|
23
25
|
s.files = `git ls-files`.split("\n")
|
24
26
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
data/lib/hiccup/core_ext/date.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Hiccup
|
2
|
+
module CoreExt
|
3
|
+
module EnumerableExtensions
|
4
|
+
|
5
|
+
def to_histogram
|
6
|
+
self.each_with_object(Hash.new { 0 }) do |item, histogram|
|
7
|
+
pattern = block_given? ? yield(item) : item
|
8
|
+
histogram[pattern] += 1
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Enumerable.send(:include, Hiccup::CoreExt::EnumerableExtensions)
|
17
|
+
Array.send(:include, Hiccup::CoreExt::EnumerableExtensions)
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Hiccup
|
2
|
+
module CoreExt
|
3
|
+
module HashExtensions
|
4
|
+
|
5
|
+
def group_by_value
|
6
|
+
each_with_object({}) do |(key, value), new_hash|
|
7
|
+
(new_hash[value]||=[]).push(key)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
alias :flip :group_by_value
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Hash.send(:include, Hiccup::CoreExt::HashExtensions)
|
data/lib/hiccup/enumerable.rb
CHANGED
@@ -66,7 +66,8 @@ module Hiccup
|
|
66
66
|
next unless temp
|
67
67
|
|
68
68
|
remainder = months_between(temp, start_date) % skip
|
69
|
-
temp = monthly_occurrence_to_date(occurrence, (skip - remainder).months.after(temp)) if (remainder > 0)
|
69
|
+
temp = monthly_occurrence_to_date(occurrence, (skip - remainder).months.after(temp)) if (remainder > 0)
|
70
|
+
next unless temp
|
70
71
|
|
71
72
|
result = temp if !result || (temp < result)
|
72
73
|
end
|
@@ -135,7 +136,8 @@ module Hiccup
|
|
135
136
|
next unless temp
|
136
137
|
|
137
138
|
remainder = months_between(temp, start_date) % skip
|
138
|
-
temp = monthly_occurrence_to_date(occurrence, remainder.months.before(temp)) if (remainder > 0)
|
139
|
+
temp = monthly_occurrence_to_date(occurrence, remainder.months.before(temp)) if (remainder > 0)
|
140
|
+
next unless temp
|
139
141
|
|
140
142
|
result = temp if !result || (temp > result)
|
141
143
|
end
|
@@ -178,6 +180,22 @@ module Hiccup
|
|
178
180
|
|
179
181
|
|
180
182
|
|
183
|
+
def n_occurrences_before(limit, date)
|
184
|
+
n_occurrences_on_or_before(limit, 1.day.before(date))
|
185
|
+
end
|
186
|
+
|
187
|
+
def n_occurrences_on_or_before(limit, date)
|
188
|
+
occurrences = []
|
189
|
+
occurrence = first_occurrence_on_or_before(date)
|
190
|
+
while occurrence && occurrences.length < limit
|
191
|
+
occurrences << occurrence
|
192
|
+
occurrence = first_occurrence_before(occurrence)
|
193
|
+
end
|
194
|
+
occurrences
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
|
181
199
|
def monthly_occurrence_to_date(occurrence, date)
|
182
200
|
year, month = date.year, date.month
|
183
201
|
|
data/lib/hiccup/humanizable.rb
CHANGED
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'active_support/core_ext/date/conversions'
|
3
|
+
require 'hiccup/core_ext/enumerable'
|
4
|
+
require 'hiccup/core_ext/hash'
|
5
|
+
|
6
|
+
|
7
|
+
module Hiccup
|
8
|
+
module Inferrable
|
9
|
+
extend ActiveSupport::Concern
|
10
|
+
|
11
|
+
module ClassMethods
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
def infer(dates, options={})
|
16
|
+
@verbose = options.fetch(:verbose, false)
|
17
|
+
dates = extract_array_of_dates!(dates)
|
18
|
+
guesses = generate_guesses(dates)
|
19
|
+
guess, score = pick_best_guess(guesses, dates)
|
20
|
+
guess
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
def generate_guesses(dates)
|
26
|
+
@start_date = dates.min
|
27
|
+
@end_date = dates.max
|
28
|
+
[].tap do |guesses|
|
29
|
+
guesses.concat generate_yearly_guesses(dates)
|
30
|
+
guesses.concat generate_monthly_guesses(dates)
|
31
|
+
guesses.concat generate_weekly_guesses(dates)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def generate_yearly_guesses(dates)
|
36
|
+
histogram_of_patterns = dates.to_histogram do |date|
|
37
|
+
[date.month, date.day]
|
38
|
+
end
|
39
|
+
patterns_by_popularity = histogram_of_patterns.flip # => {1 => [...], 2 => [...], 5 => [a, b]}
|
40
|
+
highest_popularity = patterns_by_popularity.keys.max # => 5
|
41
|
+
most_popular = patterns_by_popularity[highest_popularity].first # => a
|
42
|
+
start_date = Date.new(@start_date.year, *most_popular)
|
43
|
+
|
44
|
+
[].tap do |guesses|
|
45
|
+
(1...5).each do |skip|
|
46
|
+
guesses << self.new.tap do |schedule|
|
47
|
+
schedule.kind = :annually
|
48
|
+
schedule.start_date = start_date
|
49
|
+
schedule.end_date = @end_date
|
50
|
+
schedule.skip = skip
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def generate_monthly_guesses(dates)
|
57
|
+
histogram_of_patterns = dates.to_histogram do |date|
|
58
|
+
[date.get_nth_wday_of_month, Date::DAYNAMES[date.wday]]
|
59
|
+
end
|
60
|
+
patterns_by_popularity = histogram_of_patterns.flip
|
61
|
+
|
62
|
+
histogram_of_days = dates.to_histogram(&:day)
|
63
|
+
days_by_popularity = histogram_of_days.flip
|
64
|
+
|
65
|
+
if @verbose
|
66
|
+
puts "",
|
67
|
+
" monthly analysis:",
|
68
|
+
" input: #{dates.inspect}",
|
69
|
+
" histogram (weekday): #{histogram_of_patterns.inspect}",
|
70
|
+
" by_popularity (weekday): #{patterns_by_popularity.inspect}",
|
71
|
+
" histogram (day): #{histogram_of_days.inspect}",
|
72
|
+
" by_popularity (day): #{days_by_popularity.inspect}"
|
73
|
+
end
|
74
|
+
|
75
|
+
[].tap do |guesses|
|
76
|
+
(1...5).each do |skip|
|
77
|
+
enumerate_by_popularity(days_by_popularity) do |days|
|
78
|
+
guesses << self.new.tap do |schedule|
|
79
|
+
schedule.kind = :monthly
|
80
|
+
schedule.start_date = @start_date
|
81
|
+
schedule.end_date = @end_date
|
82
|
+
schedule.skip = skip
|
83
|
+
schedule.monthly_pattern = days
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
enumerate_by_popularity(patterns_by_popularity) do |patterns|
|
88
|
+
guesses << self.new.tap do |schedule|
|
89
|
+
schedule.kind = :monthly
|
90
|
+
schedule.start_date = @start_date
|
91
|
+
schedule.end_date = @end_date
|
92
|
+
schedule.skip = skip
|
93
|
+
schedule.monthly_pattern = patterns
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def generate_weekly_guesses(dates)
|
101
|
+
[].tap do |guesses|
|
102
|
+
histogram_of_wdays = dates.to_histogram do |date|
|
103
|
+
Date::DAYNAMES[date.wday]
|
104
|
+
end
|
105
|
+
wdays_by_popularity = histogram_of_wdays.flip
|
106
|
+
|
107
|
+
if @verbose
|
108
|
+
puts "",
|
109
|
+
" weekly analysis:",
|
110
|
+
" input: #{dates.inspect}",
|
111
|
+
" histogram: #{histogram_of_wdays.inspect}",
|
112
|
+
" by_popularity: #{wdays_by_popularity.inspect}"
|
113
|
+
end
|
114
|
+
|
115
|
+
(1...5).each do |skip|
|
116
|
+
enumerate_by_popularity(wdays_by_popularity) do |wdays|
|
117
|
+
guesses << self.new.tap do |schedule|
|
118
|
+
schedule.kind = :weekly
|
119
|
+
schedule.start_date = @start_date
|
120
|
+
schedule.end_date = @end_date
|
121
|
+
schedule.skip = skip
|
122
|
+
schedule.weekly_pattern = wdays
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
|
131
|
+
# Expects a hash of values grouped by popularity
|
132
|
+
# Yields the most popular values first, and then
|
133
|
+
# increasingly less popular values
|
134
|
+
def enumerate_by_popularity(values_by_popularity)
|
135
|
+
popularities = values_by_popularity.keys.sort.reverse
|
136
|
+
popularities.length.times do |i|
|
137
|
+
at_popularities = popularities.take(i + 1)
|
138
|
+
yield values_by_popularity.values_at(*at_popularities).flatten(1)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
|
143
|
+
|
144
|
+
def pick_best_guess(guesses, dates)
|
145
|
+
scored_guesses = guesses \
|
146
|
+
.map { |guess| [guess, score_guess(guess, dates)] } \
|
147
|
+
.sort_by { |(guess, score)| -score.to_f }
|
148
|
+
|
149
|
+
if @verbose
|
150
|
+
puts "\nGUESSES FOR #{dates}:"
|
151
|
+
scored_guesses.each do |(guess, score)|
|
152
|
+
puts " (%.3f p/%.3f b/%.3f c/%.3f) #{guess.humanize}" % [
|
153
|
+
score.to_f,
|
154
|
+
score.prediction_rate,
|
155
|
+
score.brick_penalty,
|
156
|
+
score.complexity_penalty]
|
157
|
+
end
|
158
|
+
puts ""
|
159
|
+
end
|
160
|
+
|
161
|
+
best_guess = scored_guesses.reject { |(guess, score)| score.to_f < 0.333 }.first
|
162
|
+
best_guess || Schedule.new(kind: :never)
|
163
|
+
end
|
164
|
+
|
165
|
+
def score_guess(guess, input_dates)
|
166
|
+
predicted_dates = guess.occurrences_between(guess.start_date, guess.end_date)
|
167
|
+
|
168
|
+
# prediction_rate is the percent of input dates predicted
|
169
|
+
predictions = (predicted_dates & input_dates).length
|
170
|
+
prediction_rate = Float(predictions) / Float(input_dates.length)
|
171
|
+
|
172
|
+
# bricks are dates predicted by this guess but not in the input
|
173
|
+
bricks = (predicted_dates - input_dates).length
|
174
|
+
|
175
|
+
# brick_rate is the percent of bricks to predictions
|
176
|
+
# A brick_rate >= 1 means that this guess bricks more than it predicts
|
177
|
+
brick_rate = Float(bricks) / Float(input_dates.length)
|
178
|
+
|
179
|
+
# complexity measures how many rules are necesary
|
180
|
+
# to describe the pattern
|
181
|
+
complexity = complexity_of(guess)
|
182
|
+
|
183
|
+
# complexity_rate is the number of rules per inputs
|
184
|
+
complexity_rate = Float(complexity) / Float(input_dates.length)
|
185
|
+
|
186
|
+
Score.new(prediction_rate, brick_rate, complexity_rate)
|
187
|
+
end
|
188
|
+
|
189
|
+
def complexity_of(schedule)
|
190
|
+
return schedule.weekly_pattern.length if schedule.weekly?
|
191
|
+
return schedule.monthly_pattern.length if schedule.monthly?
|
192
|
+
1
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
|
197
|
+
def extract_array_of_dates!(dates)
|
198
|
+
raise_invalid_dates_error! unless dates.respond_to?(:each)
|
199
|
+
dates.map { |date| assert_date!(date) }.sort
|
200
|
+
end
|
201
|
+
|
202
|
+
def assert_date!(date)
|
203
|
+
return date if date.is_a?(Date)
|
204
|
+
date.to_date rescue raise_invalid_dates_error!
|
205
|
+
end
|
206
|
+
|
207
|
+
def raise_invalid_dates_error!
|
208
|
+
raise ArgumentError.new("Inferrable.infer expects to receive a collection of dates")
|
209
|
+
end
|
210
|
+
|
211
|
+
|
212
|
+
|
213
|
+
class Score < Struct.new(:prediction_rate, :brick_rate, :complexity_rate)
|
214
|
+
|
215
|
+
# as brick rate rises, our confidence in this guess drops
|
216
|
+
def brick_penalty
|
217
|
+
brick_penalty = brick_rate * 0.33
|
218
|
+
brick_penalty = 1 if brick_penalty > 1
|
219
|
+
brick_penalty
|
220
|
+
end
|
221
|
+
|
222
|
+
# as the complexity rises, our confidence in this guess drops
|
223
|
+
# this hash table is a stand-in for a proper formala
|
224
|
+
#
|
225
|
+
# A complexity of 1 means that 1 rule is required per input
|
226
|
+
# date. This means we haven't really discovered a pattern.
|
227
|
+
def complexity_penalty
|
228
|
+
complexity_rate
|
229
|
+
end
|
230
|
+
|
231
|
+
# our confidence is weakened by bricks and complexity
|
232
|
+
def confidence
|
233
|
+
confidence = 1.0
|
234
|
+
confidence *= (1 - brick_penalty)
|
235
|
+
confidence *= (1 - complexity_penalty)
|
236
|
+
confidence
|
237
|
+
end
|
238
|
+
|
239
|
+
# a number between 0 and 1
|
240
|
+
def to_f
|
241
|
+
prediction_rate * confidence
|
242
|
+
end
|
243
|
+
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
data/lib/hiccup/schedule.rb
CHANGED
@@ -37,7 +37,7 @@ module Hiccup
|
|
37
37
|
component = RiCal.parse_string(component_ics).first
|
38
38
|
|
39
39
|
@obj = @klass.new
|
40
|
-
@obj.start_date = component.dtstart_property.to_datetime
|
40
|
+
@obj.start_date = component.dtstart_property.try(:to_datetime)
|
41
41
|
component.rrule_property.each do |rule|
|
42
42
|
case rule.freq
|
43
43
|
when "WEEKLY"; parse_weekly_rule(rule)
|
data/lib/hiccup/version.rb
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
|
4
|
+
class CoreExtDateTest < ActiveSupport::TestCase
|
5
|
+
|
6
|
+
|
7
|
+
test "should correctly identify the nth weekday of the month of a date" do
|
8
|
+
assert_equal 1, Date.new(2012, 7, 1).get_nth_wday_of_month
|
9
|
+
assert_equal 1, Date.new(2012, 7, 7).get_nth_wday_of_month
|
10
|
+
assert_equal 2, Date.new(2012, 7, 8).get_nth_wday_of_month
|
11
|
+
assert_equal 2, Date.new(2012, 7, 14).get_nth_wday_of_month
|
12
|
+
assert_equal 3, Date.new(2012, 7, 15).get_nth_wday_of_month
|
13
|
+
assert_equal 3, Date.new(2012, 7, 21).get_nth_wday_of_month
|
14
|
+
assert_equal 4, Date.new(2012, 7, 22).get_nth_wday_of_month
|
15
|
+
assert_equal 4, Date.new(2012, 7, 28).get_nth_wday_of_month
|
16
|
+
assert_equal 5, Date.new(2012, 7, 29).get_nth_wday_of_month
|
17
|
+
end
|
18
|
+
|
19
|
+
test "should correctly identify the nth weekday of the month of a date as a string" do
|
20
|
+
assert_equal "1 Sunday", Date.new(2012, 7, 1).get_nth_wday_string
|
21
|
+
assert_equal "1 Saturday", Date.new(2012, 7, 7).get_nth_wday_string
|
22
|
+
assert_equal "2 Sunday", Date.new(2012, 7, 8).get_nth_wday_string
|
23
|
+
assert_equal "2 Saturday", Date.new(2012, 7, 14).get_nth_wday_string
|
24
|
+
assert_equal "3 Sunday", Date.new(2012, 7, 15).get_nth_wday_string
|
25
|
+
assert_equal "3 Saturday", Date.new(2012, 7, 21).get_nth_wday_string
|
26
|
+
assert_equal "4 Sunday", Date.new(2012, 7, 22).get_nth_wday_string
|
27
|
+
assert_equal "4 Saturday", Date.new(2012, 7, 28).get_nth_wday_string
|
28
|
+
assert_equal "5 Sunday", Date.new(2012, 7, 29).get_nth_wday_string
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
end
|
data/test/enumerable_test.rb
CHANGED
@@ -55,7 +55,7 @@ class EnumerableTest < ActiveSupport::TestCase
|
|
55
55
|
dates = schedule.occurrences_during_month(2008,7).map {|date| date.day}
|
56
56
|
expected_dates = []
|
57
57
|
assert_equal expected_dates, dates, "occurrences_during_month should generate no occurrences if before start_date"
|
58
|
-
|
58
|
+
|
59
59
|
schedule = Schedule.new({
|
60
60
|
:kind => :weekly,
|
61
61
|
:weekly_pattern => %w{Monday},
|
@@ -99,6 +99,37 @@ class EnumerableTest < ActiveSupport::TestCase
|
|
99
99
|
|
100
100
|
|
101
101
|
|
102
|
+
def test_n_occurrences_before
|
103
|
+
schedule = Schedule.new({
|
104
|
+
:kind => :weekly,
|
105
|
+
:weekly_pattern => %w{Monday Wednesday Friday},
|
106
|
+
:start_date => Date.new(2009,3,15),
|
107
|
+
:ends => true,
|
108
|
+
:end_date => Date.new(2009,11,30)})
|
109
|
+
dates = schedule.n_occurrences_before(10, Date.new(2009, 10, 31)).map { |date| date.strftime("%Y-%m-%d") }
|
110
|
+
|
111
|
+
expected_dates = ["2009-10-30", "2009-10-28", "2009-10-26",
|
112
|
+
"2009-10-23", "2009-10-21", "2009-10-19",
|
113
|
+
"2009-10-16", "2009-10-14", "2009-10-12",
|
114
|
+
"2009-10-09" ]
|
115
|
+
assert_equal expected_dates, dates
|
116
|
+
end
|
117
|
+
|
118
|
+
test "n_occurrences_before should return a shorter array if no events exist before the given date" do
|
119
|
+
schedule = Schedule.new({
|
120
|
+
:kind => :weekly,
|
121
|
+
:weekly_pattern => %w{Monday Wednesday Friday},
|
122
|
+
:start_date => Date.new(2009,3,15),
|
123
|
+
:ends => true,
|
124
|
+
:end_date => Date.new(2009,11,30)})
|
125
|
+
dates = schedule.n_occurrences_before(10, Date.new(2009, 3, 20)).map { |date| date.strftime("%Y-%m-%d") }
|
126
|
+
|
127
|
+
expected_dates = ["2009-03-18", "2009-03-16"]
|
128
|
+
assert_equal expected_dates, dates
|
129
|
+
end
|
130
|
+
|
131
|
+
|
132
|
+
|
102
133
|
def test_how_contains_handles_parameter_types
|
103
134
|
date = Date.new(1981,4,23)
|
104
135
|
schedule = Schedule.new({:kind => :annually, :start_date => date})
|
@@ -127,6 +158,24 @@ class EnumerableTest < ActiveSupport::TestCase
|
|
127
158
|
|
128
159
|
|
129
160
|
|
161
|
+
test "should not throw an exception when calculating monthly recurrence and skip causes a guess to be discarded" do
|
162
|
+
schedule = Schedule.new({
|
163
|
+
:kind => :monthly,
|
164
|
+
:monthly_pattern => [
|
165
|
+
[1, "Tuesday"],
|
166
|
+
[2, "Thursday"],
|
167
|
+
[3, "Thursday"],
|
168
|
+
[3, "Tuesday"],
|
169
|
+
[4, "Tuesday"],
|
170
|
+
[5, "Thursday"] ],
|
171
|
+
:skip => 3,
|
172
|
+
:start_date => Date.new(2012,3,6),
|
173
|
+
:end_date => Date.new(2012,3,29)})
|
174
|
+
schedule.occurrences_between(schedule.start_date, schedule.end_date)
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
|
130
179
|
def test_weekly_recurrence_and_skip
|
131
180
|
schedule = Schedule.new({
|
132
181
|
:kind => :weekly,
|
data/test/humanizable_test.rb
CHANGED
@@ -54,16 +54,30 @@ class HumanizableTest < ActiveSupport::TestCase
|
|
54
54
|
{:kind => :monthly, :monthly_pattern => [[1, "Monday"], [3, "Monday"]], :skip => 2})
|
55
55
|
|
56
56
|
test_humanize(
|
57
|
-
"Every year on
|
58
|
-
{:kind => :annually})
|
57
|
+
"Every year on October 1",
|
58
|
+
{:kind => :annually, start_date: Date.new(2012, 10, 1)})
|
59
59
|
|
60
60
|
test_humanize(
|
61
|
-
"Every
|
62
|
-
{:kind => :annually, :
|
61
|
+
"Every year on October 10",
|
62
|
+
{:kind => :annually, start_date: Date.new(2012, 10, 10)})
|
63
63
|
|
64
64
|
test_humanize(
|
65
|
-
"Every
|
66
|
-
{:kind => :annually, :skip =>
|
65
|
+
"Every other year on August 12",
|
66
|
+
{:kind => :annually, :skip => 2, start_date: Date.new(2012, 8, 12)})
|
67
|
+
|
68
|
+
test_humanize(
|
69
|
+
"Every fourth year on January 31",
|
70
|
+
{:kind => :annually, :skip => 4, start_date: Date.new(1888, 1, 31)})
|
71
|
+
|
72
|
+
|
73
|
+
|
74
|
+
test "should not have spaces in front of day numbers" do
|
75
|
+
independence_day = Schedule.new({
|
76
|
+
:kind => :annually,
|
77
|
+
:start_date => Date.new(2012, 7, 4)
|
78
|
+
})
|
79
|
+
assert_equal "Every year on July 4", independence_day.humanize
|
80
|
+
end
|
67
81
|
|
68
82
|
|
69
83
|
|
@@ -17,6 +17,20 @@ class IcalSerializableTest < ActiveSupport::TestCase
|
|
17
17
|
end
|
18
18
|
|
19
19
|
|
20
|
+
def test_parsing_empty_recurrence
|
21
|
+
schedule = Schedule.from_ical("")
|
22
|
+
assert_equal :never, schedule.kind
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
test_roundtrip(
|
27
|
+
"No recurrence",
|
28
|
+
"DTSTART;VALUE=DATE-TIME:20090101T000000Z\n",
|
29
|
+
{ :kind => :never,
|
30
|
+
:start_date => DateTime.new(2009, 1, 1)
|
31
|
+
})
|
32
|
+
|
33
|
+
|
20
34
|
test_roundtrip(
|
21
35
|
"Simple weekly recurrence",
|
22
36
|
"DTSTART;VALUE=DATE-TIME:20090101T000000Z\nRRULE:FREQ=WEEKLY;BYDAY=SU\n",
|
@@ -26,6 +40,17 @@ class IcalSerializableTest < ActiveSupport::TestCase
|
|
26
40
|
})
|
27
41
|
|
28
42
|
|
43
|
+
test_roundtrip(
|
44
|
+
"Simple weekly recurrence (with an end date)",
|
45
|
+
"DTSTART;VALUE=DATE-TIME:20090101T000000Z\nRRULE:FREQ=WEEKLY;UNTIL=20091231T000000Z;BYDAY=SU\n",
|
46
|
+
{ :kind => :weekly,
|
47
|
+
:weekly_pattern => %w{Sunday},
|
48
|
+
:start_date => DateTime.new(2009, 1, 1),
|
49
|
+
:end_date => DateTime.new(2009, 12, 31),
|
50
|
+
:ends => true
|
51
|
+
})
|
52
|
+
|
53
|
+
|
29
54
|
test_roundtrip(
|
30
55
|
"Complex weekly recurrence (with an interval)",
|
31
56
|
"DTSTART;VALUE=DATE-TIME:20090101T000000Z\nRRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=TU,TH\n",
|
@@ -0,0 +1,244 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
# Ideas: see 3/4/10, 3/5/11, 3/4/12 as closer to
|
4
|
+
# a pattern than 3/4/10, 9/15/11, 3/4/12.
|
5
|
+
|
6
|
+
class InferrableTest < ActiveSupport::TestCase
|
7
|
+
include Hiccup
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
test "should raise an error if not given an array of dates" do
|
12
|
+
assert_raises ArgumentError do
|
13
|
+
Schedule.infer(["what's this?"])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
test "extract_array_of_dates! should interpret date-like things as dates" do
|
18
|
+
datelike_things = [Date.today, Time.now, DateTime.new(2012, 8, 17)]
|
19
|
+
dates = Schedule.extract_array_of_dates!(datelike_things)
|
20
|
+
assert_equal datelike_things.length, dates.length
|
21
|
+
assert dates.all? { |date| date.is_a?(Date) }, "Expected these to all be dates, but they were #{dates.map(&:class)}"
|
22
|
+
end
|
23
|
+
|
24
|
+
test "extract_array_of_dates! should put dates in order" do
|
25
|
+
input_dates = %w{2014-01-01 2007-01-01 2009-01-01}
|
26
|
+
expected_dates = %w{2007-01-01 2009-01-01 2014-01-01}
|
27
|
+
dates = Schedule.extract_array_of_dates!(input_dates)
|
28
|
+
assert_equal expected_dates, dates.map(&:to_s)
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
test "should prefer guesses that predict too many results over guesses that predict too few" do
|
34
|
+
|
35
|
+
# In this stream of dates, we could guess an annual recurrence on 1/15
|
36
|
+
# or a monthly recurrence on the 15th.
|
37
|
+
#
|
38
|
+
# - The annual recurrence would have zero bricks (no unfulfilled predictions),
|
39
|
+
# but it would fail to predict 5 of the dates in this list.
|
40
|
+
#
|
41
|
+
# - The monthly recurrence would have zero failures (there's nothing in
|
42
|
+
# in the list it _wouldn't_ predict), but it would have 6 bricks.
|
43
|
+
#
|
44
|
+
dates = %w{2011-1-15 2011-2-15 2011-5-15 2011-7-15 2011-8-15 2011-11-15 2012-1-15}
|
45
|
+
|
46
|
+
# If bricks and fails were equal, the annual recurrence would be the
|
47
|
+
# preferred guess, but the monthly one makes more sense.
|
48
|
+
# It is better to brick than to fail.
|
49
|
+
schedule = Schedule.infer(dates)
|
50
|
+
assert_equal :monthly, schedule.kind
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
|
57
|
+
# Infers annual schedules
|
58
|
+
|
59
|
+
test "should infer an annual" do
|
60
|
+
dates = %w{2010-3-4 2011-3-4 2012-3-4}
|
61
|
+
schedule = Schedule.infer(dates)
|
62
|
+
assert_equal "Every year on March 4", schedule.humanize
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
# ... with skips
|
67
|
+
|
68
|
+
test "should infer a schedule that occurs every other year" do
|
69
|
+
dates = %w{2010-3-4 2012-3-4 2014-3-4}
|
70
|
+
schedule = Schedule.infer(dates)
|
71
|
+
assert_equal "Every other year on March 4", schedule.humanize
|
72
|
+
end
|
73
|
+
|
74
|
+
# ... where some of the input is wrong
|
75
|
+
|
76
|
+
test "should infer a yearly schedule when one of the dates was rescheduled" do
|
77
|
+
dates = %w{2010-3-4 2011-9-15 2012-3-4 2013-3-4}
|
78
|
+
schedule = Schedule.infer(dates)
|
79
|
+
assert_equal "Every year on March 4", schedule.humanize
|
80
|
+
end
|
81
|
+
|
82
|
+
test "should infer a yearly schedule when the first date was rescheduled" do
|
83
|
+
dates = %w{2010-3-6 2011-3-4 2012-3-4 2013-3-4}
|
84
|
+
schedule = Schedule.infer(dates)
|
85
|
+
assert_equal "Every year on March 4", schedule.humanize
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
|
90
|
+
|
91
|
+
|
92
|
+
# Infers monthly schedules
|
93
|
+
|
94
|
+
test "should infer a monthly schedule that occurs on a date" do
|
95
|
+
dates = %w{2012-2-4 2012-3-4 2012-4-4}
|
96
|
+
schedule = Schedule.infer(dates)
|
97
|
+
assert_equal "The 4th of every month", schedule.humanize
|
98
|
+
|
99
|
+
dates = %w{2012-2-17 2012-3-17 2012-4-17}
|
100
|
+
schedule = Schedule.infer(dates)
|
101
|
+
assert_equal "The 17th of every month", schedule.humanize
|
102
|
+
end
|
103
|
+
|
104
|
+
test "should infer a monthly schedule that occurs on a weekday" do
|
105
|
+
dates = %w{2012-7-9 2012-8-13 2012-9-10}
|
106
|
+
schedule = Schedule.infer(dates)
|
107
|
+
assert_equal "The second Monday of every month", schedule.humanize
|
108
|
+
end
|
109
|
+
|
110
|
+
test "should infer a schedule that occurs several times a month" do
|
111
|
+
dates = %w{2012-7-9 2012-7-23 2012-8-13 2012-8-27 2012-9-10 2012-9-24}
|
112
|
+
schedule = Schedule.infer(dates)
|
113
|
+
assert_equal "The second Monday and fourth Monday of every month", schedule.humanize
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
# ... with skips
|
118
|
+
|
119
|
+
test "should infer a schedule that occurs every third month" do
|
120
|
+
dates = %w{2012-2-4 2012-5-4 2012-8-4}
|
121
|
+
schedule = Schedule.infer(dates)
|
122
|
+
assert_equal "The 4th of every third month", schedule.humanize
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
# ... when some dates are wrong in the input group
|
127
|
+
|
128
|
+
test "should infer a monthly (by day) schedule when one day was rescheduled" do
|
129
|
+
dates = %w{2012-10-02 2012-11-02 2012-12-03}
|
130
|
+
schedule = Schedule.infer(dates)
|
131
|
+
assert_equal "The 2nd of every month", schedule.humanize
|
132
|
+
end
|
133
|
+
|
134
|
+
test "should infer a monthly (by day) schedule when the first day was rescheduled" do
|
135
|
+
dates = %w{2012-10-03 2012-11-02 2012-12-02}
|
136
|
+
schedule = Schedule.infer(dates)
|
137
|
+
assert_equal "The 2nd of every month", schedule.humanize
|
138
|
+
end
|
139
|
+
|
140
|
+
|
141
|
+
test "should infer a monthly (by weekday) schedule when one day was rescheduled" do
|
142
|
+
dates = %w{2012-10-02 2012-11-06 2012-12-05} # 1st Tuesday, 1st Tuesday, 1st Wednesday
|
143
|
+
schedule = Schedule.infer(dates)
|
144
|
+
assert_equal "The first Tuesday of every month", schedule.humanize
|
145
|
+
end
|
146
|
+
|
147
|
+
test "should infer a monthly (by weekday) schedule when the first day was rescheduled" do
|
148
|
+
dates = %w{2012-10-03 2012-11-01 2012-12-06} # 1st Wednesday, 1st Thursday, 1st Thursday
|
149
|
+
schedule = Schedule.infer(dates)
|
150
|
+
assert_equal "The first Thursday of every month", schedule.humanize
|
151
|
+
end
|
152
|
+
|
153
|
+
test "should infer a monthly (by weekday) schedule when the first day was rescheduled 2" do
|
154
|
+
dates = %w{2012-10-11 2012-11-01 2012-12-06} # 2nd Thursday, 1st Thursday, 1st Thursday
|
155
|
+
schedule = Schedule.infer(dates)
|
156
|
+
assert_equal "The first Thursday of every month", schedule.humanize
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
# Infers weekly schedules
|
164
|
+
|
165
|
+
test "should infer a weekly schedule" do
|
166
|
+
dates = %w{2012-3-4 2012-3-11 2012-3-18}
|
167
|
+
schedule = Schedule.infer(dates)
|
168
|
+
assert_equal "Every Sunday", schedule.humanize
|
169
|
+
end
|
170
|
+
|
171
|
+
test "should infer a schedule that occurs several times a week" do
|
172
|
+
dates = %w{2012-3-6 2012-3-8 2012-3-13 2012-3-15 2012-3-20 2012-3-22}
|
173
|
+
schedule = Schedule.infer(dates)
|
174
|
+
assert_equal "Every Tuesday and Thursday", schedule.humanize
|
175
|
+
end
|
176
|
+
|
177
|
+
|
178
|
+
# ... with skips
|
179
|
+
|
180
|
+
test "should infer weekly recurrence for something that occurs every other week" do
|
181
|
+
dates = %w{2012-3-6 2012-3-8 2012-3-20 2012-3-22}
|
182
|
+
schedule = Schedule.infer(dates)
|
183
|
+
assert_equal "Tuesday and Thursday of every other week", schedule.humanize
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
# ... when some dates are missing from the input array
|
188
|
+
|
189
|
+
test "should infer a weekly schedule (missing dates)" do
|
190
|
+
dates = %w{2012-3-4 2012-3-11 2012-3-25}
|
191
|
+
schedule = Schedule.infer(dates)
|
192
|
+
assert_equal "Every Sunday", schedule.humanize
|
193
|
+
end
|
194
|
+
|
195
|
+
test "should infer a schedule that occurs several times a week (missing dates)" do
|
196
|
+
dates = %w{2012-3-6 2012-3-8 2012-3-15 2012-3-20 2012-3-27 2012-3-29}
|
197
|
+
schedule = Schedule.infer(dates)
|
198
|
+
assert_equal "Every Tuesday and Thursday", schedule.humanize
|
199
|
+
end
|
200
|
+
|
201
|
+
|
202
|
+
# ... when some dates are wrong in the input group
|
203
|
+
|
204
|
+
test "should infer a weekly schedule when one day was rescheduled" do
|
205
|
+
dates = %w{2012-10-02 2012-10-09 2012-10-15} # a Tuesday, a Tuesday, and a Monday
|
206
|
+
schedule = Schedule.infer(dates)
|
207
|
+
assert_equal "Every Tuesday", schedule.humanize
|
208
|
+
end
|
209
|
+
|
210
|
+
test "should infer a weekly schedule when the first day was rescheduled" do
|
211
|
+
dates = %w{2012-10-07 2012-10-10 2012-10-17} # a Sunday, a Wednesday, and a Wednesday
|
212
|
+
schedule = Schedule.infer(dates)
|
213
|
+
assert_equal "Every Wednesday", schedule.humanize
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
|
218
|
+
|
219
|
+
# Correctly identifies scenarios where there is no pattern
|
220
|
+
|
221
|
+
test "should not try to guess a pattern for input where there is none" do
|
222
|
+
arbitrary_date_ranges = [
|
223
|
+
%w{2013-01-01 2013-03-30 2014-08-19},
|
224
|
+
%w{2012-10-01 2012-10-09 2012-10-17}, # a Monday, a Tuesday, and a Wednesday
|
225
|
+
]
|
226
|
+
|
227
|
+
arbitrary_date_ranges.each do |dates|
|
228
|
+
schedule = Schedule.infer(dates)
|
229
|
+
fail "There should be no pattern to the dates #{dates}, but Hiccup guessed \"#{schedule.humanize}\"" unless schedule.never?
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
|
234
|
+
|
235
|
+
test "should diabolically complex schedules" do
|
236
|
+
dates = %w{2012-11-06 2012-11-08 2012-11-15 2012-11-20 2012-11-27 2012-11-29 2013-02-05 2013-02-14 2013-02-21 2013-02-19 2013-02-26 2013-05-07 2013-05-09 2013-05-16 2013-05-28 2013-05-21 2013-05-30}
|
237
|
+
schedule = Schedule.infer(dates)
|
238
|
+
assert_equal "The first Tuesday, second Thursday, third Thursday, third Tuesday, fourth Tuesday, and fifth Thursday of every third month", schedule.humanize
|
239
|
+
end
|
240
|
+
|
241
|
+
|
242
|
+
|
243
|
+
|
244
|
+
end
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -1,138 +1,201 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: hiccup
|
3
|
-
version: !ruby/object:Gem::Version
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.3.0
|
4
5
|
prerelease:
|
5
|
-
version: 0.2.1
|
6
6
|
platform: ruby
|
7
|
-
authors:
|
7
|
+
authors:
|
8
8
|
- Bob Lail
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
dependencies:
|
16
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2012-11-06 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
17
15
|
name: activesupport
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.8
|
22
|
+
type: :runtime
|
18
23
|
prerelease: false
|
19
|
-
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.2.8
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: builder
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
20
33
|
none: false
|
21
|
-
requirements:
|
22
|
-
- -
|
23
|
-
- !ruby/object:Gem::Version
|
24
|
-
version:
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
25
38
|
type: :runtime
|
26
|
-
version_requirements: *id001
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: ri_cal
|
29
39
|
prerelease: false
|
30
|
-
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: ri_cal
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
31
49
|
none: false
|
32
|
-
requirements:
|
33
|
-
- -
|
34
|
-
- !ruby/object:Gem::Version
|
35
|
-
version:
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
36
54
|
type: :development
|
37
|
-
version_requirements: *id002
|
38
|
-
- !ruby/object:Gem::Dependency
|
39
|
-
name: rails
|
40
55
|
prerelease: false
|
41
|
-
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
57
|
none: false
|
43
|
-
requirements:
|
44
|
-
- -
|
45
|
-
- !ruby/object:Gem::Version
|
46
|
-
version:
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rails
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 3.2.8
|
47
70
|
type: :development
|
48
|
-
version_requirements: *id003
|
49
|
-
- !ruby/object:Gem::Dependency
|
50
|
-
name: turn
|
51
71
|
prerelease: false
|
52
|
-
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
53
73
|
none: false
|
54
|
-
requirements:
|
55
|
-
- -
|
56
|
-
- !ruby/object:Gem::Version
|
57
|
-
version:
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 3.2.8
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: turn
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
58
86
|
type: :development
|
59
|
-
|
60
|
-
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
61
95
|
name: simplecov
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
62
103
|
prerelease: false
|
63
|
-
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: pry
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
64
113
|
none: false
|
65
|
-
requirements:
|
66
|
-
- -
|
67
|
-
- !ruby/object:Gem::Version
|
68
|
-
version:
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
69
118
|
type: :development
|
70
|
-
|
71
|
-
|
72
|
-
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: Hiccup mixes a-la-cart recurrence features into your data structure.
|
127
|
+
It doesn't dictate the data structure, just the interface.
|
128
|
+
email:
|
73
129
|
- bob.lailfamily@gmail.com
|
74
130
|
executables: []
|
75
|
-
|
76
131
|
extensions: []
|
77
|
-
|
78
132
|
extra_rdoc_files: []
|
79
|
-
|
80
|
-
files:
|
133
|
+
files:
|
81
134
|
- .gitignore
|
82
135
|
- .simplecov
|
83
136
|
- Gemfile
|
137
|
+
- README.mdown
|
84
138
|
- Rakefile
|
85
139
|
- hiccup.gemspec
|
86
140
|
- lib/hiccup.rb
|
87
141
|
- lib/hiccup/convenience.rb
|
88
142
|
- lib/hiccup/core_ext/date.rb
|
89
143
|
- lib/hiccup/core_ext/duration.rb
|
144
|
+
- lib/hiccup/core_ext/enumerable.rb
|
90
145
|
- lib/hiccup/core_ext/fixnum.rb
|
146
|
+
- lib/hiccup/core_ext/hash.rb
|
91
147
|
- lib/hiccup/enumerable.rb
|
92
148
|
- lib/hiccup/humanizable.rb
|
149
|
+
- lib/hiccup/inferrable.rb
|
93
150
|
- lib/hiccup/schedule.rb
|
94
151
|
- lib/hiccup/serializable/ical.rb
|
95
152
|
- lib/hiccup/serializers/ical.rb
|
96
153
|
- lib/hiccup/validatable.rb
|
97
154
|
- lib/hiccup/version.rb
|
155
|
+
- test/core_ext_date_test.rb
|
98
156
|
- test/duration_ext_test.rb
|
99
157
|
- test/enumerable_test.rb
|
100
158
|
- test/humanizable_test.rb
|
101
159
|
- test/ical_serializable_test.rb
|
160
|
+
- test/inferrable_test.rb
|
102
161
|
- test/test_helper.rb
|
103
162
|
- test/validatable_test.rb
|
104
|
-
has_rdoc: true
|
105
163
|
homepage: http://boblail.github.com/hiccup/
|
106
164
|
licenses: []
|
107
|
-
|
108
165
|
post_install_message:
|
109
166
|
rdoc_options: []
|
110
|
-
|
111
|
-
require_paths:
|
167
|
+
require_paths:
|
112
168
|
- lib
|
113
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
169
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
114
170
|
none: false
|
115
|
-
requirements:
|
116
|
-
- -
|
117
|
-
- !ruby/object:Gem::Version
|
118
|
-
version:
|
119
|
-
|
171
|
+
requirements:
|
172
|
+
- - ! '>='
|
173
|
+
- !ruby/object:Gem::Version
|
174
|
+
version: '0'
|
175
|
+
segments:
|
176
|
+
- 0
|
177
|
+
hash: -955647137280487206
|
178
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
179
|
none: false
|
121
|
-
requirements:
|
122
|
-
- -
|
123
|
-
- !ruby/object:Gem::Version
|
124
|
-
version:
|
180
|
+
requirements:
|
181
|
+
- - ! '>='
|
182
|
+
- !ruby/object:Gem::Version
|
183
|
+
version: '0'
|
184
|
+
segments:
|
185
|
+
- 0
|
186
|
+
hash: -955647137280487206
|
125
187
|
requirements: []
|
126
|
-
|
127
188
|
rubyforge_project: hiccup
|
128
|
-
rubygems_version: 1.
|
189
|
+
rubygems_version: 1.8.23
|
129
190
|
signing_key:
|
130
191
|
specification_version: 3
|
131
|
-
summary:
|
132
|
-
test_files:
|
192
|
+
summary: A library for working with things that recur
|
193
|
+
test_files:
|
194
|
+
- test/core_ext_date_test.rb
|
133
195
|
- test/duration_ext_test.rb
|
134
196
|
- test/enumerable_test.rb
|
135
197
|
- test/humanizable_test.rb
|
136
198
|
- test/ical_serializable_test.rb
|
199
|
+
- test/inferrable_test.rb
|
137
200
|
- test/test_helper.rb
|
138
201
|
- test/validatable_test.rb
|