versionomy 0.0.4 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +31 -0
- data/README.rdoc +144 -0
- data/Rakefile +93 -12
- data/lib/versionomy/errors.rb +30 -11
- data/lib/versionomy/format/base.rb +96 -0
- data/lib/versionomy/format/delimiter.rb +951 -0
- data/lib/versionomy/format.rb +26 -112
- data/lib/versionomy/formats/standard.rb +346 -0
- data/lib/versionomy/formats.rb +79 -0
- data/lib/versionomy/interface.rb +23 -17
- data/lib/versionomy/schema/field.rb +500 -0
- data/lib/versionomy/schema/wrapper.rb +177 -0
- data/lib/versionomy/schema.rb +41 -500
- data/lib/versionomy/value.rb +129 -157
- data/lib/versionomy/version.rb +8 -8
- data/lib/versionomy.rb +25 -10
- data/tests/tc_custom_format.rb +66 -0
- data/tests/tc_readme_examples.rb +121 -0
- data/tests/tc_standard_basic.rb +17 -16
- data/tests/tc_standard_bump.rb +11 -10
- data/tests/tc_standard_change.rb +2 -1
- data/tests/tc_standard_comparison.rb +2 -1
- data/tests/tc_standard_parse.rb +41 -23
- metadata +28 -31
- data/History.txt +0 -21
- data/Manifest.txt +0 -17
- data/README.txt +0 -133
- data/lib/versionomy/standard.rb +0 -392
@@ -0,0 +1,951 @@
|
|
1
|
+
# -----------------------------------------------------------------------------
|
2
|
+
#
|
3
|
+
# Versionomy delimiter format
|
4
|
+
#
|
5
|
+
# -----------------------------------------------------------------------------
|
6
|
+
# Copyright 2008-2009 Daniel Azuma
|
7
|
+
#
|
8
|
+
# All rights reserved.
|
9
|
+
#
|
10
|
+
# Redistribution and use in source and binary forms, with or without
|
11
|
+
# modification, are permitted provided that the following conditions are met:
|
12
|
+
#
|
13
|
+
# * Redistributions of source code must retain the above copyright notice,
|
14
|
+
# this list of conditions and the following disclaimer.
|
15
|
+
# * Redistributions in binary form must reproduce the above copyright notice,
|
16
|
+
# this list of conditions and the following disclaimer in the documentation
|
17
|
+
# and/or other materials provided with the distribution.
|
18
|
+
# * Neither the name of the copyright holder, nor the names of any other
|
19
|
+
# contributors to this software, may be used to endorse or promote products
|
20
|
+
# derived from this software without specific prior written permission.
|
21
|
+
#
|
22
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
23
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
24
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
25
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
26
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
27
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
28
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
29
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
30
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
31
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
32
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
33
|
+
# -----------------------------------------------------------------------------
|
34
|
+
;
|
35
|
+
|
36
|
+
|
37
|
+
module Versionomy
|
38
|
+
|
39
|
+
module Format
|
40
|
+
|
41
|
+
|
42
|
+
# The Delimiter format class provides a DSL for building formats that
|
43
|
+
# can handle most cases where the fields of a version number appear
|
44
|
+
# consecutively in order in the string formatting. We expect most
|
45
|
+
# version number schemes should fall into this category.
|
46
|
+
#
|
47
|
+
# In general, the strategy is to provide, for each field, a set of
|
48
|
+
# regular expressions that recognize different formats for that field.
|
49
|
+
# Every field must be of the form "(pre)(value)(post)"
|
50
|
+
# where (pre) and (post) are delimiters preceding and
|
51
|
+
# following the value. Either or both delimiters may be the empty string.
|
52
|
+
#
|
53
|
+
# To parse a string, the string is scanned from left to right and
|
54
|
+
# matched against the format for the fields in order. If the string
|
55
|
+
# matches, that part of the string is consumed and the field value is
|
56
|
+
# interpreted from it. If the string does not match, and the field is
|
57
|
+
# not marked as "required", then the field is set to its default value
|
58
|
+
# and the next field is tried.
|
59
|
+
#
|
60
|
+
# During parsing, the actual delimiters, along with other information
|
61
|
+
# such as whether or not fields are required, are saved into a default
|
62
|
+
# set of parameters for unparsing. These are saved in the unparse_params
|
63
|
+
# of the version value, so that the version number can be unparsed in
|
64
|
+
# generally the same form. If the version number value is modified, this
|
65
|
+
# allows the unparsing of the new value to generally follow the format
|
66
|
+
# of the original string.
|
67
|
+
#
|
68
|
+
# Formats that use the Delimiter mechanism also provide support for
|
69
|
+
# certain parsing and unparsing parameters. See the documentation for
|
70
|
+
# the parse and unparse methods for details.
|
71
|
+
#
|
72
|
+
# For a usage example, see the definition of the standard format in
|
73
|
+
# Versionomy::Formats#_create_standard.
|
74
|
+
|
75
|
+
class Delimiter
|
76
|
+
|
77
|
+
|
78
|
+
# Create a format using delimiter tools.
|
79
|
+
# You should provide the version number schema, a set of default
|
80
|
+
# options, and a block.
|
81
|
+
#
|
82
|
+
# Within the block, you can call methods of
|
83
|
+
# Versionomy::Format::Delimiter::Builder
|
84
|
+
# to provide parsers for the fields of the schema. Any fields you do
|
85
|
+
# not explicitly configure will get parsed in a default manner.
|
86
|
+
|
87
|
+
def initialize(schema_, default_opts_={}, &block_)
|
88
|
+
# Special case used by modified_copy
|
89
|
+
if schema_.kind_of?(Delimiter)
|
90
|
+
orig_ = schema_
|
91
|
+
@schema = orig_.schema
|
92
|
+
@default_parse_params = orig_.default_parse_params
|
93
|
+
@default_unparse_params = orig_.default_unparse_params
|
94
|
+
@field_handlers = orig_.instance_variable_get(:@field_handlers).dup
|
95
|
+
builder_ = Delimiter::Builder.new(@schema, @field_handlers,
|
96
|
+
@default_parse_params, @default_unparse_params)
|
97
|
+
::Blockenspiel.invoke(block_, builder_)
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
101
|
+
@schema = schema_
|
102
|
+
@field_handlers = {}
|
103
|
+
@default_parse_params = {}
|
104
|
+
@default_unparse_params = {}
|
105
|
+
builder_ = Delimiter::Builder.new(@schema, @field_handlers,
|
106
|
+
@default_parse_params, @default_unparse_params)
|
107
|
+
::Blockenspiel.invoke(block_, builder_)
|
108
|
+
_interpret_field_lists(@default_unparse_params)
|
109
|
+
@schema.names.each do |name_|
|
110
|
+
@field_handlers[name_] ||= Delimiter::FieldHandler.new(@schema.field_named(name_), default_opts_)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
# Returns the schema understood by this format.
|
116
|
+
# This method is required by the Format contract.
|
117
|
+
|
118
|
+
def schema
|
119
|
+
@schema
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
# Parse the given string and return a value.
|
124
|
+
# This method is required by the Format contract.
|
125
|
+
#
|
126
|
+
# This method provides, out of the box, support for the following
|
127
|
+
# parse parameters:
|
128
|
+
#
|
129
|
+
# <tt>:extra_characters</tt>::
|
130
|
+
# Determines what to do if the entire string cannot be consumed by
|
131
|
+
# the parsing process. If set to <tt>:ignore</tt> (the default),
|
132
|
+
# any extra characters are ignored. If set to <tt>:suffix</tt>,
|
133
|
+
# the extra characters are set as the <tt>:suffix</tt> unparse
|
134
|
+
# parameter and are thus appended to the end of the string when
|
135
|
+
# unparsing takes place. If set to <tt>:error</tt>, causes a
|
136
|
+
# Versionomy::Errors::ParseError to be raised if there are
|
137
|
+
# uninterpreted characters.
|
138
|
+
|
139
|
+
def parse(string_, params_=nil)
|
140
|
+
values_ = {}
|
141
|
+
parse_params_ = default_parse_params
|
142
|
+
parse_params_.merge!(params_) if params_
|
143
|
+
parse_params_[:string] = string_
|
144
|
+
parse_params_[:previous_field_missing] = false
|
145
|
+
unparse_params_ = {}
|
146
|
+
field_ = @schema.root_field
|
147
|
+
while field_
|
148
|
+
handler_ = @field_handlers[field_.name]
|
149
|
+
v_ = handler_.parse(parse_params_, unparse_params_)
|
150
|
+
values_[field_.name] = v_
|
151
|
+
field_ = field_.child(v_)
|
152
|
+
end
|
153
|
+
if parse_params_[:string].length > 0
|
154
|
+
case parse_params_[:extra_characters]
|
155
|
+
when :error
|
156
|
+
raise Errors::ParseError, "Extra characters: #{parse_params_[:string].inspect}"
|
157
|
+
when :suffix
|
158
|
+
unparse_params_[:suffix] = parse_params_[:string]
|
159
|
+
end
|
160
|
+
end
|
161
|
+
Value.new(values_, self, unparse_params_)
|
162
|
+
end
|
163
|
+
|
164
|
+
|
165
|
+
# Unparse the given value and return a string.
|
166
|
+
# This method is required by the Format contract.
|
167
|
+
#
|
168
|
+
# This method provides, out of the box, support for the following
|
169
|
+
# unparse parameters:
|
170
|
+
#
|
171
|
+
# <tt>:suffix</tt>::
|
172
|
+
# A string to append to the unparsed string. Default is nothing.
|
173
|
+
# <tt>:required_fields</tt>::
|
174
|
+
# An array of field names that must be present in the unparsed
|
175
|
+
# string. These are generally fields with default_value_optional
|
176
|
+
# set, but that we want present in the string anyway. For example,
|
177
|
+
# in the version number "2.0.0", often the third field will be
|
178
|
+
# default_value_optional, but we can include it in the required
|
179
|
+
# fields passed to unparse to force it to appear in the string.
|
180
|
+
# <tt>:optional_fields</tt>::
|
181
|
+
# An array of field names that should have their presence in
|
182
|
+
# required_fields undone.
|
183
|
+
# <tt>:<i>fieldname</i>_required</tt>::
|
184
|
+
# This is an alternate way of specifying whether a potentially
|
185
|
+
# optional field should be required. Accepted values are true
|
186
|
+
# and false.
|
187
|
+
# <tt>:<i>fieldname</i>_style</tt>::
|
188
|
+
# Specify the style for unparsing the given field. See
|
189
|
+
# Versionomy::Format::Delimiter::Builder#field for more
|
190
|
+
# discussion of styles.
|
191
|
+
# <tt>:<i>fieldname</i>_delim</tt>::
|
192
|
+
# Set the pre-delimiter for the given field, if supported.
|
193
|
+
# Note that the string specified must be legal-- it must match the
|
194
|
+
# regexp for the field. If not, it will revert to the default.
|
195
|
+
# <tt>:<i>fieldname</i>_postdelim</tt>::
|
196
|
+
# Set the post-delimiter for the given field, if supported.
|
197
|
+
# Note that the string specified must be legal-- it must match the
|
198
|
+
# regexp for the field. If not, it will revert to the default.
|
199
|
+
# <tt>:<i>fieldname</i>_case</tt>::
|
200
|
+
# This is used by letter-formatted integer fields only, and
|
201
|
+
# sets the case to use while unparsing. Recognized values are
|
202
|
+
# <tt>:lower</tt> (the default), and <tt>:upper</tt>.
|
203
|
+
|
204
|
+
def unparse(value_, params_=nil)
|
205
|
+
unparse_params_ = value_.unparse_params || default_unparse_params
|
206
|
+
_interpret_field_lists(unparse_params_)
|
207
|
+
if params_
|
208
|
+
unparse_params_.merge!(params_)
|
209
|
+
_interpret_field_lists(unparse_params_)
|
210
|
+
end
|
211
|
+
unparse_params_.delete(:skipped_handler_list)
|
212
|
+
unparse_params_.delete(:required_for_later)
|
213
|
+
string_ = ''
|
214
|
+
value_.each_field_object do |field_, val_|
|
215
|
+
handler_ = @field_handlers[field_.name]
|
216
|
+
fragment_ = handler_.unparse(val_, unparse_params_)
|
217
|
+
if fragment_
|
218
|
+
list_ = unparse_params_.delete(:skipped_handler_list)
|
219
|
+
if list_ && handler_.requires_previous_field && !unparse_params_[:required_for_later]
|
220
|
+
unparse_params_[:required_for_later] = true
|
221
|
+
list_.each do |pair_|
|
222
|
+
frag_ = pair_[0].unparse(pair_[1], unparse_params_)
|
223
|
+
unless frag_
|
224
|
+
raise Errors::UnparseError, "Field #{field_.name} empty although a prerequisite for a later field"
|
225
|
+
end
|
226
|
+
string_ << frag_
|
227
|
+
end
|
228
|
+
unparse_params_[:required_for_later] = false
|
229
|
+
end
|
230
|
+
string_ << fragment_
|
231
|
+
else
|
232
|
+
if handler_.requires_previous_field
|
233
|
+
(unparse_params_[:skipped_handler_list] ||= []) << [handler_, val_]
|
234
|
+
else
|
235
|
+
unparse_params_[:skipped_handler_list] = [[handler_, val_]]
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
string_ << (unparse_params_[:suffix] || '')
|
240
|
+
string_
|
241
|
+
end
|
242
|
+
|
243
|
+
|
244
|
+
# Return a copy of the default parsing parameters used by this format.
|
245
|
+
# This hash cannot be edited in place. To modify the default parsing
|
246
|
+
# parameters, use modified_copy and call
|
247
|
+
# Versionomy::Format::Delimiter::Builder#default_parse_params in the block.
|
248
|
+
|
249
|
+
def default_parse_params
|
250
|
+
@default_parse_params.dup
|
251
|
+
end
|
252
|
+
|
253
|
+
|
254
|
+
# Return a copy of the default unparsing parameters used by this format.
|
255
|
+
# This hash cannot be edited in place. To modify the default unparsing
|
256
|
+
# parameters, use modified_copy and call
|
257
|
+
# Versionomy::Format::Delimiter::Builder#default_unparse_params in the block.
|
258
|
+
|
259
|
+
def default_unparse_params
|
260
|
+
@default_unparse_params.dup
|
261
|
+
end
|
262
|
+
|
263
|
+
|
264
|
+
# Create a copy of this format, with the modifications given in the
|
265
|
+
# provided block. You can call methods of Versionomy::Format::Delimiter::Builder
|
266
|
+
# in the block. Field handlers that you specify in that block will
|
267
|
+
# override and change the field handlers from the original. Any fields
|
268
|
+
# not specified in this block will use the handlers from the original.
|
269
|
+
|
270
|
+
def modified_copy(&block_)
|
271
|
+
Delimiter.new(self, &block_)
|
272
|
+
end
|
273
|
+
|
274
|
+
|
275
|
+
# A utility method that interprets required_fields and
|
276
|
+
# optional_fields parameters.
|
277
|
+
|
278
|
+
def _interpret_field_lists(unparse_params_) # :nodoc:
|
279
|
+
fields_ = unparse_params_.delete(:required_fields)
|
280
|
+
if fields_
|
281
|
+
fields_ = [fields_] unless fields_.kind_of?(Array)
|
282
|
+
fields_.each do |f_|
|
283
|
+
unparse_params_["#{f_}_required".to_sym] = true
|
284
|
+
end
|
285
|
+
end
|
286
|
+
fields_ = unparse_params_.delete(:optional_fields)
|
287
|
+
if fields_
|
288
|
+
fields_ = [fields_] unless fields_.kind_of?(Array)
|
289
|
+
fields_.each do |f_|
|
290
|
+
unparse_params_["#{f_}_required".to_sym] = false
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
private :_interpret_field_lists
|
295
|
+
|
296
|
+
|
297
|
+
# This class defines methods that you can call within the DSL block
|
298
|
+
# passed to Versionomy::Format::Delimiter#new.
|
299
|
+
#
|
300
|
+
# Generally, you call the field method of this class a number of times
|
301
|
+
# to define the formatting for each field.
|
302
|
+
|
303
|
+
class Builder
|
304
|
+
|
305
|
+
include ::Blockenspiel::DSL
|
306
|
+
|
307
|
+
def initialize(schema_, field_handlers_, default_parse_params_, default_unparse_params_) # :nodoc:
|
308
|
+
@schema = schema_
|
309
|
+
@field_handlers = field_handlers_
|
310
|
+
@default_parse_params = default_parse_params_
|
311
|
+
@default_unparse_params = default_unparse_params_
|
312
|
+
end
|
313
|
+
|
314
|
+
|
315
|
+
# Specify how to handle a given field.
|
316
|
+
# You must pass the name of the field, a hash of options, and a
|
317
|
+
# block defining the handling of the field.
|
318
|
+
#
|
319
|
+
# Within the block, you set up "recognizers" for various regular
|
320
|
+
# expression patterns. These recognizers are tested in order when
|
321
|
+
# parsing a version number.
|
322
|
+
#
|
323
|
+
# The methods that can be called from the block are determined by
|
324
|
+
# the type of field. If the field is an integer field, the methods
|
325
|
+
# of Versionomy::Format::Delimiter::IntegerFieldBuilder can be
|
326
|
+
# called from the block. If the field is a string field, the methods
|
327
|
+
# of Versionomy::Format::Delimiter::StringFieldBuilder can be
|
328
|
+
# called. If the field is a symbolic field, the methods of
|
329
|
+
# Versionomy::Format::Delimiter::SymbolFieldBuilder can be called.
|
330
|
+
#
|
331
|
+
# === Options
|
332
|
+
#
|
333
|
+
# The opts hash includes a number of options that control how the
|
334
|
+
# field is parsed.
|
335
|
+
#
|
336
|
+
# Some of these are regular expressions that indicate what patterns
|
337
|
+
# are recognized by the parser. Regular expressions should be passed
|
338
|
+
# in as the string representation of the regular expression, not a
|
339
|
+
# Regexp object itself. For example, use the string '-' rather than
|
340
|
+
# the Regexp /-/ to recognize a hyphen delimiter.
|
341
|
+
#
|
342
|
+
# The following options are recognized:
|
343
|
+
#
|
344
|
+
# <tt>:default_value_optional</tt>::
|
345
|
+
# If set to true, this the field may be omitted in the unparsed
|
346
|
+
# (formatted) version number, if the value is the default value
|
347
|
+
# for this field. However, if the following field is present and
|
348
|
+
# set as <tt>:requires_previous_field</tt>, then this field is
|
349
|
+
# still unparsed even if it is its default value.
|
350
|
+
# For example, for a version number like "2.0.0", often the third
|
351
|
+
# field is optional, but the first and second are required, so it
|
352
|
+
# will often be unparsed as "2.0".
|
353
|
+
# Default is false.
|
354
|
+
# <tt>:case_sensitive</tt>::
|
355
|
+
# If set to true, the regexps are case-sensitive. Default is false.
|
356
|
+
# <tt>:delimiter_regexp</tt>::
|
357
|
+
# The regular expression string for the pre-delimiter. This pattern
|
358
|
+
# must appear before the current value in the string, and is
|
359
|
+
# consumed when the field is parsed, but is not part of the value
|
360
|
+
# itself. Default is '\.' to recognize a period.
|
361
|
+
# <tt>:post_delimiter_regexp</tt>::
|
362
|
+
# The regular expression string for the post-delimiter. This pattern
|
363
|
+
# must appear before the current value in the string, and is
|
364
|
+
# consumed when the field is parsed, but is not part of the value
|
365
|
+
# itself. Default is '' to indicate no post-delimiter.
|
366
|
+
# <tt>:expected_follower_regexp</tt>::
|
367
|
+
# The regular expression string for what characters are expected to
|
368
|
+
# follow this field in the string. These characters are not part
|
369
|
+
# of the field itself, and are *not* consumed when the field is
|
370
|
+
# parsed; however, they must be present immediately after this
|
371
|
+
# field in order for the field to be recognized. Default is '' to
|
372
|
+
# indicate that we aren't testing for any particular characters.
|
373
|
+
# <tt>:default_delimiter</tt>::
|
374
|
+
# The default delimiter string. This is the string that is used
|
375
|
+
# to unparse a field value if the field was not present when the
|
376
|
+
# value was originally parsed. For example, if you parse the string
|
377
|
+
# "2.0", bump the tiny version so that the value is "2.0.1", and
|
378
|
+
# unparse, the unparsing won't receive the second period from
|
379
|
+
# parsing the original string, so its delimiter will use the default.
|
380
|
+
# Default value is '.'
|
381
|
+
# <tt>:default_post_delimiter</tt>::
|
382
|
+
# The default post-delimiter string. Default value is '' indicating
|
383
|
+
# no post-delimiter.
|
384
|
+
# <tt>:requires_previous_field</tt>::
|
385
|
+
# If set to true, this field's presence in a formatted version string
|
386
|
+
# requires the presence of the previous field. For example, in a
|
387
|
+
# typical version number "major.minor.tiny", tiny should appear in
|
388
|
+
# the string only if minor also appears, so tiny should have this
|
389
|
+
# parameter set to true. The default is true, so you must specify
|
390
|
+
# <tt>:requires_previous_field => false</tt> explicitly if you want
|
391
|
+
# a field not to require the previous field.
|
392
|
+
# <tt>:default_style</tt>::
|
393
|
+
# The default style for this field. This is the style used for
|
394
|
+
# unparsing if the value was not constructed by a parser or is
|
395
|
+
# otherwise missing the style for this field.
|
396
|
+
#
|
397
|
+
# === Styles
|
398
|
+
#
|
399
|
+
# A field may have different representation "styles". For example,
|
400
|
+
# you could represent a patchlevel of 1 as "1.0-1" or "1.0a".
|
401
|
+
# When a version number string is parsed, the parser and unparser
|
402
|
+
# work together to remember which style was parsed, and that style
|
403
|
+
# is used when the version number is unparsed.
|
404
|
+
#
|
405
|
+
# Specify styles as options to the calls made within the block that
|
406
|
+
# is passed to this method. In the above case, you could define the
|
407
|
+
# patchlevel field with a block that has two calls, one that uses
|
408
|
+
# Delimiter::IntegerFieldBuilder#recognize_number and passes the
|
409
|
+
# option <tt>:style => :number</tt>, and another that uses
|
410
|
+
# Delimiter::IntegerFieldBuilder#recognize_letter and passes the
|
411
|
+
# option <tt>:style => :letter</tt>.
|
412
|
+
#
|
413
|
+
# The standard format uses styles to preserve the different
|
414
|
+
# syntaxes for the release_type field. See the source code in
|
415
|
+
# Versionomy::Formats#_create_standard for this example.
|
416
|
+
|
417
|
+
def field(name_, opts_={}, &block_)
|
418
|
+
name_ = name_.to_sym
|
419
|
+
field_ = @schema.field_named(name_)
|
420
|
+
if !field_
|
421
|
+
raise Errors::FormatCreationError, "Unknown field name #{name_.inspect}"
|
422
|
+
end
|
423
|
+
@field_handlers[name_] = Delimiter::FieldHandler.new(field_, opts_, &block_)
|
424
|
+
end
|
425
|
+
|
426
|
+
|
427
|
+
# Set or modify the default parameters used when parsing a value.
|
428
|
+
|
429
|
+
def default_parse_params(params_)
|
430
|
+
@default_parse_params.merge!(params_)
|
431
|
+
end
|
432
|
+
|
433
|
+
|
434
|
+
# Set or modify the default parameters used when unparsing a value.
|
435
|
+
|
436
|
+
def default_unparse_params(params_)
|
437
|
+
@default_unparse_params.merge!(params_)
|
438
|
+
end
|
439
|
+
|
440
|
+
end
|
441
|
+
|
442
|
+
|
443
|
+
# This class defines methods that can be called from the block passed
|
444
|
+
# to Versionomy::Format::Delimiter::Builder#field if the field is
|
445
|
+
# of integer type.
|
446
|
+
|
447
|
+
class IntegerFieldBuilder
|
448
|
+
|
449
|
+
include ::Blockenspiel::DSL
|
450
|
+
|
451
|
+
def initialize(recognizers_, field_, default_opts_) # :nodoc:
|
452
|
+
@recognizers = recognizers_
|
453
|
+
@field = field_
|
454
|
+
@default_opts = default_opts_
|
455
|
+
end
|
456
|
+
|
457
|
+
|
458
|
+
# Recognize a numeric-formatted integer field.
|
459
|
+
# Using the opts parameter, you can override any of the field's
|
460
|
+
# overall parsing options.
|
461
|
+
|
462
|
+
def recognize_number(opts_={})
|
463
|
+
@recognizers << Delimiter::BasicIntegerRecognizer.new(@field, @default_opts.merge(opts_))
|
464
|
+
end
|
465
|
+
|
466
|
+
|
467
|
+
# Recognize a letter-formatted integer field. That is, the value is
|
468
|
+
# formatted as an alphabetic letter where "a" represents 1, up to
|
469
|
+
# "z" representing 26.
|
470
|
+
#
|
471
|
+
# Using the opts parameter, you can override any of the field's
|
472
|
+
# overall parsing options. You may also set the following additional
|
473
|
+
# options:
|
474
|
+
#
|
475
|
+
# <tt>:case</tt>::
|
476
|
+
# Case-sensitivity of the letter. Possible values are
|
477
|
+
# <tt>:upper</tt>, <tt>:lower</tt>, and <tt>:either</tt>.
|
478
|
+
# Default is <tt>:either</tt>.
|
479
|
+
|
480
|
+
def recognize_letter(opts_={})
|
481
|
+
@recognizers << Delimiter::AlphabeticIntegerRecognizer.new(@field, @default_opts.merge(opts_))
|
482
|
+
end
|
483
|
+
|
484
|
+
end
|
485
|
+
|
486
|
+
|
487
|
+
# This class defines methods that can be called from the block passed
|
488
|
+
# to Versionomy::Format::Delimiter::Builder#field if the field is
|
489
|
+
# of string type.
|
490
|
+
|
491
|
+
class StringFieldBuilder
|
492
|
+
|
493
|
+
include ::Blockenspiel::DSL
|
494
|
+
|
495
|
+
def initialize(recognizers_, field_, default_opts_) # :nodoc:
|
496
|
+
@recognizers = recognizers_
|
497
|
+
@field = field_
|
498
|
+
@default_opts = default_opts_
|
499
|
+
end
|
500
|
+
|
501
|
+
|
502
|
+
# Recognize a string field whose value matches a regular expression.
|
503
|
+
# The regular expression must be passed as a string. E.g. use
|
504
|
+
# "[a-z]+" instead of /[a-z]+/.
|
505
|
+
# Using the opts parameter, you can override any of the field's
|
506
|
+
# overall parsing options.
|
507
|
+
|
508
|
+
def recognize_regexp(regexp_, opts_={})
|
509
|
+
@recognizers << Delimiter::RegexpStringRecognizer.new(@field, regexp_, @default_opts.merge(opts_))
|
510
|
+
end
|
511
|
+
|
512
|
+
end
|
513
|
+
|
514
|
+
|
515
|
+
# This class defines methods that can be called from the block passed
|
516
|
+
# to Versionomy::Format::Delimiter::Builder#field if the field is
|
517
|
+
# of symbolic type.
|
518
|
+
|
519
|
+
class SymbolFieldBuilder
|
520
|
+
|
521
|
+
include ::Blockenspiel::DSL
|
522
|
+
|
523
|
+
def initialize(recognizers_, field_, default_opts_) # :nodoc:
|
524
|
+
@recognizers = recognizers_
|
525
|
+
@field = field_
|
526
|
+
@default_opts = default_opts_
|
527
|
+
end
|
528
|
+
|
529
|
+
|
530
|
+
# Recognize a symbolic value represented by a particular regular
|
531
|
+
# expression. The regular expression must be passed as a string.
|
532
|
+
# E.g. use "[a-z]+" instead of /[a-z]+/.
|
533
|
+
# The "canonical" parameter indicates the canonical syntax for the
|
534
|
+
# value, for use in unparsing.
|
535
|
+
#
|
536
|
+
# Using the opts parameter, you can override any of the field's
|
537
|
+
# overall parsing options.
|
538
|
+
|
539
|
+
def recognize_regexp(value_, regexp_, canonical_, opts_={}, &block_)
|
540
|
+
@recognizers << Delimiter::RegexpSymbolRecognizer.new(@field, value_, regexp_, canonical_, @default_opts.merge(opts_))
|
541
|
+
end
|
542
|
+
|
543
|
+
|
544
|
+
# Recognize a set of symbolic values, each represented by a
|
545
|
+
# particular regular expression, but all sharing the same delimiters
|
546
|
+
# and options. Use this instead of repeated calls to recognize_regexp
|
547
|
+
# for better performance.
|
548
|
+
#
|
549
|
+
# Using the opts parameter, you can override any of the field's
|
550
|
+
# overall parsing options.
|
551
|
+
#
|
552
|
+
# In the block, you should call methods of
|
553
|
+
# Versionomy::Format::Delimiter::MappingSymbolBuilder to map values
|
554
|
+
# to regular expression representations.
|
555
|
+
|
556
|
+
def recognize_regexp_map(opts_={}, &block_)
|
557
|
+
@recognizers << Delimiter::MappingSymbolRecognizer.new(@field, @default_opts.merge(opts_), &block_)
|
558
|
+
end
|
559
|
+
|
560
|
+
end
|
561
|
+
|
562
|
+
|
563
|
+
# Methods in this class can be called from the block passed to
|
564
|
+
# Versionomy::Format::Delimiter::SymbolFieldBuilder#recognize_regexp_map
|
565
|
+
# to define the mapping between the values of a symbolic field and
|
566
|
+
# the string representations of those values.
|
567
|
+
|
568
|
+
class MappingSymbolBuilder
|
569
|
+
|
570
|
+
include ::Blockenspiel::DSL
|
571
|
+
|
572
|
+
def initialize(mappings_in_order_, mappings_by_value_) # :nodoc:
|
573
|
+
@mappings_in_order = mappings_in_order_
|
574
|
+
@mappings_by_value = mappings_by_value_
|
575
|
+
end
|
576
|
+
|
577
|
+
|
578
|
+
# Map a value to a string representation.
|
579
|
+
# The optional regexp field, if specified, provides a regular
|
580
|
+
# expression pattern for matching the value representation. If it
|
581
|
+
# is omitted, the representation is used as the regexp.
|
582
|
+
|
583
|
+
def map(value_, representation_, regexp_=nil)
|
584
|
+
regexp_ ||= representation_
|
585
|
+
array_ = [regexp_, representation_, value_]
|
586
|
+
@mappings_by_value[value_] ||= array_
|
587
|
+
@mappings_in_order << array_
|
588
|
+
end
|
589
|
+
|
590
|
+
end
|
591
|
+
|
592
|
+
|
593
|
+
# This class handles the parsing and unparsing of a single field.
|
594
|
+
# It manages an ordered list of recognizers, each understanding a
|
595
|
+
# particular syntax. These recognizers are checked in order when
|
596
|
+
# parsing and unparsing.
|
597
|
+
|
598
|
+
class FieldHandler # :nodoc:
|
599
|
+
|
600
|
+
|
601
|
+
# Creates a FieldHandler, using a DSL block appropriate to the
|
602
|
+
# field type to configure the recognizers.
|
603
|
+
|
604
|
+
def initialize(field_, default_opts_={}, &block_)
|
605
|
+
@field = field_
|
606
|
+
@recognizers = []
|
607
|
+
@requires_previous_field = default_opts_.fetch(:requires_previous_field, true)
|
608
|
+
@default_style = default_opts_.fetch(:default_style, nil)
|
609
|
+
@style_unparse_param_key = "#{field_.name}_style".to_sym
|
610
|
+
if block_
|
611
|
+
builder_ = case field_.type
|
612
|
+
when :integer
|
613
|
+
Delimiter::IntegerFieldBuilder.new(@recognizers, field_, default_opts_)
|
614
|
+
when :string
|
615
|
+
Delimiter::StringFieldBuilder.new(@recognizers, field_, default_opts_)
|
616
|
+
when :symbol
|
617
|
+
Delimiter::SymbolFieldBuilder.new(@recognizers, field_, default_opts_)
|
618
|
+
end
|
619
|
+
::Blockenspiel.invoke(block_, builder_)
|
620
|
+
end
|
621
|
+
end
|
622
|
+
|
623
|
+
|
624
|
+
# Returns true if this field can appear in an unparsed string only
|
625
|
+
# if the previous field also appears.
|
626
|
+
|
627
|
+
def requires_previous_field
|
628
|
+
@requires_previous_field
|
629
|
+
end
|
630
|
+
|
631
|
+
|
632
|
+
# Parse this field from the string.
|
633
|
+
# This must either return a parsed value, or raise an error.
|
634
|
+
# It should also set the style in the unparse_params, if the style
|
635
|
+
# is determined not to be the default.
|
636
|
+
|
637
|
+
def parse(parse_params_, unparse_params_)
|
638
|
+
pair_ = nil
|
639
|
+
@recognizers.each do |recog_|
|
640
|
+
pair_ = recog_.parse(parse_params_, unparse_params_)
|
641
|
+
break if pair_
|
642
|
+
end
|
643
|
+
parse_params_[:previous_field_missing] = pair_.nil?
|
644
|
+
pair_ ||= [@field.default_value, @default_style]
|
645
|
+
if pair_[1] && pair_[1] != @default_style
|
646
|
+
unparse_params_[@style_unparse_param_key] = pair_[1]
|
647
|
+
end
|
648
|
+
pair_[0]
|
649
|
+
end
|
650
|
+
|
651
|
+
|
652
|
+
# Unparse a string from this field value.
|
653
|
+
# This may return nil if this field is not required.
|
654
|
+
|
655
|
+
def unparse(value_, unparse_params_)
|
656
|
+
style_ = unparse_params_[@style_unparse_param_key] || @default_style
|
657
|
+
@recognizers.each do |recog_|
|
658
|
+
if recog_.should_unparse?(value_, style_)
|
659
|
+
return recog_.unparse(value_, style_, unparse_params_)
|
660
|
+
end
|
661
|
+
end
|
662
|
+
unparse_params_[:required_for_later] ? '' : nil
|
663
|
+
end
|
664
|
+
|
665
|
+
end
|
666
|
+
|
667
|
+
|
668
|
+
# A recognizer handles both parsing and unparsing of a particular kind
|
669
|
+
# of syntax. During parsing, it recognizes the syntax based on regular
|
670
|
+
# expressions for the delimiters and the value. If the string matches
|
671
|
+
# the syntax recognized by this object, an appropriate value and style
|
672
|
+
# are returned. During unparsing, the should_unparse? method should be
|
673
|
+
# called first to determine whether this object is responsible for
|
674
|
+
# unparsing the given value and style. If should_unparse? returns
|
675
|
+
# true, the unparse method should be called to actually generate a
|
676
|
+
# a string fragment, or return nil if the field is determined to be
|
677
|
+
# optional in the unparsed string.
|
678
|
+
#
|
679
|
+
# This is a base class. The actual classes should implement
|
680
|
+
# initialize, parsed_value, and unparsed_value, and may optionally
|
681
|
+
# override the should_unparse? method.
|
682
|
+
|
683
|
+
class RecognizerBase # :nodoc:
|
684
|
+
|
685
|
+
# Derived classes should call this from their initialize method
|
686
|
+
# to set up the recognizer's basic parameters.
|
687
|
+
|
688
|
+
def setup(field_, value_regexp_, opts_)
|
689
|
+
@style = opts_[:style]
|
690
|
+
@default_value_optional = opts_[:default_value_optional]
|
691
|
+
@regexp_options = opts_[:case_sensitive] ? nil : ::Regexp::IGNORECASE
|
692
|
+
@value_regexp = ::Regexp.new("^(#{value_regexp_})", @regexp_options)
|
693
|
+
regexp_ = opts_.fetch(:delimiter_regexp, '\.')
|
694
|
+
@delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("^(#{regexp_})", @regexp_options) : nil
|
695
|
+
regexp_ = opts_.fetch(:post_delimiter_regexp, '')
|
696
|
+
@post_delimiter_regexp = regexp_.length > 0 ? ::Regexp.new("^(#{regexp_})", @regexp_options) : nil
|
697
|
+
regexp_ = opts_.fetch(:expected_follower_regexp, '')
|
698
|
+
@follower_regexp = regexp_.length > 0 ? ::Regexp.new("^(#{regexp_})", @regexp_options) : nil
|
699
|
+
@default_delimiter = opts_.fetch(:default_delimiter, '.')
|
700
|
+
@default_post_delimiter = opts_.fetch(:default_post_delimiter, '')
|
701
|
+
@requires_previous_field = opts_.fetch(:requires_previous_field, true)
|
702
|
+
name_ = field_.name
|
703
|
+
@default_field_value = field_.default_value
|
704
|
+
@delim_unparse_param_key = "#{name_}_delim".to_sym
|
705
|
+
@post_delim_unparse_param_key = "#{name_}_postdelim".to_sym
|
706
|
+
@required_unparse_param_key = "#{name_}_required".to_sym
|
707
|
+
end
|
708
|
+
|
709
|
+
|
710
|
+
# Attempt to parse the field from the string if the syntax matches
|
711
|
+
# this recognizer's configuration.
|
712
|
+
# Returns either nil, indicating that this recognizer doesn't match
|
713
|
+
# the given syntax, or a two element array of the value and style.
|
714
|
+
|
715
|
+
def parse(parse_params_, unparse_params_)
|
716
|
+
return nil if @requires_previous_field && parse_params_[:previous_field_missing]
|
717
|
+
string_ = parse_params_[:string]
|
718
|
+
if @delimiter_regexp
|
719
|
+
match_ = @delimiter_regexp.match(string_)
|
720
|
+
return nil unless match_
|
721
|
+
delim_ = match_[0]
|
722
|
+
string_ = match_.post_match
|
723
|
+
else
|
724
|
+
delim_ = ''
|
725
|
+
end
|
726
|
+
match_ = @value_regexp.match(string_)
|
727
|
+
return nil unless match_
|
728
|
+
value_ = match_[0]
|
729
|
+
string_ = match_.post_match
|
730
|
+
if @post_delimiter_regexp
|
731
|
+
match_ = @post_delimiter_regexp.match(string_)
|
732
|
+
return nil unless match_
|
733
|
+
post_delim_ = match_[0]
|
734
|
+
string_ = match_.post_match
|
735
|
+
else
|
736
|
+
post_delim_ = nil
|
737
|
+
end
|
738
|
+
if @follower_regexp
|
739
|
+
match_ = @follower_regexp.match(string_)
|
740
|
+
return nil unless match_
|
741
|
+
end
|
742
|
+
value_ = parsed_value(value_, parse_params_, unparse_params_)
|
743
|
+
return nil unless value_
|
744
|
+
parse_params_[:string] = string_
|
745
|
+
if delim_ != @default_delimiter
|
746
|
+
unparse_params_[@delim_unparse_param_key] = delim_
|
747
|
+
end
|
748
|
+
if post_delim_ && post_delim_ != @default_post_delimiter
|
749
|
+
unparse_params_[@post_delim_unparse_param_key] = post_delim_
|
750
|
+
end
|
751
|
+
unparse_params_[@required_unparse_param_key] = true if @default_value_optional
|
752
|
+
[value_, @style]
|
753
|
+
end
|
754
|
+
|
755
|
+
|
756
|
+
# Returns true if this recognizer should be used to unparse the
|
757
|
+
# given value and style.
|
758
|
+
|
759
|
+
def should_unparse?(value_, style_)
|
760
|
+
style_ == @style
|
761
|
+
end
|
762
|
+
|
763
|
+
|
764
|
+
# Unparse the given value in the given style, and return a string
|
765
|
+
# fragment, or nil if the field is determined to be "optional" to
|
766
|
+
# unparse and isn't otherwise required (because a later field needs
|
767
|
+
# it to be present, for example).
|
768
|
+
#
|
769
|
+
# It is guaranteed that this will be called only if should_unparse?
|
770
|
+
# returns true.
|
771
|
+
|
772
|
+
def unparse(value_, style_, unparse_params_)
|
773
|
+
str_ = nil
|
774
|
+
if !@default_value_optional || value_ != @default_field_value ||
|
775
|
+
unparse_params_[:required_for_later] ||
|
776
|
+
unparse_params_[@required_unparse_param_key]
|
777
|
+
then
|
778
|
+
str_ = unparsed_value(value_, style_, unparse_params_)
|
779
|
+
if str_
|
780
|
+
delim_ = unparse_params_[@delim_unparse_param_key]
|
781
|
+
if !delim_ || @delimiter_regexp && @delimiter_regexp !~ delim_
|
782
|
+
delim_ = @default_delimiter
|
783
|
+
end
|
784
|
+
post_delim_ = unparse_params_[@post_delim_unparse_param_key]
|
785
|
+
if !post_delim_ || @post_delimiter_regexp && @post_delimiter_regexp !~ post_delim_
|
786
|
+
post_delim_ = @default_post_delimiter
|
787
|
+
end
|
788
|
+
str_ = delim_ + str_ + post_delim_
|
789
|
+
end
|
790
|
+
str_
|
791
|
+
else
|
792
|
+
nil
|
793
|
+
end
|
794
|
+
end
|
795
|
+
|
796
|
+
end
|
797
|
+
|
798
|
+
|
799
|
+
# A recognizer for a numeric integer field
|
800
|
+
|
801
|
+
class BasicIntegerRecognizer < RecognizerBase #:nodoc:
|
802
|
+
|
803
|
+
def initialize(field_, opts_={})
|
804
|
+
setup(field_, '\d+', opts_)
|
805
|
+
end
|
806
|
+
|
807
|
+
def parsed_value(value_, parse_params_, unparse_params_)
|
808
|
+
value_.to_i
|
809
|
+
end
|
810
|
+
|
811
|
+
def unparsed_value(value_, style_, unparse_params_)
|
812
|
+
value_.to_s
|
813
|
+
end
|
814
|
+
|
815
|
+
end
|
816
|
+
|
817
|
+
|
818
|
+
# A recognizer for an alphabetic integer field. Such a field
|
819
|
+
# represents values 1-26 as letters of the English alphabet.
|
820
|
+
|
821
|
+
class AlphabeticIntegerRecognizer < RecognizerBase # :nodoc:
|
822
|
+
|
823
|
+
def initialize(field_, opts_={})
|
824
|
+
@case_unparse_param_key = "#{field_.name}_case".to_sym
|
825
|
+
@case = opts_[:case]
|
826
|
+
case @case
|
827
|
+
when :upper
|
828
|
+
value_regexp_ = '[A-Z]'
|
829
|
+
when :lower
|
830
|
+
value_regexp_ = '[a-z]'
|
831
|
+
else #either
|
832
|
+
value_regexp_ = '[a-zA-Z]'
|
833
|
+
end
|
834
|
+
setup(field_, value_regexp_, opts_)
|
835
|
+
end
|
836
|
+
|
837
|
+
def parsed_value(value_, parse_params_, unparse_params_)
|
838
|
+
value_ = value_.unpack('c')[0] # Compatible with both 1.8 and 1.9
|
839
|
+
if value_ >= 97 && value_ <= 122
|
840
|
+
unparse_params_[@case_unparse_param_key] = :lower
|
841
|
+
value_ - 96
|
842
|
+
elsif value_ >= 65 && value_ <= 90
|
843
|
+
unparse_params_[@case_unparse_param_key] = :upper
|
844
|
+
value_ - 64
|
845
|
+
else
|
846
|
+
0
|
847
|
+
end
|
848
|
+
end
|
849
|
+
|
850
|
+
def unparsed_value(value_, style_, unparse_params_)
|
851
|
+
if value_ >= 1 && value_ <= 26
|
852
|
+
if unparse_params_[@case_unparse_param_key] == :upper
|
853
|
+
(value_+64).chr
|
854
|
+
else
|
855
|
+
(value_+96).chr
|
856
|
+
end
|
857
|
+
else
|
858
|
+
value_.to_s
|
859
|
+
end
|
860
|
+
end
|
861
|
+
|
862
|
+
end
|
863
|
+
|
864
|
+
|
865
|
+
# A recognizer for strings that match a particular given regular
|
866
|
+
# expression, for use in string-valued fields.
|
867
|
+
|
868
|
+
class RegexpStringRecognizer < RecognizerBase # :nodoc:
|
869
|
+
|
870
|
+
def initialize(field_, regexp_='[a-zA-Z0-9]+', opts_={})
|
871
|
+
setup(field_, regexp_, opts_)
|
872
|
+
end
|
873
|
+
|
874
|
+
def parsed_value(value_, parse_params_, unparse_params_)
|
875
|
+
value_
|
876
|
+
end
|
877
|
+
|
878
|
+
def unparsed_value(value_, style_, unparse_params_)
|
879
|
+
value_
|
880
|
+
end
|
881
|
+
|
882
|
+
end
|
883
|
+
|
884
|
+
|
885
|
+
# A recognizer for symbolic fields that recognizes a single regular
|
886
|
+
# expression and maps it to a single particular value.
|
887
|
+
|
888
|
+
class RegexpSymbolRecognizer < RecognizerBase # :nodoc:
|
889
|
+
|
890
|
+
def initialize(field_, value_, regexp_, canonical_, opts_={})
|
891
|
+
setup(field_, regexp_, opts_)
|
892
|
+
@value = value_
|
893
|
+
@canonical = canonical_
|
894
|
+
end
|
895
|
+
|
896
|
+
def parsed_value(value_, parse_params, unparse_params_)
|
897
|
+
@value
|
898
|
+
end
|
899
|
+
|
900
|
+
def unparsed_value(value_, style_, unparse_params_)
|
901
|
+
@canonical
|
902
|
+
end
|
903
|
+
|
904
|
+
def should_unparse?(value_, style_)
|
905
|
+
style_ == @style && value_ == @value
|
906
|
+
end
|
907
|
+
|
908
|
+
end
|
909
|
+
|
910
|
+
|
911
|
+
# A recognizer for symbolic fields that recognizes a mapping of values
|
912
|
+
# to regular expressions.
|
913
|
+
|
914
|
+
class MappingSymbolRecognizer < RecognizerBase # :nodoc:
|
915
|
+
|
916
|
+
def initialize(field_, opts_={}, &block_)
|
917
|
+
@mappings_in_order = []
|
918
|
+
@mappings_by_value = {}
|
919
|
+
builder_ = Delimiter::MappingSymbolBuilder.new(@mappings_in_order, @mappings_by_value)
|
920
|
+
::Blockenspiel.invoke(block_, builder_)
|
921
|
+
regexps_ = @mappings_in_order.map{ |map_| "(#{map_[0]})" }
|
922
|
+
setup(field_, regexps_.join('|'), opts_)
|
923
|
+
@mappings_in_order.each do |map_|
|
924
|
+
map_[0] = ::Regexp.new("^(#{map_[0]})", @regexp_options)
|
925
|
+
end
|
926
|
+
end
|
927
|
+
|
928
|
+
def parsed_value(value_, parse_params, unparse_params_)
|
929
|
+
@mappings_in_order.each do |map_|
|
930
|
+
return map_[2] if map_[0].match(value_)
|
931
|
+
end
|
932
|
+
nil
|
933
|
+
end
|
934
|
+
|
935
|
+
def unparsed_value(value_, style_, unparse_params_)
|
936
|
+
@mappings_by_value[value_][1]
|
937
|
+
end
|
938
|
+
|
939
|
+
def should_unparse?(value_, style_)
|
940
|
+
style_ == @style && @mappings_by_value.include?(value_)
|
941
|
+
end
|
942
|
+
|
943
|
+
end
|
944
|
+
|
945
|
+
|
946
|
+
end
|
947
|
+
|
948
|
+
|
949
|
+
end
|
950
|
+
|
951
|
+
end
|