tilia-vobject 4.0.0.pre.alpha2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rubocop.yml +32 -0
- data/.simplecov +4 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.sabre.md +626 -0
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +17 -0
- data/Gemfile.lock +68 -0
- data/LICENSE +27 -0
- data/LICENSE.sabre +27 -0
- data/README.md +63 -0
- data/Rakefile +17 -0
- data/bin/vobject +7 -0
- data/lib/tilia/v_object/birthday_calendar_generator.rb +142 -0
- data/lib/tilia/v_object/cli.rb +582 -0
- data/lib/tilia/v_object/component/available.rb +107 -0
- data/lib/tilia/v_object/component/v_alarm.rb +114 -0
- data/lib/tilia/v_object/component/v_availability.rb +128 -0
- data/lib/tilia/v_object/component/v_calendar.rb +468 -0
- data/lib/tilia/v_object/component/v_card.rb +457 -0
- data/lib/tilia/v_object/component/v_event.rb +127 -0
- data/lib/tilia/v_object/component/v_free_busy.rb +81 -0
- data/lib/tilia/v_object/component/v_journal.rb +75 -0
- data/lib/tilia/v_object/component/v_time_zone.rb +51 -0
- data/lib/tilia/v_object/component/v_todo.rb +147 -0
- data/lib/tilia/v_object/component.rb +591 -0
- data/lib/tilia/v_object/date_time_parser.rb +486 -0
- data/lib/tilia/v_object/document.rb +218 -0
- data/lib/tilia/v_object/element_list.rb +18 -0
- data/lib/tilia/v_object/eof_exception.rb +8 -0
- data/lib/tilia/v_object/free_busy_data.rb +149 -0
- data/lib/tilia/v_object/free_busy_generator.rb +465 -0
- data/lib/tilia/v_object/i_tip/broker.rb +909 -0
- data/lib/tilia/v_object/i_tip/i_tip_exception.rb +9 -0
- data/lib/tilia/v_object/i_tip/message.rb +109 -0
- data/lib/tilia/v_object/i_tip/same_organizer_for_all_components_exception.rb +13 -0
- data/lib/tilia/v_object/i_tip.rb +10 -0
- data/lib/tilia/v_object/node.rb +192 -0
- data/lib/tilia/v_object/parameter.rb +327 -0
- data/lib/tilia/v_object/parse_exception.rb +7 -0
- data/lib/tilia/v_object/parser/json.rb +149 -0
- data/lib/tilia/v_object/parser/mime_dir.rb +543 -0
- data/lib/tilia/v_object/parser/parser.rb +61 -0
- data/lib/tilia/v_object/parser/xml/element/key_value.rb +60 -0
- data/lib/tilia/v_object/parser/xml/element.rb +11 -0
- data/lib/tilia/v_object/parser/xml.rb +322 -0
- data/lib/tilia/v_object/parser.rb +10 -0
- data/lib/tilia/v_object/property/binary.rb +96 -0
- data/lib/tilia/v_object/property/boolean.rb +57 -0
- data/lib/tilia/v_object/property/flat_text.rb +52 -0
- data/lib/tilia/v_object/property/float_value.rb +107 -0
- data/lib/tilia/v_object/property/i_calendar/cal_address.rb +49 -0
- data/lib/tilia/v_object/property/i_calendar/date.rb +15 -0
- data/lib/tilia/v_object/property/i_calendar/date_time.rb +330 -0
- data/lib/tilia/v_object/property/i_calendar/duration.rb +65 -0
- data/lib/tilia/v_object/property/i_calendar/period.rb +124 -0
- data/lib/tilia/v_object/property/i_calendar/recur.rb +173 -0
- data/lib/tilia/v_object/property/i_calendar.rb +14 -0
- data/lib/tilia/v_object/property/integer_value.rb +60 -0
- data/lib/tilia/v_object/property/text.rb +352 -0
- data/lib/tilia/v_object/property/time.rb +85 -0
- data/lib/tilia/v_object/property/unknown.rb +30 -0
- data/lib/tilia/v_object/property/uri.rb +78 -0
- data/lib/tilia/v_object/property/utc_offset.rb +56 -0
- data/lib/tilia/v_object/property/v_card/date.rb +31 -0
- data/lib/tilia/v_object/property/v_card/date_and_or_time.rb +343 -0
- data/lib/tilia/v_object/property/v_card/date_time.rb +22 -0
- data/lib/tilia/v_object/property/v_card/language_tag.rb +41 -0
- data/lib/tilia/v_object/property/v_card/time_stamp.rb +74 -0
- data/lib/tilia/v_object/property/v_card.rb +13 -0
- data/lib/tilia/v_object/property.rb +532 -0
- data/lib/tilia/v_object/reader.rb +73 -0
- data/lib/tilia/v_object/recur/event_iterator.rb +417 -0
- data/lib/tilia/v_object/recur/no_instances_exception.rb +11 -0
- data/lib/tilia/v_object/recur/r_date_iterator.rb +138 -0
- data/lib/tilia/v_object/recur/r_rule_iterator.rb +717 -0
- data/lib/tilia/v_object/recur.rb +10 -0
- data/lib/tilia/v_object/settings.rb +32 -0
- data/lib/tilia/v_object/splitter/i_calendar.rb +95 -0
- data/lib/tilia/v_object/splitter/splitter_interface.rb +31 -0
- data/lib/tilia/v_object/splitter/v_card.rb +56 -0
- data/lib/tilia/v_object/splitter.rb +9 -0
- data/lib/tilia/v_object/string_util.rb +58 -0
- data/lib/tilia/v_object/time_zone_data/exchange_zones.rb +96 -0
- data/lib/tilia/v_object/time_zone_data/lotus_zones.rb +104 -0
- data/lib/tilia/v_object/time_zone_data/php_zones.rb +49 -0
- data/lib/tilia/v_object/time_zone_data/windows_zones.rb +121 -0
- data/lib/tilia/v_object/time_zone_data.rb +10 -0
- data/lib/tilia/v_object/time_zone_util.rb +213 -0
- data/lib/tilia/v_object/uuid_util.rb +51 -0
- data/lib/tilia/v_object/v_card_converter.rb +354 -0
- data/lib/tilia/v_object/version.rb +9 -0
- data/lib/tilia/v_object/writer.rb +56 -0
- data/lib/tilia/v_object.rb +45 -0
- data/lib/tilia/vobject.rb +1 -0
- data/resources/schema/xcal.rng +1192 -0
- data/resources/schema/xcard.rng +388 -0
- data/test/test_helper.rb +56 -0
- data/test/v_object/attach_issue_test.rb +19 -0
- data/test/v_object/birthday_calendar_generator_test.rb +463 -0
- data/test/v_object/cli_mock.rb +19 -0
- data/test/v_object/cli_test.rb +460 -0
- data/test/v_object/component/available_test.rb +59 -0
- data/test/v_object/component/v_alarm_test.rb +160 -0
- data/test/v_object/component/v_availability_test.rb +388 -0
- data/test/v_object/component/v_calendar_test.rb +646 -0
- data/test/v_object/component/v_card_test.rb +258 -0
- data/test/v_object/component/v_event_test.rb +85 -0
- data/test/v_object/component/v_free_busy_test.rb +59 -0
- data/test/v_object/component/v_journal_test.rb +85 -0
- data/test/v_object/component/v_time_zone_test.rb +47 -0
- data/test/v_object/component/v_todo_test.rb +172 -0
- data/test/v_object/component_test.rb +419 -0
- data/test/v_object/date_time_parser_test.rb +526 -0
- data/test/v_object/document_test.rb +71 -0
- data/test/v_object/element_list_test.rb +27 -0
- data/test/v_object/em_client_test.rb +53 -0
- data/test/v_object/empty_parameter_test.rb +65 -0
- data/test/v_object/empty_value_issue_test.rb +25 -0
- data/test/v_object/fake_component.rb +21 -0
- data/test/v_object/free_busy_data_test.rb +285 -0
- data/test/v_object/free_busy_generator_test.rb +637 -0
- data/test/v_object/google_colon_escaping_test.rb +27 -0
- data/test/v_object/i_calendar/attach_parse_test.rb +24 -0
- data/test/v_object/i_tip/broker_attendee_reply_test.rb +1042 -0
- data/test/v_object/i_tip/broker_delete_event_test.rb +175 -0
- data/test/v_object/i_tip/broker_new_event_test.rb +440 -0
- data/test/v_object/i_tip/broker_process_message_test.rb +153 -0
- data/test/v_object/i_tip/broker_process_reply_test.rb +402 -0
- data/test/v_object/i_tip/broker_tester.rb +71 -0
- data/test/v_object/i_tip/broker_update_event_test.rb +763 -0
- data/test/v_object/i_tip/evolution_test.rb +2644 -0
- data/test/v_object/i_tip/message_test.rb +25 -0
- data/test/v_object/issue153.vcf +352 -0
- data/test/v_object/issue153_test.rb +12 -0
- data/test/v_object/issue26_test.rb +25 -0
- data/test/v_object/issue36_work_around_test.rb +37 -0
- data/test/v_object/issue40_test.rb +26 -0
- data/test/v_object/issue64.vcf +351 -0
- data/test/v_object/issue64_test.rb +17 -0
- data/test/v_object/issue96_test.rb +22 -0
- data/test/v_object/issue_undefined_index_test.rb +24 -0
- data/test/v_object/j_cal_test.rb +150 -0
- data/test/v_object/j_card_test.rb +192 -0
- data/test/v_object/line_folding_issue_test.rb +19 -0
- data/test/v_object/mock_document.rb +6 -0
- data/test/v_object/parameter_test.rb +109 -0
- data/test/v_object/parser/json_test.rb +370 -0
- data/test/v_object/parser/mime_dir_test.rb +14 -0
- data/test/v_object/parser/quoted_printable_test.rb +78 -0
- data/test/v_object/parser/xml_test.rb +2563 -0
- data/test/v_object/property/binary_test.rb +12 -0
- data/test/v_object/property/boolean_test.rb +18 -0
- data/test/v_object/property/compound_test.rb +43 -0
- data/test/v_object/property/float_test.rb +20 -0
- data/test/v_object/property/i_calendar/cal_address_test.rb +26 -0
- data/test/v_object/property/i_calendar/date_time_test.rb +303 -0
- data/test/v_object/property/i_calendar/duration_test.rb +14 -0
- data/test/v_object/property/i_calendar/recur_test.rb +39 -0
- data/test/v_object/property/text_test.rb +81 -0
- data/test/v_object/property/v_card/date_and_or_time_test.rb +205 -0
- data/test/v_object/property/v_card/language_tag_test.rb +35 -0
- data/test/v_object/property_test.rb +338 -0
- data/test/v_object/reader_test.rb +403 -0
- data/test/v_object/recur/event_iterator/by_month_in_daily_test.rb +52 -0
- data/test/v_object/recur/event_iterator/by_set_pos_hang_test.rb +55 -0
- data/test/v_object/recur/event_iterator/expand_floating_times_test.rb +109 -0
- data/test/v_object/recur/event_iterator/fifth_tuesday_problem_test.rb +45 -0
- data/test/v_object/recur/event_iterator/incorrect_expand_test.rb +53 -0
- data/test/v_object/recur/event_iterator/infinite_loop_problem_test.rb +75 -0
- data/test/v_object/recur/event_iterator/issue48_test.rb +43 -0
- data/test/v_object/recur/event_iterator/issue50_test.rb +123 -0
- data/test/v_object/recur/event_iterator/main_test.rb +1222 -0
- data/test/v_object/recur/event_iterator/missing_overridden_test.rb +55 -0
- data/test/v_object/recur/event_iterator/no_instances_test.rb +32 -0
- data/test/v_object/recur/event_iterator/override_first_event_test.rb +106 -0
- data/test/v_object/recur/r_date_iterator_test.rb +44 -0
- data/test/v_object/recur/r_rule_iterator_test.rb +608 -0
- data/test/v_object/recurrence_iterator/UntilRespectsTimezoneTest.ics +39 -0
- data/test/v_object/slash_r_test.rb +15 -0
- data/test/v_object/splitter/i_calendar_test.rb +299 -0
- data/test/v_object/splitter/v_card_test.rb +173 -0
- data/test/v_object/string_util_test.rb +37 -0
- data/test/v_object/test_case.rb +42 -0
- data/test/v_object/time_zone_util_test.rb +271 -0
- data/test/v_object/uuid_util_test.rb +18 -0
- data/test/v_object/v_card21_test.rb +43 -0
- data/test/v_object/v_card_converter_test.rb +419 -0
- data/test/v_object/version_test.rb +15 -0
- data/test/v_object/writer_test.rb +33 -0
- data/tilia-vobject.gemspec +17 -0
- metadata +308 -0
@@ -0,0 +1,717 @@
|
|
1
|
+
module Tilia
|
2
|
+
module VObject
|
3
|
+
module Recur
|
4
|
+
# RRuleParser.
|
5
|
+
#
|
6
|
+
# This class receives an RRULE string, and allows you to iterate to get a list
|
7
|
+
# of dates in that recurrence.
|
8
|
+
#
|
9
|
+
# For instance, passing: FREQ=DAILY;LIMIT=5 will cause the iterator to contain
|
10
|
+
# 5 items, one for each day.
|
11
|
+
class RRuleIterator
|
12
|
+
# Creates the Iterator.
|
13
|
+
#
|
14
|
+
# @param string|array rrule
|
15
|
+
# @param DateTimeInterface start
|
16
|
+
def initialize(rrule, start)
|
17
|
+
@week_start = 'MO'
|
18
|
+
@counter = 0
|
19
|
+
@interval = 1
|
20
|
+
@day_map = {
|
21
|
+
'SU' => 0,
|
22
|
+
'MO' => 1,
|
23
|
+
'TU' => 2,
|
24
|
+
'WE' => 3,
|
25
|
+
'TH' => 4,
|
26
|
+
'FR' => 5,
|
27
|
+
'SA' => 6
|
28
|
+
}
|
29
|
+
@day_names = {
|
30
|
+
0 => 'Sunday',
|
31
|
+
1 => 'Monday',
|
32
|
+
2 => 'Tuesday',
|
33
|
+
3 => 'Wednesday',
|
34
|
+
4 => 'Thursday',
|
35
|
+
5 => 'Friday',
|
36
|
+
6 => 'Saturday'
|
37
|
+
}
|
38
|
+
|
39
|
+
@start_date = start
|
40
|
+
parse_r_rule(rrule)
|
41
|
+
@current_date = @start_date.clone
|
42
|
+
end
|
43
|
+
|
44
|
+
def current
|
45
|
+
return nil unless valid
|
46
|
+
@current_date.clone
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns the current item number.
|
50
|
+
#
|
51
|
+
# @return int
|
52
|
+
def key
|
53
|
+
@counter
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns whether the current item is a valid item for the recurrence
|
57
|
+
# iterator. This will return false if we've gone beyond the UNTIL or COUNT
|
58
|
+
# statements.
|
59
|
+
#
|
60
|
+
# @return bool
|
61
|
+
def valid
|
62
|
+
return @counter < @count if @count
|
63
|
+
@until.nil? || @current_date <= @until
|
64
|
+
end
|
65
|
+
|
66
|
+
# Resets the iterator.
|
67
|
+
#
|
68
|
+
# @return void
|
69
|
+
def rewind
|
70
|
+
@current_date = @start_date.clone
|
71
|
+
@counter = 0
|
72
|
+
end
|
73
|
+
|
74
|
+
# Goes on to the next iteration.
|
75
|
+
#
|
76
|
+
# @return void
|
77
|
+
def next
|
78
|
+
# Otherwise, we find the next event in the normal RRULE
|
79
|
+
# sequence.
|
80
|
+
case @frequency
|
81
|
+
when 'hourly'
|
82
|
+
next_hourly
|
83
|
+
when 'daily'
|
84
|
+
next_daily
|
85
|
+
when 'weekly'
|
86
|
+
next_weekly
|
87
|
+
when 'monthly'
|
88
|
+
next_monthly
|
89
|
+
when 'yearly'
|
90
|
+
next_yearly
|
91
|
+
end
|
92
|
+
|
93
|
+
@counter += 1
|
94
|
+
end
|
95
|
+
|
96
|
+
# Returns true if this recurring event never ends.
|
97
|
+
#
|
98
|
+
# @return bool
|
99
|
+
def infinite?
|
100
|
+
!@count && !@until
|
101
|
+
end
|
102
|
+
|
103
|
+
# This method allows you to quickly go to the next occurrence after the
|
104
|
+
# specified date.
|
105
|
+
#
|
106
|
+
# @param DateTimeInterface dt
|
107
|
+
#
|
108
|
+
# @return void
|
109
|
+
def fast_forward(dt)
|
110
|
+
self.next while valid && @current_date < dt
|
111
|
+
end
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
# The reference start date/time for the rrule.
|
116
|
+
#
|
117
|
+
# All calculations are based on this initial date.
|
118
|
+
#
|
119
|
+
# @var DateTimeInterface
|
120
|
+
# RUBY attr_accessor :start_date
|
121
|
+
|
122
|
+
# The date of the current iteration. You can get this by calling
|
123
|
+
# .current.
|
124
|
+
#
|
125
|
+
# @var DateTimeInterface
|
126
|
+
# RUBY attr_accessor :current_date
|
127
|
+
|
128
|
+
# Frequency is one of: secondly, minutely, hourly, daily, weekly, monthly,
|
129
|
+
# yearly.
|
130
|
+
#
|
131
|
+
# @var string
|
132
|
+
# RUBY attr_accessor :frequency
|
133
|
+
|
134
|
+
# The number of recurrences, or 'null' if infinitely recurring.
|
135
|
+
#
|
136
|
+
# @var int
|
137
|
+
# RUBY attr_accessor :count
|
138
|
+
|
139
|
+
# The interval.
|
140
|
+
#
|
141
|
+
# If for example frequency is set to daily, interval = 2 would mean every
|
142
|
+
# 2 days.
|
143
|
+
#
|
144
|
+
# @var int
|
145
|
+
# RUBY attr_accessor :interval
|
146
|
+
|
147
|
+
# The last instance of this recurrence, inclusively.
|
148
|
+
#
|
149
|
+
# @var DateTimeInterface|null
|
150
|
+
# RUBY attr_accessor :until
|
151
|
+
|
152
|
+
# Which seconds to recur.
|
153
|
+
#
|
154
|
+
# This is an array of integers (between 0 and 60)
|
155
|
+
#
|
156
|
+
# @var array
|
157
|
+
# RUBY attr_accessor :by_second
|
158
|
+
|
159
|
+
# Which minutes to recur.
|
160
|
+
#
|
161
|
+
# This is an array of integers (between 0 and 59)
|
162
|
+
#
|
163
|
+
# @var array
|
164
|
+
# RUBY attr_accessor :by_minute
|
165
|
+
|
166
|
+
# Which hours to recur.
|
167
|
+
#
|
168
|
+
# This is an array of integers (between 0 and 23)
|
169
|
+
#
|
170
|
+
# @var array
|
171
|
+
# RUBY attr_accessor :by_hour
|
172
|
+
|
173
|
+
# The current item in the list.
|
174
|
+
#
|
175
|
+
# You can get this number with the key method.
|
176
|
+
#
|
177
|
+
# @var int
|
178
|
+
# RUBY attr_accessor :counter
|
179
|
+
|
180
|
+
# Which weekdays to recur.
|
181
|
+
#
|
182
|
+
# This is an array of weekdays
|
183
|
+
#
|
184
|
+
# This may also be preceeded by a positive or negative integer. If present,
|
185
|
+
# this indicates the nth occurrence of a specific day within the monthly or
|
186
|
+
# yearly rrule. For instance, -2TU indicates the second-last tuesday of
|
187
|
+
# the month, or year.
|
188
|
+
#
|
189
|
+
# @var array
|
190
|
+
# RUBY attr_accessor :by_day
|
191
|
+
|
192
|
+
# Which days of the month to recur.
|
193
|
+
#
|
194
|
+
# This is an array of days of the months (1-31). The value can also be
|
195
|
+
# negative. -5 for instance means the 5th last day of the month.
|
196
|
+
#
|
197
|
+
# @var array
|
198
|
+
# RUBY attr_accessor :by_month_day
|
199
|
+
|
200
|
+
# Which days of the year to recur.
|
201
|
+
#
|
202
|
+
# This is an array with days of the year (1 to 366). The values can also
|
203
|
+
# be negative. For instance, -1 will always represent the last day of the
|
204
|
+
# year. (December 31st).
|
205
|
+
#
|
206
|
+
# @var array
|
207
|
+
# RUBY attr_accessor :by_year_day
|
208
|
+
|
209
|
+
# Which week numbers to recur.
|
210
|
+
#
|
211
|
+
# This is an array of integers from 1 to 53. The values can also be
|
212
|
+
# negative. -1 will always refer to the last week of the year.
|
213
|
+
#
|
214
|
+
# @var array
|
215
|
+
# RUBY attr_accessor :by_week_no
|
216
|
+
|
217
|
+
# Which months to recur.
|
218
|
+
#
|
219
|
+
# This is an array of integers from 1 to 12.
|
220
|
+
#
|
221
|
+
# @var array
|
222
|
+
# RUBY attr_accessor :by_month
|
223
|
+
|
224
|
+
# Which items in an existing st to recur.
|
225
|
+
#
|
226
|
+
# These numbers work together with an existing by* rule. It specifies
|
227
|
+
# exactly which items of the existing by-rule to filter.
|
228
|
+
#
|
229
|
+
# Valid values are 1 to 366 and -1 to -366. As an example, this can be
|
230
|
+
# used to recur the last workday of the month.
|
231
|
+
#
|
232
|
+
# This would be done by setting frequency to 'monthly', byDay to
|
233
|
+
# 'MO,TU,WE,TH,FR' and bySetPos to -1.
|
234
|
+
#
|
235
|
+
# @var array
|
236
|
+
# RUBY attr_accessor :by_set_pos
|
237
|
+
|
238
|
+
# When the week starts.
|
239
|
+
#
|
240
|
+
# @var string
|
241
|
+
# RUBY attr_accessor :week_start
|
242
|
+
|
243
|
+
# Does the processing for advancing the iterator for hourly frequency.
|
244
|
+
#
|
245
|
+
# @return void
|
246
|
+
def next_hourly
|
247
|
+
@current_date += @interval.hours
|
248
|
+
end
|
249
|
+
|
250
|
+
# Does the processing for advancing the iterator for daily frequency.
|
251
|
+
#
|
252
|
+
# @return void
|
253
|
+
def next_daily
|
254
|
+
unless @by_hour || @by_day
|
255
|
+
@current_date += @interval.days
|
256
|
+
return nil
|
257
|
+
end
|
258
|
+
|
259
|
+
recurrence_hours = hours if @by_hour
|
260
|
+
recurrence_days = days if @by_day
|
261
|
+
recurrence_months = months if @by_month
|
262
|
+
|
263
|
+
loop do
|
264
|
+
if @by_hour
|
265
|
+
if @current_date.hour == 23
|
266
|
+
# to obey the interval rule
|
267
|
+
@current_date += (@interval - 1).days
|
268
|
+
end
|
269
|
+
|
270
|
+
@current_date += 1.hour
|
271
|
+
|
272
|
+
else
|
273
|
+
@current_date += @interval.days
|
274
|
+
end
|
275
|
+
|
276
|
+
# Current month of the year
|
277
|
+
current_month = @current_date.month
|
278
|
+
|
279
|
+
# Current day of the week
|
280
|
+
current_day = @current_date.wday
|
281
|
+
|
282
|
+
# Current hour of the day
|
283
|
+
current_hour = @current_date.hour
|
284
|
+
|
285
|
+
break unless (@by_day && !recurrence_days.include?(current_day)) ||
|
286
|
+
(@by_hour && !recurrence_hours.include?(current_hour)) ||
|
287
|
+
(@by_month && !recurrence_months.include?(current_month))
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Does the processing for advancing the iterator for weekly frequency.
|
292
|
+
#
|
293
|
+
# @return void
|
294
|
+
def next_weekly
|
295
|
+
if !@by_hour && !@by_day
|
296
|
+
@current_date += @interval.weeks
|
297
|
+
return nil
|
298
|
+
end
|
299
|
+
|
300
|
+
recurrence_hours = hours if @by_hour
|
301
|
+
|
302
|
+
recurrence_days = days if @by_day
|
303
|
+
|
304
|
+
# First day of the week:
|
305
|
+
first_day = @day_map[@week_start]
|
306
|
+
loop do
|
307
|
+
if @by_hour
|
308
|
+
@current_date += 1.hour
|
309
|
+
else
|
310
|
+
@current_date += 1.day
|
311
|
+
end
|
312
|
+
|
313
|
+
# Current day of the week
|
314
|
+
current_day = @current_date.wday
|
315
|
+
|
316
|
+
# Current hour of the day
|
317
|
+
current_hour = @current_date.hour
|
318
|
+
|
319
|
+
# We need to roll over to the next week
|
320
|
+
if current_day == first_day && (!@by_hour || current_hour == 0)
|
321
|
+
@current_date += (@interval - 1).weeks
|
322
|
+
|
323
|
+
# We need to go to the first day of this week, but only if we
|
324
|
+
# are not already on this first day of this week.
|
325
|
+
if @current_date.wday != first_day
|
326
|
+
@current_date -= (@current_date.wday - first_day).days
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# We have a match
|
331
|
+
break unless (@by_day && !recurrence_days.include?(current_day)) || (@by_hour && !recurrence_hours.include?(current_hour))
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
# Does the processing for advancing the iterator for monthly frequency.
|
336
|
+
#
|
337
|
+
# @return void
|
338
|
+
def next_monthly
|
339
|
+
current_day_of_month = @current_date.day
|
340
|
+
unless @by_month_day || @by_day
|
341
|
+
# If the current day is higher than the 28th, rollover can
|
342
|
+
# occur to the next month. We Must skip these invalid
|
343
|
+
# entries.
|
344
|
+
if current_day_of_month < 29
|
345
|
+
@current_date += @interval.months
|
346
|
+
else
|
347
|
+
increase = 0
|
348
|
+
temp_date = nil
|
349
|
+
loop do
|
350
|
+
increase += 1
|
351
|
+
temp_date = @current_date + (@interval * increase).months
|
352
|
+
break unless temp_date.day != current_day_of_month
|
353
|
+
end
|
354
|
+
@current_date = temp_date
|
355
|
+
end
|
356
|
+
return nil
|
357
|
+
end
|
358
|
+
|
359
|
+
occurrence = nil
|
360
|
+
loop do
|
361
|
+
occurrences = monthly_occurrences
|
362
|
+
|
363
|
+
occurrence = nil
|
364
|
+
stop = false
|
365
|
+
occurrences.each do |this_occurrence|
|
366
|
+
# The first occurrence thats higher than the current
|
367
|
+
# day of the month wins.
|
368
|
+
next unless this_occurrence > current_day_of_month
|
369
|
+
occurrence = this_occurrence
|
370
|
+
stop = true
|
371
|
+
break
|
372
|
+
end
|
373
|
+
break if stop
|
374
|
+
occurrence = occurrences.last unless occurrence
|
375
|
+
|
376
|
+
# If we made it all the way here, it means there were no
|
377
|
+
# valid occurrences, and we need to advance to the next
|
378
|
+
# month.
|
379
|
+
@current_date - (@current_date.day - 1).days
|
380
|
+
@current_date += @interval.months
|
381
|
+
|
382
|
+
# This goes to 0 because we need to start counting at the
|
383
|
+
# beginning.
|
384
|
+
current_day_of_month = 0
|
385
|
+
end
|
386
|
+
|
387
|
+
@current_date += (occurrence.to_i - @current_date.day).days
|
388
|
+
end
|
389
|
+
|
390
|
+
# Does the processing for advancing the iterator for yearly frequency.
|
391
|
+
#
|
392
|
+
# @return void
|
393
|
+
def next_yearly
|
394
|
+
current_month = @current_date.month
|
395
|
+
current_year = @current_date.year
|
396
|
+
current_day_of_month = @current_date.day
|
397
|
+
|
398
|
+
# No sub-rules, so we just advance by year
|
399
|
+
unless @by_month
|
400
|
+
# Unless it was a leap day!
|
401
|
+
if current_month == 2 && current_day_of_month == 29
|
402
|
+
counter = 0
|
403
|
+
next_date = nil
|
404
|
+
loop do
|
405
|
+
counter += 1
|
406
|
+
# Here we increase the year count by the interval, until
|
407
|
+
# we hit a date that's also in a leap year.
|
408
|
+
#
|
409
|
+
# We could just find the next interval that's dividable by
|
410
|
+
# 4, but that would ignore the rule that there's no leap
|
411
|
+
# year every year that's dividable by a 100, but not by
|
412
|
+
# 400. (1800, 1900, 2100). So we just rely on the datetime
|
413
|
+
# functions instead.
|
414
|
+
next_date = @current_date + (@interval * counter).years
|
415
|
+
break if next_date.to_date.leap?
|
416
|
+
end
|
417
|
+
|
418
|
+
@current_date = next_date
|
419
|
+
|
420
|
+
return nil
|
421
|
+
end
|
422
|
+
|
423
|
+
# The easiest form
|
424
|
+
@current_date += @interval.years
|
425
|
+
return nil
|
426
|
+
end
|
427
|
+
|
428
|
+
current_month = @current_date.month
|
429
|
+
current_year = @current_date.year
|
430
|
+
current_day_of_month = @current_date.day
|
431
|
+
|
432
|
+
advanced_to_new_month = false
|
433
|
+
|
434
|
+
occurrence = nil
|
435
|
+
# If we got a byDay or getMonthDay filter, we must first expand
|
436
|
+
# further.
|
437
|
+
if @by_day || @by_month_day
|
438
|
+
loop do
|
439
|
+
occurrences = monthly_occurrences
|
440
|
+
|
441
|
+
stop = false
|
442
|
+
occurrences.each do |this_occurrence|
|
443
|
+
# The first occurrence that's higher than the current
|
444
|
+
# day of the month wins.
|
445
|
+
# If we advanced to the next month or year, the first
|
446
|
+
# occurrence is always correct.
|
447
|
+
next unless this_occurrence > current_day_of_month || advanced_to_new_month
|
448
|
+
occurrence = this_occurrence
|
449
|
+
stop = true
|
450
|
+
break
|
451
|
+
end
|
452
|
+
occurrence = occurrences.last unless occurrence
|
453
|
+
break if stop
|
454
|
+
|
455
|
+
# If we made it here, it means we need to advance to
|
456
|
+
# the next month or year.
|
457
|
+
current_day_of_month = 1
|
458
|
+
advanced_to_new_month = true
|
459
|
+
|
460
|
+
loop do
|
461
|
+
current_month += 1
|
462
|
+
if current_month > 12
|
463
|
+
current_year += @interval
|
464
|
+
current_month = 1
|
465
|
+
end
|
466
|
+
break if @by_month.include?(current_month.to_s)
|
467
|
+
end
|
468
|
+
|
469
|
+
@current_date = @current_date +
|
470
|
+
(current_year - @current_date.year).years +
|
471
|
+
(current_month - @current_date.month).months +
|
472
|
+
(current_day_of_month - @current_date.day).days
|
473
|
+
end
|
474
|
+
|
475
|
+
# If we made it here, it means we got a valid occurrence
|
476
|
+
@current_date = @current_date +
|
477
|
+
(current_year - @current_date.year).years +
|
478
|
+
(current_month - @current_date.month).months +
|
479
|
+
(occurrence - @current_date.day).days
|
480
|
+
return nil
|
481
|
+
else
|
482
|
+
# These are the 'byMonth' rules, if there are no byDay or
|
483
|
+
# byMonthDay sub-rules.
|
484
|
+
loop do
|
485
|
+
current_month += 1
|
486
|
+
if current_month > 12
|
487
|
+
current_year += @interval
|
488
|
+
current_month = 1
|
489
|
+
end
|
490
|
+
break if @by_month.include?(current_month.to_s)
|
491
|
+
end
|
492
|
+
|
493
|
+
@current_date = @current_date +
|
494
|
+
(current_year - @current_date.year).years +
|
495
|
+
(current_month - @current_date.month).months +
|
496
|
+
(current_day_of_month - @current_date.day).days
|
497
|
+
|
498
|
+
return nil
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
# This method receives a string from an RRULE property, and populates this
|
503
|
+
# class with all the values.
|
504
|
+
#
|
505
|
+
# @param string|array rrule
|
506
|
+
#
|
507
|
+
# @return void
|
508
|
+
def parse_r_rule(rrule)
|
509
|
+
if rrule.is_a?(String)
|
510
|
+
rrule = Property::ICalendar::Recur.string_to_array(rrule)
|
511
|
+
end
|
512
|
+
|
513
|
+
rrule.each do |key, value|
|
514
|
+
key = key.upcase
|
515
|
+
case key
|
516
|
+
when 'FREQ'
|
517
|
+
value = value.downcase
|
518
|
+
unless ['secondly', 'minutely', 'hourly', 'daily', 'weekly', 'monthly', 'yearly'].include?(value)
|
519
|
+
fail ArgumentError, "Unknown value for FREQ=#{value.upcase}"
|
520
|
+
end
|
521
|
+
@frequency = value
|
522
|
+
when 'UNTIL'
|
523
|
+
@until = DateTimeParser.parse(value, @start_date.time_zone)
|
524
|
+
|
525
|
+
# In some cases events are generated with an UNTIL=
|
526
|
+
# parameter before the actual start of the event.
|
527
|
+
#
|
528
|
+
# Not sure why this is happening. We assume that the
|
529
|
+
# intention was that the event only recurs once.
|
530
|
+
#
|
531
|
+
# So we are modifying the parameter so our code doesn't
|
532
|
+
# break.
|
533
|
+
@until = @start_date if @until < @start_date
|
534
|
+
when 'INTERVAL', 'COUNT'
|
535
|
+
val = value.to_i
|
536
|
+
if val < 1
|
537
|
+
fail ArgumentError, "#{key.upcase} in RRULE must be a positive integer!"
|
538
|
+
end
|
539
|
+
key = key.downcase
|
540
|
+
key == 'interval' ? @interval = val : @count = val
|
541
|
+
when 'BYSECOND'
|
542
|
+
@by_second = value.is_a?(Array) ? value : [value]
|
543
|
+
when 'BYMINUTE'
|
544
|
+
@by_minute = value.is_a?(Array) ? value : [value]
|
545
|
+
when 'BYHOUR'
|
546
|
+
@by_hour = value.is_a?(Array) ? value : [value]
|
547
|
+
when 'BYDAY'
|
548
|
+
value = value.is_a?(Array) ? value : [value]
|
549
|
+
value.each do |part|
|
550
|
+
unless part =~ /^ (-|\+)? ([1-5])? (MO|TU|WE|TH|FR|SA|SU) $/xi
|
551
|
+
fail ArgumentError, "Invalid part in BYDAY clause: #{part}"
|
552
|
+
end
|
553
|
+
end
|
554
|
+
@by_day = value
|
555
|
+
when 'BYMONTHDAY'
|
556
|
+
@by_month_day = value.is_a?(Array) ? value : [value]
|
557
|
+
when 'BYYEARDAY'
|
558
|
+
@by_year_day = value.is_a?(Array) ? value : [value]
|
559
|
+
when 'BYWEEKNO'
|
560
|
+
@by_week_no = value.is_a?(Array) ? value : [value]
|
561
|
+
when 'BYMONTH'
|
562
|
+
@by_month = value.is_a?(Array) ? value : [value]
|
563
|
+
when 'BYSETPOS'
|
564
|
+
@by_set_pos = value.is_a?(Array) ? value : [value]
|
565
|
+
when 'WKST'
|
566
|
+
@week_start = value.upcase
|
567
|
+
else
|
568
|
+
fail ArgumentError, "Not supported: #{key.upcase}"
|
569
|
+
end
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
# Mappings between the day number and english day name.
|
574
|
+
#
|
575
|
+
# @var array
|
576
|
+
# RUBY: attr_accessor :day_names
|
577
|
+
|
578
|
+
# Returns all the occurrences for a monthly frequency with a 'byDay' or
|
579
|
+
# 'byMonthDay' expansion for the current month.
|
580
|
+
#
|
581
|
+
# The returned list is an array of integers with the day of month (1-31).
|
582
|
+
#
|
583
|
+
# @return array
|
584
|
+
def monthly_occurrences
|
585
|
+
start_date = @current_date.clone
|
586
|
+
|
587
|
+
by_day_results = []
|
588
|
+
|
589
|
+
# Our strategy is to simply go through the byDays, advance the date to
|
590
|
+
# that point and add it to the results.
|
591
|
+
if @by_day
|
592
|
+
@by_day.each do |day|
|
593
|
+
day_index = @day_map[day[-2..-1]]
|
594
|
+
|
595
|
+
# Dayname will be something like 'wednesday'. Now we need to find
|
596
|
+
# all wednesdays in this month.
|
597
|
+
day_hits = []
|
598
|
+
|
599
|
+
check_date = start_date - (start_date.day - 1).days
|
600
|
+
if check_date.wday != day_index
|
601
|
+
if day_index < check_date.wday
|
602
|
+
check_date += (7 - check_date.wday + day_index).days
|
603
|
+
else
|
604
|
+
check_date += (day_index - check_date.wday).days
|
605
|
+
end
|
606
|
+
end
|
607
|
+
|
608
|
+
loop do
|
609
|
+
day_hits << check_date.day
|
610
|
+
check_date += 1.week
|
611
|
+
break unless check_date.month == start_date.month
|
612
|
+
end
|
613
|
+
|
614
|
+
# So now we have 'all wednesdays' for month. It is however
|
615
|
+
# possible that the user only really wanted the 1st, 2nd or last
|
616
|
+
# wednesday.
|
617
|
+
if day.length > 2
|
618
|
+
offset = day[0..-3].to_i
|
619
|
+
|
620
|
+
if offset > 0
|
621
|
+
# It is possible that the day does not exist, such as a
|
622
|
+
# 5th or 6th wednesday of the month.
|
623
|
+
by_day_results << day_hits[offset - 1] if day_hits[offset - 1]
|
624
|
+
else
|
625
|
+
# if it was negative we count from the end of the array
|
626
|
+
# might not exist, fx. -5th tuesday
|
627
|
+
by_day_results << day_hits[offset] if day_hits[offset]
|
628
|
+
end
|
629
|
+
else
|
630
|
+
# There was no counter (first, second, last wednesdays), so we
|
631
|
+
# just need to add the all to the list).
|
632
|
+
by_day_results.concat(day_hits)
|
633
|
+
end
|
634
|
+
end
|
635
|
+
end
|
636
|
+
|
637
|
+
by_month_day_results = []
|
638
|
+
if @by_month_day
|
639
|
+
@by_month_day.each do |month_day|
|
640
|
+
days_in_month = Time.days_in_month(start_date.month, start_date.year)
|
641
|
+
# Removing values that are out of range for this month
|
642
|
+
if month_day.to_i > days_in_month || month_day.to_i < 0 - days_in_month
|
643
|
+
next
|
644
|
+
end
|
645
|
+
if month_day.to_i > 0
|
646
|
+
by_month_day_results << month_day.to_i
|
647
|
+
else
|
648
|
+
# Negative values
|
649
|
+
by_month_day_results << days_in_month + 1 + month_day.to_i
|
650
|
+
end
|
651
|
+
end
|
652
|
+
end
|
653
|
+
|
654
|
+
# If there was just byDay or just byMonthDay, they just specify our
|
655
|
+
# (almost) final list. If both were provided, then byDay limits the
|
656
|
+
# list.
|
657
|
+
if @by_month_day && @by_day
|
658
|
+
result = by_month_day_results & by_day_results
|
659
|
+
elsif @by_month_day
|
660
|
+
result = by_month_day_results
|
661
|
+
else
|
662
|
+
result = by_day_results
|
663
|
+
end
|
664
|
+
result = result.uniq
|
665
|
+
result = result.sort
|
666
|
+
|
667
|
+
# The last thing that needs checking is the BYSETPOS. If it's set, it
|
668
|
+
# means only certain items in the set survive the filter.
|
669
|
+
return result unless @by_set_pos
|
670
|
+
|
671
|
+
filtered_result = []
|
672
|
+
@by_set_pos.each do |set_pos|
|
673
|
+
set_pos = set_pos.to_i
|
674
|
+
|
675
|
+
set_pos += 1 if set_pos < 0
|
676
|
+
filtered_result << result[set_pos - 1] if result[set_pos - 1]
|
677
|
+
end
|
678
|
+
|
679
|
+
filtered_result = filtered_result.sort
|
680
|
+
filtered_result
|
681
|
+
end
|
682
|
+
|
683
|
+
# Simple mapping from iCalendar day names to day numbers.
|
684
|
+
#
|
685
|
+
# @var array
|
686
|
+
# RUBY: attr_accessor :day_map
|
687
|
+
|
688
|
+
def hours
|
689
|
+
recurrence_hours = []
|
690
|
+
@by_hour.each do |by_hour|
|
691
|
+
recurrence_hours << by_hour.to_i
|
692
|
+
end
|
693
|
+
recurrence_hours
|
694
|
+
end
|
695
|
+
|
696
|
+
def days
|
697
|
+
recurrence_days = []
|
698
|
+
@by_day.each do |by_day|
|
699
|
+
# The day may be preceeded with a positive (+n) or
|
700
|
+
# negative (-n) integer. However, this does not make
|
701
|
+
# sense in 'weekly' so we ignore it here.
|
702
|
+
recurrence_days << @day_map[by_day[0...2]]
|
703
|
+
end
|
704
|
+
recurrence_days
|
705
|
+
end
|
706
|
+
|
707
|
+
def months
|
708
|
+
recurrence_months = []
|
709
|
+
@by_month.each do |by_month|
|
710
|
+
recurrence_months << by_month.to_i
|
711
|
+
end
|
712
|
+
recurrence_months
|
713
|
+
end
|
714
|
+
end
|
715
|
+
end
|
716
|
+
end
|
717
|
+
end
|