vpim 0.16 → 0.17

Sign up to get free protection for your applications and to get access to all the features.
data/lib/vpim/time.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  =begin
2
- $Id: time.rb,v 1.5 2005/02/02 02:55:59 sam Exp $
3
-
4
- Copyright (C) 2005 Sam Roberts
2
+ Copyright (C) 2006 Sam Roberts
5
3
 
6
4
  This library is free software; you can redistribute it and/or modify it
7
5
  under the same terms as the ruby language itself, see the file COPYING for
data/lib/vpim/time.rb~ ADDED
@@ -0,0 +1,42 @@
1
+ =begin
2
+ $Id: time.rb,v 1.5 2005/02/02 02:55:59 sam Exp $
3
+
4
+ Copyright (C) 2005 Sam Roberts
5
+
6
+ This library is free software; you can redistribute it and/or modify it
7
+ under the same terms as the ruby language itself, see the file COPYING for
8
+ details.
9
+ =end
10
+
11
+ require 'date'
12
+
13
+ # Extensions to builtin Time allowing addition to Time by multiples of other
14
+ # intervals than a second.
15
+
16
+ class Time
17
+ # Returns a new Time, +years+ later than this time. Feb 29 of a
18
+ # leap year will be rounded up to Mar 1 if the target date is not a leap
19
+ # year.
20
+ def plus_year(years)
21
+ Time.local(year + years, month, day, hour, min, sec, usec)
22
+ end
23
+
24
+ # Returns a new Time, +months+ later than this time. The day will be
25
+ # rounded down if it is not valid for that month.
26
+ # 31 plus 1 month will be on Feb 28!
27
+ def plus_month(months)
28
+ d = Date.new(year, month, day)
29
+ d >>= months
30
+ Time.local(d.year, d.month, d.day, hour, min, sec, usec)
31
+ end
32
+
33
+ # Returns a new Time, +days+ later than this time.
34
+ # Does this do as I expect over DST? What if the hour doesn't exist
35
+ # in the next day, due to DST changes?
36
+ def plus_day(days)
37
+ d = Date.new(year, month, day)
38
+ d += days
39
+ Time.local(d.year, d.month, d.day, hour, min, sec, usec)
40
+ end
41
+ end
42
+
data/lib/vpim/vcard.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  =begin
2
- $Id: vcard.rb,v 1.13 2004/12/05 03:16:33 sam Exp $
3
-
4
- Copyright (C) 2005 Sam Roberts
2
+ Copyright (C) 2006 Sam Roberts
5
3
 
6
4
  This library is free software; you can redistribute it and/or modify it
7
5
  under the same terms as the ruby language itself, see the file COPYING for
@@ -30,7 +28,7 @@ module Vpim
30
28
  #
31
29
  # TODO - an open question is what exactly "vcard 2.1" support means. While I
32
30
  # decode vCard 2.1 correctly, I don't encode it. Should I implement a
33
- # transcoder, to vCards can be decoded from either version, and then written
31
+ # transcoder, so vCards can be decoded from either version, and then written
34
32
  # to either version? Maybe an option to Field#encode()?
35
33
  #
36
34
  # TODO - there are very few methods that Vcard has that DirectoryInfo
@@ -39,17 +37,28 @@ module Vpim
39
37
  # get the email addresses, or the name, or perhaps to set fields, like
40
38
  # email=. What would be useful?
41
39
  #
42
- # = Example
40
+ # = Examples
41
+ #
42
+ # - link:ex_mkvcard.txt: example of creating a vCard
43
+ # - link:ex_cpvcard.txt: example of copying and them modifying a vCard
44
+ # - link:ex_mkv21vcard.txt: example of creating version 2.1 vCard
45
+ # - link:mutt-aliases-to-vcf.txt: convert a mutt aliases file to vCards
46
+ # - link:ex_get_vcard_photo.txt: pull photo data from a vCard
47
+ # - link:ab-query.txt: query the OS X Address Book to find vCards
48
+ # - link:vcf-to-mutt.txt: query vCards for matches, output in formats useful
49
+ # with Mutt (see link:README.mutt for details)
50
+ # - link:tabbed-file-to-vcf.txt: convert a tab-delimited file to vCards, a
51
+ # (small but) complete application contributed by Dane G. Avilla, thanks!
52
+ # - link:vcf-to-ics.txt: example of how to create calendars of birthdays from vCards
53
+ # - link:vcf-dump.txt: utility for dumping contents of .vcf files
43
54
  #
44
55
  # Here's an example of encoding a simple vCard using the low-level API:
45
56
  #
46
57
  # card = Vpim::Vcard.create
47
- # card << Vpim::DirectoryInfo::Field.create('email', user.name@example.com, 'type' => "internet" )
48
- # card << Vpim::DirectoryInfo::Field.create('url', "http://www.example.com/user" )
49
- # card << Vpim::DirectoryInfo::Field.create('fn', "User Name" )
58
+ # card << Vpim::DirectoryInfo::Field.create('email', 'user.name@example.com', 'type' => 'internet' )
59
+ # card << Vpim::DirectoryInfo::Field.create('url', 'http://www.example.com/user' )
60
+ # card << Vpim::DirectoryInfo::Field.create('fn', 'User Name' )
50
61
  # puts card.to_s
51
- #
52
- # New! Use the Vpim::Maker::Vcard to make vCards!
53
62
  class Vcard < DirectoryInfo
54
63
 
55
64
  private_class_method :new
@@ -150,27 +159,84 @@ module Vpim
150
159
  def [](name, type=nil)
151
160
  fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
152
161
 
162
+ valued = fields.select { |f| f.value != '' }
163
+ if valued.first
164
+ fields = valued
165
+ end
166
+
153
167
  # limit to preferred, if possible
154
- pref = fields.find_all { |f| f.pref? }
168
+ pref = fields.select { |f| f.pref? }
155
169
 
156
- if(pref.first)
170
+ if pref.first
157
171
  fields = pref
158
172
  end
159
173
 
160
174
  fields.first ? fields.first.value : nil
161
175
  end
162
176
 
163
- # The nickname or nil if there is none.
164
- def nickname
177
+ # The name from a vCard, including all the components of the N: and FN:
178
+ # fields.
179
+
180
+ class Name
181
+ # family name from N:
182
+ attr_reader :family
183
+ # given name from N:
184
+ attr_reader :given
185
+ # additional names from N:
186
+ attr_reader :additional
187
+ # such as "Ms." or "Dr.", from N:
188
+ attr_reader :prefix
189
+ # such as "BFA", from N:
190
+ attr_reader :suffix
191
+ # all the components of N: formtted as "#{prefix} #{given} #{additional} #{family}, #{suffix}"
192
+ attr_reader :formatted
193
+ # full name, the FN: field, a formatted version of the N: field, probably
194
+ # in a form more align with the cultural conventions of the vCard owner
195
+ # than +formatted+ is
196
+ attr_reader :fullname
197
+
198
+ def initialize(n, fn) #:nodoc:
199
+ n = Vpim.decode_list(n, ';') do |item|
200
+ item.strip
201
+ end
202
+
203
+ @family = n[0] || ""
204
+ @given = n[1] || ""
205
+ @additional = n[2] || ""
206
+ @prefix = n[3] || ""
207
+ @suffix = n[4] || ""
208
+ @formatted = [ @prefix, @given, @additional, @family ].map{|i| i == '' ? nil : i}.compact.join(' ')
209
+ if @suffix != ''
210
+ @formatted << ', ' << @suffix
211
+ end
212
+
213
+ # FIXME - make calls to #fullname fail if fn is nil
214
+ @fullname = fn
215
+ end
216
+
217
+ end
218
+
219
+ # Returns the +name+ fields, N: and FN:, as a Name.
220
+ def name
221
+ unless instance_variables.include? '@name'
222
+ @name = Name.new(self['N'], self['FN'])
223
+ end
224
+ @name
225
+ end
226
+
227
+ # Deprecated.
228
+ def nickname #:nodoc:
165
229
  nn = self['nickname']
166
- if nn
167
- nn = nn.sub(/^\s+/, '')
168
- nn = nn.sub(/\s+$/, '')
169
- nn = nil if nn == ''
230
+ if nn && nn == ''
231
+ nn = nil
170
232
  end
171
233
  nn
172
234
  end
173
235
 
236
+ # All the nicknames, [] if there are none.
237
+ def nicknames
238
+ enum_by_name('NICKNAME').select{|f| f.value != ''}.collect{|f| f.value}
239
+ end
174
240
 
175
241
  # Returns the birthday as a Date on success, nil if there was no birthday
176
242
  # field, and raises an error if the birthday field could not be expressed
@@ -0,0 +1,232 @@
1
+ =begin
2
+ $Id: vcard.rb,v 1.13 2004/12/05 03:16:33 sam Exp $
3
+
4
+ Copyright (C) 2005 Sam Roberts
5
+
6
+ This library is free software; you can redistribute it and/or modify it
7
+ under the same terms as the ruby language itself, see the file COPYING for
8
+ details.
9
+ =end
10
+
11
+ require 'vpim/dirinfo'
12
+ require 'vpim/vpim'
13
+
14
+ module Vpim
15
+ # A vCard, a specialization of a directory info object.
16
+ #
17
+ # The vCard format is specified by:
18
+ # - RFC2426: vCard MIME Directory Profile (vCard 3.0)
19
+ # - RFC2425: A MIME Content-Type for Directory Information
20
+ #
21
+ # This implements vCard 3.0, but it is also capable of decoding vCard 2.1.
22
+ #
23
+ # For information about:
24
+ # - link:rfc2426.txt: vCard MIME Directory Profile (vCard 3.0)
25
+ # - link:rfc2425.txt: A MIME Content-Type for Directory Information
26
+ # - http://www.imc.org/pdi/pdiproddev.html: vCard 2.1 Specifications
27
+ #
28
+ # vCards are usually transmitted in files with <code>.vcf</code>
29
+ # extensions.
30
+ #
31
+ # TODO - an open question is what exactly "vcard 2.1" support means. While I
32
+ # decode vCard 2.1 correctly, I don't encode it. Should I implement a
33
+ # transcoder, so vCards can be decoded from either version, and then written
34
+ # to either version? Maybe an option to Field#encode()?
35
+ #
36
+ # TODO - there are very few methods that Vcard has that DirectoryInfo
37
+ # doesn't. I could probably just do away with it entirely, but I suspect
38
+ # that there are methods that could be usefully added to Vcard, perhaps to
39
+ # get the email addresses, or the name, or perhaps to set fields, like
40
+ # email=. What would be useful?
41
+ #
42
+ # = Example
43
+ #
44
+ # Here's an example of encoding a simple vCard using the low-level API:
45
+ #
46
+ # card = Vpim::Vcard.create
47
+ # card << Vpim::DirectoryInfo::Field.create('email', 'user.name@example.com', 'type' => 'internet' )
48
+ # card << Vpim::DirectoryInfo::Field.create('url', 'http://www.example.com/user' )
49
+ # card << Vpim::DirectoryInfo::Field.create('fn', 'User Name' )
50
+ # puts card.to_s
51
+ #
52
+ # New! Use the Vpim::Maker::Vcard to make vCards!
53
+ class Vcard < DirectoryInfo
54
+
55
+ private_class_method :new
56
+
57
+ # Create a vCard 3.0 object with the minimum required fields, plus any
58
+ # +fields+ you want in the card (they can also be added later).
59
+ def Vcard.create(fields = [] )
60
+ fields.unshift Field.create('VERSION', "3.0")
61
+ super(fields, 'VCARD')
62
+ end
63
+
64
+ # Decode a collection of vCards into an array of Vcard objects.
65
+ #
66
+ # +card+ can be either a String or an IO object.
67
+ #
68
+ # Since vCards are self-delimited (by a BEGIN:vCard and an END:vCard),
69
+ # multiple vCards can be concatenated into a single directory info object.
70
+ # They may or may not be related. For example, AddressBook.app (the OS X
71
+ # contact manager) will export multiple selected cards in this format.
72
+ #
73
+ # Input data will be converted from unicode if it is detected. The heuristic
74
+ # is based on the first bytes in the string:
75
+ # - 0xEF 0xBB 0xBF: UTF-8 with a BOM, the BOM is stripped
76
+ # - 0xFE 0xFF: UTF-16 with a BOM (big-endian), the BOM is stripped and string
77
+ # is converted to UTF-8
78
+ # - 0xFF 0xFE: UTF-16 with a BOM (little-endian), the BOM is stripped and string
79
+ # is converted to UTF-8
80
+ # - 0x00 'B' or 0x00 'b': UTF-16 (big-endian), the string is converted to UTF-8
81
+ # - 'B' 0x00 or 'b' 0x00: UTF-16 (little-endian), the string is converted to UTF-8
82
+ #
83
+ # If you know that you have only one vCard, then you can decode that
84
+ # single vCard by doing something like:
85
+ #
86
+ # vcard = Vcard.decode(card_data).first
87
+ #
88
+ # Note: Should the import encoding be remembered, so that it can be reencoded in
89
+ # the same format?
90
+ def Vcard.decode(card)
91
+ if card.respond_to? :to_str
92
+ string = card.to_str
93
+ elsif card.kind_of? IO
94
+ string = card.read(nil)
95
+ else
96
+ raise ArgumentError, "Vcard.decode cannot be called with a #{card.type}"
97
+ end
98
+
99
+ case string
100
+ when /^\xEF\xBB\xBF/
101
+ string = string.sub("\xEF\xBB\xBF", '')
102
+ when /^\xFE\xFF/
103
+ arr = string.unpack('n*')
104
+ arr.shift
105
+ string = arr.pack('U*')
106
+ when /^\xFF\xFE/
107
+ arr = string.unpack('v*')
108
+ arr.shift
109
+ string = arr.pack('U*')
110
+ when /^\x00\x62/i
111
+ string = string.unpack('n*').pack('U*')
112
+ when /^\x62\x00/i
113
+ string = string.unpack('v*').pack('U*')
114
+ end
115
+
116
+ entities = Vpim.expand(Vpim.decode(string))
117
+
118
+ # Since all vCards must have a begin/end, the top-level should consist
119
+ # entirely of entities/arrays, even if its a single vCard.
120
+ if entities.detect { |e| ! e.kind_of? Array }
121
+ raise "Not a valid vCard"
122
+ end
123
+
124
+ vcards = []
125
+
126
+ for e in entities
127
+ vcards.push(new(e.flatten, 'VCARD'))
128
+ end
129
+
130
+ vcards
131
+ end
132
+
133
+ # The vCard version multiplied by 10 as an Integer. If no VERSION field
134
+ # is present (which is non-conformant), nil is returned. For example, a
135
+ # version 2.1 vCard would have a version of 21, and a version 3.0 vCard
136
+ # would have a version of 30.
137
+ def version
138
+ v = self["version"]
139
+ unless v
140
+ raise Vpim::InvalidEncodingError, 'Invalid vCard - it has no version field!'
141
+ end
142
+ v = v.to_f * 10
143
+ v = v.to_i
144
+ end
145
+
146
+ # The value of the field named +name+, optionally limited to fields of
147
+ # type +type+. If no match is found, nil is returned, if multiple matches
148
+ # are found, the first match to have one of its type values be 'pref'
149
+ # (preferred) is returned, otherwise the first match is returned.
150
+ def [](name, type=nil)
151
+ fields = enum_by_name(name).find_all { |f| type == nil || f.type?(type) }
152
+
153
+ valued = fields.select { |f| f.value != '' }
154
+ if valued.first
155
+ fields = valued
156
+ end
157
+
158
+ # limit to preferred, if possible
159
+ pref = fields.select { |f| f.pref? }
160
+
161
+ if pref.first
162
+ fields = pref
163
+ end
164
+
165
+ fields.first ? fields.first.value : nil
166
+ end
167
+
168
+ =begin
169
+ TODO
170
+ def name
171
+ h = {} Create a Struct from the FN fields (maybe add the N field value, to?)
172
+
173
+ def h.to_s
174
+ concatenate the name fields nicely to make the name
175
+ end
176
+ end
177
+ =end
178
+
179
+ # The nickname or nil if there is none.
180
+ def nickname
181
+ nn = self['nickname']
182
+ if nn && nn == ''
183
+ nn = nil
184
+ end
185
+ nn
186
+ end
187
+
188
+ # All the nicknames, [] if there are none.
189
+ def nicknames
190
+ nn = self['nickname']
191
+ if nn && nn == ''
192
+ nn = nil
193
+ end
194
+ nn
195
+ end
196
+
197
+ # Returns the birthday as a Date on success, nil if there was no birthday
198
+ # field, and raises an error if the birthday field could not be expressed
199
+ # as a recurring event.
200
+ #
201
+ # Also, I have a lot of vCards with dates that look like:
202
+ # 678-09-23
203
+ # The 678 is garbage, but 09-23 is really the birthday. This substitutes the
204
+ # current year for the 3 digit year, and then converts to a Date.
205
+ def birthday
206
+ bday = self.field('BDAY')
207
+
208
+ return nil unless bday
209
+
210
+ begin
211
+ date = bday.to_date.first
212
+
213
+ rescue Vpim::InvalidEncodingError
214
+ if bday.value =~ /(\d+)-(\d+)-(\d+)/
215
+ y = $1.to_i
216
+ m = $2.to_i
217
+ d = $3.to_i
218
+ if(y < 1900)
219
+ y = Time.now.year
220
+ end
221
+ date = Date.new(y, m, d)
222
+ else
223
+ raise
224
+ end
225
+ end
226
+
227
+ date
228
+ end
229
+
230
+ end
231
+ end
232
+
data/lib/vpim/vevent.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  =begin
2
- $Id: vevent.rb,v 1.12 2005/01/21 04:09:55 sam Exp $
3
-
4
- Copyright (C) 2005 Sam Roberts
2
+ Copyright (C) 2006 Sam Roberts
5
3
 
6
4
  This library is free software; you can redistribute it and/or modify it
7
5
  under the same terms as the ruby language itself, see the file COPYING for
@@ -0,0 +1,381 @@
1
+ =begin
2
+ $Id: vevent.rb,v 1.12 2005/01/21 04:09:55 sam Exp $
3
+
4
+ Copyright (C) 2005 Sam Roberts
5
+
6
+ This library is free software; you can redistribute it and/or modify it
7
+ under the same terms as the ruby language itself, see the file COPYING for
8
+ details.
9
+ =end
10
+
11
+ require 'vpim/dirinfo'
12
+ require 'vpim/field'
13
+ require 'vpim/rfc2425'
14
+ require 'vpim/vpim'
15
+
16
+ =begin
17
+ A vTodo that is done:
18
+
19
+ BEGIN:VTODO
20
+ COMPLETED:20040303T050000Z
21
+ DTSTAMP:20040304T011707Z
22
+ DTSTART;TZID=Canada/Eastern:20030524T115238
23
+ SEQUENCE:2
24
+ STATUS:COMPLETED
25
+ SUMMARY:Wash Car
26
+ UID:E7609713-8E13-11D7-8ACC-000393AD088C
27
+ END:VTODO
28
+
29
+ BEGIN:VTODO
30
+ DTSTAMP:20030909T015533Z
31
+ DTSTART;TZID=Canada/Eastern:20030808T000000
32
+ SEQUENCE:1
33
+ SUMMARY:Renew Passport
34
+ UID:EC76B256-BBE9-11D7-8401-000393AD088C
35
+ END:VTODO
36
+
37
+
38
+ =end
39
+
40
+ module Vpim
41
+ class Icalendar
42
+ class Vtodo
43
+ def initialize(fields) #:nodoc:
44
+ @fields = fields
45
+
46
+ outer, inner = Vpim.outer_inner(fields)
47
+
48
+ @properties = Vpim::DirectoryInfo.create(outer)
49
+
50
+ @elements = inner
51
+
52
+ # TODO - don't get properties here, put the accessor in a module,
53
+ # which can cache the results.
54
+
55
+ @summary = @properties.text('SUMMARY').first
56
+ @description = @properties.text('DESCRIPTION').first
57
+ @comment = @properties.text('COMMENT').first
58
+ @location = @properties.text('LOCATION').first
59
+ @status = @properties.text('STATUS').first
60
+ @uid = @properties.text('UID').first
61
+ @priority = @properties.text('PRIORITY').first
62
+
63
+ # See "TODO - fields" in dirinfo.rb
64
+ @dtstamp = @properties.field('dtstamp')
65
+ @dtstart = @properties.field('dtstart')
66
+ @dtend = @properties.field('dtend')
67
+ @duration = @properties.field('duration')
68
+ @due = @properties.field('due')
69
+ @rrule = @properties['rrule']
70
+
71
+ # Need to seperate status-handling out into a module...
72
+ @status_values = [ 'COMPLETED' ];
73
+
74
+ end
75
+
76
+ attr_reader :description, :summary, :comment, :location
77
+ attr_reader :properties, :fields # :nodoc:
78
+
79
+ # Create a new Vtodo object.
80
+ #
81
+ # If specified, +fields+ must be either an array of Field objects to
82
+ # add, or a Hash of String names to values that will be used to build
83
+ # Field objects. The latter is a convenient short-cut allowing the Field
84
+ # objects to be created for you when called like:
85
+ #
86
+ # Vtodo.create('SUMMARY' => "buy mangos")
87
+ #
88
+ # TODO - maybe todos are usually created in a particular way? I can
89
+ # make it easier. Ideally, I would like to make it hard to encode an invalid
90
+ # Event.
91
+ def Vtodo.create(fields=[])
92
+ di = DirectoryInfo.create([], 'VTODO')
93
+
94
+ Vpim::DirectoryInfo::Field.create_array(fields).each { |f| di.push_unique f }
95
+
96
+ new(di.to_a)
97
+ end
98
+
99
+ =begin
100
+ I think that the initialization shouldn't be done in the #initialize, so, for example,
101
+ @status = @properties.text('STATUS').first
102
+ should be in the method below.
103
+
104
+ That way, I can construct a Vtodo by just including a module for each field that is allowed
105
+ in a Vtodo, simply.
106
+ =end
107
+ def status
108
+ if(!@status); return nil; end
109
+
110
+ s = @status.upcase
111
+
112
+ unless @status_values.include?(s)
113
+ raise Vpim::InvalidEncodingError, "Invalid status '#{@status}'"
114
+ end
115
+
116
+ s
117
+ end
118
+
119
+ # +priority+ is a number from 1 to 9, with 1 being the highest and 0
120
+ # meaning "no priority", equivalent to not specifying the PRIORITY field.
121
+ # Other values are reserved by RFC2446.
122
+ def priority
123
+ p = @priority ? @priority.to_i : 0
124
+
125
+ if( p < 0 || p > 9 )
126
+ raise Vpim::InvalidEncodingError, 'Invalid priority #{@priority} - it must be 0-9!'
127
+ end
128
+ p
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ module Vpim
135
+ class Icalendar
136
+ class Vevent
137
+ def initialize(fields) #:nodoc:
138
+ @fields = fields
139
+
140
+ outer, inner = Vpim.outer_inner(fields)
141
+
142
+ @properties = Vpim::DirectoryInfo.create(outer)
143
+
144
+ @elements = inner
145
+
146
+ # TODO - don't get properties here, put the accessor in a module,
147
+ # which can cache the results.
148
+
149
+ @summary = @properties.text('SUMMARY').first
150
+ @description = @properties.text('DESCRIPTION').first
151
+ @comment = @properties.text('COMMENT').first
152
+ @location = @properties.text('LOCATION').first
153
+ @status = @properties.text('STATUS').first
154
+ @uid = @properties.text('UID').first
155
+
156
+ # See "TODO - fields" in dirinfo.rb
157
+ @dtstamp = @properties.field('dtstamp')
158
+ @dtstart = @properties.field('dtstart')
159
+ @dtend = @properties.field('dtend')
160
+ @duration = @properties.field('duration')
161
+ @rrule = @properties['rrule']
162
+
163
+ # Need to seperate status-handling out into a module...
164
+ @status_values = [ 'TENTATIVE', 'CONFIRMED', 'CANCELLED' ];
165
+
166
+ end
167
+
168
+ # Create a new Vevent object. All events must have a DTSTART field,
169
+ # specify it as either a Time or a Date in +start+, it defaults to "now"
170
+ # (is this useful?).
171
+ #
172
+ # If specified, +fields+ must be either an array of Field objects to
173
+ # add, or a Hash of String names to values that will be used to build
174
+ # Field objects. The latter is a convenient short-cut allowing the Field
175
+ # objects to be created for you when called like:
176
+ #
177
+ # Vevent.create(Date.today, 'SUMMARY' => "today's event")
178
+ #
179
+ # TODO - maybe events are usually created in a particular way? With a
180
+ # start/duration or a start/end? Maybe I can make it easier. Ideally, I
181
+ # would like to make it hard to encode an invalid Event.
182
+ def Vevent.create(start = Time.now, fields=[])
183
+ dtstart = DirectoryInfo::Field.create('DTSTART', start)
184
+ di = DirectoryInfo.create([ dtstart ], 'VEVENT')
185
+
186
+ Vpim::DirectoryInfo::Field.create_array(fields).each { |f| di.push_unique f }
187
+
188
+ new(di.to_a)
189
+ end
190
+
191
+ # Creates a yearly repeating event, such as for a birthday.
192
+ def Vevent.create_yearly(date, summary)
193
+ create(
194
+ date,
195
+ 'SUMMARY' => summary.to_str,
196
+ 'RRULE' => 'FREQ=YEARLY'
197
+ )
198
+ end
199
+
200
+ attr_reader :description, :summary, :comment, :location
201
+ attr_reader :properties, :fields # :nodoc:
202
+
203
+ #--
204
+ # The methods below should be shared, somehow, by all calendar components, not just Events.
205
+ #++
206
+
207
+ # Accept an event invitation. The +invitee+ is the Address that wishes
208
+ # to accept the event invitation as confirmed.
209
+ def accept(invitee)
210
+ # The event created is identical to this one, but
211
+ # - without the attendees
212
+ # - with the invitee added with a PARTSTAT of ACCEPTED
213
+ invitee = invitee.copy
214
+ invitee.partstat = 'ACCEPTED'
215
+
216
+ fields = []
217
+
218
+ @properties.each_with_index do
219
+ |f,i|
220
+
221
+ # put invitee in as field[1]
222
+ fields << invitee.field if i == 1
223
+
224
+ fields << f unless f.name? 'ATTENDEE'
225
+ end
226
+
227
+ Vevent.new(fields)
228
+ end
229
+
230
+ # Status values are not rejected during decoding. However, if the
231
+ # status is requested, and it's value is not one of the defined
232
+ # allowable values, an exception is raised.
233
+ def status
234
+ if(!@status); return nil; end
235
+
236
+ s = @status.upcase
237
+
238
+ unless @status_values.include?(s)
239
+ raise Vpim::InvalidEncodingError, "Invalid status '#{@status}'"
240
+ end
241
+
242
+ s
243
+ end
244
+
245
+ # TODO - def status? ...
246
+
247
+ # TODO - def status= ...
248
+
249
+ # The unique identifier of this calendar component, a string. It cannot be
250
+ # nil, if it is not found in the component, the calendar is malformed, and
251
+ # this method will raise an exception.
252
+ def uid
253
+ if(!@uid)
254
+ raise Vpim::InvalidEncodingError, 'Invalid component - no UID field was found!'
255
+ end
256
+
257
+ @uid
258
+ end
259
+
260
+ # The time stamp for this calendar component. Describe what this is....
261
+ # This field is required!
262
+ def dtstamp
263
+ if(!@dtstamp)
264
+ raise Vpim::InvalidEncodingError, 'Invalid component - no DTSTAMP field was found!'
265
+ end
266
+
267
+ @dtstamp.to_time.first
268
+ end
269
+
270
+ # The start time for this calendar component. Describe what this is....
271
+ # This field is required!
272
+ def dtstart
273
+ if(!@dtstart)
274
+ raise Vpim::InvalidEncodingError, 'Invalid component - no DTSTART field was found!'
275
+ end
276
+
277
+ @dtstart.to_time.first
278
+ end
279
+
280
+ =begin
281
+ # Set the start time for the event to +start+, a Time object.
282
+ # TODO - def dtstart=(start) ... start should be allowed to be Time/Date/DateTime
283
+ =end
284
+
285
+ # The duration in seconds of a Event, Todo, or Vfreebusy component, or
286
+ # for Alarms, the delay period prior to repeating the alarm. The
287
+ # duration is calculated from the DTEND and DTBEGIN fields if the
288
+ # DURATION field is not present. Durations of zero seconds are possible.
289
+ def duration
290
+ if(!@duration)
291
+ return nil unless @dtend
292
+
293
+ b = dtstart
294
+ e = dtend
295
+
296
+ return (e - b).to_i
297
+ end
298
+
299
+ Icalendar.decode_duration(@duration.value_raw)
300
+ end
301
+
302
+ # The end time for this calendar component. For an Event, if there is no
303
+ # end time, then nil is returned, and the event takes up no time.
304
+ # However, the end time will be calculated from the event duration, if
305
+ # present.
306
+ def dtend
307
+ if(@dtend)
308
+ @dtend.to_time.first
309
+ elsif duration
310
+ dtstart + duration
311
+ else
312
+ nil
313
+ end
314
+ end
315
+
316
+ # The recurrence rule, if any, for this event. Recurrence starts at the
317
+ # DTSTART time.
318
+ def rrule
319
+ @rrule
320
+ end
321
+
322
+ # The times this event occurs, as a Vpim::Rrule.
323
+ #
324
+ # Note: the event may occur only once.
325
+ #
326
+ # Note: occurences are currently calculated only from DTSTART and RRULE,
327
+ # no allowance for EXDATE or other fields is made.
328
+ def occurences
329
+ Vpim::Rrule.new(dtstart, @rrule)
330
+ end
331
+
332
+ # Check if this event overlaps with the time period later than or equal to +t0+, but
333
+ # earlier than +t1+.
334
+ def occurs_in?(t0, t1)
335
+ occurences.each_until(t1).detect { |t| tend = t + (duration || 0); tend > t0 }
336
+ end
337
+
338
+ # Return the event organizer, an object of Icalendar::Address (or nil if
339
+ # there is no ORGANIZER field).
340
+ #
341
+ # TODO - verify that it is illegal for there to be more than one
342
+ # ORGANIZER, if more than one is allowed, this needs to return an array.
343
+ def organizer
344
+ unless instance_variables.include? "@organizer"
345
+ @organizer = @properties.field('ORGANIZER')
346
+
347
+ if @organizer
348
+ @organizer = Icalendar::Address.new(@organizer)
349
+ end
350
+ end
351
+ @organizer.freeze
352
+ end
353
+
354
+ # Return an array of attendees, an empty array if there are none. The
355
+ # attendees are objects of Icalendar::Address. If +uri+ is specified
356
+ # only the return the attendees with this +uri+.
357
+ def attendees(uri = nil)
358
+ unless instance_variables.include? "@attendees"
359
+ @attendees = @properties.enum_by_name('ATTENDEE').map { |a| Icalendar::Address.new(a).freeze }
360
+ @attendees.freeze
361
+ end
362
+ if uri
363
+ @attendees.select { |a| a == uri } .freeze
364
+ else
365
+ @attendees
366
+ end
367
+ end
368
+
369
+ # Return true if the +uri+, usually a mailto: URI, is an attendee.
370
+ def attendee?(uri)
371
+ attendees.include? uri
372
+ end
373
+
374
+ # CONTACT - value is text, parameters are ALTREP and LANGUAGE.
375
+ #
376
+ # textual contact information, or an altrep referring to a URI pointing
377
+ # at a vCard or LDAP entry...
378
+ end
379
+ end
380
+ end
381
+