tediparse 2.0.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.
Files changed (176) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +319 -0
  3. data/LICENSE +26 -0
  4. data/README.md +308 -0
  5. data/Rakefile +33 -0
  6. data/bin/tediparse +102 -0
  7. data/doc/Defining.md +97 -0
  8. data/doc/Generating-Grammars.md +241 -0
  9. data/doc/Generating.md +332 -0
  10. data/doc/Navigating.md +670 -0
  11. data/doc/Parsing.md +133 -0
  12. data/doc/README.md +51 -0
  13. data/doc/Serializing.md +100 -0
  14. data/doc/Tokenizing.md +136 -0
  15. data/doc/Validating.md +140 -0
  16. data/lib/ruby/array.rb +226 -0
  17. data/lib/ruby/blank.rb +52 -0
  18. data/lib/ruby/exception.rb +14 -0
  19. data/lib/ruby/hash.rb +14 -0
  20. data/lib/ruby/module.rb +60 -0
  21. data/lib/ruby/object.rb +56 -0
  22. data/lib/ruby/string.rb +89 -0
  23. data/lib/ruby/to_d.rb +82 -0
  24. data/lib/ruby/to_date.rb +26 -0
  25. data/lib/ruby/to_time.rb +21 -0
  26. data/lib/ruby/try.rb +46 -0
  27. data/lib/stupidedi/builder.rb +6 -0
  28. data/lib/stupidedi/color.rb +94 -0
  29. data/lib/stupidedi/config/code_list_config.rb +45 -0
  30. data/lib/stupidedi/config/functional_group_config.rb +72 -0
  31. data/lib/stupidedi/config/interchange_config.rb +86 -0
  32. data/lib/stupidedi/config/transaction_set_config.rb +73 -0
  33. data/lib/stupidedi/config.rb +100 -0
  34. data/lib/stupidedi/either.rb +286 -0
  35. data/lib/stupidedi/exceptions/invalid_element_error.rb +7 -0
  36. data/lib/stupidedi/exceptions/invalid_schema_error.rb +7 -0
  37. data/lib/stupidedi/exceptions/missing_grammar_error.rb +50 -0
  38. data/lib/stupidedi/exceptions/output_error.rb +7 -0
  39. data/lib/stupidedi/exceptions/parse_error.rb +7 -0
  40. data/lib/stupidedi/exceptions/stupidedi_error.rb +7 -0
  41. data/lib/stupidedi/exceptions/tokenize_error.rb +7 -0
  42. data/lib/stupidedi/exceptions/zipper_error.rb +7 -0
  43. data/lib/stupidedi/exceptions.rb +13 -0
  44. data/lib/stupidedi/inspect.rb +26 -0
  45. data/lib/stupidedi/interchanges/element_types/separator_val.rb +79 -0
  46. data/lib/stupidedi/interchanges/element_types/special_val.rb +48 -0
  47. data/lib/stupidedi/interchanges/element_types.rb +11 -0
  48. data/lib/stupidedi/interchanges.rb +21 -0
  49. data/lib/stupidedi/parser/builder_dsl.rb +292 -0
  50. data/lib/stupidedi/parser/constraint_table.rb +566 -0
  51. data/lib/stupidedi/parser/generation.rb +147 -0
  52. data/lib/stupidedi/parser/identifier_stack.rb +248 -0
  53. data/lib/stupidedi/parser/instruction.rb +112 -0
  54. data/lib/stupidedi/parser/instruction_table.rb +222 -0
  55. data/lib/stupidedi/parser/navigation.rb +786 -0
  56. data/lib/stupidedi/parser/state_machine.rb +62 -0
  57. data/lib/stupidedi/parser/states/abstract_state.rb +385 -0
  58. data/lib/stupidedi/parser/states/failure_state.rb +69 -0
  59. data/lib/stupidedi/parser/states/functional_group_state.rb +101 -0
  60. data/lib/stupidedi/parser/states/initial_state.rb +62 -0
  61. data/lib/stupidedi/parser/states/interchange_state.rb +96 -0
  62. data/lib/stupidedi/parser/states/loop_state.rb +74 -0
  63. data/lib/stupidedi/parser/states/table_state.rb +90 -0
  64. data/lib/stupidedi/parser/states/transaction_set_state.rb +120 -0
  65. data/lib/stupidedi/parser/states/transmission_state.rb +59 -0
  66. data/lib/stupidedi/parser/tokenization.rb +195 -0
  67. data/lib/stupidedi/parser.rb +32 -0
  68. data/lib/stupidedi/reader/input/abstract_input.rb +137 -0
  69. data/lib/stupidedi/reader/input/delegated_input.rb +112 -0
  70. data/lib/stupidedi/reader/input/file_input.rb +157 -0
  71. data/lib/stupidedi/reader/input.rb +31 -0
  72. data/lib/stupidedi/reader/position.rb +78 -0
  73. data/lib/stupidedi/reader/result.rb +172 -0
  74. data/lib/stupidedi/reader/segment_dict.rb +176 -0
  75. data/lib/stupidedi/reader/separators.rb +90 -0
  76. data/lib/stupidedi/reader/stream_reader.rb +173 -0
  77. data/lib/stupidedi/reader/token_reader.rb +465 -0
  78. data/lib/stupidedi/reader/tokens/component_element_tok.rb +71 -0
  79. data/lib/stupidedi/reader/tokens/composite_element_tok.rb +85 -0
  80. data/lib/stupidedi/reader/tokens/repeated_element_tok.rb +74 -0
  81. data/lib/stupidedi/reader/tokens/segment_tok.rb +74 -0
  82. data/lib/stupidedi/reader/tokens/simple_element_tok.rb +76 -0
  83. data/lib/stupidedi/reader.rb +121 -0
  84. data/lib/stupidedi/schema/abstract_def.rb +76 -0
  85. data/lib/stupidedi/schema/abstract_element_def.rb +35 -0
  86. data/lib/stupidedi/schema/abstract_element_use.rb +47 -0
  87. data/lib/stupidedi/schema/abstract_use.rb +79 -0
  88. data/lib/stupidedi/schema/code_list.rb +99 -0
  89. data/lib/stupidedi/schema/component_element_use.rb +76 -0
  90. data/lib/stupidedi/schema/composite_element_def.rb +103 -0
  91. data/lib/stupidedi/schema/composite_element_use.rb +78 -0
  92. data/lib/stupidedi/schema/element_req.rb +57 -0
  93. data/lib/stupidedi/schema/functional_group_def.rb +124 -0
  94. data/lib/stupidedi/schema/generation/definition_generator.rb +139 -0
  95. data/lib/stupidedi/schema/generation/element_generator.rb +221 -0
  96. data/lib/stupidedi/schema/generation/flat_file_reader.rb +551 -0
  97. data/lib/stupidedi/schema/generation/functional_group_generator.rb +64 -0
  98. data/lib/stupidedi/schema/generation/interchange_generator.rb +145 -0
  99. data/lib/stupidedi/schema/generation/master_loader_generator.rb +121 -0
  100. data/lib/stupidedi/schema/generation/models.rb +85 -0
  101. data/lib/stupidedi/schema/generation/module_loader_generator.rb +64 -0
  102. data/lib/stupidedi/schema/generation/registration_generator.rb +230 -0
  103. data/lib/stupidedi/schema/generation/runner.rb +161 -0
  104. data/lib/stupidedi/schema/generation/segment_generator.rb +130 -0
  105. data/lib/stupidedi/schema/generation/support.rb +78 -0
  106. data/lib/stupidedi/schema/generation/support_modules_generator.rb +126 -0
  107. data/lib/stupidedi/schema/generation/version_modules.rb +35 -0
  108. data/lib/stupidedi/schema/generation.rb +94 -0
  109. data/lib/stupidedi/schema/interchange_def.rb +103 -0
  110. data/lib/stupidedi/schema/loop_def.rb +156 -0
  111. data/lib/stupidedi/schema/repeat_count.rb +86 -0
  112. data/lib/stupidedi/schema/segment_def.rb +122 -0
  113. data/lib/stupidedi/schema/segment_req.rb +46 -0
  114. data/lib/stupidedi/schema/segment_use.rb +99 -0
  115. data/lib/stupidedi/schema/simple_element_def.rb +51 -0
  116. data/lib/stupidedi/schema/simple_element_use.rb +83 -0
  117. data/lib/stupidedi/schema/syntax_note.rb +52 -0
  118. data/lib/stupidedi/schema/table_def.rb +178 -0
  119. data/lib/stupidedi/schema/transaction_set_def.rb +125 -0
  120. data/lib/stupidedi/schema.rb +30 -0
  121. data/lib/stupidedi/sets.rb +42 -0
  122. data/lib/stupidedi/transaction_sets/builder/dsl.rb +192 -0
  123. data/lib/stupidedi/transaction_sets/builder.rb +188 -0
  124. data/lib/stupidedi/transaction_sets/common/implementations/element_reqs.rb +37 -0
  125. data/lib/stupidedi/transaction_sets/common/implementations/segment_reqs.rb +31 -0
  126. data/lib/stupidedi/transaction_sets/common/implementations.rb +11 -0
  127. data/lib/stupidedi/transaction_sets/common.rb +8 -0
  128. data/lib/stupidedi/transaction_sets/validation/ambiguity.rb +395 -0
  129. data/lib/stupidedi/transaction_sets/validation/implementation.rb +12 -0
  130. data/lib/stupidedi/transaction_sets/validation.rb +9 -0
  131. data/lib/stupidedi/transaction_sets.rb +25 -0
  132. data/lib/stupidedi/values/abstract_element_val.rb +19 -0
  133. data/lib/stupidedi/values/abstract_val.rb +133 -0
  134. data/lib/stupidedi/values/composite_element_val.rb +102 -0
  135. data/lib/stupidedi/values/functional_group_val.rb +105 -0
  136. data/lib/stupidedi/values/interchange_val.rb +102 -0
  137. data/lib/stupidedi/values/invalid_envelope_val.rb +61 -0
  138. data/lib/stupidedi/values/invalid_segment_val.rb +89 -0
  139. data/lib/stupidedi/values/loop_val.rb +73 -0
  140. data/lib/stupidedi/values/repeated_element_val.rb +113 -0
  141. data/lib/stupidedi/values/segment_val.rb +105 -0
  142. data/lib/stupidedi/values/segment_val_group.rb +20 -0
  143. data/lib/stupidedi/values/simple_element_val.rb +80 -0
  144. data/lib/stupidedi/values/table_val.rb +69 -0
  145. data/lib/stupidedi/values/transaction_set_val.rb +69 -0
  146. data/lib/stupidedi/values/transmission_val.rb +56 -0
  147. data/lib/stupidedi/values.rb +22 -0
  148. data/lib/stupidedi/version.rb +4 -0
  149. data/lib/stupidedi/versions/common/element_reqs.rb +13 -0
  150. data/lib/stupidedi/versions/common/element_types/an.rb +386 -0
  151. data/lib/stupidedi/versions/common/element_types/dt.rb +572 -0
  152. data/lib/stupidedi/versions/common/element_types/id.rb +304 -0
  153. data/lib/stupidedi/versions/common/element_types/nn.rb +312 -0
  154. data/lib/stupidedi/versions/common/element_types/operators.rb +128 -0
  155. data/lib/stupidedi/versions/common/element_types/r.rb +342 -0
  156. data/lib/stupidedi/versions/common/element_types/simple_element.rb +73 -0
  157. data/lib/stupidedi/versions/common/element_types/tm.rb +347 -0
  158. data/lib/stupidedi/versions/common/element_types.rb +29 -0
  159. data/lib/stupidedi/versions/common/segment_reqs.rb +15 -0
  160. data/lib/stupidedi/versions/common/syntax_notes.rb +172 -0
  161. data/lib/stupidedi/versions/common.rb +11 -0
  162. data/lib/stupidedi/versions.rb +25 -0
  163. data/lib/stupidedi/writer/claredi.rb +178 -0
  164. data/lib/stupidedi/writer/default.rb +119 -0
  165. data/lib/stupidedi/writer.rb +7 -0
  166. data/lib/stupidedi/zipper/abstract_cursor.rb +351 -0
  167. data/lib/stupidedi/zipper/dangling_cursor.rb +103 -0
  168. data/lib/stupidedi/zipper/edited_cursor.rb +157 -0
  169. data/lib/stupidedi/zipper/memoized_cursor.rb +133 -0
  170. data/lib/stupidedi/zipper/path.rb +132 -0
  171. data/lib/stupidedi/zipper/root_cursor.rb +120 -0
  172. data/lib/stupidedi/zipper/stack_cursor.rb +107 -0
  173. data/lib/stupidedi/zipper.rb +45 -0
  174. data/lib/stupidedi.rb +69 -0
  175. data/lib/tediparse.rb +1 -0
  176. metadata +249 -0
@@ -0,0 +1,566 @@
1
+ # frozen_string_literal: true
2
+ module Stupidedi
3
+ using Refinements
4
+
5
+ module Parser
6
+ # The {ConstraintTable} is a data structure that contains one or more
7
+ # {Instruction} values for the same segment identifier. Each concrete
8
+ # subclass implements different strategies for narrowing down the
9
+ # {Instruction} list.
10
+ #
11
+ # Reducing the number of valid {Instruction} values is important because
12
+ # executing more than one {Instruction} creates a non-deterministic state --
13
+ # more than one valid parse tree exists -- which slows the parser. Most
14
+ # often there is only one valid {Instruction} but the parser cannot
15
+ # (efficiently or at all) narrow the tree down without evaluating the
16
+ # constraints declared by each {Instruction}'s {Schema::SegmentUse}, which
17
+ # is done here.
18
+ #
19
+ class ConstraintTable
20
+ # @return [Array<Instruction>]
21
+ abstract :matches, :args => %w(segment_tok strict mode state)
22
+
23
+ # @return [Array<Instruction>]
24
+ attr_reader :instructions
25
+
26
+ def copy(changes = {})
27
+ self.class.new \
28
+ changes.fetch(:instructions, instructions)
29
+ end
30
+
31
+ # @return [void]
32
+ # :nocov:
33
+ def pretty_print(q)
34
+ name = self.class.name.split("::").last
35
+ q.text "#{name}.build"
36
+ q.group(2, "([", "])") do
37
+ q.breakable
38
+ instructions.each do |op|
39
+ unless q.current_group.first?
40
+ q.text ","
41
+ q.breakable
42
+ end
43
+ q.pp op
44
+ end
45
+ end
46
+ end
47
+ # :nocov:
48
+
49
+ # @todo
50
+ def critique(segment_tok, segment_uses)
51
+ end
52
+
53
+ # Performs no filtering of the {Instruction} list. This is used when there
54
+ # already is a single {Instruction} or when a {Reader::SegmentTok} doesn't
55
+ # provide any more information to filter the list.
56
+ #
57
+ class Stub < ConstraintTable
58
+ def initialize(instructions)
59
+ @instructions = instructions
60
+ end
61
+
62
+ # @return [Array<Instruction>]
63
+ def matches(segment_tok, strict, mode, state = nil)
64
+ @instructions.tap do |xs|
65
+ critique(segment_tok, xs.map(&:segment_use)) if strict
66
+ end
67
+ end
68
+ end
69
+
70
+ # Chooses the {Instruction} that pops the fewest number of states. For
71
+ # example, in the X222 837P an HL segment signals the start of a new
72
+ # 2000 loop, but may or may not begin a new Table 2 -- the specifications
73
+ # aren't actually clear. This rule will not create a new Table 2, but
74
+ # just add a new 2000 loop under the current one.
75
+ #
76
+ class Shallowest < ConstraintTable
77
+ def initialize(instructions)
78
+ @instructions = instructions
79
+ end
80
+
81
+ # @return [Array<Instruction>]
82
+ def matches(segment_tok, strict, mode, state = nil)
83
+ @__matches ||= begin
84
+ shallowest = @instructions.map(&:pop_count).min
85
+ @instructions.select{|i| i.pop_count == shallowest }.tap do |xs|
86
+ critique(segment_tok, xs.map(&:segment_use)) if strict
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ # Chooses the {Instruction} that pops the greatest number of states.
93
+ #
94
+ class Deepest < ConstraintTable
95
+ # def initialize(instructions)
96
+ # @instructions = instructions
97
+ # end
98
+
99
+ # # @return [Array<Instruction>]
100
+ # def matches(segment_tok, strict, mode)
101
+ # @__matches ||= begin
102
+ # deepest = @instructions.map(&:pop_count).max
103
+ # @instructions.select{|i| i.pop_count == deepest }.tap do |xs|
104
+ # critique(segment_tok, xs.map(&:segment_use)) if strict
105
+ # end
106
+ # end
107
+ # end
108
+ end
109
+
110
+ # Chooses the subset of {Instruction} values based on the distinguishing
111
+ # values allowed by each {Schema::SegmentUse}. For instance, there are
112
+ # often several loops that begin with `NM1`, which are distinguished by
113
+ # the qualifier in element `NM101`.
114
+ #
115
+ class ValueBased < ConstraintTable
116
+ def initialize(instructions)
117
+ @instructions = instructions
118
+ @__basis = {}
119
+ end
120
+
121
+ # @return [Array<Instruction>]
122
+ def matches(segment_tok, strict, mode, state = nil)
123
+ invalid = true # Were all present possibly distinguishing elements invalid?
124
+ present = false # Were any possibly distinguishing elements present?
125
+
126
+ disjoint, distinct = basis(@instructions, mode)
127
+
128
+ # First check single elements that can narrow the search space to
129
+ # a single matching Instruction.
130
+ disjoint.each do |(n, m), map|
131
+ value = deconstruct(segment_tok.element_toks, n, m)
132
+
133
+ case value
134
+ when nil, :not_used, :default
135
+ # value wasn't present in segment_tok, can't use it to decide
136
+ else
137
+ singleton = map.at(value)
138
+ present = true
139
+
140
+ unless singleton.nil?
141
+ # Success, search is terminated
142
+ return singleton
143
+ else
144
+ if strict
145
+ designator = "#{segment_tok.id}#{"%02d" % (n + 1)}"
146
+ designator = designator + "-%02d" % (m + 1) unless m.nil?
147
+
148
+ raise ArgumentError,
149
+ "value #{value.to_s} is not allowed in element #{designator}"
150
+ end
151
+ end
152
+ end
153
+ end
154
+
155
+ # If we reach this line, none of the present elements could, on its
156
+ # own, narrow the search space to a single Instruction. We now test
157
+ # the combination of elements to iteratively narrow the search space
158
+ space = @instructions
159
+
160
+ # @todo: These filters could be ordered by probable effectiveness,
161
+ # so we narrow the search space by the largest amount in the fewest
162
+ # number of steps.
163
+ distinct.each do |(n, m), map|
164
+ value = deconstruct(segment_tok.element_toks, n, m)
165
+
166
+ unless value.nil?
167
+ # Lookup which instructions are compatible with this input
168
+ subset = map.at(value)
169
+ present = true
170
+
171
+ unless subset.blank?
172
+ invalid = false
173
+ space &= subset
174
+
175
+ if space.length <= 1
176
+ # Success, search is terminated
177
+ return space
178
+ end
179
+ else
180
+ # This value isn't compatible with any instruction
181
+ if strict
182
+ designator = "#{segment_tok.id}#{"%02d" % (n + 1)}"
183
+ designator = designator + "-%02d" % (m + 1) unless m.nil?
184
+
185
+ raise ArgumentError,
186
+ "value #{value.to_s} is not allowed in element #{designator}"
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ if invalid and present
193
+ # Some elements were present, but all contained invalid values, and
194
+ # even ignoring those we could not narrow the matches to a single
195
+ # instruction.
196
+ #
197
+ # We could return the remaining search space, but it is safest to
198
+ # mark this as an invalid segment and avoid the non-determinism
199
+ []
200
+ else
201
+ # Some elements were present and none were invalid, but it was not
202
+ # possible to narrow the set of matches to a single instruction.
203
+ #
204
+ # When the survivors are sibling slots in the same loop sharing a
205
+ # segment id (e.g. two N3 slots in a partner-customised 4010 PO850
206
+ # N1 loop), use the parser's current parse tree to pick the slot
207
+ # whose preceding-sibling structure has actually been entered
208
+ # (Variant A — structural reachability). The :insert gate skips
209
+ # disambiguation during :find navigation, where the caller wants
210
+ # the full set of candidates a segment could be bound to. For
211
+ # genuinely ambiguous cases (different parent loops, or different
212
+ # segment ids) the helper bails out to the full search space,
213
+ # which will cause non-determinism in the parser.
214
+ if mode == :insert
215
+ disambiguate_sibling_slots(space, state)
216
+ else
217
+ space
218
+ end
219
+ end
220
+ end
221
+
222
+ # Resolve conflicts between instructions that have identical SegmentUse
223
+ # values. For each SegmentUse, this chooses the Instruction that pops
224
+ # the fewest number of states.
225
+ #
226
+ # @return [Array<Instruction>]
227
+ def shallowest(instructions)
228
+ grouped = instructions.group_by{|i| i.segment_use.object_id }
229
+ grouped.flat_map do |k, is|
230
+ shallowest = is.map(&:pop_count).min
231
+ is.select{|i| i.pop_count == shallowest }
232
+ end
233
+ end
234
+
235
+ # When ValueBased filtering leaves multiple Instructions whose
236
+ # SegmentUses are sibling slots in the same parent loop sharing a
237
+ # segment id, pick the slot whose ancestor structure has actually
238
+ # been entered in the current parse tree. Concretely: among the
239
+ # candidate sibling positions, keep only those greater than the
240
+ # highest-position segment already consumed in the parent loop;
241
+ # then pick the earliest among those. If `state` (and thus the
242
+ # parse tree) is not available, fall back to picking the earliest
243
+ # position outright (Variant B). Falls back to the input unchanged
244
+ # when the precondition fails, preserving the existing non-
245
+ # determinism error for genuinely ambiguous cases (e.g. survivors
246
+ # in different parent loops).
247
+ #
248
+ # All survivors share a segment id by construction (instructions
249
+ # are grouped by segment_id in InstructionTable#constraints before
250
+ # ConstraintTable.build is called).
251
+ #
252
+ # @return [Array<Instruction>]
253
+ def disambiguate_sibling_slots(instructions, state = nil)
254
+ return instructions if instructions.length <= 1
255
+
256
+ collapsed = shallowest(instructions)
257
+ return collapsed if collapsed.length <= 1
258
+
259
+ uses = collapsed.map(&:segment_use)
260
+ parent = uses.first.parent
261
+ return instructions if parent.nil?
262
+ return instructions unless uses.all?{|u| u.parent.equal?(parent) }
263
+
264
+ reachable = collapsed
265
+ highest = highest_consumed_position(state, parent) if state
266
+ if highest
267
+ # Strict `>` assumes non-repeating sibling slots: a candidate
268
+ # whose position equals `highest` is excluded because that
269
+ # slot has already been consumed. If a partner grammar exposes
270
+ # repeating sibling slots, this may need to relax to `>=`.
271
+ ahead = collapsed.select{|i| i.segment_use.position > highest }
272
+ reachable = ahead unless ahead.empty?
273
+ end
274
+
275
+ min_pos = reachable.map{|i| i.segment_use.position }.min
276
+ reachable.select{|i| i.segment_use.position == min_pos }
277
+ end
278
+
279
+ # Walks up from the state's value-tree zipper to find the LoopVal/
280
+ # TableVal whose definition matches `parent` (object identity), then
281
+ # returns the highest `usage.position` among its already-consumed
282
+ # segment children. Returns nil when the matching container can't be
283
+ # found (e.g. the loop hasn't been opened yet) or has no consumed
284
+ # segments — both of which mean we have no structural signal to use.
285
+ #
286
+ # @return [Integer, nil]
287
+ def highest_consumed_position(state, parent)
288
+ return nil if state.nil?
289
+ z = state.zipper
290
+ while z
291
+ node = z.node
292
+ if (node.loop? or node.table?) and node.definition.equal?(parent)
293
+ positions = node.children
294
+ .select{|c| c.segment? and c.usage }
295
+ .map{|c| c.usage.position }
296
+ return positions.max if positions.any?
297
+ return nil
298
+ end
299
+ break if z.root?
300
+ z = z.up
301
+ end
302
+ nil
303
+ end
304
+
305
+ # @return [Array(Array<(Integer, Integer, Map)>, Array<(Integer, Integer, Map)>)]
306
+ def basis(instructions, mode)
307
+ @__basis[mode] ||= begin
308
+ # When inserting segments, given a choice between two otherwise
309
+ # equivalent instructions, prefer the one with smallest `pop_count`.
310
+ # For example, when inserting an HL*20 in X221 835, the new 2000A
311
+ # loop could potentially go in the current "Table 2 - Billing
312
+ # Provider Detail" (smaller pop_count), or the parser could create
313
+ # a whole new table (larger pop_count).
314
+ #
315
+ # When searching for segments in a parse tree (mode == :read), we
316
+ # need to try both choices. That's because this "Table 2 - Billing
317
+ # Provider Detail" could be followed by a different "Table 2 -
318
+ # Subscriber Detail", which is then followed by another "Table 2 -
319
+ # Billing Provider Detail". Then the next HL*20 would belong to the
320
+ # uncle table, not the current table.
321
+ if mode == :insert
322
+ instructions = shallowest(instructions)
323
+ end
324
+
325
+ disjoint_elements = []
326
+ distinct_elements = []
327
+
328
+ # The first SegmentUse is used to represent the structure that must
329
+ # be shared by the others: number of elements and type of elements
330
+ element_uses = instructions.head.segment_use.definition.element_uses
331
+
332
+ # Iterate over each element across all SegmentUses (think columns)
333
+ # NM1*[IL]*[ ]*..*..*..*..*..*[ ]*..*..*{..}*..
334
+ # NM1*[40]*[ ]*..*..*..*..*..*[ ]*..*..*{..}*..
335
+ element_uses.length.times do |n|
336
+ if element_uses.at(n).composite?
337
+ ms = 0 .. element_uses.at(n).definition.component_uses.length - 1
338
+ else
339
+ ms = [nil]
340
+ end
341
+
342
+ # If this is a composite element, we iterate over each component.
343
+ # Otherwise this loop iterates once with the index {m} set to nil.
344
+ ms.each do |m|
345
+ last = nil # the last subset we examined
346
+ total = Sets.empty # the union of all examined subsets
347
+
348
+ distinct = false
349
+ disjoint = true
350
+
351
+ instructions.each do |i|
352
+ element_use = i.segment_use.definition.element_uses.at(n)
353
+
354
+ unless m.nil?
355
+ element_use = element_use.definition.component_uses.at(m)
356
+ end
357
+
358
+ allowed_vals = element_use.allowed_values
359
+
360
+ # We want to know if every instruction's set of allowed values
361
+ # is disjoint (with one another). Instead of comparing each set
362
+ # with every other set, which takes (N-1)! comparisons, we can
363
+ # do it in N steps.
364
+ disjoint &&= allowed_vals.disjoint?(total)
365
+
366
+ # We also want to know if one instruction's set of allowed vals
367
+ # contains elements that aren't present in at least one other
368
+ # set. The opposite condition is easy to test: all sets contain
369
+ # the same elements (are equal). So we can similarly, check this
370
+ # condition in N steps rather than (N-1)!
371
+ distinct ||= allowed_vals != last unless last.nil?
372
+
373
+ total = allowed_vals.union(total)
374
+ last = allowed_vals
375
+ end
376
+
377
+ # puts "#{n}.#{m}: disjoint(#{disjoint}) distinct(#{distinct})"
378
+
379
+ if disjoint
380
+ # Since each instruction's set of allowed values is disjoint, we
381
+ # can build a function/hash that returns the single instruction,
382
+ # given one of the values. When given a value outside the set of
383
+ # all (combined) values, it returns nil.
384
+ disjoint_elements << [[n, m], build_disjoint(total, n, m, instructions)]
385
+ elsif distinct
386
+ # Not all instructions have the same set of allowed values. So
387
+ # we can build a function/hash that accepts one of the values
388
+ # and returns the subset of the instructions where that value
389
+ # can occur. This might be some, none, or all of the original
390
+ # instructions, so clearly this provides less information than
391
+ # if each allowed value set was disjoint.
392
+
393
+ # Currently disabled (and untested) because it doesn't look like
394
+ # any of the HIPAA schemas would use this -- so testing it would
395
+ # be a pain.
396
+ #
397
+ distinct_elements << [[n, m], build_distinct(total, n, m, instructions)]
398
+ end
399
+ end
400
+ end
401
+
402
+ [disjoint_elements, distinct_elements]
403
+ end
404
+ end
405
+
406
+ # @return [Hash<String, Array<Instruction>>]
407
+ def build_disjoint(total, n, m, instructions)
408
+ if total.finite?
409
+ # The sum of all allowed value sets is finite, so we know that each
410
+ # individual allowed value set is finite (we can iterate over it).
411
+ map = Hash.new
412
+
413
+ instructions.each do |i|
414
+ element_use = i.segment_use.definition.element_uses.at(n)
415
+
416
+ unless m.nil?
417
+ element_use = element_use.definition.component_uses.at(m)
418
+ end
419
+
420
+ allowed_vals = element_use.allowed_values
421
+ allowed_vals.each{|v| map[v] = i.cons }
422
+ end
423
+
424
+ map
425
+ else
426
+ # At least one of allowed value sets is infinite. This happens when
427
+ # it is RelativeComplement, which declares the values that are *not*
428
+ # allowed in the set.
429
+ map = Hash.new{|h,k| h[k] = instructions }
430
+
431
+ instructions.each do |i|
432
+ element_use = i.segment_use.definition.element_uses.at(n)
433
+ unless m.nil?
434
+ element_use = element_use.definition.component_uses.at(m)
435
+ end
436
+
437
+ allowed_vals = element_use.allowed_values
438
+
439
+ unless allowed_vals.finite?
440
+ allowed_vals.complement.each{|v| map[v] -= i }
441
+ end
442
+ end
443
+
444
+ # Clear the default_proc so accesses don't change the Hash
445
+ map.default = instructions
446
+ map
447
+ end
448
+ end
449
+
450
+ # @return [Hash<String, Array<Instruction>>]
451
+ def build_distinct(total, n, m, instructions)
452
+ if total.finite?
453
+ # The sum of all allowed value sets is finite, so we know that each
454
+ # individual allowed value set is finite (we can iterate over it).
455
+ map = Hash.new{|h,k| h[k] = [] }
456
+
457
+ instructions.each do |i|
458
+ element_use = i.segment_use.definition.element_uses.at(n)
459
+
460
+ unless m.nil?
461
+ element_use = element_use.definition.component_uses.at(m)
462
+ end
463
+
464
+ allowed_vals = element_use.allowed_values
465
+ allowed_vals.each{|v| map[v] << i }
466
+ end
467
+
468
+ # Clear the default_proc so accesses don't change the Hash
469
+ map.default = []
470
+ map
471
+ else
472
+ # At least one of allowed value sets is infinite. This happens when
473
+ # it is RelativeComplement, which declares the values that are *not*
474
+ # allowed in the set.
475
+ map = Hash.new{|h,k| h[k] = instructions }
476
+
477
+ instructions.each do |i|
478
+ element_use = i.segment_use.definition.element_uses.at(n)
479
+
480
+ unless m.nil?
481
+ element_use = element_use.definition.component_uses.at(m)
482
+ end
483
+
484
+ allowed_vals = element_use.allowed_values
485
+
486
+ unless allowed_vals.finite?
487
+ allowed_vals.complement.each{|v| map[v] -= i }
488
+ end
489
+ end
490
+
491
+ # Clear the default_proc so accesses don't change the Hash
492
+ map.default = instructions
493
+ map
494
+ end
495
+ end
496
+
497
+ # Return the value of the `m`-th elemnt, or if `n` is not nil, return
498
+ # the value of the `n`-th component from the `n`-th element. When the
499
+ # value is blank, the function returns `nil`.
500
+ #
501
+ # @param [Array<Reader::SimpleElementTok, Reader::CompositeElementTok>] element_toks
502
+ # @param [Integer] m
503
+ # @param [Integer, nil] n
504
+ #
505
+ # @return [String, nil]
506
+ def deconstruct(element_toks, m, n)
507
+ element_tok = element_toks.at(m)
508
+ element_tok = element_tok.element_toks.at(0) if element_tok.try(:repeated?)
509
+
510
+ if element_tok.blank?
511
+ nil
512
+ elsif n.nil?
513
+ element_tok.value
514
+ else
515
+ element_tok = element_tok.component_toks.at(n)
516
+
517
+ if element_tok.blank?
518
+ nil
519
+ else
520
+ element_tok.value
521
+ end
522
+ end
523
+ end
524
+ end
525
+ end
526
+
527
+ class << ConstraintTable
528
+ # @group Constructors
529
+ #########################################################################
530
+
531
+ # Given a list of {Instruction} values for the same segment identifier,
532
+ # this method constructs the appropriate concrete subclass of
533
+ # {ConstraintTable}.
534
+ #
535
+ # @param [Array<Instruction>] instructions
536
+ # @return [ConstraintTable]
537
+ def build(instructions)
538
+ if instructions.length <= 1
539
+ ConstraintTable::Stub.new(instructions)
540
+ elsif instructions.any?{|i| i.segment_use.nil? } and
541
+ not instructions.all?{|i| i.segment_use.nil? }
542
+ # When one of the instructions has a nil segment_use, it means
543
+ # the SegmentUse is determined when pushing the new state. There
544
+ # isn't a way to know the segment constraints from here.
545
+ ConstraintTable::Stub.new(instructions)
546
+ else
547
+ segment_uses = instructions.map{|i| i.segment_use }
548
+
549
+ if segment_uses.map{|u| u.object_id }.uniq.length <= 1
550
+ # The same SegmentUse may appear more than once, because the
551
+ # segment can be placed at different levels in the tree. If
552
+ # all the instructions have the same SegmentUse, they also have
553
+ # the same element constraints so we can't use them to narrow
554
+ # down the instruction list.
555
+ ConstraintTable::Shallowest.new(instructions)
556
+ else
557
+ ConstraintTable::ValueBased.new(instructions)
558
+ end
559
+ end
560
+ end
561
+
562
+ # @endgroup
563
+ #########################################################################
564
+ end
565
+ end
566
+ end