csv_decision 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -0
  3. data/CHANGELOG.md +4 -0
  4. data/README.md +62 -28
  5. data/csv_decision.gemspec +1 -1
  6. data/doc/CSVDecision/CellValidationError.html +2 -2
  7. data/doc/CSVDecision/Columns/Dictionary.html +114 -20
  8. data/doc/CSVDecision/Columns/Entry.html +2 -2
  9. data/doc/CSVDecision/Columns.html +109 -27
  10. data/doc/CSVDecision/Data.html +2 -2
  11. data/doc/CSVDecision/Decide.html +2 -2
  12. data/doc/CSVDecision/Decision.html +21 -21
  13. data/doc/CSVDecision/Dictionary/Entry.html +508 -0
  14. data/doc/CSVDecision/Dictionary.html +265 -0
  15. data/doc/CSVDecision/Error.html +2 -2
  16. data/doc/CSVDecision/FileError.html +3 -3
  17. data/doc/CSVDecision/Header.html +37 -136
  18. data/doc/CSVDecision/Input.html +2 -2
  19. data/doc/CSVDecision/Load.html +2 -2
  20. data/doc/CSVDecision/Matchers/Constant.html +2 -2
  21. data/doc/CSVDecision/Matchers/Function.html +2 -2
  22. data/doc/CSVDecision/Matchers/Guard.html +92 -25
  23. data/doc/CSVDecision/Matchers/Matcher.html +14 -18
  24. data/doc/CSVDecision/Matchers/Numeric.html +2 -2
  25. data/doc/CSVDecision/Matchers/Pattern.html +2 -2
  26. data/doc/CSVDecision/Matchers/Range.html +2 -2
  27. data/doc/CSVDecision/Matchers/Symbol.html +2 -2
  28. data/doc/CSVDecision/Matchers.html +5 -5
  29. data/doc/CSVDecision/Options.html +2 -2
  30. data/doc/CSVDecision/Parse.html +6 -4
  31. data/doc/CSVDecision/Result.html +944 -0
  32. data/doc/CSVDecision/ScanRow.html +70 -80
  33. data/doc/CSVDecision/Table.html +134 -54
  34. data/doc/CSVDecision.html +5 -5
  35. data/doc/_index.html +18 -4
  36. data/doc/class_list.html +1 -1
  37. data/doc/file.README.html +132 -62
  38. data/doc/index.html +132 -62
  39. data/doc/method_list.html +156 -60
  40. data/doc/top-level-namespace.html +2 -2
  41. data/lib/csv_decision/columns.rb +1 -8
  42. data/lib/csv_decision/decision.rb +45 -96
  43. data/lib/csv_decision/dictionary.rb +149 -0
  44. data/lib/csv_decision/header.rb +6 -133
  45. data/lib/csv_decision/matchers.rb +1 -2
  46. data/lib/csv_decision/parse.rb +18 -7
  47. data/lib/csv_decision/result.rb +180 -0
  48. data/lib/csv_decision/scan_row.rb +13 -7
  49. data/lib/csv_decision/table.rb +6 -5
  50. data/lib/csv_decision.rb +3 -1
  51. data/spec/csv_decision/columns_spec.rb +25 -4
  52. data/spec/csv_decision/examples_spec.rb +25 -0
  53. data/spec/csv_decision/matchers/guard_spec.rb +26 -9
  54. data/spec/csv_decision/table_spec.rb +48 -2
  55. metadata +7 -2
data/doc/index.html CHANGED
@@ -75,17 +75,17 @@ src="http://img.shields.io/badge/license-MIT-yellowgreen.svg"></a></p>
75
75
 
76
76
  <h3 id="label-CSV+based+Ruby+decision+tables">CSV based Ruby decision tables</h3>
77
77
 
78
- <p><code>csv_decision</code> is a RubyGem for CSV (comma separated values)
79
- based <a href="https://en.wikipedia.org/wiki/Decision_table">decision
80
- tables</a>. It accepts decision tables implemented as a <a
78
+ <p><code>csv_decision</code> is a RubyGem for CSV based <a
79
+ href="https://en.wikipedia.org/wiki/Decision_table">decision tables</a>. It
80
+ accepts decision tables implemented as a <a
81
81
  href="https://en.wikipedia.org/wiki/Comma-separated_values">CSV file</a>,
82
82
  which can then be used to execute complex conditional logic against an
83
83
  input hash, producing a decision as an output hash.</p>
84
84
 
85
85
  <h3 id="label-Why+use+csv_decision-3F">Why use <code>csv_decision</code>?</h3>
86
86
 
87
- <p>Typical “business logic” is notoriously illogical full of corner cases
88
- and one-off exceptions. A decision table can capture data-based decisions
87
+ <p>Typical “business logic” is notoriously illogical - full of corner cases
88
+ and one-off exceptions. A decision table can express data-based decisions
89
89
  in a way that comes more naturally to subject matter experts, who typically
90
90
  prefer spreadsheet models. Business logic may then be encapsulated,
91
91
  avoiding the need to write tortuous conditional expressions in Ruby that
@@ -93,9 +93,9 @@ draw the ire of <code>rubocop</code> and its ilk.</p>
93
93
 
94
94
  <p>This gem and the examples below take inspiration from <a
95
95
  href="https://github.com/jmettraux/rufus-decision">rufus/decision</a>.
96
- (However, that gem is no longer maintained and CSV Decision has better
97
- decision-time performance for the trade-off of slower table parse times and
98
- more memory – see <code>benchmarks/rufus_decision.rb</code>)</p>
96
+ (That gem is no longer maintained and CSV Decision has better decision-time
97
+ performance, at the expense of slower table parse times and more memory –
98
+ see <code>benchmarks/rufus_decision.rb</code>.)</p>
99
99
 
100
100
  <h3 id="label-Installation">Installation</h3>
101
101
 
@@ -109,27 +109,26 @@ more memory – see <code>benchmarks/rufus_decision.rb</code>)</p>
109
109
  <h3 id="label-Simple+example">Simple example</h3>
110
110
 
111
111
  <p>This table considers two input conditions: <code>topic</code> and
112
- <code>region</code>. These are labeled <code>in</code>. Certain
113
- combinations yield an output value for <code>team_member</code>, labeled
114
- <code>out</code>.</p>
115
-
116
- <pre class="code ruby"><code class="ruby">in :topic | in :region | out :team_member
117
- ----------+-------------+-----------------
118
- sports | Europe | Alice
119
- sports | | Bob
120
- finance | America | Charlie
121
- finance | Europe | Donald
122
- finance | | Ernest
123
- politics | Asia | Fujio
124
- politics | America | Gilbert
125
- politics | | Henry
126
- | | Zach</code></pre>
112
+ <code>region</code>, labeled <code>in:</code>. Certain combinations yield
113
+ an output value for <code>team_member</code>, labeled <code>out:</code>.</p>
114
+
115
+ <pre class="code ruby"><code class="ruby">in:topic | in:region | out:team_member
116
+ ---------+------------+----------------
117
+ sports | Europe | Alice
118
+ sports | | Bob
119
+ finance | America | Charlie
120
+ finance | Europe | Donald
121
+ finance | | Ernest
122
+ politics | Asia | Fujio
123
+ politics | America | Gilbert
124
+ politics | | Henry
125
+ | | Zach</code></pre>
127
126
 
128
127
  <p>When the topic is <code>finance</code> and the region is
129
128
  <code>Europe</code> the team member <code>Donald</code> is selected.</p>
130
129
 
131
130
  <p>This is a “first match” decision table in that as soon as a match is made
132
- execution stops and a single output value (hash) is returned.</p>
131
+ execution stops and a single output row (hash) is returned.</p>
133
132
 
134
133
  <p>The ordering of rows matters. <code>Ernest</code>, who is in charge of
135
134
  <code>finance</code> for the rest of the world, except for
@@ -138,7 +137,7 @@ colleagues <code>Charlie</code> and <code>Donald</code>. <code>Zach</code>
138
137
  has been placed last, catching all the input combos not matching any other
139
138
  row.</p>
140
139
 
141
- <p>Here it is as code:</p>
140
+ <p>Here is the example as code:</p>
142
141
 
143
142
  <p>“`ruby # Valid CSV string data = &lt;&lt;~DATA in :topic, in :region,
144
143
  out :team_member sports, Europe, Alice sports, , Bob finance, America,
@@ -147,13 +146,17 @@ politics, America, Gilbert politics, , Henry , , Zach DATA</p>
147
146
 
148
147
  <p>table = CSVDecision.parse(data)</p>
149
148
 
150
- <p>table.decide(topic: &#39;finance&#39;, region: &#39;Europe&#39;) # returns
151
- team_member: &#39;Donald&#39; table.decide(topic: &#39;sports&#39;,
152
- region: nil) # returns team_member: &#39;Bob&#39; table.decide(topic:
153
- &#39;culture&#39;, region: &#39;America&#39;) # team_member: &#39;Zach&#39;
154
- “`</p>
149
+ <p>table.decide(topic: &#39;finance&#39;, region: &#39;Europe&#39;) #=&gt; {
150
+ team_member: &#39;Donald&#39; } table.decide(topic: &#39;sports&#39;,
151
+ region: nil) #=&gt; { team_member: &#39;Bob&#39; } table.decide(topic:
152
+ &#39;culture&#39;, region: &#39;America&#39;) #=&gt; { team_member:
153
+ &#39;Zach&#39; } “`</p>
155
154
 
156
- <p>An empty <code>in</code> cell means “matches any value”, even nils.</p>
155
+ <p>An empty <code>in:</code> cell means “matches any value”, even nils.</p>
156
+
157
+ <p>Note that all column header names are symbolized, so it&#39;s actually more
158
+ accurate to write <code>in :topic</code>; however spaces before and after
159
+ the <code>:</code> do not matter.</p>
157
160
 
158
161
  <p>If you have cloned this gem&#39;s git repo, then the example can also be
159
162
  run by loading the table from a CSV file:</p>
@@ -163,12 +166,12 @@ CSVDecision.parse(Pathname(&#39;spec/data/valid/simple_example.csv&#39;))
163
166
  </code></p>
164
167
 
165
168
  <p>We can also load this same table using the option: <code>first_match:
166
- false</code>, which means that all matching rows will be accumulated into
167
- an array of hashes.</p>
169
+ false</code>, which means that <em>all</em> matching rows will be
170
+ accumulated into an array of hashes.</p>
168
171
 
169
172
  <p><code>ruby table = CSVDecision.parse(data, first_match: false)
170
- table.decide(topic: &#39;finance&#39;, region: &#39;Europe&#39;) # returns
171
- team_member: %w[Donald Ernest Zach] </code></p>
173
+ table.decide(topic: &#39;finance&#39;, region: &#39;Europe&#39;) #=&gt; {
174
+ team_member: %w[Donald Ernest Zach] } </code></p>
172
175
 
173
176
  <p>For more examples see <code>spec/csv_decision/table_spec.rb</code>.
174
177
  Complete documentation of all table parameters is in the code - see
@@ -177,26 +180,41 @@ Complete documentation of all table parameters is in the code - see
177
180
 
178
181
  <h3 id="label-CSV+Decision+features">CSV Decision features</h3>
179
182
  <ul><li>
183
+ <p>Either returns the first matching row as a hash (default), or accumulates
184
+ all matches as an array of hashes (i.e., <code>parse</code> option
185
+ <code>first_match: false</code> or CSV file option
186
+ <code>accumulate</code>).</p>
187
+ </li><li>
180
188
  <p>Fast decision-time performance (see <code>benchmarks</code> folder).</p>
181
189
  </li><li>
182
- <p>In addition to simple string matching, can match common Ruby constants,
183
- regular expressions, numeric comparisons and Ruby-style ranges.</p>
190
+ <p>In addition to simple strings, <code>csv_decision</code> can match basic
191
+ Ruby constants (e.g., <code>=nil</code>), regular expressions (e.g.,
192
+ <code>=~ on|off</code>), comparisons (e.g., <code>&gt; 100.0</code> ) and
193
+ Ruby-style ranges (e.g., <code>1..10</code>)</p>
194
+ </li><li>
195
+ <p>Can compare an input column versus another input hash key - e.g.,
196
+ <code>&gt; :column</code>.</p>
197
+ </li><li>
198
+ <p>Any cell starting with <code>#</code> is treated as a comment, and comments
199
+ may appear anywhere in the table. (Comment cells are always interpreted as
200
+ the empty string.)</p>
184
201
  </li><li>
185
- <p>Can use column symbols in comparisons for guard conditions e.g., &gt;
186
- :column.</p>
202
+ <p>Can use column symbol expressions or Ruby methods (0-arity) in input
203
+ columns for matching - e.g., <code>:column.zero?</code> or <code>:column
204
+ == 0</code>.</p>
205
+ </li><li>
206
+ <p>May also use Ruby methods in output columns - e.g.,
207
+ <code>:column.length</code>.</p>
187
208
  </li><li>
188
209
  <p>Accepts data as a file, CSV string or an array of arrays. (For safety all
189
210
  input data is force encoded to UTF-8, and non-ascii strings are converted
190
211
  to empty strings.)</p>
191
212
  </li><li>
192
213
  <p>All CSV cells are parsed for correctness, and helpful error messages
193
- generated for bad inputs.</p>
194
- </li><li>
195
- <p>Either returns the first matching row as a hash, or accumulates all matches
196
- as an array of hashes.</p>
214
+ generated for bad input.</p>
197
215
  </li></ul>
198
216
 
199
- <h3 id="label-Constants+other+than+strings">Constants other than strings</h3>
217
+ <h4 id="label-Constants+other+than+strings">Constants other than strings</h4>
200
218
 
201
219
  <p>Although <code>csv_decision</code> is string oriented, it does recognise
202
220
  other types of constant present in the input hash. Specifically, the
@@ -217,10 +235,10 @@ value: nil<br> table.decide(constant: 0) # returns value: 0<br>
217
235
  table.decide(constant: BigDecimal(&#39;100.0&#39;)) # returns value:
218
236
  BigDecimal(&#39;100.0&#39;)<br> “`</p>
219
237
 
220
- <h3 id="label-Column+header+symbols">Column header symbols</h3>
238
+ <h4 id="label-Column+header+symbols">Column header symbols</h4>
221
239
 
222
- <p>All input and output column names are symbolized, and can be used to form
223
- simple expressions that refer to values in the input hash.</p>
240
+ <p>All input and output column names are symbolized, and those symbols may be
241
+ used to form simple expressions that refer to values in the input hash.</p>
224
242
 
225
243
  <p>For example: “`ruby data = &lt;&lt;~DATA in :node, in :parent, out :top?
226
244
  , == :node, yes , , no DATA</p>
@@ -234,7 +252,8 @@ simple expressions that refer to values in the input hash.</p>
234
252
 
235
253
  <p>Note that there is no need to include an input column for
236
254
  <code>:node</code> in the decision table - it just needs to be present in
237
- the input hash. Also, <code>== :node</code> can be shortened to just
255
+ the input hash. The expression, <code>== :node</code> should be read as
256
+ <code>:parent == :node</code>. It can also be shortened to just
238
257
  <code>:node</code>, so the above decision table may be simplified to:</p>
239
258
 
240
259
  <p><code>ruby data = &lt;&lt;~DATA in :parent, out :top?
@@ -243,9 +262,9 @@ operators are also supported: <code>!=</code>, <code>&gt;</code>,
243
262
  <code>&gt;=</code>, <code>&lt;</code>, <code>&lt;=</code>. For more simple
244
263
  examples see <code>spec/csv_decision/examples_spec.rb</code>.</p>
245
264
 
246
- <h3 id="label-Column+guard+conditions">Column guard conditions</h3>
265
+ <h4 id="label-Input+guard+conditions">Input guard conditions</h4>
247
266
 
248
- <p>Sometimes it&#39;s more convenient to write guard conditions in a single
267
+ <p>Sometimes it&#39;s more convenient to write guard expressions in a single
249
268
  column specialized for that purpose. For example:</p>
250
269
 
251
270
  <pre class="code ruby"><code class="ruby"><span class='id identifier rubyid_data'>data</span> <span class='op'>=</span> <span class='heredoc_beg'>&lt;&lt;~DATA</span>
@@ -264,7 +283,39 @@ column specialized for that purpose. For example:</p>
264
283
  <span class='comment'>#=&gt; { ID: &#39;123456789012&#39;, ID_type: &#39;ISIN&#39;, len: 12 }
265
284
  </span></code></pre>
266
285
 
267
- <p>Guard columns may be anonymous, and must contain non-constant expressions.</p>
286
+ <p>Input <code>guard:</code> columns may be anonymous, and must contain
287
+ non-constant expressions. In addition to 0-arity Ruby methods, the
288
+ following comparison operators are allowed: <code>==</code>,
289
+ <code>!=</code>, <code>&gt;</code>, <code>&gt;=</code>, <code>&lt;</code>
290
+ and <code>&lt;=</code>. Also, regular expressions are supported - i.e.,
291
+ <code>=~</code> and <code>!~</code>.</p>
292
+
293
+ <h4 id="label-Output+if+conditions">Output if conditions</h4>
294
+
295
+ <p>In some situations it is useful to apply filter conditions <em>after</em>
296
+ all the output columns have been derived. For example:</p>
297
+
298
+ <pre class="code ruby"><code class="ruby"><span class='id identifier rubyid_data'>data</span> <span class='op'>=</span> <span class='heredoc_beg'>&lt;&lt;~DATA</span>
299
+ <span class='tstring_content'> in :country, guard:, out :ID, out :ID_type, out :len, if:
300
+ </span><span class='tstring_content'> US, :CUSIP.present?, :CUSIP, CUSIP8, :ID.length, :len == 8
301
+ </span><span class='tstring_content'> US, :CUSIP.present?, :CUSIP, CUSIP9, :ID.length, :len == 9
302
+ </span><span class='tstring_content'> US, :CUSIP.present?, :CUSIP, DUMMY, :ID.length,
303
+ </span><span class='tstring_content'> , :ISIN.present?, :ISIN, ISIN, :ID.length, :len == 12
304
+ </span><span class='tstring_content'> , :ISIN.present?, :ISIN, DUMMY, :ID.length,
305
+ </span><span class='tstring_content'> , :CUSIP.present?, :CUSIP, DUMMY, :ID.length,
306
+ </span><span class='heredoc_end'> DATA
307
+ </span>
308
+ <span class='id identifier rubyid_table'>table</span> <span class='op'>=</span> <span class='const'><span class='object_link'><a href="CSVDecision.html" title="CSVDecision (module)">CSVDecision</a></span></span><span class='period'>.</span><span class='id identifier rubyid_parse'><span class='object_link'><a href="CSVDecision.html#parse-class_method" title="CSVDecision.parse (method)">parse</a></span></span><span class='lparen'>(</span><span class='id identifier rubyid_data'>data</span><span class='rparen'>)</span>
309
+ <span class='id identifier rubyid_table'>table</span><span class='period'>.</span><span class='id identifier rubyid_decide'>decide</span><span class='lparen'>(</span><span class='label'>country:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>US</span><span class='tstring_end'>&#39;</span></span><span class='comma'>,</span> <span class='label'>CUSIP:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>123456789</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span> <span class='comment'>#=&gt; {ID: &#39;123456789&#39;, ID_type: &#39;CUSIP9&#39;, len: 9}
310
+ </span><span class='id identifier rubyid_table'>table</span><span class='period'>.</span><span class='id identifier rubyid_decide'>decide</span><span class='lparen'>(</span><span class='label'>CUSIP:</span> <span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>12345678</span><span class='tstring_end'>&#39;</span></span><span class='comma'>,</span> <span class='label'>ISIN:</span><span class='tstring'><span class='tstring_beg'>&#39;</span><span class='tstring_content'>1234567890</span><span class='tstring_end'>&#39;</span></span><span class='rparen'>)</span> <span class='comment'>#=&gt; {ID: &#39;1234567890&#39;, ID_type: &#39;DUMMY&#39;, len: 10}
311
+ </span></code></pre>
312
+
313
+ <p>Output <code>if:</code> columns may be anonymous, and must contain
314
+ non-constant expressions. In addition to 0-arity Ruby methods, the
315
+ following comparison operators are allowed: <code>==</code>,
316
+ <code>!=</code>, <code>&gt;</code>, <code>&gt;=</code>, <code>&lt;</code>
317
+ and <code>&lt;=</code>. Also, regular expressions are supported - i.e.,
318
+ <code>=~</code> and <code>!~</code>.</p>
268
319
 
269
320
  <h3 id="label-Testing">Testing</h3>
270
321
 
@@ -277,21 +328,40 @@ bundle install rspec </code></p>
277
328
  <h3 id="label-Planned+features">Planned features</h3>
278
329
 
279
330
  <p><code>csv_decision</code> is still a work in progress, and will be enhanced
280
- to support the following features: * Use of column symbol expressions or
281
- built-in guard functions in the input columns for matching. * Input
282
- columns may be indexed for faster lookup performance. * May use functions
283
- in the output columns to formulate the final decision. * Input hash values
284
- may be conditionally defaulted using a constant or a function call *
285
- Output columns may use interpolated strings referencing column symbols. *
286
- May be extended with a user-supplied library of Ruby functions for tailored
287
- logic. * Can use post-match guard conditions to filter the results of
288
- multi-row decision output.</p>
331
+ to support the following features: * Text-only input columns may be
332
+ indexed for faster lookup performance. * Input hash values may be
333
+ (conditionally) defaulted with a constant or a function call. * Output
334
+ columns may construct interpolated strings referencing column symbols. *
335
+ Supply a pre-defined library of functions that can be called within input
336
+ columns to implement matching logic or from the output columns to
337
+ formulate the final decision. * Available functions may be extended with a
338
+ user-supplied library of Ruby methods for tailored logic.</p>
339
+
340
+ <h3 id="label-Reasons+for+the+limitations+of+column+expressions">Reasons for the limitations of column expressions</h3>
341
+
342
+ <p>The simple column expressions allowed by <code>csv_decision</code> are
343
+ purposely limited for reasons of understandability and maintainability. The
344
+ whole point of this gem is to make decision rules easier to express and
345
+ comprehend as declarative, tabular logic. While Ruby makes it easy to
346
+ execute arbitrary code embedded within a CSV file, this could easily result
347
+ in hard to debug logic that also poses safety risks.</p>
348
+
349
+ <h2 id="label-Changelog">Changelog</h2>
350
+
351
+ <p>See <a href="./CHANGELOG.md">CHANGELOG.md</a> for a list of changes.</p>
352
+
353
+ <h2 id="label-License">License</h2>
354
+
355
+ <p>CSV Decision © 2017-2018 by <a
356
+ href="mailto:brett@phillips-vickers.com">Brett Vickers</a>. CSV Decision is
357
+ licensed under the MIT license. Please see the <a
358
+ href="./LICENSE">LICENSE</a> document for more information.</p>
289
359
  </div></div>
290
360
 
291
361
  <div id="footer">
292
- Generated on Sat Dec 30 13:04:04 2017 by
362
+ Generated on Fri Jan 5 21:43:59 2018 by
293
363
  <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
294
- 0.9.12 (ruby-2.3.0).
364
+ 0.9.12 (ruby-2.4.0).
295
365
  </div>
296
366
 
297
367
  </div>