csv_decision 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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