versionomy 0.0.4 → 0.1.0
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/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
|