rubyredrick-ri_cal 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +3 -0
- data/Manifest.txt +122 -0
- data/README.txt +271 -0
- data/Rakefile +31 -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 +2 -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 +22 -0
- data/lib/ri_cal/component/calendar.rb +199 -0
- data/lib/ri_cal/component/event.rb +30 -0
- data/lib/ri_cal/component/freebusy.rb +19 -0
- data/lib/ri_cal/component/journal.rb +22 -0
- data/lib/ri_cal/component/t_z_info_timezone.rb +124 -0
- data/lib/ri_cal/component/timezone/daylight_period.rb +26 -0
- data/lib/ri_cal/component/timezone/standard_period.rb +24 -0
- data/lib/ri_cal/component/timezone/timezone_period.rb +54 -0
- data/lib/ri_cal/component/timezone.rb +193 -0
- data/lib/ri_cal/component/todo.rb +26 -0
- data/lib/ri_cal/component.rb +224 -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 +27 -0
- data/lib/ri_cal/core_extensions/date.rb +13 -0
- data/lib/ri_cal/core_extensions/date_time/conversions.rb +27 -0
- data/lib/ri_cal/core_extensions/date_time.rb +13 -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 +32 -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 +27 -0
- data/lib/ri_cal/core_extensions/time/week_day_predicates.rb +88 -0
- data/lib/ri_cal/core_extensions/time.rb +11 -0
- data/lib/ri_cal/core_extensions.rb +6 -0
- data/lib/ri_cal/invalid_timezone_identifer.rb +20 -0
- data/lib/ri_cal/occurrence_enumerator.rb +172 -0
- data/lib/ri_cal/parser.rb +138 -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 +1526 -0
- data/lib/ri_cal/properties/freebusy.rb +594 -0
- data/lib/ri_cal/properties/journal.rb +1240 -0
- data/lib/ri_cal/properties/timezone.rb +151 -0
- data/lib/ri_cal/properties/timezone_period.rb +416 -0
- data/lib/ri_cal/properties/todo.rb +1562 -0
- data/lib/ri_cal/property_value/array.rb +19 -0
- data/lib/ri_cal/property_value/cal_address.rb +12 -0
- data/lib/ri_cal/property_value/date.rb +119 -0
- data/lib/ri_cal/property_value/date_time/additive_methods.rb +43 -0
- data/lib/ri_cal/property_value/date_time/time_machine.rb +180 -0
- data/lib/ri_cal/property_value/date_time/timezone_support.rb +65 -0
- data/lib/ri_cal/property_value/date_time.rb +324 -0
- data/lib/ri_cal/property_value/duration.rb +106 -0
- data/lib/ri_cal/property_value/geo.rb +12 -0
- data/lib/ri_cal/property_value/integer.rb +13 -0
- data/lib/ri_cal/property_value/occurrence_list.rb +82 -0
- data/lib/ri_cal/property_value/period.rb +63 -0
- data/lib/ri_cal/property_value/recurrence_rule/enumeration_support_methods.rb +98 -0
- data/lib/ri_cal/property_value/recurrence_rule/enumerator.rb +77 -0
- data/lib/ri_cal/property_value/recurrence_rule/initialization_methods.rb +149 -0
- data/lib/ri_cal/property_value/recurrence_rule/negative_setpos_enumerator.rb +54 -0
- data/lib/ri_cal/property_value/recurrence_rule/numbered_span.rb +32 -0
- data/lib/ri_cal/property_value/recurrence_rule/occurence_incrementer.rb +794 -0
- data/lib/ri_cal/property_value/recurrence_rule/recurring_day.rb +132 -0
- data/lib/ri_cal/property_value/recurrence_rule/recurring_month_day.rb +61 -0
- data/lib/ri_cal/property_value/recurrence_rule/recurring_numbered_week.rb +34 -0
- data/lib/ri_cal/property_value/recurrence_rule/recurring_year_day.rb +50 -0
- data/lib/ri_cal/property_value/recurrence_rule/validations.rb +126 -0
- data/lib/ri_cal/property_value/recurrence_rule.rb +146 -0
- data/lib/ri_cal/property_value/text.rb +41 -0
- data/lib/ri_cal/property_value/uri.rb +12 -0
- data/lib/ri_cal/property_value/utc_offset.rb +34 -0
- data/lib/ri_cal/property_value.rb +110 -0
- data/lib/ri_cal/required_timezones.rb +56 -0
- data/lib/ri_cal/time_with_floating_timezone.rb +59 -0
- data/lib/ri_cal.rb +134 -0
- data/ri_cal.gemspec +47 -0
- data/sample_ical_files/from_ical_dot_app/test1.ics +38 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/script/txt2html +71 -0
- data/spec/ri_cal/component/alarm_spec.rb +13 -0
- data/spec/ri_cal/component/calendar_spec.rb +55 -0
- data/spec/ri_cal/component/event_spec.rb +157 -0
- data/spec/ri_cal/component/freebusy_spec.rb +13 -0
- data/spec/ri_cal/component/journal_spec.rb +13 -0
- data/spec/ri_cal/component/t_z_info_timezone_spec.rb +37 -0
- data/spec/ri_cal/component/timezone_spec.rb +155 -0
- data/spec/ri_cal/component/todo_spec.rb +61 -0
- data/spec/ri_cal/component_spec.rb +212 -0
- data/spec/ri_cal/core_extensions/time/calculations_spec.rb +189 -0
- data/spec/ri_cal/core_extensions/time/week_day_predicates_spec.rb +46 -0
- data/spec/ri_cal/occurrence_enumerator_spec.rb +218 -0
- data/spec/ri_cal/parser_spec.rb +304 -0
- data/spec/ri_cal/property_value/date_spec.rb +22 -0
- data/spec/ri_cal/property_value/date_time_spec.rb +448 -0
- data/spec/ri_cal/property_value/duration_spec.rb +80 -0
- data/spec/ri_cal/property_value/period_spec.rb +50 -0
- data/spec/ri_cal/property_value/recurrence_rule/recurring_year_day_spec.rb +22 -0
- data/spec/ri_cal/property_value/recurrence_rule_spec.rb +1815 -0
- data/spec/ri_cal/property_value/text_spec.rb +14 -0
- data/spec/ri_cal/property_value/utc_offset_spec.rb +49 -0
- data/spec/ri_cal/property_value_spec.rb +111 -0
- data/spec/ri_cal/required_timezones_spec.rb +68 -0
- data/spec/ri_cal_spec.rb +54 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +24 -0
- data/tasks/ri_cal.rake +403 -0
- data/tasks/spec.rake +35 -0
- metadata +201 -0
@@ -0,0 +1,153 @@
|
|
1
|
+
module RiCal
|
2
|
+
module CoreExtensions #:nodoc:
|
3
|
+
module Time #:nodoc:
|
4
|
+
#- ©2009 Rick DeNatale
|
5
|
+
#- All rights reserved. Refer to the file README.txt for the license
|
6
|
+
#
|
7
|
+
# Provide calculation methods for use by the RiCal gem
|
8
|
+
# This module is included by Time, Date, and DateTime
|
9
|
+
module Calculations
|
10
|
+
# A predicate method used to determine if the receiver is within a leap year
|
11
|
+
def leap_year?
|
12
|
+
year % 4 == 0 && (year % 400 == 0 || year % 100 != 0)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Return the number of days in the month which includes the receiver
|
16
|
+
def days_in_month
|
17
|
+
raw = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][self.month]
|
18
|
+
self.month == 2 && leap_year? ? raw + 1 : raw
|
19
|
+
end
|
20
|
+
|
21
|
+
# Return the date on which the first iso week with a given starting week day occurs
|
22
|
+
# for a given iso year
|
23
|
+
#
|
24
|
+
#
|
25
|
+
# From RFC 2445 page 43:
|
26
|
+
# A week is defined as a seven day period, starting on the day of the week defined to be the
|
27
|
+
# week start (see WKST). Week number one of the calendar year is the first week which contains
|
28
|
+
# at least four (4) days in that calendar
|
29
|
+
# year.
|
30
|
+
#
|
31
|
+
# == parameters
|
32
|
+
# year:: the iso year
|
33
|
+
# wkst:: an integer representing the day of the week on which weeks are deemed to start. This uses
|
34
|
+
# the ruby convention where 0 represents Sunday.
|
35
|
+
def self.iso_week_one(year, wkst)
|
36
|
+
#
|
37
|
+
# Note that wkst uses the ruby definition, with Sunday = 0
|
38
|
+
#
|
39
|
+
# A good article about calculating ISO week number is at
|
40
|
+
# http://www.boyet.com/Articles/PublishedArticles/CalculatingtheISOweeknumb.html
|
41
|
+
#
|
42
|
+
# RFC 2445 generalizes the notion of ISO week by allowing the start of the week to vary.
|
43
|
+
# In order to adopt the algorithm in the referenced article, we must determine, for each
|
44
|
+
# wkst value, the day in January which must be contained in week 1 of the year.
|
45
|
+
#
|
46
|
+
# For a given wkst week 1 for a year is the first week which
|
47
|
+
# 1) Starts with a day with a wday of wkst
|
48
|
+
# 2) Contains a majority (4 or more) of days in that year
|
49
|
+
#
|
50
|
+
# If end of prior Dec, start of Jan Week 1 starts on For WKST =
|
51
|
+
|
52
|
+
# MO TU WE TH FR SA SU MO TU WE TH FR SA SU MO TU WE TH FR SA SU
|
53
|
+
# 01 02 03 04 05 06 07 08 09 10 11 12 13 14 01-07 02-08 03-09 04-10 05-11 06-12 07-13
|
54
|
+
# 31 01 02 03 04 05 06 07 08 09 10 11 12 13 31-06 01-07 02-08 03-09 04-10 05-11 06-12
|
55
|
+
# 30 31 01 02 03 04 05 06 07 08 09 10 11 12 30-05 31-06 01-07 02-08 03-09 04-10 05-11
|
56
|
+
# 29 30 31 01 02 03 04 05 06 07 08 09 10 11 29-04 30-05 31-06 01-07 02-08 03-09 04-10
|
57
|
+
# 28 29 30 31 01 02 03 04 05 06 07 08 09 10 04-10 29-04 30-05 31-06 01-07 02-08 03-09
|
58
|
+
# 27 28 29 30 31 01 02 03 04 05 06 07 08 09 03-09 04-10 29-04 30-05 31-06 01-07 02-08
|
59
|
+
# 26 27 28 29 30 31 01 02 03 04 05 06 07 08 02-08 03-09 04-10 29-04 30-05 31-06 01-07
|
60
|
+
# 25 26 27 28 29 30 31 01 02 03 04 05 06 07 01-07 02-08 03-09 04-10 29-04 30-05 31-06
|
61
|
+
# Week 1 must contain 4 4 4 4 ? ? ?
|
62
|
+
#
|
63
|
+
# So for a wkst of FR, SA, or SU, there is no date which MUST be contained in the 1st week
|
64
|
+
# We'll have to brute force that
|
65
|
+
if (1..4).include?(wkst)
|
66
|
+
# return the date of the wkst day which is less than or equal to jan4th
|
67
|
+
jan4th = ::Date.new(year, 1, 4)
|
68
|
+
result = jan4th - (convert_wday(jan4th.wday) - convert_wday(wkst))
|
69
|
+
else
|
70
|
+
# return the date of the wkst day which is greater than or equal to Dec 31 of the prior year
|
71
|
+
dec29th = ::Date.new(year-1, 12, 29)
|
72
|
+
result = dec29th + convert_wday(wkst) - convert_wday(dec29th.wday)
|
73
|
+
end
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
# Convert the receivers wday to RFC 2445 format. Whereas the Ruby time/date classes use
|
78
|
+
# 0 to represent Sunday, RFC 2445 uses 7.
|
79
|
+
def self.convert_wday(wday)
|
80
|
+
wday == 0 ? 7 : wday
|
81
|
+
end
|
82
|
+
|
83
|
+
# Return an array containing the iso year and iso week number for the receiver.
|
84
|
+
# Note that the iso year may be the year before or after the calendar year containing the receiver.
|
85
|
+
# == parameter
|
86
|
+
# wkst:: an integer representing the day of the week on which weeks are deemed to start. This uses
|
87
|
+
# the ruby convention where 0 represents Sunday.
|
88
|
+
def iso_year_and_week_one_start(wkst)
|
89
|
+
iso_year = self.year
|
90
|
+
date = ::Date.new(self.year, self.month, self.mday)
|
91
|
+
if (date >= ::Date.new(iso_year, 12, 29))
|
92
|
+
week_one_start = Calculations.iso_week_one(iso_year + 1, wkst)
|
93
|
+
if date < week_one_start
|
94
|
+
week_one_start = Calculations.iso_week_one(iso_year, wkst)
|
95
|
+
else
|
96
|
+
iso_year += 1
|
97
|
+
end
|
98
|
+
else
|
99
|
+
week_one_start = Calculations.iso_week_one(iso_year, wkst)
|
100
|
+
if (date < week_one_start)
|
101
|
+
iso_year -= 1
|
102
|
+
week_one_start = Calculations.iso_week_one(iso_year, wkst)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
[iso_year, week_one_start]
|
106
|
+
end
|
107
|
+
|
108
|
+
def iso_year_and_week_num(wkst) #:nodoc:
|
109
|
+
iso_year, week_one_start = *iso_year_and_week_one_start(wkst)
|
110
|
+
[iso_year, (::Date.new(self.year, self.month, self.mday) - week_one_start).to_i / 7 + 1]
|
111
|
+
end
|
112
|
+
|
113
|
+
# return the number of weeks in the the iso year containing the receiver
|
114
|
+
# == parameter
|
115
|
+
# wkst:: an integer representing the day of the week on which weeks are deemed to start. This uses
|
116
|
+
# the ruby convention where 0 represents Sunday.
|
117
|
+
def iso_weeks_in_year(wkst)
|
118
|
+
iso_year, week_one_start = *iso_year_and_week_one_start(wkst)
|
119
|
+
probe_date = week_one_start + (7*52)
|
120
|
+
if probe_date.iso_year(wkst) == iso_year
|
121
|
+
53
|
122
|
+
else
|
123
|
+
52
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# return the iso week number of the receiver
|
128
|
+
# == parameter
|
129
|
+
# wkst:: an integer representing the day of the week on which weeks are deemed to start. This uses
|
130
|
+
# the ruby convention where 0 represents Sunday.
|
131
|
+
def iso_week_num(wkst)
|
132
|
+
iso_year_and_week_num(wkst)[1]
|
133
|
+
end
|
134
|
+
|
135
|
+
# return the iso year of the receiver
|
136
|
+
# == parameter
|
137
|
+
# wkst:: an integer representing the day of the week on which weeks are deemed to start. This uses
|
138
|
+
# the ruby convention where 0 represents Sunday.
|
139
|
+
def iso_year(wkst)
|
140
|
+
iso_year_and_week_num(wkst)[0]
|
141
|
+
end
|
142
|
+
|
143
|
+
# return the first day of the iso year of the receiver
|
144
|
+
# == parameter
|
145
|
+
# wkst:: an integer representing the day of the week on which weeks are deemed to start. This uses
|
146
|
+
# the ruby convention where 0 represents Sunday.
|
147
|
+
def iso_year_start(wkst)
|
148
|
+
iso_year_and_week_one_start(wkst)[1]
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module RiCal
|
2
|
+
module CoreExtensions #:nodoc:
|
3
|
+
module Time #:nodoc:
|
4
|
+
#- ©2009 Rick DeNatale
|
5
|
+
#- All rights reserved. Refer to the file README.txt for the license
|
6
|
+
#
|
7
|
+
module Conversions
|
8
|
+
# Return an RiCal::PropertyValue::DateTime representing the receiver
|
9
|
+
def to_ri_cal_date_time_value
|
10
|
+
RiCal::PropertyValue::DateTime.from_time(self)
|
11
|
+
end
|
12
|
+
|
13
|
+
alias_method :to_ri_cal_date_or_date_time_value, :to_ri_cal_date_time_value
|
14
|
+
|
15
|
+
# Return the natural ri_cal_property for this object
|
16
|
+
def to_ri_cal_property_value
|
17
|
+
to_ri_cal_date_time_value
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return a proxy to this object which will be interpreted as a floating time.
|
21
|
+
def with_floating_timezone
|
22
|
+
RiCal::TimeWithFloatingTimezone.new(self)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module RiCal
|
2
|
+
module CoreExtensions #:nodoc:
|
3
|
+
module Time #:nodoc:
|
4
|
+
#- ©2009 Rick DeNatale
|
5
|
+
#- All rights reserved. Refer to the file README.txt for the license
|
6
|
+
#
|
7
|
+
# Provide predicate and related methods for use by the RiCal gem
|
8
|
+
# This module is included by Time, Date, and DateTime
|
9
|
+
module WeekDayPredicates
|
10
|
+
|
11
|
+
# Determine the equivalent time on the day which falls on a particular weekday of the same year as the receiver
|
12
|
+
#
|
13
|
+
# == Parameters
|
14
|
+
# n:: the ordinal number being requested
|
15
|
+
# which_wday:: the weekday using Ruby time conventions, i.e. 0 => Sunday, 1 => Monday, ...
|
16
|
+
|
17
|
+
# e.g. to obtain the 2nd Monday of the receivers year use
|
18
|
+
#
|
19
|
+
# time.nth_wday_in_year(2, 1)
|
20
|
+
def nth_wday_in_year(n, which_wday, for_time = self)
|
21
|
+
if n > 0
|
22
|
+
first_of_year = for_time.to_ri_cal_property_value.change(:month => 1, :day => 1)
|
23
|
+
first_in_year = first_of_year.advance(:days => (which_wday - first_of_year.wday + 7) % 7)
|
24
|
+
first_in_year.advance(:days => (7*(n - 1)))
|
25
|
+
else
|
26
|
+
december25 = for_time.to_ri_cal_property_value.change(:month => 12, :day => 25)
|
27
|
+
last_in_year = december25.advance(:days => (which_wday - december25.wday + 7) % 7)
|
28
|
+
last_in_year.advance(:days => (7 * (n + 1)))
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# A predicate to determine whether or not the receiver falls on a particular weekday of its year.
|
33
|
+
#
|
34
|
+
# See #nth_wday_in_year
|
35
|
+
#
|
36
|
+
# == Parameters
|
37
|
+
# n:: the ordinal number being requested
|
38
|
+
# which_wday:: the weekday using Ruby time conventions, i.e. 0 => Sunday, 1 => Monday, ...
|
39
|
+
def nth_wday_in_year?(n, which_wday)
|
40
|
+
target = nth_wday_in_year(n, which_wday)
|
41
|
+
[self.year, self.mon, self.day] == [target.year, target.mon, target.day]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Determine the day which falls on a particular weekday of the same month as the receiver
|
45
|
+
#
|
46
|
+
# == Parameters
|
47
|
+
# n:: the ordinal number being requested
|
48
|
+
# which_wday:: the weekday using Ruby time conventions, i.e. 0 => Sunday, 1 => Monday, ...
|
49
|
+
|
50
|
+
# e.g. to obtain the 3nd Tuesday of the receivers month use
|
51
|
+
#
|
52
|
+
# time.nth_wday_in_month(2, 2)
|
53
|
+
def nth_wday_in_month(n, which_wday, for_time = self)
|
54
|
+
first_of_month = for_time.to_ri_cal_property_value.change(:day => 1)
|
55
|
+
first_in_month = first_of_month.advance(:days => (which_wday - first_of_month.wday))
|
56
|
+
first_in_month = first_in_month.advance(:days => 7) if first_in_month.month != first_of_month.month
|
57
|
+
if n > 0
|
58
|
+
first_in_month.advance(:days => (7*(n - 1)))
|
59
|
+
else
|
60
|
+
possible = first_in_month.advance(:days => 21)
|
61
|
+
possible = possible.advance(:days => 7) while possible.month == first_in_month.month
|
62
|
+
last_in_month = possible.advance(:days => - 7)
|
63
|
+
(last_in_month.advance(:days => - (7*(n.abs - 1))))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# A predicate to determine whether or not the receiver falls on a particular weekday of its month.
|
68
|
+
#
|
69
|
+
# == Parameters
|
70
|
+
# n:: the ordinal number being requested
|
71
|
+
# which_wday:: the weekday using Ruby time conventions, i.e. 0 => Sunday, 1 => Monday, ...
|
72
|
+
def nth_wday_in_month?(n, which_wday)
|
73
|
+
target = nth_wday_in_month(n, which_wday)
|
74
|
+
[self.year, self.month, self.day] == [target.year, target.month, target.day]
|
75
|
+
end
|
76
|
+
|
77
|
+
# Return a DateTime which is the beginning of the first day on or before the receiver
|
78
|
+
# with the specified wday
|
79
|
+
def start_of_week_with_wkst(wkst)
|
80
|
+
wkst ||= 1
|
81
|
+
date = ::Date.civil(self.year, self.month, self.day)
|
82
|
+
date -= 1 while date.wday != wkst
|
83
|
+
date
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
require "#{File.dirname(__FILE__)}/time/conversions.rb"
|
2
|
+
require "#{File.dirname(__FILE__)}/time/week_day_predicates.rb"
|
3
|
+
require "#{File.dirname(__FILE__)}/time/calculations.rb"
|
4
|
+
#- ©2009 Rick DeNatale
|
5
|
+
#- All rights reserved. Refer to the file README.txt for the license
|
6
|
+
#
|
7
|
+
class Time #:nodoc:
|
8
|
+
include RiCal::CoreExtensions::Time::WeekDayPredicates
|
9
|
+
include RiCal::CoreExtensions::Time::Calculations
|
10
|
+
include RiCal::CoreExtensions::DateTime::Conversions
|
11
|
+
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)
|
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)
|
17
|
+
new("#{identifier.inspect} is not known to the tzinfo database")
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,172 @@
|
|
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)
|
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
|
+
end
|
24
|
+
|
25
|
+
# OccurrenceMerger takes multiple recurrence rules and enumerates the combination in sequence.
|
26
|
+
class OccurrenceMerger # :nodoc:
|
27
|
+
def self.for(component, rules)
|
28
|
+
if rules.nil? || rules.empty?
|
29
|
+
EmptyRulesEnumerator
|
30
|
+
elsif rules.length == 1
|
31
|
+
rules.first.enumerator(component)
|
32
|
+
else
|
33
|
+
new(component, rules)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_accessor :enumerators, :nexts
|
38
|
+
|
39
|
+
def initialize(component, rules)
|
40
|
+
self.enumerators = rules.map {|rrule| rrule.enumerator(component)}
|
41
|
+
@bounded = enumerators.all? {|enumerator| enumerator.bounded?}
|
42
|
+
self.nexts = @enumerators.map {|enumerator| enumerator.next_occurrence}
|
43
|
+
end
|
44
|
+
|
45
|
+
# return the earliest of each of the enumerators next occurrences
|
46
|
+
def next_occurrence
|
47
|
+
result = nexts.compact.sort.first
|
48
|
+
if result
|
49
|
+
nexts.each_with_index { |datetimevalue, i| @nexts[i] = @enumerators[i].next_occurrence if result == datetimevalue }
|
50
|
+
end
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
def bounded?
|
55
|
+
@bounded
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# EnumerationInstance holds the values needed during the enumeration of occurrences for a component.
|
60
|
+
class EnumerationInstance # :nodoc:
|
61
|
+
include Enumerable
|
62
|
+
|
63
|
+
def initialize(component, options = {})
|
64
|
+
@component = component
|
65
|
+
@start = options[:starting]
|
66
|
+
@cutoff = options[:before]
|
67
|
+
@count = options[:count]
|
68
|
+
@rrules = OccurrenceMerger.for(@component, [@component.rrule_property, @component.rdate_property].flatten.compact)
|
69
|
+
@exrules = OccurrenceMerger.for(@component, [@component.exrule_property, @component.exdate_property].flatten.compact)
|
70
|
+
end
|
71
|
+
|
72
|
+
# return the next exclusion which starts at the same time or after the start time of the occurrence
|
73
|
+
# return nil if this exhausts the exclusion rules
|
74
|
+
def exclusion_for(occurrence)
|
75
|
+
while (@next_exclusion && @next_exclusion[:start] < occurrence[:start])
|
76
|
+
@next_exclusion = @exrules.next_occurrence
|
77
|
+
end
|
78
|
+
@next_exclusion
|
79
|
+
end
|
80
|
+
|
81
|
+
# TODO: Need to research this, I beleive that this should also take the end time into account,
|
82
|
+
# but I need to research
|
83
|
+
def exclusion_match?(occurrence, exclusion)
|
84
|
+
exclusion && occurrence[:start] == occurrence[:start]
|
85
|
+
end
|
86
|
+
|
87
|
+
def exclude?(occurrence)
|
88
|
+
exclusion_match?(occurrence, exclusion_for(occurrence))
|
89
|
+
end
|
90
|
+
|
91
|
+
# yield each occurrence to a block
|
92
|
+
# some components may be open-ended, e.g. have no COUNT or DTEND
|
93
|
+
def each
|
94
|
+
occurrence = @rrules.next_occurrence
|
95
|
+
yielded = 0
|
96
|
+
@next_exclusion = @exrules.next_occurrence
|
97
|
+
while (occurrence)
|
98
|
+
if (@cutoff && occurrence[:start] >= @cutoff) || (@count && yielded >= @count)
|
99
|
+
occurrence = nil
|
100
|
+
else
|
101
|
+
unless exclude?(occurrence)
|
102
|
+
yielded += 1
|
103
|
+
yield @component.recurrence(occurrence)
|
104
|
+
end
|
105
|
+
occurrence = @rrules.next_occurrence
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def bounded?
|
111
|
+
@rrules.bounded? || @count || @cutoff
|
112
|
+
end
|
113
|
+
|
114
|
+
def to_a
|
115
|
+
raise ArgumentError.new("This component is unbounded, cannot produce an array of occurrences!") unless bounded?
|
116
|
+
super
|
117
|
+
end
|
118
|
+
|
119
|
+
alias_method :entries, :to_a
|
120
|
+
end
|
121
|
+
|
122
|
+
# return an array of occurrences according to the options parameter. If a component is not bounded, and
|
123
|
+
# the number of occurrences to be returned is not constrained by either the :before, or :count options
|
124
|
+
# an ArgumentError will be raised.
|
125
|
+
#
|
126
|
+
# The components returned will be the same type as the receiver, but will have any recurrence properties
|
127
|
+
# (rrule, rdate, exrule, exdate) removed since they are single occurrences, and will have the recurrence-id
|
128
|
+
# property set to the occurrences dtstart value. (see RFC 2445 sec 4.8.4.4 pp 107-109)
|
129
|
+
#
|
130
|
+
# parameter options:
|
131
|
+
# * :starting:: a Date, Time, or DateTime, no occurrences starting before this argument will be returned
|
132
|
+
# * :before:: a Date, Time, or DateTime, no occurrences starting on or after this argument will be returned.
|
133
|
+
# * :count:: an integer which limits the number of occurrences returned.
|
134
|
+
def occurrences(options={})
|
135
|
+
EnumerationInstance.new(self, options).to_a
|
136
|
+
end
|
137
|
+
|
138
|
+
# execute the block for each occurrence
|
139
|
+
def each(&block) # :yields: Component
|
140
|
+
EnumerationInstance.new(self).each(&block)
|
141
|
+
end
|
142
|
+
|
143
|
+
# A predicate which determines whether the component has a bounded set of occurrences
|
144
|
+
def bounded?
|
145
|
+
EnumerationInstance.new(self).bounded?
|
146
|
+
end
|
147
|
+
#
|
148
|
+
def set_occurrence_properties!(occurrence) # :nodoc:
|
149
|
+
occurrence_end = occurrence[:end]
|
150
|
+
occurrence_start = occurrence[:start]
|
151
|
+
@rrule_property = nil
|
152
|
+
@exrule_property = nil
|
153
|
+
@rdate_property = nil
|
154
|
+
@exdate_property = nil
|
155
|
+
@recurrence_id_property = occurrence_start
|
156
|
+
@dtstart_property = occurrence_start
|
157
|
+
if occurrence_end
|
158
|
+
@dtend_property = occurrence_end
|
159
|
+
else
|
160
|
+
if dtend
|
161
|
+
my_duration = @dtend_property - @dtstart_property
|
162
|
+
@dtend_property = occurrence_start + my_duration
|
163
|
+
end
|
164
|
+
end
|
165
|
+
self
|
166
|
+
end
|
167
|
+
|
168
|
+
def recurrence(occurrence) # :nodoc:
|
169
|
+
result = self.dup.set_occurrence_properties!(occurrence)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
module RiCal
|
2
|
+
#- ©2009 Rick DeNatale
|
3
|
+
#- All rights reserved. Refer to the file README.txt for the license
|
4
|
+
#
|
5
|
+
class Parser # :nodoc:
|
6
|
+
def next_line #:nodoc:
|
7
|
+
result = nil
|
8
|
+
begin
|
9
|
+
result = buffer_or_line
|
10
|
+
@buffer = nil
|
11
|
+
while /^\s/ =~ buffer_or_line
|
12
|
+
result = "#{result}#{@buffer[1..-1]}"
|
13
|
+
@buffer = nil
|
14
|
+
end
|
15
|
+
rescue EOFError
|
16
|
+
return nil
|
17
|
+
ensure
|
18
|
+
return result
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_params(string) #:nodoc:
|
23
|
+
if string
|
24
|
+
string.split(";").inject({}) { |result, val|
|
25
|
+
m = /^(.+)=(.+)$/.match(val)
|
26
|
+
invalid unless m
|
27
|
+
result[m[1]] = m[2]
|
28
|
+
result
|
29
|
+
}
|
30
|
+
else
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def params_and_value(string) #:nodoc:
|
36
|
+
string = string.sub(/^:/,'')
|
37
|
+
return ["", string] unless string.match(/^;/)
|
38
|
+
segments = string.sub(';','').split(":")
|
39
|
+
return ["", string] if segments.length < 2
|
40
|
+
quote_count = 0
|
41
|
+
gathering_params = true
|
42
|
+
params = []
|
43
|
+
values = []
|
44
|
+
segments.each do |segment|
|
45
|
+
if gathering_params
|
46
|
+
params << segment
|
47
|
+
quote_count += segment.count("\"")
|
48
|
+
gathering_params = (1 == quote_count % 2)
|
49
|
+
else
|
50
|
+
values << segment
|
51
|
+
end
|
52
|
+
end
|
53
|
+
[params.join(":"), values.join(":")]
|
54
|
+
end
|
55
|
+
|
56
|
+
def separate_line(string) #:nodoc:
|
57
|
+
match = string.match(/^([^;:]*)(.*)$/)
|
58
|
+
name = match[1]
|
59
|
+
params, value = *params_and_value(match[2])
|
60
|
+
{
|
61
|
+
:name => name,
|
62
|
+
:params => parse_params(params),
|
63
|
+
:value => value
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
def next_separated_line #:nodoc:
|
68
|
+
line = next_line
|
69
|
+
line ? separate_line(line) : nil
|
70
|
+
end
|
71
|
+
|
72
|
+
def buffer_or_line #:nodoc:
|
73
|
+
@buffer ||= @io.readline.chomp
|
74
|
+
end
|
75
|
+
|
76
|
+
def initialize(io = StringIO.new("")) #:nodoc:
|
77
|
+
@io = io
|
78
|
+
end
|
79
|
+
|
80
|
+
def self.parse(io = StringIO.new("")) #:nodoc:
|
81
|
+
new(io).parse
|
82
|
+
end
|
83
|
+
|
84
|
+
def invalid #:nodoc:
|
85
|
+
raise Exception.new("Invalid icalendar file")
|
86
|
+
end
|
87
|
+
|
88
|
+
def still_in(component, separated_line) #:nodoc:
|
89
|
+
invalid unless separated_line
|
90
|
+
separated_line[:value] != component || separated_line[:name] != "END"
|
91
|
+
end
|
92
|
+
|
93
|
+
def parse #:nodoc:
|
94
|
+
result = []
|
95
|
+
while start_line = next_line
|
96
|
+
@parent_stack = []
|
97
|
+
result << parse_one(start_line, nil)
|
98
|
+
end
|
99
|
+
result
|
100
|
+
end
|
101
|
+
|
102
|
+
# TODO: Need to parse non-standard component types (iana-token or x-name)
|
103
|
+
def parse_one(start, parent_component) #:nodoc:
|
104
|
+
|
105
|
+
@parent_stack << parent_component
|
106
|
+
if Hash === start
|
107
|
+
first_line = start
|
108
|
+
else
|
109
|
+
first_line = separate_line(start)
|
110
|
+
end
|
111
|
+
invalid unless first_line[:name] == "BEGIN"
|
112
|
+
result = case first_line[:value]
|
113
|
+
when "VCALENDAR"
|
114
|
+
RiCal::Component::Calendar.from_parser(self, parent_component)
|
115
|
+
when "VEVENT"
|
116
|
+
RiCal::Component::Event.from_parser(self, parent_component)
|
117
|
+
when "VTODO"
|
118
|
+
RiCal::Component::Todo.from_parser(self, parent_component)
|
119
|
+
when "VJOURNAL"
|
120
|
+
RiCal::Component::Journal.from_parser(self, parent_component)
|
121
|
+
when "VFREEBUSY"
|
122
|
+
RiCal::Component::Freebusy.from_parser(self, parent_component)
|
123
|
+
when "VTIMEZONE"
|
124
|
+
RiCal::Component::Timezone.from_parser(self, parent_component)
|
125
|
+
when "VALARM"
|
126
|
+
RiCal::Component::Alarm.from_parser(self, parent_component)
|
127
|
+
when "DAYLIGHT"
|
128
|
+
RiCal::Component::Timezone::DaylightPeriod.from_parser(self, parent_component)
|
129
|
+
when "STANDARD"
|
130
|
+
RiCal::Component::Timezone::StandardPeriod.from_parser(self, parent_component)
|
131
|
+
else
|
132
|
+
invalid
|
133
|
+
end
|
134
|
+
@parent_stack.pop
|
135
|
+
result
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|