demingfactor-ri_cal 0.9.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.
Files changed (167) hide show
  1. checksums.yaml +7 -0
  2. data/History.txt +402 -0
  3. data/Manifest.txt +161 -0
  4. data/README.txt +410 -0
  5. data/Rakefile +69 -0
  6. data/VERSION +1 -0
  7. data/bin/ri_cal +8 -0
  8. data/component_attributes/alarm.yml +10 -0
  9. data/component_attributes/calendar.yml +4 -0
  10. data/component_attributes/component_property_defs.yml +180 -0
  11. data/component_attributes/event.yml +45 -0
  12. data/component_attributes/freebusy.yml +16 -0
  13. data/component_attributes/journal.yml +35 -0
  14. data/component_attributes/timezone.yml +3 -0
  15. data/component_attributes/timezone_period.yml +11 -0
  16. data/component_attributes/todo.yml +46 -0
  17. data/copyrights.txt +1 -0
  18. data/docs/draft-ietf-calsify-2446bis-08.txt +7280 -0
  19. data/docs/draft-ietf-calsify-rfc2445bis-09.txt +10416 -0
  20. data/docs/incrementers.txt +7 -0
  21. data/docs/rfc2445.pdf +0 -0
  22. data/lib/ri_cal/component/alarm.rb +19 -0
  23. data/lib/ri_cal/component/calendar.rb +257 -0
  24. data/lib/ri_cal/component/event.rb +58 -0
  25. data/lib/ri_cal/component/freebusy.rb +16 -0
  26. data/lib/ri_cal/component/journal.rb +27 -0
  27. data/lib/ri_cal/component/non_standard.rb +33 -0
  28. data/lib/ri_cal/component/t_z_info_timezone.rb +153 -0
  29. data/lib/ri_cal/component/timezone/daylight_period.rb +25 -0
  30. data/lib/ri_cal/component/timezone/standard_period.rb +23 -0
  31. data/lib/ri_cal/component/timezone/timezone_period.rb +76 -0
  32. data/lib/ri_cal/component/timezone.rb +197 -0
  33. data/lib/ri_cal/component/todo.rb +42 -0
  34. data/lib/ri_cal/component.rb +256 -0
  35. data/lib/ri_cal/core_extensions/array/conversions.rb +15 -0
  36. data/lib/ri_cal/core_extensions/array.rb +7 -0
  37. data/lib/ri_cal/core_extensions/date/conversions.rb +56 -0
  38. data/lib/ri_cal/core_extensions/date.rb +13 -0
  39. data/lib/ri_cal/core_extensions/date_time/conversions.rb +50 -0
  40. data/lib/ri_cal/core_extensions/date_time.rb +15 -0
  41. data/lib/ri_cal/core_extensions/object/conversions.rb +20 -0
  42. data/lib/ri_cal/core_extensions/object.rb +8 -0
  43. data/lib/ri_cal/core_extensions/string/conversions.rb +57 -0
  44. data/lib/ri_cal/core_extensions/string.rb +8 -0
  45. data/lib/ri_cal/core_extensions/time/calculations.rb +153 -0
  46. data/lib/ri_cal/core_extensions/time/conversions.rb +42 -0
  47. data/lib/ri_cal/core_extensions/time/tzid_access.rb +50 -0
  48. data/lib/ri_cal/core_extensions/time/week_day_predicates.rb +55 -0
  49. data/lib/ri_cal/core_extensions/time.rb +14 -0
  50. data/lib/ri_cal/core_extensions.rb +11 -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 +148 -0
  58. data/lib/ri_cal/properties/alarm.rb +390 -0
  59. data/lib/ri_cal/properties/calendar.rb +164 -0
  60. data/lib/ri_cal/properties/event.rb +1523 -0
  61. data/lib/ri_cal/properties/freebusy.rb +593 -0
  62. data/lib/ri_cal/properties/journal.rb +1237 -0
  63. data/lib/ri_cal/properties/timezone.rb +150 -0
  64. data/lib/ri_cal/properties/timezone_period.rb +416 -0
  65. data/lib/ri_cal/properties/todo.rb +1559 -0
  66. data/lib/ri_cal/properties.rb +12 -0
  67. data/lib/ri_cal/property_value/array.rb +27 -0
  68. data/lib/ri_cal/property_value/cal_address.rb +11 -0
  69. data/lib/ri_cal/property_value/date.rb +184 -0
  70. data/lib/ri_cal/property_value/date_time/additive_methods.rb +44 -0
  71. data/lib/ri_cal/property_value/date_time/time_machine.rb +159 -0
  72. data/lib/ri_cal/property_value/date_time/timezone_support.rb +100 -0
  73. data/lib/ri_cal/property_value/date_time.rb +359 -0
  74. data/lib/ri_cal/property_value/duration.rb +110 -0
  75. data/lib/ri_cal/property_value/geo.rb +11 -0
  76. data/lib/ri_cal/property_value/integer.rb +12 -0
  77. data/lib/ri_cal/property_value/occurrence_list.rb +144 -0
  78. data/lib/ri_cal/property_value/period.rb +86 -0
  79. data/lib/ri_cal/property_value/recurrence_rule/enumeration_support_methods.rb +100 -0
  80. data/lib/ri_cal/property_value/recurrence_rule/enumerator.rb +79 -0
  81. data/lib/ri_cal/property_value/recurrence_rule/initialization_methods.rb +148 -0
  82. data/lib/ri_cal/property_value/recurrence_rule/negative_setpos_enumerator.rb +53 -0
  83. data/lib/ri_cal/property_value/recurrence_rule/numbered_span.rb +31 -0
  84. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_day_incrementer.rb +86 -0
  85. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_hour_incrementer.rb +31 -0
  86. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_minute_incrementer.rb +32 -0
  87. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_month_incrementer.rb +52 -0
  88. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_monthday_incrementer.rb +31 -0
  89. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_numbered_day_incrementer.rb +38 -0
  90. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_second_incrementer.rb +32 -0
  91. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_weekno_incrementer.rb +69 -0
  92. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_yearday_incrementer.rb +31 -0
  93. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/daily_incrementer.rb +28 -0
  94. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/frequency_incrementer.rb +80 -0
  95. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/hourly_incrementer.rb +23 -0
  96. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/list_incrementer.rb +106 -0
  97. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/minutely_incrementer.rb +23 -0
  98. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/monthly_incrementer.rb +33 -0
  99. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/null_sub_cycle_incrementer.rb +43 -0
  100. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/secondly_incrementer.rb +28 -0
  101. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/weekly_incrementer.rb +37 -0
  102. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/yearly_incrementer.rb +57 -0
  103. data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer.rb +135 -0
  104. data/lib/ri_cal/property_value/recurrence_rule/recurring_day.rb +131 -0
  105. data/lib/ri_cal/property_value/recurrence_rule/recurring_month_day.rb +64 -0
  106. data/lib/ri_cal/property_value/recurrence_rule/recurring_numbered_week.rb +33 -0
  107. data/lib/ri_cal/property_value/recurrence_rule/recurring_year_day.rb +53 -0
  108. data/lib/ri_cal/property_value/recurrence_rule/time_manipulation.rb +42 -0
  109. data/lib/ri_cal/property_value/recurrence_rule/validations.rb +125 -0
  110. data/lib/ri_cal/property_value/recurrence_rule.rb +154 -0
  111. data/lib/ri_cal/property_value/text.rb +44 -0
  112. data/lib/ri_cal/property_value/uri.rb +11 -0
  113. data/lib/ri_cal/property_value/utc_offset.rb +33 -0
  114. data/lib/ri_cal/property_value/zulu_date_time.rb +34 -0
  115. data/lib/ri_cal/property_value.rb +159 -0
  116. data/lib/ri_cal/required_timezones.rb +55 -0
  117. data/lib/ri_cal.rb +187 -0
  118. data/parked_specs/ri_cal/claudio_a_bug_spec.rb +100 -0
  119. data/performance/empty_propval/subject.rb +43 -0
  120. data/performance/paris_eastern/subject.rb +90 -0
  121. data/performance/penultimate_weekday/subject.rb +15 -0
  122. data/performance/psm_big_enum/ical.ics +3171 -0
  123. data/performance/psm_big_enum/subject.rb +16 -0
  124. data/performance/utah_cycling/subject.rb +55 -0
  125. data/ri_cal.gemspec +244 -0
  126. data/script/benchmark_subject +23 -0
  127. data/script/console +10 -0
  128. data/script/destroy +14 -0
  129. data/script/generate +14 -0
  130. data/script/profile_subject +29 -0
  131. data/script/txt2html +71 -0
  132. data/spec/ri_cal/bugreports_spec.rb +276 -0
  133. data/spec/ri_cal/component/alarm_spec.rb +12 -0
  134. data/spec/ri_cal/component/calendar_spec.rb +88 -0
  135. data/spec/ri_cal/component/event_spec.rb +735 -0
  136. data/spec/ri_cal/component/freebusy_spec.rb +12 -0
  137. data/spec/ri_cal/component/journal_spec.rb +37 -0
  138. data/spec/ri_cal/component/t_z_info_timezone_spec.rb +60 -0
  139. data/spec/ri_cal/component/timezone_spec.rb +236 -0
  140. data/spec/ri_cal/component/todo_spec.rb +112 -0
  141. data/spec/ri_cal/component_spec.rb +224 -0
  142. data/spec/ri_cal/core_extensions/string/conversions_spec.rb +78 -0
  143. data/spec/ri_cal/core_extensions/time/calculations_spec.rb +188 -0
  144. data/spec/ri_cal/core_extensions/time/week_day_predicates_spec.rb +45 -0
  145. data/spec/ri_cal/fast_date_time_spec.rb +77 -0
  146. data/spec/ri_cal/inf_loop_spec.rb +78 -0
  147. data/spec/ri_cal/occurrence_enumerator_spec.rb +611 -0
  148. data/spec/ri_cal/parser_spec.rb +337 -0
  149. data/spec/ri_cal/property_value/date_spec.rb +53 -0
  150. data/spec/ri_cal/property_value/date_time_spec.rb +383 -0
  151. data/spec/ri_cal/property_value/duration_spec.rb +126 -0
  152. data/spec/ri_cal/property_value/occurrence_list_spec.rb +72 -0
  153. data/spec/ri_cal/property_value/period_spec.rb +63 -0
  154. data/spec/ri_cal/property_value/recurrence_rule/recurring_year_day_spec.rb +21 -0
  155. data/spec/ri_cal/property_value/recurrence_rule_spec.rb +1814 -0
  156. data/spec/ri_cal/property_value/text_spec.rb +25 -0
  157. data/spec/ri_cal/property_value/utc_offset_spec.rb +48 -0
  158. data/spec/ri_cal/property_value_spec.rb +125 -0
  159. data/spec/ri_cal/required_timezones_spec.rb +67 -0
  160. data/spec/ri_cal_spec.rb +53 -0
  161. data/spec/spec.opts +4 -0
  162. data/spec/spec_helper.rb +50 -0
  163. data/tasks/gem_loader/load_active_support.rb +3 -0
  164. data/tasks/gem_loader/load_tzinfo_gem.rb +2 -0
  165. data/tasks/ri_cal.rake +412 -0
  166. data/tasks/spec.rake +102 -0
  167. metadata +246 -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
@@ -0,0 +1,17 @@
1
+ module RiCal
2
+ class OccurrencePeriod
3
+ attr_reader :dtstart, :dtend
4
+ def initialize(dtstart, dtend)
5
+ @dtstart = dtstart
6
+ @dtend = dtend
7
+ end
8
+
9
+ def to_s
10
+ "op:#{dtstart}-#{dtend}"
11
+ end
12
+
13
+ def <=>(other)
14
+ [dtstart, dtend] <=> [other.dtstart, other.dtend]
15
+ end
16
+ end
17
+ end