hiccup 0.0.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile 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