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.
- checksums.yaml +7 -0
- data/History.txt +402 -0
- data/Manifest.txt +161 -0
- data/README.txt +410 -0
- data/Rakefile +69 -0
- data/VERSION +1 -0
- data/bin/ri_cal +8 -0
- data/component_attributes/alarm.yml +10 -0
- data/component_attributes/calendar.yml +4 -0
- data/component_attributes/component_property_defs.yml +180 -0
- data/component_attributes/event.yml +45 -0
- data/component_attributes/freebusy.yml +16 -0
- data/component_attributes/journal.yml +35 -0
- data/component_attributes/timezone.yml +3 -0
- data/component_attributes/timezone_period.yml +11 -0
- data/component_attributes/todo.yml +46 -0
- data/copyrights.txt +1 -0
- data/docs/draft-ietf-calsify-2446bis-08.txt +7280 -0
- data/docs/draft-ietf-calsify-rfc2445bis-09.txt +10416 -0
- data/docs/incrementers.txt +7 -0
- data/docs/rfc2445.pdf +0 -0
- data/lib/ri_cal/component/alarm.rb +19 -0
- data/lib/ri_cal/component/calendar.rb +257 -0
- data/lib/ri_cal/component/event.rb +58 -0
- data/lib/ri_cal/component/freebusy.rb +16 -0
- data/lib/ri_cal/component/journal.rb +27 -0
- data/lib/ri_cal/component/non_standard.rb +33 -0
- data/lib/ri_cal/component/t_z_info_timezone.rb +153 -0
- data/lib/ri_cal/component/timezone/daylight_period.rb +25 -0
- data/lib/ri_cal/component/timezone/standard_period.rb +23 -0
- data/lib/ri_cal/component/timezone/timezone_period.rb +76 -0
- data/lib/ri_cal/component/timezone.rb +197 -0
- data/lib/ri_cal/component/todo.rb +42 -0
- data/lib/ri_cal/component.rb +256 -0
- data/lib/ri_cal/core_extensions/array/conversions.rb +15 -0
- data/lib/ri_cal/core_extensions/array.rb +7 -0
- data/lib/ri_cal/core_extensions/date/conversions.rb +56 -0
- data/lib/ri_cal/core_extensions/date.rb +13 -0
- data/lib/ri_cal/core_extensions/date_time/conversions.rb +50 -0
- data/lib/ri_cal/core_extensions/date_time.rb +15 -0
- data/lib/ri_cal/core_extensions/object/conversions.rb +20 -0
- data/lib/ri_cal/core_extensions/object.rb +8 -0
- data/lib/ri_cal/core_extensions/string/conversions.rb +57 -0
- data/lib/ri_cal/core_extensions/string.rb +8 -0
- data/lib/ri_cal/core_extensions/time/calculations.rb +153 -0
- data/lib/ri_cal/core_extensions/time/conversions.rb +42 -0
- data/lib/ri_cal/core_extensions/time/tzid_access.rb +50 -0
- data/lib/ri_cal/core_extensions/time/week_day_predicates.rb +55 -0
- data/lib/ri_cal/core_extensions/time.rb +14 -0
- data/lib/ri_cal/core_extensions.rb +11 -0
- data/lib/ri_cal/fast_date_time.rb +234 -0
- data/lib/ri_cal/floating_timezone.rb +32 -0
- data/lib/ri_cal/invalid_property_value.rb +8 -0
- data/lib/ri_cal/invalid_timezone_identifier.rb +20 -0
- data/lib/ri_cal/occurrence_enumerator.rb +265 -0
- data/lib/ri_cal/occurrence_period.rb +17 -0
- data/lib/ri_cal/parser.rb +148 -0
- data/lib/ri_cal/properties/alarm.rb +390 -0
- data/lib/ri_cal/properties/calendar.rb +164 -0
- data/lib/ri_cal/properties/event.rb +1523 -0
- data/lib/ri_cal/properties/freebusy.rb +593 -0
- data/lib/ri_cal/properties/journal.rb +1237 -0
- data/lib/ri_cal/properties/timezone.rb +150 -0
- data/lib/ri_cal/properties/timezone_period.rb +416 -0
- data/lib/ri_cal/properties/todo.rb +1559 -0
- data/lib/ri_cal/properties.rb +12 -0
- data/lib/ri_cal/property_value/array.rb +27 -0
- data/lib/ri_cal/property_value/cal_address.rb +11 -0
- data/lib/ri_cal/property_value/date.rb +184 -0
- data/lib/ri_cal/property_value/date_time/additive_methods.rb +44 -0
- data/lib/ri_cal/property_value/date_time/time_machine.rb +159 -0
- data/lib/ri_cal/property_value/date_time/timezone_support.rb +100 -0
- data/lib/ri_cal/property_value/date_time.rb +359 -0
- data/lib/ri_cal/property_value/duration.rb +110 -0
- data/lib/ri_cal/property_value/geo.rb +11 -0
- data/lib/ri_cal/property_value/integer.rb +12 -0
- data/lib/ri_cal/property_value/occurrence_list.rb +144 -0
- data/lib/ri_cal/property_value/period.rb +86 -0
- data/lib/ri_cal/property_value/recurrence_rule/enumeration_support_methods.rb +100 -0
- data/lib/ri_cal/property_value/recurrence_rule/enumerator.rb +79 -0
- data/lib/ri_cal/property_value/recurrence_rule/initialization_methods.rb +148 -0
- data/lib/ri_cal/property_value/recurrence_rule/negative_setpos_enumerator.rb +53 -0
- data/lib/ri_cal/property_value/recurrence_rule/numbered_span.rb +31 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_day_incrementer.rb +86 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_hour_incrementer.rb +31 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_minute_incrementer.rb +32 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_month_incrementer.rb +52 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_monthday_incrementer.rb +31 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_numbered_day_incrementer.rb +38 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_second_incrementer.rb +32 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_weekno_incrementer.rb +69 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/by_yearday_incrementer.rb +31 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/daily_incrementer.rb +28 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/frequency_incrementer.rb +80 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/hourly_incrementer.rb +23 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/list_incrementer.rb +106 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/minutely_incrementer.rb +23 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/monthly_incrementer.rb +33 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/null_sub_cycle_incrementer.rb +43 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/secondly_incrementer.rb +28 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/weekly_incrementer.rb +37 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer/yearly_incrementer.rb +57 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurrence_incrementer.rb +135 -0
- data/lib/ri_cal/property_value/recurrence_rule/recurring_day.rb +131 -0
- data/lib/ri_cal/property_value/recurrence_rule/recurring_month_day.rb +64 -0
- data/lib/ri_cal/property_value/recurrence_rule/recurring_numbered_week.rb +33 -0
- data/lib/ri_cal/property_value/recurrence_rule/recurring_year_day.rb +53 -0
- data/lib/ri_cal/property_value/recurrence_rule/time_manipulation.rb +42 -0
- data/lib/ri_cal/property_value/recurrence_rule/validations.rb +125 -0
- data/lib/ri_cal/property_value/recurrence_rule.rb +154 -0
- data/lib/ri_cal/property_value/text.rb +44 -0
- data/lib/ri_cal/property_value/uri.rb +11 -0
- data/lib/ri_cal/property_value/utc_offset.rb +33 -0
- data/lib/ri_cal/property_value/zulu_date_time.rb +34 -0
- data/lib/ri_cal/property_value.rb +159 -0
- data/lib/ri_cal/required_timezones.rb +55 -0
- data/lib/ri_cal.rb +187 -0
- data/parked_specs/ri_cal/claudio_a_bug_spec.rb +100 -0
- data/performance/empty_propval/subject.rb +43 -0
- data/performance/paris_eastern/subject.rb +90 -0
- data/performance/penultimate_weekday/subject.rb +15 -0
- data/performance/psm_big_enum/ical.ics +3171 -0
- data/performance/psm_big_enum/subject.rb +16 -0
- data/performance/utah_cycling/subject.rb +55 -0
- data/ri_cal.gemspec +244 -0
- data/script/benchmark_subject +23 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/script/profile_subject +29 -0
- data/script/txt2html +71 -0
- data/spec/ri_cal/bugreports_spec.rb +276 -0
- data/spec/ri_cal/component/alarm_spec.rb +12 -0
- data/spec/ri_cal/component/calendar_spec.rb +88 -0
- data/spec/ri_cal/component/event_spec.rb +735 -0
- data/spec/ri_cal/component/freebusy_spec.rb +12 -0
- data/spec/ri_cal/component/journal_spec.rb +37 -0
- data/spec/ri_cal/component/t_z_info_timezone_spec.rb +60 -0
- data/spec/ri_cal/component/timezone_spec.rb +236 -0
- data/spec/ri_cal/component/todo_spec.rb +112 -0
- data/spec/ri_cal/component_spec.rb +224 -0
- data/spec/ri_cal/core_extensions/string/conversions_spec.rb +78 -0
- data/spec/ri_cal/core_extensions/time/calculations_spec.rb +188 -0
- data/spec/ri_cal/core_extensions/time/week_day_predicates_spec.rb +45 -0
- data/spec/ri_cal/fast_date_time_spec.rb +77 -0
- data/spec/ri_cal/inf_loop_spec.rb +78 -0
- data/spec/ri_cal/occurrence_enumerator_spec.rb +611 -0
- data/spec/ri_cal/parser_spec.rb +337 -0
- data/spec/ri_cal/property_value/date_spec.rb +53 -0
- data/spec/ri_cal/property_value/date_time_spec.rb +383 -0
- data/spec/ri_cal/property_value/duration_spec.rb +126 -0
- data/spec/ri_cal/property_value/occurrence_list_spec.rb +72 -0
- data/spec/ri_cal/property_value/period_spec.rb +63 -0
- data/spec/ri_cal/property_value/recurrence_rule/recurring_year_day_spec.rb +21 -0
- data/spec/ri_cal/property_value/recurrence_rule_spec.rb +1814 -0
- data/spec/ri_cal/property_value/text_spec.rb +25 -0
- data/spec/ri_cal/property_value/utc_offset_spec.rb +48 -0
- data/spec/ri_cal/property_value_spec.rb +125 -0
- data/spec/ri_cal/required_timezones_spec.rb +67 -0
- data/spec/ri_cal_spec.rb +53 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +50 -0
- data/tasks/gem_loader/load_active_support.rb +3 -0
- data/tasks/gem_loader/load_tzinfo_gem.rb +2 -0
- data/tasks/ri_cal.rake +412 -0
- data/tasks/spec.rake +102 -0
- 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,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
|