vpim 0.16 → 0.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- $Id: date.rb,v 1.8 2004/11/17 05:06:27 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/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
- $Id: dirinfo.rb,v 1.19 2004/11/17 05:06:27 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
@@ -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
- # For example, to get an Array of all the email addresses in the card, you
140
- # could do:
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('email').collect { |f| f.value }
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
- # card.groups.sort.each do |group|
152
- # card.enum_by_group(group).each do |field|
153
- # puts "#{group} -> #{field.name}"
154
- # end
155
- # end
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
- # card.enum_by_group('agroup').to_a
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? "begin"
243
+ unless @fields.first.name? 'BEGIN'
212
244
  raise "Needs BEGIN, found: #{@fields.first.encode nil}"
213
245
  end
214
- unless @fields.last.name? "end"
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.downcase} != #{@fields.last.value.downcase}"
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
+