tilia-vobject 4.0.0.pre.alpha2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (193) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rubocop.yml +32 -0
  4. data/.simplecov +4 -0
  5. data/.travis.yml +3 -0
  6. data/CHANGELOG.sabre.md +626 -0
  7. data/CONTRIBUTING.md +25 -0
  8. data/Gemfile +17 -0
  9. data/Gemfile.lock +68 -0
  10. data/LICENSE +27 -0
  11. data/LICENSE.sabre +27 -0
  12. data/README.md +63 -0
  13. data/Rakefile +17 -0
  14. data/bin/vobject +7 -0
  15. data/lib/tilia/v_object/birthday_calendar_generator.rb +142 -0
  16. data/lib/tilia/v_object/cli.rb +582 -0
  17. data/lib/tilia/v_object/component/available.rb +107 -0
  18. data/lib/tilia/v_object/component/v_alarm.rb +114 -0
  19. data/lib/tilia/v_object/component/v_availability.rb +128 -0
  20. data/lib/tilia/v_object/component/v_calendar.rb +468 -0
  21. data/lib/tilia/v_object/component/v_card.rb +457 -0
  22. data/lib/tilia/v_object/component/v_event.rb +127 -0
  23. data/lib/tilia/v_object/component/v_free_busy.rb +81 -0
  24. data/lib/tilia/v_object/component/v_journal.rb +75 -0
  25. data/lib/tilia/v_object/component/v_time_zone.rb +51 -0
  26. data/lib/tilia/v_object/component/v_todo.rb +147 -0
  27. data/lib/tilia/v_object/component.rb +591 -0
  28. data/lib/tilia/v_object/date_time_parser.rb +486 -0
  29. data/lib/tilia/v_object/document.rb +218 -0
  30. data/lib/tilia/v_object/element_list.rb +18 -0
  31. data/lib/tilia/v_object/eof_exception.rb +8 -0
  32. data/lib/tilia/v_object/free_busy_data.rb +149 -0
  33. data/lib/tilia/v_object/free_busy_generator.rb +465 -0
  34. data/lib/tilia/v_object/i_tip/broker.rb +909 -0
  35. data/lib/tilia/v_object/i_tip/i_tip_exception.rb +9 -0
  36. data/lib/tilia/v_object/i_tip/message.rb +109 -0
  37. data/lib/tilia/v_object/i_tip/same_organizer_for_all_components_exception.rb +13 -0
  38. data/lib/tilia/v_object/i_tip.rb +10 -0
  39. data/lib/tilia/v_object/node.rb +192 -0
  40. data/lib/tilia/v_object/parameter.rb +327 -0
  41. data/lib/tilia/v_object/parse_exception.rb +7 -0
  42. data/lib/tilia/v_object/parser/json.rb +149 -0
  43. data/lib/tilia/v_object/parser/mime_dir.rb +543 -0
  44. data/lib/tilia/v_object/parser/parser.rb +61 -0
  45. data/lib/tilia/v_object/parser/xml/element/key_value.rb +60 -0
  46. data/lib/tilia/v_object/parser/xml/element.rb +11 -0
  47. data/lib/tilia/v_object/parser/xml.rb +322 -0
  48. data/lib/tilia/v_object/parser.rb +10 -0
  49. data/lib/tilia/v_object/property/binary.rb +96 -0
  50. data/lib/tilia/v_object/property/boolean.rb +57 -0
  51. data/lib/tilia/v_object/property/flat_text.rb +52 -0
  52. data/lib/tilia/v_object/property/float_value.rb +107 -0
  53. data/lib/tilia/v_object/property/i_calendar/cal_address.rb +49 -0
  54. data/lib/tilia/v_object/property/i_calendar/date.rb +15 -0
  55. data/lib/tilia/v_object/property/i_calendar/date_time.rb +330 -0
  56. data/lib/tilia/v_object/property/i_calendar/duration.rb +65 -0
  57. data/lib/tilia/v_object/property/i_calendar/period.rb +124 -0
  58. data/lib/tilia/v_object/property/i_calendar/recur.rb +173 -0
  59. data/lib/tilia/v_object/property/i_calendar.rb +14 -0
  60. data/lib/tilia/v_object/property/integer_value.rb +60 -0
  61. data/lib/tilia/v_object/property/text.rb +352 -0
  62. data/lib/tilia/v_object/property/time.rb +85 -0
  63. data/lib/tilia/v_object/property/unknown.rb +30 -0
  64. data/lib/tilia/v_object/property/uri.rb +78 -0
  65. data/lib/tilia/v_object/property/utc_offset.rb +56 -0
  66. data/lib/tilia/v_object/property/v_card/date.rb +31 -0
  67. data/lib/tilia/v_object/property/v_card/date_and_or_time.rb +343 -0
  68. data/lib/tilia/v_object/property/v_card/date_time.rb +22 -0
  69. data/lib/tilia/v_object/property/v_card/language_tag.rb +41 -0
  70. data/lib/tilia/v_object/property/v_card/time_stamp.rb +74 -0
  71. data/lib/tilia/v_object/property/v_card.rb +13 -0
  72. data/lib/tilia/v_object/property.rb +532 -0
  73. data/lib/tilia/v_object/reader.rb +73 -0
  74. data/lib/tilia/v_object/recur/event_iterator.rb +417 -0
  75. data/lib/tilia/v_object/recur/no_instances_exception.rb +11 -0
  76. data/lib/tilia/v_object/recur/r_date_iterator.rb +138 -0
  77. data/lib/tilia/v_object/recur/r_rule_iterator.rb +717 -0
  78. data/lib/tilia/v_object/recur.rb +10 -0
  79. data/lib/tilia/v_object/settings.rb +32 -0
  80. data/lib/tilia/v_object/splitter/i_calendar.rb +95 -0
  81. data/lib/tilia/v_object/splitter/splitter_interface.rb +31 -0
  82. data/lib/tilia/v_object/splitter/v_card.rb +56 -0
  83. data/lib/tilia/v_object/splitter.rb +9 -0
  84. data/lib/tilia/v_object/string_util.rb +58 -0
  85. data/lib/tilia/v_object/time_zone_data/exchange_zones.rb +96 -0
  86. data/lib/tilia/v_object/time_zone_data/lotus_zones.rb +104 -0
  87. data/lib/tilia/v_object/time_zone_data/php_zones.rb +49 -0
  88. data/lib/tilia/v_object/time_zone_data/windows_zones.rb +121 -0
  89. data/lib/tilia/v_object/time_zone_data.rb +10 -0
  90. data/lib/tilia/v_object/time_zone_util.rb +213 -0
  91. data/lib/tilia/v_object/uuid_util.rb +51 -0
  92. data/lib/tilia/v_object/v_card_converter.rb +354 -0
  93. data/lib/tilia/v_object/version.rb +9 -0
  94. data/lib/tilia/v_object/writer.rb +56 -0
  95. data/lib/tilia/v_object.rb +45 -0
  96. data/lib/tilia/vobject.rb +1 -0
  97. data/resources/schema/xcal.rng +1192 -0
  98. data/resources/schema/xcard.rng +388 -0
  99. data/test/test_helper.rb +56 -0
  100. data/test/v_object/attach_issue_test.rb +19 -0
  101. data/test/v_object/birthday_calendar_generator_test.rb +463 -0
  102. data/test/v_object/cli_mock.rb +19 -0
  103. data/test/v_object/cli_test.rb +460 -0
  104. data/test/v_object/component/available_test.rb +59 -0
  105. data/test/v_object/component/v_alarm_test.rb +160 -0
  106. data/test/v_object/component/v_availability_test.rb +388 -0
  107. data/test/v_object/component/v_calendar_test.rb +646 -0
  108. data/test/v_object/component/v_card_test.rb +258 -0
  109. data/test/v_object/component/v_event_test.rb +85 -0
  110. data/test/v_object/component/v_free_busy_test.rb +59 -0
  111. data/test/v_object/component/v_journal_test.rb +85 -0
  112. data/test/v_object/component/v_time_zone_test.rb +47 -0
  113. data/test/v_object/component/v_todo_test.rb +172 -0
  114. data/test/v_object/component_test.rb +419 -0
  115. data/test/v_object/date_time_parser_test.rb +526 -0
  116. data/test/v_object/document_test.rb +71 -0
  117. data/test/v_object/element_list_test.rb +27 -0
  118. data/test/v_object/em_client_test.rb +53 -0
  119. data/test/v_object/empty_parameter_test.rb +65 -0
  120. data/test/v_object/empty_value_issue_test.rb +25 -0
  121. data/test/v_object/fake_component.rb +21 -0
  122. data/test/v_object/free_busy_data_test.rb +285 -0
  123. data/test/v_object/free_busy_generator_test.rb +637 -0
  124. data/test/v_object/google_colon_escaping_test.rb +27 -0
  125. data/test/v_object/i_calendar/attach_parse_test.rb +24 -0
  126. data/test/v_object/i_tip/broker_attendee_reply_test.rb +1042 -0
  127. data/test/v_object/i_tip/broker_delete_event_test.rb +175 -0
  128. data/test/v_object/i_tip/broker_new_event_test.rb +440 -0
  129. data/test/v_object/i_tip/broker_process_message_test.rb +153 -0
  130. data/test/v_object/i_tip/broker_process_reply_test.rb +402 -0
  131. data/test/v_object/i_tip/broker_tester.rb +71 -0
  132. data/test/v_object/i_tip/broker_update_event_test.rb +763 -0
  133. data/test/v_object/i_tip/evolution_test.rb +2644 -0
  134. data/test/v_object/i_tip/message_test.rb +25 -0
  135. data/test/v_object/issue153.vcf +352 -0
  136. data/test/v_object/issue153_test.rb +12 -0
  137. data/test/v_object/issue26_test.rb +25 -0
  138. data/test/v_object/issue36_work_around_test.rb +37 -0
  139. data/test/v_object/issue40_test.rb +26 -0
  140. data/test/v_object/issue64.vcf +351 -0
  141. data/test/v_object/issue64_test.rb +17 -0
  142. data/test/v_object/issue96_test.rb +22 -0
  143. data/test/v_object/issue_undefined_index_test.rb +24 -0
  144. data/test/v_object/j_cal_test.rb +150 -0
  145. data/test/v_object/j_card_test.rb +192 -0
  146. data/test/v_object/line_folding_issue_test.rb +19 -0
  147. data/test/v_object/mock_document.rb +6 -0
  148. data/test/v_object/parameter_test.rb +109 -0
  149. data/test/v_object/parser/json_test.rb +370 -0
  150. data/test/v_object/parser/mime_dir_test.rb +14 -0
  151. data/test/v_object/parser/quoted_printable_test.rb +78 -0
  152. data/test/v_object/parser/xml_test.rb +2563 -0
  153. data/test/v_object/property/binary_test.rb +12 -0
  154. data/test/v_object/property/boolean_test.rb +18 -0
  155. data/test/v_object/property/compound_test.rb +43 -0
  156. data/test/v_object/property/float_test.rb +20 -0
  157. data/test/v_object/property/i_calendar/cal_address_test.rb +26 -0
  158. data/test/v_object/property/i_calendar/date_time_test.rb +303 -0
  159. data/test/v_object/property/i_calendar/duration_test.rb +14 -0
  160. data/test/v_object/property/i_calendar/recur_test.rb +39 -0
  161. data/test/v_object/property/text_test.rb +81 -0
  162. data/test/v_object/property/v_card/date_and_or_time_test.rb +205 -0
  163. data/test/v_object/property/v_card/language_tag_test.rb +35 -0
  164. data/test/v_object/property_test.rb +338 -0
  165. data/test/v_object/reader_test.rb +403 -0
  166. data/test/v_object/recur/event_iterator/by_month_in_daily_test.rb +52 -0
  167. data/test/v_object/recur/event_iterator/by_set_pos_hang_test.rb +55 -0
  168. data/test/v_object/recur/event_iterator/expand_floating_times_test.rb +109 -0
  169. data/test/v_object/recur/event_iterator/fifth_tuesday_problem_test.rb +45 -0
  170. data/test/v_object/recur/event_iterator/incorrect_expand_test.rb +53 -0
  171. data/test/v_object/recur/event_iterator/infinite_loop_problem_test.rb +75 -0
  172. data/test/v_object/recur/event_iterator/issue48_test.rb +43 -0
  173. data/test/v_object/recur/event_iterator/issue50_test.rb +123 -0
  174. data/test/v_object/recur/event_iterator/main_test.rb +1222 -0
  175. data/test/v_object/recur/event_iterator/missing_overridden_test.rb +55 -0
  176. data/test/v_object/recur/event_iterator/no_instances_test.rb +32 -0
  177. data/test/v_object/recur/event_iterator/override_first_event_test.rb +106 -0
  178. data/test/v_object/recur/r_date_iterator_test.rb +44 -0
  179. data/test/v_object/recur/r_rule_iterator_test.rb +608 -0
  180. data/test/v_object/recurrence_iterator/UntilRespectsTimezoneTest.ics +39 -0
  181. data/test/v_object/slash_r_test.rb +15 -0
  182. data/test/v_object/splitter/i_calendar_test.rb +299 -0
  183. data/test/v_object/splitter/v_card_test.rb +173 -0
  184. data/test/v_object/string_util_test.rb +37 -0
  185. data/test/v_object/test_case.rb +42 -0
  186. data/test/v_object/time_zone_util_test.rb +271 -0
  187. data/test/v_object/uuid_util_test.rb +18 -0
  188. data/test/v_object/v_card21_test.rb +43 -0
  189. data/test/v_object/v_card_converter_test.rb +419 -0
  190. data/test/v_object/version_test.rb +15 -0
  191. data/test/v_object/writer_test.rb +33 -0
  192. data/tilia-vobject.gemspec +17 -0
  193. metadata +308 -0
@@ -0,0 +1,486 @@
1
+ module Tilia
2
+ module VObject
3
+ # DateTimeParser.
4
+ #
5
+ # This class is responsible for parsing the several different date and time
6
+ # formats iCalendar and vCards have.
7
+ class DateTimeParser
8
+ # Parses an iCalendar (rfc5545) formatted datetime and returns a
9
+ # DateTimeImmutable object.
10
+ #
11
+ # Specifying a reference timezone is optional. It will only be used
12
+ # if the non-UTC format is used. The argument is used as a reference, the
13
+ # returned DateTimeImmutable object will still be in the UTC timezone.
14
+ #
15
+ # @param string dt
16
+ # @param DateTimeZone tz
17
+ #
18
+ # @return DateTimeImmutable
19
+ def self.parse_date_time(dt, tz = nil)
20
+ # Format is YYYYMMDD + "T" + hhmmss
21
+ matches = /^([0-9]{4})([0-1][0-9])([0-3][0-9])T([0-2][0-9])([0-5][0-9])([0-5][0-9])([Z]?)$/.match(dt)
22
+
23
+ unless matches
24
+ fail "The supplied iCalendar datetime value is incorrect: #{dt}"
25
+ end
26
+
27
+ tz = ActiveSupport::TimeZone.new('UTC') if matches[7] == 'Z' || tz.nil?
28
+ date = tz.parse("#{matches[1]}-#{matches[2]}-#{matches[3]} #{matches[4]}:#{matches[5]}:#{matches[6]}")
29
+
30
+ date
31
+ end
32
+
33
+ # Parses an iCalendar (rfc5545) formatted date and returns a DateTimeImmutable object.
34
+ #
35
+ # @param string date
36
+ # @param DateTimeZone tz
37
+ #
38
+ # @return DateTimeImmutable
39
+ def self.parse_date(date, tz = nil)
40
+ # Format is YYYYMMDD
41
+ matches = /^([0-9]{4})([0-1][0-9])([0-3][0-9])$/.match(date)
42
+
43
+ unless matches
44
+ fail "The supplied iCalendar date value is incorrect: #{date}"
45
+ end
46
+
47
+ tz = ActiveSupport::TimeZone.new('UTC') if tz.nil?
48
+
49
+ date = tz.parse("#{matches[1]}-#{matches[2]}-#{matches[3]}")
50
+
51
+ date
52
+ end
53
+
54
+ # Parses an iCalendar (RFC5545) formatted duration value.
55
+ #
56
+ # This method will either return a DateTimeInterval object, or a string
57
+ # suitable for strtotime or DateTime::modify.
58
+ #
59
+ # @param string duration
60
+ # @param bool as_string
61
+ #
62
+ # @return DateInterval|string
63
+ def self.parse_duration(duration, as_string = false)
64
+ matches = /^(?<plusminus>\+|-)?P((?<week>\d+)W)?((?<day>\d+)D)?(T((?<hour>\d+)H)?((?<minute>\d+)M)?((?<second>\d+)S)?)?$/.match(duration.to_s)
65
+
66
+ unless matches
67
+ fail "The supplied iCalendar duration value is incorrect: #{duration}"
68
+ end
69
+
70
+ unless as_string
71
+ invert = false
72
+
73
+ invert = true if matches['plusminus'] == '-'
74
+
75
+ parts = [
76
+ 'week',
77
+ 'day',
78
+ 'hour',
79
+ 'minute',
80
+ 'second'
81
+ ]
82
+
83
+ new_matches = {}
84
+ parts.each do |part|
85
+ new_matches[part] = matches[part].to_i
86
+ end
87
+ matches = new_matches
88
+
89
+ # We need to re-construct the duration string, because weeks and
90
+ # days are not supported by DateInterval in the same string.
91
+ duration = matches['week'].weeks +
92
+ matches['day'].days +
93
+ matches['hour'].hours +
94
+ matches['minute'].minutes +
95
+ matches['second'].seconds
96
+
97
+ duration = -duration if invert
98
+
99
+ return duration
100
+ end
101
+
102
+ parts = [
103
+ 'week',
104
+ 'day',
105
+ 'hour',
106
+ 'minute',
107
+ 'second'
108
+ ]
109
+
110
+ new_dur = ''
111
+
112
+ parts.each do |part|
113
+ new_dur += " #{matches[part]} #{part}s" if matches[part].to_i > 0
114
+ end
115
+
116
+ new_dur = (matches['plusminus'] == '-' ? '-' : '+') + new_dur.strip
117
+
118
+ new_dur = '+0 seconds' if new_dur == '+'
119
+
120
+ new_dur
121
+ end
122
+
123
+ # Parses either a Date or DateTime, or Duration value.
124
+ #
125
+ # @param string date
126
+ # @param DateTimeZone|string reference_tz
127
+ #
128
+ # @return DateTimeImmutable|DateInterval
129
+ def self.parse(date, reference_tz = nil)
130
+ if date[0] == 'P' || (date[0] == '-' && date[1] == 'P')
131
+ parse_duration(date)
132
+ elsif date.length == 8
133
+ parse_date(date, reference_tz)
134
+ else
135
+ parse_date_time(date, reference_tz)
136
+ end
137
+ end
138
+
139
+ # This method parses a vCard date and or time value.
140
+ #
141
+ # This can be used for the DATE, DATE-TIME, TIMESTAMP and
142
+ # DATE-AND-OR-TIME value.
143
+ #
144
+ # This method returns an array, not a DateTime value.
145
+ #
146
+ # The elements in the array are in the following order:
147
+ # year, month, date, hour, minute, second, timezone
148
+ #
149
+ # Almost any part of the string may be omitted. It's for example legal to
150
+ # just specify seconds, leave out the year, etc.
151
+ #
152
+ # Timezone is either returned as 'Z' or as '+0800'
153
+ #
154
+ # For any non-specified values null is returned.
155
+ #
156
+ # List of date formats that are supported:
157
+ # YYYY
158
+ # YYYY-MM
159
+ # YYYYMMDD
160
+ # --MMDD
161
+ # ---DD
162
+ #
163
+ # YYYY-MM-DD
164
+ # --MM-DD
165
+ # ---DD
166
+ #
167
+ # List of supported time formats:
168
+ #
169
+ # HH
170
+ # HHMM
171
+ # HHMMSS
172
+ # -MMSS
173
+ # --SS
174
+ #
175
+ # HH
176
+ # HH:MM
177
+ # HH:MM:SS
178
+ # -MM:SS
179
+ # --SS
180
+ #
181
+ # A full basic-format date-time string looks like :
182
+ # 20130603T133901
183
+ #
184
+ # A full extended-format date-time string looks like :
185
+ # 2013-06-03T13:39:01
186
+ #
187
+ # Times may be postfixed by a timezone offset. This can be either 'Z' for
188
+ # UTC, or a string like -0500 or +1100.
189
+ #
190
+ # @param string date
191
+ #
192
+ # @return array
193
+ def self.parse_v_card_date_time(date)
194
+ regex = /^
195
+ (?: # date part
196
+ (?:
197
+ (?: (?<year> [0-9]{4}) (?: -)?| --)
198
+ (?<month> [0-9]{2})?
199
+ |---)
200
+ (?<date> [0-9]{2})?
201
+ )?
202
+ (?:T # time part
203
+ (?<hour> [0-9]{2} | -)
204
+ (?<minute> [0-9]{2} | -)?
205
+ (?<second> [0-9]{2})?
206
+
207
+ (?: \.[0-9]{3})? # milliseconds
208
+ (?<timezone> # timezone offset
209
+
210
+ Z | (?: \+|-)(?: [0-9]{4})
211
+
212
+ )?
213
+
214
+ )?
215
+ $/x
216
+
217
+ matches = regex.match(date)
218
+ unless matches
219
+ # Attempting to parse the extended format.
220
+ regex = /^
221
+ (?: # date part
222
+ (?: (?<year> [0-9]{4}) - | -- )
223
+ (?<month> [0-9]{2}) -
224
+ (?<date> [0-9]{2})
225
+ )?
226
+ (?:T # time part
227
+
228
+ (?: (?<hour> [0-9]{2}) : | -)
229
+ (?: (?<minute> [0-9]{2}) : | -)?
230
+ (?<second> [0-9]{2})?
231
+
232
+ (?: \.[0-9]{3})? # milliseconds
233
+ (?<timezone> # timezone offset
234
+
235
+ Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
236
+
237
+ )?
238
+
239
+ )?
240
+ $/x
241
+
242
+ matches = regex.match(date)
243
+ unless matches
244
+ fail ArgumentError, "Invalid vCard date-time string: #{date}"
245
+ end
246
+ end
247
+
248
+ parts = [
249
+ 'year',
250
+ 'month',
251
+ 'date',
252
+ 'hour',
253
+ 'minute',
254
+ 'second',
255
+ 'timezone'
256
+ ]
257
+
258
+ result = {}
259
+ parts.each do |part|
260
+ if matches[part].blank?
261
+ result[part] = nil
262
+ elsif matches[part] == '-' || matches[part] == '--'
263
+ result[part] = nil
264
+ else
265
+ if part == 'timezone'
266
+ result[part] = matches[part]
267
+ else
268
+ result[part] = matches[part].to_i
269
+ end
270
+ end
271
+ end
272
+
273
+ result
274
+ end
275
+
276
+ # This method parses a vCard TIME value.
277
+ #
278
+ # This method returns an array, not a DateTime value.
279
+ #
280
+ # The elements in the array are in the following order:
281
+ # hour, minute, second, timezone
282
+ #
283
+ # Almost any part of the string may be omitted. It's for example legal to
284
+ # just specify seconds, leave out the hour etc.
285
+ #
286
+ # Timezone is either returned as 'Z' or as '+08:00'
287
+ #
288
+ # For any non-specified values null is returned.
289
+ #
290
+ # List of supported time formats:
291
+ #
292
+ # HH
293
+ # HHMM
294
+ # HHMMSS
295
+ # -MMSS
296
+ # --SS
297
+ #
298
+ # HH
299
+ # HH:MM
300
+ # HH:MM:SS
301
+ # -MM:SS
302
+ # --SS
303
+ #
304
+ # A full basic-format time string looks like :
305
+ # 133901
306
+ #
307
+ # A full extended-format time string looks like :
308
+ # 13:39:01
309
+ #
310
+ # Times may be postfixed by a timezone offset. This can be either 'Z' for
311
+ # UTC, or a string like -0500 or +11:00.
312
+ #
313
+ # @param string date
314
+ #
315
+ # @return array
316
+ def self.parse_v_card_time(date)
317
+ regex = /^
318
+ (?<hour> [0-9]{2} | -)
319
+ (?<minute> [0-9]{2} | -)?
320
+ (?<second> [0-9]{2})?
321
+
322
+ (?: \.[0-9]{3})? # milliseconds
323
+ (?<timezone> # timezone offset
324
+
325
+ Z | (?: \+|-)(?: [0-9]{4})
326
+
327
+ )?
328
+ $/x
329
+
330
+ matches = regex.match(date)
331
+ unless matches
332
+ # Attempting to parse the extended format.
333
+ regex = /^
334
+ (?: (?<hour> [0-9]{2}) : | -)
335
+ (?: (?<minute> [0-9]{2}) : | -)?
336
+ (?<second> [0-9]{2})?
337
+
338
+ (?: \.[0-9]{3})? # milliseconds
339
+ (?<timezone> # timezone offset
340
+
341
+ Z | (?: \+|-)(?: [0-9]{2}:[0-9]{2})
342
+
343
+ )?
344
+ $/x
345
+
346
+ matches = regex.match(date)
347
+ unless matches
348
+ fail ArgumentError, "Invalid vCard time string: #{date}"
349
+ end
350
+ end
351
+
352
+ parts = [
353
+ 'hour',
354
+ 'minute',
355
+ 'second',
356
+ 'timezone'
357
+ ]
358
+
359
+ result = {}
360
+ parts.each do |part|
361
+ if matches[part].blank?
362
+ result[part] = nil
363
+ elsif matches[part] == '-' || matches[part] == '--'
364
+ result[part] = nil
365
+ else
366
+ result[part] = matches[part]
367
+ end
368
+ end
369
+
370
+ result
371
+ end
372
+
373
+ # This method parses a vCard date and or time value.
374
+ #
375
+ # This can be used for the DATE, DATE-TIME and
376
+ # DATE-AND-OR-TIME value.
377
+ #
378
+ # This method returns an array, not a DateTime value.
379
+ # The elements in the array are in the following order:
380
+ # year, month, date, hour, minute, second, timezone
381
+ # Almost any part of the string may be omitted. It's for example legal to
382
+ # just specify seconds, leave out the year, etc.
383
+ #
384
+ # Timezone is either returned as 'Z' or as '+0800'
385
+ #
386
+ # For any non-specified values null is returned.
387
+ #
388
+ # List of date formats that are supported:
389
+ # 20150128
390
+ # 2015-01
391
+ # --01
392
+ # --0128
393
+ # ---28
394
+ #
395
+ # List of supported time formats:
396
+ # 13
397
+ # 1353
398
+ # 135301
399
+ # -53
400
+ # -5301
401
+ # --01 (unreachable, see the tests)
402
+ # --01Z
403
+ # --01+1234
404
+ #
405
+ # List of supported date-time formats:
406
+ # 20150128T13
407
+ # --0128T13
408
+ # ---28T13
409
+ # ---28T1353
410
+ # ---28T135301
411
+ # ---28T13Z
412
+ # ---28T13+1234
413
+ #
414
+ # See the regular expressions for all the possible patterns.
415
+ #
416
+ # Times may be postfixed by a timezone offset. This can be either 'Z' for
417
+ # UTC, or a string like -0500 or +1100.
418
+ #
419
+ # @param string date
420
+ #
421
+ # @return array
422
+ def self.parse_v_card_date_and_or_time(date)
423
+ # \d{8}|\d{4}-\d\d|--\d\d(\d\d)?|---\d\d
424
+ value_date = /^(?:
425
+ (?<year>\d{4})(?<month>\d\d)(?<date>\d\d)
426
+ |(?<year0>\d{4})-(?<month0>\d\d)
427
+ |--(?<month1>\d\d)(?<date0>\d\d)?
428
+ |---(?<date1>\d\d)
429
+ )$/x
430
+
431
+ # (\d\d(\d\d(\d\d)?)?|-\d\d(\d\d)?|--\d\d)(Z|[+\-]\d\d(\d\d)?)?
432
+ value_time = /^(?:
433
+ ((?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?
434
+ |-(?<minute0>\d\d)(?<second0>\d\d)?
435
+ |--(?<second1>\d\d))
436
+ (?<timezone>(Z|[+\-]\d\d(\d\d)?))?
437
+ )$/x
438
+
439
+ # (\d{8}|--\d{4}|---\d\d)T\d\d(\d\d(\d\d)?)?(Z|[+\-]\d\d(\d\d?)?
440
+ value_date_time = /^(?:
441
+ ((?<year>\d{4})(?<month>\d\d)(?<date>\d\d)
442
+ |--(?<month0>\d\d)(?<date0>\d\d)
443
+ |---(?<date1>\d\d))
444
+ T
445
+ (?<hour>\d\d)((?<minute>\d\d)(?<second>\d\d)?)?
446
+ (?<timezone>(Z|[+\-]\d\d(\d\d?)))?
447
+ )$/x
448
+
449
+ # date-and-or-time is date | date-time | time
450
+ # in this strict order.
451
+ matches = value_date.match(date)
452
+ matches = value_date_time.match(date) unless matches
453
+ matches = value_time.match(date) unless matches
454
+ unless matches
455
+ fail ArgumentError, "Invalid vCard date-time string: #{date}"
456
+ end
457
+
458
+ map = {
459
+ 'year' => 'year',
460
+ 'year0' => 'year',
461
+ 'month' => 'month',
462
+ 'month0' => 'month',
463
+ 'month1' => 'month',
464
+ 'date' => 'date',
465
+ 'date0' => 'date',
466
+ 'date1' => 'date',
467
+ 'hour' => 'hour',
468
+ 'minute' => 'minute',
469
+ 'minute0' => 'minute',
470
+ 'second' => 'second',
471
+ 'second0' => 'second',
472
+ 'second1' => 'second',
473
+ 'timezone' => 'timezone'
474
+ }
475
+
476
+ parts = {}
477
+ map.each do |key, real_key|
478
+ parts[real_key] ||= nil
479
+ parts[real_key] = matches[key] if matches.names.include?(key) && matches[key]
480
+ end
481
+
482
+ parts
483
+ end
484
+ end
485
+ end
486
+ end
@@ -0,0 +1,218 @@
1
+ module Tilia
2
+ module VObject
3
+ # Document.
4
+ #
5
+ # A document is just like a component, except that it's also the top level
6
+ # element.
7
+ #
8
+ # Both a VCALENDAR and a VCARD are considered documents.
9
+ #
10
+ # This class also provides a registry for document types.
11
+ class Document < Component
12
+ # Unknown document type.
13
+ UNKNOWN ||= 1
14
+
15
+ # vCalendar 1.0.
16
+ VCALENDAR10 ||= 2
17
+
18
+ # iCalendar 2.0.
19
+ ICALENDAR20 ||= 3
20
+
21
+ # vCard 2.1.
22
+ VCARD21 ||= 4
23
+
24
+ # vCard 3.0.
25
+ VCARD30 ||= 5
26
+
27
+ # vCard 4.0.
28
+ VCARD40 ||= 6
29
+
30
+ # The default name for this component.
31
+ #
32
+ # This should be 'VCALENDAR' or 'VCARD'.
33
+ #
34
+ # @var string
35
+ @default_name
36
+
37
+ # List of properties, and which classes they map to.
38
+ #
39
+ # @var array
40
+ @property_map = {}
41
+
42
+ # List of components, along with which classes they map to.
43
+ #
44
+ # @var array
45
+ @component_map = {}
46
+
47
+ # List of value-types, and which classes they map to.
48
+ #
49
+ # @var array
50
+ @value_map = {}
51
+
52
+ class << self
53
+ attr_accessor :default_name
54
+ attr_accessor :property_map
55
+ attr_accessor :component_map
56
+ attr_accessor :value_map
57
+ end
58
+
59
+ # Creates a new document.
60
+ #
61
+ # We're changing the default behavior slightly here. First, we don't want
62
+ # to have to specify a name (we already know it), and we want to allow
63
+ # children to be specified in the first argument.
64
+ #
65
+ # But, the default behavior also works.
66
+ #
67
+ # So the two sigs:
68
+ #
69
+ # new Document(array children = [], defaults = true)
70
+ # new Document(string name, array children = [], defaults = true)
71
+ #
72
+ # @return void
73
+ def initialize(*args)
74
+ if args.size == 0 || args[0].is_a?(Hash)
75
+ args.unshift(self.class.default_name)
76
+ args.unshift(self)
77
+
78
+ super(*args)
79
+ else
80
+ args.unshift(self)
81
+ super(*args)
82
+ end
83
+ end
84
+
85
+ # Returns the current document type.
86
+ #
87
+ # @return int
88
+ def document_type
89
+ self.class::UNKNOWN
90
+ end
91
+
92
+ # Creates a new component or property.
93
+ #
94
+ # If it's a known component, we will automatically call createComponent.
95
+ # otherwise, we'll assume it's a property and call createProperty instead.
96
+ #
97
+ # @param string name
98
+ # @param string arg1,... Unlimited number of args
99
+ #
100
+ # @return mixed
101
+ def create(name, *args)
102
+ if self.class.component_map.key?(name.upcase)
103
+ create_component(name, *args)
104
+ else
105
+ create_property(name, *args)
106
+ end
107
+ end
108
+
109
+ # Creates a new component.
110
+ #
111
+ # This method automatically searches for the correct component class, based
112
+ # on its name.
113
+ #
114
+ # You can specify the children either in key=>value syntax, in which case
115
+ # properties will automatically be created, or you can just pass a list of
116
+ # Component and Property object.
117
+ #
118
+ # By default, a set of sensible values will be added to the component. For
119
+ # an iCalendar object, this may be something like CALSCALE:GREGORIAN. To
120
+ # ensure that this does not happen, set defaults to false.
121
+ #
122
+ # @param string name
123
+ # @param array children
124
+ # @param bool defaults
125
+ #
126
+ # @return Component
127
+ def create_component(name, children = nil, defaults = true)
128
+ name = name.upcase
129
+
130
+ klass = Component
131
+
132
+ klass = self.class.component_map[name] if self.class.component_map.key?(name)
133
+
134
+ children = [] unless children
135
+ klass.new(self, name, children, defaults)
136
+ end
137
+
138
+ # Factory method for creating new properties.
139
+ #
140
+ # This method automatically searches for the correct property class, based
141
+ # on its name.
142
+ #
143
+ # You can specify the parameters either in key=>value syntax, in which case
144
+ # parameters will automatically be created, or you can just pass a list of
145
+ # Parameter objects.
146
+ #
147
+ # @param string name
148
+ # @param mixed value
149
+ # @param array parameters
150
+ # @param string value_type Force a specific valuetype, such as URI or TEXT
151
+ #
152
+ # @return Property
153
+ def create_property(name, value = nil, parameters = nil, value_type = nil)
154
+ parameters = {} unless parameters
155
+
156
+ # If there's a . in the name, it means it's prefixed by a groupname.
157
+ i = name.index('.')
158
+ if i
159
+ group = name[0...i]
160
+ name = name[i + 1..-1].upcase
161
+ else
162
+ name = name.upcase
163
+ group = nil
164
+ end
165
+
166
+ klass = nil
167
+
168
+ if value_type
169
+ # The valueType argument comes first to figure out the correct
170
+ # class.
171
+ klass = class_name_for_property_value(value_type)
172
+ end
173
+
174
+ unless klass
175
+ # If a VALUE parameter is supplied, we should use that.
176
+ if parameters.key?('VALUE')
177
+ klass = class_name_for_property_value(parameters['VALUE'])
178
+ else
179
+ klass = class_name_for_property_name(name)
180
+ end
181
+ end
182
+
183
+ klass.new(self, name, value, parameters, group)
184
+ end
185
+
186
+ # This method returns a full class-name for a value parameter.
187
+ #
188
+ # For instance, DTSTART may have VALUE=DATE. In that case we will look in
189
+ # our valueMap table and return the appropriate class name.
190
+ #
191
+ # This method returns null if we don't have a specialized class.
192
+ #
193
+ # @param string value_param
194
+ #
195
+ # @return void
196
+ def class_name_for_property_value(value_param)
197
+ value_param = value_param.upcase
198
+
199
+ return self.class.value_map[value_param] if self.class.value_map.key?(value_param)
200
+
201
+ nil
202
+ end
203
+
204
+ # Returns the default class for a property name.
205
+ #
206
+ # @param string property_name
207
+ #
208
+ # @return string
209
+ def class_name_for_property_name(property_name)
210
+ if self.class.property_map.key?(property_name)
211
+ self.class.property_map[property_name]
212
+ else
213
+ Property::Unknown
214
+ end
215
+ end
216
+ end
217
+ end
218
+ end
@@ -0,0 +1,18 @@
1
+ module Tilia
2
+ module VObject
3
+ # VObject ElementList.
4
+ #
5
+ # This class represents a list of elements. Lists are the result of queries,
6
+ # such as doing vcalendar.vevent where there's multiple VEVENT objects.
7
+ class ElementList < Array
8
+ def initialize(*args)
9
+ super(*args)
10
+ freeze
11
+ end
12
+
13
+ def delete(_offset)
14
+ fail 'RuntimeError: can\'t modify frozen Array'
15
+ end
16
+ end
17
+ end
18
+ end