vpim 0.16 → 0.17

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.
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
+