csv_decision 0.0.8 → 0.0.9

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +62 -28
  5. data/csv_decision.gemspec +1 -1
  6. data/doc/CSVDecision/CellValidationError.html +2 -2
  7. data/doc/CSVDecision/Columns/Dictionary.html +114 -20
  8. data/doc/CSVDecision/Columns/Entry.html +2 -2
  9. data/doc/CSVDecision/Columns.html +109 -27
  10. data/doc/CSVDecision/Data.html +2 -2
  11. data/doc/CSVDecision/Decide.html +2 -2
  12. data/doc/CSVDecision/Decision.html +21 -21
  13. data/doc/CSVDecision/Dictionary/Entry.html +508 -0
  14. data/doc/CSVDecision/Dictionary.html +265 -0
  15. data/doc/CSVDecision/Error.html +2 -2
  16. data/doc/CSVDecision/FileError.html +3 -3
  17. data/doc/CSVDecision/Header.html +37 -136
  18. data/doc/CSVDecision/Input.html +2 -2
  19. data/doc/CSVDecision/Load.html +2 -2
  20. data/doc/CSVDecision/Matchers/Constant.html +2 -2
  21. data/doc/CSVDecision/Matchers/Function.html +2 -2
  22. data/doc/CSVDecision/Matchers/Guard.html +92 -25
  23. data/doc/CSVDecision/Matchers/Matcher.html +14 -18
  24. data/doc/CSVDecision/Matchers/Numeric.html +2 -2
  25. data/doc/CSVDecision/Matchers/Pattern.html +2 -2
  26. data/doc/CSVDecision/Matchers/Range.html +2 -2
  27. data/doc/CSVDecision/Matchers/Symbol.html +2 -2
  28. data/doc/CSVDecision/Matchers.html +5 -5
  29. data/doc/CSVDecision/Options.html +2 -2
  30. data/doc/CSVDecision/Parse.html +6 -4
  31. data/doc/CSVDecision/Result.html +944 -0
  32. data/doc/CSVDecision/ScanRow.html +70 -80
  33. data/doc/CSVDecision/Table.html +134 -54
  34. data/doc/CSVDecision.html +5 -5
  35. data/doc/_index.html +18 -4
  36. data/doc/class_list.html +1 -1
  37. data/doc/file.README.html +132 -62
  38. data/doc/index.html +132 -62
  39. data/doc/method_list.html +156 -60
  40. data/doc/top-level-namespace.html +2 -2
  41. data/lib/csv_decision/columns.rb +1 -8
  42. data/lib/csv_decision/decision.rb +45 -96
  43. data/lib/csv_decision/dictionary.rb +149 -0
  44. data/lib/csv_decision/header.rb +6 -133
  45. data/lib/csv_decision/matchers.rb +1 -2
  46. data/lib/csv_decision/parse.rb +18 -7
  47. data/lib/csv_decision/result.rb +180 -0
  48. data/lib/csv_decision/scan_row.rb +13 -7
  49. data/lib/csv_decision/table.rb +6 -5
  50. data/lib/csv_decision.rb +3 -1
  51. data/spec/csv_decision/columns_spec.rb +25 -4
  52. data/spec/csv_decision/examples_spec.rb +25 -0
  53. data/spec/csv_decision/matchers/guard_spec.rb +26 -9
  54. data/spec/csv_decision/table_spec.rb +48 -2
  55. metadata +7 -2
@@ -11,26 +11,25 @@ module CSVDecision
11
11
  # @param table [CSVDecision::Table] Decision table being processed.
12
12
  # @param input [Hash{Symbol=>Object}] Input hash data structure.
13
13
  def initialize(table:, input:)
14
- @result = {}
15
-
16
- # Relevant table attributes
17
- @first_match = table.options[:first_match]
18
- @outs = table.columns.outs
19
- @outs_functions = table.outs_functions
20
-
21
- # Partial result always includes the input hash for calculating output functions
22
- @partial_result = input[:hash].dup if @outs_functions
14
+ # The result object is a hash of values, and each value will be an array if this is
15
+ # a multi-row result for the +first_match: false+ option.
16
+ @result = Result.new(table: table, input: input)
23
17
 
18
+ # All rows picked by the matching process. An array if +first_match: false+, otherwise
19
+ # a single row.
24
20
  @rows_picked = []
21
+
22
+ # Relevant table attributes
23
+ table_attributes(table)
25
24
  end
26
25
 
27
26
  # Scan the decision table up against the input hash.
28
27
  #
29
- # @param table [CSVDecision::Table] Decision table being processed.
30
- # @param input (see #initialize)
31
- # @return [self] Decision object built so far.
28
+ # @param (see #initialize)
29
+ # @return [{Symbol=>Object}] Decision result.
32
30
  def scan(table:, input:)
33
31
  table.each do |row, index|
32
+ # +row_scan+ returns false if more rows need to be scanned, truthy otherwise.
34
33
  return result if row_scan(input: input, row: row, scan_row: table.scan_rows[index])
35
34
  end
36
35
 
@@ -39,133 +38,83 @@ module CSVDecision
39
38
 
40
39
  private
41
40
 
42
- # Calculate the final result.
43
- # @return [nil, Hash{Symbol=>Object}] Final result hash if found, otherwise nil for no result.
41
+ # Record the relevant table attributes.
42
+ def table_attributes(table)
43
+ @first_match = table.options[:first_match]
44
+ @outs = table.columns.outs
45
+ @outs_functions = table.outs_functions
46
+ end
47
+
48
+ # Derive the final result.
49
+ #
50
+ # @return [nil, Hash{Symbol=>Object}] Final result hash if matches found,
51
+ # otherwise the empty hash for no result.
44
52
  def result
45
53
  return {} if @rows_picked.blank?
46
- @first_match ? final_result : accumulated_result
54
+ @first_match ? @result.attributes : accumulated_result
47
55
  end
48
56
 
57
+ # Scan the row for matches against the input conditions.
49
58
  def row_scan(input:, row:, scan_row:)
59
+ # +add+ returns false if more rows need to be scanned, truthy otherwise.
50
60
  add(row) if Decide.matches?(row: row, input: input, scan_row: scan_row)
51
61
  end
52
62
 
53
63
  # Add a matched row to the decision object being built.
54
64
  #
55
- # @param row [Array]
65
+ # @param row [Array] Data row.
66
+ # @return [false, Hash]
56
67
  def add(row)
57
68
  return add_first_match(row) if @first_match
58
69
 
59
70
  # Accumulate output rows
60
71
  @rows_picked << row
61
- @outs.each_pair { |col, column| accumulate_outs(column_name: column.name, cell: row[col]) }
72
+ @result.accumulate_outs(row)
62
73
 
63
74
  # Not done
64
75
  false
65
76
  end
66
77
 
67
78
  def accumulated_result
68
- return final_result unless @outs_functions
69
- return eval_outs(@rows_picked.first) unless @multi_result
79
+ return @result.final unless @outs_functions
80
+ return @result.eval_outs(@rows_picked.first) unless @result.multi_result
70
81
 
71
82
  multi_row_result
72
83
  end
73
84
 
74
85
  def multi_row_result
75
86
  # Scan each output column that contains functions
76
- @outs.each_pair do |col, column|
77
- # Does this column have any functions defined?
78
- next unless column.eval
79
-
80
- eval_column_procs(col, column)
81
- end
82
-
83
- final_result
84
- end
85
-
86
- def accumulate_outs(column_name:, cell:)
87
- case (current = @result[column_name])
88
- when nil
89
- @result[column_name] = cell
90
-
91
- when Array
92
- @result[column_name] << cell
87
+ @outs.each_pair { |col, column| eval_column_procs(col: col, column: column) if column.eval }
93
88
 
94
- else
95
- @result[column_name] = [current, cell]
96
- @multi_result ||= true
97
- end
89
+ @result.final
98
90
  end
99
91
 
100
- def eval_column_procs(col, column)
92
+ def eval_column_procs(col:, column:)
101
93
  @rows_picked.each_with_index do |row, index|
102
94
  proc = row[col]
103
95
  next unless proc.is_a?(Matchers::Proc)
104
96
 
105
97
  # Evaluate the proc and update the result
106
- eval_cell_proc(proc: proc, column_name: column.name, index: index)
98
+ @result.eval_cell_proc(proc: proc, column_name: column.name, index: index)
107
99
  end
108
100
  end
109
101
 
110
- # Update the partial result calculated so far and call the function
111
- def eval_cell_proc(proc:, column_name:, index:)
112
- value = proc.function[partial_result(index)]
113
- @multi_result ? @result[column_name][index] = value : @result[column_name] = value
114
- end
115
-
116
- def partial_result(index)
117
- @result.each_pair do |column_name, value|
118
- # Delete this column from the partial result in case there is data from a prior result row
119
- next @partial_result.delete(column_name) if value[index].is_a?(Matchers::Proc)
120
- @partial_result[column_name] = value[index]
121
- end
122
-
123
- @partial_result
124
- end
125
-
126
- def final_result
127
- @result
128
- end
129
-
130
102
  def add_first_match(row)
131
- @rows_picked = row
132
-
133
- return eval_outs(row) if @outs_functions
134
-
135
- # Common case is just copying output column values to the final result
136
- @outs.each_pair { |col, column| @result[column.name] = row[col] }
137
- end
138
-
139
- def eval_outs(row)
140
- # Set the constants first, in case the functions refer to them
141
- eval_outs_constants(row)
142
-
143
- # Then evaluate the functions, left to right
144
- eval_outs_procs(row)
103
+ # This decision row may contain procs, which if present will need to be evaluated.
104
+ # If this row contains if: columns then this row may be filtered out, in which case
105
+ # this method call will return false.
106
+ return eval_single_row(row) if @outs_functions
145
107
 
146
- final_result
108
+ # Common case is just copying output column values to the final result.
109
+ @rows_picked = row
110
+ @result.add_outs(row)
147
111
  end
148
112
 
149
- def eval_outs_constants(row)
150
- @outs.each_pair do |col, column|
151
- value = row[col]
152
- next if value.is_a?(Matchers::Proc)
153
-
154
- @partial_result[column.name] = value
155
- @result[column.name] = value
156
- end
157
- end
113
+ def eval_single_row(row)
114
+ return false unless (result = @result.eval_outs(row))
158
115
 
159
- def eval_outs_procs(row)
160
- @outs.each_pair do |col, column|
161
- proc = row[col]
162
- next unless proc.is_a?(Matchers::Proc)
163
-
164
- value = proc.function[@partial_result]
165
-
166
- @partial_result[column.name] = value
167
- @result[column.name] = value
168
- end
116
+ @rows_picked = row
117
+ result
169
118
  end
170
119
  end
171
120
  end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017.
5
+ # @author Brett Vickers <brett@phillips-vickers.com>
6
+ # See LICENSE and README.md for details.
7
+ module CSVDecision
8
+ # Parse the CSV file's header row. These methods are only required at table load time.
9
+ # @api private
10
+ module Dictionary
11
+ # Table used to build a column dictionary entry.
12
+ ENTRY = {
13
+ in: { type: :in, eval: nil },
14
+ 'in/text': { type: :in, eval: false },
15
+ out: { type: :out, eval: nil },
16
+ 'out/text': { type: :out, eval: false },
17
+ guard: { type: :guard, eval: true },
18
+ if: { type: :if, eval: true }
19
+ }.freeze
20
+ private_constant :ENTRY
21
+
22
+ # Value object to hold column dictionary entries.
23
+ Entry = Struct.new(:name, :eval, :type) do
24
+ def ins?
25
+ %i[in guard].member?(type) ? true : false
26
+ end
27
+ end
28
+
29
+ # TODO: implement all anonymous column types
30
+ # COLUMN_TYPE_ANONYMOUS = Set.new(%i[path if guard]).freeze
31
+ # These column types do not need a name
32
+ COLUMN_TYPE_ANONYMOUS = Set.new(%i[guard if]).freeze
33
+ private_constant :COLUMN_TYPE_ANONYMOUS
34
+
35
+ # Classify and build a dictionary of all input and output columns by
36
+ # parsing the header row.
37
+ #
38
+ # @param header [Array<String>] The header row after removing any empty columns.
39
+ # @return [Hash<Hash>] Column dictionary is a hash of hashes.
40
+ def self.build(header:, dictionary:)
41
+ header.each_with_index do |cell, index|
42
+ dictionary = parse_cell(cell: cell, index: index, dictionary: dictionary)
43
+ end
44
+
45
+ validate(dictionary)
46
+ end
47
+
48
+ def self.validate(dictionary)
49
+ dictionary.outs.each_pair do |col, column|
50
+ validate_out(dictionary: dictionary, column_name: column.name, col: col)
51
+ end
52
+
53
+ dictionary
54
+ end
55
+ private_class_method :validate
56
+
57
+ def self.validate_out(dictionary:, column_name:, col:)
58
+ if dictionary.ins.any? { |_, column| column_name == column.name }
59
+ raise CellValidationError, "output column name '#{column_name}' is also an input column"
60
+ end
61
+
62
+ return unless dictionary.outs.any? { |key, column| column_name == column.name && col != key }
63
+ raise CellValidationError, "output column name '#{column_name}' is duplicated"
64
+ end
65
+ private_class_method :validate_out
66
+
67
+ def self.validate_column(cell:, index:)
68
+ match = Header::COLUMN_TYPE.match(cell)
69
+ raise CellValidationError, 'column name is not well formed' unless match
70
+
71
+ column_type = match['type']&.downcase&.to_sym
72
+ column_name = column_name(type: column_type, name: match['name'], index: index)
73
+
74
+ [column_type, column_name]
75
+ rescue CellValidationError => exp
76
+ raise CellValidationError, "header column '#{cell}' is not valid as the #{exp.message}"
77
+ end
78
+ private_class_method :validate_column
79
+
80
+ def self.column_name(type:, name:, index:)
81
+ # if: columns are named after their index, which is an integer and so cannot
82
+ # clash with other column name types, which are symbols.
83
+ return index if type == :if
84
+
85
+ return format_column_name(name) if name.present?
86
+
87
+ return if COLUMN_TYPE_ANONYMOUS.member?(type)
88
+ raise CellValidationError, 'column name is missing'
89
+ end
90
+ private_class_method :column_name
91
+
92
+ def self.format_column_name(name)
93
+ column_name = name.strip.tr("\s", '_')
94
+
95
+ return column_name.to_sym if Header::COLUMN_NAME_RE.match(column_name)
96
+ raise CellValidationError, "column name '#{name}' contains invalid characters"
97
+ end
98
+ private_class_method :format_column_name
99
+
100
+ # Returns the normalized column type, along with an indication if
101
+ # the column requires evaluation
102
+ def self.column_type(column_name, entry)
103
+ Entry.new(column_name, entry[:eval], entry[:type])
104
+ end
105
+ private_class_method :column_type
106
+
107
+ def self.parse_cell(cell:, index:, dictionary:)
108
+ column_type, column_name = validate_column(cell: cell, index: index)
109
+
110
+ entry = column_type(column_name, ENTRY[column_type])
111
+
112
+ dictionary_entry(dictionary: dictionary, entry: entry, index: index)
113
+ end
114
+ private_class_method :parse_cell
115
+
116
+ def self.dictionary_entry(dictionary:, entry:, index:)
117
+ case entry.type
118
+ # Header column that has a function for setting the value (planned feature)
119
+ # when :set, :'set/nil?', :'set/blank?'
120
+ # # Default function will set the input value unconditionally or conditionally
121
+ # dictionary.defaults[index] =
122
+ # Columns::Default.new(entry.name, nil, default_if(type))
123
+ #
124
+ # # Treat set: as an in: column
125
+ # dictionary.ins[index] = entry
126
+
127
+ when :in, :guard
128
+ dictionary.ins[index] = entry
129
+
130
+ when :out
131
+ dictionary.outs[index] = entry
132
+
133
+ when :if
134
+ dictionary.outs[index] = entry
135
+ dictionary.ifs[index] = entry
136
+ end
137
+
138
+ dictionary
139
+ end
140
+ private_class_method :dictionary_entry
141
+
142
+ # def self.default_if(type)
143
+ # return nil if type == :set
144
+ # return :nil? if type == :'set/nil'
145
+ # :blank?
146
+ # end
147
+ # private_class_method :default_if
148
+ end
149
+ end
@@ -20,37 +20,20 @@ module CSVDecision
20
20
  \s*:\s*(?<name>\S?.*)\z
21
21
  }xi
22
22
 
23
- COLUMN_ENTRY = {
24
- in: { type: :in, eval: nil },
25
- 'in/text': { type: :in, eval: false },
26
- out: { type: :out, eval: nil },
27
- 'out/text': { type: :out, eval: false },
28
- guard: { type: :guard, eval: true },
29
- if: { type: :if, eval: true }
30
- }.freeze
31
- private_constant :COLUMN_ENTRY
32
-
33
- # TODO: implement all anonymous column types
34
- # COLUMN_TYPE_ANONYMOUS = Set.new(%i[path if guard]).freeze
35
- # These column types do not need a name
36
- COLUMN_TYPE_ANONYMOUS = Set.new(%i[guard]).freeze
37
- private_constant :COLUMN_TYPE_ANONYMOUS
38
-
39
23
  # Regular expression string for a column name.
40
24
  # More lenient than a Ruby method name - note any spaces will have been replaced with
41
25
  # underscores.
42
26
  COLUMN_NAME = "\\w[\\w:/!?]*"
43
27
 
44
- # Column name regular expression.
45
- COLUMN_NAME_RE = Matchers.regexp(COLUMN_NAME)
46
- private_constant :COLUMN_NAME_RE
28
+ # Regular expression for matching a column name.
29
+ COLUMN_NAME_RE = Matchers.regexp(Header::COLUMN_NAME)
47
30
 
48
31
  # Check if the given row contains a recognisable header cell.
49
32
  #
50
33
  # @param row [Array<String>] Header row.
51
34
  # @return [Boolean] Return true if the row looks like a header.
52
35
  def self.row?(row)
53
- row.find { |cell| cell.match(COLUMN_TYPE) }
36
+ row.any? { |cell| cell.match(COLUMN_TYPE) }
54
37
  end
55
38
 
56
39
  # Strip empty columns from all data rows.
@@ -66,52 +49,9 @@ module CSVDecision
66
49
  rows.shift
67
50
  end
68
51
 
69
- # Classify and build a dictionary of all input and output columns.
70
- #
71
- # @param row [Array<String>] The header row after removing any empty columns.
72
- # @return [Hash<Hash>] Column dictionary is a hash of hashes.
73
- def self.dictionary(row:)
74
- dictionary = Columns::Dictionary.new
75
-
76
- row.each_with_index do |cell, index|
77
- dictionary = parse_cell(cell: cell, index: index, dictionary: dictionary)
78
- end
79
-
80
- validate(dictionary: dictionary)
81
- end
82
-
83
- def self.validate(dictionary:)
84
- dictionary.outs.each_value do |column|
85
- next unless input_column?(dictionary: dictionary, column_name: column.name)
86
-
87
- raise CellValidationError, "output column name '#{column.name}' is also an input column"
88
- end
89
-
90
- dictionary
91
- end
92
- private_class_method :validate
93
-
94
- def self.input_column?(dictionary:, column_name:)
95
- dictionary.ins.each_value { |column| return true if column_name == column.name }
96
-
97
- false
98
- end
99
- private_class_method :input_column?
100
-
101
- def self.validate_column(cell:)
102
- match = COLUMN_TYPE.match(cell)
103
- raise CellValidationError, 'column name is not well formed' unless match
104
-
105
- column_type = match['type']&.downcase&.to_sym
106
- column_name = column_name(type: column_type, name: match['name'])
107
-
108
- [column_type, column_name]
109
- rescue CellValidationError => exp
110
- raise CellValidationError, "header column '#{cell}' is not valid as the #{exp.message}"
111
- end
112
- private_class_method :validate_column
113
-
114
- # Array of all empty column indices.
52
+ # Build an array of all empty column indices.
53
+ # @param row [Array]
54
+ # @return [false, Array<Integer>]
115
55
  def self.empty_columns?(row:)
116
56
  result = []
117
57
  row&.each_with_index { |cell, index| result << index if cell == '' }
@@ -119,72 +59,5 @@ module CSVDecision
119
59
  result.empty? ? false : result
120
60
  end
121
61
  private_class_method :empty_columns?
122
-
123
- def self.column_name(type:, name:)
124
- return format_column_name(name) if name.present?
125
-
126
- return if COLUMN_TYPE_ANONYMOUS.member?(type)
127
-
128
- raise CellValidationError, 'column name is missing'
129
- end
130
- private_class_method :column_name
131
-
132
- def self.format_column_name(name)
133
- column_name = name.strip.tr("\s", '_')
134
-
135
- return column_name.to_sym if COLUMN_NAME_RE.match(column_name)
136
-
137
- raise CellValidationError, "column name '#{name}' contains invalid characters"
138
- end
139
- private_class_method :format_column_name
140
-
141
- # Returns the normalized column type, along with an indication if
142
- # the column requires evaluation
143
- def self.column_type(column_name, type)
144
- entry = COLUMN_ENTRY[type]
145
- Columns::Entry.new(column_name, entry[:eval], entry[:type])
146
- end
147
- private_class_method :column_type
148
-
149
- def self.parse_cell(cell:, index:, dictionary:)
150
- column_type, column_name = validate_column(cell: cell)
151
-
152
- entry = column_type(column_name, column_type)
153
-
154
- dictionary_entry(dictionary: dictionary,
155
- type: entry.type,
156
- entry: entry,
157
- index: index)
158
- end
159
- private_class_method :parse_cell
160
-
161
- def self.dictionary_entry(dictionary:, type:, entry:, index:)
162
- case type
163
- # Header column that has a function for setting the value (planned feature)
164
- # when :set, :'set/nil', :'set/blank'
165
- # # Default function will set the input value unconditionally or conditionally
166
- # dictionary.defaults[index] =
167
- # Columns::Default.new(entry.name, nil, default_if(type))
168
- #
169
- # # Treat set: as an in: column
170
- # dictionary.ins[index] = entry
171
-
172
- when :in, :guard
173
- dictionary.ins[index] = entry
174
-
175
- when :out
176
- dictionary.outs[index] = entry
177
- end
178
-
179
- dictionary
180
- end
181
- private_class_method :dictionary_entry
182
-
183
- # def self.default_if(type)
184
- # return nil if type == :set
185
- # return :nil? if type == :'set/nil'
186
- # :blank?
187
- # end
188
- # private_class_method :default_if
189
62
  end
190
63
  end
@@ -124,8 +124,7 @@ module CSVDecision
124
124
  Matchers.parse(columns: columns, matchers: @outs, row: row)
125
125
  end
126
126
 
127
- # @abstract Subclass and override {#matches?} to implement
128
- # a custom Matcher class.
127
+ # Subclass and override {#matches?} to implement a custom Matcher class.
129
128
  class Matcher
130
129
  def initialize(_options = nil); end
131
130
 
@@ -14,7 +14,7 @@ module CSVDecision
14
14
  # Error validating a cell when parsing input table data.
15
15
  class CellValidationError < Error; end
16
16
 
17
- # Table parsing error message enhanced to include the file being processed
17
+ # Table parsing error message enhanced to include the file being processed.
18
18
  class FileError < Error; end
19
19
 
20
20
  # Builds a decision table from the input data - which may either be a file, CSV string
@@ -62,6 +62,7 @@ module CSVDecision
62
62
 
63
63
  parse_table(table: table, input: data, options: options)
64
64
 
65
+ # The table object is now immutable.
65
66
  table.columns.deep_freeze
66
67
  table.freeze
67
68
  rescue CSVDecision::Error => exp
@@ -93,25 +94,35 @@ module CSVDecision
93
94
 
94
95
  def self.parse_data(table:, matchers:)
95
96
  table.rows.each_with_index do |row, index|
96
- row, table.scan_rows[index] = matchers.parse_ins(columns: table.columns.ins, row: row)
97
- row, table.outs_rows[index] = matchers.parse_outs(columns: table.columns.outs, row: row)
97
+ # Mutate the row if we find anything other than a simple string constant in its
98
+ # data cells.
99
+ row = parse_row(table: table, matchers: matchers, row: row, index: index)
98
100
 
99
- # Does the table have any output functions?
101
+ # Does the row have any output functions?
100
102
  outs_functions(table: table, index: index)
101
103
 
104
+ # No more mutations required for this row.
102
105
  row.freeze
103
106
  end
104
107
  end
105
108
  private_class_method :parse_data
106
109
 
110
+ def self.parse_row(table:, matchers:, row:, index:)
111
+ row, table.scan_rows[index] = matchers.parse_ins(columns: table.columns.ins, row: row)
112
+ row, table.outs_rows[index] = matchers.parse_outs(columns: table.columns.outs, row: row)
113
+
114
+ row
115
+ end
116
+ private_class_method :parse_row
117
+
107
118
  def self.outs_functions(table:, index:)
108
119
  return if table.outs_rows[index].procs.empty?
109
120
 
110
121
  # Set this flag as the table has output functions
111
- table.outs_functions ||= true
122
+ table.outs_functions = true
112
123
 
113
- outs = table.columns.outs
114
- table.outs_rows[index].procs.each { |col| outs[col].eval = true }
124
+ # Update the output columns that contain functions needing evaluation.
125
+ table.outs_rows[index].procs.each { |col| table.columns.outs[col].eval = true }
115
126
  end
116
127
  private_class_method :outs_functions
117
128
  end