ri_cal 0.5.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.
- data/History.txt +45 -0
- data/Manifest.txt +129 -0
- data/README.txt +394 -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 +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.rb +144 -0
- data/lib/ri_cal/component.rb +247 -0
- data/lib/ri_cal/component/alarm.rb +21 -0
- data/lib/ri_cal/component/calendar.rb +219 -0
- data/lib/ri_cal/component/event.rb +60 -0
- data/lib/ri_cal/component/freebusy.rb +18 -0
- data/lib/ri_cal/component/journal.rb +30 -0
- data/lib/ri_cal/component/t_z_info_timezone.rb +123 -0
- data/lib/ri_cal/component/timezone.rb +196 -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 +53 -0
- data/lib/ri_cal/component/todo.rb +43 -0
- data/lib/ri_cal/core_extensions.rb +6 -0
- data/lib/ri_cal/core_extensions/array.rb +7 -0
- data/lib/ri_cal/core_extensions/array/conversions.rb +15 -0
- data/lib/ri_cal/core_extensions/date.rb +13 -0
- data/lib/ri_cal/core_extensions/date/conversions.rb +61 -0
- data/lib/ri_cal/core_extensions/date_time.rb +15 -0
- data/lib/ri_cal/core_extensions/date_time/conversions.rb +50 -0
- data/lib/ri_cal/core_extensions/object.rb +8 -0
- data/lib/ri_cal/core_extensions/object/conversions.rb +20 -0
- data/lib/ri_cal/core_extensions/string.rb +8 -0
- data/lib/ri_cal/core_extensions/string/conversions.rb +63 -0
- data/lib/ri_cal/core_extensions/time.rb +13 -0
- data/lib/ri_cal/core_extensions/time/calculations.rb +153 -0
- data/lib/ri_cal/core_extensions/time/conversions.rb +61 -0
- data/lib/ri_cal/core_extensions/time/tzid_access.rb +50 -0
- data/lib/ri_cal/core_extensions/time/week_day_predicates.rb +88 -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_identifer.rb +20 -0
- data/lib/ri_cal/occurrence_enumerator.rb +206 -0
- data/lib/ri_cal/occurrence_period.rb +17 -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.rb +149 -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 +175 -0
- data/lib/ri_cal/property_value/date_time.rb +335 -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 +181 -0
- data/lib/ri_cal/property_value/date_time/timezone_support.rb +96 -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 +82 -0
- data/lib/ri_cal/property_value/recurrence_rule.rb +145 -0
- data/lib/ri_cal/property_value/recurrence_rule/enumeration_support_methods.rb +97 -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/occurence_incrementer.rb +793 -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 +60 -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 +49 -0
- data/lib/ri_cal/property_value/recurrence_rule/validations.rb +125 -0
- data/lib/ri_cal/property_value/text.rb +40 -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/required_timezones.rb +55 -0
- data/ri_cal.gemspec +49 -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 +12 -0
- data/spec/ri_cal/component/calendar_spec.rb +54 -0
- data/spec/ri_cal/component/event_spec.rb +601 -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 +36 -0
- data/spec/ri_cal/component/timezone_spec.rb +218 -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/occurrence_enumerator_spec.rb +573 -0
- data/spec/ri_cal/parser_spec.rb +303 -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 +49 -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 +46 -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 +410 -0
- data/tasks/spec.rake +50 -0
- metadata +221 -0
|
@@ -0,0 +1,61 @@
|
|
|
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(timezone_finder = nil) #:nodoc:
|
|
10
|
+
RiCal::PropertyValue::DateTime.new(
|
|
11
|
+
timezone_finder,
|
|
12
|
+
:value => strftime("%Y%m%dT%H%M%S"),
|
|
13
|
+
:params => {"TZID" => self.tzid || :default})
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
alias_method :to_ri_cal_date_or_date_time_value, :to_ri_cal_date_time_value #:nodoc:
|
|
17
|
+
alias_method :to_ri_cal_occurrence_list_value, :to_ri_cal_date_time_value #:nodoc:
|
|
18
|
+
|
|
19
|
+
# Return the natural ri_cal_property for this object
|
|
20
|
+
def to_ri_cal_property_value(timezone_finder = nil) #:nodoc:
|
|
21
|
+
to_ri_cal_date_time_value(timezone_finder)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Return a copy of this object which will be interpreted as a floating time.
|
|
25
|
+
def with_floating_timezone
|
|
26
|
+
dup.set_tzid(:floating)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
unless defined? ActiveSupport
|
|
30
|
+
# Converts a Time object to a Date, dropping hour, minute, and second precision.
|
|
31
|
+
#
|
|
32
|
+
# my_time = Time.now # => Mon Nov 12 22:59:51 -0500 2007
|
|
33
|
+
# my_time.to_date # => Mon, 12 Nov 2007
|
|
34
|
+
#
|
|
35
|
+
# your_time = Time.parse("1/13/2009 1:13:03 P.M.") # => Tue Jan 13 13:13:03 -0500 2009
|
|
36
|
+
# your_time.to_date # => Tue, 13 Jan 2009
|
|
37
|
+
def to_date
|
|
38
|
+
::Date.new(year, month, day)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A method to keep Time, Date and DateTime instances interchangeable on conversions.
|
|
42
|
+
# In this case, it simply returns +self+.
|
|
43
|
+
def to_time
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Converts a Time instance to a Ruby DateTime instance, preserving UTC offset.
|
|
48
|
+
#
|
|
49
|
+
# my_time = Time.now # => Mon Nov 12 23:04:21 -0500 2007
|
|
50
|
+
# my_time.to_datetime # => Mon, 12 Nov 2007 23:04:21 -0500
|
|
51
|
+
#
|
|
52
|
+
# your_time = Time.parse("1/13/2009 1:13:03 P.M.") # => Tue Jan 13 13:13:03 -0500 2009
|
|
53
|
+
# your_time.to_datetime # => Tue, 13 Jan 2009 13:13:03 -0500
|
|
54
|
+
def to_datetime
|
|
55
|
+
::DateTime.civil(year, month, day, hour, min, sec, Rational(utc_offset, 86400))
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
# Provides a tzid attribute for ::Time and ::DateTime
|
|
8
|
+
module TzidAccess
|
|
9
|
+
# The tzid attribute is used by RiCal, it should be a valid timezone identifier within a calendar,
|
|
10
|
+
# :floating to indicate a floating time, or nil to use the default timezone in effect
|
|
11
|
+
#
|
|
12
|
+
# See PropertyValue::DateTime#default_tzid= and Component::Calendar#tzid=
|
|
13
|
+
attr_accessor :tzid
|
|
14
|
+
|
|
15
|
+
# Convenience method, sets the tzid and returns the receiver
|
|
16
|
+
def set_tzid(time_zone_identifier)
|
|
17
|
+
self.tzid = time_zone_identifier
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Predicate indicating whether or not the instance represents a floating time
|
|
22
|
+
def has_floating_timezone?
|
|
23
|
+
tzid == :floating
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module TimeWithZoneExtension #:nodoc:
|
|
31
|
+
def tzid
|
|
32
|
+
utc? ? "UTC" : time_zone.tzinfo.identifier
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Predicate indicating whether or not the instance represents a floating time
|
|
36
|
+
def has_floating_timezone?
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_ri_cal_date_time_value(timezone_finder=nil)
|
|
41
|
+
::RiCal::PropertyValue::DateTime.new(timezone_finder, :params => {"TZID" => tzid}, :value => strftime("%Y%m%dT%H%M%S"))
|
|
42
|
+
end
|
|
43
|
+
alias_method :to_ri_cal_date_or_date_time_value, :to_ri_cal_date_time_value
|
|
44
|
+
alias_method :to_ri_cal_occurrence_list_value, :to_ri_cal_date_time_value
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
if RiCal::TimeWithZone
|
|
49
|
+
RiCal::TimeWithZone.class_eval {include RiCal::TimeWithZoneExtension}
|
|
50
|
+
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,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
|
+
Rational(0, 24)
|
|
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,206 @@
|
|
|
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, options = {})
|
|
77
|
+
@component = component
|
|
78
|
+
@start = options[:starting]
|
|
79
|
+
@cutoff = options[:before]
|
|
80
|
+
@count = options[:count]
|
|
81
|
+
@rrules = OccurrenceMerger.for(@component, [@component.rrule_property, @component.rdate_property].flatten.compact)
|
|
82
|
+
@exrules = OccurrenceMerger.for(@component, [@component.exrule_property, @component.exdate_property].flatten.compact)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# return the next exclusion which starts at the same time or after the start time of the occurrence
|
|
86
|
+
# return nil if this exhausts the exclusion rules
|
|
87
|
+
def exclusion_for(occurrence)
|
|
88
|
+
while (@next_exclusion && @next_exclusion.dtstart < occurrence.dtstart)
|
|
89
|
+
@next_exclusion = @exrules.next_occurrence
|
|
90
|
+
end
|
|
91
|
+
@next_exclusion
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# TODO: Need to research this, I beleive that this should also take the end time into account,
|
|
95
|
+
# but I need to research
|
|
96
|
+
def exclusion_match?(occurrence, exclusion)
|
|
97
|
+
exclusion && (occurrence.dtstart == exclusion.dtstart)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Also exclude occurrences before the :starting date_time
|
|
101
|
+
def exclude?(occurrence)
|
|
102
|
+
exclusion_match?(occurrence, exclusion_for(occurrence)) ||
|
|
103
|
+
(@start && occurrence.dtstart.to_datetime < @start)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# yield each occurrence to a block
|
|
107
|
+
# some components may be open-ended, e.g. have no COUNT or DTEND
|
|
108
|
+
def each
|
|
109
|
+
if @rrules.empty?
|
|
110
|
+
yield @component
|
|
111
|
+
else
|
|
112
|
+
occurrence = @rrules.next_occurrence
|
|
113
|
+
yielded = 0
|
|
114
|
+
@next_exclusion = @exrules.next_occurrence
|
|
115
|
+
while (occurrence)
|
|
116
|
+
if (@cutoff && occurrence.dtstart.to_datetime >= @cutoff) || (@count && yielded >= @count)
|
|
117
|
+
occurrence = nil
|
|
118
|
+
else
|
|
119
|
+
unless exclude?(occurrence)
|
|
120
|
+
yielded += 1
|
|
121
|
+
yield @component.recurrence(occurrence)
|
|
122
|
+
end
|
|
123
|
+
occurrence = @rrules.next_occurrence
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def bounded?
|
|
130
|
+
@rrules.bounded? || @count || @cutoff
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def to_a
|
|
134
|
+
raise ArgumentError.new("This component is unbounded, cannot produce an array of occurrences!") unless bounded?
|
|
135
|
+
super
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
alias_method :entries, :to_a
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# return an array of occurrences according to the options parameter. If a component is not bounded, and
|
|
142
|
+
# the number of occurrences to be returned is not constrained by either the :before, or :count options
|
|
143
|
+
# an ArgumentError will be raised.
|
|
144
|
+
#
|
|
145
|
+
# The components returned will be the same type as the receiver, but will have any recurrence properties
|
|
146
|
+
# (rrule, rdate, exrule, exdate) removed since they are single occurrences, and will have the recurrence-id
|
|
147
|
+
# property set to the occurrences dtstart value. (see RFC 2445 sec 4.8.4.4 pp 107-109)
|
|
148
|
+
#
|
|
149
|
+
# parameter options:
|
|
150
|
+
# * :starting:: a Date, Time, or DateTime, no occurrences starting before this argument will be returned
|
|
151
|
+
# * :before:: a Date, Time, or DateTime, no occurrences starting on or after this argument will be returned.
|
|
152
|
+
# * :count:: an integer which limits the number of occurrences returned.
|
|
153
|
+
def occurrences(options={})
|
|
154
|
+
EnumerationInstance.new(self, options).to_a
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# execute the block for each occurrence
|
|
158
|
+
def each(&block) # :yields: Component
|
|
159
|
+
EnumerationInstance.new(self).each(&block)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# A predicate which determines whether the component has a bounded set of occurrences
|
|
163
|
+
def bounded?
|
|
164
|
+
EnumerationInstance.new(self).bounded?
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Return a array whose first element is a UTC DateTime representing the start of the first
|
|
168
|
+
# occurrence, and whose second element is a UTC DateTime representing the end of the last
|
|
169
|
+
# occurrence.
|
|
170
|
+
# If the receiver is not bounded then the second element will be nil.
|
|
171
|
+
#
|
|
172
|
+
# The purpose of this method is to provide values which may be used as database attributes so
|
|
173
|
+
# that a query can find all occurence enumerating components which may have occurrences within
|
|
174
|
+
# a range of times.
|
|
175
|
+
def zulu_occurrence_range
|
|
176
|
+
if bounded?
|
|
177
|
+
all = occurrences
|
|
178
|
+
first, last = all.first, all.last
|
|
179
|
+
else
|
|
180
|
+
first = occurrences(:count => 1).first
|
|
181
|
+
last = nil
|
|
182
|
+
end
|
|
183
|
+
[first.zulu_occurrence_range_start_time, last ? last.zulu_occurrence_range_finish_time : nil]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def set_occurrence_properties!(occurrence) # :nodoc:
|
|
187
|
+
occurrence_end = occurrence.dtend
|
|
188
|
+
occurrence_start = occurrence.dtstart
|
|
189
|
+
@rrule_property = nil
|
|
190
|
+
@exrule_property = nil
|
|
191
|
+
@rdate_property = nil
|
|
192
|
+
@exdate_property = nil
|
|
193
|
+
@recurrence_id_property = occurrence_start
|
|
194
|
+
if @dtend_property && !occurrence_end
|
|
195
|
+
occurrence_end = occurrence_start + (@dtend_property - @dtstart_property)
|
|
196
|
+
end
|
|
197
|
+
@dtstart_property = dtstart_property.for_occurrence(occurrence_start)
|
|
198
|
+
@dtend_property = dtend_property.for_occurrence(occurrence_end) if @dtend_property
|
|
199
|
+
self
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def recurrence(occurrence) # :nodoc:
|
|
203
|
+
result = self.dup.set_occurrence_properties!(occurrence)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|