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.
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