csv_decision2 0.5.1

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 (134) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +3 -0
  3. data/.coveralls.yml +2 -0
  4. data/.gitignore +14 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +30 -0
  7. data/.travis.yml +6 -0
  8. data/CHANGELOG.md +85 -0
  9. data/Dockerfile +6 -0
  10. data/Gemfile +7 -0
  11. data/LICENSE +21 -0
  12. data/README.md +356 -0
  13. data/benchmarks/rufus_decision.rb +158 -0
  14. data/csv_decision2.gemspec +38 -0
  15. data/doc/CSVDecision/CellValidationError.html +143 -0
  16. data/doc/CSVDecision/Columns/Default.html +589 -0
  17. data/doc/CSVDecision/Columns/Dictionary.html +801 -0
  18. data/doc/CSVDecision/Columns/Entry.html +508 -0
  19. data/doc/CSVDecision/Columns.html +1259 -0
  20. data/doc/CSVDecision/Constant.html +254 -0
  21. data/doc/CSVDecision/Data.html +479 -0
  22. data/doc/CSVDecision/Decide.html +302 -0
  23. data/doc/CSVDecision/Decision.html +1011 -0
  24. data/doc/CSVDecision/Defaults.html +291 -0
  25. data/doc/CSVDecision/Dictionary/Entry.html +1147 -0
  26. data/doc/CSVDecision/Dictionary.html +426 -0
  27. data/doc/CSVDecision/Error.html +139 -0
  28. data/doc/CSVDecision/FileError.html +143 -0
  29. data/doc/CSVDecision/Function.html +240 -0
  30. data/doc/CSVDecision/Guard.html +245 -0
  31. data/doc/CSVDecision/Header.html +647 -0
  32. data/doc/CSVDecision/Index.html +741 -0
  33. data/doc/CSVDecision/Input.html +404 -0
  34. data/doc/CSVDecision/Load.html +296 -0
  35. data/doc/CSVDecision/Matchers/Constant.html +484 -0
  36. data/doc/CSVDecision/Matchers/Function.html +511 -0
  37. data/doc/CSVDecision/Matchers/Guard.html +503 -0
  38. data/doc/CSVDecision/Matchers/Matcher.html +507 -0
  39. data/doc/CSVDecision/Matchers/Numeric.html +415 -0
  40. data/doc/CSVDecision/Matchers/Pattern.html +491 -0
  41. data/doc/CSVDecision/Matchers/Proc.html +704 -0
  42. data/doc/CSVDecision/Matchers/Range.html +379 -0
  43. data/doc/CSVDecision/Matchers/Symbol.html +426 -0
  44. data/doc/CSVDecision/Matchers.html +1567 -0
  45. data/doc/CSVDecision/Numeric.html +259 -0
  46. data/doc/CSVDecision/Options.html +443 -0
  47. data/doc/CSVDecision/Parse.html +282 -0
  48. data/doc/CSVDecision/Paths.html +742 -0
  49. data/doc/CSVDecision/Result.html +1200 -0
  50. data/doc/CSVDecision/Scan/InputHashes.html +369 -0
  51. data/doc/CSVDecision/Scan.html +313 -0
  52. data/doc/CSVDecision/ScanRow.html +866 -0
  53. data/doc/CSVDecision/Symbol.html +256 -0
  54. data/doc/CSVDecision/Table.html +1470 -0
  55. data/doc/CSVDecision/TableValidationError.html +143 -0
  56. data/doc/CSVDecision/Validate.html +422 -0
  57. data/doc/CSVDecision.html +621 -0
  58. data/doc/_index.html +471 -0
  59. data/doc/class_list.html +51 -0
  60. data/doc/css/common.css +1 -0
  61. data/doc/css/full_list.css +58 -0
  62. data/doc/css/style.css +499 -0
  63. data/doc/file.README.html +421 -0
  64. data/doc/file_list.html +56 -0
  65. data/doc/frames.html +17 -0
  66. data/doc/index.html +421 -0
  67. data/doc/js/app.js +248 -0
  68. data/doc/js/full_list.js +216 -0
  69. data/doc/js/jquery.js +4 -0
  70. data/doc/method_list.html +1163 -0
  71. data/doc/top-level-namespace.html +110 -0
  72. data/docker-compose.yml +13 -0
  73. data/lib/csv_decision/columns.rb +192 -0
  74. data/lib/csv_decision/data.rb +92 -0
  75. data/lib/csv_decision/decision.rb +196 -0
  76. data/lib/csv_decision/defaults.rb +47 -0
  77. data/lib/csv_decision/dictionary.rb +180 -0
  78. data/lib/csv_decision/header.rb +83 -0
  79. data/lib/csv_decision/index.rb +107 -0
  80. data/lib/csv_decision/input.rb +121 -0
  81. data/lib/csv_decision/load.rb +36 -0
  82. data/lib/csv_decision/matchers/constant.rb +74 -0
  83. data/lib/csv_decision/matchers/function.rb +56 -0
  84. data/lib/csv_decision/matchers/guard.rb +142 -0
  85. data/lib/csv_decision/matchers/numeric.rb +44 -0
  86. data/lib/csv_decision/matchers/pattern.rb +94 -0
  87. data/lib/csv_decision/matchers/range.rb +95 -0
  88. data/lib/csv_decision/matchers/symbol.rb +149 -0
  89. data/lib/csv_decision/matchers.rb +220 -0
  90. data/lib/csv_decision/options.rb +124 -0
  91. data/lib/csv_decision/parse.rb +165 -0
  92. data/lib/csv_decision/paths.rb +78 -0
  93. data/lib/csv_decision/result.rb +204 -0
  94. data/lib/csv_decision/scan.rb +117 -0
  95. data/lib/csv_decision/scan_row.rb +142 -0
  96. data/lib/csv_decision/table.rb +101 -0
  97. data/lib/csv_decision/validate.rb +85 -0
  98. data/lib/csv_decision.rb +45 -0
  99. data/spec/csv_decision/columns_spec.rb +251 -0
  100. data/spec/csv_decision/constant_spec.rb +36 -0
  101. data/spec/csv_decision/data_spec.rb +50 -0
  102. data/spec/csv_decision/decision_spec.rb +19 -0
  103. data/spec/csv_decision/examples_spec.rb +242 -0
  104. data/spec/csv_decision/index_spec.rb +58 -0
  105. data/spec/csv_decision/input_spec.rb +55 -0
  106. data/spec/csv_decision/load_spec.rb +28 -0
  107. data/spec/csv_decision/matchers/function_spec.rb +82 -0
  108. data/spec/csv_decision/matchers/guard_spec.rb +170 -0
  109. data/spec/csv_decision/matchers/numeric_spec.rb +47 -0
  110. data/spec/csv_decision/matchers/pattern_spec.rb +183 -0
  111. data/spec/csv_decision/matchers/range_spec.rb +70 -0
  112. data/spec/csv_decision/matchers/symbol_spec.rb +67 -0
  113. data/spec/csv_decision/options_spec.rb +94 -0
  114. data/spec/csv_decision/parse_spec.rb +44 -0
  115. data/spec/csv_decision/table_spec.rb +683 -0
  116. data/spec/csv_decision_spec.rb +7 -0
  117. data/spec/data/invalid/empty.csv +0 -0
  118. data/spec/data/invalid/invalid_header1.csv +4 -0
  119. data/spec/data/invalid/invalid_header2.csv +4 -0
  120. data/spec/data/invalid/invalid_header3.csv +4 -0
  121. data/spec/data/invalid/invalid_header4.csv +4 -0
  122. data/spec/data/valid/benchmark_regexp.csv +10 -0
  123. data/spec/data/valid/index_example.csv +13 -0
  124. data/spec/data/valid/multi_column_index.csv +10 -0
  125. data/spec/data/valid/multi_column_index2.csv +12 -0
  126. data/spec/data/valid/options_in_file1.csv +5 -0
  127. data/spec/data/valid/options_in_file2.csv +5 -0
  128. data/spec/data/valid/options_in_file3.csv +13 -0
  129. data/spec/data/valid/regular_expressions.csv +11 -0
  130. data/spec/data/valid/simple_constants.csv +5 -0
  131. data/spec/data/valid/simple_example.csv +10 -0
  132. data/spec/data/valid/valid.csv +4 -0
  133. data/spec/spec_helper.rb +106 -0
  134. metadata +352 -0
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/csv_decision'
4
+
5
+ SPEC_DATA_VALID ||= File.join(CSVDecision.root, 'spec', 'data', 'valid')
6
+ SPEC_DATA_INVALID ||= File.join(CSVDecision.root, 'spec', 'data', 'invalid')
7
+
8
+ describe CSVDecision::Columns do
9
+ describe '#new' do
10
+ # it 'creates a columns object' do
11
+ # table = CSVDecision::Table.new
12
+ # columns = CSVDecision::Columns.new(table)
13
+ #
14
+ # expect(columns).to be_a(CSVDecision::Columns)
15
+ # end
16
+ end
17
+
18
+ it 'rejects a duplicate output column name' do
19
+ data = <<~DATA
20
+ IN :input, OUT :output, IN/text : input, OUT/text:output
21
+ input0, output0, input1, output1
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
33
+ table = CSVDecision.parse(data)
34
+
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, 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
+
41
+ expect(table.columns.dictionary).to eq(input: :in, output: 1, output2: 3)
42
+ end
43
+
44
+ it 'recognises all input and output column symbols' do
45
+ data = <<~DATA
46
+ IN :input, OUT :output, IN/text :input, OUT/text:output2, out: len, guard:
47
+ input0, output0, input1, output1, :input2.length,
48
+ input1, output1, input1, output2, :input3.length, :input4.present?
49
+ DATA
50
+ table = CSVDecision.parse(data)
51
+
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, 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
+
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
+
61
+ expect(table.columns.dictionary)
62
+ .to eq(input: :in, output: 1, output2: 3, len: 4, input2: :in, input3: :in, input4: :in)
63
+
64
+ expect(table.columns.input_keys).to eq %i[input input2 input4 input3]
65
+ end
66
+
67
+ it 'recognises the output symbol referenced by an output function' do
68
+ data = <<~DATA
69
+ IN :input, OUT :output, IN/text :input, OUT/text:output2, out: input3, out: len
70
+ input0, output0, input1, output1, , :input2.length
71
+ input1, output1, input1, output2, :input4.present?, :input3.length
72
+ DATA
73
+
74
+ table = CSVDecision.parse(data)
75
+
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, 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
+
84
+ expect(table.columns.dictionary)
85
+ .to eq(input: :in, output: 1, output2: 3, len: 5, input2: :in, input3: 4, input4: :in)
86
+
87
+ expect(table.columns.input_keys).to eq %i[input input2 input4]
88
+ end
89
+
90
+ it 'raises an error for an output column referring to a later output column' do
91
+ data = <<~DATA
92
+ IN :input, OUT :output, IN/text :input, OUT/text:output2, out: len, out: input3,
93
+ input0, output0, input1, output1, :input2.length,
94
+ input1, output1, input1, output2, :input3.length :input4.upcase
95
+ DATA
96
+
97
+ expect { CSVDecision.parse(data) }
98
+ .to raise_error(
99
+ CSVDecision::CellValidationError,
100
+ "output column 'len' makes an out of order reference to output column 'input3'"
101
+ )
102
+ end
103
+
104
+ it 'raises an error for an output column referring to itself' do
105
+ data = <<~DATA
106
+ IN :input, OUT :output, IN/text :input, OUT/text:output2, out: len, out: input3,
107
+ input0, output0, input1, output1, :len.length,
108
+ input1, output1, input1, output2, :len.length :input4.upcase
109
+ DATA
110
+
111
+ expect { CSVDecision.parse(data) }
112
+ .to raise_error(CSVDecision::CellValidationError,
113
+ "output column 'len' makes reference to itself")
114
+ end
115
+
116
+ it 'parses a decision table columns from a CSV file' do
117
+ file = Pathname(File.join(SPEC_DATA_VALID, 'valid.csv'))
118
+ result = CSVDecision.parse(file)
119
+
120
+ expect(result.columns).to be_a(CSVDecision::Columns)
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
+ end
126
+
127
+ it 'rejects an invalid header column' do
128
+ data = [
129
+ ['IN :input', 'BAD :output'],
130
+ ['input', '']
131
+ ]
132
+
133
+ expect { CSVDecision.parse(data) }
134
+ .to raise_error(CSVDecision::CellValidationError,
135
+ "header column 'BAD :output' is not valid as " \
136
+ 'the column name is not well formed')
137
+ end
138
+
139
+ it 'rejects a missing column name' do
140
+ data = [
141
+ ['IN :input', 'IN: '],
142
+ ['input', '']
143
+ ]
144
+
145
+ expect { CSVDecision.parse(data) }
146
+ .to raise_error(CSVDecision::CellValidationError,
147
+ "header column 'IN:' is not valid as the column name is missing")
148
+ end
149
+
150
+ it 'rejects an invalid column name' do
151
+ data = [
152
+ ['IN :input', 'IN: a-b'],
153
+ ['input', '']
154
+ ]
155
+
156
+ expect { CSVDecision.parse(data) }
157
+ .to raise_error(CSVDecision::CellValidationError,
158
+ "header column 'IN: a-b' is not valid as " \
159
+ "the column name 'a-b' contains invalid characters")
160
+ end
161
+
162
+ context 'rejects invalid CSV decision table columns' do
163
+ Dir[File.join(SPEC_DATA_INVALID, 'invalid_header*.csv')].each do |file_name|
164
+ pathname = Pathname(file_name)
165
+
166
+ it "rejects CSV file #{pathname.basename}" do
167
+ expect { CSVDecision.parse(pathname) }
168
+ .to raise_error(CSVDecision::FileError, /\Aerror processing CSV file/)
169
+ end
170
+ end
171
+ end
172
+
173
+ it 'recognises the guard column' do
174
+ data = <<~DATA
175
+ IN :country, guard:, out :PAID, out :PAID_type
176
+ US, :CUSIP.present?, :CUSIP, CUSUP
177
+ GB, :SEDOL.present?, :SEDOL, SEDOL
178
+ DATA
179
+ table = CSVDecision.parse(data)
180
+
181
+ expect(table.columns.ins[1].to_h)
182
+ .to eq(name: nil, eval: true, type: :guard, set_if: nil)
183
+
184
+ expect(table.columns.input_keys).to eq %i[country CUSIP SEDOL]
185
+ end
186
+
187
+ it 'rejects output column being same as input column' do
188
+ data = <<~DATA
189
+ IN :country, guard:, out :PAID, out :country
190
+ US, :CUSIP.present?, :CUSIP, CUSUP
191
+ GB, :SEDOL.present?, :SEDOL, SEDOL
192
+ DATA
193
+
194
+ expect { CSVDecision.parse(data) }
195
+ .to raise_error(CSVDecision::CellValidationError,
196
+ "output column name 'country' is also an input column")
197
+ end
198
+
199
+ it 'rejects output column being same as an input symbol not in the header' do
200
+ data = <<~DATA
201
+ in :parent, out :node
202
+ ==:node, top
203
+ , child
204
+ DATA
205
+ expect { CSVDecision.parse(data) }
206
+ .to raise_error(CSVDecision::CellValidationError,
207
+ "output column name 'node' is also an input column")
208
+ end
209
+
210
+ it 'recognises the if: column' do
211
+ data = <<~DATA
212
+ in :country, out :PAID, out :PAID_type, if:
213
+ US, :CUSIP, CUSIP, :PAID.present?
214
+ GB, :SEDOL, SEDOL, :PAID.present?
215
+ DATA
216
+ table = CSVDecision.parse(data)
217
+
218
+ expect(table.columns.ifs[3].to_h).to eq(name: 3, eval: true, type: :if, set_if: nil)
219
+ expect(table.columns.input_keys).to eq %i[country CUSIP SEDOL]
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
237
+
238
+ it 'recognises the path: columns' do
239
+ data = <<~DATA
240
+ path:, path:, in :type_cd, out :value, if:
241
+ header, , !nil?, :type_cd, :value.present?
242
+ payload, , !nil?, :type_cd, :value.present?
243
+ payload, ref_data, , :type_id, :value.present?
244
+ DATA
245
+ table = CSVDecision.parse(data)
246
+
247
+ expect(table.columns.input_keys).to eq %i[type_cd type_id]
248
+ expect(table.columns.paths[0].to_h).to eq(name: nil, eval: false, type: :path, set_if: nil)
249
+ expect(table.columns.paths[1].to_h).to eq(name: nil, eval: false, type: :path, set_if: nil)
250
+ end
251
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/csv_decision'
4
+
5
+ describe CSVDecision::Matchers::Constant do
6
+ describe '#matches?' do
7
+ context 'constant matches value' do
8
+ data = [
9
+ ['= 1', 1],
10
+ ['== 1', 1],
11
+ [':=1', 1],
12
+ ['==.1', BigDecimal('0.1')],
13
+ [':= 1.1', BigDecimal('1.1')]
14
+ ]
15
+
16
+ data.each do |cell, value|
17
+ it "constant #{cell} matches #{value}" do
18
+ proc = described_class.matches?(cell)
19
+ expect(proc).to be_a(CSVDecision::Matchers::Proc)
20
+ expect(proc.type).to eq :constant
21
+ expect(proc.function).to eq value
22
+ end
23
+ end
24
+ end
25
+
26
+ context 'does not match strings and non-constants' do
27
+ data = ['true', 'nil', 'false', ':column', '> 0', '!= 1.0', 'abc.*def', '-1..1', '0...3']
28
+
29
+ data.each do |cell|
30
+ it "cell #{cell} is not a non-string constant}" do
31
+ expect(described_class.matches?(cell)).to eq false
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+ require_relative '../../lib/csv_decision'
3
+
4
+ describe CSVDecision::Data do
5
+ it 'parses an CSV string' do
6
+ result = CSVDecision::Data.to_array(data: '')
7
+ expect(result).to be_a Array
8
+ expect(result.empty?).to eq true
9
+ end
10
+
11
+ it 'parses an array' do
12
+ result = CSVDecision::Data.to_array(data: [[]])
13
+ expect(result).to eq []
14
+
15
+ data = [
16
+ ['#header', "R\u00E9sum\u00E9", '# comments'],
17
+ ['IN :input', ' OUT :output ', nil],
18
+ ['input', '# comment', nil]
19
+ ]
20
+ result = CSVDecision::Data.to_array(data: data)
21
+ expect(result).to eq [['IN :input', 'OUT :output', ''], ['input', '', '']]
22
+ end
23
+
24
+ it 'parses a CSV file' do
25
+ file = Pathname(File.join(CSVDecision.root, 'spec/data/valid', 'valid.csv'))
26
+ result = CSVDecision::Data.to_array(data: file)
27
+ expected = [
28
+ ['', 'IN :input', '', 'OUT :output', ''],
29
+ ['', 'input', '', '', '']
30
+ ]
31
+ expect(result).to eq(expected)
32
+
33
+ file = Pathname(File.join(CSVDecision.root, 'spec/data/valid', 'options_in_file2.csv'))
34
+ result = CSVDecision::Data.to_array(data: file)
35
+ expected = [
36
+ ['accumulate'],
37
+ ['regexp_implicit'],
38
+ ['IN :input', 'OUT :output'],
39
+ ['input', '']
40
+ ]
41
+ expect(result).to eq(expected)
42
+ end
43
+
44
+ it 'raises an error for invalid input' do
45
+ expect { CSVDecision::Data.to_array(data: {}) }
46
+ .to raise_error(ArgumentError,
47
+ 'Hash input invalid; ' \
48
+ 'input must be a file path name, CSV string or array of arrays')
49
+ end
50
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/csv_decision'
4
+
5
+ describe CSVDecision::Decision do
6
+ it 'decision for table with no functions and first_match: true' do
7
+ data = <<~DATA
8
+ IN :input, OUT :output, IN: input1
9
+ input0, output0, input1
10
+ input0, output1,
11
+ DATA
12
+
13
+ table = CSVDecision.parse(data)
14
+
15
+ decision = CSVDecision::Decision.new(table: table)
16
+
17
+ expect(decision).to be_a(CSVDecision::Decision)
18
+ end
19
+ end
@@ -0,0 +1,242 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/csv_decision'
4
+
5
+ context 'simple examples' do
6
+ context 'simple example - strings-only' do
7
+ data = <<~DATA
8
+ in :topic, in :region, out :team_member
9
+ sports, Europe, Alice
10
+ sports, , Bob
11
+ finance, America, Charlie
12
+ finance, Europe, Donald
13
+ finance, , Ernest
14
+ politics, Asia, Fujio
15
+ politics, America, Gilbert
16
+ politics, , Henry
17
+ , , Zach
18
+ DATA
19
+
20
+ it 'makes correct decisions for CSV string' do
21
+ table = CSVDecision.parse(data)
22
+
23
+ result = table.decide(topic: 'finance', region: 'Europe')
24
+ expect(result).to eq(team_member: 'Donald')
25
+
26
+ result = table.decide(topic: 'sports', region: nil)
27
+ expect(result).to eq(team_member: 'Bob')
28
+
29
+ result = table.decide(topic: 'culture', region: 'America')
30
+ expect(result).to eq(team_member: 'Zach')
31
+ end
32
+
33
+ it 'makes correct decisions for CSV file' do
34
+ table = CSVDecision.parse(Pathname('spec/data/valid/simple_example.csv'))
35
+
36
+ result = table.decide(topic: 'finance', region: 'Europe')
37
+ expect(result).to eq(team_member: 'Donald')
38
+
39
+ result = table.decide(topic: 'sports', region: nil)
40
+ expect(result).to eq(team_member: 'Bob')
41
+
42
+ result = table.decide(topic: 'culture', region: 'America')
43
+ expect(result).to eq(team_member: 'Zach')
44
+ end
45
+ end
46
+
47
+ context 'simple example - constants' do
48
+ data = <<~DATA
49
+ in :constant, out :value
50
+ :=nil, :=nil
51
+ ==false, ==false
52
+ =true, =true
53
+ = 0, = 0
54
+ :=100.0, :=100.0
55
+ DATA
56
+
57
+ it 'makes correct decisions for CSV string' do
58
+ table = CSVDecision.parse(data)
59
+
60
+ result = table.decide(constant: nil)
61
+ expect(result).to eq(value: nil)
62
+
63
+ result = table.decide(constant: true)
64
+ expect(result).to eq(value: true)
65
+
66
+ result = table.decide(constant: false)
67
+ expect(result).to eq(value: false)
68
+
69
+ result = table.decide(constant: 0)
70
+ expect(result).to eq(value: 0)
71
+
72
+ result = table.decide(constant: BigDecimal('100.0'))
73
+ expect(result).to eq(value: BigDecimal('100.0'))
74
+ end
75
+ end
76
+
77
+ context 'simple example - symbols' do
78
+ data = <<~DATA
79
+ in :node, in :parent, out :top?
80
+ , ==:node, yes
81
+ , , no
82
+ DATA
83
+
84
+ it 'makes correct decisions' do
85
+ table = CSVDecision.parse(data)
86
+
87
+ result = table.decide(node: 0, parent: 0)
88
+ expect(result).to eq(top?: 'yes')
89
+
90
+ result = table.decide(node: 1, parent: 0)
91
+ expect(result).to eq(top?: 'no')
92
+
93
+ result = table.decide(node: '0', parent: 0)
94
+ expect(result).to eq(top?: 'no')
95
+ end
96
+ end
97
+
98
+ context 'simple example - column symbols not in header' do
99
+ data = <<~DATA
100
+ in :parent, out :top?
101
+ ==:node, yes
102
+ , no
103
+ DATA
104
+
105
+ it 'makes correct decisions' do
106
+ table = CSVDecision.parse(data)
107
+
108
+ result = table.decide(node: 0, parent: 0)
109
+ expect(result).to eq(top?: 'yes')
110
+
111
+ result = table.decide(node: 1, parent: 0)
112
+ expect(result).to eq(top?: 'no')
113
+
114
+ result = table.decide(node: '0', parent: 0)
115
+ expect(result).to eq(top?: 'no')
116
+ end
117
+ end
118
+
119
+ it 'makes correct decision for table with symbol ordered compares' do
120
+ data = <<~DATA
121
+ in :traded, in :settled, out :status
122
+ , :traded, same day
123
+ , >:traded, pending
124
+ , <:traded, invalid trade
125
+ , , invalid data
126
+ DATA
127
+ table = CSVDecision.parse(data)
128
+
129
+ expect(table.columns.input_keys).to eq %i[traded settled]
130
+
131
+ expect(table.decide(traded: '20171227', settled: '20171227')).to eq(status: 'same day')
132
+ expect(table.decide(traded: 20171227, settled: 20171227 )).to eq(status: 'same day')
133
+ expect(table.decide(traded: '20171227', settled: '20171228')).to eq(status: 'pending')
134
+ expect(table.decide(traded: 20171227, settled: 20171228 )).to eq(status: 'pending')
135
+ expect(table.decide(traded: '20171228', settled: '20171227')).to eq(status: 'invalid trade')
136
+ expect(table.decide(traded: 20171228, settled: 20171227 )).to eq(status: 'invalid trade')
137
+ expect(table.decide(traded: '20171227', settled: 20171228 )).to eq(status: 'invalid data')
138
+ end
139
+
140
+ it 'makes a correct decision using a guard column' do
141
+ data = <<~DATA
142
+ in :country, guard:, out :ID, out :ID_type, out :len
143
+ US, :CUSIP.present?, :CUSIP, CUSIP, :ID.length
144
+ GB, :SEDOL.present?, :SEDOL, SEDOL, :ID.length
145
+ , :ISIN.present?, :ISIN, ISIN, :ID.length
146
+ , :SEDOL.present?, :SEDOL, SEDOL, :ID.length
147
+ , :CUSIP.present?, :CUSIP, CUSIP, :ID.length
148
+ , , := nil, := nil, := nil
149
+ DATA
150
+
151
+ table = CSVDecision.parse(data)
152
+
153
+ expect(table.decide(country: 'US', CUSIP: '123456789'))
154
+ .to eq(ID: '123456789', ID_type: 'CUSIP', len: 9)
155
+ expect(table.decide(country: 'EU', CUSIP: '123456789', ISIN:'123456789012'))
156
+ .to eq(ID: '123456789012', ID_type: 'ISIN', len: 12)
157
+ end
158
+
159
+ it 'makes a correct decision using an if column' do
160
+ data = <<~DATA
161
+ in :country, guard:, out :ID, out :ID_type, out :len, if:
162
+ US, :CUSIP.present?, :CUSIP, CUSIP8, :ID.length, :len == 8
163
+ US, :CUSIP.present?, :CUSIP, CUSIP9, :ID.length, :len == 9
164
+ US, :CUSIP.present?, :CUSIP, DUMMY, :ID.length,
165
+ , :ISIN.present?, :ISIN, ISIN, :ID.length, :len == 12
166
+ , :ISIN.present?, :ISIN, DUMMY, :ID.length,
167
+ , :CUSIP.present?, :CUSIP, DUMMY, :ID.length,
168
+ DATA
169
+
170
+ table = CSVDecision.parse(data)
171
+
172
+ expect(table.decide(country: 'US', CUSIP: '12345678'))
173
+ .to eq(ID: '12345678', ID_type: 'CUSIP8', len: 8)
174
+ expect(table.decide(country: 'US', CUSIP: '123456789'))
175
+ .to eq(ID: '123456789', ID_type: 'CUSIP9', len: 9)
176
+ expect(table.decide(country: 'US', CUSIP: '1234567890'))
177
+ .to eq(ID: '1234567890', ID_type: 'DUMMY', len: 10)
178
+ expect(table.decide(country: nil, CUSIP: '123456789', ISIN:'123456789012'))
179
+ .to eq(ID: '123456789012', ID_type: 'ISIN', len: 12)
180
+ expect(table.decide(CUSIP: '12345678', ISIN:'1234567890'))
181
+ .to eq(ID: '1234567890', ID_type: 'DUMMY', len: 10)
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
200
+
201
+ it 'recognises in column method call conditions' do
202
+ data = <<~DATA
203
+ set/nil? :country, in :CUSIP, in :ISIN, set: class, out :PAID, out: len, if:
204
+ US, , , :class.upcase,
205
+ US, .present?, , != PRIVATE, :CUSIP, :PAID.length, :len == 9
206
+ !=US, , .present?, != PRIVATE, :ISIN, :PAID.length, :len == 12
207
+ US, =.present?, , PRIVATE, :CUSIP, :PAID.length,
208
+ !=US, , !.blank?, PRIVATE, :ISIN, :PAID.length,
209
+ DATA
210
+
211
+ table = CSVDecision.parse(data)
212
+ expect(table.decide(CUSIP: '1234567890', class: 'Private')).to eq(PAID: '1234567890', len: 10)
213
+ expect(table.decide(CUSIP: '123456789', class: 'Public')).to eq(PAID: '123456789', len: 9)
214
+ expect(table.decide(ISIN: '123456789', country: 'GB', class: 'public')).to eq({})
215
+ expect(table.decide(ISIN: '123456789012', country: 'GB', class: 'private')).to eq(PAID: '123456789012', len: 12)
216
+ end
217
+
218
+ it 'scans the input hash paths accumulating matches' do
219
+ data = <<~DATA
220
+ path:, path:, out :value
221
+ header, , :source_name
222
+ header, metrics, :service_name
223
+ payload, , :amount
224
+ payload, ref_data, :account_id
225
+ DATA
226
+ table = CSVDecision.parse(data, first_match: false)
227
+
228
+ input = {
229
+ header: {
230
+ id: 1, type_cd: 'BUY', source_name: 'Client', client_name: 'AAPL',
231
+ metrics: { service_name: 'Trading', receive_time: '12:00' }
232
+ },
233
+ payload: {
234
+ tran_id: 9, amount: '100.00',
235
+ ref_data: { account_id: '5010', type_id: 'BUYL' }
236
+ }
237
+ }
238
+
239
+ expect(table.decide(input)).to eq(value: %w[Client Trading 100.00 5010])
240
+ expect(table.decide!(input)).to eq(value: %w[Client Trading 100.00 5010])
241
+ end
242
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../lib/csv_decision'
4
+
5
+ SPEC_DATA_VALID ||= File.join(CSVDecision.root, 'spec', 'data', 'valid')
6
+ SPEC_DATA_INVALID ||= File.join(CSVDecision.root, 'spec', 'data', 'invalid')
7
+
8
+ describe CSVDecision::Index do
9
+ it 'indexes a single column CSV' do
10
+ file = Pathname(File.join(SPEC_DATA_VALID, 'options_in_file3.csv'))
11
+ result = CSVDecision.parse(file)
12
+
13
+ expected = {
14
+ 'none' => 0,
15
+ 'one' => 1,
16
+ 'two' => 2,
17
+ 'three' => 3,
18
+ nil => 4,
19
+ 0 => 5,
20
+ 1 => 6,
21
+ 2 => 7,
22
+ 3 => 8
23
+ }
24
+
25
+ expect(result.index.columns).to eq [0]
26
+ expect(result.index.hash).to eql expected
27
+ end
28
+
29
+ it 'indexes two columns with contiguous values' do
30
+ file = Pathname(File.join(SPEC_DATA_VALID, 'multi_column_index.csv'))
31
+ result = CSVDecision.parse(file)
32
+
33
+ expected = {
34
+ %w[integer none] => [[0, 1]],
35
+ %w[integer one] => [[2, 3]],
36
+ %w[string none] => [[4, 5]],
37
+ %w[string one] => [[6, 7]]
38
+ }
39
+
40
+ expect(result.index.columns).to eq [1, 2]
41
+ expect(result.index.hash).to eql expected
42
+ end
43
+
44
+ it 'indexes two columns with non-contiguous values' do
45
+ file = Pathname(File.join(SPEC_DATA_VALID, 'multi_column_index2.csv'))
46
+ result = CSVDecision.parse(file)
47
+
48
+ expected = {
49
+ %w[integer none] => [0, 8],
50
+ %w[string none] => [[1, 2]],
51
+ %w[string one] => [3, [6, 7]],
52
+ %w[integer one] => [[4, 5]]
53
+ }
54
+
55
+ expect(result.index.columns).to eq [1, 2]
56
+ expect(result.index.hash).to eql expected
57
+ end
58
+ end