csv_decision2 0.5.1

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