CSV Decision
CSV based Ruby decision tables (a lightweight Hash transformation gem)
csv_decision is a RubyGem for CSV (comma separated values) based
decision tables.
It accepts decision tables implemented as a
CSV file,
which can then be used to execute complex conditional logic against an input hash,
producing a decision as an output hash.
### CSV Decision features
- Fast decision-time performance (see
benchmark.rb). - In addition to simple string matching, can match common Ruby constants, regular expressions, numeric comparisons and Ruby-style ranges.
- Can use column symbols in comparisons for guard conditions -- e.g., > :column.
- Accepts data as a file, CSV string or an array of arrays. (For safety all input data is force encoded to UTF-8, and non-ascii strings are converted to empty strings.)
- All CSV cells are parsed for correctness, and helpful error messages generated for bad inputs.
- Either returns the first matching row as a hash, or accumulates all matches as an array of hashes.
### Planned features
csv_decision is still a work in progress, and will be enhanced to support
the following features:
- Input columns may be indexed for faster lookup performance.
- May use functions in the output columns to formulate the final decision.
- Input hash values may be conditionally defaulted using a constant or a function call
- Use of column symbol references or built-in guard functions in the input columns for matching.
- Output columns may use interpolated strings referencing column symbols.
- May be extended with user-defined Ruby functions for tailored logic.
- Can use post-match guard conditions to filter the results of multi-row decision output.
### Why use csv_decision?
Typical "business logic" is notoriously illogical -- full of corner cases and one-off
exceptions.
A decision table can capture data-based decisions in a way that comes more naturally
to subject matter experts, who typically prefer spreadsheet models.
Business logic may then be encapsulated, avoiding the need to write tortuous
conditional expressions in Ruby that draw the ire of rubocop and its ilk.
This gem takes its inspiration from rufus/decision. (That gem is no longer maintained and has issues with execution performance.)
### Installation
To get started, just add csv_decision to your Gemfile, and then run bundle:
gem 'csv_decision', '~> 0.0.1'
or simply
gem install csv_decision
### Simple example
A decision table may be as simple or as complex as you like (although very complex tables defeat the whole purpose). Basic usage will be illustrated by an example taken from: https://jmettraux.wordpress.com/2009/04/25/rufus-decision-11-ruby-decision-tables/.
This example considers two input conditions: topic and region.
These are labeled in. Certain combinations yield an output value for team_member,
labeled out.
in :topic | in :region | out :team_member
----------+-------------+-----------------
sports | Europe | Alice
sports | | Bob
finance | America | Charlie
finance | Europe | Donald
finance | | Ernest
politics | Asia | Fujio
politics | America | Gilbert
politics | | Henry
| | Zach
When the topic is finance and the region is Europe the team member Donald
is selected.
This is a "first match" decision table in that as soon as a match is made execution stops and a single output value (hash) is returned.
The ordering of rows matters. Ernest, who is in charge of finance for the rest of
the world, except for America and Europe, must come after his colleagues
Charlie and Donald. Zach has been placed last, catching all the input combos
not matching any other row.
Now for some code.
# Valid CSV string
data = " in :topic, in :region, out :team_member\n sports, Europe, Alice\n sports, , Bob\n finance, America, Charlie\n finance, Europe, Donald\n finance, , Ernest\n politics, Asia, Fujio\n politics, America, Gilbert\n politics, , Henry\n , , Zach\n DATA\n\n table = CSVDecision.parse(data)\n\n table.decide(topic: 'finance', region: 'Europe') # returns team_member: 'Donald'\n table.decide(topic: 'sports', region: nil) # returns team_member: 'Bob'\n table.decide(topic: 'culture', region: 'America') # team_member: 'Zach'\n"
An empty in cell means "matches any value".
If you have cloned this gem's git repo, then this example can also be run by loading the table from a CSV file:
table = CSVDecision.parse(Pathname('spec/data/valid/simple_example.csv'))
We can also load this same table using the option: first_match: false.
table = CSVDecision.parse(data, first_match: false)
table.decide(topic: 'finance', region: 'Europe') # returns team_member: %w[Donald Ernest Zach]
For more examples see spec/csv_decision/table_spec.rb.
Complete documentation of all table parameters is in the code - see
lib/csv_decision/parse.rb and lib/csv_decision/table.rb.
### Constants other than strings
Although csv_decision is string oriented, it does recognise other types of constant
present in the input hash. Specifically, the following classes are recognized:
Integer, BigDecimal, NilClass, TrueClass and FalseClass.
This is accomplished by prefixing the value with one of the operators =, == or :=.
(The syntax is intentionally lax.)
For example:
data = " in :constant, out :value\n :=nil, :=nil\n ==false, ==false\n =true, =true\n = 0, = 0\n :=100.0, :=100.0\n DATA\n\n table = CSVDecision.parse(data)\n table.decide(constant: nil) # returns value: nil \n table.decide(constant: 0) # returns value: 0 \n table.decide(constant: BigDecimal('100.0')) # returns value: BigDecimal('100.0') \n"
### Column header symbols All input and output column names are symbolized, and can be used to form simple expressions that refer to values in the input hash.
For example:
data = " in :node, in :parent, out :top?\n , == :node, yes\n , , no\n DATA\n\n table = CSVDecision.parse(data)\n table.decide(node: 0, parent: 0) # returns top?: 'yes'\n table.decide(node: 1, parent: 0) # returns top?: 'no'\n"
Note that there is no need to include an input column for :node in the decision
table - it just needs to be present in the input hash. Also, == :node can be
shortened to just :node, so the above decision table may be simplified to:
data = " in :parent, out :top?\n :node, yes\n , no\n DATA\n"
These comparison operators are also supported: !=, >, >=, <, <=.
For more simple examples see spec/csv_decision/examples_spec.rb.
### Testing
csv_decision includes thorough RSpec tests:
# Execute within a clone of the csv_decision Git repository:
bundle install
rspec