csv_decision 0.2.0 → 0.3.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -1
  3. data/README.md +11 -8
  4. data/benchmarks/rufus_decision.rb +9 -1
  5. data/csv_decision.gemspec +1 -1
  6. data/doc/CSVDecision/CellValidationError.html +1 -1
  7. data/doc/CSVDecision/Columns/Dictionary.html +29 -29
  8. data/doc/CSVDecision/Columns.html +394 -47
  9. data/doc/CSVDecision/Data.html +2 -2
  10. data/doc/CSVDecision/Decide.html +23 -159
  11. data/doc/CSVDecision/Decision.html +370 -32
  12. data/doc/CSVDecision/Defaults.html +1 -1
  13. data/doc/CSVDecision/Dictionary/Entry.html +157 -55
  14. data/doc/CSVDecision/Dictionary.html +37 -21
  15. data/doc/CSVDecision/Error.html +1 -1
  16. data/doc/CSVDecision/FileError.html +1 -1
  17. data/doc/CSVDecision/Header.html +142 -1
  18. data/doc/CSVDecision/Index.html +741 -0
  19. data/doc/CSVDecision/Input.html +14 -61
  20. data/doc/CSVDecision/Load.html +1 -1
  21. data/doc/CSVDecision/Matchers/Constant.html +1 -1
  22. data/doc/CSVDecision/Matchers/Function.html +1 -1
  23. data/doc/CSVDecision/Matchers/Guard.html +13 -147
  24. data/doc/CSVDecision/Matchers/Matcher.html +13 -13
  25. data/doc/CSVDecision/Matchers/Numeric.html +1 -1
  26. data/doc/CSVDecision/Matchers/Pattern.html +1 -1
  27. data/doc/CSVDecision/Matchers/Proc.html +147 -14
  28. data/doc/CSVDecision/Matchers/Range.html +1 -1
  29. data/doc/CSVDecision/Matchers/Symbol.html +1 -1
  30. data/doc/CSVDecision/Matchers.html +55 -162
  31. data/doc/CSVDecision/Options.html +21 -21
  32. data/doc/CSVDecision/Parse.html +2 -180
  33. data/doc/CSVDecision/Result.html +220 -38
  34. data/doc/CSVDecision/ScanRow.html +69 -325
  35. data/doc/CSVDecision/Table.html +128 -40
  36. data/doc/CSVDecision/TableValidationError.html +1 -1
  37. data/doc/CSVDecision/Validate.html +1 -1
  38. data/doc/CSVDecision.html +4 -4
  39. data/doc/_index.html +8 -8
  40. data/doc/class_list.html +1 -1
  41. data/doc/file.README.html +13 -11
  42. data/doc/index.html +13 -11
  43. data/doc/method_list.html +206 -150
  44. data/doc/top-level-namespace.html +1 -1
  45. data/lib/csv_decision/columns.rb +87 -1
  46. data/lib/csv_decision/decision.rb +54 -29
  47. data/lib/csv_decision/defaults.rb +1 -1
  48. data/lib/csv_decision/dictionary.rb +32 -22
  49. data/lib/csv_decision/header.rb +17 -0
  50. data/lib/csv_decision/index.rb +107 -0
  51. data/lib/csv_decision/input.rb +45 -13
  52. data/lib/csv_decision/matchers/guard.rb +2 -0
  53. data/lib/csv_decision/matchers.rb +14 -8
  54. data/lib/csv_decision/options.rb +7 -19
  55. data/lib/csv_decision/parse.rb +12 -96
  56. data/lib/csv_decision/result.rb +10 -9
  57. data/lib/csv_decision/scan_row.rb +20 -44
  58. data/lib/csv_decision/table.rb +7 -4
  59. data/lib/csv_decision.rb +1 -1
  60. data/spec/csv_decision/columns_spec.rb +6 -6
  61. data/spec/csv_decision/data_spec.rb +0 -5
  62. data/spec/csv_decision/index_spec.rb +58 -0
  63. data/spec/csv_decision/input_spec.rb +7 -2
  64. data/spec/csv_decision/options_spec.rb +16 -1
  65. data/spec/csv_decision/parse_spec.rb +4 -5
  66. data/spec/csv_decision/table_spec.rb +70 -0
  67. data/spec/data/{valid → invalid}/empty.csv +0 -0
  68. data/spec/data/valid/index_example.csv +12 -0
  69. data/spec/data/valid/multi_column_index.csv +10 -0
  70. data/spec/data/valid/multi_column_index2.csv +12 -0
  71. data/spec/data/valid/options_in_file3.csv +13 -0
  72. metadata +16 -5
  73. data/lib/csv_decision/decide.rb +0 -45
@@ -31,6 +31,18 @@ module CSVDecision
31
31
  freeze
32
32
  end
33
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
+
34
46
  # @return [Symbol] Type of the function value - e.g., :constant or :guard.
35
47
  def type
36
48
  fetch(0)
@@ -80,18 +92,12 @@ module CSVDecision
80
92
  NUMERIC_RE = regexp(NUMERIC)
81
93
  private_constant :NUMERIC_RE
82
94
 
83
- # @param value [Object] Value from the input hash.
84
- # @return [Boolean] Return true if value is an Integer or a BigDecimal, false otherwise.
85
- def self.numeric?(value)
86
- value.is_a?(Integer) || value.is_a?(BigDecimal)
87
- end
88
-
89
95
  # Validate a numeric value and convert it to an Integer or BigDecimal if a valid numeric string.
90
96
  #
91
97
  # @param value [nil, String, Integer, BigDecimal]
92
98
  # @return [nil, Integer, BigDecimal]
93
99
  def self.numeric(value)
94
- return value if numeric?(value)
100
+ return value if value.is_a?(Integer) || value.is_a?(BigDecimal)
95
101
  return unless value.is_a?(String)
96
102
 
97
103
  to_numeric(value)
@@ -124,7 +130,7 @@ module CSVDecision
124
130
  # Convert values in the data row if not just a simple constant.
125
131
  row = scan_row.scan_columns(columns: columns, matchers: matchers, row: row)
126
132
 
127
- [row, scan_row.freeze]
133
+ [row, scan_row]
128
134
  end
129
135
 
130
136
  # @return [Array<Matchers::Matcher>] Matchers for the input columns.
@@ -5,18 +5,6 @@
5
5
  # @author Brett Vickers <brett@phillips-vickers.com>
6
6
  # See LICENSE and README.md for details.
7
7
  module CSVDecision
8
- # # Specialized cell value matchers beyond simple string compares.
9
- # # By default all these matchers are tried in the specified order on all
10
- # # input data cells.
11
- # DEFAULT_MATCHERS = [
12
- # Matchers::Range,
13
- # Matchers::Numeric,
14
- # Matchers::Pattern,
15
- # Matchers::Constant,
16
- # Matchers::Symbol,
17
- # Matchers::Guard
18
- # ].freeze
19
-
20
8
  # Validate and normalize the options values supplied.
21
9
  # @api private
22
10
  module Options
@@ -44,10 +32,9 @@ module CSVDecision
44
32
  # These options may appear in the CSV file before the header row.
45
33
  # They get converted to a normalized option key value pair.
46
34
  CSV_NAMES = {
47
- first_match: [:first_match, true],
48
- accumulate: [:first_match, false],
35
+ first_match: [:first_match, true], accumulate: [:first_match, false],
49
36
  regexp_implicit: [:regexp_implicit, true],
50
- text_only: [:text_only, true]
37
+ text_only: [:text_only, true], string_search: [:text_only, true]
51
38
  }.freeze
52
39
  private_constant :CSV_NAMES
53
40
 
@@ -55,7 +42,7 @@ module CSVDecision
55
42
  #
56
43
  # @param options [Hash] Input options hash supplied by the user.
57
44
  # @return [Hash] Options hash filled in with all required values, defaulted if necessary.
58
- # @raise [ArgumentError] For invalid option keys.
45
+ # @raise [CellValidationError] For invalid option keys.
59
46
  def self.normalize(options)
60
47
  validate(options)
61
48
  default(options)
@@ -119,8 +106,9 @@ module CSVDecision
119
106
  private_class_method :matchers
120
107
 
121
108
  def self.option?(cell)
122
- key = cell.downcase.to_sym
123
- return CSV_NAMES[key] if CSV_NAMES.key?(key)
109
+ key = cell.strip.downcase.to_sym
110
+
111
+ CSV_NAMES[key]
124
112
  end
125
113
  private_class_method :option?
126
114
 
@@ -129,7 +117,7 @@ module CSVDecision
129
117
 
130
118
  return if invalid_options.empty?
131
119
 
132
- raise ArgumentError, "invalid option(s) supplied: #{invalid_options.inspect}"
120
+ raise CellValidationError, "invalid option(s) supplied: #{invalid_options.inspect}"
133
121
  end
134
122
  private_class_method :validate
135
123
  end
@@ -78,37 +78,29 @@ module CSVDecision
78
78
  private_class_method :raise_error
79
79
 
80
80
  def self.parse_table(table:, input:, options:)
81
- # Parse input data into an array of arrays
81
+ # Parse input data into an array of arrays.
82
82
  table.rows = Data.to_array(data: input)
83
83
 
84
84
  # Pick up any options specified in the CSV file before the header row.
85
85
  # These override any options passed as parameters to the parse method.
86
86
  table.options = Options.from_csv(rows: table.rows, options: options).freeze
87
87
 
88
- # Matchers
89
- matchers = CSVDecision::Matchers.new(options)
88
+ # Parse table header and data rows with special cell matchers.
89
+ parse_with_matchers(table: table, matchers: CSVDecision::Matchers.new(options))
90
90
 
91
- # Parse the header row
92
- table.columns = parse_header(table: table, matchers: matchers)
93
-
94
- # Parse the table's the data rows.
95
- parse_data(table: table, matchers: matchers)
91
+ # Build the index if one is indicated
92
+ Index.build(table: table)
96
93
  end
97
94
  private_class_method :parse_table
98
95
 
99
- def self.parse_header(table:, matchers:)
96
+ def self.parse_with_matchers(table:, matchers:)
100
97
  # Parse the header row
101
- table.columns = CSVDecision::Columns.new(table)
102
-
103
- # Parse the defaults row if present
104
- return table.columns if table.columns.defaults.blank?
98
+ table.columns = Header.parse(table: table, matchers: matchers)
105
99
 
106
- table.columns.defaults =
107
- Defaults.parse(columns: table.columns, matchers: matchers.outs, row: table.rows.shift)
108
-
109
- table.columns
100
+ # Parse the table's the data rows.
101
+ parse_data(table: table, matchers: matchers)
110
102
  end
111
- private_class_method :parse_header
103
+ private_class_method :parse_with_matchers
112
104
 
113
105
  def self.parse_data(table:, matchers:)
114
106
  table.rows.each_with_index do |row, index|
@@ -139,7 +131,7 @@ module CSVDecision
139
131
  row, table.scan_rows[index] = matchers.parse_ins(columns: table.columns.ins, row: row)
140
132
 
141
133
  # Add any symbol references made by input cell procs to the column dictionary
142
- ins_column_dictionary(columns: table.columns.dictionary, row: row)
134
+ Columns.ins_dictionary(columns: table.columns.dictionary, row: row)
143
135
 
144
136
  row
145
137
  end
@@ -149,88 +141,12 @@ module CSVDecision
149
141
  # Parse the output cells for this row
150
142
  row, table.outs_rows[index] = matchers.parse_outs(columns: table.columns.outs, row: row)
151
143
 
152
- outs_column_dictionary(columns: table.columns, row: row)
144
+ Columns.outs_dictionary(columns: table.columns, row: row)
153
145
 
154
146
  row
155
147
  end
156
148
  private_class_method :parse_row_outs
157
149
 
158
- def self.outs_column_dictionary(columns:, row:)
159
- row.each_with_index do |cell, index|
160
- outs_check_cell(columns: columns, cell: cell, index: index)
161
- end
162
- end
163
- private_class_method :outs_column_dictionary
164
-
165
- def self.outs_check_cell(columns:, cell:, index:)
166
- return unless cell.is_a?(Matchers::Proc)
167
- return if cell.symbols.nil?
168
-
169
- check_outs_symbols(columns: columns, cell: cell, index: index)
170
- end
171
- private_class_method :outs_check_cell
172
-
173
- def self.check_outs_symbols(columns:, cell:, index:)
174
- Array(cell.symbols).each do |symbol|
175
- check_outs_symbol(columns: columns, symbol: symbol, index: index)
176
- end
177
- end
178
- private_class_method :check_outs_symbols
179
-
180
- def self.check_outs_symbol(columns:, symbol:, index:)
181
- in_out = columns.dictionary[symbol]
182
-
183
- # If its an input column symbol then we're good.
184
- return if ins_symbol?(columns: columns, symbol: symbol, in_out: in_out)
185
-
186
- # Check if this output symbol reference is on or after this cell's column
187
- invalid_out_ref?(columns, index, in_out)
188
- end
189
- private_class_method :check_outs_symbol
190
-
191
- # If the symbol exists either as an input or does not exist then we're good.
192
- def self.ins_symbol?(columns:, symbol:, in_out:)
193
- return true if in_out == :in
194
-
195
- # It must an input symbol, as all the output symbols have been parsed.
196
- return columns.dictionary[symbol] = :in if in_out.nil?
197
-
198
- false
199
- end
200
- private_class_method :ins_symbol?
201
-
202
- def self.invalid_out_ref?(columns, index, in_out)
203
- return false if in_out < index
204
-
205
- that_column = if in_out == index
206
- 'reference to itself'
207
- else
208
- "an out of order reference to output column '#{columns.outs[in_out].name}'"
209
- end
210
- raise CellValidationError,
211
- "output column '#{columns.outs[index].name}' makes #{that_column}"
212
- end
213
-
214
- def self.ins_column_dictionary(columns:, row:)
215
- row.each { |cell| ins_cell_dictionary(columns: columns, cell: cell) }
216
- end
217
- private_class_method :ins_column_dictionary
218
-
219
- def self.ins_cell_dictionary(columns:, cell:)
220
- return unless cell.is_a?(Matchers::Proc)
221
- return if cell.symbols.nil?
222
-
223
- add_ins_symbols(columns: columns, cell: cell)
224
- end
225
- # private_class_method :ins_cell_dictionary
226
-
227
- def self.add_ins_symbols(columns:, cell:)
228
- Array(cell.symbols).each do |symbol|
229
- Dictionary.add_name(columns: columns, name: symbol)
230
- end
231
- end
232
- private_class_method :add_ins_symbols
233
-
234
150
  def self.outs_functions(table:, index:)
235
151
  return if table.outs_rows[index].procs.empty?
236
152
 
@@ -12,12 +12,19 @@ module CSVDecision
12
12
  # both result values and if: columns, which eventually get evaluated and removed.
13
13
  attr_reader :attributes
14
14
 
15
+ # @return [Hash{Index=>Dictionary::Entry}] Output columns.
16
+ attr_reader :outs
17
+
18
+ # @return [nil, true] Set to true if the table has output functions.
19
+ attr_reader :outs_functions
20
+
15
21
  # @return [Boolean] Returns true if this is a multi-row result
16
22
  attr_reader :multi_result
17
23
 
18
24
  # (see Decision.initialize)
19
25
  def initialize(table:, input:)
20
26
  @outs = table.columns.outs
27
+ @outs_functions = table.outs_functions
21
28
  @if_columns = table.columns.ifs
22
29
 
23
30
  # Partial result always copies in the input hash for calculating output functions.
@@ -25,14 +32,10 @@ module CSVDecision
25
32
  # have the same symbol as an input hash key.
26
33
  # However, the rest of this hash is mutated as output column evaluation results
27
34
  # are accumulated.
28
- @partial_result = input[:hash]&.slice(*table.columns.input_keys) if table.outs_functions
35
+ @partial_result = input&.slice(*table.columns.input_keys) if @outs_functions
29
36
 
30
37
  # Attributes hash contains the output decision key value pairs
31
38
  @attributes = {}
32
-
33
- # Set to true if the result has more than one row.
34
- # Only possible for the first_match: false option.
35
- @multi_result = false
36
39
  end
37
40
 
38
41
  # Common case for building a single row result is just copying output column values to the
@@ -156,10 +159,8 @@ module CSVDecision
156
159
  proc = row[col]
157
160
  next unless proc.is_a?(Matchers::Proc)
158
161
 
159
- value = proc.function[@partial_result]
160
-
161
- @partial_result[column.name] = value
162
- @attributes[column.name] = value
162
+ @attributes[column.name] = proc.function[@partial_result]
163
+ @partial_result[column.name] = @attributes[column.name]
163
164
  end
164
165
  end
165
166
 
@@ -14,6 +14,7 @@ module CSVDecision
14
14
 
15
15
  # Scan the table cell against all matches.
16
16
  #
17
+ # @param column [Dictionary::Entry] Column dictionary entry.
17
18
  # @param matchers [Array<Matchers::Matcher>]
18
19
  # @param cell [String]
19
20
  # @return [false, Matchers::Proc]
@@ -27,21 +28,6 @@ module CSVDecision
27
28
  invalid_constant?(type: :constant, column: column)
28
29
  end
29
30
 
30
- # Evaluate the cell proc against the column's input value and/or input hash.
31
- #
32
- # @param proc [CSVDecision::Proc] Proc in the table cell.
33
- # @param value [Object] Value supplied in the input hash corresponding to this column.
34
- # @param hash [{Symbol=>Object}] Input hash with symbolized keys.
35
- def self.eval_matcher(proc:, hash:, value: nil)
36
- function = proc.function
37
-
38
- # A symbol guard expression just needs to be passed the input hash
39
- return function[hash] if proc.type == :guard
40
-
41
- # All other procs can take one or two args
42
- function.arity == 1 ? function[value] : function[value, hash]
43
- end
44
-
45
31
  def self.scan_matchers(column:, matchers:, cell:)
46
32
  matchers.each do |matcher|
47
33
  # Guard function only accepts the same matchers as an output column.
@@ -78,7 +64,7 @@ module CSVDecision
78
64
  private_class_method :invalid_constant?
79
65
 
80
66
  # @return [Array<Integer>] Column indices for simple constants.
81
- attr_reader :constants
67
+ attr_accessor :constants
82
68
 
83
69
  # @return [Array<Integer>] Column indices for Proc objects.
84
70
  attr_reader :procs
@@ -98,46 +84,35 @@ module CSVDecision
98
84
  # non-string constant.
99
85
  def scan_columns(row:, columns:, matchers:)
100
86
  columns.each_pair do |col, column|
101
- # An empty input cell matches everything, and so never needs to be scanned
102
- next if (cell = row[col]) == '' && column.ins?
87
+ cell = row[col]
88
+
89
+ # An empty input cell matches everything, and so never needs to be scanned,
90
+ # but it cannot be indexed either.
91
+ next column.indexed = false if cell == '' && column.ins?
103
92
 
104
- # If the column is text only then no special matchers need be invoked
93
+ # If the column is text only then no special matchers need be used.
105
94
  next @constants << col if column.eval == false
106
95
 
107
96
  # Need to scan the cell against all matchers, and possibly overwrite
108
- # the cell contents with a Matchers::Proc.
97
+ # the cell contents with a Matchers::Proc value.
109
98
  row[col] = scan_cell(column: column, col: col, matchers: matchers, cell: cell)
110
99
  end
111
100
 
112
101
  row
113
102
  end
114
103
 
115
- # Match cells containing simple constants.
104
+ # Match cells in the input hash against a decision table row.
116
105
  # @param row (see ScanRow.scan_columns)
117
- # @param scan_cols [Hash{Integer=>Object}]
106
+ # @param hash (see Decision#row_scan)
118
107
  # @return [Boolean] True for a match, false otherwise.
119
- def match_constants?(row:, scan_cols:)
120
- constants.each do |col|
121
- return false unless row[col] == scan_cols[col]
122
- end
108
+ def match?(row:, scan_cols:, hash:)
109
+ # Check any table row cell constants first, and maybe fail fast...
110
+ return false if @constants.any? { |col| row[col] != scan_cols[col] }
123
111
 
124
- true
125
- end
126
-
127
- # Match cells containing a Proc object.
128
- # @param row (see ScanRow.scan_columns)
129
- # @param input [Hash{Symbol => Hash{Symbol=>Object}, Hash{Integer=>Object}}]
130
- # @return [Boolean] True for a match, false otherwise.
131
- def match_procs?(row:, input:)
132
- hash = input[:hash]
133
- scan_cols = input[:scan_cols]
134
-
135
- procs.each do |col|
136
- match = ScanRow.eval_matcher(proc: row[col], value: scan_cols[col], hash: hash)
137
- return false unless match
138
- end
112
+ return true if @procs.empty?
139
113
 
140
- true
114
+ # These table row cells are Proc objects which need evaluating
115
+ @procs.all? { |col| row[col].call(value: scan_cols[col], hash: hash) }
141
116
  end
142
117
 
143
118
  private
@@ -146,14 +121,14 @@ module CSVDecision
146
121
  # Scan the cell against all the matchers
147
122
  proc = ScanRow.scan(column: column, matchers: matchers, cell: cell)
148
123
 
149
- return set(proc, col) if proc
124
+ return set(proc, col, column) if proc
150
125
 
151
126
  # Just a plain constant
152
127
  @constants << col
153
128
  cell
154
129
  end
155
130
 
156
- def set(proc, col)
131
+ def set(proc, col, column)
157
132
  # Unbox a constant
158
133
  if proc.type == :constant
159
134
  @constants << col
@@ -161,6 +136,7 @@ module CSVDecision
161
136
  end
162
137
 
163
138
  @procs << col
139
+ column.indexed = false
164
140
  proc
165
141
  end
166
142
  end
@@ -13,7 +13,7 @@ module CSVDecision
13
13
  # @param input [Hash] Input hash.
14
14
  # @return [{Symbol => Object, Array<Object>}] Decision hash.
15
15
  def decide(input)
16
- Decide.decide(table: self, input: input, symbolize_keys: true)
16
+ Decision.make(table: self, input: input, symbolize_keys: true)
17
17
  end
18
18
 
19
19
  # Unsafe version of decide - may mutate the input hash and assumes the input
@@ -24,7 +24,7 @@ module CSVDecision
24
24
  # Input hash will be mutated by any functions that have side effects.
25
25
  # @return (see #decide)
26
26
  def decide!(input)
27
- Decide.decide(table: self, input: input, symbolize_keys: false)
27
+ Decision.make(table: self, input: input, symbolize_keys: false)
28
28
  end
29
29
 
30
30
  # @return [CSVDecision::Columns] Dictionary of all input and output columns.
@@ -33,6 +33,9 @@ module CSVDecision
33
33
  # @return [File, Pathname, nil] File path name if decision table was loaded from a CSV file.
34
34
  attr_accessor :file
35
35
 
36
+ # @return [CSVDecision::Index] The index built on one or more input columns.
37
+ attr_accessor :index
38
+
36
39
  # @return [Hash] All options, explicitly set or defaulted, used to parse the table.
37
40
  attr_accessor :options
38
41
 
@@ -65,7 +68,7 @@ module CSVDecision
65
68
  # @api private
66
69
  def each(first = 0, last = @rows.count - 1)
67
70
  index = first
68
- while index <= (last || first)
71
+ while index <= last
69
72
  yield(@rows[index], index)
70
73
 
71
74
  index += 1
@@ -76,13 +79,13 @@ module CSVDecision
76
79
  def initialize
77
80
  @columns = nil
78
81
  @file = nil
82
+ @index = nil
79
83
  @options = nil
80
84
  @outs_functions = nil
81
85
  @outs_rows = []
82
86
  @if_rows = []
83
87
  @rows = []
84
88
  @scan_rows = []
85
- # @tables = nil
86
89
  end
87
90
  end
88
91
  end
data/lib/csv_decision.rb CHANGED
@@ -15,11 +15,11 @@ module CSVDecision
15
15
 
16
16
  autoload :Columns, 'csv_decision/columns'
17
17
  autoload :Data, 'csv_decision/data'
18
- autoload :Decide, 'csv_decision/decide'
19
18
  autoload :Decision, 'csv_decision/decision'
20
19
  autoload :Defaults, 'csv_decision/defaults'
21
20
  autoload :Dictionary, 'csv_decision/dictionary'
22
21
  autoload :Header, 'csv_decision/header'
22
+ autoload :Index, 'csv_decision/index'
23
23
  autoload :Input, 'csv_decision/input'
24
24
  autoload :Load, 'csv_decision/load'
25
25
  autoload :Matchers, 'csv_decision/matchers'
@@ -7,12 +7,12 @@ SPEC_DATA_INVALID ||= File.join(CSVDecision.root, 'spec', 'data', 'invalid')
7
7
 
8
8
  describe CSVDecision::Columns do
9
9
  describe '#new' do
10
- it 'creates a columns object' do
11
- table = CSVDecision::Table.new
12
- columns = CSVDecision::Columns.new(table)
13
-
14
- expect(columns).to be_a(CSVDecision::Columns)
15
- end
10
+ # it 'creates a columns object' do
11
+ # table = CSVDecision::Table.new
12
+ # columns = CSVDecision::Columns.new(table)
13
+ #
14
+ # expect(columns).to be_a(CSVDecision::Columns)
15
+ # end
16
16
  end
17
17
 
18
18
  it 'rejects a duplicate output column name' do
@@ -22,11 +22,6 @@ describe CSVDecision::Data do
22
22
  end
23
23
 
24
24
  it 'parses a CSV file' do
25
- file = File.new(File.join(CSVDecision.root, 'spec/data/valid', 'empty.csv'))
26
- result = CSVDecision::Data.to_array(data: file)
27
- expect(result).to be_a Array
28
- expect(result.empty?).to eq true
29
-
30
25
  file = Pathname(File.join(CSVDecision.root, 'spec/data/valid', 'valid.csv'))
31
26
  result = CSVDecision::Data.to_array(data: file)
32
27
  expected = [
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/csv_decision'
4
+
5
+ SPEC_DATA_VALID ||= File.join(CSVDecision.root, 'spec', 'data', 'valid')
6
+ SPEC_DATA_INVALID ||= File.join(CSVDecision.root, 'spec', 'data', 'invalid')
7
+
8
+ describe CSVDecision::Index do
9
+ it 'indexes a single column CSV' do
10
+ file = Pathname(File.join(SPEC_DATA_VALID, 'options_in_file3.csv'))
11
+ result = CSVDecision.parse(file)
12
+
13
+ expected = {
14
+ 'none' => 0,
15
+ 'one' => 1,
16
+ 'two' => 2,
17
+ 'three' => 3,
18
+ nil => 4,
19
+ 0 => 5,
20
+ 1 => 6,
21
+ 2 => 7,
22
+ 3 => 8
23
+ }
24
+
25
+ expect(result.index.columns).to eq [0]
26
+ expect(result.index.hash).to eql expected
27
+ end
28
+
29
+ it 'indexes two columns with contiguous values' do
30
+ file = Pathname(File.join(SPEC_DATA_VALID, 'multi_column_index.csv'))
31
+ result = CSVDecision.parse(file)
32
+
33
+ expected = {
34
+ %w[integer none] => [[0, 1]],
35
+ %w[integer one] => [[2, 3]],
36
+ %w[string none] => [[4, 5]],
37
+ %w[string one] => [[6, 7]]
38
+ }
39
+
40
+ expect(result.index.columns).to eq [1, 2]
41
+ expect(result.index.hash).to eql expected
42
+ end
43
+
44
+ it 'indexes two columns with non-contiguous values' do
45
+ file = Pathname(File.join(SPEC_DATA_VALID, 'multi_column_index2.csv'))
46
+ result = CSVDecision.parse(file)
47
+
48
+ expected = {
49
+ %w[integer none] => [0, 8],
50
+ %w[string none] => [[1, 2]],
51
+ %w[string one] => [3, [6, 7]],
52
+ %w[integer one] => [[4, 5]]
53
+ }
54
+
55
+ expect(result.index.columns).to eq [1, 2]
56
+ expect(result.index.hash).to eql expected
57
+ end
58
+ end
@@ -22,7 +22,8 @@ describe CSVDecision::Input do
22
22
  input = { 'input' => 'input0', input1: 'input1' }
23
23
  expected = {
24
24
  hash: { input: 'input0', input1: 'input1' },
25
- scan_cols: { 0 => 'input0', 2 => 'input1'}
25
+ scan_cols: { 0 => 'input0', 2 => 'input1'},
26
+ key: 'input0'
26
27
  }
27
28
 
28
29
  result = CSVDecision::Input.parse(table: table, input: input, symbolize_keys: true)
@@ -41,7 +42,11 @@ describe CSVDecision::Input do
41
42
 
42
43
  table = CSVDecision.parse(data)
43
44
  input = { input: 'input0', input1: 'input1' }
44
- expected = { hash: input, scan_cols: { 0 => 'input0', 2 => 'input1'} }
45
+ expected = {
46
+ hash: input,
47
+ scan_cols: { 0 => 'input0', 2 => 'input1'},
48
+ key: 'input0'
49
+ }
45
50
 
46
51
  result = CSVDecision::Input.parse(table: table, input: input, symbolize_keys: false)
47
52
 
@@ -3,6 +3,7 @@
3
3
  require_relative '../../lib/csv_decision'
4
4
 
5
5
  SPEC_DATA_VALID ||= File.join(CSVDecision.root, 'spec', 'data', 'valid')
6
+ SPEC_DATA_INVALID ||= File.join(CSVDecision.root, 'spec', 'data', 'invalid')
6
7
 
7
8
  describe CSVDecision::Options do
8
9
  it 'sets the default options' do
@@ -48,7 +49,8 @@ describe CSVDecision::Options do
48
49
  DATA
49
50
 
50
51
  expect { CSVDecision.parse(data, bad_option: false) }
51
- .to raise_error(ArgumentError, "invalid option(s) supplied: [:bad_option]")
52
+ .to raise_error(CSVDecision::CellValidationError,
53
+ "invalid option(s) supplied: [:bad_option]")
52
54
  end
53
55
 
54
56
  it 'parses options from a CSV file' do
@@ -76,4 +78,17 @@ describe CSVDecision::Options do
76
78
  }
77
79
  expect(result.options).to eql expected
78
80
  end
81
+
82
+ it 'parses index option from the CSV file' do
83
+ file = Pathname(File.join(SPEC_DATA_VALID, 'options_in_file3.csv'))
84
+ result = CSVDecision.parse(file)
85
+
86
+ expected = {
87
+ first_match: false,
88
+ regexp_implicit: true,
89
+ text_only: false,
90
+ matchers: CSVDecision::Options::DEFAULT_MATCHERS
91
+ }
92
+ expect(result.options).to eql expected
93
+ end
79
94
  end
@@ -3,11 +3,10 @@
3
3
  require_relative '../../lib/csv_decision'
4
4
 
5
5
  describe CSVDecision::Parse do
6
- it 'loads an empty decision table' do
7
- table = CSVDecision.parse('')
8
- expect(table).to be_a CSVDecision::Table
9
- expect(table.frozen?).to eq true
10
- expect(table.rows.empty?).to eq true
6
+ it 'rejects an empty decision table' do
7
+ expect { CSVDecision.parse('') }
8
+ .to raise_error(CSVDecision::TableValidationError,
9
+ 'table has no header row')
11
10
  end
12
11
 
13
12
  it 'parses a decision table from a CSV file' do