icalendar 0.95 → 0.96

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 (64) hide show
  1. data/README +9 -4
  2. data/Rakefile +4 -3
  3. data/docs/examples/parse_cal.rb +1 -1
  4. data/lib/icalendar/calendar.rb +15 -51
  5. data/lib/icalendar/component.rb +75 -21
  6. data/lib/icalendar/component/event.rb +60 -11
  7. data/lib/icalendar/component/timezone.rb +31 -7
  8. data/lib/icalendar/component/todo.rb +1 -1
  9. data/lib/icalendar/conversions.rb +3 -1
  10. data/lib/icalendar/foo.rb +394 -0
  11. data/lib/icalendar/parser.rb +29 -22
  12. data/test/calendar_test.rb +37 -37
  13. data/test/component/event_test.rb +1 -1
  14. data/test/component_test.rb +56 -56
  15. data/test/interactive.rb +15 -0
  16. data/test/life.ics +45 -0
  17. data/test/parameter_test.rb +20 -0
  18. metadata +23 -70
  19. data/docs/api/classes/Array.html +0 -146
  20. data/docs/api/classes/Date.html +0 -157
  21. data/docs/api/classes/DateTime.html +0 -178
  22. data/docs/api/classes/Fixnum.html +0 -146
  23. data/docs/api/classes/Float.html +0 -146
  24. data/docs/api/classes/Icalendar/Alarm.html +0 -184
  25. data/docs/api/classes/Icalendar/Base.html +0 -118
  26. data/docs/api/classes/Icalendar/Calendar.html +0 -411
  27. data/docs/api/classes/Icalendar/Component.html +0 -306
  28. data/docs/api/classes/Icalendar/DateProp.html +0 -187
  29. data/docs/api/classes/Icalendar/DateProp/ClassMethods.html +0 -195
  30. data/docs/api/classes/Icalendar/Event.html +0 -202
  31. data/docs/api/classes/Icalendar/Freebusy.html +0 -157
  32. data/docs/api/classes/Icalendar/InvalidComponentClass.html +0 -117
  33. data/docs/api/classes/Icalendar/InvalidPropertyValue.html +0 -117
  34. data/docs/api/classes/Icalendar/Journal.html +0 -190
  35. data/docs/api/classes/Icalendar/Parameter.html +0 -166
  36. data/docs/api/classes/Icalendar/Parser.html +0 -447
  37. data/docs/api/classes/Icalendar/Timezone.html +0 -197
  38. data/docs/api/classes/Icalendar/Todo.html +0 -199
  39. data/docs/api/classes/String.html +0 -160
  40. data/docs/api/classes/Time.html +0 -161
  41. data/docs/api/created.rid +0 -1
  42. data/docs/api/files/COPYING.html +0 -163
  43. data/docs/api/files/GPL.html +0 -531
  44. data/docs/api/files/README.html +0 -241
  45. data/docs/api/files/lib/icalendar/base_rb.html +0 -108
  46. data/docs/api/files/lib/icalendar/calendar_rb.html +0 -101
  47. data/docs/api/files/lib/icalendar/component/alarm_rb.html +0 -101
  48. data/docs/api/files/lib/icalendar/component/event_rb.html +0 -101
  49. data/docs/api/files/lib/icalendar/component/freebusy_rb.html +0 -101
  50. data/docs/api/files/lib/icalendar/component/journal_rb.html +0 -101
  51. data/docs/api/files/lib/icalendar/component/timezone_rb.html +0 -101
  52. data/docs/api/files/lib/icalendar/component/todo_rb.html +0 -101
  53. data/docs/api/files/lib/icalendar/component_rb.html +0 -101
  54. data/docs/api/files/lib/icalendar/conversions_rb.html +0 -108
  55. data/docs/api/files/lib/icalendar/helpers_rb.html +0 -101
  56. data/docs/api/files/lib/icalendar/parameter_rb.html +0 -101
  57. data/docs/api/files/lib/icalendar/parser_rb.html +0 -109
  58. data/docs/api/files/lib/icalendar_rb.html +0 -118
  59. data/docs/api/fr_class_index.html +0 -48
  60. data/docs/api/fr_file_index.html +0 -43
  61. data/docs/api/fr_method_index.html +0 -63
  62. data/docs/api/index.html +0 -24
  63. data/docs/api/rdoc-style.css +0 -208
  64. data/lib/icalendar/#helpers.rb# +0 -92
@@ -20,24 +20,48 @@ module Icalendar
20
20
  # New York City starting from 1967. Each line represents a description
21
21
  # or rule for a particular observance.
22
22
  class Timezone < Component
23
-
23
+ ical_component :standard, :daylight
24
+
24
25
  # Single properties
25
26
  ical_property :dtstart, :start
26
27
  ical_property :tzoffsetto, :timezone_offset_to
27
28
  ical_property :tzoffsetfrom, :timezone_offset_from
28
-
29
+
29
30
  # Multi-properties
30
31
  ical_multi_property :comment, :comment, :comments
31
32
  ical_multi_property :rdate, :recurrence_date, :recurrence_dates
32
33
  ical_multi_property :rrule, :recurrence_rule, :recurrence_rules
33
-
34
+
35
+ # Define a custom add component method because standard and daylight
36
+ # are the only components that can occur just once with their parent.
37
+ def add_component(component)
38
+ key = component.class.to_s.downcase.gsub('icalendar::','').to_sym
39
+ @components[key] = component
40
+ end
41
+
42
+ def initialize(name = "VTIMEZONE")
43
+ super(name)
44
+ end
45
+
46
+ end
47
+
48
+ # A Standard component is a sub-component of the Timezone component which
49
+ # is used to describe the standard time offset.
50
+ class Standard < Timezone
51
+
34
52
  def initialize()
35
- super("VTIMEZONE")
36
- @components = components
53
+ super("STANDARD")
37
54
  end
55
+ end
38
56
 
39
- def to_ical
40
- super.to_s { |s| s << @components.each { |component| component.to_s } }
57
+ # A Daylight component is a sub-component of the Timezone component which
58
+ # is used to describe the time offset for what is commonly known as
59
+ # daylight savings time.
60
+ class Daylight < Timezone
61
+
62
+ def initialize()
63
+ super("DAYLIGHT")
41
64
  end
42
65
  end
66
+
43
67
  end
@@ -12,7 +12,7 @@ module Icalendar
12
12
  # represent an item of work assigned to an individual; such as "turn in
13
13
  # travel expense today".
14
14
  class Todo < Component
15
- attr_reader :alarms
15
+ ical_component :alarms
16
16
 
17
17
  # Single properties
18
18
  ical_property :klass, nil, :class
@@ -18,7 +18,7 @@ require 'date'
18
18
 
19
19
  class String
20
20
  def to_ical
21
- "#{self}"
21
+ "#{self.dump[1...-1]}"
22
22
  end
23
23
  end
24
24
 
@@ -73,6 +73,8 @@ class DateTime < Date
73
73
  if utc
74
74
  s << "Z"
75
75
  end
76
+
77
+ s
76
78
  end
77
79
  end
78
80
 
@@ -0,0 +1,394 @@
1
+ =begin
2
+ Copyright (C) 2005 Jeff Rose
3
+ Copyright (C) 2005 Sam Roberts
4
+
5
+ This library is free software; you can redistribute it and/or modify it
6
+ under the same terms as the ruby language itself, see the file COPYING for
7
+ details.
8
+ =end
9
+
10
+ require 'date'
11
+ require 'uri'
12
+
13
+ module Icalendar
14
+ class Parser < Icalendar::Base
15
+ # 1*(ALPHA / DIGIT / "=")
16
+ NAME = '[-a-z0-9]+'
17
+
18
+ # <"> <Any character except CTLs, DQUOTE> <">
19
+ QSTR = '"[^"]*"'
20
+
21
+ # *<Any character except CTLs, DQUOTE, ";", ":", ",">
22
+ PTEXT = '[^";:,]*'
23
+
24
+ # param-value = ptext / quoted-string
25
+ PVALUE = "#{PTEXT}|#{QSTR}"
26
+
27
+ # Contentline
28
+ LINE = "(#{NAME})([^:]*)\:(.*)"
29
+
30
+ # param = name "=" param-value *("," param-value)
31
+ PARAM = ";(#{NAME})(=?)((?:#{PVALUE})(?:,#{PVALUE})*)"
32
+
33
+ # date = date-fullyear ["-"] date-month ["-"] date-mday
34
+ # date-fullyear = 4 DIGIT
35
+ # date-month = 2 DIGIT
36
+ # date-mday = 2 DIGIT
37
+ DATE = '(\d\d\d\d)-?(\d\d)-?(\d\d)'
38
+
39
+ # time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone]
40
+ # time-hour = 2 DIGIT
41
+ # time-minute = 2 DIGIT
42
+ # time-second = 2 DIGIT
43
+ # time-secfrac = "," 1*DIGIT
44
+ # time-zone = "Z" / time-numzone
45
+ # time-numzome = sign time-hour [":"] time-minute
46
+ TIME = '(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?'
47
+
48
+ def initialize(src)
49
+ @@logger.debug("New Calendar Parser")
50
+
51
+ # Setup the parser method hash table
52
+ setup_parsers()
53
+
54
+ # Define the next line method different depending on whether
55
+ # this is a string or an IO object so we can be efficient about
56
+ # parsing large files...
57
+
58
+ # Just do the unfolding work in one shot if its a whole string
59
+ if src.respond_to?(:split)
60
+ @lines = []
61
+ @index = 0
62
+
63
+ puts "String here!!!!!!!!!!!!!!!"
64
+ # Split into an array of lines, then unfold those into a new array
65
+ src.split(/\r?\n/).each {|line| unfold(@lines, line) }
66
+
67
+ # Now that we are unfolded we can just iterate through the array.
68
+ # Dynamically define next line for a string.
69
+ def next_line
70
+ if @index == @lines.size
71
+ return nil
72
+ else
73
+ line = @lines[@index]
74
+ @index += 1
75
+ return line
76
+ end
77
+ end
78
+
79
+ # If its a file we need to read and unfold on the go to save from reading
80
+ # large amounts of data into memory.
81
+ elsif src.respond_to?(:gets)
82
+ @file = src
83
+ @prev_line = src.gets
84
+
85
+ # Dynamically define next line for an IO object
86
+ def next_line
87
+ line = @prev_line
88
+
89
+ # Loop through until we get to a non-continuation line...
90
+ loop do
91
+ nextLine = @file.gets
92
+
93
+ unfold(@prev_line, nextLine)
94
+ # If it's a continuation line, add it to the last.
95
+ # If it's an empty line, drop it from the input.
96
+ if( nextLine =~ /^[ \t]/ )
97
+ line << nextLine[1, nextLine.size]
98
+ elsif( nextLine =~ /^$/ )
99
+ else
100
+ @prev_line = nextLine
101
+ break
102
+ end
103
+ end
104
+ line
105
+ end
106
+ else
107
+ raise ArgumentError, "CalendarParser.new cannot be called with a #{src.class} type!"
108
+ end
109
+ end
110
+
111
+ # Unfold lines of an icalendar file to comply with specification.
112
+ def unfold(last, line)
113
+
114
+ # Get rid of newlines
115
+ unless nextLine.nil?
116
+ nextLine.chomp!
117
+ end
118
+
119
+ # If it's a continuation line, add it to the last.
120
+ # If it's an empty line, drop it from the input.
121
+ if( line =~ /^[ \t]/ )
122
+ last << last.pop + line[1, line.size-1]
123
+ elsif( line =~ /^$/ )
124
+ else
125
+ last << line
126
+ end
127
+ end
128
+
129
+ # Parse the calendar into an object representation
130
+ def parse
131
+ calendars = []
132
+
133
+ # Outer loop for Calendar objects
134
+ #@src.each(/\r?\n/) do |line|
135
+ while(line = next_line)
136
+ fields = parse_line(line)
137
+
138
+ # Just iterate through until we find the beginning of a calendar object
139
+ if fields[:name] == "BEGIN" and fields[:value] == "VCALENDAR"
140
+ cal = parse_component
141
+ calendars << cal
142
+ end
143
+ end
144
+
145
+ calendars
146
+ end
147
+
148
+ private
149
+
150
+ # Parse a single VCALENDAR object
151
+ # -- This should consist of the PRODID, VERSION, option METHOD & CALSCALE,
152
+ # and then one or more calendar components: VEVENT, VTODO, VJOURNAL,
153
+ # VFREEBUSY, VTIMEZONE
154
+ def parse_component(component = Calendar.new)
155
+ while (line = next_line)
156
+ fields = parse_line(line)
157
+
158
+ name = fields[:name]
159
+
160
+ # Although properties are supposed to come before components, we should
161
+ # be able to handle them in any order...
162
+ if name == "END"
163
+ break
164
+ elsif name == "BEGIN" # New component
165
+ case(fields[:value])
166
+ when "VEVENT"
167
+ component.events << parse_component(Event.new)
168
+ when "VTODO"
169
+ component.todos << parse_component(Todo.new)
170
+ when "VJOURNAL"
171
+ component.journals << parse_component(Journal.new)
172
+ when "VFREEBUSY"
173
+ component.freebusys << parse_component(Freebusy.new)
174
+ when "VTIMEZONE"
175
+ component.timezones << parse_component(Timezone.new)
176
+ when "VALARM"
177
+ component.alarms << parse_component(Alarm.new)
178
+ end
179
+ else # If its not a component then it should be a property
180
+
181
+ # Just set the properties so that the parser can still
182
+ # parse invalid files...
183
+ @@logger.debug("Setting #{name} => #{fields[:value]}")
184
+
185
+ # Lookup the property name to see if we have a string to
186
+ # object parser for this property type.
187
+ if @parsers.has_key?(name.upcase)
188
+ val = @parsers[name.upcase].call(name, fields[:params], fields[:value])
189
+ else
190
+ val = fields[:value]
191
+ end
192
+
193
+ if component.multi_property?(name.upcase) && component.properties
194
+ val = [val]
195
+
196
+ if fields[:params].empty?
197
+ params = [nil]
198
+ else
199
+ params = fields[:params]
200
+ end
201
+
202
+ if component.properties.has_key?(name)
203
+ component.properties[name] += val
204
+ component.property_params[name] += params
205
+ else
206
+ component.properties[name] = val
207
+ component.property_params[name] = params
208
+ end
209
+
210
+ else
211
+ component.properties[name] = val
212
+
213
+ unless fields[:params].empty?
214
+ component.property_params[name] = fields[:params]
215
+ end
216
+ end
217
+ end
218
+ end
219
+
220
+ component
221
+
222
+ end
223
+
224
+ def parse_line(line)
225
+ unless line =~ %r{#{LINE}}i # Case insensitive match for a valid line
226
+ raise "Invalid line in calendar string!"
227
+ end
228
+
229
+ name = $1.upcase # The case insensitive part is upcased for easier comparison...
230
+ paramslist = $2
231
+ value = $3
232
+
233
+ params = {}
234
+
235
+ # Collect the params, if any.
236
+ if paramslist.size > 1
237
+
238
+ # v3.0 and v2.1 params
239
+ paramslist.scan( %r{#{PARAM}}i ) do
240
+
241
+ # param names are case-insensitive, and multi-valued
242
+ pname = $1
243
+ pvals = $3
244
+
245
+ # v2.1 pvals have no '=' sign, figure out what kind of param it
246
+ # is (either its a known encoding, or we treat it as a 'type'
247
+ # param).
248
+ if $2 == ""
249
+ pvals = $1
250
+ case $1
251
+ when /quoted-printable/i
252
+ pname = 'encoding'
253
+
254
+ when /base64/i
255
+ pname = 'encoding'
256
+
257
+ else
258
+ pname = 'type'
259
+ end
260
+ end
261
+
262
+ unless params.key? pname
263
+ params[pname] = []
264
+ end
265
+ pvals.scan( %r{(#{PVALUE})} ) do
266
+ if $1.size > 0
267
+ params[pname] << $1
268
+ end
269
+ end
270
+ end
271
+ end
272
+
273
+ {:name => name, :params => params, :value => value}
274
+ end
275
+
276
+ ## Following is a collection of parsing functions for various
277
+ ## icalendar property value data types... First we setup
278
+ ## a hash with property names pointing to methods...
279
+ def setup_parsers
280
+ @parsers = {}
281
+
282
+ # Integer properties
283
+ m = self.method(:parse_integer)
284
+ @parsers["PERCENT-COMPLETE"] = m
285
+ @parsers["PRIORITY"] = m
286
+ @parsers["REPEAT"] = m
287
+ @parsers["SEQUENCE"] = m
288
+
289
+ # Dates and Times
290
+ m = self.method(:parse_datetime)
291
+ @parsers["COMPLETED"] = m
292
+ @parsers["DTEND"] = m
293
+ @parsers["DUE"] = m
294
+ @parsers["DTSTART"] = m
295
+ @parsers["RECURRENCE-ID"] = m
296
+ @parsers["EXDATE"] = m
297
+ @parsers["RDATE"] = m
298
+ @parsers["CREATED"] = m
299
+ @parsers["DTSTAMP"] = m
300
+ @parsers["LAST-MODIFIED"] = m
301
+
302
+ # URI's
303
+ m = self.method(:parse_uri)
304
+ @parsers["TZURL"] = m
305
+ @parsers["ATTENDEE"] = m
306
+ @parsers["ORGANIZER"] = m
307
+ @parsers["URL"] = m
308
+
309
+ # This is a URI by default, and if its not a valid URI
310
+ # it will be returned as a string which works for binary data
311
+ # the other possible type.
312
+ @parsers["ATTACH"] = m
313
+
314
+ # GEO
315
+ m = self.method(:parse_geo)
316
+ @parsers["GEO"] = m
317
+
318
+ end
319
+
320
+ # Booleans
321
+ # NOTE: It appears that although this is a valid data type
322
+ # there aren't any properties that use it... Maybe get
323
+ # rid of this in the future.
324
+ def parse_boolean(name, params, value)
325
+ if value.upcase == "FALSE"
326
+ false
327
+ else
328
+ true
329
+ end
330
+ end
331
+
332
+ # Dates, Date-Times & Times
333
+ # NOTE: invalid dates & times will be returned as strings...
334
+ def parse_datetime(name, params, value)
335
+ begin
336
+ DateTime.parse(value)
337
+ rescue Exception
338
+ value
339
+ end
340
+
341
+ end
342
+
343
+ # Durations
344
+ # TODO: Need to figure out the best way to represent durations
345
+ # so just returning string for now.
346
+ def parse_duration(name, params, value)
347
+ value
348
+ end
349
+
350
+ # Floats
351
+ # NOTE: returns 0.0 if it can't parse the value
352
+ def parse_float(name, params, value)
353
+ value.to_f
354
+ end
355
+
356
+ # Integers
357
+ # NOTE: returns 0 if it can't parse the value
358
+ def parse_integer(name, params, value)
359
+ value.to_i
360
+ end
361
+
362
+ # Periods
363
+ # TODO: Got to figure out how to represent periods also...
364
+ def parse_period(name, params, value)
365
+ value
366
+ end
367
+
368
+ # Calendar Address's & URI's
369
+ # NOTE: invalid URI's will be returned as strings...
370
+ def parse_uri(name, params, value)
371
+ begin
372
+ URI.parse(value)
373
+ rescue Exception
374
+ value
375
+ end
376
+ end
377
+
378
+ # Geographical location (GEO)
379
+ # NOTE: returns an array with two floats (long & lat)
380
+ # if the parsing fails return the string
381
+ def parse_geo(name, params, value)
382
+ strloc = value.split(';')
383
+ if strloc.size != 2
384
+ return value
385
+ end
386
+
387
+ val = []
388
+ val[0] = strloc[0].to_f
389
+ val[1] = strloc[1].to_f
390
+ val
391
+ end
392
+
393
+ end
394
+ end