csv_decision2 0.5.1

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 (134) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +3 -0
  3. data/.coveralls.yml +2 -0
  4. data/.gitignore +14 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +30 -0
  7. data/.travis.yml +6 -0
  8. data/CHANGELOG.md +85 -0
  9. data/Dockerfile +6 -0
  10. data/Gemfile +7 -0
  11. data/LICENSE +21 -0
  12. data/README.md +356 -0
  13. data/benchmarks/rufus_decision.rb +158 -0
  14. data/csv_decision2.gemspec +38 -0
  15. data/doc/CSVDecision/CellValidationError.html +143 -0
  16. data/doc/CSVDecision/Columns/Default.html +589 -0
  17. data/doc/CSVDecision/Columns/Dictionary.html +801 -0
  18. data/doc/CSVDecision/Columns/Entry.html +508 -0
  19. data/doc/CSVDecision/Columns.html +1259 -0
  20. data/doc/CSVDecision/Constant.html +254 -0
  21. data/doc/CSVDecision/Data.html +479 -0
  22. data/doc/CSVDecision/Decide.html +302 -0
  23. data/doc/CSVDecision/Decision.html +1011 -0
  24. data/doc/CSVDecision/Defaults.html +291 -0
  25. data/doc/CSVDecision/Dictionary/Entry.html +1147 -0
  26. data/doc/CSVDecision/Dictionary.html +426 -0
  27. data/doc/CSVDecision/Error.html +139 -0
  28. data/doc/CSVDecision/FileError.html +143 -0
  29. data/doc/CSVDecision/Function.html +240 -0
  30. data/doc/CSVDecision/Guard.html +245 -0
  31. data/doc/CSVDecision/Header.html +647 -0
  32. data/doc/CSVDecision/Index.html +741 -0
  33. data/doc/CSVDecision/Input.html +404 -0
  34. data/doc/CSVDecision/Load.html +296 -0
  35. data/doc/CSVDecision/Matchers/Constant.html +484 -0
  36. data/doc/CSVDecision/Matchers/Function.html +511 -0
  37. data/doc/CSVDecision/Matchers/Guard.html +503 -0
  38. data/doc/CSVDecision/Matchers/Matcher.html +507 -0
  39. data/doc/CSVDecision/Matchers/Numeric.html +415 -0
  40. data/doc/CSVDecision/Matchers/Pattern.html +491 -0
  41. data/doc/CSVDecision/Matchers/Proc.html +704 -0
  42. data/doc/CSVDecision/Matchers/Range.html +379 -0
  43. data/doc/CSVDecision/Matchers/Symbol.html +426 -0
  44. data/doc/CSVDecision/Matchers.html +1567 -0
  45. data/doc/CSVDecision/Numeric.html +259 -0
  46. data/doc/CSVDecision/Options.html +443 -0
  47. data/doc/CSVDecision/Parse.html +282 -0
  48. data/doc/CSVDecision/Paths.html +742 -0
  49. data/doc/CSVDecision/Result.html +1200 -0
  50. data/doc/CSVDecision/Scan/InputHashes.html +369 -0
  51. data/doc/CSVDecision/Scan.html +313 -0
  52. data/doc/CSVDecision/ScanRow.html +866 -0
  53. data/doc/CSVDecision/Symbol.html +256 -0
  54. data/doc/CSVDecision/Table.html +1470 -0
  55. data/doc/CSVDecision/TableValidationError.html +143 -0
  56. data/doc/CSVDecision/Validate.html +422 -0
  57. data/doc/CSVDecision.html +621 -0
  58. data/doc/_index.html +471 -0
  59. data/doc/class_list.html +51 -0
  60. data/doc/css/common.css +1 -0
  61. data/doc/css/full_list.css +58 -0
  62. data/doc/css/style.css +499 -0
  63. data/doc/file.README.html +421 -0
  64. data/doc/file_list.html +56 -0
  65. data/doc/frames.html +17 -0
  66. data/doc/index.html +421 -0
  67. data/doc/js/app.js +248 -0
  68. data/doc/js/full_list.js +216 -0
  69. data/doc/js/jquery.js +4 -0
  70. data/doc/method_list.html +1163 -0
  71. data/doc/top-level-namespace.html +110 -0
  72. data/docker-compose.yml +13 -0
  73. data/lib/csv_decision/columns.rb +192 -0
  74. data/lib/csv_decision/data.rb +92 -0
  75. data/lib/csv_decision/decision.rb +196 -0
  76. data/lib/csv_decision/defaults.rb +47 -0
  77. data/lib/csv_decision/dictionary.rb +180 -0
  78. data/lib/csv_decision/header.rb +83 -0
  79. data/lib/csv_decision/index.rb +107 -0
  80. data/lib/csv_decision/input.rb +121 -0
  81. data/lib/csv_decision/load.rb +36 -0
  82. data/lib/csv_decision/matchers/constant.rb +74 -0
  83. data/lib/csv_decision/matchers/function.rb +56 -0
  84. data/lib/csv_decision/matchers/guard.rb +142 -0
  85. data/lib/csv_decision/matchers/numeric.rb +44 -0
  86. data/lib/csv_decision/matchers/pattern.rb +94 -0
  87. data/lib/csv_decision/matchers/range.rb +95 -0
  88. data/lib/csv_decision/matchers/symbol.rb +149 -0
  89. data/lib/csv_decision/matchers.rb +220 -0
  90. data/lib/csv_decision/options.rb +124 -0
  91. data/lib/csv_decision/parse.rb +165 -0
  92. data/lib/csv_decision/paths.rb +78 -0
  93. data/lib/csv_decision/result.rb +204 -0
  94. data/lib/csv_decision/scan.rb +117 -0
  95. data/lib/csv_decision/scan_row.rb +142 -0
  96. data/lib/csv_decision/table.rb +101 -0
  97. data/lib/csv_decision/validate.rb +85 -0
  98. data/lib/csv_decision.rb +45 -0
  99. data/spec/csv_decision/columns_spec.rb +251 -0
  100. data/spec/csv_decision/constant_spec.rb +36 -0
  101. data/spec/csv_decision/data_spec.rb +50 -0
  102. data/spec/csv_decision/decision_spec.rb +19 -0
  103. data/spec/csv_decision/examples_spec.rb +242 -0
  104. data/spec/csv_decision/index_spec.rb +58 -0
  105. data/spec/csv_decision/input_spec.rb +55 -0
  106. data/spec/csv_decision/load_spec.rb +28 -0
  107. data/spec/csv_decision/matchers/function_spec.rb +82 -0
  108. data/spec/csv_decision/matchers/guard_spec.rb +170 -0
  109. data/spec/csv_decision/matchers/numeric_spec.rb +47 -0
  110. data/spec/csv_decision/matchers/pattern_spec.rb +183 -0
  111. data/spec/csv_decision/matchers/range_spec.rb +70 -0
  112. data/spec/csv_decision/matchers/symbol_spec.rb +67 -0
  113. data/spec/csv_decision/options_spec.rb +94 -0
  114. data/spec/csv_decision/parse_spec.rb +44 -0
  115. data/spec/csv_decision/table_spec.rb +683 -0
  116. data/spec/csv_decision_spec.rb +7 -0
  117. data/spec/data/invalid/empty.csv +0 -0
  118. data/spec/data/invalid/invalid_header1.csv +4 -0
  119. data/spec/data/invalid/invalid_header2.csv +4 -0
  120. data/spec/data/invalid/invalid_header3.csv +4 -0
  121. data/spec/data/invalid/invalid_header4.csv +4 -0
  122. data/spec/data/valid/benchmark_regexp.csv +10 -0
  123. data/spec/data/valid/index_example.csv +13 -0
  124. data/spec/data/valid/multi_column_index.csv +10 -0
  125. data/spec/data/valid/multi_column_index2.csv +12 -0
  126. data/spec/data/valid/options_in_file1.csv +5 -0
  127. data/spec/data/valid/options_in_file2.csv +5 -0
  128. data/spec/data/valid/options_in_file3.csv +13 -0
  129. data/spec/data/valid/regular_expressions.csv +11 -0
  130. data/spec/data/valid/simple_constants.csv +5 -0
  131. data/spec/data/valid/simple_example.csv +10 -0
  132. data/spec/data/valid/valid.csv +4 -0
  133. data/spec/spec_helper.rb +106 -0
  134. metadata +352 -0
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017.
5
+ # @author Brett Vickers.
6
+ # See LICENSE and README.md for details.
7
+ module CSVDecision
8
+ # Match table data cells against a valid decision table expression or a simple constant.
9
+ # @api private
10
+ class Matchers
11
+ # Composite object for a data cell proc. Note that we do not need it to be comparable.
12
+ # Implemented as an immutable array of 2 or 3 entries for memory compactness and speed.
13
+ # @api private
14
+ class Proc < Array
15
+ # @param type [Symbol] Type of the function value - e.g., :constant or :guard.
16
+ # @param function [Object] Either a lambda function,
17
+ # or some kind of constant such as an Integer.
18
+ # @param symbols [nil, Symbol, Array<Symbol>] The symbol or list of symbols
19
+ # that the function uses to reference input hash keys (which are always symbolized).
20
+ def initialize(type:, function:, symbols: nil)
21
+ super()
22
+
23
+ self << type
24
+
25
+ # Function values should always be frozen
26
+ self << function.freeze
27
+
28
+ # Some function values, such as constants or 0-arity functions, do not reference symbols.
29
+ self << symbols if symbols
30
+
31
+ freeze
32
+ end
33
+
34
+ # @param hash [Hash] Input hash to function call.
35
+ # @param value [Object] Input value to function call.
36
+ # @return [Object] Value returned from function call.
37
+ def call(hash:, value: nil)
38
+ func = fetch(1)
39
+
40
+ return func.call(hash) if fetch(0) == :guard
41
+
42
+ # All other procs can take one or two args
43
+ func.arity == 1 ? func.call(value) : func.call(value, hash)
44
+ end
45
+
46
+ # @return [Symbol] Type of the function value - e.g., :constant or :guard.
47
+ def type
48
+ fetch(0)
49
+ end
50
+
51
+ # @return [Object] Either a lambda function, or some kind of constant such as an Integer.
52
+ def function
53
+ fetch(1)
54
+ end
55
+
56
+ # @return [nil, Symbol, Array<Symbol>] The symbol or list of symbols
57
+ # that the function uses to reference input hash keys (which are always symbolized).
58
+ def symbols
59
+ fetch(2, nil)
60
+ end
61
+ end
62
+
63
+ # Negation sign prefixed to ranges and functions.
64
+ NEGATE = '!'
65
+
66
+ # All regular expressions used for matching are anchored inside their own
67
+ # non-capturing group.
68
+ #
69
+ # @param value [String] String used to form an anchored regular expression.
70
+ # @return [Regexp] Anchored, frozen regular expression.
71
+ def self.regexp(value)
72
+ Regexp.new("\\A(?:#{value})\\z").freeze
73
+ end
74
+
75
+ # Symbols used for inequality
76
+ INEQUALITY = '!=|!'
77
+
78
+ # Match Regexp for inequality
79
+ INEQUALITY_RE = regexp(INEQUALITY)
80
+
81
+ # Equality, cell constants and functions specified by prefixing the value with
82
+ # one of these 3 symbols.
83
+ EQUALS = '==|:=|='
84
+
85
+ # Match Regexp for equality
86
+ EQUALS_RE = regexp(EQUALS)
87
+
88
+ # Method names are stricter than CSV column names.
89
+ METHOD_NAME_RE = /\A[_a-z][_a-z0-9]*[?!=]?\z/
90
+
91
+ # Normalize the operators which are a variation on equals/assignment.
92
+ #
93
+ # @param operator [String]
94
+ # @return [String]
95
+ def self.normalize_operator(operator)
96
+ EQUALS_RE.match?(operator) ? '==' : operator
97
+ end
98
+
99
+ # Regular expression used to recognise a numeric string with or without a decimal point.
100
+ NUMERIC = '[-+]?\d*(?<decimal>\.?)\d*'
101
+
102
+ NUMERIC_RE = regexp(NUMERIC)
103
+ private_constant :NUMERIC_RE
104
+
105
+ # Validate a numeric value and convert it to an Integer or BigDecimal if a valid numeric string.
106
+ #
107
+ # @param value [nil, String, Integer, BigDecimal]
108
+ # @return [nil, Integer, BigDecimal]
109
+ def self.numeric(value)
110
+ return value if value.is_a?(Integer) || value.is_a?(BigDecimal)
111
+ return unless value.is_a?(String)
112
+
113
+ to_numeric(value)
114
+ end
115
+
116
+ # Convert a numeric string into an Integer or BigDecimal, otherwise return nil.
117
+ #
118
+ # @param value [String]
119
+ # @return [nil, Integer, BigDecimal]
120
+ def self.to_numeric(value)
121
+ return unless (match = NUMERIC_RE.match(value))
122
+
123
+ return value.to_i if match['decimal'] == ''
124
+ BigDecimal(value.chomp('.'))
125
+ end
126
+
127
+ # Compare one object with another if they both respond to the compare method.
128
+ #
129
+ # @param lhs [Object]
130
+ # @param compare [Object]
131
+ # @param rhs [Object]
132
+ # @return [nil, Boolean]
133
+ def self.compare?(lhs:, compare:, rhs:)
134
+ # Is the rhs the same class or a superclass of lhs, and does rhs respond to the
135
+ # compare method?
136
+ return lhs.send(compare, rhs) if lhs.is_a?(rhs.class) && rhs.respond_to?(compare)
137
+
138
+ nil
139
+ end
140
+
141
+ # Parse the supplied input columns for the row supplied using an array of matchers.
142
+ #
143
+ # @param columns [Hash{Integer=>Columns::Entry}] Input columns hash.
144
+ # @param matchers [Array<Matchers::Matcher>]
145
+ # @param row [Array<String>] Data row being parsed.
146
+ # @return [Array<(Array, ScanRow)>] Used to scan a table row against an input hash for matches.
147
+ def self.parse(columns:, matchers:, row:)
148
+ # Build an array of column indexes requiring simple constant matches,
149
+ # and a second array of columns requiring special matchers.
150
+ scan_row = ScanRow.new
151
+
152
+ # Scan the columns in the data row, and build an object to scan this row against
153
+ # an input hash.
154
+ # Convert values in the data row if not just a simple constant.
155
+ row = scan_row.scan_columns(columns: columns, matchers: matchers, row: row)
156
+
157
+ [row, scan_row]
158
+ end
159
+
160
+ # @return [Array<Matchers::Matcher>] Matchers for the input columns.
161
+ attr_reader :ins
162
+
163
+ # @return [Array<Matchers::Matcher>] Matchers for the output columns.
164
+ attr_reader :outs
165
+
166
+ # @param options (see CSVDecision.parse)
167
+ def initialize(options)
168
+ matchers = options[:matchers].collect { |klass| klass.new(options) }
169
+ @ins = matchers.select(&:ins?)
170
+ @outs = matchers.select(&:outs?)
171
+ end
172
+
173
+ # Parse the row's input columns using the input matchers.
174
+ #
175
+ # @param columns (see Matchers.parse)
176
+ # @param row (see Matchers.parse)
177
+ # @return (see Matchers.parse)
178
+ def parse_ins(columns:, row:)
179
+ Matchers.parse(columns: columns, matchers: @ins, row: row)
180
+ end
181
+
182
+ # Parse the row's output columns using the output matchers.
183
+ #
184
+ # @param columns (see Matchers.parse)
185
+ # @param row (see Matchers.parse)
186
+ # @return (see Matchers.parse)
187
+ def parse_outs(columns:, row:)
188
+ Matchers.parse(columns: columns, matchers: @outs, row: row)
189
+ end
190
+
191
+ # Subclass and override {#matches?} to implement a custom Matcher class.
192
+ class Matcher
193
+ def initialize(_options = nil); end
194
+
195
+ # Determine if the input cell string is recognised by this Matcher.
196
+ #
197
+ # @param cell [String] Data row cell.
198
+ # @return [false, CSVDecision::Proc] Returns false if this cell is not a match; otherwise
199
+ # returns the +CSVDecision::Proc+ object indicating if this is a constant or some type of
200
+ # function.
201
+ def matches?(cell); end
202
+
203
+ # Does this matcher apply to output cells?
204
+ #
205
+ # @return [Boolean] Return true if this matcher applies to output cells,
206
+ # false otherwise.
207
+ def outs?
208
+ false
209
+ end
210
+
211
+ # Does this matcher apply to output cells?
212
+ #
213
+ # @return [Boolean] Return true if this matcher applies to input cells,
214
+ # false otherwise.
215
+ def ins?
216
+ true
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,124 @@
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
+ # Validate and normalize the options values supplied.
9
+ # @api private
10
+ module Options
11
+ # Specialized cell value matchers beyond simple string compares.
12
+ # By default all these matchers are tried in the specified order on all
13
+ # input data cells.
14
+ DEFAULT_MATCHERS = [
15
+ Matchers::Range,
16
+ Matchers::Numeric,
17
+ Matchers::Pattern,
18
+ Matchers::Constant,
19
+ Matchers::Symbol,
20
+ Matchers::Guard
21
+ ].freeze
22
+
23
+ # All valid CSVDecision::parse options with their default values.
24
+ VALID = {
25
+ first_match: true,
26
+ regexp_implicit: false,
27
+ text_only: false,
28
+ matchers: DEFAULT_MATCHERS
29
+ }.freeze
30
+ private_constant :VALID
31
+
32
+ # These options may appear in the CSV file before the header row.
33
+ # They get converted to a normalized option key value pair.
34
+ CSV_NAMES = {
35
+ first_match: [:first_match, true], accumulate: [:first_match, false],
36
+ regexp_implicit: [:regexp_implicit, true],
37
+ text_only: [:text_only, true], string_search: [:text_only, true]
38
+ }.freeze
39
+ private_constant :CSV_NAMES
40
+
41
+ # Validate options and supply default values for any options not explicitly set.
42
+ #
43
+ # @param options [Hash] Input options hash supplied by the user.
44
+ # @return [Hash] Options hash filled in with all required values, defaulted if necessary.
45
+ # @raise [CellValidationError] For invalid option keys.
46
+ def self.normalize(options)
47
+ validate(options)
48
+ default(options)
49
+ end
50
+
51
+ # Read any options supplied in the CSV file placed before the header row.
52
+ #
53
+ # @param rows [Array<Array<String>>] Table data rows.
54
+ # @param options [Hash] Input options hash built so far.
55
+ # @return [Hash] Options hash overridden with any values found in the CSV file.
56
+ def self.from_csv(rows:, options:)
57
+ row = rows.first
58
+ return options if row.nil?
59
+
60
+ # Have we hit the header row?
61
+ return options if Header.row?(row)
62
+
63
+ # Scan each cell looking for valid option values
64
+ options = scan_cells(row: row, options: options)
65
+
66
+ rows.shift
67
+ from_csv(rows: rows, options: options)
68
+ end
69
+
70
+ def self.scan_cells(row:, options:)
71
+ # Scan each cell looking for valid option values
72
+ row.each do |cell|
73
+ next if cell == ''
74
+
75
+ key, value = option?(cell)
76
+ options[key] = value if key
77
+ end
78
+
79
+ options
80
+ end
81
+ private_class_method :scan_cells
82
+
83
+ def self.default(options)
84
+ result = options.dup
85
+
86
+ # The user may override the list of matchers to be used
87
+ result[:matchers] = matchers(result)
88
+
89
+ # Supply any missing options with default values
90
+ VALID.each_pair do |key, value|
91
+ next if result.key?(key)
92
+ result[key] = value
93
+ end
94
+
95
+ result
96
+ end
97
+ private_class_method :default
98
+
99
+ def self.matchers(options)
100
+ return [] if options.key?(:matchers) && !options[:matchers]
101
+ return [] if options[:text_only]
102
+ return DEFAULT_MATCHERS unless options.key?(:matchers)
103
+
104
+ options[:matchers]
105
+ end
106
+ private_class_method :matchers
107
+
108
+ def self.option?(cell)
109
+ key = cell.strip.downcase.to_sym
110
+
111
+ CSV_NAMES[key]
112
+ end
113
+ private_class_method :option?
114
+
115
+ def self.validate(options)
116
+ invalid_options = options.keys - VALID.keys
117
+
118
+ return if invalid_options.empty?
119
+
120
+ raise CellValidationError, "invalid option(s) supplied: #{invalid_options.inspect}"
121
+ end
122
+ private_class_method :validate
123
+ end
124
+ end
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017.
5
+ # @author Brett Vickers.
6
+ # See LICENSE and README.md for details.
7
+ module CSVDecision
8
+ # All CSVDecision specific errors
9
+ class Error < StandardError; end
10
+
11
+ # Error validating a cell when parsing input table data.
12
+ class TableValidationError < Error; end
13
+
14
+ # Error validating a cell when parsing input table cell data.
15
+ class CellValidationError < Error; end
16
+
17
+ # Table parsing error message enhanced to include the file being processed.
18
+ class FileError < Error; end
19
+
20
+ # Builds a decision table from the input data - which may either be a file, CSV string
21
+ # or an array of arrays.
22
+ #
23
+ # @example Simple Example
24
+ # If you have cloned the gem's git repo, then you can run:
25
+ # table = CSVDecision.parse(Pathname('spec/data/valid/simple_example.csv'))
26
+ # #=> CSVDecision::Table
27
+ # table.decide(topic: 'finance', region: 'Europe') #=> team_member: 'Donald'
28
+ #
29
+ # @param data [Pathname, File, Array<Array<String>>, String] input data given as
30
+ # a CSV file, array of arrays or CSV string.
31
+ # @param options [Hash{Symbol=>Object}] Options hash controlling how the table is parsed and
32
+ # interpreted.
33
+ #
34
+ # @option options [Boolean] :first_match Stop scanning after finding the first row match.
35
+ # @option options [Boolean] :regexp_implicit Make regular expressions implicit rather than
36
+ # requiring the comparator =~. (Use with care.)
37
+ # @option options [Boolean] :text_only All cells treated as simple strings by turning off all
38
+ # special matchers.
39
+ # @option options [Array<Matchers::Matcher>] :matchers May be used to control the inclusion and
40
+ # ordering of special matchers. (Advanced feature, use with care.)
41
+ #
42
+ # @return [CSVDecision::Table] Resulting decision table.
43
+ #
44
+ # @raise [CSVDecision::CellValidationError] Table parsing cell validation error.
45
+ # @raise [CSVDecision::FileError] Table parsing error for a named CSV file.
46
+ #
47
+ def self.parse(data, options = {})
48
+ Parse.table(data: data, options: Options.normalize(options))
49
+ end
50
+
51
+ # Methods to parse the decision table and return CSVDecision::Table object.
52
+ # @api private
53
+ module Parse
54
+ # Parse the CSV file or input data and create a new decision table object.
55
+ #
56
+ # @param (see CSVDecision.parse)
57
+ # @return (see CSVDecision.parse)
58
+ def self.table(data:, options:)
59
+ table = CSVDecision::Table.new
60
+
61
+ # In most cases the decision table will be loaded from a CSV file.
62
+ table.file = data if Data.input_file?(data)
63
+
64
+ parse_table(table: table, input: data, options: options)
65
+
66
+ # The table object is now immutable.
67
+ table.columns.freeze
68
+ table.freeze
69
+ rescue CSVDecision::Error => exp
70
+ raise_error(file: table.file, exception: exp)
71
+ end
72
+
73
+ def self.raise_error(file:, exception:)
74
+ raise exception unless file
75
+
76
+ raise CSVDecision::FileError,
77
+ "error processing CSV file #{file}\n#{exception.inspect}"
78
+ end
79
+ private_class_method :raise_error
80
+
81
+ def self.parse_table(table:, input:, options:)
82
+ # Parse input data into an array of arrays.
83
+ table.rows = Data.to_array(data: input)
84
+
85
+ # Pick up any options specified in the CSV file before the header row.
86
+ # These override any options passed as parameters to the parse method.
87
+ table.options = Options.from_csv(rows: table.rows, options: options).freeze
88
+
89
+ # Parse table header and data rows with special cell matchers.
90
+ parse_with_matchers(table: table, matchers: CSVDecision::Matchers.new(options))
91
+
92
+ # Build the data index if one is indicated
93
+ Index.build(table: table)
94
+
95
+ # Build a paths index if one is indicated
96
+ Paths.scan(table: table)
97
+ end
98
+ private_class_method :parse_table
99
+
100
+ def self.parse_with_matchers(table:, matchers:)
101
+ # Parse the header row
102
+ table.columns = Header.parse(table: table, matchers: matchers)
103
+
104
+ # Parse the table's the data rows.
105
+ parse_data(table: table, matchers: matchers)
106
+ end
107
+ private_class_method :parse_with_matchers
108
+
109
+ def self.parse_data(table:, matchers:)
110
+ table.rows.each_with_index do |row, index|
111
+ # Mutate the row if we find anything other than a simple string constant in its
112
+ # data cells.
113
+ row = parse_row(table: table, matchers: matchers, row: row, index: index)
114
+
115
+ # Does the row have any output functions?
116
+ outs_functions(table: table, index: index)
117
+
118
+ # No more mutations required for this row.
119
+ row.freeze
120
+ end
121
+ end
122
+ private_class_method :parse_data
123
+
124
+ def self.parse_row(table:, matchers:, row:, index:)
125
+ # Parse the input cells for this row
126
+ row = parse_row_ins(table: table, matchers: matchers, row: row, index: index)
127
+
128
+ # Parse the output cells for this row
129
+ parse_row_outs(table: table, matchers: matchers, row: row, index: index)
130
+ end
131
+ private_class_method :parse_row
132
+
133
+ def self.parse_row_ins(table:, matchers:, row:, index:)
134
+ # Parse the input cells for this row
135
+ row, table.scan_rows[index] = matchers.parse_ins(columns: table.columns.ins, row: row)
136
+
137
+ # Add any symbol references made by input cell procs to the column dictionary
138
+ Columns.ins_dictionary(columns: table.columns.dictionary, row: row)
139
+
140
+ row
141
+ end
142
+ private_class_method :parse_row_ins
143
+
144
+ def self.parse_row_outs(table:, matchers:, row:, index:)
145
+ # Parse the output cells for this row
146
+ row, table.outs_rows[index] = matchers.parse_outs(columns: table.columns.outs, row: row)
147
+
148
+ Columns.outs_dictionary(columns: table.columns, row: row)
149
+
150
+ row
151
+ end
152
+ private_class_method :parse_row_outs
153
+
154
+ def self.outs_functions(table:, index:)
155
+ return if table.outs_rows[index].procs.empty?
156
+
157
+ # Set this flag as the table has output functions
158
+ table.outs_functions = true
159
+
160
+ # Update the output columns that contain functions needing evaluation.
161
+ table.outs_rows[index].procs.each { |col| table.columns.outs[col].eval = true }
162
+ end
163
+ private_class_method :outs_functions
164
+ end
165
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ # CSV Decision: CSV based Ruby decision tables.
4
+ # Created December 2017.
5
+ # @author Brett Vickers.
6
+ # See LICENSE and README.md for details.
7
+ module CSVDecision
8
+ # Build an index for a decision table with one or more input columns
9
+ # designated as keys
10
+ # @api private
11
+ class Paths
12
+ # Build the index of paths
13
+ #
14
+ # @param table [CSVDecision::Table] Decision table being indexed.
15
+ # @return [CSVDecision::Paths] The built index of paths.
16
+ def self.scan(table:)
17
+ # Do we even have paths?
18
+ columns = table.columns.paths.keys
19
+ return [] if columns.empty?
20
+
21
+ table.paths = Paths.new(table: table, columns: columns).paths
22
+ end
23
+
24
+ # @param current_value [Integer, Array] Current path value.
25
+ # @param index [Integer] Array row index to be included in the path entry.
26
+ # @return [Integer, Array] New path key value.
27
+ def self.value(current_value, index)
28
+ return [current_value, index] if current_value.is_a?(Integer)
29
+
30
+ current_value[-1] = index
31
+ current_value
32
+ end
33
+
34
+ # @param value [String] Cell value for the path: column.
35
+ # @return [nil, Symbol] Non-empty string converted to a symbol.
36
+ def self.symbol(value)
37
+ value.blank? ? nil : value.to_sym
38
+ end
39
+
40
+ # @return [Hash] The index hash mapping in input values to one or more data array row indexes.
41
+ attr_reader :paths
42
+
43
+ # @param table [CSVDecision::Table] Decision table.
44
+ # @param columns [Array<Index>] Array of column indexes to be indexed.
45
+ def initialize(table:, columns:)
46
+ @paths = []
47
+ @columns = columns
48
+
49
+ build(table)
50
+
51
+ freeze
52
+ end
53
+
54
+ private
55
+
56
+ def build(table)
57
+ last_path = nil
58
+ key = -1
59
+ rows = nil
60
+ table.each do |row, index|
61
+ path = build_path(row: row)
62
+ if path == last_path
63
+ rows = Paths.value(rows, index)
64
+ else
65
+ rows = index
66
+ key += 1
67
+ last_path = path
68
+ end
69
+
70
+ @paths[key] = [path, rows]
71
+ end
72
+ end
73
+
74
+ def build_path(row:)
75
+ @columns.map { |col| Paths.symbol(row[col]) }.compact
76
+ end
77
+ end
78
+ end