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.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +62 -28
  5. data/csv_decision.gemspec +1 -1
  6. data/doc/CSVDecision/CellValidationError.html +2 -2
  7. data/doc/CSVDecision/Columns/Dictionary.html +114 -20
  8. data/doc/CSVDecision/Columns/Entry.html +2 -2
  9. data/doc/CSVDecision/Columns.html +109 -27
  10. data/doc/CSVDecision/Data.html +2 -2
  11. data/doc/CSVDecision/Decide.html +2 -2
  12. data/doc/CSVDecision/Decision.html +21 -21
  13. data/doc/CSVDecision/Dictionary/Entry.html +508 -0
  14. data/doc/CSVDecision/Dictionary.html +265 -0
  15. data/doc/CSVDecision/Error.html +2 -2
  16. data/doc/CSVDecision/FileError.html +3 -3
  17. data/doc/CSVDecision/Header.html +37 -136
  18. data/doc/CSVDecision/Input.html +2 -2
  19. data/doc/CSVDecision/Load.html +2 -2
  20. data/doc/CSVDecision/Matchers/Constant.html +2 -2
  21. data/doc/CSVDecision/Matchers/Function.html +2 -2
  22. data/doc/CSVDecision/Matchers/Guard.html +92 -25
  23. data/doc/CSVDecision/Matchers/Matcher.html +14 -18
  24. data/doc/CSVDecision/Matchers/Numeric.html +2 -2
  25. data/doc/CSVDecision/Matchers/Pattern.html +2 -2
  26. data/doc/CSVDecision/Matchers/Range.html +2 -2
  27. data/doc/CSVDecision/Matchers/Symbol.html +2 -2
  28. data/doc/CSVDecision/Matchers.html +5 -5
  29. data/doc/CSVDecision/Options.html +2 -2
  30. data/doc/CSVDecision/Parse.html +6 -4
  31. data/doc/CSVDecision/Result.html +944 -0
  32. data/doc/CSVDecision/ScanRow.html +70 -80
  33. data/doc/CSVDecision/Table.html +134 -54
  34. data/doc/CSVDecision.html +5 -5
  35. data/doc/_index.html +18 -4
  36. data/doc/class_list.html +1 -1
  37. data/doc/file.README.html +132 -62
  38. data/doc/index.html +132 -62
  39. data/doc/method_list.html +156 -60
  40. data/doc/top-level-namespace.html +2 -2
  41. data/lib/csv_decision/columns.rb +1 -8
  42. data/lib/csv_decision/decision.rb +45 -96
  43. data/lib/csv_decision/dictionary.rb +149 -0
  44. data/lib/csv_decision/header.rb +6 -133
  45. data/lib/csv_decision/matchers.rb +1 -2
  46. data/lib/csv_decision/parse.rb +18 -7
  47. data/lib/csv_decision/result.rb +180 -0
  48. data/lib/csv_decision/scan_row.rb +13 -7
  49. data/lib/csv_decision/table.rb +6 -5
  50. data/lib/csv_decision.rb +3 -1
  51. data/spec/csv_decision/columns_spec.rb +25 -4
  52. data/spec/csv_decision/examples_spec.rb +25 -0
  53. data/spec/csv_decision/matchers/guard_spec.rb +26 -9
  54. data/spec/csv_decision/table_spec.rb +48 -2
  55. 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 guard column
21
- guard_constant?(type: :constant, column: column)
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
- guard_constant?(type: proc.type, column: column) if proc
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.guard_constant?(type:, column:)
53
- return false unless type == :constant && column.type == :guard
58
+ def self.invalid_constant?(type:, column:)
59
+ return false unless type == :constant && NO_CONSTANTS.member?(column.type)
54
60
 
55
- raise CellValidationError, 'guard column cannot contain constants'
61
+ raise CellValidationError, "#{column.type}: column cannot contain constants"
56
62
  end
57
- private_class_method :guard_constant?
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
  #
@@ -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 'parses a decision table columns from a CSV string' do
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: :output, eval: false, type: :out)
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::Columns::Entry.new(:input, nil, :in))
48
+ .to eq(0 => CSVDecision::Dictionary::Entry.new(:input, nil, :in))
39
49
  expect(result.columns.outs)
40
- .to eq(1 => CSVDecision::Columns::Entry.new(:output, nil, :out))
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 string in a guard column' do
136
+ context 'raises an error for a constant in a guard column' do
137
137
  data = <<~DATA
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
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
- IN :country, guard : country, out :PAID, out :PAID_type, out :len
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.8
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: 2017-12-31 00:00:00.000000000 Z
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