sdague-icalendar 1.0.2.1

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 (47) hide show
  1. data/COPYING +56 -0
  2. data/GPL +340 -0
  3. data/README +263 -0
  4. data/Rakefile +110 -0
  5. data/docs/api/STUB +0 -0
  6. data/docs/rfcs/itip_notes.txt +69 -0
  7. data/docs/rfcs/rfc2425.pdf +0 -0
  8. data/docs/rfcs/rfc2426.pdf +0 -0
  9. data/docs/rfcs/rfc2445.pdf +0 -0
  10. data/docs/rfcs/rfc2446.pdf +0 -0
  11. data/docs/rfcs/rfc2447.pdf +0 -0
  12. data/docs/rfcs/rfc3283.txt +738 -0
  13. data/examples/create_cal.rb +45 -0
  14. data/examples/parse_cal.rb +20 -0
  15. data/examples/single_event.ics +18 -0
  16. data/lib/hash_attrs.rb +34 -0
  17. data/lib/icalendar.rb +36 -0
  18. data/lib/icalendar/base.rb +43 -0
  19. data/lib/icalendar/calendar.rb +111 -0
  20. data/lib/icalendar/component.rb +438 -0
  21. data/lib/icalendar/component/alarm.rb +44 -0
  22. data/lib/icalendar/component/event.rb +129 -0
  23. data/lib/icalendar/component/freebusy.rb +37 -0
  24. data/lib/icalendar/component/journal.rb +61 -0
  25. data/lib/icalendar/component/timezone.rb +105 -0
  26. data/lib/icalendar/component/todo.rb +64 -0
  27. data/lib/icalendar/conversions.rb +144 -0
  28. data/lib/icalendar/helpers.rb +109 -0
  29. data/lib/icalendar/parameter.rb +33 -0
  30. data/lib/icalendar/parser.rb +485 -0
  31. data/lib/meta.rb +32 -0
  32. data/test/calendar_test.rb +71 -0
  33. data/test/component/event_test.rb +220 -0
  34. data/test/component/timezone_test.rb +67 -0
  35. data/test/component/todo_test.rb +13 -0
  36. data/test/component_test.rb +66 -0
  37. data/test/conversions_test.rb +97 -0
  38. data/test/coverage/STUB +0 -0
  39. data/test/fixtures/folding.ics +23 -0
  40. data/test/fixtures/life.ics +46 -0
  41. data/test/fixtures/simplecal.ics +119 -0
  42. data/test/fixtures/single_event.ics +23 -0
  43. data/test/interactive.rb +17 -0
  44. data/test/parameter_test.rb +29 -0
  45. data/test/parser_test.rb +84 -0
  46. data/test/read_write.rb +23 -0
  47. metadata +108 -0
@@ -0,0 +1,144 @@
1
+ =begin
2
+ Copyright (C) 2005 Jeff Rose
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'date'
10
+
11
+ ### Add some to_ical methods to classes
12
+
13
+ # class Object
14
+ # def to_ical
15
+ # raise(NotImplementedError, "This object does not implement the to_ical method!")
16
+ # end
17
+ # end
18
+
19
+ module Icalendar
20
+ module TzidSupport
21
+ attr_accessor :icalendar_tzid
22
+ end
23
+ end
24
+
25
+ require 'uri/generic'
26
+
27
+ class String
28
+ def to_ical
29
+ self
30
+ end
31
+ end
32
+
33
+ class Fixnum
34
+ def to_ical
35
+ "#{self}"
36
+ end
37
+ end
38
+
39
+ class Float
40
+ def to_ical
41
+ "#{self}"
42
+ end
43
+ end
44
+
45
+ # From the spec: "Values in a list of values MUST be separated by a COMMA
46
+ # character (US-ASCII decimal 44)."
47
+ class Array
48
+ def to_ical
49
+ map{|elem| elem.to_ical}.join ','
50
+ end
51
+ end
52
+
53
+ module URI
54
+ class Generic
55
+ def to_ical
56
+ "#{self}"
57
+ end
58
+ end
59
+ end
60
+
61
+ class DateTime < Date
62
+ attr_accessor :ical_params
63
+ include Icalendar::TzidSupport
64
+
65
+ def to_ical
66
+ s = ""
67
+
68
+ # 4 digit year
69
+ s << self.year.to_s
70
+
71
+ # Double digit month
72
+ s << "0" unless self.month > 9
73
+ s << self.month.to_s
74
+
75
+ # Double digit day
76
+ s << "0" unless self.day > 9
77
+ s << self.day.to_s
78
+
79
+ s << "T"
80
+
81
+ # Double digit hour
82
+ s << "0" unless self.hour > 9
83
+ s << self.hour.to_s
84
+
85
+ # Double digit minute
86
+ s << "0" unless self.min > 9
87
+ s << self.min.to_s
88
+
89
+ # Double digit second
90
+ s << "0" unless self.sec > 9
91
+ s << self.sec.to_s
92
+
93
+ # UTC time gets a Z suffix
94
+ if icalendar_tzid == "UTC"
95
+ s << "Z"
96
+ end
97
+
98
+ s
99
+ end
100
+ end
101
+
102
+ class Date
103
+ attr_accessor :ical_params
104
+ def to_ical(utc = false)
105
+ s = ""
106
+
107
+ # 4 digit year
108
+ s << self.year.to_s
109
+
110
+ # Double digit month
111
+ s << "0" unless self.month > 9
112
+ s << self.month.to_s
113
+
114
+ # Double digit day
115
+ s << "0" unless self.day > 9
116
+ s << self.day.to_s
117
+ end
118
+ end
119
+
120
+ class Time
121
+ attr_accessor :ical_params
122
+ def to_ical(utc = false)
123
+ s = ""
124
+
125
+ # Double digit hour
126
+ s << "0" unless self.hour > 9
127
+ s << self.hour.to_s
128
+
129
+ # Double digit minute
130
+ s << "0" unless self.min > 9
131
+ s << self.min.to_s
132
+
133
+ # Double digit second
134
+ s << "0" unless self.sec > 9
135
+ s << self.sec.to_s
136
+
137
+ # UTC time gets a Z suffix
138
+ if utc
139
+ s << "Z"
140
+ end
141
+
142
+ s
143
+ end
144
+ end
@@ -0,0 +1,109 @@
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
+ module Icalendar
11
+ module DateProp
12
+ # date = date-fullyear date-month date-mday
13
+ # date-fullyear = 4 DIGIT
14
+ # date-month = 2 DIGIT
15
+ # date-mday = 2 DIGIT
16
+ DATE = '(\d\d\d\d)(\d\d)(\d\d)'
17
+
18
+ # time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone]
19
+ # time-hour = 2 DIGIT
20
+ # time-minute = 2 DIGIT
21
+ # time-second = 2 DIGIT
22
+ # time-secfrac = "," 1*DIGIT
23
+ # time-zone = "Z" / time-numzone
24
+ # time-numzome = sign time-hour [":"] time-minute
25
+ # TIME = '(\d\d)(\d\d)(\d\d)(Z)?'
26
+ TIME = '(\d\d)(\d\d)(\d\d)'
27
+
28
+ # This method is called automatically when the module is mixed in.
29
+ # I guess you have to do this to mixin class methods rather than instance methods.
30
+ def self.append_features(base)
31
+ super
32
+ klass.extend(ClassMethods)
33
+ end
34
+
35
+ # This is made a sub-module just so it can be added as class
36
+ # methods rather than instance methods.
37
+ module ClassMethods
38
+ def date_property(dp, alias_name = nil)
39
+ dp = "#{dp}".strip.downcase
40
+ getter = dp
41
+ setter = "#{dp}="
42
+ query = "#{dp}?"
43
+
44
+ unless instance_methods.include? getter
45
+ code = <<-code
46
+ def #{getter}(*a)
47
+ if a.empty?
48
+ @properties[#{dp.upcase}]
49
+ else
50
+ self.#{dp} = a.first
51
+ end
52
+ end
53
+ code
54
+
55
+ module_eval code
56
+ end
57
+
58
+ unless instance_methods.include? setter
59
+ code = <<-code
60
+ def #{setter} a
61
+ @properties[#{dp.upcase}] = a
62
+ end
63
+ code
64
+
65
+ module_eval code
66
+ end
67
+
68
+ unless instance_methods.include? query
69
+ code = <<-code
70
+ def #{query}
71
+ @properties.has_key?(#{dp.upcase})
72
+ end
73
+ code
74
+
75
+ module_eval code
76
+ end
77
+
78
+ # Define the getter
79
+ getter = "get#{property.to_s.capitalize}"
80
+ define_method(getter.to_sym) do
81
+ puts "inside getting..."
82
+ getDateProperty(property.to_s.upcase)
83
+ end
84
+
85
+ # Define the setter
86
+ setter = "set#{property.to_s.capitalize}"
87
+ define_method(setter.to_sym) do |*params|
88
+ date = params[0]
89
+ utc = params[1]
90
+ puts "inside setting..."
91
+ setDateProperty(property.to_s.upcase, date, utc)
92
+ end
93
+
94
+ # Create aliases if a name was specified
95
+ # if not aliasName.nil?
96
+ # gasym = "get#{aliasName.to_s.capitalize}".to_sym
97
+ # gsym = getter.to_sym
98
+ # alias gasym gsym
99
+
100
+ # sasym = "set#{aliasName.to_s.capitalize}".to_sym
101
+ # ssym = setter.to_sym
102
+ # alias sasym ssym
103
+ # end
104
+ end
105
+
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,33 @@
1
+ =begin
2
+ Copyright (C) 2005 Jeff Rose
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ module Icalendar
10
+
11
+ # A property can have attributes associated with it. These "property
12
+ # parameters" contain meta-information about the property or the
13
+ # property value. Property parameters are provided to specify such
14
+ # information as the location of an alternate text representation for a
15
+ # property value, the language of a text property value, the data type
16
+ # of the property value and other attributes.
17
+ class Parameter < Icalendar::Content
18
+
19
+ def to_s
20
+ s = ""
21
+
22
+ s << "#{@name}="
23
+ if is_escapable?
24
+ s << escape(print_value())
25
+ else
26
+ s << print_value
27
+ end
28
+
29
+ s
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,485 @@
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
+ require 'stringio'
13
+
14
+ module Icalendar
15
+ class RRule
16
+
17
+ class Weekday
18
+ def initialize(day, position)
19
+ @day, @position = day, position
20
+ end
21
+
22
+ def to_s
23
+ "#{@position}#{day}"
24
+ end
25
+ end
26
+
27
+ def initialize(name, params, value, parser)
28
+ frequency_match = value.match(/FREQ=(SECONDLY|MINUTELY|HOURLY|DAILY|WEEKLY|MONTHLY|YEARLY)/)
29
+ raise Icalendar::InvalidPropertyValue.new("FREQ must be specified for RRULE values") unless frequency_match
30
+ @frequency = frequency_match[1]
31
+ @until = parse_date_val("UNTIL", value)
32
+ @count = parse_int_val("COUNT", value)
33
+ raise Icalendar::InvalidPropertyValue.new("UNTIL and COUNT must not both be specified for RRULE values") if [@until, @count].compact.length > 1
34
+ @interval = parse_int_val("INTERVAL", value)
35
+ @by_list = {:bysecond => parse_int_list("BYSECOND", value)}
36
+ @by_list[:byminute] = parse_int_list("BYMINUTE",value)
37
+ @by_list[:byhour] = parse_int_list("BYHOUR", value)
38
+ @by_list[:byday] = parse_weekday_list("BYDAY", value)
39
+ @by_list[:bymonthday] = parse_int_list("BYMONTHDAY", value)
40
+ @by_list[:byyearday] = parse_int_list("BYYEARDAY", value)
41
+ @by_list[:byweekno] = parse_int_list("BYWEEKNO", value)
42
+ @by_list[:bymonth] = parse_int_list("BYMONTH", value)
43
+ @by_list[:bysetpos] = parse_int_list("BYSETPOS", value)
44
+ @wkst = parse_wkstart(value)
45
+ end
46
+
47
+ def to_ical
48
+ result = ["FREQ=#{@frequency}"]
49
+ result << ";UNTIL=#{@until.to_ical}" if @until
50
+ result << ";COUNT=#{@count}" if @count
51
+ result << ";INTERVAL=#{@interval}" if @interval
52
+ @by_list.each do |key, value|
53
+ result << ";#{key.to_s.upcase}=#{value}" if value
54
+ end
55
+ result << ";WKST=#{@wkst}" if @wkst
56
+ result.join
57
+ end
58
+
59
+ def parse_date_val(name, string)
60
+ match = string.match(/;#{name}=(.*?)(;|$)/)
61
+ match ? DateTime.parse(match[1]) : nil
62
+ end
63
+
64
+ def parse_int_val(name, string)
65
+ match = string.match(/;#{name}=(\d+)(;|$)/)
66
+ match ? match[1].to_i : nil
67
+ end
68
+
69
+ def parse_int_list(name, string)
70
+ match = string.match(/;#{name}=([+-]?.*?)(;|$)/)
71
+ if match
72
+ match[1].split(",").map {|int| int.to_i}
73
+ else
74
+ nil
75
+ end
76
+ end
77
+
78
+ def parse_weekday_list(name, string)
79
+ match = string.match(/;#{name}=(.*?)(;|$)/)
80
+ if match
81
+ match[1].split(",").map {|weekday|
82
+ wd_match = weekday.match(/([+-]?\d*)(SU|MO|TU|WE|TH|FR|SA)/)
83
+ Weekday.new(wd_match[2], wd_match[1])
84
+ }
85
+ else
86
+ nil
87
+ end
88
+ end
89
+
90
+ def parse_wkstart(string)
91
+ match = string.match(/;WKSTART=(SU|MO|TU|WE|TH|FR|SA)(;|$)/)
92
+ if match
93
+ %w{SU MO TU WE TH FR SA}.index(match[1])
94
+ else
95
+ nil
96
+ end
97
+ end
98
+
99
+ def occurrences_of_event_starting(event, datetime)
100
+ initial_start = event.dtstart
101
+ (0...@count).map {|day_offset|
102
+ occurrence = event.clone
103
+ occurrence.dtstart = initial_start + day_offset
104
+ occurrence.clone
105
+ }
106
+ end
107
+ end
108
+
109
+ def Icalendar.parse(src, single = false)
110
+ cals = Icalendar::Parser.new(src).parse
111
+
112
+ if single
113
+ cals.first
114
+ else
115
+ cals
116
+ end
117
+ end
118
+
119
+ class Parser < Icalendar::Base
120
+ # date = date-fullyear ["-"] date-month ["-"] date-mday
121
+ # date-fullyear = 4 DIGIT
122
+ # date-month = 2 DIGIT
123
+ # date-mday = 2 DIGIT
124
+ DATE = '(\d\d\d\d)-?(\d\d)-?(\d\d)'
125
+
126
+ # time = time-hour [":"] time-minute [":"] time-second [time-secfrac] [time-zone]
127
+ # time-hour = 2 DIGIT
128
+ # time-minute = 2 DIGIT
129
+ # time-second = 2 DIGIT
130
+ # time-secfrac = "," 1*DIGIT
131
+ # time-zone = "Z" / time-numzone
132
+ # time-numzome = sign time-hour [":"] time-minute
133
+ TIME = '(\d\d):?(\d\d):?(\d\d)(\.\d+)?(Z|[-+]\d\d:?\d\d)?'
134
+
135
+ def initialize(src)
136
+ # Setup the parser method hash table
137
+ setup_parsers()
138
+
139
+ if src.respond_to?(:gets)
140
+ @file = src
141
+ elsif (not src.nil?) and src.respond_to?(:to_s)
142
+ @file = StringIO.new(src.to_s, 'r')
143
+ else
144
+ raise ArgumentError, "CalendarParser.new cannot be called with a #{src.class} type!"
145
+ end
146
+
147
+ @prev_line = @file.gets
148
+ @prev_line.chomp! unless @prev_line.nil?
149
+
150
+ @@logger.debug("New Calendar Parser: #{@file.inspect}")
151
+ end
152
+
153
+ # Define next line for an IO object.
154
+ # Works for strings now with StringIO
155
+ def next_line
156
+ line = @prev_line
157
+
158
+ if line.nil?
159
+ return nil
160
+ end
161
+
162
+ # Loop through until we get to a non-continuation line...
163
+ loop do
164
+ nextLine = @file.gets
165
+ @@logger.debug "new_line: #{nextLine}"
166
+
167
+ if !nextLine.nil?
168
+ nextLine.chomp!
169
+ end
170
+
171
+ # If it's a continuation line, add it to the last.
172
+ # If it's an empty line, drop it from the input.
173
+ if( nextLine =~ /^[ \t]/ )
174
+ line << nextLine[1, nextLine.size]
175
+ elsif( nextLine =~ /^$/ )
176
+ else
177
+ @prev_line = nextLine
178
+ break
179
+ end
180
+ end
181
+ line
182
+ end
183
+
184
+ # Parse the calendar into an object representation
185
+ def parse
186
+ calendars = []
187
+
188
+ @@logger.debug "parsing..."
189
+ # Outer loop for Calendar objects
190
+ while (line = next_line)
191
+ fields = parse_line(line)
192
+
193
+ # Just iterate through until we find the beginning of a calendar object
194
+ if fields[:name] == "BEGIN" and fields[:value] == "VCALENDAR"
195
+ cal = parse_component
196
+ @@logger.debug "Added parsed calendar..."
197
+ calendars << cal
198
+ end
199
+ end
200
+
201
+ calendars
202
+ end
203
+
204
+ private
205
+
206
+ # Parse a single VCALENDAR object
207
+ # -- This should consist of the PRODID, VERSION, option METHOD & CALSCALE,
208
+ # and then one or more calendar components: VEVENT, VTODO, VJOURNAL,
209
+ # VFREEBUSY, VTIMEZONE
210
+ def parse_component(component = Calendar.new)
211
+ @@logger.debug "parsing new component..."
212
+
213
+ while (line = next_line)
214
+ fields = parse_line(line)
215
+
216
+ name = fields[:name].upcase
217
+
218
+ # Although properties are supposed to come before components, we should
219
+ # be able to handle them in any order...
220
+ if name == "END"
221
+ break
222
+ elsif name == "BEGIN" # New component
223
+ case(fields[:value])
224
+ when "VEVENT" # Event
225
+ component.add_component parse_component(Event.new)
226
+ when "VTODO" # Todo entry
227
+ component.add_component parse_component(Todo.new)
228
+ when "VALARM" # Alarm sub-component for event and todo
229
+ component.add_component parse_component(Alarm.new)
230
+ when "VJOURNAL" # Journal entry
231
+ component.add_component parse_component(Journal.new)
232
+ when "VFREEBUSY" # Free/Busy section
233
+ component.add_component parse_component(Freebusy.new)
234
+ when "VTIMEZONE" # Timezone specification
235
+ component.add_component parse_component(Timezone.new)
236
+ when "STANDARD" # Standard time sub-component for timezone
237
+ component.add_component parse_component(Standard.new)
238
+ when "DAYLIGHT" # Daylight time sub-component for timezone
239
+ component.add_component parse_component(Daylight.new)
240
+ else # Uknown component type, skip to matching end
241
+ until ((line = next_line) == "END:#{fields[:value]}"); end
242
+ next
243
+ end
244
+ else # If its not a component then it should be a property
245
+ params = fields[:params]
246
+ value = fields[:value]
247
+
248
+ # Lookup the property name to see if we have a string to
249
+ # object parser for this property type.
250
+ orig_value = value
251
+ if @parsers.has_key?(name)
252
+ value = @parsers[name].call(name, params, value)
253
+ end
254
+
255
+ name = name.downcase
256
+
257
+ # TODO: check to see if there are any more conflicts.
258
+ if name == 'class' or name == 'method'
259
+ name = "ip_" + name
260
+ end
261
+
262
+ # Replace dashes with underscores
263
+ name = name.gsub('-', '_')
264
+
265
+ if component.multi_property?(name)
266
+ adder = "add_" + name
267
+ if component.respond_to?(adder)
268
+ component.send(adder, value, params)
269
+ else
270
+ raise(UnknownPropertyMethod, "Unknown property type: #{adder}")
271
+ end
272
+ else
273
+ if component.respond_to?(name)
274
+ component.send(name, value, params)
275
+ else
276
+ raise(UnknownPropertyMethod, "Unknown property type: #{name}")
277
+ end
278
+ end
279
+ end
280
+ end
281
+
282
+ component
283
+ end
284
+
285
+ # 1*(ALPHA / DIGIT / "=")
286
+ NAME = '[-a-z0-9]+'
287
+
288
+ # <"> <Any character except CTLs, DQUOTE> <">
289
+ QSTR = '"[^"]*"'
290
+
291
+ # Contentline
292
+ LINE = "(#{NAME})(.*(?:#{QSTR})|(?:[^:]*))\:(.*)"
293
+
294
+ # *<Any character except CTLs, DQUOTE, ";", ":", ",">
295
+ PTEXT = '[^";:,]*'
296
+
297
+ # param-value = ptext / quoted-string
298
+ PVALUE = "#{QSTR}|#{PTEXT}"
299
+
300
+ # param = name "=" param-value *("," param-value)
301
+ PARAM = ";(#{NAME})(=?)((?:#{PVALUE})(?:,#{PVALUE})*)"
302
+
303
+ def parse_line(line)
304
+ unless line =~ %r{#{LINE}}i # Case insensitive match for a valid line
305
+ raise "Invalid line in calendar string!"
306
+ end
307
+
308
+ name = $1.upcase # The case insensitive part is upcased for easier comparison...
309
+ paramslist = $2
310
+ value = $3.gsub("\\;", ";").gsub("\\,", ",").gsub("\\n", "\n").gsub("\\\\", "\\")
311
+
312
+ # Parse the parameters
313
+ params = {}
314
+ if paramslist.size > 1
315
+ paramslist.scan( %r{#{PARAM}}i ) do
316
+
317
+ # parameter names are case-insensitive, and multi-valued
318
+ pname = $1
319
+ pvals = $3
320
+
321
+ # If their isn't an '=' sign then we need to do some custom
322
+ # business. Defaults to 'type'
323
+ if $2 == ""
324
+ pvals = $1
325
+ case $1
326
+ when /quoted-printable/i
327
+ pname = 'encoding'
328
+
329
+ when /base64/i
330
+ pname = 'encoding'
331
+
332
+ else
333
+ pname = 'type'
334
+ end
335
+ end
336
+
337
+ # Make entries into the params dictionary where the name
338
+ # is the key and the value is an array of values.
339
+ unless params.key? pname
340
+ params[pname] = []
341
+ end
342
+
343
+ # Save all the values into the array.
344
+ pvals.scan( %r{(#{PVALUE})} ) do
345
+ if $1.size > 0
346
+ params[pname] << $1
347
+ end
348
+ end
349
+ end
350
+ end
351
+
352
+ {:name => name, :value => value, :params => params}
353
+ end
354
+
355
+ ## Following is a collection of parsing functions for various
356
+ ## icalendar property value data types... First we setup
357
+ ## a hash with property names pointing to methods...
358
+ def setup_parsers
359
+ @parsers = {}
360
+
361
+ # Integer properties
362
+ m = self.method(:parse_integer)
363
+ @parsers["PERCENT-COMPLETE"] = m
364
+ @parsers["PRIORITY"] = m
365
+ @parsers["REPEAT"] = m
366
+ @parsers["SEQUENCE"] = m
367
+
368
+ # Dates and Times
369
+ m = self.method(:parse_datetime)
370
+ @parsers["COMPLETED"] = m
371
+ @parsers["DTEND"] = m
372
+ @parsers["DUE"] = m
373
+ @parsers["DTSTART"] = m
374
+ @parsers["RECURRENCE-ID"] = m
375
+ @parsers["EXDATE"] = m
376
+ @parsers["RDATE"] = m
377
+ @parsers["CREATED"] = m
378
+ @parsers["DTSTAMP"] = m
379
+ @parsers["LAST-MODIFIED"] = m
380
+
381
+ # URI's
382
+ m = self.method(:parse_uri)
383
+ @parsers["TZURL"] = m
384
+ @parsers["ATTENDEE"] = m
385
+ @parsers["ORGANIZER"] = m
386
+ @parsers["URL"] = m
387
+
388
+ # This is a URI by default, and if its not a valid URI
389
+ # it will be returned as a string which works for binary data
390
+ # the other possible type.
391
+ @parsers["ATTACH"] = m
392
+
393
+ # GEO
394
+ m = self.method(:parse_geo)
395
+ @parsers["GEO"] = m
396
+
397
+ #RECUR
398
+ m = self.method(:parse_recur)
399
+ @parsers["RRULE"] = m
400
+ @parsers["EXRULE"] = m
401
+
402
+ end
403
+
404
+ # Booleans
405
+ # NOTE: It appears that although this is a valid data type
406
+ # there aren't any properties that use it... Maybe get
407
+ # rid of this in the future.
408
+ def parse_boolean(name, params, value)
409
+ if value.upcase == "FALSE"
410
+ false
411
+ else
412
+ true
413
+ end
414
+ end
415
+
416
+ # Dates, Date-Times & Times
417
+ # NOTE: invalid dates & times will be returned as strings...
418
+ def parse_datetime(name, params, value)
419
+ begin
420
+ result = DateTime.parse(value)
421
+ if /Z$/ =~ value
422
+ timezone = "UTC"
423
+ else
424
+ timezone = params["TZID"].first if params["TZID"]
425
+ end
426
+ result.icalendar_tzid = timezone
427
+ result
428
+ rescue Exception
429
+ value
430
+ end
431
+ end
432
+
433
+ def parse_recur(name, params, value)
434
+ ::Icalendar::RRule.new(name, params, value, self)
435
+ end
436
+
437
+ # Durations
438
+ # TODO: Need to figure out the best way to represent durations
439
+ # so just returning string for now.
440
+ def parse_duration(name, params, value)
441
+ value
442
+ end
443
+
444
+ # Floats
445
+ # NOTE: returns 0.0 if it can't parse the value
446
+ def parse_float(name, params, value)
447
+ value.to_f
448
+ end
449
+
450
+ # Integers
451
+ # NOTE: returns 0 if it can't parse the value
452
+ def parse_integer(name, params, value)
453
+ value.to_i
454
+ end
455
+
456
+ # Periods
457
+ # TODO: Got to figure out how to represent periods also...
458
+ def parse_period(name, params, value)
459
+ value
460
+ end
461
+
462
+ # Calendar Address's & URI's
463
+ # NOTE: invalid URI's will be returned as strings...
464
+ def parse_uri(name, params, value)
465
+ begin
466
+ URI.parse(value)
467
+ rescue Exception
468
+ value
469
+ end
470
+ end
471
+
472
+ # Geographical location (GEO)
473
+ # NOTE: returns an array with two floats (long & lat)
474
+ # if the parsing fails return the string
475
+ def parse_geo(name, params, value)
476
+ strloc = value.split(';')
477
+ if strloc.size != 2
478
+ return value
479
+ end
480
+
481
+ Geo.new(strloc[0].to_f, strloc[1].to_f)
482
+ end
483
+
484
+ end
485
+ end