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.
- 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
|