csv_decision 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +26 -2
  4. data/csv_decision.gemspec +1 -1
  5. data/doc/CSVDecision/CellValidationError.html +2 -2
  6. data/doc/CSVDecision/Columns/Default.html +203 -23
  7. data/doc/CSVDecision/Columns/Dictionary.html +118 -25
  8. data/doc/CSVDecision/Columns.html +213 -31
  9. data/doc/CSVDecision/Data.html +1 -1
  10. data/doc/CSVDecision/Decide.html +1 -1
  11. data/doc/CSVDecision/Decision.html +1 -1
  12. data/doc/CSVDecision/Defaults.html +291 -0
  13. data/doc/CSVDecision/Dictionary/Entry.html +584 -47
  14. data/doc/CSVDecision/Dictionary.html +20 -20
  15. data/doc/CSVDecision/Error.html +2 -2
  16. data/doc/CSVDecision/FileError.html +1 -1
  17. data/doc/CSVDecision/Header.html +110 -33
  18. data/doc/CSVDecision/Input.html +1 -1
  19. data/doc/CSVDecision/Load.html +1 -1
  20. data/doc/CSVDecision/Matchers/Constant.html +12 -37
  21. data/doc/CSVDecision/Matchers/Function.html +1 -1
  22. data/doc/CSVDecision/Matchers/Guard.html +15 -13
  23. data/doc/CSVDecision/Matchers/Matcher.html +1 -1
  24. data/doc/CSVDecision/Matchers/Numeric.html +13 -21
  25. data/doc/CSVDecision/Matchers/Pattern.html +14 -14
  26. data/doc/CSVDecision/Matchers/Proc.html +1 -1
  27. data/doc/CSVDecision/Matchers/Range.html +13 -58
  28. data/doc/CSVDecision/Matchers/Symbol.html +1 -1
  29. data/doc/CSVDecision/Matchers.html +9 -9
  30. data/doc/CSVDecision/Options.html +1 -1
  31. data/doc/CSVDecision/Parse.html +90 -19
  32. data/doc/CSVDecision/Result.html +1 -1
  33. data/doc/CSVDecision/ScanRow.html +17 -17
  34. data/doc/CSVDecision/Table.html +50 -48
  35. data/doc/CSVDecision/TableValidationError.html +143 -0
  36. data/doc/CSVDecision/Validate.html +422 -0
  37. data/doc/CSVDecision.html +8 -8
  38. data/doc/_index.html +33 -1
  39. data/doc/class_list.html +1 -1
  40. data/doc/file.README.html +27 -3
  41. data/doc/index.html +27 -3
  42. data/doc/method_list.html +193 -89
  43. data/doc/top-level-namespace.html +1 -1
  44. data/lib/csv_decision/columns.rb +28 -27
  45. data/lib/csv_decision/defaults.rb +47 -0
  46. data/lib/csv_decision/dictionary.rb +104 -112
  47. data/lib/csv_decision/header.rb +13 -10
  48. data/lib/csv_decision/input.rb +53 -5
  49. data/lib/csv_decision/matchers/constant.rb +1 -2
  50. data/lib/csv_decision/matchers/guard.rb +3 -2
  51. data/lib/csv_decision/matchers/numeric.rb +4 -6
  52. data/lib/csv_decision/matchers/pattern.rb +6 -8
  53. data/lib/csv_decision/matchers/range.rb +1 -3
  54. data/lib/csv_decision/matchers.rb +7 -7
  55. data/lib/csv_decision/parse.rb +24 -3
  56. data/lib/csv_decision/scan_row.rb +16 -16
  57. data/lib/csv_decision/table.rb +3 -7
  58. data/lib/csv_decision/validate.rb +85 -0
  59. data/lib/csv_decision.rb +3 -1
  60. data/spec/csv_decision/columns_spec.rb +38 -22
  61. data/spec/csv_decision/examples_spec.rb +17 -0
  62. data/spec/csv_decision/matchers/range_spec.rb +0 -32
  63. data/spec/csv_decision/table_spec.rb +39 -0
  64. metadata +7 -2
@@ -51,9 +51,6 @@ module CSVDecision
51
51
  # Negation sign prefixed to ranges and functions.
52
52
  NEGATE = '!'
53
53
 
54
- # Cell constants and functions specified by prefixing the value with one of these 3 symbols.
55
- EQUALS = '==|:=|='
56
-
57
54
  # All regular expressions used for matching are anchored inside their own
58
55
  # non-capturing group.
59
56
  #
@@ -63,6 +60,9 @@ module CSVDecision
63
60
  Regexp.new("\\A(?:#{value})\\z").freeze
64
61
  end
65
62
 
63
+ # Cell constants and functions specified by prefixing the value with one of these 3 symbols.
64
+ EQUALS = '==|:=|='
65
+
66
66
  EQUALS_RE = regexp(EQUALS)
67
67
  private_constant :EQUALS_RE
68
68
 
@@ -71,7 +71,7 @@ module CSVDecision
71
71
  # @param operator [String]
72
72
  # @return [String]
73
73
  def self.normalize_operator(operator)
74
- EQUALS_RE.match(operator) ? '==' : operator
74
+ EQUALS_RE.match?(operator) ? '==' : operator
75
75
  end
76
76
 
77
77
  # Regular expression used to recognise a numeric string with or without a decimal point.
@@ -135,9 +135,9 @@ module CSVDecision
135
135
 
136
136
  # @param options (see CSVDecision.parse)
137
137
  def initialize(options)
138
- @matchers = options[:matchers].collect { |klass| klass.new(options) }
139
- @ins = @matchers.select(&:ins?)
140
- @outs = @matchers.select(&:outs?)
138
+ matchers = options[:matchers].collect { |klass| klass.new(options) }
139
+ @ins = matchers.select(&:ins?)
140
+ @outs = matchers.select(&:outs?)
141
141
  end
142
142
 
143
143
  # Parse the row's input columns using the input matchers.
@@ -9,6 +9,9 @@ module CSVDecision
9
9
  class Error < StandardError; end
10
10
 
11
11
  # Error validating a cell when parsing input table data.
12
+ class TableValidationError < Error; end
13
+
14
+ # Error validating a cell when parsing input table cell data.
12
15
  class CellValidationError < Error; end
13
16
 
14
17
  # Table parsing error message enhanced to include the file being processed.
@@ -82,13 +85,31 @@ module CSVDecision
82
85
  # These override any options passed as parameters to the parse method.
83
86
  table.options = Options.from_csv(rows: table.rows, options: options).freeze
84
87
 
88
+ # Matchers
89
+ matchers = CSVDecision::Matchers.new(options)
90
+
85
91
  # Parse the header row
86
- table.columns = CSVDecision::Columns.new(table)
92
+ table.columns = parse_header(table: table, matchers: matchers)
87
93
 
88
- parse_data(table: table, matchers: Matchers.new(options))
94
+ # Parse the table's the data rows.
95
+ parse_data(table: table, matchers: matchers)
89
96
  end
90
97
  private_class_method :parse_table
91
98
 
99
+ def self.parse_header(table:, matchers:)
100
+ # Parse the header row
101
+ table.columns = CSVDecision::Columns.new(table)
102
+
103
+ # Parse the defaults row if present
104
+ return table.columns if table.columns.defaults.blank?
105
+
106
+ table.columns.defaults =
107
+ Defaults.parse(columns: table.columns, matchers: matchers.outs, row: table.rows.shift)
108
+
109
+ table.columns
110
+ end
111
+ private_class_method :parse_header
112
+
92
113
  def self.parse_data(table:, matchers:)
93
114
  table.rows.each_with_index do |row, index|
94
115
  # Mutate the row if we find anything other than a simple string constant in its
@@ -201,7 +222,7 @@ module CSVDecision
201
222
 
202
223
  add_ins_symbols(columns: columns, cell: cell)
203
224
  end
204
- private_class_method :ins_cell_dictionary
225
+ # private_class_method :ins_cell_dictionary
205
226
 
206
227
  def self.add_ins_symbols(columns:, cell:)
207
228
  Array(cell.symbols).each do |symbol|
@@ -27,6 +27,21 @@ module CSVDecision
27
27
  invalid_constant?(type: :constant, column: column)
28
28
  end
29
29
 
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
+
30
45
  def self.scan_matchers(column:, matchers:, cell:)
31
46
  matchers.each do |matcher|
32
47
  # Guard function only accepts the same matchers as an output column.
@@ -62,21 +77,6 @@ module CSVDecision
62
77
  end
63
78
  private_class_method :invalid_constant?
64
79
 
65
- # Evaluate the cell proc against the column's input value and/or input hash.
66
- #
67
- # @param proc [CSVDecision::Proc] Proc in the table cell.
68
- # @param value [Object] Value supplied in the input hash corresponding to this column.
69
- # @param hash [{Symbol=>Object}] Input hash with symbolized keys.
70
- def self.eval_matcher(proc:, hash:, value: nil)
71
- function = proc.function
72
-
73
- # A symbol guard expression just needs to be passed the input hash
74
- return function[hash] if proc.type == :guard
75
-
76
- # All other procs can take one or two args
77
- function.arity == 1 ? function[value] : function[value, hash]
78
- end
79
-
80
80
  # @return [Array<Integer>] Column indices for simple constants.
81
81
  attr_reader :constants
82
82
 
@@ -91,7 +91,7 @@ module CSVDecision
91
91
  # Scan all the specified +columns+ (e.g., inputs) in the given +data+ row using the +matchers+
92
92
  # array supplied.
93
93
  #
94
- # @param row [Array] Data row.
94
+ # @param row [Array<String>] Data row - still just all string constants.
95
95
  # @param columns [Array<Columns::Entry>] Array of column dictionary entries.
96
96
  # @param matchers [Array<Matchers::Matcher>] Array of table cell matchers.
97
97
  # @return [Array] Data row with anything not a string constant replaced with a Proc or a
@@ -11,12 +11,13 @@ module CSVDecision
11
11
  #
12
12
  # @note Input hash keys may or may not be symbolized.
13
13
  # @param input [Hash] Input hash.
14
- # @return [Hash{Symbol => Object, Array<Object>}] Decision hash.
14
+ # @return [{Symbol => Object, Array<Object>}] Decision hash.
15
15
  def decide(input)
16
16
  Decide.decide(table: self, input: input, symbolize_keys: true)
17
17
  end
18
18
 
19
- # Unsafe version of decide - assumes the input hash is symbolized.
19
+ # Unsafe version of decide - may mutate the input hash and assumes the input
20
+ # hash is symbolized.
20
21
  #
21
22
  # @param input (see #decide)
22
23
  # @note Input hash must have its keys symbolized.
@@ -56,11 +57,6 @@ module CSVDecision
56
57
  # @api private
57
58
  attr_accessor :if_rows
58
59
 
59
- # @return Array<CSVDecision::Table>] pre-loaded tables passed to this decision table
60
- # at load time. Used to allow this decision table to lookup values in other
61
- # decision tables. (Planned feature.)
62
- # attr_reader :tables
63
-
64
60
  # Iterate through all data rows of the decision table, with an optional
65
61
  # first and last row index given.
66
62
  #
@@ -0,0 +1,85 @@
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
+ # Parse and validate the column names in the header row.
9
+ # These methods are only required at table load time.
10
+ # @api private
11
+ module Validate
12
+ # These column types do not need a name.
13
+ COLUMN_TYPE_ANONYMOUS = Set.new(%i[guard if]).freeze
14
+ private_constant :COLUMN_TYPE_ANONYMOUS
15
+
16
+ # Validate a column header cell and return its type and name.
17
+ #
18
+ # @param cell [String] Header cell.
19
+ # @param index [Integer] The header column's index.
20
+ # @return [Array<(Symbol, Symbol)>] Column type and column name symbols.
21
+ def self.column(cell:, index:)
22
+ match = Header::COLUMN_TYPE.match(cell)
23
+ raise CellValidationError, 'column name is not well formed' unless match
24
+
25
+ column_type = match['type']&.downcase&.to_sym
26
+ column_name = column_name(type: column_type, name: match['name'], index: index)
27
+
28
+ [column_type, column_name]
29
+ rescue CellValidationError => exp
30
+ raise CellValidationError, "header column '#{cell}' is not valid as the #{exp.message}"
31
+ end
32
+
33
+ # Validate the column name against the dictionary of column names.
34
+ #
35
+ # @param columns [Symbol=>[false, Integer]] Column name dictionary.
36
+ # @param name [Symbol] Column name.
37
+ # @param out [false, Integer] False if an input column, otherwise the column index of
38
+ # the output column.
39
+ # @return [void]
40
+ # @raise [CellValidationError] Column name invalid.
41
+ def self.name(columns:, name:, out:)
42
+ return unless (in_out = columns[name])
43
+
44
+ return validate_out_name(in_out: in_out, name: name) if out
45
+ validate_in_name(in_out: in_out, name: name)
46
+ end
47
+
48
+ def self.column_name(type:, name:, index:)
49
+ # if: columns are named after their index, which is an integer and so cannot
50
+ # clash with other column name types, which are symbols.
51
+ return index if type == :if
52
+
53
+ return format_column_name(name) if name.present?
54
+
55
+ return if COLUMN_TYPE_ANONYMOUS.member?(type)
56
+ raise CellValidationError, 'column name is missing'
57
+ end
58
+ private_class_method :column_name
59
+
60
+ def self.format_column_name(name)
61
+ column_name = name.strip.tr("\s", '_')
62
+
63
+ return column_name.to_sym if Header.column_name?(column_name)
64
+ raise CellValidationError, "column name '#{name}' contains invalid characters"
65
+ end
66
+ private_class_method :format_column_name
67
+
68
+ def self.validate_out_name(in_out:, name:)
69
+ if in_out == :in
70
+ raise CellValidationError, "output column name '#{name}' is also an input column"
71
+ end
72
+
73
+ raise CellValidationError, "output column name '#{name}' is duplicated"
74
+ end
75
+ private_class_method :validate_out_name
76
+
77
+ def self.validate_in_name(in_out:, name:)
78
+ # in: columns may be duped
79
+ return if in_out == :in
80
+
81
+ raise CellValidationError, "output column name '#{name}' is also an input column"
82
+ end
83
+ private_class_method :validate_in_name
84
+ end
85
+ end
data/lib/csv_decision.rb CHANGED
@@ -14,10 +14,11 @@ module CSVDecision
14
14
  end
15
15
 
16
16
  autoload :Columns, 'csv_decision/columns'
17
- autoload :Dictionary, 'csv_decision/dictionary'
18
17
  autoload :Data, 'csv_decision/data'
19
18
  autoload :Decide, 'csv_decision/decide'
20
19
  autoload :Decision, 'csv_decision/decision'
20
+ autoload :Defaults, 'csv_decision/defaults'
21
+ autoload :Dictionary, 'csv_decision/dictionary'
21
22
  autoload :Header, 'csv_decision/header'
22
23
  autoload :Input, 'csv_decision/input'
23
24
  autoload :Load, 'csv_decision/load'
@@ -27,6 +28,7 @@ module CSVDecision
27
28
  autoload :Result, 'csv_decision/result'
28
29
  autoload :ScanRow, 'csv_decision/scan_row'
29
30
  autoload :Table, 'csv_decision/table'
31
+ autoload :Validate, 'csv_decision/validate'
30
32
 
31
33
  class Matchers
32
34
  autoload :Constant, 'csv_decision/matchers/constant'
@@ -33,10 +33,10 @@ describe CSVDecision::Columns do
33
33
  table = CSVDecision.parse(data)
34
34
 
35
35
  expect(table.columns).to be_a(CSVDecision::Columns)
36
- expect(table.columns.ins[0].to_h).to eq(name: :input, eval: nil, type: :in)
37
- expect(table.columns.ins[2].to_h).to eq(name: :input, eval: false, type: :in)
38
- expect(table.columns.outs[1].to_h).to eq(name: :output, eval: nil, type: :out)
39
- expect(table.columns.outs[3].to_h).to eq(name: :output2, eval: false, type: :out)
36
+ expect(table.columns.ins[0].to_h).to eq(name: :input, eval: nil, type: :in, set_if: nil)
37
+ expect(table.columns.ins[2].to_h).to eq(name: :input, eval: false, type: :in, set_if: nil)
38
+ expect(table.columns.outs[1].to_h).to eq(name: :output, eval: nil, type: :out, set_if: nil)
39
+ expect(table.columns.outs[3].to_h).to eq(name: :output2, eval: false, type: :out, set_if: nil)
40
40
 
41
41
  expect(table.columns.dictionary).to eq(input: :in, output: 1, output2: 3)
42
42
  end
@@ -50,13 +50,13 @@ describe CSVDecision::Columns do
50
50
  table = CSVDecision.parse(data)
51
51
 
52
52
  expect(table.columns).to be_a(CSVDecision::Columns)
53
- expect(table.columns.ins[0].to_h).to eq(name: :input, eval: nil, type: :in)
54
- expect(table.columns.ins[2].to_h).to eq(name: :input, eval: false, type: :in)
55
- expect(table.columns.ins[5].to_h).to eq(name: nil, eval: true, type: :guard)
53
+ expect(table.columns.ins[0].to_h).to eq(name: :input, eval: nil, type: :in, set_if: nil)
54
+ expect(table.columns.ins[2].to_h).to eq(name: :input, eval: false, type: :in, set_if: nil)
55
+ expect(table.columns.ins[5].to_h).to eq(name: nil, eval: true, type: :guard, set_if: nil)
56
56
 
57
- expect(table.columns.outs[1].to_h).to eq(name: :output, eval: nil, type: :out)
58
- expect(table.columns.outs[3].to_h).to eq(name: :output2, eval: false, type: :out)
59
- expect(table.columns.outs[4].to_h).to eq(name: :len, eval: true, type: :out)
57
+ expect(table.columns.outs[1].to_h).to eq(name: :output, eval: nil, type: :out, set_if: nil)
58
+ expect(table.columns.outs[3].to_h).to eq(name: :output2, eval: false, type: :out, set_if: nil)
59
+ expect(table.columns.outs[4].to_h).to eq(name: :len, eval: true, type: :out, set_if: nil)
60
60
 
61
61
  expect(table.columns.dictionary)
62
62
  .to eq(input: :in, output: 1, output2: 3, len: 4, input2: :in, input3: :in, input4: :in)
@@ -74,12 +74,12 @@ describe CSVDecision::Columns do
74
74
  table = CSVDecision.parse(data)
75
75
 
76
76
  expect(table.columns).to be_a(CSVDecision::Columns)
77
- expect(table.columns.ins[0].to_h).to eq(name: :input, eval: nil, type: :in)
78
- expect(table.columns.ins[2].to_h).to eq(name: :input, eval: false, type: :in)
79
- expect(table.columns.outs[1].to_h).to eq(name: :output, eval: nil, type: :out)
80
- expect(table.columns.outs[3].to_h).to eq(name: :output2, eval: false, type: :out)
81
- expect(table.columns.outs[4].to_h).to eq(name: :input3, eval: true, type: :out)
82
- expect(table.columns.outs[5].to_h).to eq(name: :len, eval: true, type: :out)
77
+ expect(table.columns.ins[0].to_h).to eq(name: :input, eval: nil, type: :in, set_if: nil)
78
+ expect(table.columns.ins[2].to_h).to eq(name: :input, eval: false, type: :in, set_if: nil)
79
+ expect(table.columns.outs[1].to_h).to eq(name: :output, eval: nil, type: :out, set_if: nil)
80
+ expect(table.columns.outs[3].to_h).to eq(name: :output2, eval: false, type: :out, set_if: nil)
81
+ expect(table.columns.outs[4].to_h).to eq(name: :input3, eval: true, type: :out, set_if: nil)
82
+ expect(table.columns.outs[5].to_h).to eq(name: :len, eval: true, type: :out, set_if: nil)
83
83
 
84
84
  expect(table.columns.dictionary)
85
85
  .to eq(input: :in, output: 1, output2: 3, len: 5, input2: :in, input3: 4, input4: :in)
@@ -118,10 +118,10 @@ describe CSVDecision::Columns do
118
118
  result = CSVDecision.parse(file)
119
119
 
120
120
  expect(result.columns).to be_a(CSVDecision::Columns)
121
- expect(result.columns.ins)
122
- .to eq(0 => CSVDecision::Dictionary::Entry.new(:input, nil, :in))
123
- expect(result.columns.outs)
124
- .to eq(1 => CSVDecision::Dictionary::Entry.new(:output, nil, :out))
121
+ expect(result.columns.ins.count).to eq 1
122
+ expect(result.columns.outs.count).to eq 1
123
+ expect(result.columns.ins[0].to_h).to eql(name: :input, type: :in, eval: nil, set_if: nil)
124
+ expect(result.columns.outs[1].to_h).to eql(name: :output, type: :out, eval: nil, set_if: nil)
125
125
  end
126
126
 
127
127
  it 'rejects an invalid header column' do
@@ -179,7 +179,7 @@ describe CSVDecision::Columns do
179
179
  table = CSVDecision.parse(data)
180
180
 
181
181
  expect(table.columns.ins[1].to_h)
182
- .to eq(name: nil, eval: true, type: :guard)
182
+ .to eq(name: nil, eval: true, type: :guard, set_if: nil)
183
183
 
184
184
  expect(table.columns.input_keys).to eq %i[country CUSIP SEDOL]
185
185
  end
@@ -215,7 +215,23 @@ describe CSVDecision::Columns do
215
215
  DATA
216
216
  table = CSVDecision.parse(data)
217
217
 
218
- expect(table.columns.ifs[3].to_h).to eq(name: 3, eval: true, type: :if)
218
+ expect(table.columns.ifs[3].to_h).to eq(name: 3, eval: true, type: :if, set_if: nil)
219
219
  expect(table.columns.input_keys).to eq %i[country CUSIP SEDOL]
220
220
  end
221
+
222
+ it 'recognises the set: columns' do
223
+ data = <<~DATA
224
+ set/nil? :country, guard:, set: class, out :PAID, out: len, if:
225
+ US, , :class.upcase,
226
+ US, :CUSIP.present?, != PRIVATE, :CUSIP, :PAID.length, :len == 9
227
+ !=US, :ISIN.present?, != PRIVATE, :ISIN, :PAID.length, :len == 12
228
+ DATA
229
+ table = CSVDecision.parse(data)
230
+
231
+ expect(table.columns.ins[0].to_h).to eq(name: :country, eval: nil, type: :set, set_if: :nil?)
232
+ expect(table.columns.ins[1].to_h).to eq(name: nil, eval: true, type: :guard, set_if: nil)
233
+ expect(table.columns.ins[2].to_h).to eq(name: :class, eval: nil, type: :set, set_if: true)
234
+
235
+ expect(table.columns.input_keys).to eq %i[country class CUSIP ISIN]
236
+ end
221
237
  end
@@ -180,4 +180,21 @@ context 'simple examples' do
180
180
  expect(table.decide(CUSIP: '12345678', ISIN:'1234567890'))
181
181
  .to eq(ID: '1234567890', ID_type: 'DUMMY', len: 10)
182
182
  end
183
+
184
+ it 'recognises the set: columns and uses correct defaults' do
185
+ data = <<~DATA
186
+ set/nil? :country, guard:, set: class, out :PAID, out: len, if:
187
+ US, , :class.upcase,
188
+ US, :CUSIP.present?, != PRIVATE, :CUSIP, :PAID.length, :len == 9
189
+ !=US, :ISIN.present?, != PRIVATE, :ISIN, :PAID.length, :len == 12
190
+ US, :CUSIP.present?, PRIVATE, :CUSIP, :PAID.length,
191
+ !=US, :ISIN.present?, PRIVATE, :ISIN, :PAID.length,
192
+ DATA
193
+
194
+ table = CSVDecision.parse(data)
195
+ expect(table.decide(CUSIP: '1234567890', class: 'Private')).to eq(PAID: '1234567890', len: 10)
196
+ expect(table.decide(CUSIP: '123456789', class: 'Public')).to eq(PAID: '123456789', len: 9)
197
+ expect(table.decide(ISIN: '123456789', country: 'GB', class: 'public')).to eq({})
198
+ expect(table.decide(ISIN: '123456789012', country: 'GB', class: 'private')).to eq(PAID: '123456789012', len: 12)
199
+ end
183
200
  end
@@ -10,38 +10,6 @@ describe CSVDecision::Matchers::Range do
10
10
  it { is_expected.to respond_to(:matches?).with(1).argument }
11
11
  end
12
12
 
13
- # context 'cell value matching' do
14
- # ranges = {
15
- # '-1..1' => { min: '-1', type: '..', max: '1', negate: '' },
16
- # '! -1..1' => { min: '-1', type: '..', max: '1', negate: '!' },
17
- # '!-1.0..1.1' => { min: '-1.0', type: '..', max: '1.1', negate: '!' },
18
- # '!-1.0...1.1' => { min: '-1.0', type: '...', max: '1.1', negate: '!' }
19
- # }
20
- # ranges.each_pair do |range, expected|
21
- # it "matches #{range} as a numeric range" do
22
- # match = described_class::NUMERIC_RANGE.match(range)
23
- # expect(match['min']).to eq expected[:min]
24
- # expect(match['max']).to eq expected[:max]
25
- # expect(match['type']).to eq expected[:type]
26
- # expect(match['negate']).to eq expected[:negate]
27
- # end
28
- # end
29
- #
30
- # ranges = {
31
- # 'a..z' => { min: 'a', type: '..', max: 'z', negate: '' },
32
- # '!1...9' => { min: '1', type: '...', max: '9', negate: '!' },
33
- # }
34
- # ranges.each_pair do |range, expected|
35
- # it "matches #{range} as an alphanumeric range" do
36
- # match = described_class::ALNUM_RANGE.match(range)
37
- # expect(match['min']).to eq expected[:min]
38
- # expect(match['max']).to eq expected[:max]
39
- # expect(match['type']).to eq expected[:type]
40
- # expect(match['negate']).to eq expected[:negate]
41
- # end
42
- # end
43
- # end
44
-
45
13
  describe '#matches?' do
46
14
  matcher = described_class.new
47
15
 
@@ -451,5 +451,44 @@ describe CSVDecision::Table do
451
451
  end
452
452
  end
453
453
  end
454
+
455
+ context 'recognises the set: columns and uses correct defaults' do
456
+ examples = [
457
+ { example: 'evaluates set/nil? and set columns',
458
+ options: { first_match: true },
459
+ data: <<~DATA
460
+ set/nil? :country, in: type, guard:, set: class, out :PAID, out: len, if:
461
+ US, , , :class.upcase,
462
+ US, Equity, :CUSIP.present?, != PRIVATE, :CUSIP, :PAID.length, :len == 9
463
+ !=US, Equity, :ISIN.present?, != PRIVATE, :ISIN, :PAID.length, :len == 12
464
+ US, , :CUSIP.present?, PRIVATE, :CUSIP, :PAID.length,
465
+ !=US, , :ISIN.present?, PRIVATE, :ISIN, :PAID.length,
466
+ DATA
467
+ },
468
+ { example: 'evaluates set/blank? and set columns',
469
+ options: { first_match: true },
470
+ data: <<~DATA
471
+ set/blank? :country, in: type, guard:, set: class, out :PAID, out: len, if:
472
+ US, , , :class.upcase,
473
+ US, Equity, :CUSIP.present?, != PRIVATE, :CUSIP, :PAID.length, :len == 9
474
+ !=US, Equity, :ISIN.present?, != PRIVATE, :ISIN, :PAID.length, :len == 12
475
+ US, , :CUSIP.present?, PRIVATE, :CUSIP, :PAID.length,
476
+ !=US, , :ISIN.present?, PRIVATE, :ISIN, :PAID.length,
477
+ DATA
478
+ }
479
+ ]
480
+ examples.each do |test|
481
+ %i[decide decide!].each do |method|
482
+ it "#{method} correctly #{test[:example]}" do
483
+ table = CSVDecision.parse(test[:data], test[:options])
484
+
485
+ expect(table.send(method, CUSIP: '1234567890', class: 'Private')).to eq(PAID: '1234567890', len: 10)
486
+ expect(table.send(method, CUSIP: '123456789', type: 'Equity', class: 'Public')).to eq(PAID: '123456789', len: 9)
487
+ expect(table.send(method, ISIN: '123456789', country: 'GB', class: 'public')).to eq({})
488
+ expect(table.send(method, ISIN: '123456789012', country: 'GB', class: 'private')).to eq(PAID: '123456789012', len: 12)
489
+ end
490
+ end
491
+ end
492
+ end
454
493
  end
455
494
  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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Brett Vickers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-01-07 00:00:00.000000000 Z
11
+ date: 2018-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -179,6 +179,7 @@ files:
179
179
  - doc/CSVDecision/Data.html
180
180
  - doc/CSVDecision/Decide.html
181
181
  - doc/CSVDecision/Decision.html
182
+ - doc/CSVDecision/Defaults.html
182
183
  - doc/CSVDecision/Dictionary.html
183
184
  - doc/CSVDecision/Dictionary/Entry.html
184
185
  - doc/CSVDecision/Error.html
@@ -205,6 +206,8 @@ files:
205
206
  - doc/CSVDecision/ScanRow.html
206
207
  - doc/CSVDecision/Symbol.html
207
208
  - doc/CSVDecision/Table.html
209
+ - doc/CSVDecision/TableValidationError.html
210
+ - doc/CSVDecision/Validate.html
208
211
  - doc/_index.html
209
212
  - doc/class_list.html
210
213
  - doc/css/common.css
@@ -224,6 +227,7 @@ files:
224
227
  - lib/csv_decision/data.rb
225
228
  - lib/csv_decision/decide.rb
226
229
  - lib/csv_decision/decision.rb
230
+ - lib/csv_decision/defaults.rb
227
231
  - lib/csv_decision/dictionary.rb
228
232
  - lib/csv_decision/header.rb
229
233
  - lib/csv_decision/input.rb
@@ -241,6 +245,7 @@ files:
241
245
  - lib/csv_decision/result.rb
242
246
  - lib/csv_decision/scan_row.rb
243
247
  - lib/csv_decision/table.rb
248
+ - lib/csv_decision/validate.rb
244
249
  - spec/csv_decision/columns_spec.rb
245
250
  - spec/csv_decision/constant_spec.rb
246
251
  - spec/csv_decision/data_spec.rb