csv_decision2 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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,683 @@
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
+
7
+ describe CSVDecision::Table do
8
+ describe '#decide' do
9
+ context 'makes correct decisions for simple, text-only tables' do
10
+ examples = [
11
+ {
12
+ example: 'parses CSV file',
13
+ options: {},
14
+ data: Pathname(File.join(SPEC_DATA_VALID, 'simple_example.csv'))
15
+ },
16
+ {
17
+ example: 'parses data array',
18
+ options: {},
19
+ data: [
20
+ ['in :topic', 'in :region', 'out :team member'],
21
+ ['sports', 'Europe', 'Alice'],
22
+ ['sports', '', 'Bob'],
23
+ ['finance', 'America', 'Charlie'],
24
+ ['finance', 'Europe', 'Donald'],
25
+ ['finance', '', 'Ernest'],
26
+ ['politics', 'Asia', 'Fujio'],
27
+ ['politics', 'America', 'Gilbert'],
28
+ ['politics', '', 'Henry'],
29
+ ['', '', 'Zach']
30
+ ]
31
+ },
32
+ ]
33
+ examples.each do |test|
34
+ %i[decide decide!].each do |method|
35
+ it "#{method} correctly #{test[:example]} with first_match: true" do
36
+ options = test[:options].merge(first_match: true)
37
+ table = CSVDecision.parse(test[:data], options)
38
+
39
+ expect(table.send(method, topic: 'finance', region: 'Europe')).to eq(team_member: 'Donald')
40
+ expect(table.send(method, topic: 'sports', region: nil)).to eq(team_member: 'Bob')
41
+ expect(table.send(method, topic: 'culture', region: 'America')).to eq(team_member: 'Zach')
42
+ end
43
+
44
+ it "#{method} correctly #{test[:example]} with first_match: false" do
45
+ options = test[:options].merge(first_match: false)
46
+ table = CSVDecision.parse(test[:data], options)
47
+
48
+ expect(table.send(method, topic: 'finance', region: 'Europe'))
49
+ .to eq(team_member: %w[Donald Ernest Zach])
50
+ expect(table.send(method, topic: 'sports', region: nil))
51
+ .to eq(team_member: %w[Bob Zach])
52
+ expect(table.send(method, topic: 'culture', region: 'America'))
53
+ .to eq(team_member: 'Zach')
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ context 'makes correct decisions for simple non-string constants' do
60
+ examples = [
61
+ {
62
+ example: 'parses CSV file',
63
+ options: {},
64
+ data: Pathname(File.join(SPEC_DATA_VALID, 'simple_constants.csv'))
65
+ },
66
+ {
67
+ example: 'parses CSV string',
68
+ options: {},
69
+ data: <<~DATA
70
+ in :constant, out :type
71
+ :=nil, :=nil
72
+ = 0, = 0
73
+ :=100.0, :=100
74
+ , Unrecognized
75
+ DATA
76
+ },
77
+ ]
78
+ examples.each do |test|
79
+ %i[decide decide!].each do |method|
80
+ it "#{method} correctly #{test[:example]} with first_match: true" do
81
+ options = test[:options].merge(first_match: true)
82
+ table = CSVDecision.parse(test[:data], options)
83
+
84
+ expect(table.send(method, constant: nil)).to eq(type: nil)
85
+ expect(table.send(method, constant: 0)).to eq(type: 0)
86
+ expect(table.send(method, constant: BigDecimal('100.0'))).to eq(type: BigDecimal('100.0'))
87
+ expect(table.send(method, constant: ':=nil')).to eq(type: 'Unrecognized')
88
+ expect(table.send(method, constant: '= 0')).to eq(type: 'Unrecognized')
89
+ expect(table.send(method, constant: ':=100.0')).to eq(type: 'Unrecognized')
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ context 'makes correct decisions for a table with regexps and ranges' do
96
+ examples = [
97
+ {
98
+ example: 'implicit regular expressions from CSV file',
99
+ options: {},
100
+ data: Pathname('spec/data/valid/regular_expressions.csv')
101
+ },
102
+ {
103
+ example: 'implicit regular expressions',
104
+ options: { regexp_implicit: true },
105
+ data: <<~DATA
106
+ in :age, in :trait, out :salesperson
107
+ 18..35, maniac, Adelsky
108
+ 23..40, bad|maniac, Bronco
109
+ 36..50, bad.*, Espadas
110
+ := 100, , Thorsten
111
+ 44..100, !~ maniac, Ojiisan
112
+ > 100, maniac.*, Chester
113
+ 23..35, .*rich, Kerfelden
114
+ , cheerful, Swanson
115
+ , maniac, Korolev
116
+ DATA
117
+ },
118
+ {
119
+ example: 'explicit regular expressions',
120
+ options: { regexp_implicit: false },
121
+ data: <<~DATA
122
+ in :age, in :trait, out :salesperson
123
+ 18..35, maniac, Adelsky
124
+ 23..40, =~ bad|maniac, Bronco
125
+ 36..50, =~ bad.*, Espadas
126
+ ==100, , Thorsten
127
+ 44..100, !~ maniac, Ojiisan
128
+ > 100, =~ maniac.*, Chester
129
+ 23..35, =~ .*rich, Kerfelden
130
+ , cheerful, Swanson
131
+ , maniac, Korolev
132
+ DATA
133
+ },
134
+ {
135
+ example: 'guard condition',
136
+ options: { regexp_implicit: false },
137
+ data: <<~DATA
138
+ in :age, guard:, out :salesperson
139
+ 18..35, :trait == maniac, Adelsky
140
+ 23..40, :trait =~ bad|maniac, Bronco
141
+ 36..50, :trait =~ bad.*, Espadas
142
+ ==100, , Thorsten
143
+ 44..100, :trait !~ maniac, Ojiisan
144
+ > 100, :trait =~ maniac.*, Chester
145
+ 23..35, :trait =~ .*rich, Kerfelden
146
+ , :trait == cheerful, Swanson
147
+ , :trait == maniac, Korolev
148
+ DATA
149
+ },
150
+ {
151
+ example: 'multiple in column references',
152
+ options: { regexp_implicit: false },
153
+ data: <<~DATA
154
+ in :age, in :age, in :trait, out :salesperson
155
+ >= 18, <= 35, maniac, Adelsky
156
+ >= 23, <= 40, =~ bad|maniac, Bronco
157
+ >= 36, <= 50, =~ bad.*, Espadas
158
+ == 100, , , Thorsten
159
+ >= 44, <= 100, != maniac, Ojiisan
160
+ > 100, , =~ maniac.*, Chester
161
+ >= 23, <= 35, =~ .*rich, Kerfelden
162
+ , , cheerful, Swanson
163
+ , , maniac, Korolev
164
+ DATA
165
+ },
166
+ ]
167
+ examples.each do |test|
168
+ %i[decide decide!].each do |method|
169
+ it "#{method} correctly uses #{test[:example]} with first_match: true" do
170
+ options = test[:options].merge(first_match: true)
171
+ table = CSVDecision.parse(test[:data], options)
172
+
173
+ expect(table.send(method, age: 100)).to eq(salesperson: 'Thorsten')
174
+ expect(table.send(method, age: 25, trait: 'very rich')).to eq(salesperson: 'Kerfelden')
175
+ expect(table.send(method, age: 25, trait: 'maniac')).to eq(salesperson: 'Adelsky')
176
+ expect(table.send(method, age: 44, trait: 'maniac')).to eq(salesperson: 'Korolev')
177
+ expect(table.send(method, age: 101, trait: 'maniacal')).to eq(salesperson: 'Chester')
178
+ expect(table.send(method, age: 44, trait: 'cheerful')).to eq(salesperson: 'Ojiisan')
179
+ expect(table.send(method, age: 49, trait: 'bad')).to eq(salesperson: 'Espadas')
180
+ expect(table.send(method, age: '40', trait: 'maniac')).to eq(salesperson: 'Bronco')
181
+ end
182
+
183
+ it "#{method} correctly uses #{test[:example]} with first_match: false" do
184
+ options = test[:options].merge(first_match: false)
185
+ table = CSVDecision.parse(test[:data], options)
186
+
187
+ expect(table.send(method, age: 100))
188
+ .to eq(salesperson: %w[Thorsten Ojiisan])
189
+ expect(table.send(method, age: 25, trait: 'very rich'))
190
+ .to eq(salesperson: 'Kerfelden')
191
+ expect(table.send(method, age: 25, trait: 'maniac'))
192
+ .to eq(salesperson: %w[Adelsky Bronco Korolev])
193
+ expect(table.send(method, age: 44, trait: 'maniac'))
194
+ .to eq(salesperson: 'Korolev')
195
+ expect(table.send(method, age: 101, trait: 'maniacal'))
196
+ .to eq(salesperson: 'Chester')
197
+ expect(table.send(method, age: 45, trait: 'cheerful'))
198
+ .to eq(salesperson: %w[Ojiisan Swanson])
199
+ expect(table.send(method, age: 49, trait: 'bad'))
200
+ .to eq(salesperson: %w[Espadas Ojiisan])
201
+ expect(table.send(method, age: '40', trait: 'maniac'))
202
+ .to eq(salesperson: %w[Bronco Korolev])
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ context 'makes correct decision for table with symbol equality compares' do
209
+ examples = [
210
+ { example: 'uses == :node',
211
+ options: {},
212
+ data: <<~DATA
213
+ in :node, in :parent, out :top?
214
+ , ==:node, yes
215
+ , , no
216
+ DATA
217
+ },
218
+ { example: 'uses :node',
219
+ options: {},
220
+ data: <<~DATA
221
+ in :node, in :parent, out :top?
222
+ , :node, yes
223
+ , , no
224
+ DATA
225
+ },
226
+ { example: 'uses := :node',
227
+ options: {},
228
+ data: <<~DATA
229
+ in :node, in :parent, out :top?
230
+ , := :node, yes
231
+ , , no
232
+ DATA
233
+ },
234
+ { example: 'uses = :node',
235
+ options: {},
236
+ data: <<~DATA
237
+ in :node, in :parent, out :top?
238
+ , = :node, yes
239
+ , , no
240
+ DATA
241
+ },
242
+ { example: 'uses :node, drops :node input column',
243
+ options: {},
244
+ data: <<~DATA
245
+ in :parent, out :top?
246
+ :node, yes
247
+ , no
248
+ DATA
249
+ },
250
+ { example: 'uses :parent, drops :parent input column',
251
+ options: {},
252
+ data: <<~DATA
253
+ in :node, out :top?
254
+ :parent, yes
255
+ , no
256
+ DATA
257
+ },
258
+ { example: 'uses ==:parent & != :parent',
259
+ options: { first_match: false },
260
+ data: <<~DATA
261
+ in :node, out :top?
262
+ == :parent, yes
263
+ != :parent, no
264
+ DATA
265
+ },
266
+ { example: 'uses != :parent, drops :parent input column',
267
+ options: {},
268
+ data: <<~DATA
269
+ in :node, out :top?
270
+ !:parent, no
271
+ , yes
272
+ DATA
273
+ },
274
+ { example: 'uses != :parent and == :parent',
275
+ options: { first_match: false },
276
+ data: <<~DATA
277
+ in :node, out :top?
278
+ != :parent, no
279
+ == :parent, yes
280
+ DATA
281
+ }
282
+ ]
283
+ examples.each do |test|
284
+ %i[decide decide!].each do |method|
285
+ it "#{method} correctly #{test[:example]}" do
286
+ options = test[:options]
287
+ table = CSVDecision.parse(test[:data], options)
288
+
289
+ expect(table.send(method, node: 0, parent: 0)).to eq(top?: 'yes')
290
+ expect(table.send(method, node: 1, parent: 0)).to eq(top?: 'no')
291
+ expect(table.send(method, node: '0', parent: 0)).to eq(top?: 'no')
292
+ end
293
+ end
294
+ end
295
+ end
296
+
297
+ context 'makes correct decision for table with symbol ordered compares' do
298
+ examples = [
299
+ { example: 'explicitly mentions :traded',
300
+ options: {},
301
+ data: <<~DATA
302
+ in :traded, in :settled, out :status
303
+ , :traded, same day
304
+ , >:traded, pending
305
+ , <:traded, invalid trade
306
+ , , invalid data
307
+ DATA
308
+ },
309
+ { example: 'does not mention :traded',
310
+ options: {},
311
+ data: <<~DATA
312
+ in :settled, out :status
313
+ :traded, same day
314
+ >:traded, pending
315
+ <:traded, invalid trade
316
+ , invalid data
317
+ DATA
318
+ }
319
+ ]
320
+ examples.each do |test|
321
+ %i[decide decide!].each do |method|
322
+ it "#{method} correctly #{test[:example]}" do
323
+ table = CSVDecision.parse(test[:data], test[:options])
324
+
325
+ expect(table.send(method, traded: '20171227', settled: '20171227')).to eq(status: 'same day')
326
+ expect(table.send(method, traded: 20171227, settled: 20171227 )).to eq(status: 'same day')
327
+ expect(table.send(method, traded: '20171227', settled: '20171228')).to eq(status: 'pending')
328
+ expect(table.send(method, traded: 20171227, settled: 20171228 )).to eq(status: 'pending')
329
+ expect(table.send(method, traded: '20171228', settled: '20171227')).to eq(status: 'invalid trade')
330
+ expect(table.send(method, traded: 20171228, settled: 20171227 )).to eq(status: 'invalid trade')
331
+ expect(table.send(method, traded: '20171227', settled: 20171228 )).to eq(status: 'invalid data')
332
+ end
333
+ end
334
+ end
335
+ end
336
+
337
+ context 'makes correct decisions for table with column symbol guards' do
338
+ examples = [
339
+ { example: 'evaluates guard conditions & output functions',
340
+ options: {},
341
+ data: <<~DATA
342
+ IN :country, guard:, out :PAID, out :PAID_type, out :len
343
+ US, :CUSIP.present?, :CUSIP, CUSIP, :PAID.length
344
+ GB, :SEDOL.present?, :SEDOL, SEDOL, :PAID.length
345
+ , :ISIN.present?, :ISIN, ISIN, :PAID.length
346
+ , :SEDOL.present?, :SEDOL, SEDOL, :PAID.length
347
+ , :CUSIP.present?, :CUSIP, CUSIP, :PAID.length
348
+ , , := nil, MISSING, := nil
349
+ DATA
350
+ },
351
+ { example: 'evaluates named guard condition',
352
+ options: {},
353
+ data: <<~DATA
354
+ in :country, guard: country, out :PAID, out :PAID_type, out :len
355
+ US, :CUSIP.present?, :CUSIP, CUSIP, :PAID.length
356
+ GB, :SEDOL.present?, :SEDOL, SEDOL, :PAID.length
357
+ , :ISIN.present?, :ISIN, ISIN, :PAID.length
358
+ , :SEDOL.present?, :SEDOL, SEDOL, :PAID.length
359
+ , :CUSIP.present?, :CUSIP, CUSIP, :PAID.length
360
+ , , := nil, MISSING, := nil
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
386
+ }
387
+ ]
388
+ examples.each do |test|
389
+ %i[decide decide!].each do |method|
390
+ it "#{method} correctly #{test[:example]}" do
391
+ table = CSVDecision.parse(test[:data], test[:options])
392
+
393
+ expect(table.send(method, country: 'US', CUSIP: '123456789'))
394
+ .to eq(PAID: '123456789', PAID_type: 'CUSIP', len: 9)
395
+ expect(table.send(method, country: 'EU', CUSIP: '123456789', ISIN:'123456789012'))
396
+ .to eq(PAID: '123456789012', PAID_type: 'ISIN', len: 12)
397
+ expect(table.send(method, country: 'AU', ISIN: ''))
398
+ .to eq(PAID: nil, PAID_type: 'MISSING', len: nil)
399
+ end
400
+ end
401
+ end
402
+ end
403
+
404
+ context 'makes correct decisions for table with column symbol guards and first_match: false' do
405
+ examples = [
406
+ { example: 'evaluates guard conditions & output functions',
407
+ options: { first_match: false },
408
+ data: <<~DATA
409
+ IN :country, guard:, out :ID, out :ID_type, out :len
410
+ US, :CUSIP.present?, :CUSIP, CUSIP, :ID.length
411
+ GB, :SEDOL.present?, :SEDOL, SEDOL, :ID.length
412
+ , :SEDOL.present?, :SEDOL, SEDOL, :ID.length
413
+ , :ISIN.present?, :ISIN, ISIN, :ID.length
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
435
+ }
436
+ ]
437
+ examples.each do |test|
438
+ %i[decide decide!].each do |method|
439
+ it "#{method} correctly #{test[:example]}" do
440
+ table = CSVDecision.parse(test[:data], test[:options])
441
+
442
+ expect(table.send(method, country: 'US', CUSIP: '123456789', Ticker: 'USTY'))
443
+ .to eq(ID: '123456789', ID_type: 'CUSIP', len: 9)
444
+
445
+ expect(table.send(method, country: 'US', CUSIP: '123456789', ISIN: '123456789012'))
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({})
450
+ end
451
+ end
452
+ end
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
493
+
494
+ context 'uses single column index to make correct decisions' do
495
+ examples = [
496
+ { example: 'evaluates single-column index CSV string',
497
+ options: { first_match: false },
498
+ data: <<~DATA
499
+ text_only
500
+ in:topic, in:region, out:team_member
501
+ sports, Europe, Alice
502
+ sports, , Bob
503
+ finance, America, Charlie
504
+ finance, Europe, Donald
505
+ finance, , Ernest
506
+ politics, Asia, Fujio
507
+ politics, America, Gilbert
508
+ politics, , Henry
509
+ sports, , Zach
510
+ finance, , Zach
511
+ politics, , Zach
512
+ DATA
513
+ },
514
+ { example: 'evaluates single-column index CSV file',
515
+ options: { first_match: false },
516
+ data: Pathname(File.join(SPEC_DATA_VALID, 'index_example.csv'))
517
+ }
518
+ ]
519
+ examples.each do |test|
520
+ %i[decide decide!].each do |method|
521
+ it "#{method} correctly #{test[:example]}" do
522
+ table = CSVDecision.parse(test[:data], test[:options])
523
+
524
+ expect(table.send(method, topic: 'politics', region: 'Arctic'))
525
+ .to eq(team_member: %w[Henry Zach])
526
+ expect(table.send(method, topic: 'culture', region: 'America'))
527
+ .to eq({})
528
+ end
529
+ end
530
+ end
531
+ end
532
+
533
+ context 'uses multi-column index to make correct decisions' do
534
+ examples = [
535
+ { example: 'evaluates multi-column index CSV string with guard',
536
+ options: { first_match: true },
537
+ data: <<~DATA
538
+ guard:, in :type, IN :input, OUT :output
539
+ :number.present?, integer, none, :=0
540
+ :number.blank?, integer, none, :=nil
541
+ :number.present?, integer, one, :=1
542
+ :number.blank?, integer, one, :=nil
543
+ :string.present?, string, none, 0
544
+ :number.blank?, string, none, :=nil
545
+ :string.present?, string, one, 1
546
+ :number.blank?, string, one, :=nil
547
+ DATA
548
+ },
549
+ { example: 'evaluates multi-column index CSV string with symbol conditions',
550
+ options: { first_match: true },
551
+ data: <<~DATA
552
+ in: number, in: string, in :type, IN :input, OUT :output
553
+ .present?, , integer, none, :=0
554
+ .blank?, , integer, none, :=nil
555
+ .present?, , integer, one, :=1
556
+ .blank?, , integer, one, :=nil
557
+ , .present?, string, none, 0
558
+ .blank?, , string, none, :=nil
559
+ , .present?, string, one, 1
560
+ .blank?, , string, one, :=nil
561
+ DATA
562
+ },
563
+ { example: 'evaluates multi-column index CSV string with negated symbol conditions',
564
+ options: { first_match: true },
565
+ data: <<~DATA
566
+ in: number, in: string, in :type, IN :input, OUT :output
567
+ !.blank?, , integer, none, :=0
568
+ != .present?, , integer, none, :=nil
569
+ =.present?, , integer, one, :=1
570
+ ==.blank?, , integer, one, :=nil
571
+ , != !blank?, string, none, 0
572
+ .blank?, , string, none, :=nil
573
+ , !.blank?, string, one, 1
574
+ !present?, , string, one, :=nil
575
+ DATA
576
+ },
577
+ { example: 'evaluates multi-column index CSV file',
578
+ options: { first_match: true },
579
+ data: Pathname(File.join(SPEC_DATA_VALID, 'multi_column_index.csv'))
580
+ }
581
+ ]
582
+ examples.each do |test|
583
+ %i[decide decide!].each do |method|
584
+ it "#{method} correctly #{test[:example]}" do
585
+ table = CSVDecision.parse(test[:data], test[:options])
586
+
587
+ expect(table.send(method, number: 1, type: 'integer', input: 'none')).to eq(output: 0)
588
+ expect(table.send(method, number: nil, type: 'string', input: 'one')).to eq(output: nil)
589
+ expect(table.send(method, string: '1', type: 'string', input: 'one')).to eq(output: '1')
590
+ expect(table.send(method, number: '1', type: 'string', input: 'one')).to eq({})
591
+ end
592
+ end
593
+ end
594
+ end
595
+
596
+ it 'scans the input hash paths for a first match' do
597
+ data = <<~DATA
598
+ path:, path:, in :type_cd, out :value, if:
599
+ header, , !nil?, :type_cd, :value.present?
600
+ payload, , !nil?, :type_cd, :value.present?
601
+ payload, ref_data, , :type_id, :value.present?
602
+ DATA
603
+ table = CSVDecision.parse(data)
604
+
605
+ input = {
606
+ header: { id: 1, type_cd: 'BUY' },
607
+ payload: { tran_id: 9,
608
+ ref_data: { account_id: 5, type_id: 'BUYL' }
609
+ }
610
+ }
611
+ expect(table.decide(input)).to eq(value: 'BUY')
612
+ expect(table.decide!(input)).to eq(value: 'BUY')
613
+
614
+ input = {
615
+ header: { id: 1 },
616
+ payload: { tran_id: 9,
617
+ ref_data: { account_id: 5, type_id: 'BUYL' }
618
+ }
619
+ }
620
+ expect(table.decide(input)).to eq(value: 'BUYL')
621
+ expect(table.decide!(input)).to eq(value: 'BUYL')
622
+
623
+ input = {
624
+ payload: { tran_id: 9,
625
+ ref_data: { account_id: 5, type_id: 'BUYL' }
626
+ }
627
+ }
628
+ expect(table.decide(input)).to eq(value: 'BUYL')
629
+
630
+ input = {
631
+ payload: { tran_id: nil,
632
+ ref_data: { type_id: '' }
633
+ }
634
+ }
635
+ expect(table.decide(input)).to eq({})
636
+ end
637
+
638
+ it 'scans the input hash paths accumulating matches' do
639
+ data = <<~DATA
640
+ path:, path:, out :value, out :key, if:
641
+ header, , :source_name, source_nm, :value.present?
642
+ header, , :client_name, client_nm, :value.present?
643
+ header, , :client_ref, client_ref_id, :value.present?
644
+ header, metrics, :service_name, service_nm, :value.present?
645
+ payload, , :amount, trade_am, :value.present?
646
+ payload, ref_data, :account_id, account_id, :value.present?
647
+ header, metrics, :receive_time, receive_tm, :value.present?
648
+ DATA
649
+ table = CSVDecision.parse(data, first_match: false)
650
+
651
+ input = {
652
+ header: {
653
+ id: 1, type_cd: 'BUY', source_name: 'Client', client_name: 'AAPL', client_ref: 'A1',
654
+ metrics: { service_name: 'Trading', receive_time: '12:00' }
655
+ },
656
+ payload: { tran_id: 9,
657
+ amount: '100.00',
658
+ ref_data: { account_id: '5010', type_id: 'BUYL' }
659
+ }
660
+ }
661
+ result = { value: %w[Client AAPL A1 Trading 100.00 5010 12:00],
662
+ key: %w[source_nm client_nm client_ref_id service_nm trade_am account_id receive_tm] }
663
+ expect(table.decide(input)).to eq result
664
+ expect(table.decide!(input)).to eq result
665
+
666
+ input = {
667
+ header: {
668
+ id: 1, type_cd: 'BUY', source_name: 'Client', client_ref: 'A1',
669
+ metrics: { service_name: 'Trading' }
670
+ },
671
+ payload: { tran_id: 9,
672
+ amount: '100.00',
673
+ ref_data: { type_id: 'BUYL' }
674
+ }
675
+ }
676
+ result = { value: %w[Client A1 Trading 100.00],
677
+ key: %w[source_nm client_ref_id service_nm trade_am] }
678
+
679
+ expect(table.decide(input)).to eq result
680
+ expect(table.decide!(input)).to eq result
681
+ end
682
+ end
683
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe CSVDecision do
4
+ describe '.root' do
5
+ specify { expect(CSVDecision.root).to eq File.dirname __dir__ }
6
+ end
7
+ end
File without changes