awallis-ri_cal 0.8.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (166) hide show
  1. data/History.txt +402 -0
  2. data/Manifest.txt +161 -0
  3. data/README.txt +410 -0
  4. data/Rakefile +69 -0
  5. data/VERSION +1 -0
  6. data/bin/ri_cal +8 -0
  7. data/component_attributes/alarm.yml +10 -0
  8. data/component_attributes/calendar.yml +4 -0
  9. data/component_attributes/component_property_defs.yml +180 -0
  10. data/component_attributes/event.yml +45 -0
  11. data/component_attributes/freebusy.yml +16 -0
  12. data/component_attributes/journal.yml +35 -0
  13. data/component_attributes/timezone.yml +3 -0
  14. data/component_attributes/timezone_period.yml +11 -0
  15. data/component_attributes/todo.yml +46 -0
  16. data/copyrights.txt +1 -0
  17. data/docs/draft-ietf-calsify-2446bis-08.txt +7280 -0
  18. data/docs/draft-ietf-calsify-rfc2445bis-09.txt +10416 -0
  19. data/docs/incrementers.txt +7 -0
  20. data/docs/rfc2445.pdf +0 -0
  21. data/lib/ri_cal.rb +187 -0
  22. data/lib/ri_cal/component.rb +256 -0
  23. data/lib/ri_cal/component/alarm.rb +19 -0
  24. data/lib/ri_cal/component/calendar.rb +257 -0
  25. data/lib/ri_cal/component/event.rb +58 -0
  26. data/lib/ri_cal/component/freebusy.rb +16 -0
  27. data/lib/ri_cal/component/journal.rb +27 -0
  28. data/lib/ri_cal/component/non_standard.rb +33 -0
  29. data/lib/ri_cal/component/t_z_info_timezone.rb +153 -0
  30. data/lib/ri_cal/component/timezone.rb +197 -0
  31. data/lib/ri_cal/component/timezone/daylight_period.rb +25 -0
  32. data/lib/ri_cal/component/timezone/standard_period.rb +23 -0
  33. data/lib/ri_cal/component/timezone/timezone_period.rb +76 -0
  34. data/lib/ri_cal/component/todo.rb +42 -0
  35. data/lib/ri_cal/core_extensions.rb +11 -0
  36. data/lib/ri_cal/core_extensions/array.rb +7 -0
  37. data/lib/ri_cal/core_extensions/array/conversions.rb +15 -0
  38. data/lib/ri_cal/core_extensions/date.rb +13 -0
  39. data/lib/ri_cal/core_extensions/date/conversions.rb +56 -0
  40. data/lib/ri_cal/core_extensions/date_time.rb +15 -0
  41. data/lib/ri_cal/core_extensions/date_time/conversions.rb +50 -0
  42. data/lib/ri_cal/core_extensions/object.rb +8 -0
  43. data/lib/ri_cal/core_extensions/object/conversions.rb +20 -0
  44. data/lib/ri_cal/core_extensions/string.rb +8 -0
  45. data/lib/ri_cal/core_extensions/string/conversions.rb +57 -0
  46. data/lib/ri_cal/core_extensions/time.rb +14 -0
  47. data/lib/ri_cal/core_extensions/time/calculations.rb +153 -0
  48. data/lib/ri_cal/core_extensions/time/conversions.rb +42 -0
  49. data/lib/ri_cal/core_extensions/time/tzid_access.rb +50 -0
  50. data/lib/ri_cal/core_extensions/time/week_day_predicates.rb +55 -0
  51. data/lib/ri_cal/fast_date_time.rb +234 -0
  52. data/lib/ri_cal/floating_timezone.rb +32 -0
  53. data/lib/ri_cal/invalid_property_value.rb +8 -0
  54. data/lib/ri_cal/invalid_timezone_identifier.rb +20 -0
  55. data/lib/ri_cal/occurrence_enumerator.rb +265 -0
  56. data/lib/ri_cal/occurrence_period.rb +17 -0
  57. data/lib/ri_cal/parser.rb +145 -0
  58. data/lib/ri_cal/properties.rb +12 -0
  59. data/lib/ri_cal/properties/alarm.rb +390 -0
  60. data/lib/ri_cal/properties/calendar.rb +164 -0
  61. data/lib/ri_cal/properties/event.rb +1523 -0
  62. data/lib/ri_cal/properties/freebusy.rb +593 -0
  63. data/lib/ri_cal/properties/journal.rb +1237 -0
  64. data/lib/ri_cal/properties/timezone.rb +150 -0
  65. data/lib/ri_cal/properties/timezone_period.rb +416 -0
  66. data/lib/ri_cal/properties/todo.rb +1559 -0
  67. data/lib/ri_cal/property_value.rb +159 -0
  68. data/lib/ri_cal/property_value/array.rb +27 -0
  69. data/lib/ri_cal/property_value/cal_address.rb +11 -0
  70. data/lib/ri_cal/property_value/date.rb +184 -0
  71. data/lib/ri_cal/property_value/date_time.rb +359 -0
  72. data/lib/ri_cal/property_value/date_time/additive_methods.rb +44 -0
  73. data/lib/ri_cal/property_value/date_time/time_machine.rb +159 -0
  74. data/lib/ri_cal/property_value/date_time/timezone_support.rb +100 -0
  75. data/lib/ri_cal/property_value/duration.rb +110 -0
  76. data/lib/ri_cal/property_value/geo.rb +11 -0
  77. data/lib/ri_cal/property_value/integer.rb +12 -0
  78. data/lib/ri_cal/property_value/occurrence_list.rb +144 -0
  79. data/lib/ri_cal/property_value/period.rb +86 -0
  80. data/lib/ri_cal/property_value/recurrence_rule.rb +154 -0
  81. data/lib/ri_cal/property_value/recurrence_rule/enumeration_support_methods.rb +100 -0
  82. data/lib/ri_cal/property_value/recurrence_rule/enumerator.rb +79 -0
  83. data/lib/ri_cal/property_value/recurrence_rule/initialization_methods.rb +148 -0
  84. data/lib/ri_cal/property_value/recurrence_rule/negative_setpos_enumerator.rb +53 -0
  85. data/lib/ri_cal/property_value/recurrence_rule/numbered_span.rb +31 -0
  86. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer.rb +135 -0
  87. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_day_incrementer.rb +86 -0
  88. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_hour_incrementer.rb +31 -0
  89. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_minute_incrementer.rb +32 -0
  90. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_month_incrementer.rb +52 -0
  91. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_monthday_incrementer.rb +31 -0
  92. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_numbered_day_incrementer.rb +38 -0
  93. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_second_incrementer.rb +32 -0
  94. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_weekno_incrementer.rb +69 -0
  95. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_yearday_incrementer.rb +31 -0
  96. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/daily_incrementer.rb +28 -0
  97. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/frequency_incrementer.rb +80 -0
  98. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/hourly_incrementer.rb +23 -0
  99. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/list_incrementer.rb +106 -0
  100. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/minutely_incrementer.rb +23 -0
  101. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/monthly_incrementer.rb +33 -0
  102. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/null_sub_cycle_incrementer.rb +43 -0
  103. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/secondly_incrementer.rb +28 -0
  104. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/weekly_incrementer.rb +37 -0
  105. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/yearly_incrementer.rb +57 -0
  106. data/lib/ri_cal/property_value/recurrence_rule/recurring_day.rb +131 -0
  107. data/lib/ri_cal/property_value/recurrence_rule/recurring_month_day.rb +64 -0
  108. data/lib/ri_cal/property_value/recurrence_rule/recurring_numbered_week.rb +33 -0
  109. data/lib/ri_cal/property_value/recurrence_rule/recurring_year_day.rb +53 -0
  110. data/lib/ri_cal/property_value/recurrence_rule/time_manipulation.rb +42 -0
  111. data/lib/ri_cal/property_value/recurrence_rule/validations.rb +125 -0
  112. data/lib/ri_cal/property_value/text.rb +44 -0
  113. data/lib/ri_cal/property_value/uri.rb +11 -0
  114. data/lib/ri_cal/property_value/utc_offset.rb +33 -0
  115. data/lib/ri_cal/property_value/zulu_date_time.rb +34 -0
  116. data/lib/ri_cal/required_timezones.rb +55 -0
  117. data/parked_specs/ri_cal/claudio_a_bug_spec.rb +100 -0
  118. data/performance/empty_propval/subject.rb +43 -0
  119. data/performance/paris_eastern/subject.rb +90 -0
  120. data/performance/penultimate_weekday/subject.rb +15 -0
  121. data/performance/psm_big_enum/ical.ics +3171 -0
  122. data/performance/psm_big_enum/subject.rb +16 -0
  123. data/performance/utah_cycling/subject.rb +55 -0
  124. data/ri_cal.gemspec +244 -0
  125. data/script/benchmark_subject +23 -0
  126. data/script/console +10 -0
  127. data/script/destroy +14 -0
  128. data/script/generate +14 -0
  129. data/script/profile_subject +29 -0
  130. data/script/txt2html +71 -0
  131. data/spec/ri_cal/bugreports_spec.rb +276 -0
  132. data/spec/ri_cal/component/alarm_spec.rb +12 -0
  133. data/spec/ri_cal/component/calendar_spec.rb +88 -0
  134. data/spec/ri_cal/component/event_spec.rb +735 -0
  135. data/spec/ri_cal/component/freebusy_spec.rb +12 -0
  136. data/spec/ri_cal/component/journal_spec.rb +37 -0
  137. data/spec/ri_cal/component/t_z_info_timezone_spec.rb +60 -0
  138. data/spec/ri_cal/component/timezone_spec.rb +236 -0
  139. data/spec/ri_cal/component/todo_spec.rb +112 -0
  140. data/spec/ri_cal/component_spec.rb +224 -0
  141. data/spec/ri_cal/core_extensions/string/conversions_spec.rb +78 -0
  142. data/spec/ri_cal/core_extensions/time/calculations_spec.rb +188 -0
  143. data/spec/ri_cal/core_extensions/time/week_day_predicates_spec.rb +45 -0
  144. data/spec/ri_cal/fast_date_time_spec.rb +77 -0
  145. data/spec/ri_cal/inf_loop_spec.rb +78 -0
  146. data/spec/ri_cal/occurrence_enumerator_spec.rb +611 -0
  147. data/spec/ri_cal/parser_spec.rb +337 -0
  148. data/spec/ri_cal/property_value/date_spec.rb +53 -0
  149. data/spec/ri_cal/property_value/date_time_spec.rb +383 -0
  150. data/spec/ri_cal/property_value/duration_spec.rb +126 -0
  151. data/spec/ri_cal/property_value/occurrence_list_spec.rb +72 -0
  152. data/spec/ri_cal/property_value/period_spec.rb +63 -0
  153. data/spec/ri_cal/property_value/recurrence_rule/recurring_year_day_spec.rb +21 -0
  154. data/spec/ri_cal/property_value/recurrence_rule_spec.rb +1814 -0
  155. data/spec/ri_cal/property_value/text_spec.rb +25 -0
  156. data/spec/ri_cal/property_value/utc_offset_spec.rb +48 -0
  157. data/spec/ri_cal/property_value_spec.rb +125 -0
  158. data/spec/ri_cal/required_timezones_spec.rb +67 -0
  159. data/spec/ri_cal_spec.rb +53 -0
  160. data/spec/spec.opts +4 -0
  161. data/spec/spec_helper.rb +50 -0
  162. data/tasks/gem_loader/load_active_support.rb +3 -0
  163. data/tasks/gem_loader/load_tzinfo_gem.rb +2 -0
  164. data/tasks/ri_cal.rake +412 -0
  165. data/tasks/spec.rake +102 -0
  166. metadata +255 -0
@@ -0,0 +1,234 @@
1
+ module RiCal
2
+ #- ©2009 Rick DeNatale
3
+ #- All rights reserved. Refer to the file README.txt for the license
4
+ #
5
+ # FastDateTime mimics the Ruby Standard library DateTime class but avoids the use of Rational
6
+ # Instead of using a Rational for the utc offset, FastDateTime uses an integer seconds value
7
+ class FastDateTime
8
+ attr_accessor :date, :hour, :min, :sec, :offset, :secs_since_bod
9
+
10
+ SECONDS_IN_A_DAY = 60*60*24 unless defined? SECONDS_IN_A_DAY
11
+
12
+ include Comparable
13
+
14
+ def initialize(year, month, day, hour, min, sec, offset_seconds)
15
+ @date = Date.civil(year, month, day)
16
+ @secs_since_bod = hms_to_seconds(hour, min, sec)
17
+ @hour, @min, @sec, @offset = hour, min, sec, offset_seconds
18
+ end
19
+
20
+ def self.from_date_time(date_time)
21
+ new(date_time.year, date_time.month, date_time.day, date_time.hour, date_time.min, date_time.sec, (date_time.offset * SECONDS_IN_A_DAY).to_i)
22
+ end
23
+
24
+ def self.from_time(time)
25
+ new(time.year, time.month, time.day, time.hour, time.min, time.sec, (time.utc_offset.offset * SECONDS_IN_A_DAY))
26
+ end
27
+
28
+ def self.from_date(date)
29
+ new(date.year, date.month, date.day, 0, 0, 0, 0)
30
+ end
31
+
32
+ def self.from_date_at_end_of_day(date)
33
+ new(date.year, date.month, date.day, 23, 59, 59, 0)
34
+ end
35
+
36
+ alias_method :utc_offset_seconds, :offset
37
+
38
+ def ical_str
39
+ "%04d%02d%02dT%02d%02d%02d" % [year, month, day, hour, min, sec]
40
+ end
41
+
42
+ def ical_date_str
43
+ "%04d%02d%02d" % [year, month, day]
44
+ end
45
+
46
+ def year
47
+ @date.year
48
+ end
49
+
50
+ def month
51
+ @date.month
52
+ end
53
+
54
+ alias_method :mon, :month
55
+
56
+ def day
57
+ @date.day
58
+ end
59
+
60
+ def wday
61
+ @date.wday
62
+ end
63
+
64
+ def to_datetime
65
+ DateTime.civil(year, month, day, hour, min, sec, RiCal.RationalOffset[utc_offset_seconds])
66
+ end
67
+
68
+ def ==(other)
69
+ [date, secs_since_bod, offset] == [other.date, other.secs_since_bod, other.offset]
70
+ end
71
+
72
+ def <=> (other)
73
+ if FastDateTime === other
74
+ [date, secs_since_bod] <=> [other.date, other.secs_since_bod]
75
+ else
76
+ [year, month, day, hour, min, sec] <=> [other.year, other.month, other.day, other.hour, other.min, other.sec]
77
+ end
78
+ end
79
+
80
+ def to_s
81
+ "#{year}/#{month}/#{day} #{hour}:#{min}:#{sec} #{offset}"
82
+ end
83
+
84
+ # def jd
85
+ # date.jd
86
+ # end
87
+ #
88
+ def days_in_month
89
+ date.days_in_month
90
+ end
91
+
92
+ alias_method :inspect, :to_s
93
+
94
+ # Return a new FastDateTime based on the receiver but with changes specified by the options
95
+ def change(options)
96
+ FastDateTime.new(
97
+ options[:year] || year,
98
+ options[:month] || month,
99
+ options[:day] || day,
100
+ options[:hour] || hour,
101
+ options[:min] || (options[:hour] ? 0 : min),
102
+ options[:sec] || ((options[:hour] || options[:min]) ? 0 : sec),
103
+ options[:offset] || offset
104
+ )
105
+ end
106
+
107
+ # def new_offset(ofst)
108
+ # if ofst == offset
109
+ # self
110
+ # else
111
+ # advance(:seconds => offset - ofset, :offset => ofst)
112
+ # end
113
+ # end
114
+
115
+ def utc
116
+ if offset == 0
117
+ self
118
+ else
119
+ advance(:seconds => -offset, :offset => 0)
120
+ end
121
+ end
122
+
123
+ def hms_to_seconds(hours, minutes, seconds)
124
+ seconds + 60 *(minutes + (60 * hours))
125
+ end
126
+
127
+ def seconds_to_hms(total_seconds)
128
+ sign = total_seconds <=> 0
129
+ remaining = total_seconds.abs
130
+ seconds = sign * (remaining % 60)
131
+ remaining = remaining / 60
132
+ minutes = sign * (remaining % 60)
133
+ [remaining / 60, minutes, seconds]
134
+ end
135
+
136
+ def adjust_day_delta(day_delta, new_secs_since_bod)
137
+ if new_secs_since_bod == 0
138
+ [day_delta, new_secs_since_bod]
139
+ elsif new_secs_since_bod > 0
140
+ [day_delta + (new_secs_since_bod / SECONDS_IN_A_DAY), new_secs_since_bod % SECONDS_IN_A_DAY]
141
+ else
142
+ [day_delta - (1 + new_secs_since_bod.abs / SECONDS_IN_A_DAY),
143
+ SECONDS_IN_A_DAY - (new_secs_since_bod.abs % SECONDS_IN_A_DAY)]
144
+ end
145
+ end
146
+
147
+
148
+ def advance(options) # :nodoc:
149
+ new_date = @date
150
+ new_offset = options[:offset] || offset
151
+ month_delta = (options[:years] || 0) * 12 + (options[:months] || 0)
152
+ day_delta = (options[:weeks] || 0) * 7 + (options[:days] || 0)
153
+ sec_delta = hms_to_seconds((options[:hours] || 0), (options[:minutes] || 0), (options[:seconds] || 0))
154
+ day_delta, new_secs_since_bod = *adjust_day_delta(day_delta, secs_since_bod + sec_delta)
155
+ new_hour, new_min, new_sec = *seconds_to_hms(new_secs_since_bod)
156
+ new_date = new_date >> month_delta unless month_delta == 0
157
+ new_date += day_delta unless day_delta == 0
158
+ FastDateTime.new(new_date.year, new_date.month, new_date.day, new_hour, new_min, new_sec, new_offset)
159
+ end
160
+
161
+ # Determine the day which falls on a particular weekday of the same month as the receiver
162
+ #
163
+ # == Parameters
164
+ # n:: the ordinal number being requested
165
+ # which_wday:: the weekday using Ruby time conventions, i.e. 0 => Sunday, 1 => Monday, ...
166
+
167
+ # e.g. to obtain the 3nd Tuesday of the receivers month use
168
+ #
169
+ # time.nth_wday_in_month(2, 2)
170
+ def nth_wday_in_month(n, which_wday)
171
+ first_of_month = change(:day => 1)
172
+ first_in_month = first_of_month.advance(:days => (which_wday - first_of_month.wday))
173
+ first_in_month = first_in_month.advance(:days => 7) if first_in_month.month != first_of_month.month
174
+ if n > 0
175
+ first_in_month.advance(:days => (7*(n - 1)))
176
+ else
177
+ possible = first_in_month.advance(:days => 21)
178
+ possible = possible.advance(:days => 7) while possible.month == first_in_month.month
179
+ last_in_month = possible.advance(:days => - 7)
180
+ (last_in_month.advance(:days => - (7*(n.abs - 1))))
181
+ end
182
+ end
183
+
184
+ # Determine the equivalent time on the day which falls on a particular weekday of the same year as the receiver
185
+ #
186
+ # == Parameters
187
+ # n:: the ordinal number being requested
188
+ # which_wday:: the weekday using Ruby time conventions, i.e. 0 => Sunday, 1 => Monday, ...
189
+
190
+ # e.g. to obtain the 2nd Monday of the receivers year use
191
+ #
192
+ # time.nth_wday_in_year(2, 1)
193
+ def nth_wday_in_year(n, which_wday)
194
+ if n > 0
195
+ first_of_year = change(:month => 1, :day => 1)
196
+ first_in_year = first_of_year.advance(:days => (which_wday - first_of_year.wday + 7) % 7)
197
+ first_in_year.advance(:days => (7*(n - 1)))
198
+ else
199
+ december25 = change(:month => 12, :day => 25)
200
+ last_in_year = december25.advance(:days => (which_wday - december25.wday + 7) % 7)
201
+ last_in_year.advance(:days => (7 * (n + 1)))
202
+ end
203
+ end
204
+
205
+
206
+ # Return a DateTime which is the beginning of the first day on or before the receiver
207
+ # with the specified wday
208
+ def start_of_week_with_wkst(wkst)
209
+ wkst ||= 1
210
+ date = @date
211
+ date -= 1 while date.wday != wkst
212
+ date
213
+ end
214
+
215
+ def iso_weeks_in_year(wkst)
216
+ @date.iso_weeks_in_year(wkst)
217
+ end
218
+
219
+ def iso_year_start(wkst)
220
+ @date.iso_year_start(wkst)
221
+ end
222
+
223
+ def iso_year_and_week_one_start(wkst)
224
+ @date.iso_year_and_week_one_start(wkst)
225
+ end
226
+
227
+ def cmp_fast_date_time_value(other)
228
+ other <=> self
229
+ end
230
+
231
+
232
+ end
233
+
234
+ end
@@ -0,0 +1,32 @@
1
+ module RiCal
2
+ #- ©2009 Rick DeNatale
3
+ #- All rights reserved. Refer to the file README.txt for the license
4
+ #
5
+ # FloatingTimezone represents the 'time zone' for a time or date time with no timezone
6
+ # Times with floating timezones are always interpreted in the timezone of the observer
7
+ class FloatingTimezone
8
+
9
+ def self.identifier #:nodoc:
10
+ nil
11
+ end
12
+
13
+ def self.tzinfo_timezone #:nodoc:
14
+ nil
15
+ end
16
+
17
+ def self.rational_utc_offset(local) #:nodoc:
18
+ @offset = RiCal.RationalOffset[0]
19
+ end
20
+
21
+ # Return the time unchanged
22
+ def self.utc_to_local(time)
23
+ time.with_floating_timezone.to_ri_cal_date_time_value
24
+ end
25
+
26
+ # Return the time unchanged
27
+ def self.local_to_utc(time)
28
+ time.with_floating_timezone.to_ri_cal_date_time_value
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,8 @@
1
+ module RiCal
2
+ #- ©2009 Rick DeNatale
3
+ #- All rights reserved. Refer to the file README.txt for the license
4
+ #
5
+ # An InvalidPropertyValue error is raised when an improper value is assigned to a property
6
+ class InvalidPropertyValue < StandardError
7
+ end
8
+ end
@@ -0,0 +1,20 @@
1
+ module RiCal
2
+ #- ©2009 Rick DeNatale
3
+ #- All rights reserved. Refer to the file README.txt for the license
4
+ #
5
+ # An InvalidTimezoneIdentifier error is raised when a DATETIME property with an invalid timezone is
6
+ # involved in a timezone conversion operation
7
+ #
8
+ # Rather than attempting to detect invalid timezones immediately the detection is deferred to avoid problems
9
+ # such as importing a calendar which has forward reference to VTIMEZONE components.
10
+ class InvalidTimezoneIdentifier < StandardError
11
+
12
+ def self.not_found_in_calendar(identifier) #:nodoc:
13
+ new("#{identifier.inspect} is not the identifier of a VTIMEZONE component of this calendar")
14
+ end
15
+
16
+ def self.invalid_tzinfo_identifier(identifier) #:nodoc:
17
+ new("#{identifier.inspect} is not known to the tzinfo database")
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,265 @@
1
+ module RiCal
2
+ #- ©2009 Rick DeNatale
3
+ #- All rights reserved. Refer to the file README.txt for the license
4
+ #
5
+ # OccurrenceEnumerator provides common methods for CalendarComponents that support recurrence
6
+ # i.e. Event, Journal, Todo, and TimezonePeriod
7
+ module OccurrenceEnumerator
8
+
9
+ include Enumerable
10
+
11
+ def default_duration # :nodoc:
12
+ dtend && dtstart.to_ri_cal_date_time_value.duration_until(dtend.to_ri_cal_date_time_value)
13
+ end
14
+
15
+ def default_start_time # :nodoc:
16
+ dtstart && dtstart.to_ri_cal_date_time_value
17
+ end
18
+
19
+ class EmptyRulesEnumerator # :nodoc:
20
+ def self.next_occurrence
21
+ nil
22
+ end
23
+
24
+ def self.bounded?
25
+ true
26
+ end
27
+
28
+ def self.empty?
29
+ true
30
+ end
31
+ end
32
+
33
+ # OccurrenceMerger takes multiple recurrence rules and enumerates the combination in sequence.
34
+ class OccurrenceMerger # :nodoc:
35
+ def self.for(component, rules)
36
+ if rules.nil? || rules.empty?
37
+ EmptyRulesEnumerator
38
+ elsif rules.length == 1
39
+ rules.first.enumerator(component)
40
+ else
41
+ new(component, rules)
42
+ end
43
+ end
44
+
45
+ attr_accessor :enumerators, :nexts
46
+
47
+ def initialize(component, rules)
48
+ self.enumerators = rules.map {|rrule| rrule.enumerator(component)}
49
+ @bounded = enumerators.all? {|enumerator| enumerator.bounded?}
50
+ @empty = enumerators.all? {|enumerator| enumerator.empty?}
51
+ self.nexts = @enumerators.map {|enumerator| enumerator.next_occurrence}
52
+ end
53
+
54
+ def empty?
55
+ @empty
56
+ end
57
+
58
+ # return the earliest of each of the enumerators next occurrences
59
+ def next_occurrence
60
+ result = nexts.compact.sort.first
61
+ if result
62
+ nexts.each_with_index { |datetimevalue, i| @nexts[i] = @enumerators[i].next_occurrence if result == datetimevalue }
63
+ end
64
+ result
65
+ end
66
+
67
+ def bounded?
68
+ @bounded
69
+ end
70
+ end
71
+
72
+ # EnumerationInstance holds the values needed during the enumeration of occurrences for a component.
73
+ class EnumerationInstance # :nodoc:
74
+ include Enumerable
75
+
76
+ def initialize(component)
77
+ @component = component
78
+ @rrules = OccurrenceMerger.for(@component, [@component.rrule_property, @component.rdate_property].flatten.compact)
79
+ @exrules = OccurrenceMerger.for(@component, [@component.exrule_property, @component.exdate_property].flatten.compact)
80
+ @yielded = 0
81
+ end
82
+
83
+ # return the next exclusion which starts at the same time or after the start time of the occurrence
84
+ # return nil if this exhausts the exclusion rules
85
+ def exclusion_for(occurrence)
86
+ while (@next_exclusion && @next_exclusion.dtstart < occurrence.dtstart)
87
+ @next_exclusion = @exrules.next_occurrence
88
+ end
89
+ @next_exclusion
90
+ end
91
+
92
+ # TODO: Need to research this, I beleive that this should also take the end time into account,
93
+ # but I need to research
94
+ def exclusion_match?(occurrence, exclusion)
95
+ exclusion && (occurrence.dtstart == exclusion.dtstart)
96
+ end
97
+
98
+ # Also exclude occurrences before the :starting date_time
99
+ def before_start?(occurrence)
100
+ (@start && occurrence.dtstart.to_datetime < @start) ||
101
+ @overlap_range && occurrence.before_range?(@overlap_range)
102
+ end
103
+
104
+ def next_occurrence
105
+ @next_exclusion ||= @exrules.next_occurrence
106
+ occurrence = nil
107
+
108
+ until occurrence
109
+ if (occurrence = @rrules.next_occurrence)
110
+ if exclusion_match?(occurrence, exclusion_for(occurrence))
111
+ occurrence = nil # Look for the next one
112
+ end
113
+ else
114
+ break
115
+ end
116
+ end
117
+ occurrence
118
+ end
119
+
120
+ def options_stop(occurrence)
121
+ occurrence != :excluded &&
122
+ (@cutoff && occurrence.dtstart.to_datetime >= @cutoff) ||
123
+ (@count && @yielded >= @count) ||
124
+ (@overlap_range && occurrence.after_range?(@overlap_range))
125
+ end
126
+
127
+
128
+ # yield each occurrence to a block
129
+ # some components may be open-ended, e.g. have no COUNT or DTEND
130
+ def each(options = nil)
131
+ process_options(options) if options
132
+ if @rrules.empty?
133
+ unless before_start?(@component)
134
+ yield @component unless options_stop(@component)
135
+ end
136
+ else
137
+ occurrence = next_occurrence
138
+ while (occurrence)
139
+ candidate = @component.recurrence(occurrence)
140
+ if options_stop(candidate)
141
+ occurrence = nil
142
+ else
143
+ unless before_start?(candidate)
144
+ @yielded += 1
145
+ yield candidate
146
+ end
147
+ occurrence = next_occurrence
148
+ end
149
+ end
150
+ end
151
+ end
152
+
153
+ def bounded?
154
+ @rrules.bounded? || @count || @cutoff || @overlap_range
155
+ end
156
+
157
+ def process_overlap_range(overlap_range)
158
+ if overlap_range
159
+ @overlap_range = [overlap_range.first.to_overlap_range_start, overlap_range.last.to_overlap_range_end]
160
+ end
161
+ end
162
+
163
+ def process_options(options)
164
+ @start = options[:starting] && options[:starting].to_datetime
165
+ @cutoff = options[:before] && options[:before].to_datetime
166
+ @overlap_range = process_overlap_range(options[:overlapping])
167
+ @count = options[:count]
168
+ end
169
+
170
+ def to_a(options = {})
171
+ process_options(options)
172
+ raise ArgumentError.new("This component is unbounded, cannot produce an array of occurrences!") unless bounded?
173
+ super()
174
+ end
175
+
176
+ alias_method :entries, :to_a
177
+ end
178
+
179
+ # return an array of occurrences according to the options parameter. If a component is not bounded, and
180
+ # the number of occurrences to be returned is not constrained by either the :before, or :count options
181
+ # an ArgumentError will be raised.
182
+ #
183
+ # The components returned will be the same type as the receiver, but will have any recurrence properties
184
+ # (rrule, rdate, exrule, exdate) removed since they are single occurrences, and will have the recurrence-id
185
+ # property set to the occurrences dtstart value. (see RFC 2445 sec 4.8.4.4 pp 107-109)
186
+ #
187
+ # parameter options:
188
+ # * :starting:: a Date, Time, or DateTime, no occurrences starting before this argument will be returned
189
+ # * :before:: a Date, Time, or DateTime, no occurrences starting on or after this argument will be returned.
190
+ # * :count:: an integer which limits the number of occurrences returned.
191
+ # * :overlapping:: a two element array of Dates, Times, or DateTimes, assumed to be in chronological order. Only occurrences which are either totally or partially within the range will be returned.
192
+ def occurrences(options={})
193
+ enumeration_instance.to_a(options)
194
+ end
195
+
196
+ # TODO: Thread safe?
197
+ def enumeration_instance #:nodoc:
198
+ EnumerationInstance.new(self)
199
+ end
200
+
201
+ def before_range?(overlap_range)
202
+ finish = finish_time
203
+ !finish_time || finish_time < overlap_range.first
204
+ end
205
+
206
+ def after_range?(overlap_range)
207
+ start = start_time
208
+ !start || start > overlap_range.last
209
+ end
210
+
211
+ # execute the block for each occurrence
212
+ def each(&block) # :yields: Component
213
+ enumeration_instance.each(&block)
214
+ end
215
+
216
+ # A predicate which determines whether the component has a bounded set of occurrences
217
+ def bounded?
218
+ enumeration_instance.bounded?
219
+ end
220
+
221
+ # Return a array whose first element is a UTC DateTime representing the start of the first
222
+ # occurrence, and whose second element is a UTC DateTime representing the end of the last
223
+ # occurrence.
224
+ # If the receiver is not bounded then the second element will be nil.
225
+ #
226
+ # The purpose of this method is to provide values which may be used as database attributes so
227
+ # that a query can find all occurence enumerating components which may have occurrences within
228
+ # a range of times.
229
+ def zulu_occurrence_range
230
+ if bounded?
231
+ all = occurrences
232
+ first, last = all.first, all.last
233
+ else
234
+ first = occurrences(:count => 1).first
235
+ last = nil
236
+ end
237
+ [first.zulu_occurrence_range_start_time, last ? last.zulu_occurrence_range_finish_time : nil]
238
+ end
239
+
240
+ def set_occurrence_properties!(occurrence) # :nodoc:
241
+ occurrence_end = occurrence.dtend
242
+ occurrence_start = occurrence.dtstart
243
+ @rrule_property = nil
244
+ @exrule_property = nil
245
+ @rdate_property = nil
246
+ @exdate_property = nil
247
+ @recurrence_id_property = occurrence_start
248
+ if @dtend_property && !occurrence_end
249
+ occurrence_end = occurrence_start + (@dtend_property - @dtstart_property)
250
+ end
251
+ @dtstart_property = @dtstart_property.for_occurrence(occurrence_start)
252
+ @dtend_property = (@dtend_property || @dtstart_property).for_occurrence(occurrence_end) if occurrence_end
253
+ self
254
+ end
255
+
256
+ def recurrence(occurrence) # :nodoc:
257
+ result = self.dup.set_occurrence_properties!(occurrence)
258
+ end
259
+
260
+ def recurs?
261
+ @rrule_property && @rrule_property.length > 0 || @rdate_property && @rdate_property.length > 0
262
+ end
263
+
264
+ end
265
+ end