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 CHANGED
@@ -5,3 +5,4 @@ pkg/*
5
5
  .redcar
6
6
  vendor/ruby
7
7
  bin
8
+ .rvmrc
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
@@ -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
@@ -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
- @criteria = criteria
8
- @result = result
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
- @rules = rules
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 = FasterCSV.parse(csv)
15
- keys = data.shift
21
+ data = CSV.parse(csv)
22
+ Ruleset.new(data)
16
23
  end
17
24
  end
18
25
  end
@@ -1,3 +1,3 @@
1
1
  module DecisionTable
2
- VERSION = "0.0.1"
2
+ VERSION = "0.0.2"
3
3
  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
- pending "maybe later"
28
- # Given(:csv_data) { File.read(File.join(File.dirname(__FILE__), "rules.csv")) }
29
- # When(:ruleset) { Ruleset.parse_csv(csv_data) }
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
@@ -1,6 +1,7 @@
1
1
  require 'rubygems'
2
2
  require 'bundler/setup'
3
3
  require 'rspec/given'
4
+ require 'pry'
4
5
  require 'decision_table' # and any other gems you need
5
6
 
6
7
  RSpec.configure do |config|
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.1
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: 2012-04-13 00:00:00.000000000 Z
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: &70177089993400 !ruby/object:Gem::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: *70177089993400
24
+ version_requirements: *70175504320560
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: rspec-given
27
- requirement: &70177089992480 !ruby/object:Gem::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: *70177089992480
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: &70177089991940 !ruby/object:Gem::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: *70177089991940
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