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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -1
- data/README.md +11 -8
- data/benchmarks/rufus_decision.rb +9 -1
- data/csv_decision.gemspec +1 -1
- data/doc/CSVDecision/CellValidationError.html +1 -1
- data/doc/CSVDecision/Columns/Dictionary.html +29 -29
- data/doc/CSVDecision/Columns.html +394 -47
- data/doc/CSVDecision/Data.html +2 -2
- data/doc/CSVDecision/Decide.html +23 -159
- data/doc/CSVDecision/Decision.html +370 -32
- data/doc/CSVDecision/Defaults.html +1 -1
- data/doc/CSVDecision/Dictionary/Entry.html +157 -55
- data/doc/CSVDecision/Dictionary.html +37 -21
- data/doc/CSVDecision/Error.html +1 -1
- data/doc/CSVDecision/FileError.html +1 -1
- data/doc/CSVDecision/Header.html +142 -1
- data/doc/CSVDecision/Index.html +741 -0
- data/doc/CSVDecision/Input.html +14 -61
- data/doc/CSVDecision/Load.html +1 -1
- data/doc/CSVDecision/Matchers/Constant.html +1 -1
- data/doc/CSVDecision/Matchers/Function.html +1 -1
- data/doc/CSVDecision/Matchers/Guard.html +13 -147
- data/doc/CSVDecision/Matchers/Matcher.html +13 -13
- data/doc/CSVDecision/Matchers/Numeric.html +1 -1
- data/doc/CSVDecision/Matchers/Pattern.html +1 -1
- data/doc/CSVDecision/Matchers/Proc.html +147 -14
- data/doc/CSVDecision/Matchers/Range.html +1 -1
- data/doc/CSVDecision/Matchers/Symbol.html +1 -1
- data/doc/CSVDecision/Matchers.html +55 -162
- data/doc/CSVDecision/Options.html +21 -21
- data/doc/CSVDecision/Parse.html +2 -180
- data/doc/CSVDecision/Result.html +220 -38
- data/doc/CSVDecision/ScanRow.html +69 -325
- data/doc/CSVDecision/Table.html +128 -40
- data/doc/CSVDecision/TableValidationError.html +1 -1
- data/doc/CSVDecision/Validate.html +1 -1
- data/doc/CSVDecision.html +4 -4
- data/doc/_index.html +8 -8
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +13 -11
- data/doc/index.html +13 -11
- data/doc/method_list.html +206 -150
- data/doc/top-level-namespace.html +1 -1
- data/lib/csv_decision/columns.rb +87 -1
- data/lib/csv_decision/decision.rb +54 -29
- data/lib/csv_decision/defaults.rb +1 -1
- data/lib/csv_decision/dictionary.rb +32 -22
- data/lib/csv_decision/header.rb +17 -0
- data/lib/csv_decision/index.rb +107 -0
- data/lib/csv_decision/input.rb +45 -13
- data/lib/csv_decision/matchers/guard.rb +2 -0
- data/lib/csv_decision/matchers.rb +14 -8
- data/lib/csv_decision/options.rb +7 -19
- data/lib/csv_decision/parse.rb +12 -96
- data/lib/csv_decision/result.rb +10 -9
- data/lib/csv_decision/scan_row.rb +20 -44
- data/lib/csv_decision/table.rb +7 -4
- data/lib/csv_decision.rb +1 -1
- data/spec/csv_decision/columns_spec.rb +6 -6
- data/spec/csv_decision/data_spec.rb +0 -5
- data/spec/csv_decision/index_spec.rb +58 -0
- data/spec/csv_decision/input_spec.rb +7 -2
- data/spec/csv_decision/options_spec.rb +16 -1
- data/spec/csv_decision/parse_spec.rb +4 -5
- data/spec/csv_decision/table_spec.rb +70 -0
- data/spec/data/{valid → invalid}/empty.csv +0 -0
- data/spec/data/valid/index_example.csv +12 -0
- data/spec/data/valid/multi_column_index.csv +10 -0
- data/spec/data/valid/multi_column_index2.csv +12 -0
- data/spec/data/valid/options_in_file3.csv +13 -0
- metadata +16 -5
- 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
|
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
|
133
|
+
[row, scan_row]
|
128
134
|
end
|
129
135
|
|
130
136
|
# @return [Array<Matchers::Matcher>] Matchers for the input columns.
|
data/lib/csv_decision/options.rb
CHANGED
@@ -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 [
|
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
|
-
|
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
|
120
|
+
raise CellValidationError, "invalid option(s) supplied: #{invalid_options.inspect}"
|
133
121
|
end
|
134
122
|
private_class_method :validate
|
135
123
|
end
|
data/lib/csv_decision/parse.rb
CHANGED
@@ -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
|
-
#
|
89
|
-
matchers
|
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
|
-
#
|
92
|
-
|
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.
|
96
|
+
def self.parse_with_matchers(table:, matchers:)
|
100
97
|
# Parse the header row
|
101
|
-
table.columns =
|
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.
|
107
|
-
|
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 :
|
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
|
-
|
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
|
-
|
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
|
|
data/lib/csv_decision/result.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
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
|
-
|
102
|
-
|
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
|
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
|
104
|
+
# Match cells in the input hash against a decision table row.
|
116
105
|
# @param row (see ScanRow.scan_columns)
|
117
|
-
# @param
|
106
|
+
# @param hash (see Decision#row_scan)
|
118
107
|
# @return [Boolean] True for a match, false otherwise.
|
119
|
-
def
|
120
|
-
constants
|
121
|
-
|
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
|
-
|
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
|
data/lib/csv_decision/table.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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 <=
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
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 = {
|
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(
|
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 '
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|