vpim 0.16
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/vpim/date.rb +198 -0
- data/lib/vpim/dirinfo.rb +229 -0
- data/lib/vpim/duration.rb +121 -0
- data/lib/vpim/enumerator.rb +29 -0
- data/lib/vpim/field.rb +511 -0
- data/lib/vpim/icalendar.rb +555 -0
- data/lib/vpim/maker/vcard.rb +337 -0
- data/lib/vpim/rfc2425.rb +247 -0
- data/lib/vpim/rrule.rb +482 -0
- data/lib/vpim/time.rb +42 -0
- data/lib/vpim/vcard.rb +210 -0
- data/lib/vpim/vevent.rb +381 -0
- data/lib/vpim/vpim.rb +96 -0
- data/lib/vpim/vpim.rb~ +96 -0
- metadata +60 -0
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
ADDED
@@ -0,0 +1,210 @@
|
|
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, to 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
|
+
# limit to preferred, if possible
|
154
|
+
pref = fields.find_all { |f| f.pref? }
|
155
|
+
|
156
|
+
if(pref.first)
|
157
|
+
fields = pref
|
158
|
+
end
|
159
|
+
|
160
|
+
fields.first ? fields.first.value : nil
|
161
|
+
end
|
162
|
+
|
163
|
+
# The nickname or nil if there is none.
|
164
|
+
def nickname
|
165
|
+
nn = self['nickname']
|
166
|
+
if nn
|
167
|
+
nn = nn.sub(/^\s+/, '')
|
168
|
+
nn = nn.sub(/\s+$/, '')
|
169
|
+
nn = nil if nn == ''
|
170
|
+
end
|
171
|
+
nn
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
# Returns the birthday as a Date on success, nil if there was no birthday
|
176
|
+
# field, and raises an error if the birthday field could not be expressed
|
177
|
+
# as a recurring event.
|
178
|
+
#
|
179
|
+
# Also, I have a lot of vCards with dates that look like:
|
180
|
+
# 678-09-23
|
181
|
+
# The 678 is garbage, but 09-23 is really the birthday. This substitutes the
|
182
|
+
# current year for the 3 digit year, and then converts to a Date.
|
183
|
+
def birthday
|
184
|
+
bday = self.field('BDAY')
|
185
|
+
|
186
|
+
return nil unless bday
|
187
|
+
|
188
|
+
begin
|
189
|
+
date = bday.to_date.first
|
190
|
+
|
191
|
+
rescue Vpim::InvalidEncodingError
|
192
|
+
if bday.value =~ /(\d+)-(\d+)-(\d+)/
|
193
|
+
y = $1.to_i
|
194
|
+
m = $2.to_i
|
195
|
+
d = $3.to_i
|
196
|
+
if(y < 1900)
|
197
|
+
y = Time.now.year
|
198
|
+
end
|
199
|
+
date = Date.new(y, m, d)
|
200
|
+
else
|
201
|
+
raise
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
date
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
data/lib/vpim/vevent.rb
ADDED
@@ -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
|
+
|