fairtilizer-vpim 0.695

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. data/CHANGES +510 -0
  2. data/COPYING +58 -0
  3. data/README +182 -0
  4. data/lib/vpim.rb +13 -0
  5. data/lib/vpim/address.rb +219 -0
  6. data/lib/vpim/agent/atomize.rb +104 -0
  7. data/lib/vpim/agent/base.rb +73 -0
  8. data/lib/vpim/agent/calendars.rb +173 -0
  9. data/lib/vpim/agent/handler.rb +26 -0
  10. data/lib/vpim/agent/ics.rb +161 -0
  11. data/lib/vpim/attachment.rb +102 -0
  12. data/lib/vpim/date.rb +222 -0
  13. data/lib/vpim/dirinfo.rb +277 -0
  14. data/lib/vpim/duration.rb +119 -0
  15. data/lib/vpim/enumerator.rb +32 -0
  16. data/lib/vpim/field.rb +614 -0
  17. data/lib/vpim/icalendar.rb +384 -0
  18. data/lib/vpim/maker/vcard.rb +16 -0
  19. data/lib/vpim/property/base.rb +193 -0
  20. data/lib/vpim/property/common.rb +315 -0
  21. data/lib/vpim/property/location.rb +38 -0
  22. data/lib/vpim/property/priority.rb +43 -0
  23. data/lib/vpim/property/recurrence.rb +69 -0
  24. data/lib/vpim/property/resources.rb +24 -0
  25. data/lib/vpim/repo.rb +261 -0
  26. data/lib/vpim/rfc2425.rb +367 -0
  27. data/lib/vpim/rrule.rb +591 -0
  28. data/lib/vpim/time.rb +40 -0
  29. data/lib/vpim/vcard.rb +1426 -0
  30. data/lib/vpim/version.rb +18 -0
  31. data/lib/vpim/vevent.rb +187 -0
  32. data/lib/vpim/view.rb +90 -0
  33. data/lib/vpim/vjournal.rb +58 -0
  34. data/lib/vpim/vpim.rb +65 -0
  35. data/lib/vpim/vtodo.rb +103 -0
  36. data/samples/README.mutt +93 -0
  37. data/samples/ab-query.rb +57 -0
  38. data/samples/agent.ru +10 -0
  39. data/samples/cmd-itip.rb +156 -0
  40. data/samples/ex_cpvcard.rb +55 -0
  41. data/samples/ex_get_vcard_photo.rb +22 -0
  42. data/samples/ex_mkv21vcard.rb +34 -0
  43. data/samples/ex_mkvcard.rb +64 -0
  44. data/samples/ex_mkyourown.rb +29 -0
  45. data/samples/ics-dump.rb +210 -0
  46. data/samples/ics-to-rss.rb +84 -0
  47. data/samples/mutt-aliases-to-vcf.rb +45 -0
  48. data/samples/osx-wrappers.rb +86 -0
  49. data/samples/reminder.rb +209 -0
  50. data/samples/rrule.rb +71 -0
  51. data/samples/tabbed-file-to-vcf.rb +390 -0
  52. data/samples/vcf-dump.rb +86 -0
  53. data/samples/vcf-lines.rb +61 -0
  54. data/samples/vcf-to-ics.rb +22 -0
  55. data/samples/vcf-to-mutt.rb +121 -0
  56. data/test/test_agent_atomize.rb +84 -0
  57. data/test/test_agent_calendars.rb +128 -0
  58. data/test/test_agent_ics.rb +96 -0
  59. data/test/test_all.rb +17 -0
  60. data/test/test_date.rb +120 -0
  61. data/test/test_dur.rb +41 -0
  62. data/test/test_field.rb +156 -0
  63. data/test/test_ical.rb +437 -0
  64. data/test/test_misc.rb +13 -0
  65. data/test/test_repo.rb +129 -0
  66. data/test/test_rrule.rb +1030 -0
  67. data/test/test_vcard.rb +973 -0
  68. data/test/test_view.rb +79 -0
  69. metadata +140 -0
@@ -0,0 +1,102 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'vpim/icalendar'
10
+
11
+ module Vpim
12
+
13
+ # Attachments are used by both iCalendar and vCard. They are either a URI or
14
+ # inline data, and their decoded value will be either a Uri or a Inline, as
15
+ # appropriate.
16
+ #
17
+ # Besides the methods specific to their class, both kinds of object implement
18
+ # a set of common methods, allowing them to be treated uniformly:
19
+ # - Uri#to_io, Inline#to_io: return an IO from which the value can be read.
20
+ # - Uri#to_s, Inline#to_s: return the value as a String.
21
+ # - Uri#format, Inline#format: the format of the value. This is supposed to
22
+ # be an "iana defined" identifier (like "image/jpeg"), but could be almost
23
+ # anything (or nothing) in practice. Since the parameter is optional, it may
24
+ # be "".
25
+ #
26
+ # The objects can also be distinguished by their class, if necessary.
27
+ module Attachment
28
+
29
+ # TODO - It might be possible to autodetect the format from the first few
30
+ # bytes of the value, and return the appropriate MIME type when format
31
+ # isn't defined.
32
+ #
33
+ # iCalendar and vCard put the format in different parameters, and the
34
+ # default kind of value is different.
35
+ def Attachment.decode(field, defkind, fmtparam) #:nodoc:
36
+ format = field.pvalue(fmtparam) || ''
37
+ kind = field.kind || defkind
38
+ case kind
39
+ when 'text'
40
+ Inline.new(Vpim.decode_text(field.value), format)
41
+ when 'uri'
42
+ Uri.new(field.value_raw, format)
43
+ when 'binary'
44
+ Inline.new(field.value, format)
45
+ else
46
+ raise InvalidEncodingError, "Attachment of type #{kind} is not allowed"
47
+ end
48
+ end
49
+
50
+ # Extends a String to support some of the same methods as Uri.
51
+ class Inline < String
52
+ def initialize(s, format) #:nodoc:
53
+ @format = format
54
+ super(s)
55
+ end
56
+
57
+ # Return an IO object for the inline data. See +stringio+ for more
58
+ # information.
59
+ def to_io
60
+ StringIO.new(self)
61
+ end
62
+
63
+ # The format of the inline data.
64
+ # See Attachment.
65
+ attr_reader :format
66
+ end
67
+
68
+ # Encapsulates a URI and implements some methods of String.
69
+ class Uri
70
+ def initialize(uri, format) #:nodoc:
71
+ @uri = uri
72
+ @format = format
73
+ end
74
+
75
+ # The URI value.
76
+ attr_reader :uri
77
+
78
+ # The format of the data referred to by the URI.
79
+ # See Attachment.
80
+ attr_reader :format
81
+
82
+ # Return an IO object from opening the URI. See +open-uri+ for more
83
+ # information.
84
+ def to_io
85
+ open(@uri)
86
+ end
87
+
88
+ # Return the String from reading the IO object to end-of-data.
89
+ def to_s
90
+ to_io.read(nil)
91
+ end
92
+
93
+ def inspect #:nodoc:
94
+ s = "<#{self.class.to_s}: #{uri.inspect}>"
95
+ s << ", #{@format.inspect}" if @format
96
+ s
97
+ end
98
+ end
99
+
100
+ end
101
+ end
102
+
@@ -0,0 +1,222 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'date'
10
+
11
+ # Extensions to the standard library Date.
12
+ class Date
13
+
14
+ TIME_START = Date.new(1970, 1, 1)
15
+ SECS_PER_DAY = 24 * 60 * 60
16
+
17
+ # Converts this object to a Time object, or throws an ArgumentError if
18
+ # conversion is not possible because it is before the start of epoch.
19
+ def vpim_to_time
20
+ raise ArgumentError, 'date is before the start of system time' if self < TIME_START
21
+ days = self - TIME_START
22
+
23
+ Time.at((days * SECS_PER_DAY).to_i)
24
+ end
25
+
26
+ # If wday responds to to_str, convert it to the wday number by searching for
27
+ # a wday that matches, using as many characters as are in wday to do the
28
+ # comparison. wday must be 2 or more characters long in order to be a unique
29
+ # match, other than that, "mo", "Mon", and "MonDay" are all valid strings
30
+ # for wday 1.
31
+ #
32
+ # This method can be called on a valid wday, and it will return it. Perhaps
33
+ # it should be called by default inside the Date#new*() methods so that
34
+ # non-integer wday arguments can be used? Perhaps a similar method should
35
+ # exist for months? But with months, we all know January is 1, who can
36
+ # remember where Date chooses to start its wday count!
37
+ #
38
+ # Examples:
39
+ # Date.bywday(2004, 2, Date.str2wday('TU')) => the first Tuesday in
40
+ # February
41
+ # Date.bywday(2004, 2, Date.str2wday(2)) => the same day, but notice
42
+ # that a valid wday integer can be passed right through.
43
+ #
44
+ def Date.str2wday(wdaystr)
45
+ return wdaystr unless wdaystr.respond_to? :to_str
46
+
47
+ str = wdaystr.to_str.upcase
48
+ if str.length < 2
49
+ raise ArgumentError, 'wday #{wday} is not long enough to be a unique weekday name'
50
+ end
51
+
52
+ wday = Date::DAYNAMES.map { |n| n.slice(0, str.length).upcase }.index(str)
53
+
54
+ return wday if wday
55
+
56
+ raise ArgumentError, 'wday #{wdaystr} was not a recognizable weekday name'
57
+ end
58
+
59
+
60
+ # Create a new Date object for the date specified by year +year+, month
61
+ # +mon+, and day-of-the-week +wday+.
62
+ #
63
+ # The nth, +n+, occurrence of +wday+ within the period will be generated
64
+ # (+n+ defaults to 1). If +n+ is positive, the nth occurrence from the
65
+ # beginning of the period will be returned, if negative, the nth occurrence
66
+ # from the end of the period will be returned.
67
+ #
68
+ # The period is a year, unless +month+ is non-nil, in which case it is just
69
+ # that month.
70
+ #
71
+ # Examples:
72
+ # - Date.bywday(2004, nil, 1, 9) => the ninth Sunday of 2004
73
+ # - Date.bywday(2004, nil, 1) => the first Sunday of 2004
74
+ # - Date.bywday(2004, nil, 1, -2) => the second last Sunday of 2004
75
+ # - Date.bywday(2004, 12, 1) => the first sunday in the 12th month of 2004
76
+ # - Date.bywday(2004, 2, 2, -1) => last Tuesday in the 2nd month in 2004
77
+ # - Date.bywday(2004, -2, 3, -2) => second last Wednesday in the second last month of 2004
78
+ #
79
+ # Compare this to Date.new, which allows a Date to be created by
80
+ # day-of-the-month, mday, to Date.ordinal, which allows a Date to be created by
81
+ # day-of-the-year, yday, and to Date.commercial, which allows a Date to be created
82
+ # by day-of-the-week, but within a specific week.
83
+ def Date.bywday(year, mon, wday, n = 1, sg=Date::ITALY)
84
+ # Normalize mon to 1-12.
85
+ if mon
86
+ if mon > 12 || mon == 0 || mon < -12
87
+ raise ArgumentError, "mon #{mon} must be 1-12 or negative 1-12"
88
+ end
89
+ if mon < 0
90
+ mon = 13 + mon
91
+ end
92
+ end
93
+ if wday < 0 || wday > 6
94
+ raise ArgumentError, 'wday must be in range 0-6, or a weekday name'
95
+ end
96
+
97
+ # Determine direction of indexing.
98
+ inc = n <=> 0
99
+ if inc == 0
100
+ raise ArgumentError, 'n must be greater or less than zero'
101
+ end
102
+
103
+ # if !mon, n is index into year, but direction of search is determined by
104
+ # sign of n
105
+ d = Date.new(year, mon ? mon : inc, inc, sg)
106
+
107
+ while d.wday != wday
108
+ d += inc
109
+ end
110
+
111
+ # Now we have found the first/last day with the correct wday, search
112
+ # for nth occurrence, by jumping by n.abs-1 weeks forward or backward.
113
+ d += 7 * (n.abs - 1) * inc
114
+
115
+ if d.year != year
116
+ raise ArgumentError, 'n is out of bounds of year'
117
+ end
118
+ if mon && d.mon != mon
119
+ raise ArgumentError, 'n is out of bounds of month'
120
+ end
121
+ d
122
+ end
123
+
124
+ # Return the first day of the week for the specified date. Commercial weeks
125
+ # start on Monday, but the weekstart can be specified (as 0-6, where 0 is
126
+ # sunday, or in formate of Date.str2day).
127
+ def Date.weekstart(year, mon, day, weekstart="MO")
128
+ wkst = Date.str2wday(weekstart)
129
+ d = Date.new(year, mon, day)
130
+ until d.wday == wkst
131
+ d = d - 1
132
+ end
133
+ d
134
+ end
135
+ end
136
+
137
+ # DateGen generates arrays of dates matching simple criteria.
138
+ class DateGen
139
+
140
+ # Generate an array of a week's dates, where week is specified by year, mon,
141
+ # day, and the weekstart (the day-of-week that is considered the "first" day
142
+ # of that week, 0-6, where 0 is sunday).
143
+ def DateGen.weekofdate(year, mon, day, weekstart)
144
+ d = Date.weekstart(year, mon, day, weekstart)
145
+ week = []
146
+ 7.times do
147
+ week << d
148
+ d = d + 1
149
+ end
150
+ week
151
+ end
152
+
153
+ # Generate an array of dates on +wday+ (the day-of-week,
154
+ # 0-6, where 0 is Sunday).
155
+ #
156
+ # If +n+ is specified, only the nth occurrence of +wday+ within the period
157
+ # will be generated. If +n+ is positive, the nth occurrence from the
158
+ # beginning of the period will be returned, if negative, the nth occurrence
159
+ # from the end of the period will be returned.
160
+ #
161
+ # The period is a year, unless +month+ is non-nil, in which case it is just
162
+ # that month.
163
+ #
164
+ # Examples:
165
+ # - DateGen.bywday(2004, nil, 1, 9) => the ninth Sunday in 2004
166
+ # - DateGen.bywday(2004, nil, 1) => all Sundays in 2004
167
+ # - DateGen.bywday(2004, nil, 1, -2) => second last Sunday in 2004
168
+ # - DateGen.bywday(2004, 12, 1) => all sundays in December 2004
169
+ # - DateGen.bywday(2004, 2, 2, -1) => last Tuesday in February in 2004
170
+ # - DateGen.bywday(2004, -2, 3, -2) => second last Wednesday in November of 2004
171
+ #
172
+ # Compare to Date.bywday(), which allows a single Date to be created with
173
+ # similar criteria.
174
+ def DateGen.bywday(year, month, wday, n = nil)
175
+ seed = Date.bywday(year, month, wday, n ? n : 1)
176
+
177
+ dates = [ seed ]
178
+
179
+ return dates if n
180
+
181
+ succ = seed.clone
182
+
183
+ # Collect all matches until we're out of the year (or month, if specified)
184
+ loop do
185
+ succ += 7
186
+
187
+ break if succ.year != year
188
+ break if month && succ.month != seed.month
189
+
190
+ dates.push succ
191
+ end
192
+ dates.sort!
193
+ dates
194
+ end
195
+
196
+ # Generate an array of dates on +mday+ (the day-of-month, 1-31). For months
197
+ # in which the +mday+ is not present, no date will be generated.
198
+ #
199
+ # The period is a year, unless +month+ is non-nil, in which case it is just
200
+ # that month.
201
+ #
202
+ # Compare to Date.new(), which allows a single Date to be created with
203
+ # similar criteria.
204
+ def DateGen.bymonthday(year, month, mday)
205
+ months = month ? [ month ] : 1..12
206
+ dates = [ ]
207
+
208
+ months.each do |m|
209
+ begin
210
+ dates << Date.new(year, m, mday)
211
+ rescue ArgumentError
212
+ # Don't generate dates for invalid combinations (Feb 29, when it's not
213
+ # a leap year, for example).
214
+ #
215
+ # TODO - should we raise when month is out of range, or mday can never
216
+ # be in range (32)?
217
+ end
218
+ end
219
+ dates
220
+ end
221
+ end
222
+
@@ -0,0 +1,277 @@
1
+ =begin
2
+ Copyright (C) 2008 Sam Roberts
3
+
4
+ This library is free software; you can redistribute it and/or modify it
5
+ under the same terms as the ruby language itself, see the file COPYING for
6
+ details.
7
+ =end
8
+
9
+ require 'vpim/enumerator'
10
+ require 'vpim/field'
11
+ require 'vpim/rfc2425'
12
+ require 'vpim/vpim'
13
+
14
+ module Vpim
15
+ # An RFC 2425 directory info object.
16
+ #
17
+ # A directory information object is a sequence of fields. The basic
18
+ # structure of the object, and the way in which it is broken into fields
19
+ # is common to all profiles of the directory info type.
20
+ #
21
+ # A vCard, for example, is a specialization of a directory info object.
22
+ #
23
+ # - [RFC2425] the directory information framework (ftp://ftp.ietf.org/rfc/rfc2425.txt)
24
+ #
25
+ # Here's an example of encoding a simple vCard using the low-level APIs:
26
+ #
27
+ # card = Vpim::Vcard.create
28
+ # card << Vpim::DirectoryInfo::Field.create('EMAIL', 'user.name@example.com', 'TYPE' => 'INTERNET' )
29
+ # card << Vpim::DirectoryInfo::Field.create('URL', 'http://www.example.com/user' )
30
+ # card << Vpim::DirectoryInfo::Field.create('FN', 'User Name' )
31
+ # puts card.to_s
32
+ #
33
+ # Don't do it like that, use Vpim::Vcard::Maker.
34
+ class DirectoryInfo
35
+ include Enumerable
36
+
37
+ private_class_method :new
38
+
39
+ # Initialize a DirectoryInfo object from +fields+. If +profile+ is
40
+ # specified, check the BEGIN/END fields.
41
+ def initialize(fields, profile = nil) #:nodoc:
42
+ if fields.detect { |f| ! f.kind_of? DirectoryInfo::Field }
43
+ raise ArgumentError, 'fields must be an array of DirectoryInfo::Field objects'
44
+ end
45
+
46
+ @string = nil # this is used as a flag to indicate that recoding will be necessary
47
+ @fields = fields
48
+
49
+ check_begin_end(profile) if profile
50
+ end
51
+
52
+ # Decode +card+ into a DirectoryInfo object.
53
+ #
54
+ # +card+ may either be a something that is convertible to a string using
55
+ # #to_str or an Array of objects that can be joined into a string using
56
+ # #join("\n"), or an IO object (which will be read to end-of-file).
57
+ #
58
+ # The lines in the string may be delimited using IETF (CRLF) or Unix (LF) conventions.
59
+ #
60
+ # A DirectoryInfo is mutable, you can add new fields to it, see
61
+ # Vpim::DirectoryInfo::Field#create() for how to create a new Field.
62
+ #
63
+ # TODO: I don't believe this is ever used, maybe I can remove it.
64
+ def DirectoryInfo.decode(card) #:nodoc:
65
+ if card.respond_to? :to_str
66
+ string = card.to_str
67
+ elsif card.kind_of? Array
68
+ string = card.join("\n")
69
+ elsif card.kind_of? IO
70
+ string = card.read(nil)
71
+ else
72
+ raise ArgumentError, "DirectoryInfo cannot be created from a #{card.type}"
73
+ end
74
+
75
+ fields = Vpim.decode(string)
76
+
77
+ new(fields)
78
+ end
79
+
80
+ # Create a new DirectoryInfo object. The +fields+ are an optional array of
81
+ # DirectoryInfo::Field objects to add to the new object, between the
82
+ # BEGIN/END. If the +profile+ string is not nil, then it is the name of
83
+ # the directory info profile, and the BEGIN:+profile+/END:+profile+ fields
84
+ # will be added.
85
+ #
86
+ # A DirectoryInfo is mutable, you can add new fields to it using #push(),
87
+ # and see Field#create().
88
+ def DirectoryInfo.create(fields = [], profile = nil)
89
+
90
+ if profile
91
+ p = profile.to_str
92
+ f = [ Field.create('BEGIN', p) ]
93
+ f.concat fields
94
+ f.push Field.create('END', p)
95
+ fields = f
96
+ end
97
+
98
+ new(fields, profile)
99
+ end
100
+
101
+ # The first field named +name+, or nil if no
102
+ # match is found.
103
+ def field(name)
104
+ enum_by_name(name).each { |f| return f }
105
+ nil
106
+ end
107
+
108
+ # The value of the first field named +name+, or nil if no
109
+ # match is found.
110
+ def [](name)
111
+ enum_by_name(name).each { |f| return f.value if f.value != ''}
112
+ enum_by_name(name).each { |f| return f.value }
113
+ nil
114
+ end
115
+
116
+ # An array of all the values of fields named +name+, converted to text
117
+ # (using Field#to_text()).
118
+ #
119
+ # TODO - call this #texts(), as in the plural?
120
+ def text(name)
121
+ accum = []
122
+ each do |f|
123
+ if f.name? name
124
+ accum << f.to_text
125
+ end
126
+ end
127
+ accum
128
+ end
129
+
130
+ # Array of all the Field#group()s.
131
+ def groups
132
+ @fields.collect { |f| f.group } .compact.uniq
133
+ end
134
+
135
+ # All fields, frozen.
136
+ def fields #:nodoc:
137
+ @fields.dup.freeze
138
+ end
139
+
140
+ # Yields for each Field for which +cond+.call(field) is true. The
141
+ # (default) +cond+ of nil is considered true for all fields, so
142
+ # this acts like a normal #each() when called with no arguments.
143
+ def each(cond = nil) # :yields: Field
144
+ @fields.each do |field|
145
+ if(cond == nil || cond.call(field))
146
+ yield field
147
+ end
148
+ end
149
+ self
150
+ end
151
+
152
+ # Returns an Enumerator for each Field for which #name?(+name+) is true.
153
+ #
154
+ # An Enumerator supports all the methods of Enumerable, so it allows iteration,
155
+ # collection, mapping, etc.
156
+ #
157
+ # Examples:
158
+ #
159
+ # Print all the nicknames in a card:
160
+ #
161
+ # card.enum_by_name('NICKNAME') { |f| puts f.value }
162
+ #
163
+ # Print an Array of the preferred email addresses in the card:
164
+ #
165
+ # pref_emails = card.enum_by_name('EMAIL').select { |f| f.pref? }
166
+ def enum_by_name(name)
167
+ Enumerator.new(self, Proc.new { |field| field.name?(name) })
168
+ end
169
+
170
+ # Returns an Enumerator for each Field for which #group?(+group+) is true.
171
+ #
172
+ # For example, to print all the fields, sorted by group, you could do:
173
+ #
174
+ # card.groups.sort.each do |group|
175
+ # card.enum_by_group(group).each do |field|
176
+ # puts "#{group} -> #{field.name}"
177
+ # end
178
+ # end
179
+ #
180
+ # or to get an array of all the fields in group 'AGROUP', you could do:
181
+ #
182
+ # card.enum_by_group('AGROUP').to_a
183
+ def enum_by_group(group)
184
+ Enumerator.new(self, Proc.new { |field| field.group?(group) })
185
+ end
186
+
187
+ # Returns an Enumerator for each Field for which +cond+.call(field) is true.
188
+ def enum_by_cond(cond)
189
+ Enumerator.new(self, cond )
190
+ end
191
+
192
+ # Force card to be reencoded from the fields.
193
+ def dirty #:nodoc:
194
+ #string = nil
195
+ end
196
+
197
+ # Append +field+ to the fields. Note that it won't be literally appended
198
+ # to the fields, it will be inserted before the closing END field.
199
+ def push(field)
200
+ dirty
201
+ @fields[-1,0] = field
202
+ self
203
+ end
204
+
205
+ alias << push
206
+
207
+ # Push +field+ onto the fields, unless there is already a field
208
+ # with this name.
209
+ def push_unique(field)
210
+ push(field) unless @fields.detect { |f| f.name? field.name }
211
+ self
212
+ end
213
+
214
+ # Append +field+ to the end of all the fields. This isn't usually what you
215
+ # want to do, usually a DirectoryInfo's first and last fields are a
216
+ # BEGIN/END pair, see #push().
217
+ def push_end(field)
218
+ @fields << field
219
+ self
220
+ end
221
+
222
+ # Delete +field+.
223
+ #
224
+ # Warning: You can't delete BEGIN: or END: fields, but other
225
+ # profile-specific fields can be deleted, including mandatory ones. For
226
+ # vCards in particular, in order to avoid destroying them, I suggest
227
+ # creating a new Vcard, and copying over all the fields that you still
228
+ # want, rather than using #delete. This is easy with Vcard::Maker#copy, see
229
+ # the Vcard::Maker examples.
230
+ def delete(field)
231
+ case
232
+ when field.name?('BEGIN'), field.name?('END')
233
+ raise ArgumentError, 'Cannot delete BEGIN or END fields.'
234
+ else
235
+ @fields.delete field
236
+ end
237
+
238
+ self
239
+ end
240
+
241
+ # The string encoding of the DirectoryInfo. See Field#encode for information
242
+ # about the width parameter.
243
+ def encode(width=nil)
244
+ unless @string
245
+ @string = @fields.collect { |f| f.encode(width) } . join ""
246
+ end
247
+ @string
248
+ end
249
+
250
+ alias to_s encode
251
+
252
+ # Check that the DirectoryInfo object is correctly delimited by a BEGIN
253
+ # and END, that their profile values match, and if +profile+ is specified, that
254
+ # they are the specified profile.
255
+ def check_begin_end(profile=nil) #:nodoc:
256
+ unless @fields.first
257
+ raise "No fields to check"
258
+ end
259
+ unless @fields.first.name? 'BEGIN'
260
+ raise "Needs BEGIN, found: #{@fields.first.encode nil}"
261
+ end
262
+ unless @fields.last.name? 'END'
263
+ raise "Needs END, found: #{@fields.last.encode nil}"
264
+ end
265
+ unless @fields.last.value? @fields.first.value
266
+ raise "BEGIN/END mismatch: (#{@fields.first.value} != #{@fields.last.value}"
267
+ end
268
+ if profile
269
+ if ! @fields.first.value? profile
270
+ raise "Mismatched profile"
271
+ end
272
+ end
273
+ true
274
+ end
275
+ end
276
+ end
277
+