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.
Files changed (123) hide show
  1. data/History.txt +3 -0
  2. data/Manifest.txt +122 -0
  3. data/README.txt +271 -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 +2 -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/component/alarm.rb +22 -0
  21. data/lib/ri_cal/component/calendar.rb +199 -0
  22. data/lib/ri_cal/component/event.rb +30 -0
  23. data/lib/ri_cal/component/freebusy.rb +19 -0
  24. data/lib/ri_cal/component/journal.rb +22 -0
  25. data/lib/ri_cal/component/t_z_info_timezone.rb +124 -0
  26. data/lib/ri_cal/component/timezone/daylight_period.rb +26 -0
  27. data/lib/ri_cal/component/timezone/standard_period.rb +24 -0
  28. data/lib/ri_cal/component/timezone/timezone_period.rb +54 -0
  29. data/lib/ri_cal/component/timezone.rb +193 -0
  30. data/lib/ri_cal/component/todo.rb +26 -0
  31. data/lib/ri_cal/component.rb +224 -0
  32. data/lib/ri_cal/core_extensions/array/conversions.rb +15 -0
  33. data/lib/ri_cal/core_extensions/array.rb +7 -0
  34. data/lib/ri_cal/core_extensions/date/conversions.rb +27 -0
  35. data/lib/ri_cal/core_extensions/date.rb +13 -0
  36. data/lib/ri_cal/core_extensions/date_time/conversions.rb +27 -0
  37. data/lib/ri_cal/core_extensions/date_time.rb +13 -0
  38. data/lib/ri_cal/core_extensions/object/conversions.rb +20 -0
  39. data/lib/ri_cal/core_extensions/object.rb +8 -0
  40. data/lib/ri_cal/core_extensions/string/conversions.rb +32 -0
  41. data/lib/ri_cal/core_extensions/string.rb +8 -0
  42. data/lib/ri_cal/core_extensions/time/calculations.rb +153 -0
  43. data/lib/ri_cal/core_extensions/time/conversions.rb +27 -0
  44. data/lib/ri_cal/core_extensions/time/week_day_predicates.rb +88 -0
  45. data/lib/ri_cal/core_extensions/time.rb +11 -0
  46. data/lib/ri_cal/core_extensions.rb +6 -0
  47. data/lib/ri_cal/invalid_timezone_identifer.rb +20 -0
  48. data/lib/ri_cal/occurrence_enumerator.rb +172 -0
  49. data/lib/ri_cal/parser.rb +138 -0
  50. data/lib/ri_cal/properties/alarm.rb +390 -0
  51. data/lib/ri_cal/properties/calendar.rb +164 -0
  52. data/lib/ri_cal/properties/event.rb +1526 -0
  53. data/lib/ri_cal/properties/freebusy.rb +594 -0
  54. data/lib/ri_cal/properties/journal.rb +1240 -0
  55. data/lib/ri_cal/properties/timezone.rb +151 -0
  56. data/lib/ri_cal/properties/timezone_period.rb +416 -0
  57. data/lib/ri_cal/properties/todo.rb +1562 -0
  58. data/lib/ri_cal/property_value/array.rb +19 -0
  59. data/lib/ri_cal/property_value/cal_address.rb +12 -0
  60. data/lib/ri_cal/property_value/date.rb +119 -0
  61. data/lib/ri_cal/property_value/date_time/additive_methods.rb +43 -0
  62. data/lib/ri_cal/property_value/date_time/time_machine.rb +180 -0
  63. data/lib/ri_cal/property_value/date_time/timezone_support.rb +65 -0
  64. data/lib/ri_cal/property_value/date_time.rb +324 -0
  65. data/lib/ri_cal/property_value/duration.rb +106 -0
  66. data/lib/ri_cal/property_value/geo.rb +12 -0
  67. data/lib/ri_cal/property_value/integer.rb +13 -0
  68. data/lib/ri_cal/property_value/occurrence_list.rb +82 -0
  69. data/lib/ri_cal/property_value/period.rb +63 -0
  70. data/lib/ri_cal/property_value/recurrence_rule/enumeration_support_methods.rb +98 -0
  71. data/lib/ri_cal/property_value/recurrence_rule/enumerator.rb +77 -0
  72. data/lib/ri_cal/property_value/recurrence_rule/initialization_methods.rb +149 -0
  73. data/lib/ri_cal/property_value/recurrence_rule/negative_setpos_enumerator.rb +54 -0
  74. data/lib/ri_cal/property_value/recurrence_rule/numbered_span.rb +32 -0
  75. data/lib/ri_cal/property_value/recurrence_rule/occurence_incrementer.rb +794 -0
  76. data/lib/ri_cal/property_value/recurrence_rule/recurring_day.rb +132 -0
  77. data/lib/ri_cal/property_value/recurrence_rule/recurring_month_day.rb +61 -0
  78. data/lib/ri_cal/property_value/recurrence_rule/recurring_numbered_week.rb +34 -0
  79. data/lib/ri_cal/property_value/recurrence_rule/recurring_year_day.rb +50 -0
  80. data/lib/ri_cal/property_value/recurrence_rule/validations.rb +126 -0
  81. data/lib/ri_cal/property_value/recurrence_rule.rb +146 -0
  82. data/lib/ri_cal/property_value/text.rb +41 -0
  83. data/lib/ri_cal/property_value/uri.rb +12 -0
  84. data/lib/ri_cal/property_value/utc_offset.rb +34 -0
  85. data/lib/ri_cal/property_value.rb +110 -0
  86. data/lib/ri_cal/required_timezones.rb +56 -0
  87. data/lib/ri_cal/time_with_floating_timezone.rb +59 -0
  88. data/lib/ri_cal.rb +134 -0
  89. data/ri_cal.gemspec +47 -0
  90. data/sample_ical_files/from_ical_dot_app/test1.ics +38 -0
  91. data/script/console +10 -0
  92. data/script/destroy +14 -0
  93. data/script/generate +14 -0
  94. data/script/txt2html +71 -0
  95. data/spec/ri_cal/component/alarm_spec.rb +13 -0
  96. data/spec/ri_cal/component/calendar_spec.rb +55 -0
  97. data/spec/ri_cal/component/event_spec.rb +157 -0
  98. data/spec/ri_cal/component/freebusy_spec.rb +13 -0
  99. data/spec/ri_cal/component/journal_spec.rb +13 -0
  100. data/spec/ri_cal/component/t_z_info_timezone_spec.rb +37 -0
  101. data/spec/ri_cal/component/timezone_spec.rb +155 -0
  102. data/spec/ri_cal/component/todo_spec.rb +61 -0
  103. data/spec/ri_cal/component_spec.rb +212 -0
  104. data/spec/ri_cal/core_extensions/time/calculations_spec.rb +189 -0
  105. data/spec/ri_cal/core_extensions/time/week_day_predicates_spec.rb +46 -0
  106. data/spec/ri_cal/occurrence_enumerator_spec.rb +218 -0
  107. data/spec/ri_cal/parser_spec.rb +304 -0
  108. data/spec/ri_cal/property_value/date_spec.rb +22 -0
  109. data/spec/ri_cal/property_value/date_time_spec.rb +448 -0
  110. data/spec/ri_cal/property_value/duration_spec.rb +80 -0
  111. data/spec/ri_cal/property_value/period_spec.rb +50 -0
  112. data/spec/ri_cal/property_value/recurrence_rule/recurring_year_day_spec.rb +22 -0
  113. data/spec/ri_cal/property_value/recurrence_rule_spec.rb +1815 -0
  114. data/spec/ri_cal/property_value/text_spec.rb +14 -0
  115. data/spec/ri_cal/property_value/utc_offset_spec.rb +49 -0
  116. data/spec/ri_cal/property_value_spec.rb +111 -0
  117. data/spec/ri_cal/required_timezones_spec.rb +68 -0
  118. data/spec/ri_cal_spec.rb +54 -0
  119. data/spec/spec.opts +4 -0
  120. data/spec/spec_helper.rb +24 -0
  121. data/tasks/ri_cal.rake +403 -0
  122. data/tasks/spec.rake +35 -0
  123. 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,6 @@
1
+ #- ©2009 Rick DeNatale
2
+ #- All rights reserved. Refer to the file README.txt for the license
3
+ #
4
+ Dir[File.dirname(__FILE__) + "/core_extensions/*.rb"].sort.each do |path|
5
+ require path
6
+ 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