ri_cal 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (130) hide show
  1. data/History.txt +45 -0
  2. data/Manifest.txt +129 -0
  3. data/README.txt +394 -0
  4. data/Rakefile +31 -0
  5. data/bin/ri_cal +8 -0
  6. data/component_attributes/alarm.yml +10 -0
  7. data/component_attributes/calendar.yml +4 -0
  8. data/component_attributes/component_property_defs.yml +180 -0
  9. data/component_attributes/event.yml +45 -0
  10. data/component_attributes/freebusy.yml +16 -0
  11. data/component_attributes/journal.yml +35 -0
  12. data/component_attributes/timezone.yml +3 -0
  13. data/component_attributes/timezone_period.yml +11 -0
  14. data/component_attributes/todo.yml +46 -0
  15. data/copyrights.txt +1 -0
  16. data/docs/draft-ietf-calsify-2446bis-08.txt +7280 -0
  17. data/docs/draft-ietf-calsify-rfc2445bis-09.txt +10416 -0
  18. data/docs/incrementers.txt +7 -0
  19. data/docs/rfc2445.pdf +0 -0
  20. data/lib/ri_cal.rb +144 -0
  21. data/lib/ri_cal/component.rb +247 -0
  22. data/lib/ri_cal/component/alarm.rb +21 -0
  23. data/lib/ri_cal/component/calendar.rb +219 -0
  24. data/lib/ri_cal/component/event.rb +60 -0
  25. data/lib/ri_cal/component/freebusy.rb +18 -0
  26. data/lib/ri_cal/component/journal.rb +30 -0
  27. data/lib/ri_cal/component/t_z_info_timezone.rb +123 -0
  28. data/lib/ri_cal/component/timezone.rb +196 -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 +53 -0
  32. data/lib/ri_cal/component/todo.rb +43 -0
  33. data/lib/ri_cal/core_extensions.rb +6 -0
  34. data/lib/ri_cal/core_extensions/array.rb +7 -0
  35. data/lib/ri_cal/core_extensions/array/conversions.rb +15 -0
  36. data/lib/ri_cal/core_extensions/date.rb +13 -0
  37. data/lib/ri_cal/core_extensions/date/conversions.rb +61 -0
  38. data/lib/ri_cal/core_extensions/date_time.rb +15 -0
  39. data/lib/ri_cal/core_extensions/date_time/conversions.rb +50 -0
  40. data/lib/ri_cal/core_extensions/object.rb +8 -0
  41. data/lib/ri_cal/core_extensions/object/conversions.rb +20 -0
  42. data/lib/ri_cal/core_extensions/string.rb +8 -0
  43. data/lib/ri_cal/core_extensions/string/conversions.rb +63 -0
  44. data/lib/ri_cal/core_extensions/time.rb +13 -0
  45. data/lib/ri_cal/core_extensions/time/calculations.rb +153 -0
  46. data/lib/ri_cal/core_extensions/time/conversions.rb +61 -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 +88 -0
  49. data/lib/ri_cal/floating_timezone.rb +32 -0
  50. data/lib/ri_cal/invalid_property_value.rb +8 -0
  51. data/lib/ri_cal/invalid_timezone_identifer.rb +20 -0
  52. data/lib/ri_cal/occurrence_enumerator.rb +206 -0
  53. data/lib/ri_cal/occurrence_period.rb +17 -0
  54. data/lib/ri_cal/parser.rb +138 -0
  55. data/lib/ri_cal/properties/alarm.rb +390 -0
  56. data/lib/ri_cal/properties/calendar.rb +164 -0
  57. data/lib/ri_cal/properties/event.rb +1526 -0
  58. data/lib/ri_cal/properties/freebusy.rb +594 -0
  59. data/lib/ri_cal/properties/journal.rb +1240 -0
  60. data/lib/ri_cal/properties/timezone.rb +151 -0
  61. data/lib/ri_cal/properties/timezone_period.rb +416 -0
  62. data/lib/ri_cal/properties/todo.rb +1562 -0
  63. data/lib/ri_cal/property_value.rb +149 -0
  64. data/lib/ri_cal/property_value/array.rb +27 -0
  65. data/lib/ri_cal/property_value/cal_address.rb +11 -0
  66. data/lib/ri_cal/property_value/date.rb +175 -0
  67. data/lib/ri_cal/property_value/date_time.rb +335 -0
  68. data/lib/ri_cal/property_value/date_time/additive_methods.rb +44 -0
  69. data/lib/ri_cal/property_value/date_time/time_machine.rb +181 -0
  70. data/lib/ri_cal/property_value/date_time/timezone_support.rb +96 -0
  71. data/lib/ri_cal/property_value/duration.rb +110 -0
  72. data/lib/ri_cal/property_value/geo.rb +11 -0
  73. data/lib/ri_cal/property_value/integer.rb +12 -0
  74. data/lib/ri_cal/property_value/occurrence_list.rb +144 -0
  75. data/lib/ri_cal/property_value/period.rb +82 -0
  76. data/lib/ri_cal/property_value/recurrence_rule.rb +145 -0
  77. data/lib/ri_cal/property_value/recurrence_rule/enumeration_support_methods.rb +97 -0
  78. data/lib/ri_cal/property_value/recurrence_rule/enumerator.rb +79 -0
  79. data/lib/ri_cal/property_value/recurrence_rule/initialization_methods.rb +148 -0
  80. data/lib/ri_cal/property_value/recurrence_rule/negative_setpos_enumerator.rb +53 -0
  81. data/lib/ri_cal/property_value/recurrence_rule/numbered_span.rb +31 -0
  82. data/lib/ri_cal/property_value/recurrence_rule/occurence_incrementer.rb +793 -0
  83. data/lib/ri_cal/property_value/recurrence_rule/recurring_day.rb +131 -0
  84. data/lib/ri_cal/property_value/recurrence_rule/recurring_month_day.rb +60 -0
  85. data/lib/ri_cal/property_value/recurrence_rule/recurring_numbered_week.rb +33 -0
  86. data/lib/ri_cal/property_value/recurrence_rule/recurring_year_day.rb +49 -0
  87. data/lib/ri_cal/property_value/recurrence_rule/validations.rb +125 -0
  88. data/lib/ri_cal/property_value/text.rb +40 -0
  89. data/lib/ri_cal/property_value/uri.rb +11 -0
  90. data/lib/ri_cal/property_value/utc_offset.rb +33 -0
  91. data/lib/ri_cal/required_timezones.rb +55 -0
  92. data/ri_cal.gemspec +49 -0
  93. data/sample_ical_files/from_ical_dot_app/test1.ics +38 -0
  94. data/script/console +10 -0
  95. data/script/destroy +14 -0
  96. data/script/generate +14 -0
  97. data/script/txt2html +71 -0
  98. data/spec/ri_cal/component/alarm_spec.rb +12 -0
  99. data/spec/ri_cal/component/calendar_spec.rb +54 -0
  100. data/spec/ri_cal/component/event_spec.rb +601 -0
  101. data/spec/ri_cal/component/freebusy_spec.rb +12 -0
  102. data/spec/ri_cal/component/journal_spec.rb +37 -0
  103. data/spec/ri_cal/component/t_z_info_timezone_spec.rb +36 -0
  104. data/spec/ri_cal/component/timezone_spec.rb +218 -0
  105. data/spec/ri_cal/component/todo_spec.rb +112 -0
  106. data/spec/ri_cal/component_spec.rb +224 -0
  107. data/spec/ri_cal/core_extensions/string/conversions_spec.rb +78 -0
  108. data/spec/ri_cal/core_extensions/time/calculations_spec.rb +188 -0
  109. data/spec/ri_cal/core_extensions/time/week_day_predicates_spec.rb +45 -0
  110. data/spec/ri_cal/occurrence_enumerator_spec.rb +573 -0
  111. data/spec/ri_cal/parser_spec.rb +303 -0
  112. data/spec/ri_cal/property_value/date_spec.rb +53 -0
  113. data/spec/ri_cal/property_value/date_time_spec.rb +383 -0
  114. data/spec/ri_cal/property_value/duration_spec.rb +126 -0
  115. data/spec/ri_cal/property_value/occurrence_list_spec.rb +72 -0
  116. data/spec/ri_cal/property_value/period_spec.rb +49 -0
  117. data/spec/ri_cal/property_value/recurrence_rule/recurring_year_day_spec.rb +21 -0
  118. data/spec/ri_cal/property_value/recurrence_rule_spec.rb +1814 -0
  119. data/spec/ri_cal/property_value/text_spec.rb +25 -0
  120. data/spec/ri_cal/property_value/utc_offset_spec.rb +48 -0
  121. data/spec/ri_cal/property_value_spec.rb +125 -0
  122. data/spec/ri_cal/required_timezones_spec.rb +67 -0
  123. data/spec/ri_cal_spec.rb +53 -0
  124. data/spec/spec.opts +4 -0
  125. data/spec/spec_helper.rb +46 -0
  126. data/tasks/gem_loader/load_active_support.rb +3 -0
  127. data/tasks/gem_loader/load_tzinfo_gem.rb +2 -0
  128. data/tasks/ri_cal.rake +410 -0
  129. data/tasks/spec.rake +50 -0
  130. 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,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,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