hiccup 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|