csv_decision 0.0.8 → 0.0.9
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/.rubocop.yml +3 -0
- data/CHANGELOG.md +4 -0
- data/README.md +62 -28
- data/csv_decision.gemspec +1 -1
- data/doc/CSVDecision/CellValidationError.html +2 -2
- data/doc/CSVDecision/Columns/Dictionary.html +114 -20
- data/doc/CSVDecision/Columns/Entry.html +2 -2
- data/doc/CSVDecision/Columns.html +109 -27
- data/doc/CSVDecision/Data.html +2 -2
- data/doc/CSVDecision/Decide.html +2 -2
- data/doc/CSVDecision/Decision.html +21 -21
- data/doc/CSVDecision/Dictionary/Entry.html +508 -0
- data/doc/CSVDecision/Dictionary.html +265 -0
- data/doc/CSVDecision/Error.html +2 -2
- data/doc/CSVDecision/FileError.html +3 -3
- data/doc/CSVDecision/Header.html +37 -136
- data/doc/CSVDecision/Input.html +2 -2
- data/doc/CSVDecision/Load.html +2 -2
- data/doc/CSVDecision/Matchers/Constant.html +2 -2
- data/doc/CSVDecision/Matchers/Function.html +2 -2
- data/doc/CSVDecision/Matchers/Guard.html +92 -25
- data/doc/CSVDecision/Matchers/Matcher.html +14 -18
- data/doc/CSVDecision/Matchers/Numeric.html +2 -2
- data/doc/CSVDecision/Matchers/Pattern.html +2 -2
- data/doc/CSVDecision/Matchers/Range.html +2 -2
- data/doc/CSVDecision/Matchers/Symbol.html +2 -2
- data/doc/CSVDecision/Matchers.html +5 -5
- data/doc/CSVDecision/Options.html +2 -2
- data/doc/CSVDecision/Parse.html +6 -4
- data/doc/CSVDecision/Result.html +944 -0
- data/doc/CSVDecision/ScanRow.html +70 -80
- data/doc/CSVDecision/Table.html +134 -54
- data/doc/CSVDecision.html +5 -5
- data/doc/_index.html +18 -4
- data/doc/class_list.html +1 -1
- data/doc/file.README.html +132 -62
- data/doc/index.html +132 -62
- data/doc/method_list.html +156 -60
- data/doc/top-level-namespace.html +2 -2
- data/lib/csv_decision/columns.rb +1 -8
- data/lib/csv_decision/decision.rb +45 -96
- data/lib/csv_decision/dictionary.rb +149 -0
- data/lib/csv_decision/header.rb +6 -133
- data/lib/csv_decision/matchers.rb +1 -2
- data/lib/csv_decision/parse.rb +18 -7
- data/lib/csv_decision/result.rb +180 -0
- data/lib/csv_decision/scan_row.rb +13 -7
- data/lib/csv_decision/table.rb +6 -5
- data/lib/csv_decision.rb +3 -1
- data/spec/csv_decision/columns_spec.rb +25 -4
- data/spec/csv_decision/examples_spec.rb +25 -0
- data/spec/csv_decision/matchers/guard_spec.rb +26 -9
- data/spec/csv_decision/table_spec.rb +48 -2
- metadata +7 -2
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# CSV Decision: CSV based Ruby decision tables.
|
4
|
+
# Created December 2017.
|
5
|
+
# @author Brett Vickers <brett@phillips-vickers.com>
|
6
|
+
# See LICENSE and README.md for details.
|
7
|
+
module CSVDecision
|
8
|
+
# Accumulate the matching row(s) into a result hash.
|
9
|
+
# @api private
|
10
|
+
class Result
|
11
|
+
# @return [Hash{Symbol=>Object}, Hash{Integer=>Object}] The decision result hash containing
|
12
|
+
# both result values and if: columns, which eventually get evaluated and removed.
|
13
|
+
attr_reader :attributes
|
14
|
+
|
15
|
+
# @return [Boolean] Returns true if this is a multi-row result
|
16
|
+
attr_reader :multi_result
|
17
|
+
|
18
|
+
# (see Decision.initialize)
|
19
|
+
def initialize(table:, input:)
|
20
|
+
@outs = table.columns.outs
|
21
|
+
@if_columns = table.columns.ifs
|
22
|
+
|
23
|
+
# Partial result always includes the input hash for calculating output functions.
|
24
|
+
@partial_result = input[:hash].dup if table.outs_functions
|
25
|
+
|
26
|
+
@attributes = {}
|
27
|
+
@multi_result = false
|
28
|
+
end
|
29
|
+
|
30
|
+
# Common case for building a single row result is just copying output column values to the
|
31
|
+
# final result hash.
|
32
|
+
# @param row [Array]
|
33
|
+
# @return [void]
|
34
|
+
def add_outs(row)
|
35
|
+
@outs.each_pair { |col, column| @attributes[column.name] = row[col] }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Accumulate the outs into arrays.
|
39
|
+
# @param row [Array]
|
40
|
+
# @return [void]
|
41
|
+
def accumulate_outs(row)
|
42
|
+
@outs.each_pair { |col, column| add_cell(column_name: column.name, cell: row[col]) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Derive the final result.
|
46
|
+
# @return [{Symbol=>Object}]
|
47
|
+
def final
|
48
|
+
return @attributes if @if_columns.empty?
|
49
|
+
|
50
|
+
@multi_result ? multi_row_result : single_row_result
|
51
|
+
end
|
52
|
+
|
53
|
+
# Evaluate the output columns, and use them to start building the final result,
|
54
|
+
# along with the partial result required to evaluate functions.
|
55
|
+
#
|
56
|
+
# @param row [Array]
|
57
|
+
# @return (see #final)
|
58
|
+
def eval_outs(row)
|
59
|
+
# Set the constants first, in case the functions refer to them
|
60
|
+
eval_outs_constants(row: row)
|
61
|
+
|
62
|
+
# Then evaluate the procs, left to right
|
63
|
+
eval_outs_procs(row: row)
|
64
|
+
|
65
|
+
final
|
66
|
+
end
|
67
|
+
|
68
|
+
# Evaluate the cell proc using the partial result calculated so far.
|
69
|
+
#
|
70
|
+
# @param proc [Matchers::Pro]
|
71
|
+
# @param column_name [Symbol, Integer]
|
72
|
+
# @param index [Integer]
|
73
|
+
def eval_cell_proc(proc:, column_name:, index:)
|
74
|
+
@attributes[column_name][index] = proc.function[partial_result(index)]
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
# Case where we have a single row result
|
80
|
+
def single_row_result
|
81
|
+
@if_columns.each_key do |col|
|
82
|
+
return nil unless @attributes[col]
|
83
|
+
|
84
|
+
# Remove the if: column from the final result
|
85
|
+
@attributes.delete(col)
|
86
|
+
end
|
87
|
+
|
88
|
+
@attributes
|
89
|
+
end
|
90
|
+
|
91
|
+
def multi_row_result
|
92
|
+
@if_columns.each_key { |col| check_if_column(col) }
|
93
|
+
|
94
|
+
normalize_result
|
95
|
+
end
|
96
|
+
|
97
|
+
def check_if_column(col)
|
98
|
+
delete_rows = []
|
99
|
+
@attributes[col].each_with_index { |value, index| delete_rows << index unless value }
|
100
|
+
|
101
|
+
# Remove this if: column from the final result
|
102
|
+
@attributes.delete(col)
|
103
|
+
|
104
|
+
# Adjust the row index as we delete rows in sequence.
|
105
|
+
delete_rows.each_with_index { |index, sequence| delete_row(index - sequence) }
|
106
|
+
end
|
107
|
+
|
108
|
+
# Each result "row", given by the row +index+ is a collection of column arrays.
|
109
|
+
# @param index [Integer] Row index.
|
110
|
+
# @return [{Symbol=>Object}, {Integer=>Object}]
|
111
|
+
def delete_row(index)
|
112
|
+
@attributes.transform_values { |value| value.delete_at(index) }
|
113
|
+
end
|
114
|
+
|
115
|
+
# @return [{Symbol=>Object}] Decision result hash with any if: columns removed.
|
116
|
+
def normalize_result
|
117
|
+
# Peek at the first column's result and see how many rows it contains.
|
118
|
+
count = @attributes.values.first.count
|
119
|
+
@multi_result = count > 1
|
120
|
+
|
121
|
+
case count
|
122
|
+
when 0
|
123
|
+
{}
|
124
|
+
# Single row array values do not require arrays.
|
125
|
+
when 1
|
126
|
+
@attributes.transform_values!(&:first)
|
127
|
+
else
|
128
|
+
@attributes
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def eval_outs_constants(row:)
|
133
|
+
@outs.each_pair do |col, column|
|
134
|
+
value = row[col]
|
135
|
+
next if value.is_a?(Matchers::Proc)
|
136
|
+
|
137
|
+
@partial_result[column.name] = value
|
138
|
+
@attributes[column.name] = value
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def eval_outs_procs(row:)
|
143
|
+
@outs.each_pair do |col, column|
|
144
|
+
proc = row[col]
|
145
|
+
next unless proc.is_a?(Matchers::Proc)
|
146
|
+
|
147
|
+
value = proc.function[@partial_result]
|
148
|
+
|
149
|
+
@partial_result[column.name] = value
|
150
|
+
@attributes[column.name] = value
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def partial_result(index)
|
155
|
+
@attributes.each_pair do |column_name, value|
|
156
|
+
# Delete this column from the partial result in case there is data from a prior result row
|
157
|
+
next @partial_result.delete(column_name) if value[index].is_a?(Matchers::Proc)
|
158
|
+
|
159
|
+
# Add this constant value to the partial result row built so far.
|
160
|
+
@partial_result[column_name] = value[index]
|
161
|
+
end
|
162
|
+
|
163
|
+
@partial_result
|
164
|
+
end
|
165
|
+
|
166
|
+
def add_cell(column_name:, cell:)
|
167
|
+
case (current = @attributes[column_name])
|
168
|
+
when nil
|
169
|
+
@attributes[column_name] = cell
|
170
|
+
|
171
|
+
when Array
|
172
|
+
@attributes[column_name] << cell
|
173
|
+
|
174
|
+
else
|
175
|
+
@attributes[column_name] = [current, cell]
|
176
|
+
@multi_result = true
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
@@ -8,17 +8,23 @@ module CSVDecision
|
|
8
8
|
# Data row object indicating which columns are constants versus procs.
|
9
9
|
# @api private
|
10
10
|
class ScanRow
|
11
|
+
# These column types cannot have constants in their data cells.
|
12
|
+
NO_CONSTANTS = Set.new(%i[guard if]).freeze
|
13
|
+
private_constant :NO_CONSTANTS
|
14
|
+
|
11
15
|
# Scan the table cell against all matches.
|
12
16
|
#
|
13
17
|
# @param matchers [Array<Matchers::Matcher>]
|
14
18
|
# @param cell [String]
|
15
19
|
# @return [false, Matchers::Proc]
|
16
20
|
def self.scan(column:, matchers:, cell:)
|
21
|
+
return false if cell == ''
|
22
|
+
|
17
23
|
proc = scan_matchers(column: column, matchers: matchers, cell: cell)
|
18
24
|
return proc if proc
|
19
25
|
|
20
|
-
# Must be a simple string constant - this is OK except for a
|
21
|
-
|
26
|
+
# Must be a simple string constant - this is OK except for a certain column types.
|
27
|
+
invalid_constant?(type: :constant, column: column)
|
22
28
|
end
|
23
29
|
|
24
30
|
def self.scan_matchers(column:, matchers:, cell:)
|
@@ -43,18 +49,18 @@ module CSVDecision
|
|
43
49
|
|
44
50
|
def self.scan_proc(column:, cell:, matcher:)
|
45
51
|
proc = matcher.matches?(cell)
|
46
|
-
|
52
|
+
invalid_constant?(type: proc.type, column: column) if proc
|
47
53
|
|
48
54
|
proc
|
49
55
|
end
|
50
56
|
private_class_method :scan_proc
|
51
57
|
|
52
|
-
def self.
|
53
|
-
return false unless type == :constant && column.type
|
58
|
+
def self.invalid_constant?(type:, column:)
|
59
|
+
return false unless type == :constant && NO_CONSTANTS.member?(column.type)
|
54
60
|
|
55
|
-
raise CellValidationError,
|
61
|
+
raise CellValidationError, "#{column.type}: column cannot contain constants"
|
56
62
|
end
|
57
|
-
private_class_method :
|
63
|
+
private_class_method :invalid_constant?
|
58
64
|
|
59
65
|
# Evaluate the cell proc against the column's input value and/or input hash.
|
60
66
|
#
|
data/lib/csv_decision/table.rb
CHANGED
@@ -27,15 +27,12 @@ module CSVDecision
|
|
27
27
|
end
|
28
28
|
|
29
29
|
# @return [CSVDecision::Columns] Dictionary of all input and output columns.
|
30
|
-
# @api private
|
31
30
|
attr_accessor :columns
|
32
31
|
|
33
32
|
# @return [File, Pathname, nil] File path name if decision table was loaded from a CSV file.
|
34
|
-
# @api private
|
35
33
|
attr_accessor :file
|
36
34
|
|
37
35
|
# @return [Hash] All options, explicitly set or defaulted, used to parse the table.
|
38
|
-
# @api private
|
39
36
|
attr_accessor :options
|
40
37
|
|
41
38
|
# Set if the table row has any output functions (planned feature)
|
@@ -55,6 +52,10 @@ module CSVDecision
|
|
55
52
|
# @api private
|
56
53
|
attr_accessor :outs_rows
|
57
54
|
|
55
|
+
# @return [Array<CSVDecision::ScanRow>] Used to implement filtering of final results.
|
56
|
+
# @api private
|
57
|
+
attr_accessor :if_rows
|
58
|
+
|
58
59
|
# @return Array<CSVDecision::Table>] pre-loaded tables passed to this decision table
|
59
60
|
# at load time. Used to allow this decision table to lookup values in other
|
60
61
|
# decision tables. (Planned feature.)
|
@@ -79,13 +80,13 @@ module CSVDecision
|
|
79
80
|
def initialize
|
80
81
|
@columns = nil
|
81
82
|
@file = nil
|
82
|
-
@matchers = []
|
83
83
|
@options = nil
|
84
84
|
@outs_functions = nil
|
85
85
|
@outs_rows = []
|
86
|
+
@if_rows = []
|
86
87
|
@rows = []
|
87
88
|
@scan_rows = []
|
88
|
-
@tables = nil
|
89
|
+
# @tables = nil
|
89
90
|
end
|
90
91
|
end
|
91
92
|
end
|
data/lib/csv_decision.rb
CHANGED
@@ -13,16 +13,18 @@ module CSVDecision
|
|
13
13
|
File.dirname __dir__
|
14
14
|
end
|
15
15
|
|
16
|
+
autoload :Columns, 'csv_decision/columns'
|
17
|
+
autoload :Dictionary, 'csv_decision/dictionary'
|
16
18
|
autoload :Data, 'csv_decision/data'
|
17
19
|
autoload :Decide, 'csv_decision/decide'
|
18
20
|
autoload :Decision, 'csv_decision/decision'
|
19
|
-
autoload :Columns, 'csv_decision/columns'
|
20
21
|
autoload :Header, 'csv_decision/header'
|
21
22
|
autoload :Input, 'csv_decision/input'
|
22
23
|
autoload :Load, 'csv_decision/load'
|
23
24
|
autoload :Matchers, 'csv_decision/matchers'
|
24
25
|
autoload :Options, 'csv_decision/options'
|
25
26
|
autoload :Parse, 'csv_decision/parse'
|
27
|
+
autoload :Result, 'csv_decision/result'
|
26
28
|
autoload :ScanRow, 'csv_decision/scan_row'
|
27
29
|
autoload :Table, 'csv_decision/table'
|
28
30
|
|
@@ -15,18 +15,28 @@ describe CSVDecision::Columns do
|
|
15
15
|
end
|
16
16
|
end
|
17
17
|
|
18
|
-
it '
|
18
|
+
it 'rejects a duplicate output column name' do
|
19
19
|
data = <<~DATA
|
20
20
|
IN :input, OUT :output, IN/text : input, OUT/text:output
|
21
21
|
input0, output0, input1, output1
|
22
22
|
DATA
|
23
|
+
expect { CSVDecision.parse(data) }
|
24
|
+
.to raise_error(CSVDecision::CellValidationError,
|
25
|
+
"output column name 'output' is duplicated")
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'parses a decision table columns from a CSV string' do
|
29
|
+
data = <<~DATA
|
30
|
+
IN :input, OUT :output, IN/text : input, OUT/text:output2
|
31
|
+
input0, output0, input1, output1
|
32
|
+
DATA
|
23
33
|
table = CSVDecision.parse(data)
|
24
34
|
|
25
35
|
expect(table.columns).to be_a(CSVDecision::Columns)
|
26
36
|
expect(table.columns.ins[0].to_h).to eq(name: :input, eval: nil, type: :in)
|
27
37
|
expect(table.columns.ins[2].to_h).to eq(name: :input, eval: false, type: :in)
|
28
38
|
expect(table.columns.outs[1].to_h).to eq(name: :output, eval: nil, type: :out)
|
29
|
-
expect(table.columns.outs[3].to_h).to eq(name: :
|
39
|
+
expect(table.columns.outs[3].to_h).to eq(name: :output2, eval: false, type: :out)
|
30
40
|
end
|
31
41
|
|
32
42
|
it 'parses a decision table columns from a CSV file' do
|
@@ -35,9 +45,9 @@ describe CSVDecision::Columns do
|
|
35
45
|
|
36
46
|
expect(result.columns).to be_a(CSVDecision::Columns)
|
37
47
|
expect(result.columns.ins)
|
38
|
-
.to eq(0 => CSVDecision::
|
48
|
+
.to eq(0 => CSVDecision::Dictionary::Entry.new(:input, nil, :in))
|
39
49
|
expect(result.columns.outs)
|
40
|
-
.to eq(1 => CSVDecision::
|
50
|
+
.to eq(1 => CSVDecision::Dictionary::Entry.new(:output, nil, :out))
|
41
51
|
end
|
42
52
|
|
43
53
|
it 'rejects an invalid header column' do
|
@@ -108,4 +118,15 @@ describe CSVDecision::Columns do
|
|
108
118
|
.to raise_error(CSVDecision::CellValidationError,
|
109
119
|
"output column name 'country' is also an input column")
|
110
120
|
end
|
121
|
+
|
122
|
+
it 'recognises the if: column' do
|
123
|
+
data = <<~DATA
|
124
|
+
in :country, out :PAID, out :PAID_type, if:
|
125
|
+
US, :CUSIP, CUSIP, :PAID.present?
|
126
|
+
GB, :SEDOL, SEDOL, :PAID.present?
|
127
|
+
DATA
|
128
|
+
table = CSVDecision.parse(data)
|
129
|
+
|
130
|
+
expect(table.columns.ifs[3].to_h).to eq(name: 3, eval: true, type: :if)
|
131
|
+
end
|
111
132
|
end
|
@@ -133,4 +133,29 @@ context 'simple examples' do
|
|
133
133
|
expect(table.decide(country: 'EU', CUSIP: '123456789', ISIN:'123456789012'))
|
134
134
|
.to eq(ID: '123456789012', ID_type: 'ISIN', len: 12)
|
135
135
|
end
|
136
|
+
|
137
|
+
it 'makes a correct decision using an if column' do
|
138
|
+
data = <<~DATA
|
139
|
+
in :country, guard:, out :ID, out :ID_type, out :len, if:
|
140
|
+
US, :CUSIP.present?, :CUSIP, CUSIP8, :ID.length, :len == 8
|
141
|
+
US, :CUSIP.present?, :CUSIP, CUSIP9, :ID.length, :len == 9
|
142
|
+
US, :CUSIP.present?, :CUSIP, DUMMY, :ID.length,
|
143
|
+
, :ISIN.present?, :ISIN, ISIN, :ID.length, :len == 12
|
144
|
+
, :ISIN.present?, :ISIN, DUMMY, :ID.length,
|
145
|
+
, :CUSIP.present?, :CUSIP, DUMMY, :ID.length,
|
146
|
+
DATA
|
147
|
+
|
148
|
+
table = CSVDecision.parse(data)
|
149
|
+
|
150
|
+
expect(table.decide(country: 'US', CUSIP: '12345678'))
|
151
|
+
.to eq(ID: '12345678', ID_type: 'CUSIP8', len: 8)
|
152
|
+
expect(table.decide(country: 'US', CUSIP: '123456789'))
|
153
|
+
.to eq(ID: '123456789', ID_type: 'CUSIP9', len: 9)
|
154
|
+
expect(table.decide(country: 'US', CUSIP: '1234567890'))
|
155
|
+
.to eq(ID: '1234567890', ID_type: 'DUMMY', len: 10)
|
156
|
+
expect(table.decide(country: nil, CUSIP: '123456789', ISIN:'123456789012'))
|
157
|
+
.to eq(ID: '123456789012', ID_type: 'ISIN', len: 12)
|
158
|
+
expect(table.decide(CUSIP: '12345678', ISIN:'1234567890'))
|
159
|
+
.to eq(ID: '1234567890', ID_type: 'DUMMY', len: 10)
|
160
|
+
end
|
136
161
|
end
|
@@ -133,20 +133,37 @@ describe CSVDecision::Matchers::Guard do
|
|
133
133
|
end
|
134
134
|
end
|
135
135
|
|
136
|
-
context 'raises an error for a
|
136
|
+
context 'raises an error for a constant in a guard column' do
|
137
137
|
data = <<~DATA
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
138
|
+
IN :country, guard : country, out :PAID, out :PAID_type, out :len
|
139
|
+
US, :CUSIP.present?, :CUSIP, CUSIP, :PAID.length
|
140
|
+
GB, :SEDOL.present?, :SEDOL, SEDOL, :PAID.length
|
141
|
+
, :ISIN.present?, :ISIN, ISIN, :PAID.length
|
142
|
+
, :SEDOL.present?, :SEDOL, SEDOL, :PAID.length
|
143
|
+
, :CUSIP.present?, :CUSIP, CUSIP, :PAID.length
|
144
|
+
, := nil, := nil, MISSING, := nil
|
145
145
|
DATA
|
146
146
|
|
147
147
|
specify do
|
148
148
|
expect { CSVDecision.parse(data) }
|
149
|
-
.to raise_error(CSVDecision::CellValidationError, 'guard column cannot contain constants')
|
149
|
+
.to raise_error(CSVDecision::CellValidationError, 'guard: column cannot contain constants')
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context 'raises an error for a constant in an if column' do
|
154
|
+
data = <<~DATA
|
155
|
+
IN :country, guard : country, out :PAID, out :PAID_type, if:
|
156
|
+
US, :CUSIP.present?, :CUSIP, CUSIP, TRUE
|
157
|
+
GB, :SEDOL.present?, :SEDOL, SEDOL,
|
158
|
+
, :ISIN.present?, :ISIN, ISIN,
|
159
|
+
, :SEDOL.present?, :SEDOL, SEDOL,
|
160
|
+
, :CUSIP.present?, :CUSIP, CUSIP,
|
161
|
+
, := nil, := nil, MISSING, := nil
|
162
|
+
DATA
|
163
|
+
|
164
|
+
specify do
|
165
|
+
expect { CSVDecision.parse(data) }
|
166
|
+
.to raise_error(CSVDecision::CellValidationError, 'if: column cannot contain constants')
|
150
167
|
end
|
151
168
|
end
|
152
169
|
end
|
@@ -280,7 +280,6 @@ describe CSVDecision::Table do
|
|
280
280
|
DATA
|
281
281
|
}
|
282
282
|
]
|
283
|
-
|
284
283
|
examples.each do |test|
|
285
284
|
%i[decide decide!].each do |method|
|
286
285
|
it "#{method} correctly #{test[:example]}" do
|
@@ -352,7 +351,7 @@ describe CSVDecision::Table do
|
|
352
351
|
{ example: 'evaluates named guard condition',
|
353
352
|
options: {},
|
354
353
|
data: <<~DATA
|
355
|
-
|
354
|
+
in :country, guard: country, out :PAID, out :PAID_type, out :len
|
356
355
|
US, :CUSIP.present?, :CUSIP, CUSIP, :PAID.length
|
357
356
|
GB, :SEDOL.present?, :SEDOL, SEDOL, :PAID.length
|
358
357
|
, :ISIN.present?, :ISIN, ISIN, :PAID.length
|
@@ -360,6 +359,30 @@ describe CSVDecision::Table do
|
|
360
359
|
, :CUSIP.present?, :CUSIP, CUSIP, :PAID.length
|
361
360
|
, , := nil, MISSING, := nil
|
362
361
|
DATA
|
362
|
+
},
|
363
|
+
{ example: 'evaluates named if condition',
|
364
|
+
options: {},
|
365
|
+
data: <<~DATA
|
366
|
+
in :country, out :PAID, out :PAID_type, out :len, if:
|
367
|
+
US, :CUSIP, CUSIP, :PAID.length, :PAID.present?
|
368
|
+
GB, :SEDOL, SEDOL, :PAID.length, :PAID.present?
|
369
|
+
, :ISIN, ISIN, :PAID.length, :PAID.present?
|
370
|
+
, :SEDOL, SEDOL, :PAID.length, :PAID.present?
|
371
|
+
, :CUSIP, CUSIP, :PAID.length, :PAID.present?
|
372
|
+
, := nil, MISSING, := nil,
|
373
|
+
DATA
|
374
|
+
},
|
375
|
+
{ example: 'evaluates multiple if conditions',
|
376
|
+
options: {},
|
377
|
+
data: <<~DATA
|
378
|
+
in :country, out :PAID, if:, out :PAID_type, out :len, if:, if: stupid
|
379
|
+
US, :CUSIP, !:PAID.blank?, CUSIP, :PAID.length, :PAID.present?, :len >= 9
|
380
|
+
GB, :SEDOL, !:PAID.blank?, SEDOL, :PAID.length, :PAID.present?, :len >= 9
|
381
|
+
, :ISIN, !:PAID.blank?, ISIN, :PAID.length, :PAID.present?, :len >= 9
|
382
|
+
, :SEDOL, !:PAID.blank?, SEDOL, :PAID.length, :PAID.present?, :len >= 9
|
383
|
+
, :CUSIP, !:PAID.blank?, CUSIP, :PAID.length, :PAID.present?, :len >= 9
|
384
|
+
, := nil, , MISSING, := nil,,
|
385
|
+
DATA
|
363
386
|
}
|
364
387
|
]
|
365
388
|
examples.each do |test|
|
@@ -389,6 +412,26 @@ describe CSVDecision::Table do
|
|
389
412
|
, :SEDOL.present?, :SEDOL, SEDOL, :ID.length
|
390
413
|
, :ISIN.present?, :ISIN, ISIN, :ID.length
|
391
414
|
DATA
|
415
|
+
},
|
416
|
+
{ example: 'evaluates if: column conditions & output functions',
|
417
|
+
options: { first_match: false },
|
418
|
+
data: <<~DATA
|
419
|
+
IN :country, out :ID, out :ID_type, out :len, if:
|
420
|
+
US, :CUSIP, CUSIP, :ID.length, :ID.present?
|
421
|
+
GB, :SEDOL, SEDOL, :ID.length, :ID.present?
|
422
|
+
, :SEDOL, SEDOL, :ID.length, :ID.present?
|
423
|
+
, :ISIN, ISIN, :ID.length, :ID.present?
|
424
|
+
DATA
|
425
|
+
},
|
426
|
+
{ example: 'evaluates multiple if: column conditions & output functions',
|
427
|
+
options: { first_match: false },
|
428
|
+
data: <<~DATA
|
429
|
+
IN :country, out :ID, if:, out :ID_type, out :len, if:, if:
|
430
|
+
US, :CUSIP, !:ID.blank?, CUSIP, :ID.length, :len == 9, :ID.present?
|
431
|
+
GB, :SEDOL, !:ID.blank?, SEDOL, :ID.length, :len == 7, :ID.present?
|
432
|
+
, :SEDOL, !:ID.blank?, SEDOL, :ID.length, :len == 7, :ID.present?
|
433
|
+
, :ISIN, !:ID.blank?, ISIN, :ID.length, :len ==12, :ID.present?
|
434
|
+
DATA
|
392
435
|
}
|
393
436
|
]
|
394
437
|
examples.each do |test|
|
@@ -401,6 +444,9 @@ describe CSVDecision::Table do
|
|
401
444
|
|
402
445
|
expect(table.send(method, country: 'US', CUSIP: '123456789', ISIN: '123456789012'))
|
403
446
|
.to eq(ID: %w[123456789 123456789012], ID_type: %w[CUSIP ISIN], len: [9, 12])
|
447
|
+
|
448
|
+
expect(table.send(method, country: 'US', Ticker: 'USTY'))
|
449
|
+
.to eq({})
|
404
450
|
end
|
405
451
|
end
|
406
452
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: csv_decision
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brett Vickers
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-01-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -207,6 +207,8 @@ files:
|
|
207
207
|
- doc/CSVDecision/Data.html
|
208
208
|
- doc/CSVDecision/Decide.html
|
209
209
|
- doc/CSVDecision/Decision.html
|
210
|
+
- doc/CSVDecision/Dictionary.html
|
211
|
+
- doc/CSVDecision/Dictionary/Entry.html
|
210
212
|
- doc/CSVDecision/Error.html
|
211
213
|
- doc/CSVDecision/FileError.html
|
212
214
|
- doc/CSVDecision/Function.html
|
@@ -226,6 +228,7 @@ files:
|
|
226
228
|
- doc/CSVDecision/Numeric.html
|
227
229
|
- doc/CSVDecision/Options.html
|
228
230
|
- doc/CSVDecision/Parse.html
|
231
|
+
- doc/CSVDecision/Result.html
|
229
232
|
- doc/CSVDecision/ScanRow.html
|
230
233
|
- doc/CSVDecision/Symbol.html
|
231
234
|
- doc/CSVDecision/Table.html
|
@@ -248,6 +251,7 @@ files:
|
|
248
251
|
- lib/csv_decision/data.rb
|
249
252
|
- lib/csv_decision/decide.rb
|
250
253
|
- lib/csv_decision/decision.rb
|
254
|
+
- lib/csv_decision/dictionary.rb
|
251
255
|
- lib/csv_decision/header.rb
|
252
256
|
- lib/csv_decision/input.rb
|
253
257
|
- lib/csv_decision/load.rb
|
@@ -261,6 +265,7 @@ files:
|
|
261
265
|
- lib/csv_decision/matchers/symbol.rb
|
262
266
|
- lib/csv_decision/options.rb
|
263
267
|
- lib/csv_decision/parse.rb
|
268
|
+
- lib/csv_decision/result.rb
|
264
269
|
- lib/csv_decision/scan_row.rb
|
265
270
|
- lib/csv_decision/table.rb
|
266
271
|
- spec/csv_decision/columns_spec.rb
|