csv_decision 0.2.0 → 0.3.0

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