decision_table 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/README.textile +23 -0
- data/Rakefile +10 -0
- data/decision_table.gemspec +2 -0
- data/lib/decision_table/rule.rb +17 -8
- data/lib/decision_table/ruleset.rb +12 -5
- data/lib/decision_table/version.rb +1 -1
- data/spec/rule_spec.rb +13 -7
- data/spec/ruleset_spec.rb +10 -5
- data/spec/spec_helper.rb +1 -0
- metadata +31 -8
data/.gitignore
CHANGED
data/README.textile
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
h1. Decision Table
|
2
|
+
|
3
|
+
decision_table is a gem to build, parse and evaluate simple rules than can be expressed in a table. We've extracted this gem from code on real projects and seen in come in quite handy elsewhere. A simple example is probably the best way to show what it's all about.
|
4
|
+
|
5
|
+
Let's say you're building a system to recommend home improvements to homeowners. For each improvement, there is a table that expresses how to decide if an improvement is recommended. Here's the example rule to determine if replacing insulation in the attic is recommended:
|
6
|
+
|
7
|
+
| is_well_insulated? | attic_accessible? | recommended |
|
8
|
+
| true | | false |
|
9
|
+
| false | true | true |
|
10
|
+
| false | flase | false |
|
11
|
+
|
12
|
+
decision_table will interpret tables like this expressed as a 2D array and produce a DecisionTable::Ruleset.
|
13
|
+
A ruleset, when given a domain object that implements insulation_r_value and attr_accessible? methods,
|
14
|
+
will return the result of the first applicable rule, applicable being defined as a match for all
|
15
|
+
non-blank colums.
|
16
|
+
|
17
|
+
For cases where you only have a single applicable rule, you can express it as string like so:
|
18
|
+
|
19
|
+
bc.. drinks_coffee?:false,writes_ruby?:true,number_of_children:2
|
20
|
+
|
21
|
+
h2. Other stuff you should look at
|
22
|
+
|
23
|
+
This gem is just what you want if you can already express your rules in this format and know the order of precedence. If you don't know this, you might need some fancier machine-learning classification thingie. In this case, might I recommend taking a look at "ai4r":http://ai4r.rubyforge.org. In particular the ID3 decision trees algorithm might be what you need.
|
data/Rakefile
CHANGED
@@ -1 +1,11 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
desc 'Default: run specs.'
|
5
|
+
task :default => :spec
|
6
|
+
|
7
|
+
desc "Run specs"
|
8
|
+
RSpec::Core::RakeTask.new do |t|
|
9
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
10
|
+
# Put spec opts in a file named .rspec in root
|
11
|
+
end
|
data/decision_table.gemspec
CHANGED
@@ -21,5 +21,7 @@ Gem::Specification.new do |s|
|
|
21
21
|
# specify any dependencies here; for example:
|
22
22
|
s.add_development_dependency "rspec"
|
23
23
|
s.add_development_dependency "rspec-given"
|
24
|
+
s.add_development_dependency "pry"
|
25
|
+
s.add_development_dependency "rake"
|
24
26
|
s.add_runtime_dependency "activesupport"
|
25
27
|
end
|
data/lib/decision_table/rule.rb
CHANGED
@@ -1,19 +1,28 @@
|
|
1
1
|
module DecisionTable
|
2
2
|
class Rule
|
3
|
-
|
3
|
+
|
4
4
|
attr_accessor :criteria, :result
|
5
|
-
|
5
|
+
|
6
6
|
def initialize(criteria, result)
|
7
|
-
|
8
|
-
|
7
|
+
if criteria.is_a?(Hash)
|
8
|
+
@criteria = criteria
|
9
|
+
@result = result
|
10
|
+
else
|
11
|
+
@criteria = {}
|
12
|
+
keys = criteria
|
13
|
+
values = result
|
14
|
+
keys.pop if keys.size == values.size
|
15
|
+
keys.each_with_index { |key, index| @criteria[key] = values[index] }
|
16
|
+
@result = values.last
|
17
|
+
end
|
9
18
|
end
|
10
|
-
|
19
|
+
|
11
20
|
def applies?(candidate)
|
12
21
|
criteria.all? do |k, v|
|
13
22
|
v.blank? || matches?(candidate, k, v)
|
14
23
|
end
|
15
24
|
end
|
16
|
-
|
25
|
+
|
17
26
|
def matches?(candidate, key, value)
|
18
27
|
if value.to_s == "true"
|
19
28
|
candidate.send(key)
|
@@ -23,7 +32,7 @@ module DecisionTable
|
|
23
32
|
candidate.send(key) == value
|
24
33
|
end
|
25
34
|
end
|
26
|
-
|
35
|
+
|
27
36
|
def self.parse(rule_string)
|
28
37
|
criteria = {}
|
29
38
|
rule_string.split(",").each do |crit|
|
@@ -32,6 +41,6 @@ module DecisionTable
|
|
32
41
|
end
|
33
42
|
Rule.new(criteria, true)
|
34
43
|
end
|
35
|
-
|
44
|
+
|
36
45
|
end
|
37
46
|
end
|
@@ -1,18 +1,25 @@
|
|
1
|
+
require 'csv'
|
2
|
+
|
1
3
|
module DecisionTable
|
2
4
|
class Ruleset
|
3
5
|
attr_accessor :rules
|
4
6
|
def initialize(rules)
|
5
|
-
|
7
|
+
if rules[0].is_a?(Rule)
|
8
|
+
@rules = rules
|
9
|
+
else
|
10
|
+
keys = rules.shift
|
11
|
+
@rules = rules.map { |values| Rule.new(keys, values) }
|
12
|
+
end
|
6
13
|
end
|
7
|
-
|
14
|
+
|
8
15
|
def evaluate(candidate)
|
9
16
|
rule = rules.detect { |rule| rule.applies?(candidate) }
|
10
17
|
rule.result if rule
|
11
18
|
end
|
12
|
-
|
19
|
+
|
13
20
|
def self.parse_csv(csv)
|
14
|
-
data =
|
15
|
-
|
21
|
+
data = CSV.parse(csv)
|
22
|
+
Ruleset.new(data)
|
16
23
|
end
|
17
24
|
end
|
18
25
|
end
|
data/spec/rule_spec.rb
CHANGED
@@ -2,30 +2,36 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe DecisionTable::Rule do
|
4
4
|
Given(:candidate) { double(:candidate, :foo => "bar", :bing => "baz") }
|
5
|
-
|
5
|
+
|
6
|
+
describe "constructing from arrays" do
|
7
|
+
Given(:rule) { DecisionTable::Rule.new(["foo", "bing", "outcome"], ["bar", "baz", "good"])}
|
8
|
+
Then { rule.applies?(candidate).should be_true }
|
9
|
+
Then { rule.result.should == "good" }
|
10
|
+
end
|
11
|
+
|
6
12
|
describe("applies") do
|
7
13
|
context "when all values match" do
|
8
14
|
Given(:rule) { DecisionTable::Rule.new({:foo => "bar", :bing => "baz"}, true) }
|
9
15
|
Then { rule.applies?(candidate).should be_true}
|
10
16
|
end
|
11
|
-
|
17
|
+
|
12
18
|
context "when all specified values match" do
|
13
19
|
Given(:rule ) { DecisionTable::Rule.new({:foo => "bar", :bing => ""}, true) }
|
14
20
|
Then { rule.applies?(candidate).should be_true}
|
15
21
|
end
|
16
|
-
|
22
|
+
|
17
23
|
context "when values don't match" do
|
18
24
|
Given(:rule) { DecisionTable::Rule.new({:foo => "not bar", :bing => "baz"}, true) }
|
19
25
|
Then { rule.applies?(candidate).should be_false}
|
20
26
|
end
|
21
|
-
|
27
|
+
|
22
28
|
end
|
23
|
-
|
29
|
+
|
24
30
|
describe("parsing rule") do
|
25
31
|
Given(:rule_string) { "foo:bar,bing:baz" }
|
26
32
|
When(:rule) { DecisionTable::Rule.parse(rule_string) }
|
27
33
|
Then { rule.applies?(candidate).should be_true }
|
28
|
-
|
34
|
+
|
29
35
|
context "with boolean rules" do
|
30
36
|
Given(:candidate) { double(:candidate, foo: true, bar: false) }
|
31
37
|
Given(:non_matching) { double(:candidate, foo: nil, bar: true) }
|
@@ -34,5 +40,5 @@ describe DecisionTable::Rule do
|
|
34
40
|
Then { rule.applies?(non_matching).should be_false }
|
35
41
|
end
|
36
42
|
end
|
37
|
-
|
43
|
+
|
38
44
|
end
|
data/spec/ruleset_spec.rb
CHANGED
@@ -22,12 +22,17 @@ module DecisionTable
|
|
22
22
|
Then { Ruleset.new([rule]).evaluate(candidate).should be_nil }
|
23
23
|
end
|
24
24
|
end
|
25
|
-
|
25
|
+
|
26
|
+
describe "instantiating from 2d array of strings" do
|
27
|
+
Given(:ruleset) { Ruleset.new [["foo", "baz", "result"],["bar", "bing", true]]}
|
28
|
+
Then { ruleset.rules.size.should == 1}
|
29
|
+
Then { ruleset.evaluate(candidate).should == true }
|
30
|
+
end
|
31
|
+
|
26
32
|
describe "parsing CSV" do
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
# Then { ruleset.rules.size.should == 2}
|
33
|
+
Given(:csv_data) { File.read(File.join(File.dirname(__FILE__), "rules.csv")) }
|
34
|
+
When(:ruleset) { Ruleset.parse_csv(csv_data) }
|
35
|
+
Then { ruleset.rules.size.should == 2}
|
31
36
|
end
|
32
37
|
end
|
33
38
|
end
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: decision_table
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2013-06-11 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: rspec
|
16
|
-
requirement: &
|
16
|
+
requirement: &70175504320560 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :development
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70175504320560
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: rspec-given
|
27
|
-
requirement: &
|
27
|
+
requirement: &70175504314500 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,32 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :development
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70175504314500
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: pry
|
38
|
+
requirement: &70175500209920 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
type: :development
|
45
|
+
prerelease: false
|
46
|
+
version_requirements: *70175500209920
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rake
|
49
|
+
requirement: &70175500204980 !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
type: :development
|
56
|
+
prerelease: false
|
57
|
+
version_requirements: *70175500204980
|
36
58
|
- !ruby/object:Gem::Dependency
|
37
59
|
name: activesupport
|
38
|
-
requirement: &
|
60
|
+
requirement: &70175500201560 !ruby/object:Gem::Requirement
|
39
61
|
none: false
|
40
62
|
requirements:
|
41
63
|
- - ! '>='
|
@@ -43,7 +65,7 @@ dependencies:
|
|
43
65
|
version: '0'
|
44
66
|
type: :runtime
|
45
67
|
prerelease: false
|
46
|
-
version_requirements: *
|
68
|
+
version_requirements: *70175500201560
|
47
69
|
description: If you have decision algorithm that can be easily expressed in a table,
|
48
70
|
this gem can help
|
49
71
|
email:
|
@@ -54,6 +76,7 @@ extra_rdoc_files: []
|
|
54
76
|
files:
|
55
77
|
- .gitignore
|
56
78
|
- Gemfile
|
79
|
+
- README.textile
|
57
80
|
- Rakefile
|
58
81
|
- decision_table.gemspec
|
59
82
|
- lib/decision_table.rb
|