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/agent/plist.rb +86 -0
- data/lib/vpim/date.rb +1 -3
- data/lib/vpim/date.rb~ +198 -0
- data/lib/vpim/dirinfo.rb +47 -15
- data/lib/vpim/dirinfo.rb~ +242 -0
- data/lib/vpim/duration.rb +1 -3
- data/lib/vpim/duration.rb~ +121 -0
- data/lib/vpim/enumerator.rb +1 -3
- data/lib/vpim/enumerator.rb~ +29 -0
- data/lib/vpim/field.rb +141 -56
- data/lib/vpim/field.rb~ +594 -0
- data/lib/vpim/icalendar.rb +10 -16
- data/lib/vpim/icalendar.rb~ +548 -0
- data/lib/vpim/maker/vcard.rb +124 -46
- data/lib/vpim/maker/vcard.rb~ +382 -0
- data/lib/vpim/rfc2425.rb +30 -17
- data/lib/vpim/rfc2425.rb~ +246 -0
- data/lib/vpim/rrule.rb +2 -4
- data/lib/vpim/rrule.rb~ +482 -0
- data/lib/vpim/time.rb +1 -3
- data/lib/vpim/time.rb~ +42 -0
- data/lib/vpim/vcard.rb +84 -18
- data/lib/vpim/vcard.rb~ +232 -0
- data/lib/vpim/vevent.rb +1 -3
- data/lib/vpim/vevent.rb~ +381 -0
- data/lib/vpim/vpim.rb +61 -29
- data/lib/vpim/vpim.rb~ +61 -29
- metadata +16 -2
@@ -0,0 +1,86 @@
|
|
1
|
+
# http://www2a.biglobe.ne.jp/~seki/ruby/src/plist.rb
|
2
|
+
require 'rexml/document'
|
3
|
+
require 'time'
|
4
|
+
|
5
|
+
class Plist #:nodoc:
|
6
|
+
def self.file_to_plist(fname)
|
7
|
+
File.open(fname) do |fp|
|
8
|
+
doc = REXML::Document.new(fp)
|
9
|
+
return self.new.visit(REXML::XPath.match(doc, '/plist/')[0])
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
setup_method_table
|
15
|
+
end
|
16
|
+
|
17
|
+
def visit(node)
|
18
|
+
visit_one(node.elements[1])
|
19
|
+
end
|
20
|
+
|
21
|
+
def visit_one(node)
|
22
|
+
choose_method(node.name).call(node)
|
23
|
+
end
|
24
|
+
|
25
|
+
def visit_null(node)
|
26
|
+
p node if $DEBUG
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
|
30
|
+
def visit_dict(node)
|
31
|
+
dict = {}
|
32
|
+
es = node.elements.to_a
|
33
|
+
while key = es.shift
|
34
|
+
next unless key.name == 'key'
|
35
|
+
dict[key.text] = visit_one(es.shift)
|
36
|
+
end
|
37
|
+
dict
|
38
|
+
end
|
39
|
+
|
40
|
+
def visit_array(node)
|
41
|
+
node.elements.collect do |x|
|
42
|
+
visit_one(x)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def visit_integer(node)
|
47
|
+
node.text.to_i
|
48
|
+
end
|
49
|
+
|
50
|
+
def visit_real(node)
|
51
|
+
node.text.to_f
|
52
|
+
end
|
53
|
+
|
54
|
+
def visit_string(node)
|
55
|
+
node.text.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
def visit_date(node)
|
59
|
+
Time.parse(node.text.to_s)
|
60
|
+
end
|
61
|
+
|
62
|
+
def visit_true(node)
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
def visit_false(node)
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
def choose_method(name)
|
72
|
+
@method.fetch(name, method(:visit_null))
|
73
|
+
end
|
74
|
+
|
75
|
+
def setup_method_table
|
76
|
+
@method = {}
|
77
|
+
@method['dict'] = method(:visit_dict)
|
78
|
+
@method['integer'] = method(:visit_integer)
|
79
|
+
@method['real'] = method(:visit_real)
|
80
|
+
@method['string'] = method(:visit_string)
|
81
|
+
@method['date'] = method(:visit_date)
|
82
|
+
@method['true'] = method(:visit_true)
|
83
|
+
@method['false'] = method(:visit_false)
|
84
|
+
@method['array'] = method(:visit_array)
|
85
|
+
end
|
86
|
+
end
|
data/lib/vpim/date.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
=begin
|
2
|
-
|
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/date.rb~
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
=begin
|
2
|
+
$Id: date.rb,v 1.8 2004/11/17 05:06:27 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 the standard library Date.
|
14
|
+
class Date
|
15
|
+
|
16
|
+
TIME_START = Date.new(1970, 1, 1)
|
17
|
+
SECS_PER_DAY = 24 * 60 * 60
|
18
|
+
|
19
|
+
# Converts this object to a Time object, or throws an ArgumentError if
|
20
|
+
# conversion is not possible because it is before the start of epoch.
|
21
|
+
def to_time
|
22
|
+
raise ArgumentError, 'date is before the start of system time' if self < TIME_START
|
23
|
+
days = self - TIME_START
|
24
|
+
|
25
|
+
Time.at((days * SECS_PER_DAY).to_i)
|
26
|
+
end
|
27
|
+
|
28
|
+
# If wday responds to to_str, convert it to the wday number by searching for
|
29
|
+
# a wday that matches, using as many characters as are in wday to do the
|
30
|
+
# comparison. wday must be 2 or more characters long in order to be a unique
|
31
|
+
# match, other than that, "mo", "Mon", and "MonDay" are all valid strings
|
32
|
+
# for wday 1.
|
33
|
+
#
|
34
|
+
# This method can be called on a valid wday, and it will return it. Perhaps
|
35
|
+
# it should be called by default inside the Date#new*() methods so that
|
36
|
+
# non-integer wday arguments can be used? Perhaps a similar method should
|
37
|
+
# exist for months? But with months, we all know January is 1, who can
|
38
|
+
# remember where Date chooses to start its wday count!
|
39
|
+
#
|
40
|
+
# Examples:
|
41
|
+
# Date.bywday(2004, 2, Date.str2wday('TU')) => the first Tuesday in
|
42
|
+
# February
|
43
|
+
# Date.bywday(2004, 2, Date.str2wday(2)) => the same day, but notice
|
44
|
+
# that a valid wday integer can be passed right through.
|
45
|
+
#
|
46
|
+
def Date.str2wday(wdaystr)
|
47
|
+
return wdaystr unless wdaystr.respond_to? :to_str
|
48
|
+
|
49
|
+
str = wdaystr.to_str.upcase
|
50
|
+
if str.length < 2
|
51
|
+
raise ArgumentError, 'wday #{wday} is not long enough to be a unique weekday name'
|
52
|
+
end
|
53
|
+
|
54
|
+
wday = Date::DAYNAMES.map { |n| n.slice(0, str.length).upcase }.index(str)
|
55
|
+
|
56
|
+
return wday if wday
|
57
|
+
|
58
|
+
raise ArgumentError, 'wday #{wdaystr} was not a recognizable weekday name'
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# Create a new Date object for the date specified by year +year+, month
|
63
|
+
# +mon+, and day-of-the-week +wday+.
|
64
|
+
#
|
65
|
+
# The nth, +n+, occurrence of +wday+ within the period will be generated
|
66
|
+
# (+n+ defaults to 1). If +n+ is positive, the nth occurence from the
|
67
|
+
# beginning of the period will be returned, if negative, the nth occurrence
|
68
|
+
# from the end of the period will be returned.
|
69
|
+
#
|
70
|
+
# The period is a year, unless +month+ is non-nil, in which case it is just
|
71
|
+
# that month.
|
72
|
+
#
|
73
|
+
# Examples:
|
74
|
+
# - Date.bywday(2004, nil, 1, 9) => the ninth Sunday of 2004
|
75
|
+
# - Date.bywday(2004, nil, 1) => the first Sunday of 2004
|
76
|
+
# - Date.bywday(2004, nil, 1, -2) => the second last Sunday of 2004
|
77
|
+
# - Date.bywday(2004, 12, 1) => the first sunday in the 12th month of 2004
|
78
|
+
# - Date.bywday(2004, 2, 2, -1) => last Tuesday in the 2nd month in 2004
|
79
|
+
# - Date.bywday(2004, -2, 3, -2) => second last Wednesday in the second last month of 2004
|
80
|
+
#
|
81
|
+
# Compare this to Date.new, which allows a Date to be created by
|
82
|
+
# day-of-the-month, mday, to Date.new2, which allows a Date to be created by
|
83
|
+
# day-of-the-year, yday, and to Date.neww, which allows a Date to be created
|
84
|
+
# by day-of-the-week, but within a specific week.
|
85
|
+
def Date.bywday(year, mon, wday, n = 1, sg=Date::ITALY)
|
86
|
+
# Normalize mon to 1-12.
|
87
|
+
if mon
|
88
|
+
if mon > 12 || mon == 0 || mon < -12
|
89
|
+
raise ArgumentError, "mon #{mon} must be 1-12 or negative 1-12"
|
90
|
+
end
|
91
|
+
if mon < 0
|
92
|
+
mon = 13 + mon
|
93
|
+
end
|
94
|
+
end
|
95
|
+
if wday < 0 || wday > 6
|
96
|
+
raise ArgumentError, 'wday must be in range 0-6, or a weekday name'
|
97
|
+
end
|
98
|
+
|
99
|
+
# Determine direction of indexing.
|
100
|
+
inc = n <=> 0
|
101
|
+
if inc == 0
|
102
|
+
raise ArgumentError, 'n must be greater or less than zero'
|
103
|
+
end
|
104
|
+
|
105
|
+
# if !mon, n is index into year, but direction of search is determined by
|
106
|
+
# sign of n
|
107
|
+
d = Date.new(year, mon ? mon : inc, inc, sg)
|
108
|
+
|
109
|
+
while d.wday != wday
|
110
|
+
d += inc
|
111
|
+
end
|
112
|
+
|
113
|
+
# Now we have found the first/last day with the correct wday, search
|
114
|
+
# for nth occurrence, by jumping by n.abs-1 weeks forward or backward.
|
115
|
+
d += 7 * (n.abs - 1) * inc
|
116
|
+
|
117
|
+
if d.year != year
|
118
|
+
raise ArgumentError, 'n is out of bounds of year'
|
119
|
+
end
|
120
|
+
if mon && d.mon != mon
|
121
|
+
raise ArgumentError, 'n is out of bounds of month'
|
122
|
+
end
|
123
|
+
d
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# DateGen generates arrays of dates matching simple criteria.
|
128
|
+
class DateGen
|
129
|
+
# Generate an array of dates on +wday+ (the day-of-week,
|
130
|
+
# 0-6, where 0 is Sunday).
|
131
|
+
#
|
132
|
+
# If +n+ is specified, only the nth occurrence of +wday+ within the period
|
133
|
+
# will be generated. If +n+ is positive, the nth occurence from the
|
134
|
+
# beginning of the period will be returned, if negative, the nth occurrence
|
135
|
+
# from the end of the period will be returned.
|
136
|
+
#
|
137
|
+
# The period is a year, unless +month+ is non-nil, in which case it is just
|
138
|
+
# that month.
|
139
|
+
#
|
140
|
+
# Examples:
|
141
|
+
# - DateGen.bywday(2004, nil, 1, 9) => the ninth Sunday in 2004
|
142
|
+
# - DateGen.bywday(2004, nil, 1) => all Sundays in 2004
|
143
|
+
# - DateGen.bywday(2004, nil, 1, -2) => second last Sunday in 2004
|
144
|
+
# - DateGen.bywday(2004, 12, 1) => all sundays in December 2004
|
145
|
+
# - DateGen.bywday(2004, 2, 2, -1) => last Tuesday in February in 2004
|
146
|
+
# - DateGen.bywday(2004, -2, 3, -2) => second last Wednesday in November of 2004
|
147
|
+
#
|
148
|
+
# Compare to Date.bywday(), which allows a single Date to be created with
|
149
|
+
# similar criteria.
|
150
|
+
def DateGen.bywday(year, month, wday, n = nil)
|
151
|
+
seed = Date.bywday(year, month, wday, n ? n : 1)
|
152
|
+
|
153
|
+
dates = [ seed ]
|
154
|
+
|
155
|
+
return dates if n
|
156
|
+
|
157
|
+
succ = seed.clone
|
158
|
+
|
159
|
+
# Collect all matches until we're out of the year (or month, if specified)
|
160
|
+
loop do
|
161
|
+
succ += 7
|
162
|
+
|
163
|
+
break if succ.year != year
|
164
|
+
break if month && succ.month != seed.month
|
165
|
+
|
166
|
+
dates.push succ
|
167
|
+
end
|
168
|
+
dates.sort!
|
169
|
+
dates
|
170
|
+
end
|
171
|
+
|
172
|
+
# Generate an array of dates on +mday+ (the day-of-month, 1-31). For months
|
173
|
+
# in which the +mday+ is not present, no date will be generated.
|
174
|
+
#
|
175
|
+
# The period is a year, unless +month+ is non-nil, in which case it is just
|
176
|
+
# that month.
|
177
|
+
#
|
178
|
+
# Compare to Date.new(), which allows a single Date to be created with
|
179
|
+
# similar criteria.
|
180
|
+
def DateGen.bymonthday(year, month, mday)
|
181
|
+
months = month ? [ month ] : 1..12
|
182
|
+
dates = [ ]
|
183
|
+
|
184
|
+
months.each do |m|
|
185
|
+
begin
|
186
|
+
dates << Date.new(year, m, mday)
|
187
|
+
rescue ArgumentError
|
188
|
+
# Don't generate dates for invalid combinations (Feb 29, when it's not
|
189
|
+
# a leap year, for example).
|
190
|
+
#
|
191
|
+
# TODO - should we raise when month is out of range, or mday can never
|
192
|
+
# be in range (32)?
|
193
|
+
end
|
194
|
+
end
|
195
|
+
dates
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
data/lib/vpim/dirinfo.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
=begin
|
2
|
-
|
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
|
@@ -100,6 +98,7 @@ module Vpim
|
|
100
98
|
# The value of the first field named +name+, or nil if no
|
101
99
|
# match is found.
|
102
100
|
def [](name)
|
101
|
+
enum_by_name(name).each { |f| return f.value if f.value != ''}
|
103
102
|
enum_by_name(name).each { |f| return f.value }
|
104
103
|
nil
|
105
104
|
end
|
@@ -136,10 +135,18 @@ module Vpim
|
|
136
135
|
|
137
136
|
# Returns an Enumerator for each Field for which #name?(+name+) is true.
|
138
137
|
#
|
139
|
-
#
|
140
|
-
#
|
138
|
+
# An Enumerator supports all the methods of Enumerable, so it allows iteration,
|
139
|
+
# collection, mapping, etc.
|
140
|
+
#
|
141
|
+
# Examples:
|
142
|
+
#
|
143
|
+
# Print all the nicknames in a card:
|
144
|
+
#
|
145
|
+
# card.enum_by_name('NICKNAME') { |f| puts f.value }
|
146
|
+
#
|
147
|
+
# Print an Array of the preferred email addresses in the card:
|
141
148
|
#
|
142
|
-
# card.enum_by_name('
|
149
|
+
# pref_emails = card.enum_by_name('EMAIL').select { |f| f.pref? }
|
143
150
|
def enum_by_name(name)
|
144
151
|
Enumerator.new(self, Proc.new { |field| field.name?(name) })
|
145
152
|
end
|
@@ -148,15 +155,15 @@ module Vpim
|
|
148
155
|
#
|
149
156
|
# For example, to print all the fields, sorted by group, you could do:
|
150
157
|
#
|
151
|
-
#
|
152
|
-
#
|
153
|
-
#
|
154
|
-
#
|
155
|
-
#
|
158
|
+
# card.groups.sort.each do |group|
|
159
|
+
# card.enum_by_group(group).each do |field|
|
160
|
+
# puts "#{group} -> #{field.name}"
|
161
|
+
# end
|
162
|
+
# end
|
156
163
|
#
|
157
164
|
# or to get an array of all the fields in group 'agroup', you could do:
|
158
165
|
#
|
159
|
-
#
|
166
|
+
# card.enum_by_group('agroup').to_a
|
160
167
|
def enum_by_group(group)
|
161
168
|
Enumerator.new(self, Proc.new { |field| field.group?(group) })
|
162
169
|
end
|
@@ -166,9 +173,15 @@ module Vpim
|
|
166
173
|
Enumerator.new(self, cond )
|
167
174
|
end
|
168
175
|
|
176
|
+
# Force card to be reencoded from the fields.
|
177
|
+
def dirty #:nodoc:
|
178
|
+
#string = nil
|
179
|
+
end
|
180
|
+
|
169
181
|
# Append +field+ to the fields. Note that it won't be literally appended
|
170
182
|
# to the fields, it will be inserted before the closing END field.
|
171
183
|
def push(field)
|
184
|
+
dirty
|
172
185
|
@fields[-1,0] = field
|
173
186
|
self
|
174
187
|
end
|
@@ -190,6 +203,25 @@ module Vpim
|
|
190
203
|
self
|
191
204
|
end
|
192
205
|
|
206
|
+
# Delete +field+.
|
207
|
+
#
|
208
|
+
# Warning: You can't delete BEGIN: or END: fields, but other
|
209
|
+
# profile-specific fields can be deleted, including mandatory ones. For
|
210
|
+
# vCards in particular, in order to avoid destroying them, I suggest
|
211
|
+
# creating a new Vcard, and copying over all the fields that you still
|
212
|
+
# want, rather than using #delete. This is easy with Maker::Vcard#copy, see
|
213
|
+
# the Maker::Vcard examples.
|
214
|
+
def delete(field)
|
215
|
+
case
|
216
|
+
when field.name?('BEGIN'), field.name?('END')
|
217
|
+
raise ArgumentError, 'Cannot delete BEGIN or END fields.'
|
218
|
+
else
|
219
|
+
@fields.delete field
|
220
|
+
end
|
221
|
+
|
222
|
+
self
|
223
|
+
end
|
224
|
+
|
193
225
|
# The string encoding of the DirectoryInfo. See Field#encode for information
|
194
226
|
# about the width parameter.
|
195
227
|
def encode(width=nil)
|
@@ -208,14 +240,14 @@ module Vpim
|
|
208
240
|
unless @fields.first
|
209
241
|
raise "No fields to check"
|
210
242
|
end
|
211
|
-
unless @fields.first.name?
|
243
|
+
unless @fields.first.name? 'BEGIN'
|
212
244
|
raise "Needs BEGIN, found: #{@fields.first.encode nil}"
|
213
245
|
end
|
214
|
-
unless @fields.last.name?
|
246
|
+
unless @fields.last.name? 'END'
|
215
247
|
raise "Needs END, found: #{@fields.last.encode nil}"
|
216
248
|
end
|
217
249
|
unless @fields.last.value? @fields.first.value
|
218
|
-
raise "BEGIN/END mismatch: (#{@fields.first.value
|
250
|
+
raise "BEGIN/END mismatch: (#{@fields.first.value} != #{@fields.last.value}"
|
219
251
|
end
|
220
252
|
if profile
|
221
253
|
if ! @fields.first.value? profile
|
@@ -0,0 +1,242 @@
|
|
1
|
+
=begin
|
2
|
+
$Id: dirinfo.rb,v 1.19 2004/11/17 05:06:27 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/enumerator'
|
12
|
+
require 'vpim/field'
|
13
|
+
require 'vpim/rfc2425'
|
14
|
+
require 'vpim/vpim'
|
15
|
+
|
16
|
+
module Vpim
|
17
|
+
# An RFC 2425 directory info object.
|
18
|
+
#
|
19
|
+
# A directory information object is a sequence of fields. The basic
|
20
|
+
# structure of the object, and the way in which it is broken into fields
|
21
|
+
# is common to all profiles of the directory info type.
|
22
|
+
#
|
23
|
+
# A vCard, for example, is a specialization of a directory info object.
|
24
|
+
#
|
25
|
+
# [RFC2425] the directory information framework (ftp://ftp.ietf.org/rfc/rfc2425.txt)
|
26
|
+
class DirectoryInfo
|
27
|
+
include Enumerable
|
28
|
+
|
29
|
+
private_class_method :new
|
30
|
+
|
31
|
+
# Initialize a DirectoryInfo object from +fields+. If +profile+ is
|
32
|
+
# specified, check the BEGIN/END fields.
|
33
|
+
def initialize(fields, profile = nil) #:nodoc:
|
34
|
+
if fields.detect { |f| ! f.kind_of? DirectoryInfo::Field }
|
35
|
+
raise ArgumentError, 'fields must be an array of DirectoryInfo::Field objects'
|
36
|
+
end
|
37
|
+
|
38
|
+
@string = nil # this is used as a flag to indicate that recoding will be necessary
|
39
|
+
@fields = fields
|
40
|
+
|
41
|
+
check_begin_end(profile) if profile
|
42
|
+
end
|
43
|
+
|
44
|
+
# Decode +card+ into a DirectoryInfo object.
|
45
|
+
#
|
46
|
+
# +card+ may either be a something that is convertible to a string using
|
47
|
+
# #to_str or an array of objects that can be joined into a string using
|
48
|
+
# #join("\n"), or an IO object (which will be read to end-of-file).
|
49
|
+
#
|
50
|
+
# The lines in the string may be delimited using IETF (CRLF) or Unix (LF) conventions.
|
51
|
+
#
|
52
|
+
# A DirectoryInfo is mutable, you can add new fields to it, see
|
53
|
+
# Vpim::DirectoryInfo::Field#create() for how to create a new Field.
|
54
|
+
#
|
55
|
+
# TODO: I don't believe this is ever used, maybe I can remove it.
|
56
|
+
def DirectoryInfo.decode(card) #:nodoc:
|
57
|
+
if card.respond_to? :to_str
|
58
|
+
string = card.to_str
|
59
|
+
elsif card.kind_of? Array
|
60
|
+
string = card.join("\n")
|
61
|
+
elsif card.kind_of? IO
|
62
|
+
string = card.read(nil)
|
63
|
+
else
|
64
|
+
raise ArgumentError, "DirectoryInfo cannot be created from a #{card.type}"
|
65
|
+
end
|
66
|
+
|
67
|
+
fields = Vpim.decode(string)
|
68
|
+
|
69
|
+
new(fields)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Create a new DirectoryInfo object. The +fields+ are an optional array of
|
73
|
+
# DirectoryInfo::Field objects to add to the new object, between the
|
74
|
+
# BEGIN/END. If the +profile+ string is not nil, then it is the name of
|
75
|
+
# the directory info profile, and the BEGIN:+profile+/END:+profile+ fields
|
76
|
+
# will be added.
|
77
|
+
#
|
78
|
+
# A DirectoryInfo is mutable, you can add new fields to it using #push(),
|
79
|
+
# and see Field#create().
|
80
|
+
def DirectoryInfo.create(fields = [], profile = nil)
|
81
|
+
|
82
|
+
if profile
|
83
|
+
p = profile.to_str
|
84
|
+
f = [ Field.create('BEGIN', p) ]
|
85
|
+
f.concat fields
|
86
|
+
f.push Field.create('END', p)
|
87
|
+
fields = f
|
88
|
+
end
|
89
|
+
|
90
|
+
new(fields, profile)
|
91
|
+
end
|
92
|
+
|
93
|
+
# The first field named +name+, or nil if no
|
94
|
+
# match is found.
|
95
|
+
def field(name)
|
96
|
+
enum_by_name(name).each { |f| return f }
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
# The value of the first field named +name+, or nil if no
|
101
|
+
# match is found.
|
102
|
+
def [](name)
|
103
|
+
enum_by_name(name).each { |f| return f.value if f.value != ''}
|
104
|
+
enum_by_name(name).each { |f| return f.value }
|
105
|
+
nil
|
106
|
+
end
|
107
|
+
|
108
|
+
# An array of all the values of fields named +name+, converted to text
|
109
|
+
# (using Field#to_text()).
|
110
|
+
#
|
111
|
+
# TODO - call this #texts(), as in the plural?
|
112
|
+
def text(name)
|
113
|
+
enum_by_name(name).map { |f| f.to_text }
|
114
|
+
end
|
115
|
+
|
116
|
+
# Array of all the Field#group()s.
|
117
|
+
def groups
|
118
|
+
@fields.collect { |f| f.group } .compact.uniq
|
119
|
+
end
|
120
|
+
|
121
|
+
# All fields, frozen.
|
122
|
+
def fields #:nodoc:
|
123
|
+
@fields.dup.freeze
|
124
|
+
end
|
125
|
+
|
126
|
+
# Yields for each Field for which +cond+.call(field) is true. The
|
127
|
+
# (default) +cond+ of nil is considered true for all fields, so
|
128
|
+
# this acts like a normal #each() when called with no arguments.
|
129
|
+
def each(cond = nil) # :yields: Field
|
130
|
+
@fields.each do |field|
|
131
|
+
if(cond == nil || cond.call(field))
|
132
|
+
yield field
|
133
|
+
end
|
134
|
+
end
|
135
|
+
self
|
136
|
+
end
|
137
|
+
|
138
|
+
# Returns an Enumerator for each Field for which #name?(+name+) is true.
|
139
|
+
#
|
140
|
+
# An Enumerator supports all the methods of Enumerable, so it allows iteration,
|
141
|
+
# collection, mapping, etc.
|
142
|
+
#
|
143
|
+
# Examples:
|
144
|
+
#
|
145
|
+
# Print all the nicknames in a card:
|
146
|
+
#
|
147
|
+
# card.enum_by_name('nickname') { |f| puts f.value }
|
148
|
+
#
|
149
|
+
# Get an Array of the preferred email addresses in the card:
|
150
|
+
#
|
151
|
+
# card.enum_by_name('email').collect { |f| f.pref? ? f.value : nil }.compact
|
152
|
+
# FIXME - make sure this works!
|
153
|
+
def enum_by_name(name)
|
154
|
+
Enumerator.new(self, Proc.new { |field| field.name?(name) })
|
155
|
+
end
|
156
|
+
|
157
|
+
# Returns an Enumerator for each Field for which #group?(+group+) is true.
|
158
|
+
#
|
159
|
+
# For example, to print all the fields, sorted by group, you could do:
|
160
|
+
#
|
161
|
+
# card.groups.sort.each do |group|
|
162
|
+
# card.enum_by_group(group).each do |field|
|
163
|
+
# puts "#{group} -> #{field.name}"
|
164
|
+
# end
|
165
|
+
# end
|
166
|
+
#
|
167
|
+
# or to get an array of all the fields in group 'agroup', you could do:
|
168
|
+
#
|
169
|
+
# card.enum_by_group('agroup').to_a
|
170
|
+
def enum_by_group(group)
|
171
|
+
Enumerator.new(self, Proc.new { |field| field.group?(group) })
|
172
|
+
end
|
173
|
+
|
174
|
+
# Returns an Enumerator for each Field for which +cond+.call(field) is true.
|
175
|
+
def enum_by_cond(cond)
|
176
|
+
Enumerator.new(self, cond )
|
177
|
+
end
|
178
|
+
|
179
|
+
# Append +field+ to the fields. Note that it won't be literally appended
|
180
|
+
# to the fields, it will be inserted before the closing END field.
|
181
|
+
def push(field)
|
182
|
+
@fields[-1,0] = field
|
183
|
+
self
|
184
|
+
end
|
185
|
+
|
186
|
+
alias << push
|
187
|
+
|
188
|
+
# Push +field+ onto the fields, unless there is already a field
|
189
|
+
# with this name.
|
190
|
+
def push_unique(field)
|
191
|
+
push(field) unless @fields.detect { |f| f.name? field.name }
|
192
|
+
self
|
193
|
+
end
|
194
|
+
|
195
|
+
# Append +field+ to the end of all the fields. This isn't usually what you
|
196
|
+
# want to do, usually a DirectoryInfo's first and last fields are a
|
197
|
+
# BEGIN/END pair, see #push().
|
198
|
+
def push_end(field)
|
199
|
+
@fields << field
|
200
|
+
self
|
201
|
+
end
|
202
|
+
|
203
|
+
def delete(field)
|
204
|
+
end
|
205
|
+
|
206
|
+
# The string encoding of the DirectoryInfo. See Field#encode for information
|
207
|
+
# about the width parameter.
|
208
|
+
def encode(width=nil)
|
209
|
+
unless @string
|
210
|
+
@string = @fields.collect { |f| f.encode(width) } . join ""
|
211
|
+
end
|
212
|
+
@string
|
213
|
+
end
|
214
|
+
|
215
|
+
alias to_s encode
|
216
|
+
|
217
|
+
# Check that the DirectoryInfo object is correctly delimited by a BEGIN
|
218
|
+
# and END, that their profile values match, and if +profile+ is specified, that
|
219
|
+
# they are the specified profile.
|
220
|
+
def check_begin_end(profile=nil) #:nodoc:
|
221
|
+
unless @fields.first
|
222
|
+
raise "No fields to check"
|
223
|
+
end
|
224
|
+
unless @fields.first.name? "begin"
|
225
|
+
raise "Needs BEGIN, found: #{@fields.first.encode nil}"
|
226
|
+
end
|
227
|
+
unless @fields.last.name? "end"
|
228
|
+
raise "Needs END, found: #{@fields.last.encode nil}"
|
229
|
+
end
|
230
|
+
unless @fields.last.value? @fields.first.value
|
231
|
+
raise "BEGIN/END mismatch: (#{@fields.first.value.downcase} != #{@fields.last.value.downcase}"
|
232
|
+
end
|
233
|
+
if profile
|
234
|
+
if ! @fields.first.value? profile
|
235
|
+
raise "Mismatched profile"
|
236
|
+
end
|
237
|
+
end
|
238
|
+
true
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|