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 CHANGED
@@ -1 +1,9 @@
1
- require 'bundler/gem_tasks'
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "lib"
6
+ t.libs << "test"
7
+ t.pattern = "test/**/*_test.rb"
8
+ t.verbose = false
9
+ end
@@ -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) }
@@ -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
- # Your code goes here...
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