Class: Rufus::Decision::Table
- Inherits:
-
Object
- Object
- Rufus::Decision::Table
- Defined in:
- lib/rufus/decision/table.rb
Overview
A decision table is a description of a set of rules as a CSV (comma separated values) file. Such a file can be edited / generated by a spreadsheet (Excel, Google spreadsheets, Gnumeric, …)
Disclaimer
The decision / CSV table system is no replacement for full rule engines with forward and backward chaining, RETE implementation and the like…
Usage
The following CSV file
in:topic,in:region,out:team_member sports,europe,Alice sports,,Bob finance,america,Charly finance,europe,Donald finance,,Ernest politics,asia,Fujio politics,america,Gilbert politics,,Henry ,,Zach
embodies a rule for distributing items (piece of news) labelled with a topic and a region to various members of a team. For example, all news about finance from Europe are to be routed to Donald.
Evaluation occurs row by row. The “in out” row states which field is considered at input and which are to be modified if the “ins” do match.
The default behaviour is to change the value of the “outs” if all the “ins” match and then terminate. An empty “in” cell means “matches any”.
Enough words, some code :
require 'rufus/decision' table = Rufus::Decision::Table.new(%{ in:topic,in:region,out:team_member sports,europe,Alice sports,,Bob finance,america,Charly finance,europe,Donald finance,,Ernest politics,asia,Fujio politics,america,Gilbert politics,,Henry ,,Zach }) h = {} h["topic"] = "politics" table.transform!(h) puts h["team_member"] # will yield "Henry" who takes care of all the politics stuff, # except for Asia and America
’>’, ’>=’, ’<’ and ’<=’ can be put in front of individual cell values :
table = Rufus::Decision::Table.new(%{ , in:fx, out:fy , >100,a >=10,b ,c }) h = { 'fx' => '10' } h = table.transform(h) p h # => { 'fx' => '10', 'fy' => 'b' }
Such comparisons are done after the elements are transformed to float numbers. By default, non-numeric arguments will get compared as Strings.
transform and transform!
The method transform! acts directly on its parameter hash, the method transform will act on a copy of it. Both methods return their transformed hash.
[ruby] ranges
Ruby-like ranges are also accepted in cells.
in:f0,out:result , 0..32,low 33..66,medium 67..100,high
will set the field ‘result’ to ‘low’ for f0 => 24
Options
You can put options on their own in a cell BEFORE the line containing “in:xxx” and “out:yyy” (ins and outs).
Three options are supported, “ignorecase”, “through” and “accumulate”.
- “ignorecase”, if found by the decision table will make any match (in the “in” columns) case unsensitive.
- “through”, will make sure that EVERY row is evaluated and potentially applied. The default behaviour (without “through”), is to stop the evaluation after applying the results of the first matching row.
- “accumulate”, behaves as with “through” set but instead of overriding values each time a match is found, will gather them in an array.
an example of ‘accumulate’
accumulate
in:f0,out:result
,
,normal
>10,large
>100,xl
will yield { result => [ 'normal', 'large' ]} for f0 => 56
- “unbounded”, by default, string matching is ‘bounded’, “apple” will match ‘apple’, but not ‘greenapple’. When “unbounded” is set, ‘greenapple’ will match. (‘bounded’, in reality, means the target value is surrounded by ^ and $)
Setting options at table initialization
It’s OK to set the options at initialization time :
table = Rufus::Decision::Table.new( csv, :ruby_eval => true, :accumulate => true)
Cross references
By using the ‘dollar notation’, it’s possible to reference a value already in the hash (that is, the hash undergoing ‘transformation’).
in:value,in:roundup,out:newvalue
0..32,true,32
33..65,true,65
66..99,true,99
,,${value}
Here, if ‘roundup’ is set to true, newvalue will hold 32, 65 or 99 as value, else it will simply hold the ‘value’.
The value is the value as currently found in the transformed hash, not as found in the original (non-transformed) hash.
Ruby code evaluation
The dollar notation can be used for yet another trick, evaluation of ruby code at transform time.
Note though that this feature is only enabled via the :ruby_eval option of the transform!() method.
decisionTable.transform!(h, :ruby_eval => true)
That decision table may look like :
in:value,in:result
0..32,${r:Time.now.to_f}
33..65,${r:call_that_other_function()}
66..99,${r:${value} * 3}
(It’s a very simplistic example, but I hope it demonstrates the capabilities of this technique)
It’s OK to set the :ruby_eval parameter when initializing the decision table :
table = Rufus::Decision::Table.new(csv, :ruby_eval => true)
so that there is no need to specify it at transform() call time.
See also
Direct Known Subclasses
Constant Summary
- IN =
/^in:/- OUT =
/^out:/- IN_OR_OUT =
/^(in|out):/- NUMERIC_COMPARISON =
/^([><]=?)(.*)$/- RUBY_NUMERIC_RANGE_REGEXP = A regexp for checking if a string is a numeric Ruby range.
Regexp.compile( "^\\d+(\\.\\d+)?\\.{2,3}\\d+(\\.\\d+)?$")
- RUBY_ALPHA_RANGE_REGEXP = A regexp for checking if a string is an alpha Ruby range.
Regexp.compile( "^([A-Za-z])(\\.{2,3})([A-Za-z])$")
Instance Attribute Summary
- - (Object) accumulate when set to true, multiple matches result get accumulated in an array.
- - (Object) first_match when set to true, the transformation process stops after the first match got applied.
- - (Object) ignore_case when set to true, matches evaluation ignores case.
- - (Object) ruby_eval when set to true, evaluation of ruby code for output is allowed.
- - (Object) unbound false (bounded) by default : exact matches for string matching.
Instance Method Summary
- - (Table) initialize(csv, options = {}) constructor The constructor for DecisionTable, you can pass a String, an Array (of arrays), a File object.
- - (Object) to_csv Outputs back this table as a CSV String.
- - (Object) transform(hash) (also: #run) Like transform, but the original hash doesn’t get touched, a copy of it gets transformed and finally returned.
- - (Object) transform!(hash) Passes the hash through the decision table and returns it, transformed.
Constructor Details
- (Table) initialize(csv, options = {})
The constructor for DecisionTable, you can pass a String, an Array (of arrays), a File object. The CSV parser coming with Ruby will take care of it and a DecisionTable instance will be built.
Options are :through, :ignore_case, :accumulate (which forces :through to true when set) and :ruby_eval. See Rufus::Decision::Table for more details.
Options passed to this method do override the options defined in the CSV itself.
options
- :through : when set, all the rows of the decision table are considered
- :ignore_case : case is ignored (not ignored by default)
- :accumulate : gather instead of overriding (implies :through)
- :ruby_eval : ruby code evaluation is OK
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 |
# File 'lib/rufus/decision/table.rb', line 283 def initialize (csv, ={}) @rows = Rufus::Decision.csv_to_a(csv) parse_header_row @first_match = false if [:through] == true @first_match = true if @first_match.nil? set_opt(, :ignore_case, :ignorecase) set_opt(, :accumulate) set_opt(, :ruby_eval) set_opt(, :unbounded) @first_match = false if @accumulate end |
Instance Attribute Details
- (Object) accumulate
when set to true, multiple matches result get accumulated in an array.
252 253 254 |
# File 'lib/rufus/decision/table.rb', line 252 def accumulate @accumulate end |
- (Object) first_match
when set to true, the transformation process stops after the first match got applied.
243 244 245 |
# File 'lib/rufus/decision/table.rb', line 243 def first_match @first_match end |
- (Object) ignore_case
when set to true, matches evaluation ignores case.
247 248 249 |
# File 'lib/rufus/decision/table.rb', line 247 def ignore_case @ignore_case end |
- (Object) ruby_eval
when set to true, evaluation of ruby code for output is allowed. False by default.
257 258 259 |
# File 'lib/rufus/decision/table.rb', line 257 def ruby_eval @ruby_eval end |
- (Object) unbound
false (bounded) by default : exact matches for string matching. When ‘unbounded’, target ‘apple’ will match for values like ‘greenapples’ or ‘apple seed’.
263 264 265 |
# File 'lib/rufus/decision/table.rb', line 263 def unbound @unbound end |
Instance Method Details
- (Object) to_csv
Outputs back this table as a CSV String
330 331 332 333 334 335 |
# File 'lib/rufus/decision/table.rb', line 330 def to_csv @rows.inject([ @header.to_csv ]) { |a, row| a << row.join(',') }.join("\n") end |
- (Object) transform(hash) Also known as: run
Like transform, but the original hash doesn’t get touched, a copy of it gets transformed and finally returned.
305 306 307 308 |
# File 'lib/rufus/decision/table.rb', line 305 def transform (hash) transform!(hash.dup) end |
- (Object) transform!(hash)
Passes the hash through the decision table and returns it, transformed.
313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/rufus/decision/table.rb', line 313 def transform! (hash) hash = Rufus::Decision::EvalHashFilter.new(hash) if @ruby_eval @rows.each do |row| next unless matches?(row, hash) apply(row, hash) break if @first_match end hash.is_a?(Rufus::Decision::HashFilter) ? hash.parent_hash : hash end |