stupidedi 1.3.21 → 1.3.22
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.
- checksums.yaml +4 -4
- data/lib/stupidedi/builder/builder_dsl.rb +5 -0
- data/lib/stupidedi/builder/constraint_table.rb +100 -74
- data/lib/stupidedi/builder/generation.rb +73 -70
- data/lib/stupidedi/builder/instruction.rb +10 -0
- data/lib/stupidedi/builder/instruction_table.rb +19 -7
- data/lib/stupidedi/builder/state_machine.rb +9 -3
- data/lib/stupidedi/builder/states/abstract_state.rb +1 -1
- data/lib/stupidedi/builder/states/failure_state.rb +1 -1
- data/lib/stupidedi/builder/states/initial_state.rb +4 -4
- data/lib/stupidedi/contrib/002001/guides/SH856.rb +4 -4
- data/lib/stupidedi/contrib/002001/transaction_set_defs/PO830.rb +2 -2
- data/lib/stupidedi/contrib/003010/guides/PS830.rb +4 -4
- data/lib/stupidedi/contrib/003010/guides/RA820.rb +22 -12
- data/lib/stupidedi/contrib/003010/transaction_set_defs/PC860.rb +2 -2
- data/lib/stupidedi/contrib/003010/transaction_set_defs/PS830.rb +1 -1
- data/lib/stupidedi/contrib/003050/guides/PO850.rb +2 -2
- data/lib/stupidedi/contrib/003050/transaction_set_defs/PO850.rb +2 -2
- data/lib/stupidedi/contrib/004010/guides.rb +0 -1
- data/lib/stupidedi/contrib/004010/transaction_set_defs/AR943.rb +1 -1
- data/lib/stupidedi/contrib/004010/transaction_set_defs/IM210.rb +2 -2
- data/lib/stupidedi/contrib/004010/transaction_set_defs/RE944.rb +1 -1
- data/lib/stupidedi/contrib/004010/transaction_set_defs/SH856.rb +1 -7
- data/lib/stupidedi/editor.rb +0 -1
- data/lib/stupidedi/guides/004010/guide_builder.rb +1 -1
- data/lib/stupidedi/guides/005010/X223-HC837I.rb +1192 -1195
- data/lib/stupidedi/guides/005010/guide_builder.rb +1 -1
- data/lib/stupidedi/schema.rb +1 -0
- data/lib/stupidedi/schema/auditor.rb +435 -0
- data/lib/stupidedi/schema/loop_def.rb +18 -1
- data/lib/stupidedi/schema/transaction_set_def.rb +12 -0
- data/lib/stupidedi/version.rb +1 -1
- data/lib/stupidedi/versions/functional_groups/004010/transaction_set_defs/HP835.rb +3 -17
- data/lib/stupidedi/versions/functional_groups/005010/element_types/time_val.rb +3 -2
- data/lib/stupidedi/versions/functional_groups/005010/segment_defs.rb +9 -6
- data/lib/stupidedi/versions/functional_groups/005010/transaction_set_defs/HB271.rb +25 -9
- data/lib/stupidedi/versions/functional_groups/005010/transaction_set_defs/HP835.rb +2 -2
- data/lib/stupidedi/zipper.rb +20 -1
- data/lib/stupidedi/zipper/memoized_cursor.rb +2 -0
- data/lib/stupidedi/zipper/path.rb +10 -0
- data/lib/stupidedi/zipper/root_cursor.rb +1 -1
- data/lib/stupidedi/zipper/stack_cursor.rb +174 -0
- data/spec/examples/stupidedi/audit_spec.rb +58 -0
- data/spec/spec_helper.rb +21 -13
- data/spec/support/rcov.rb +9 -4
- metadata +4 -1
@@ -73,7 +73,7 @@ module Stupidedi
|
|
73
73
|
unless elements.length == segment_def.element_uses.length
|
74
74
|
raise Exceptions::InvalidSchemaError,
|
75
75
|
"segment #{segment_def.id} has #{segment_def.element_uses.length}" +
|
76
|
-
" elements but #{elements.length} arguments were
|
76
|
+
" elements but #{elements.length} arguments were given"
|
77
77
|
end
|
78
78
|
|
79
79
|
element_index = "00"
|
data/lib/stupidedi/schema.rb
CHANGED
@@ -0,0 +1,435 @@
|
|
1
|
+
require "stupidedi"
|
2
|
+
|
3
|
+
module Stupidedi
|
4
|
+
using Refinements
|
5
|
+
|
6
|
+
module Schema
|
7
|
+
|
8
|
+
class Auditor
|
9
|
+
include Color
|
10
|
+
include Builder::Tokenization
|
11
|
+
|
12
|
+
attr_reader :config
|
13
|
+
attr_reader :machine
|
14
|
+
attr_reader :reader
|
15
|
+
attr_reader :queue
|
16
|
+
|
17
|
+
def initialize(machine, reader, isa_elements, gs_elements, st_elements)
|
18
|
+
@config = machine.config
|
19
|
+
@reader = reader
|
20
|
+
@queue = []
|
21
|
+
@queued = Set.new
|
22
|
+
|
23
|
+
@elements = Hash.new{|h,k| h[k] = [] }
|
24
|
+
@elements[:ISA] = isa_elements
|
25
|
+
@elements[:GS] = gs_elements
|
26
|
+
@elements[:ST] = st_elements
|
27
|
+
|
28
|
+
enqueue(machine.active.head)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Throw an exception if the transaction set definition has any ambiguity
|
32
|
+
# that could cause non-deterministic parser state.
|
33
|
+
def audit
|
34
|
+
while cursor = @queue.shift
|
35
|
+
step(Stupidedi::Builder::StateMachine.new(@config, [cursor]))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def enqueue(zipper)
|
42
|
+
# Avoid unecessary traversal over instruction tables that we've already
|
43
|
+
# queued. NOTE: This means some segments in the definition could be
|
44
|
+
# skipped entirely, due to have the same #successors as another segment.
|
45
|
+
#
|
46
|
+
# For instance, consider HP835's Table 1:
|
47
|
+
#
|
48
|
+
# d::TableDef.header("Table 1 - Header",
|
49
|
+
# b::Segment(100, s::ST, "Transaction Set Header",
|
50
|
+
# b::Segment(200, s::BPR, "Financial Information",<Paste>
|
51
|
+
# b::Segment(400, s::TRN, "Reassociation Trace Number",
|
52
|
+
# b::Segment(500, s::CUR, "Foreign Currency Information",
|
53
|
+
# b::Segment(600, s::REF, "Receiver Identification",
|
54
|
+
# b::Segment(600, s::REF, "Version Identification",
|
55
|
+
# b::Segment(700, s::DTM, "Production Date",
|
56
|
+
#
|
57
|
+
# It's important to know that two segments at the same position don't
|
58
|
+
# come "before" or "after" each other, even though they might be listed
|
59
|
+
# in a particular order.
|
60
|
+
#
|
61
|
+
# Starting at the CUR segment, its successors are the two REF segments
|
62
|
+
# and some other stuff. If we generate the first REF segment, due to
|
63
|
+
# having the same position (600) as the other REF segment, it also has
|
64
|
+
# both REF segments as successors. The second REF segment also has the
|
65
|
+
# same set of successors.
|
66
|
+
#
|
67
|
+
# Therefore, all three segments (CUR, REF, REF) have the same set of
|
68
|
+
# successors and it doesn't matter which one we choose; we only need
|
69
|
+
# to choose one.
|
70
|
+
|
71
|
+
id = zipper.node.instructions.hash
|
72
|
+
|
73
|
+
unless @queued.member?(id)
|
74
|
+
@queued.add(id)
|
75
|
+
@queue.push(zipper)
|
76
|
+
# puts "Queued: #{pp_zipper(zipper)}: #{id}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Starting from the given deterministic `StateMachine`, evaluate each of
|
81
|
+
# its `#successors` to detect when an input could make it impossible to
|
82
|
+
# choose only a single successor state, due to ambiguity in the definition
|
83
|
+
# of the transaction set. If this situation is detected, an exception will
|
84
|
+
# be thrown.
|
85
|
+
#
|
86
|
+
# Otherwise, for each successor, generate the corresponding segment and
|
87
|
+
# add the resulting StateMachine to the queue to be checked recursively.
|
88
|
+
def step(machine)
|
89
|
+
# @todo: when segment_id is :ISA, :GS, or :ST, use the correct values?
|
90
|
+
machine.successors.head.constraints.each do |segment_id, table|
|
91
|
+
case table
|
92
|
+
when Stupidedi::Builder::ConstraintTable::Shallowest,
|
93
|
+
Stupidedi::Builder::ConstraintTable::Deepest
|
94
|
+
segment_tok =
|
95
|
+
mksegment_tok(@reader.segment_dict, segment_id, @elements[segment_id], nil)
|
96
|
+
|
97
|
+
op = table.matches(segment_tok, false).head
|
98
|
+
as = machine.execute(op, machine.active.head, @reader, segment_tok)
|
99
|
+
enqueue(as)
|
100
|
+
|
101
|
+
when Stupidedi::Builder::ConstraintTable::Stub
|
102
|
+
if table.instructions.length == 1
|
103
|
+
segment_tok =
|
104
|
+
mksegment_tok(@reader.segment_dict, segment_id, @elements[segment_id], nil)
|
105
|
+
|
106
|
+
op = table.matches(nil, false).head
|
107
|
+
as = machine.execute(op, machine.active.head, @reader, segment_tok)
|
108
|
+
enqueue(as)
|
109
|
+
else
|
110
|
+
raise Exceptions::InvalidSchemaError,
|
111
|
+
"proceeding from [#{pp_machine(machine)}], there are no " +
|
112
|
+
"constraints on segment #{segment_id} to choose between:\n" +
|
113
|
+
table.instructions.map{|x| pp_instruction(x) }.join
|
114
|
+
end
|
115
|
+
|
116
|
+
when Stupidedi::Builder::ConstraintTable::ValueBased
|
117
|
+
disjoint, distinct = table.basis(table.instructions)
|
118
|
+
|
119
|
+
if disjoint.empty?
|
120
|
+
# We're definitely going to throw an error now, but we need to
|
121
|
+
# get more information to make a useful error message. First
|
122
|
+
# check that interesting elements are required
|
123
|
+
designators = optional_elements(distinct, table.instructions)
|
124
|
+
|
125
|
+
case designators.length
|
126
|
+
when 0
|
127
|
+
when 1
|
128
|
+
raise Exceptions::InvalidSchemaError,
|
129
|
+
"proceeding from [#{pp_machine(machine)}], the element " +
|
130
|
+
"#{designators.join(", ")} is designated optional, but " +
|
131
|
+
"is necessary to choose between:\n" +
|
132
|
+
table.instructions.map{|x| pp_instruction(x) }.join
|
133
|
+
else
|
134
|
+
raise Exceptions::InvalidSchemaError,
|
135
|
+
"proceeding from [#{pp_machine(machine)}], the elements " +
|
136
|
+
"#{designators.join(", ")} are designated optional, but " +
|
137
|
+
"at least one is necessary to choose between:\n" +
|
138
|
+
table.instructions.map{|x| pp_instruction(x) }.join
|
139
|
+
end
|
140
|
+
|
141
|
+
# Next report cases of element values that cause ambiguity
|
142
|
+
designators, ex_designator, ex_value, ex_instructions =
|
143
|
+
distinct_elements(distinct, table.instructions)
|
144
|
+
|
145
|
+
case designators.length
|
146
|
+
when 0
|
147
|
+
raise Exceptions::InvalidSchemaError,
|
148
|
+
"proceeding from [#{pp_machine(machine)}], there are no " +
|
149
|
+
"constraints on segment #{segment_id} to choose " +
|
150
|
+
"between:\n" + table.instructions.map{|x| pp_instruction(x) }.join
|
151
|
+
when 1
|
152
|
+
raise Exceptions::InvalidSchemaError,
|
153
|
+
"proceeding from [#{pp_machine(machine)}], the element " +
|
154
|
+
"#{ex_designator} has overlapping values among possible " +
|
155
|
+
"choices; for example when it has value the #{ex_value}, "+
|
156
|
+
"it is not possible to choose between:\n" +
|
157
|
+
ex_instructions.map{|x| pp_instruction(x) }.join
|
158
|
+
else
|
159
|
+
raise Exceptions::InvalidSchemaError,
|
160
|
+
"proceeding from [#{pp_machine(machine)}], the elements " +
|
161
|
+
"#{designators.join(", ")} each have overlapping values " +
|
162
|
+
"among choices; for example when #{ex_designator} has " +
|
163
|
+
"the value #{ex_value}, it is not possible to choose " +
|
164
|
+
"between:\n" + ex_instructions.map{|x| pp_instruction(x) }.join
|
165
|
+
end
|
166
|
+
else
|
167
|
+
# It's possible there is no ambiguity here, but we need to check
|
168
|
+
designators = optional_elements(disjoint, table.instructions)
|
169
|
+
|
170
|
+
case designators.length
|
171
|
+
when 0
|
172
|
+
when 1
|
173
|
+
raise Exceptions::InvalidSchemaError,
|
174
|
+
"proceeding from [#{pp_machine(machine)}], the element " +
|
175
|
+
"#{designators.join(", ")} is designated optional in at " +
|
176
|
+
"least one case, but is necessary to choose between:\n" +
|
177
|
+
table.instructions.map{|x| pp_instruction(x) }.join
|
178
|
+
else
|
179
|
+
raise Exceptions::InvalidSchemaError,
|
180
|
+
"proceeding from [#{pp_machine(machine)}], the elements " +
|
181
|
+
"#{designators.join(", ")} are designated optional in at " +
|
182
|
+
"least one case each, but at least one is necessary to " +
|
183
|
+
"choose between:\n" +
|
184
|
+
table.instructions.map{|x| pp_instruction(x) }.join
|
185
|
+
end
|
186
|
+
|
187
|
+
mksegments(table).each do |op, segment_tok|
|
188
|
+
enqueue(machine.execute(op, machine.active.head, @reader, segment_tok))
|
189
|
+
end
|
190
|
+
end
|
191
|
+
else
|
192
|
+
raise "unexpected kind of constraint table: #{constraint_table.class}"
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# For each instruction in the given ConstraintTable::ValueBased, return
|
198
|
+
# the segment token which will be matched to that instruction.
|
199
|
+
#
|
200
|
+
# @return [Array<(Instruction, SegmentTok)>]
|
201
|
+
def mksegments(table)
|
202
|
+
disjoint, = table.basis(table.instructions)
|
203
|
+
remaining = Set.new(table.instructions)
|
204
|
+
segment_id = table.instructions.head.segment_use.id
|
205
|
+
segments = []
|
206
|
+
|
207
|
+
# Scan each interesting element location
|
208
|
+
disjoint.each do |(m, n), map|
|
209
|
+
if remaining.empty?
|
210
|
+
# We've already generated results for each possible instruction
|
211
|
+
break
|
212
|
+
end
|
213
|
+
|
214
|
+
# Likely many values are mapped to only a few unique keys; in this
|
215
|
+
# case, `instructions` is a singleton array (that's what makes the
|
216
|
+
# value non-ambiguous, there's only one possible instruction)
|
217
|
+
map.each do |value, instructions|
|
218
|
+
op, = instructions
|
219
|
+
repeatable = op.segment_use.definition.element_uses.at(m).repeatable?
|
220
|
+
elements = Array.new(m)
|
221
|
+
elements[m] =
|
222
|
+
if n.nil?
|
223
|
+
if repeatable
|
224
|
+
repeated(value)
|
225
|
+
else
|
226
|
+
value
|
227
|
+
end
|
228
|
+
else
|
229
|
+
components = Array.new(n)
|
230
|
+
components[n] = value
|
231
|
+
|
232
|
+
if repeatable
|
233
|
+
repeated(composite(components))
|
234
|
+
else
|
235
|
+
composite(components)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
if remaining.member?(op)
|
240
|
+
remaining.delete(op)
|
241
|
+
segments.push([op, mksegment_tok(@reader.segment_dict, segment_id, elements, nil)])
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
segments
|
247
|
+
end
|
248
|
+
|
249
|
+
# Return a list of element designators that are needed to choose a single
|
250
|
+
# instruction, but are not #required? in the transaction set definition.
|
251
|
+
#
|
252
|
+
# @return [Array<String>]
|
253
|
+
def optional_elements(disjoint, instructions)
|
254
|
+
designators = []
|
255
|
+
|
256
|
+
disjoint.each do |(m, n), map|
|
257
|
+
required = true
|
258
|
+
|
259
|
+
# Element must be required for all possible segment_uses
|
260
|
+
instructions.each do |op|
|
261
|
+
element_use = op.segment_use.definition.element_uses.at(m)
|
262
|
+
|
263
|
+
if n.nil?
|
264
|
+
required &&= element_use.required?
|
265
|
+
else
|
266
|
+
component_use = element_use.definition.component_uses.at(n)
|
267
|
+
required &&= element_use.required? and component_use.required?
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
unless required
|
272
|
+
segment_id = instructions.head.segment_use.id
|
273
|
+
designators << "#{segment_id}-#{'%02d' % (m+1)}#{n.try{'-%02d' % (n+1)}}"
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
if designators.length < disjoint.length
|
278
|
+
# This means at least one designator *was* required, therefore we can
|
279
|
+
# be assured that valid input will not be ambiguous
|
280
|
+
[]
|
281
|
+
else
|
282
|
+
# None of the useful elements were required, so even valid input (that
|
283
|
+
# has none of these elements present), will be ambiguous
|
284
|
+
designators
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Build a list of offending elements (eg, HL-03) and provide an example
|
289
|
+
# value that resolves to conflicting instructions.
|
290
|
+
#
|
291
|
+
# @return [(Array<String>, String, String, Array<Instruction>)]
|
292
|
+
def distinct_elements(distinct, instructions)
|
293
|
+
designators = []
|
294
|
+
ex_designator = nil
|
295
|
+
ex_value = nil
|
296
|
+
ex_instructions = nil
|
297
|
+
|
298
|
+
distinct.each do |(m, n), map|
|
299
|
+
designators <<
|
300
|
+
"#{instructions.head.segment_use.id}-#{'%02d' % (m+1)}#{n.try{'-%02d' % (n+1)}}"
|
301
|
+
|
302
|
+
if ex_designator.nil?
|
303
|
+
ex_designator = designators.last
|
304
|
+
ex_value, ex_instructions = map.max_by{|_, v| v.length }
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
return designators, ex_designator, ex_value, ex_instructions
|
309
|
+
end
|
310
|
+
|
311
|
+
# @return [String]
|
312
|
+
def pp_machine(machine)
|
313
|
+
pp_zipper(machine.active.head)
|
314
|
+
end
|
315
|
+
|
316
|
+
# @return [String]
|
317
|
+
def pp_zipper(zipper)
|
318
|
+
segment = zipper.node.zipper.node
|
319
|
+
|
320
|
+
if segment.valid?
|
321
|
+
id = '% 3s' % segment.definition.id.to_s
|
322
|
+
name = segment.definition.name
|
323
|
+
width = 18
|
324
|
+
|
325
|
+
if name.length > width - 2
|
326
|
+
id = id + ": #{name.slice(0, width - 2)}.."
|
327
|
+
else
|
328
|
+
id = id + ": #{name}"
|
329
|
+
end
|
330
|
+
|
331
|
+
args = "position:%d, repeat:%s" %
|
332
|
+
[segment.usage.position,
|
333
|
+
segment.usage.repeat_count.inspect]
|
334
|
+
|
335
|
+
ansi.segment("SegmentVal[#{ansi.bold(id)}](#{args})")
|
336
|
+
else
|
337
|
+
segment.pretty_inspect
|
338
|
+
end
|
339
|
+
end
|
340
|
+
|
341
|
+
# @return [String]
|
342
|
+
def pp_instruction(op)
|
343
|
+
id = '% 3s' % op.segment_id.to_s
|
344
|
+
|
345
|
+
unless op.segment_use.nil?
|
346
|
+
width = 30
|
347
|
+
name = op.segment_use.definition.name
|
348
|
+
|
349
|
+
# Truncate the segment name to `width` characters
|
350
|
+
if name.length > width - 2
|
351
|
+
id = id + ": #{name.slice(0, width - 2)}.."
|
352
|
+
else
|
353
|
+
id = id + ": #{name.ljust(width)}"
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
args = "position:%d, repeat:%s, pop:%d" %
|
358
|
+
[op.segment_use.position,
|
359
|
+
op.segment_use.repeat_count.inspect,
|
360
|
+
op.pop_count]
|
361
|
+
|
362
|
+
unless op.push.nil?
|
363
|
+
args += ", push:#{op.push.try{|c| c.name.split('::').last}}"
|
364
|
+
end
|
365
|
+
|
366
|
+
" Instruction[#{id}](#{args})\n"
|
367
|
+
end
|
368
|
+
end
|
369
|
+
|
370
|
+
class << Auditor
|
371
|
+
|
372
|
+
def build(definition)
|
373
|
+
# Use dummy identifiers to link definition to the parser
|
374
|
+
config = mkconfig(definition, "ISA11", "GS01", "GS08", "ST01")
|
375
|
+
builder = Builder::BuilderDsl.new(
|
376
|
+
Builder::StateMachine.build(config, Zipper::Stack), false)
|
377
|
+
|
378
|
+
# These lists of elements are re-used when Auditor needs to construct
|
379
|
+
# an ISA, GS, or ST segment as it walks the definition.
|
380
|
+
isa_elements =
|
381
|
+
[ "00", "AUTHORIZATION",
|
382
|
+
"00", "PASSWORD",
|
383
|
+
"ZZ", "SUBMITTER ID",
|
384
|
+
"ZZ", "RECEIVER ID",
|
385
|
+
"191225", "1230",
|
386
|
+
"^",
|
387
|
+
"ISA11",
|
388
|
+
"123456789", "0", "T", ":" ]
|
389
|
+
|
390
|
+
gs_elements =
|
391
|
+
[ "GS01",
|
392
|
+
"SENDER ID",
|
393
|
+
"RECEIVER ID",
|
394
|
+
"20191225", "1230", "1", "X",
|
395
|
+
"GS08" ]
|
396
|
+
|
397
|
+
st_elements =
|
398
|
+
[ "ST01",
|
399
|
+
"1234" ]
|
400
|
+
|
401
|
+
builder.ISA(*isa_elements)
|
402
|
+
builder.GS(*gs_elements)
|
403
|
+
builder.ST(*st_elements)
|
404
|
+
|
405
|
+
new(builder.machine, builder.reader, isa_elements, gs_elements, st_elements)
|
406
|
+
end
|
407
|
+
|
408
|
+
def mkconfig(definition, isa11, gs01, gs08, st01)
|
409
|
+
segment = definition.table_defs.head.header_segment_uses.head.definition
|
410
|
+
|
411
|
+
# Infer the FunctionalGroupDef based on the given `definition`
|
412
|
+
element = segment.element_uses.head.definition
|
413
|
+
version = element.class.name.split('::').slice(0..-3)
|
414
|
+
grpdefn = Object.const_get("FunctionalGroupDef".snoc(version).join('::'))
|
415
|
+
|
416
|
+
Config.new.customize do |c|
|
417
|
+
c.interchange.customize do |x|
|
418
|
+
# We can use whatever interchange version we like, it does not
|
419
|
+
# have any bearing or relationship to the given `definition`
|
420
|
+
x.register(isa11, Versions::Interchanges::FiveOhOne::InterchangeDef)
|
421
|
+
end
|
422
|
+
|
423
|
+
c.functional_group.customize do |x|
|
424
|
+
x.register(gs08, grpdefn)
|
425
|
+
end
|
426
|
+
|
427
|
+
c.transaction_set.customize do |x|
|
428
|
+
x.register(gs08, gs01, st01, definition)
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
end
|
435
|
+
end
|