csv_decision 0.0.3 → 0.0.4

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 (87) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +2 -0
  3. data/.gitignore +2 -1
  4. data/.travis.yml +2 -3
  5. data/CHANGELOG.md +19 -1
  6. data/README.md +49 -16
  7. data/{benchmark.rb → benchmarks/rufus_decision.rb} +1 -1
  8. data/csv_decision.gemspec +1 -1
  9. data/doc/CSVDecision/CellValidationError.html +143 -0
  10. data/doc/CSVDecision/Columns/Default.html +409 -0
  11. data/doc/CSVDecision/Columns/Dictionary.html +410 -0
  12. data/doc/CSVDecision/Columns/Entry.html +321 -0
  13. data/doc/CSVDecision/Columns.html +476 -0
  14. data/doc/CSVDecision/Constant.html +295 -0
  15. data/doc/CSVDecision/Data.html +344 -0
  16. data/doc/CSVDecision/Decide.html +434 -0
  17. data/doc/CSVDecision/Decision.html +604 -0
  18. data/doc/CSVDecision/Error.html +139 -0
  19. data/doc/CSVDecision/FileError.html +143 -0
  20. data/doc/CSVDecision/Function.html +229 -0
  21. data/doc/CSVDecision/Header.html +520 -0
  22. data/doc/CSVDecision/Input.html +305 -0
  23. data/doc/CSVDecision/Load.html +225 -0
  24. data/doc/CSVDecision/Matchers/Constant.html +242 -0
  25. data/doc/CSVDecision/Matchers/Function.html +342 -0
  26. data/doc/CSVDecision/Matchers/Matcher.html +325 -0
  27. data/doc/CSVDecision/Matchers/Numeric.html +277 -0
  28. data/doc/CSVDecision/Matchers/Pattern.html +600 -0
  29. data/doc/CSVDecision/Matchers/Range.html +413 -0
  30. data/doc/CSVDecision/Matchers/Symbol.html +280 -0
  31. data/doc/CSVDecision/Matchers.html +1529 -0
  32. data/doc/CSVDecision/Numeric.html +259 -0
  33. data/doc/CSVDecision/Options.html +445 -0
  34. data/doc/CSVDecision/Parse.html +270 -0
  35. data/doc/CSVDecision/ScanRow.html +746 -0
  36. data/doc/CSVDecision/Symbol.html +256 -0
  37. data/doc/CSVDecision/Table.html +1115 -0
  38. data/doc/CSVDecision.html +652 -0
  39. data/doc/_index.html +410 -0
  40. data/doc/class_list.html +51 -0
  41. data/doc/css/common.css +1 -0
  42. data/doc/css/full_list.css +58 -0
  43. data/doc/css/style.css +499 -0
  44. data/doc/file.README.html +264 -0
  45. data/doc/file_list.html +56 -0
  46. data/doc/frames.html +17 -0
  47. data/doc/index.html +264 -0
  48. data/doc/js/app.js +248 -0
  49. data/doc/js/full_list.js +216 -0
  50. data/doc/js/jquery.js +4 -0
  51. data/doc/method_list.html +683 -0
  52. data/doc/top-level-namespace.html +110 -0
  53. data/lib/csv_decision/columns.rb +15 -12
  54. data/lib/csv_decision/constant.rb +54 -0
  55. data/lib/csv_decision/decide.rb +5 -5
  56. data/lib/csv_decision/decision.rb +3 -1
  57. data/lib/csv_decision/function.rb +32 -0
  58. data/lib/csv_decision/header.rb +27 -18
  59. data/lib/csv_decision/input.rb +11 -8
  60. data/lib/csv_decision/matchers/constant.rb +18 -0
  61. data/lib/csv_decision/matchers/function.rb +11 -44
  62. data/lib/csv_decision/matchers/numeric.rb +5 -33
  63. data/lib/csv_decision/matchers/pattern.rb +26 -11
  64. data/lib/csv_decision/matchers/range.rb +21 -5
  65. data/lib/csv_decision/matchers/symbol.rb +20 -0
  66. data/lib/csv_decision/matchers.rb +85 -20
  67. data/lib/csv_decision/numeric.rb +38 -0
  68. data/lib/csv_decision/options.rb +36 -27
  69. data/lib/csv_decision/parse.rb +46 -39
  70. data/lib/csv_decision/scan_row.rb +19 -7
  71. data/lib/csv_decision/symbol.rb +73 -0
  72. data/lib/csv_decision/table.rb +24 -18
  73. data/lib/csv_decision.rb +25 -18
  74. data/spec/csv_decision/columns_spec.rb +1 -1
  75. data/spec/csv_decision/constant_spec.rb +60 -0
  76. data/spec/csv_decision/examples_spec.rb +119 -0
  77. data/spec/csv_decision/matchers/function_spec.rb +48 -28
  78. data/spec/csv_decision/matchers/numeric_spec.rb +4 -41
  79. data/spec/csv_decision/matchers/range_spec.rb +31 -61
  80. data/spec/csv_decision/matchers/symbol_spec.rb +65 -0
  81. data/spec/csv_decision/options_spec.rb +14 -2
  82. data/spec/csv_decision/parse_spec.rb +10 -0
  83. data/spec/csv_decision/table_spec.rb +112 -6
  84. data/spec/data/valid/simple_constants.csv +3 -3
  85. metadata +62 -7
  86. data/spec/csv_decision/simple_example_spec.rb +0 -75
  87. /data/spec/{csv_decision.rb → csv_decision_spec.rb} +0 -0
@@ -0,0 +1,264 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>
7
+ File: README
8
+
9
+ &mdash; Documentation by YARD 0.9.12
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" charset="utf-8" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" charset="utf-8" />
16
+
17
+ <script type="text/javascript" charset="utf-8">
18
+ pathId = "README";
19
+ relpath = '';
20
+ </script>
21
+
22
+
23
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
24
+
25
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
26
+
27
+
28
+ </head>
29
+ <body>
30
+ <div class="nav_wrap">
31
+ <iframe id="nav" src="file_list.html?1"></iframe>
32
+ <div id="resizer"></div>
33
+ </div>
34
+
35
+ <div id="main" tabindex="-1">
36
+ <div id="header">
37
+ <div id="menu">
38
+
39
+ <a href="_index.html">Index</a> &raquo;
40
+ <span class="title">File: README</span>
41
+
42
+ </div>
43
+
44
+ <div id="search">
45
+
46
+ <a class="full_list_link" id="class_list_link"
47
+ href="class_list.html">
48
+
49
+ <svg width="24" height="24">
50
+ <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
51
+ <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
52
+ <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
53
+ </svg>
54
+ </a>
55
+
56
+ </div>
57
+ <div class="clear"></div>
58
+ </div>
59
+
60
+ <div id="content"><div id='filecontents'>
61
+ <h1 id="label-CSV+Decision">CSV Decision</h1>
62
+
63
+ <p><a href="https://badge.fury.io/rb/csv_decision"><img
64
+ src="https://badge.fury.io/rb/csv_decision.svg"></a> <a
65
+ href="https://travis-ci.org/bpvickers/csv_decision"><img
66
+ src="https://travis-ci.org/bpvickers/csv_decision.svg?branch=master"></a>
67
+ <a
68
+ href="https://coveralls.io/github/bpvickers/csv_decision?branch=master"><img
69
+ src="https://coveralls.io/repos/github/bpvickers/csv_decision/badge.svg?branch=master"></a>
70
+ <a
71
+ href="https://codeclimate.com/github/bpvickers/csv_decision/maintainability"><img
72
+ src="https://api.codeclimate.com/v1/badges/466a6c52e8f6a3840967/maintainability"></a>
73
+ <a href="#license"><img
74
+ src="http://img.shields.io/badge/license-MIT-yellowgreen.svg"></a></p>
75
+
76
+ <h3 id="label-CSV+based+Ruby+decision+tables+-28a+lightweight+Hash+transformation+gem-29">CSV based Ruby decision tables (a lightweight Hash transformation gem)</h3>
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
81
+ href="https://en.wikipedia.org/wiki/Comma-separated_values">CSV file</a>,
82
+ which can then be used to execute complex conditional logic against an
83
+ input hash, producing a decision as an output hash.</p>
84
+
85
+ <p>### CSV Decision features * Fast decision-time performance (see
86
+ <code>benchmark.rb</code>). * In addition to simple string matching, can
87
+ match common Ruby constants, regular expressions, numeric comparisons and
88
+ Ruby-style ranges. * Can use column symbols in comparisons for guard
89
+ conditions – e.g., &gt; :column. * Accepts data as a file, CSV string or
90
+ an array of arrays. (For safety all input data is force encoded to UTF-8,
91
+ and non-ascii strings are converted to empty strings.) * All CSV cells are
92
+ parsed for correctness, and helpful error messages generated for bad
93
+ inputs. * Either returns the first matching row as a hash, or accumulates
94
+ all matches as an array of hashes.</p>
95
+
96
+ <p>### Planned features <code>csv_decision</code> is still a work in
97
+ progress, and will be enhanced to support the following features: * Input
98
+ columns may be indexed for faster lookup performance. * May use functions
99
+ in the output columns to formulate the final decision. * Input hash values
100
+ may be conditionally defaulted using a constant or a function call * Use
101
+ of column symbol references or built-in guard functions in the input
102
+ columns for matching. * Output columns may use interpolated strings
103
+ referencing column symbols. * May be extended with user-defined Ruby
104
+ functions for tailored logic. * Can use post-match guard conditions to
105
+ filter the results of multi-row decision output.</p>
106
+
107
+ <p>### Why use <code>csv_decision</code>?</p>
108
+
109
+ <p>Typical “business logic” is notoriously illogical – full of corner cases
110
+ and one-off exceptions. A decision table can capture data-based decisions
111
+ in a way that comes more naturally to subject matter experts, who
112
+ typically prefer spreadsheet models. Business logic may then be
113
+ encapsulated, avoiding the need to write tortuous conditional expressions
114
+ in Ruby that draw the ire of <code>rubocop</code> and its ilk.</p>
115
+
116
+ <p>This gem takes its inspiration from <a
117
+ href="https://github.com/jmettraux/rufus-decision">rufus/decision</a>.
118
+ (That gem is no longer maintained and has issues with execution
119
+ performance.)</p>
120
+
121
+ <p>### Installation</p>
122
+
123
+ <p>To get started, just add <code>csv_decision</code> to your
124
+ <code>Gemfile</code>, and then run <code>bundle</code>:</p>
125
+
126
+ <p><code>ruby gem &#39;csv_decision&#39;, &#39;~&gt; 0.0.1&#39; </code></p>
127
+
128
+ <p>or simply <code>bash gem install csv_decision </code></p>
129
+
130
+ <p>### Simple example</p>
131
+
132
+ <p>A decision table may be as simple or as complex as you like (although very
133
+ complex tables defeat the whole purpose). Basic usage will be illustrated
134
+ by an example taken from: <a
135
+ href="https://jmettraux.wordpress.com/2009/04/25/rufus-decision-11-ruby-decision-tables">jmettraux.wordpress.com/2009/04/25/rufus-decision-11-ruby-decision-tables</a>/.</p>
136
+
137
+ <p>This example considers two input conditions: <code>topic</code> and
138
+ <code>region</code>. These are labeled <code>in</code>. Certain
139
+ combinations yield an output value for <code>team_member</code>, labeled
140
+ <code>out</code>.</p>
141
+
142
+ <pre class="code ruby"><code class="ruby">in :topic | in :region | out :team_member
143
+ ----------+-------------+-----------------
144
+ sports | Europe | Alice
145
+ sports | | Bob
146
+ finance | America | Charlie
147
+ finance | Europe | Donald
148
+ finance | | Ernest
149
+ politics | Asia | Fujio
150
+ politics | America | Gilbert
151
+ politics | | Henry
152
+ | | Zach</code></pre>
153
+
154
+ <p>When the topic is <code>finance</code> and the region is
155
+ <code>Europe</code> the team member <code>Donald</code> is selected.</p>
156
+
157
+ <p>This is a “first match” decision table in that as soon as a match is made
158
+ execution stops and a single output value (hash) is returned.</p>
159
+
160
+ <p>The ordering of rows matters. <code>Ernest</code>, who is in charge of
161
+ <code>finance</code> for the rest of the world, except for
162
+ <code>America</code> and <code>Europe</code>, <em>must</em> come after his
163
+ colleagues <code>Charlie</code> and <code>Donald</code>. <code>Zach</code>
164
+ has been placed last, catching all the input combos not matching any other
165
+ row.</p>
166
+
167
+ <p>Now for some code.</p>
168
+
169
+ <p>“`ruby # Valid CSV string data = &lt;&lt;~DATA in :topic, in :region,
170
+ out :team_member sports, Europe, Alice sports, , Bob finance, America,
171
+ Charlie finance, Europe, Donald finance, , Ernest politics, Asia, Fujio
172
+ politics, America, Gilbert politics, , Henry , , Zach DATA</p>
173
+
174
+ <p>table = CSVDecision.parse(data)</p>
175
+
176
+ <p>table.decide(topic: &#39;finance&#39;, region: &#39;Europe&#39;) # returns
177
+ team_member: &#39;Donald&#39; table.decide(topic: &#39;sports&#39;,
178
+ region: nil) # returns team_member: &#39;Bob&#39; table.decide(topic:
179
+ &#39;culture&#39;, region: &#39;America&#39;) # team_member: &#39;Zach&#39;
180
+ “`</p>
181
+
182
+ <p>An empty <code>in</code> cell means “matches any value”.</p>
183
+
184
+ <p>If you have cloned this gem&#39;s git repo, then this example can also be
185
+ run by loading the table from a CSV file:</p>
186
+
187
+ <p><code>ruby table =
188
+ CSVDecision.parse(Pathname(&#39;spec/data/valid/simple_example.csv&#39;))
189
+ </code></p>
190
+
191
+ <p>We can also load this same table using the option: <code>first_match:
192
+ false</code>.</p>
193
+
194
+ <p><code>ruby table = CSVDecision.parse(data, first_match: false)
195
+ table.decide(topic: &#39;finance&#39;, region: &#39;Europe&#39;) # returns
196
+ team_member: %w[Donald Ernest Zach] </code></p>
197
+
198
+ <p>For more examples see <code>spec/csv_decision/table_spec.rb</code>.
199
+ Complete documentation of all table parameters is in the code - see
200
+ <code>lib/csv_decision/parse.rb</code> and
201
+ <code>lib/csv_decision/table.rb</code>.</p>
202
+
203
+ <p>### Constants other than strings Although <code>csv_decision</code> is
204
+ string oriented, it does recognise other types of constant present in the
205
+ input hash. Specifically, the following classes are recognized:
206
+ <code>Integer</code>, <code>BigDecimal</code>, <code>NilClass</code>,
207
+ <code>TrueClass</code> and <code>FalseClass</code>.</p>
208
+
209
+ <p>This is accomplished by prefixing the value with one of the operators
210
+ <code>=</code>, <code>==</code> or <code>:=</code>. (The syntax is
211
+ intentionally lax.)</p>
212
+
213
+ <p>For example: “`ruby data = &lt;&lt;~DATA in :constant, out :value
214
+ :=nil, :=nil ==false, ==false =true, =true = 0, = 0 :=100.0, :=100.0
215
+ DATA</p>
216
+
217
+ <p>table = CSVDecision.parse(data) table.decide(constant: nil) # returns
218
+ value: nil<br> table.decide(constant: 0) # returns value: 0<br>
219
+ table.decide(constant: BigDecimal(&#39;100.0&#39;)) # returns value:
220
+ BigDecimal(&#39;100.0&#39;)<br> “`</p>
221
+
222
+ <p>### Column header symbols All input and output column names are
223
+ symbolized, and can be used to form simple expressions that refer to
224
+ values in the input hash.</p>
225
+
226
+ <p>For example: “`ruby data = &lt;&lt;~DATA in :node, in :parent, out :top?
227
+ , == :node, yes , , no DATA</p>
228
+
229
+ <pre class="code ruby"><code class="ruby"><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>
230
+ <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'>node:</span> <span class='int'>0</span><span class='comma'>,</span> <span class='label'>parent:</span> <span class='int'>0</span><span class='rparen'>)</span> <span class='comment'># returns top?: &#39;yes&#39;
231
+ </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'>node:</span> <span class='int'>1</span><span class='comma'>,</span> <span class='label'>parent:</span> <span class='int'>0</span><span class='rparen'>)</span> <span class='comment'># returns top?: &#39;no&#39;
232
+ </span></code></pre>
233
+
234
+ <p>“`</p>
235
+
236
+ <p>Note that there is no need to include an input column for
237
+ <code>:node</code> in the decision table - it just needs to be present in
238
+ the input hash. Also, <code>== :node</code> can be shortened to just
239
+ <code>:node</code>, so the above decision table may be simplified to:</p>
240
+
241
+ <p><code>ruby data = &lt;&lt;~DATA in :parent, out :top?
242
+ :node, yes , no DATA </code> These comparison
243
+ operators are also supported: <code>!=</code>, <code>&gt;</code>,
244
+ <code>&gt;=</code>, <code>&lt;</code>, <code>&lt;=</code>. For more simple
245
+ examples see <code>spec/csv_decision/examples_spec.rb</code>.</p>
246
+
247
+ <p>### Testing</p>
248
+
249
+ <p><code>csv_decision</code> includes thorough <a
250
+ href="http://rspec.info">RSpec</a> tests:</p>
251
+
252
+ <p><code>bash # Execute within a clone of the csv_decision Git repository:
253
+ bundle install rspec </code></p>
254
+ </div></div>
255
+
256
+ <div id="footer">
257
+ Generated on Tue Dec 26 18:31:37 2017 by
258
+ <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
259
+ 0.9.12 (ruby-2.3.0).
260
+ </div>
261
+
262
+ </div>
263
+ </body>
264
+ </html>
@@ -0,0 +1,56 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
5
+ <meta charset="utf-8" />
6
+
7
+ <link rel="stylesheet" href="css/full_list.css" type="text/css" media="screen" charset="utf-8" />
8
+
9
+ <link rel="stylesheet" href="css/common.css" type="text/css" media="screen" charset="utf-8" />
10
+
11
+
12
+
13
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
14
+
15
+ <script type="text/javascript" charset="utf-8" src="js/full_list.js"></script>
16
+
17
+
18
+ <title>File List</title>
19
+ <base id="base_target" target="_parent" />
20
+ </head>
21
+ <body>
22
+ <div id="content">
23
+ <div class="fixed_header">
24
+ <h1 id="full_list_header">File List</h1>
25
+ <div id="full_list_nav">
26
+
27
+ <span><a target="_self" href="class_list.html">
28
+ Classes
29
+ </a></span>
30
+
31
+ <span><a target="_self" href="method_list.html">
32
+ Methods
33
+ </a></span>
34
+
35
+ <span><a target="_self" href="file_list.html">
36
+ Files
37
+ </a></span>
38
+
39
+ </div>
40
+
41
+ <div id="search">Search: <input type="text" /></div>
42
+ </div>
43
+
44
+ <ul id="full_list" class="file">
45
+
46
+
47
+ <li id="object_README" class="odd">
48
+ <div class="item"><span class="object_link"><a href="index.html" title="README">README</a></span></div>
49
+ </li>
50
+
51
+
52
+
53
+ </ul>
54
+ </div>
55
+ </body>
56
+ </html>
data/doc/frames.html ADDED
@@ -0,0 +1,17 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>Documentation by YARD 0.9.12</title>
6
+ </head>
7
+ <script type="text/javascript" charset="utf-8">
8
+ var match = unescape(window.location.hash).match(/^#!(.+)/);
9
+ var name = match ? match[1] : 'index.html';
10
+ name = name.replace(/^(\w+):\/\//, '').replace(/^\/\//, '');
11
+ window.top.location = name;
12
+ </script>
13
+ <noscript>
14
+ <h1>Oops!</h1>
15
+ <h2>YARD requires JavaScript!</h2>
16
+ </noscript>
17
+ </html>
data/doc/index.html ADDED
@@ -0,0 +1,264 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>
7
+ File: README
8
+
9
+ &mdash; Documentation by YARD 0.9.12
10
+
11
+ </title>
12
+
13
+ <link rel="stylesheet" href="css/style.css" type="text/css" charset="utf-8" />
14
+
15
+ <link rel="stylesheet" href="css/common.css" type="text/css" charset="utf-8" />
16
+
17
+ <script type="text/javascript" charset="utf-8">
18
+ pathId = "README";
19
+ relpath = '';
20
+ </script>
21
+
22
+
23
+ <script type="text/javascript" charset="utf-8" src="js/jquery.js"></script>
24
+
25
+ <script type="text/javascript" charset="utf-8" src="js/app.js"></script>
26
+
27
+
28
+ </head>
29
+ <body>
30
+ <div class="nav_wrap">
31
+ <iframe id="nav" src="class_list.html?1"></iframe>
32
+ <div id="resizer"></div>
33
+ </div>
34
+
35
+ <div id="main" tabindex="-1">
36
+ <div id="header">
37
+ <div id="menu">
38
+
39
+ <a href="_index.html">Index</a> &raquo;
40
+ <span class="title">File: README</span>
41
+
42
+ </div>
43
+
44
+ <div id="search">
45
+
46
+ <a class="full_list_link" id="class_list_link"
47
+ href="class_list.html">
48
+
49
+ <svg width="24" height="24">
50
+ <rect x="0" y="4" width="24" height="4" rx="1" ry="1"></rect>
51
+ <rect x="0" y="12" width="24" height="4" rx="1" ry="1"></rect>
52
+ <rect x="0" y="20" width="24" height="4" rx="1" ry="1"></rect>
53
+ </svg>
54
+ </a>
55
+
56
+ </div>
57
+ <div class="clear"></div>
58
+ </div>
59
+
60
+ <div id="content"><div id='filecontents'>
61
+ <h1 id="label-CSV+Decision">CSV Decision</h1>
62
+
63
+ <p><a href="https://badge.fury.io/rb/csv_decision"><img
64
+ src="https://badge.fury.io/rb/csv_decision.svg"></a> <a
65
+ href="https://travis-ci.org/bpvickers/csv_decision"><img
66
+ src="https://travis-ci.org/bpvickers/csv_decision.svg?branch=master"></a>
67
+ <a
68
+ href="https://coveralls.io/github/bpvickers/csv_decision?branch=master"><img
69
+ src="https://coveralls.io/repos/github/bpvickers/csv_decision/badge.svg?branch=master"></a>
70
+ <a
71
+ href="https://codeclimate.com/github/bpvickers/csv_decision/maintainability"><img
72
+ src="https://api.codeclimate.com/v1/badges/466a6c52e8f6a3840967/maintainability"></a>
73
+ <a href="#license"><img
74
+ src="http://img.shields.io/badge/license-MIT-yellowgreen.svg"></a></p>
75
+
76
+ <h3 id="label-CSV+based+Ruby+decision+tables+-28a+lightweight+Hash+transformation+gem-29">CSV based Ruby decision tables (a lightweight Hash transformation gem)</h3>
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
81
+ href="https://en.wikipedia.org/wiki/Comma-separated_values">CSV file</a>,
82
+ which can then be used to execute complex conditional logic against an
83
+ input hash, producing a decision as an output hash.</p>
84
+
85
+ <p>### CSV Decision features * Fast decision-time performance (see
86
+ <code>benchmark.rb</code>). * In addition to simple string matching, can
87
+ match common Ruby constants, regular expressions, numeric comparisons and
88
+ Ruby-style ranges. * Can use column symbols in comparisons for guard
89
+ conditions – e.g., &gt; :column. * Accepts data as a file, CSV string or
90
+ an array of arrays. (For safety all input data is force encoded to UTF-8,
91
+ and non-ascii strings are converted to empty strings.) * All CSV cells are
92
+ parsed for correctness, and helpful error messages generated for bad
93
+ inputs. * Either returns the first matching row as a hash, or accumulates
94
+ all matches as an array of hashes.</p>
95
+
96
+ <p>### Planned features <code>csv_decision</code> is still a work in
97
+ progress, and will be enhanced to support the following features: * Input
98
+ columns may be indexed for faster lookup performance. * May use functions
99
+ in the output columns to formulate the final decision. * Input hash values
100
+ may be conditionally defaulted using a constant or a function call * Use
101
+ of column symbol references or built-in guard functions in the input
102
+ columns for matching. * Output columns may use interpolated strings
103
+ referencing column symbols. * May be extended with user-defined Ruby
104
+ functions for tailored logic. * Can use post-match guard conditions to
105
+ filter the results of multi-row decision output.</p>
106
+
107
+ <p>### Why use <code>csv_decision</code>?</p>
108
+
109
+ <p>Typical “business logic” is notoriously illogical – full of corner cases
110
+ and one-off exceptions. A decision table can capture data-based decisions
111
+ in a way that comes more naturally to subject matter experts, who
112
+ typically prefer spreadsheet models. Business logic may then be
113
+ encapsulated, avoiding the need to write tortuous conditional expressions
114
+ in Ruby that draw the ire of <code>rubocop</code> and its ilk.</p>
115
+
116
+ <p>This gem takes its inspiration from <a
117
+ href="https://github.com/jmettraux/rufus-decision">rufus/decision</a>.
118
+ (That gem is no longer maintained and has issues with execution
119
+ performance.)</p>
120
+
121
+ <p>### Installation</p>
122
+
123
+ <p>To get started, just add <code>csv_decision</code> to your
124
+ <code>Gemfile</code>, and then run <code>bundle</code>:</p>
125
+
126
+ <p><code>ruby gem &#39;csv_decision&#39;, &#39;~&gt; 0.0.1&#39; </code></p>
127
+
128
+ <p>or simply <code>bash gem install csv_decision </code></p>
129
+
130
+ <p>### Simple example</p>
131
+
132
+ <p>A decision table may be as simple or as complex as you like (although very
133
+ complex tables defeat the whole purpose). Basic usage will be illustrated
134
+ by an example taken from: <a
135
+ href="https://jmettraux.wordpress.com/2009/04/25/rufus-decision-11-ruby-decision-tables">jmettraux.wordpress.com/2009/04/25/rufus-decision-11-ruby-decision-tables</a>/.</p>
136
+
137
+ <p>This example considers two input conditions: <code>topic</code> and
138
+ <code>region</code>. These are labeled <code>in</code>. Certain
139
+ combinations yield an output value for <code>team_member</code>, labeled
140
+ <code>out</code>.</p>
141
+
142
+ <pre class="code ruby"><code class="ruby">in :topic | in :region | out :team_member
143
+ ----------+-------------+-----------------
144
+ sports | Europe | Alice
145
+ sports | | Bob
146
+ finance | America | Charlie
147
+ finance | Europe | Donald
148
+ finance | | Ernest
149
+ politics | Asia | Fujio
150
+ politics | America | Gilbert
151
+ politics | | Henry
152
+ | | Zach</code></pre>
153
+
154
+ <p>When the topic is <code>finance</code> and the region is
155
+ <code>Europe</code> the team member <code>Donald</code> is selected.</p>
156
+
157
+ <p>This is a “first match” decision table in that as soon as a match is made
158
+ execution stops and a single output value (hash) is returned.</p>
159
+
160
+ <p>The ordering of rows matters. <code>Ernest</code>, who is in charge of
161
+ <code>finance</code> for the rest of the world, except for
162
+ <code>America</code> and <code>Europe</code>, <em>must</em> come after his
163
+ colleagues <code>Charlie</code> and <code>Donald</code>. <code>Zach</code>
164
+ has been placed last, catching all the input combos not matching any other
165
+ row.</p>
166
+
167
+ <p>Now for some code.</p>
168
+
169
+ <p>“`ruby # Valid CSV string data = &lt;&lt;~DATA in :topic, in :region,
170
+ out :team_member sports, Europe, Alice sports, , Bob finance, America,
171
+ Charlie finance, Europe, Donald finance, , Ernest politics, Asia, Fujio
172
+ politics, America, Gilbert politics, , Henry , , Zach DATA</p>
173
+
174
+ <p>table = CSVDecision.parse(data)</p>
175
+
176
+ <p>table.decide(topic: &#39;finance&#39;, region: &#39;Europe&#39;) # returns
177
+ team_member: &#39;Donald&#39; table.decide(topic: &#39;sports&#39;,
178
+ region: nil) # returns team_member: &#39;Bob&#39; table.decide(topic:
179
+ &#39;culture&#39;, region: &#39;America&#39;) # team_member: &#39;Zach&#39;
180
+ “`</p>
181
+
182
+ <p>An empty <code>in</code> cell means “matches any value”.</p>
183
+
184
+ <p>If you have cloned this gem&#39;s git repo, then this example can also be
185
+ run by loading the table from a CSV file:</p>
186
+
187
+ <p><code>ruby table =
188
+ CSVDecision.parse(Pathname(&#39;spec/data/valid/simple_example.csv&#39;))
189
+ </code></p>
190
+
191
+ <p>We can also load this same table using the option: <code>first_match:
192
+ false</code>.</p>
193
+
194
+ <p><code>ruby table = CSVDecision.parse(data, first_match: false)
195
+ table.decide(topic: &#39;finance&#39;, region: &#39;Europe&#39;) # returns
196
+ team_member: %w[Donald Ernest Zach] </code></p>
197
+
198
+ <p>For more examples see <code>spec/csv_decision/table_spec.rb</code>.
199
+ Complete documentation of all table parameters is in the code - see
200
+ <code>lib/csv_decision/parse.rb</code> and
201
+ <code>lib/csv_decision/table.rb</code>.</p>
202
+
203
+ <p>### Constants other than strings Although <code>csv_decision</code> is
204
+ string oriented, it does recognise other types of constant present in the
205
+ input hash. Specifically, the following classes are recognized:
206
+ <code>Integer</code>, <code>BigDecimal</code>, <code>NilClass</code>,
207
+ <code>TrueClass</code> and <code>FalseClass</code>.</p>
208
+
209
+ <p>This is accomplished by prefixing the value with one of the operators
210
+ <code>=</code>, <code>==</code> or <code>:=</code>. (The syntax is
211
+ intentionally lax.)</p>
212
+
213
+ <p>For example: “`ruby data = &lt;&lt;~DATA in :constant, out :value
214
+ :=nil, :=nil ==false, ==false =true, =true = 0, = 0 :=100.0, :=100.0
215
+ DATA</p>
216
+
217
+ <p>table = CSVDecision.parse(data) table.decide(constant: nil) # returns
218
+ value: nil<br> table.decide(constant: 0) # returns value: 0<br>
219
+ table.decide(constant: BigDecimal(&#39;100.0&#39;)) # returns value:
220
+ BigDecimal(&#39;100.0&#39;)<br> “`</p>
221
+
222
+ <p>### Column header symbols All input and output column names are
223
+ symbolized, and can be used to form simple expressions that refer to
224
+ values in the input hash.</p>
225
+
226
+ <p>For example: “`ruby data = &lt;&lt;~DATA in :node, in :parent, out :top?
227
+ , == :node, yes , , no DATA</p>
228
+
229
+ <pre class="code ruby"><code class="ruby"><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>
230
+ <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'>node:</span> <span class='int'>0</span><span class='comma'>,</span> <span class='label'>parent:</span> <span class='int'>0</span><span class='rparen'>)</span> <span class='comment'># returns top?: &#39;yes&#39;
231
+ </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'>node:</span> <span class='int'>1</span><span class='comma'>,</span> <span class='label'>parent:</span> <span class='int'>0</span><span class='rparen'>)</span> <span class='comment'># returns top?: &#39;no&#39;
232
+ </span></code></pre>
233
+
234
+ <p>“`</p>
235
+
236
+ <p>Note that there is no need to include an input column for
237
+ <code>:node</code> in the decision table - it just needs to be present in
238
+ the input hash. Also, <code>== :node</code> can be shortened to just
239
+ <code>:node</code>, so the above decision table may be simplified to:</p>
240
+
241
+ <p><code>ruby data = &lt;&lt;~DATA in :parent, out :top?
242
+ :node, yes , no DATA </code> These comparison
243
+ operators are also supported: <code>!=</code>, <code>&gt;</code>,
244
+ <code>&gt;=</code>, <code>&lt;</code>, <code>&lt;=</code>. For more simple
245
+ examples see <code>spec/csv_decision/examples_spec.rb</code>.</p>
246
+
247
+ <p>### Testing</p>
248
+
249
+ <p><code>csv_decision</code> includes thorough <a
250
+ href="http://rspec.info">RSpec</a> tests:</p>
251
+
252
+ <p><code>bash # Execute within a clone of the csv_decision Git repository:
253
+ bundle install rspec </code></p>
254
+ </div></div>
255
+
256
+ <div id="footer">
257
+ Generated on Tue Dec 26 18:31:36 2017 by
258
+ <a href="http://yardoc.org" title="Yay! A Ruby Documentation Tool" target="_parent">yard</a>
259
+ 0.9.12 (ruby-2.3.0).
260
+ </div>
261
+
262
+ </div>
263
+ </body>
264
+ </html>