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,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