stupidedi 1.3.21 → 1.3.22
Sign up to get free protection for your applications and to get access to all the features.
- 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
|