ruleby 0.9.b4 → 0.9.b7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +5 -0
  2. data/GPL.txt +341 -0
  3. data/LICENSE.txt +52 -0
  4. data/README.markdown +22 -0
  5. data/benchmarks/basic_rules.rb +61 -0
  6. data/benchmarks/joined_rules.rb +66 -0
  7. data/benchmarks/miss_manners/data.rb +146 -0
  8. data/benchmarks/miss_manners/miss_manners.rb +33 -0
  9. data/benchmarks/miss_manners/model.rb +193 -0
  10. data/benchmarks/miss_manners/rules.rb +105 -0
  11. data/benchmarks/model.rb +36 -0
  12. data/examples/diagnosis.rb +129 -0
  13. data/examples/fibonacci_example1.rb +44 -0
  14. data/examples/fibonacci_example2.rb +40 -0
  15. data/examples/fibonacci_example3.rb +78 -0
  16. data/examples/fibonacci_rulebook.rb +84 -0
  17. data/examples/hello.rb +45 -0
  18. data/examples/ticket.rb +113 -0
  19. data/examples/wordgame.rb +107 -0
  20. data/lib/core/engine.rb +26 -24
  21. data/lib/core/nodes.rb +77 -35
  22. data/lib/ruleby.rb +0 -37
  23. data/ruleby.gemspec +17 -0
  24. data/spec/and_or_spec.rb +252 -0
  25. data/spec/coercion_spec.rb +5 -0
  26. data/spec/collect_spec.rb +1021 -0
  27. data/spec/errors_spec.rb +148 -0
  28. data/spec/ferrari_spec.rb +39 -0
  29. data/spec/function_spec.rb +199 -0
  30. data/spec/hello_spec.rb +34 -0
  31. data/spec/node_sharing_spec.rb +53 -0
  32. data/spec/property_spec.rb +69 -0
  33. data/spec/spec.opts +4 -0
  34. data/spec/spec_helper.rb +9 -0
  35. data/tasks/documentation.rake +32 -0
  36. data/tasks/rspec.rake +21 -0
  37. data/tasks/test.rake +9 -0
  38. data/tests/assert_facts.rb +130 -0
  39. data/tests/common.rb +29 -0
  40. data/tests/duck_type.rb +79 -0
  41. data/tests/gets.rb +48 -0
  42. data/tests/join_nodes.rb +63 -0
  43. data/tests/nil.rb +72 -0
  44. data/tests/not_patterns.rb +91 -0
  45. data/tests/or_patterns.rb +154 -0
  46. data/tests/regex.rb +47 -0
  47. data/tests/self_reference.rb +54 -0
  48. metadata +81 -49
@@ -0,0 +1,113 @@
1
+ # This file is part of the Ruleby project (http://ruleby.org)
2
+ #
3
+ # This application is free software; you can redistribute it and/or
4
+ # modify it under the terms of the Ruby license defined in the
5
+ # LICENSE.txt file.
6
+ #
7
+ # Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
8
+ #
9
+ # * Authors: Joe Kutner, Matt Smith
10
+ #
11
+
12
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/')
13
+ require 'ruleby'
14
+
15
+ include Ruleby
16
+
17
+ class Customer
18
+ def initialize(name,subscription)
19
+ @name = name
20
+ @subscription = subscription
21
+ end
22
+ attr_reader :name,:subscription
23
+ def to_s
24
+ return '[Customer ' + @name.to_s + ' : ' + @subscription.to_s + ']';
25
+ end
26
+ end
27
+
28
+ class Ticket
29
+ def initialize(customer)
30
+ @customer = customer
31
+ @status = :New
32
+ end
33
+ attr :status, true
34
+ attr_reader :customer
35
+ def to_s
36
+ return '[Ticket ' + @customer.to_s + ' : ' + @status.to_s + ']';
37
+ end
38
+ end
39
+
40
+ # This example is used in JBoss-Rules to demonstrate durations and the use of
41
+ # custom DSL. We are simply using it here to demonstrate another example.
42
+ class TroubleTicketRulebook < Rulebook
43
+ def rules
44
+
45
+ # This is uses the letigre syntax... but we can mix and match syntaxes in
46
+ # the same rule set.
47
+ rule :New_Ticket, {:priority => 10}, # :duration => 10},
48
+ [Customer, :c],
49
+ [Ticket, :ticket, {m.customer => :c}, m.status == :New] do |vars|
50
+ puts 'New : ' + vars[:ticket].to_s
51
+ end
52
+
53
+ # Now we are using the ferrari syntax. The rule method can detect which
54
+ # syntax we are using, and compile accordingly.
55
+ rule :Silver_Priority, #{:duration => 3000},
56
+ [Customer, :customer, m.subscription == 'Silver'],
57
+ [Ticket,:ticket, m.customer == b(:customer), m.status == :New] do |vars|
58
+ vars[:ticket].status = :Escalate
59
+ modify vars[:ticket]
60
+ end
61
+
62
+ rule :Gold_Priority, #{:duration => 1000},
63
+ [Customer, :customer, m.subscription == 'Gold'],
64
+ [Ticket,:ticket, m.customer == b(:customer), m.status == :New] do |vars|
65
+ vars[:ticket].status = :Escalate
66
+ modify vars[:ticket]
67
+ end
68
+
69
+ rule :Platinum_Priority,
70
+ [Customer, :customer, m.subscription == 'Platinum'],
71
+ [Ticket,:ticket, m.customer == b(:customer), m.status == :New] do |vars|
72
+ vars[:ticket].status = :Escalate
73
+ modify vars[:ticket]
74
+ end
75
+
76
+ rule :Escalate,
77
+ [Customer, :c],
78
+ [Ticket, :ticket, {m.customer => :c}, m.status == :Escalate] do |vars|
79
+ puts 'Email : ' + vars[:ticket].to_s
80
+ end
81
+
82
+ rule :Done,
83
+ [Customer, :c],
84
+ [Ticket, :ticket, {m.customer => :c}, m.status == :Done] do |vars|
85
+ puts 'Done : ' + vars[:ticket].to_s
86
+ end
87
+ end
88
+ end
89
+
90
+ # FACTS
91
+
92
+ a = Customer.new('A', 'Gold')
93
+ b = Customer.new('B', 'Platinum')
94
+ c = Customer.new('C', 'Silver')
95
+ d = Customer.new('D', 'Silver')
96
+
97
+ t1 = Ticket.new(a)
98
+ t2 = Ticket.new(b)
99
+ t3 = Ticket.new(c)
100
+ t4 = Ticket.new(d)
101
+
102
+ engine :engine do |e|
103
+ TroubleTicketRulebook.new(e).rules
104
+ e.assert a
105
+ e.assert b
106
+ e.assert c
107
+ e.assert d
108
+ e.assert t1
109
+ e.assert t2
110
+ e.assert t3
111
+ e.assert t4
112
+ e.match
113
+ end
@@ -0,0 +1,107 @@
1
+ # This file is part of the Ruleby project (http://ruleby.org)
2
+ #
3
+ # This application is free software; you can redistribute it and/or
4
+ # modify it under the terms of the Ruby license defined in the
5
+ # LICENSE.txt file.
6
+ #
7
+ # Copyright (c) 2007 Joe Kutner and Matt Smith. All rights reserved.
8
+ #
9
+ # * Authors: Joe Kutner
10
+ #
11
+
12
+
13
+ # This example solves the number puzzle problem where
14
+ # GERALD
15
+ # + DONALD
16
+ # ------
17
+ # = ROBERT
18
+
19
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '../lib/')
20
+ require 'ruleby'
21
+
22
+ include Ruleby
23
+
24
+ class Combination
25
+ attr:a
26
+ attr:x
27
+ def initialize(a,x)
28
+ @a = a
29
+ @x = x
30
+ end
31
+ end
32
+
33
+ class WordGameRulebook < Ruleby::Rulebook
34
+ def rules
35
+ rule :generate_combos, [String, :a], [Fixnum, :x] do |v|
36
+ assert Combination.new(v[:a], v[:x])
37
+ end
38
+
39
+ c1 = c{|t,d| ((d+d) % 10) == t }
40
+ c2 = c{|r,d,t,l| ((d+d+(10*l)+(10*l)) % 100) == ((10 * r) + t) }
41
+ c3 = c{|e,d,l,a,r,t| ((d+d+(10*l)+(10*l)+(100*a)+(100*a)) % 1000) == ((100*e)+(10*r)+t) }
42
+ c4 = c{|b,d,l,a,r,n,e,t| ((d+d+(10*l)+(10*l)+(100*a)+(100*a)+(1000*r)+(1000*n)) % 10000) == ((1000*b)+(100*e)+(10*r)+t) }
43
+ c5 = c{|g,d,l,a,r,n,e,o,b,t| (d+d+(10*l)+(10*l)+(100*a)+(100*a)+(1000*r)+(1000*n)+(10000*e)+(10000*o)+(100000*g)+(100000*d)) == ((100000*r)+(10000*o)+(1000*b)+(100*e)+(10*r)+t) }
44
+
45
+ rule :find_solution,
46
+ [Combination, m.a=='D', {m.x => :d}],
47
+ [Combination, m.a=='T', {m.x.not==b(:d)=>:t}, m.x(:d, &c1)],
48
+ [Combination, m.a=='L', {m.x.not==b(:d)=>:l}, m.x.not==b(:t)],
49
+ [Combination, m.a=='R', {m.x.not==b(:d)=>:r}, m.x.not==b(:t), m.x.not==b(:l),
50
+ m.x(:d,:t,:l, &c2)],
51
+ [Combination, m.a=='A', {m.x.not==b(:d)=>:a}, m.x.not==b(:t), m.x.not==b(:l), m.x.not==b(:r)],
52
+ [Combination, m.a=='E', {m.x.not==b(:d)=>:e}, m.x.not==b(:t), m.x.not==b(:l), m.x.not==b(:r),
53
+ m.x.not==b(:a), m.x(:d,:l,:a,:r,:t, &c3)],
54
+ [Combination, m.a=='N', {m.x.not==b(:d)=>:n}, m.x.not==b(:t), m.x.not==b(:l), m.x.not==b(:r),
55
+ m.x.not==b(:a), m.x.not==b(:e)],
56
+ [Combination, m.a=='B', {m.x.not==b(:d)=>:b}, m.x.not==b(:t), m.x.not==b(:l), m.x.not==b(:r),
57
+ m.x.not==b(:a), m.x.not==b(:e), m.x.not==b(:n), m.x(:d,:l,:a,:r,:n,:e,:t, &c4)],
58
+ [Combination, m.a=='O', {m.x.not==b(:d)=>:o}, m.x.not==b(:t), m.x.not==b(:l), m.x.not==b(:r),
59
+ m.x.not==b(:a), m.x.not==b(:e), m.x.not==b(:n), m.x.not==b(:b)],
60
+ [Combination, m.a=='G', {m.x.not==b(:d)=>:g}, m.x.not==b(:t), m.x.not==b(:l), m.x.not==b(:r),
61
+ m.x.not==b(:a), m.x.not==b(:e), m.x.not==b(:n), m.x.not==b(:b), m.x.not==b(:o),
62
+ m.x(:d,:l,:a,:r,:n,:e,:o,:b,:t, &c5)] do |v|
63
+ puts "One Solution is:"
64
+ puts " G = #{v[:g]}"
65
+ puts " E = #{v[:e]}"
66
+ puts " R = #{v[:r]}"
67
+ puts " A = #{v[:a]}"
68
+ puts " L = #{v[:l]}"
69
+ puts " D = #{v[:d]}"
70
+ puts " O = #{v[:o]}"
71
+ puts " N = #{v[:n]}"
72
+ puts " B = #{v[:b]}"
73
+ puts " T = #{v[:t]}"
74
+ puts ""
75
+ puts " #{v[:g]} #{v[:e]} #{v[:r]} #{v[:a]} #{v[:l]} #{v[:d]}"
76
+ puts " + #{v[:d]} #{v[:o]} #{v[:n]} #{v[:a]} #{v[:l]} #{v[:d]}"
77
+ puts " ------"
78
+ puts " = #{v[:r]} #{v[:o]} #{v[:b]} #{v[:e]} #{v[:r]} #{v[:t]}"
79
+ end
80
+ end
81
+ end
82
+
83
+ e = engine :e do |e|
84
+ WordGameRulebook.new(e).rules
85
+ e.assert 0
86
+ e.assert 1
87
+ e.assert 2
88
+ e.assert 3
89
+ e.assert 4
90
+ e.assert 5
91
+ e.assert 6
92
+ e.assert 7
93
+ e.assert 8
94
+ e.assert 9
95
+ e.assert 'G'
96
+ e.assert 'E'
97
+ e.assert 'R'
98
+ e.assert 'A'
99
+ e.assert 'L'
100
+ e.assert 'D'
101
+ e.assert 'O'
102
+ e.assert 'N'
103
+ e.assert 'B'
104
+ e.assert 'T'
105
+ end
106
+
107
+ e.match
@@ -73,11 +73,11 @@ module Ruleby
73
73
  i = 0; while @match.recency[i] == a2.match.recency[i] && i < @match.recency.size-1 && i < a2.match.recency.size-1
74
74
  i += 1
75
75
  end
76
- return @match.recency[i] <=> a2.match.recency[i]
76
+ @match.recency[i] <=> a2.match.recency[i]
77
77
  end
78
78
 
79
79
  def ==(a2)
80
- return a2 != nil && @action == a2.action && @match == a2.match
80
+ a2 != nil && @action == a2.action && @match == a2.match
81
81
  end
82
82
 
83
83
  def modify(match)
@@ -110,12 +110,10 @@ module Ruleby
110
110
  # A fact is an object that is stored in working memory. The rules in the
111
111
  # system will either look for the existence or absence of particular facts.
112
112
  class Fact
113
- attr :token, true
114
- attr :recency, true
113
+ attr :recency, true
115
114
  attr_reader :object
116
115
 
117
- def initialize(object, token)
118
- @token = token
116
+ def initialize(object)
119
117
  @object = object
120
118
  end
121
119
 
@@ -163,20 +161,19 @@ module Ruleby
163
161
  end
164
162
 
165
163
  def assert_fact(fact)
166
- raise 'The fact asserted cannot be nil!' unless fact.object
167
- if (fact.token == :plus)
168
- fact.recency = @recency
169
- @recency += 1
170
- @facts.push fact
171
- return fact
172
- else #if (fact.token == :minus)
173
- i = @facts.index(fact)
174
- raise 'The fact to remove does not exist!' unless i
175
- existing_fact = @facts[i]
176
- @facts.delete_at(i)
177
- existing_fact.token = fact.token
178
- return existing_fact
179
- end
164
+ raise 'The fact asserted cannot be nil!' if fact.object.nil?
165
+ fact.recency = @recency
166
+ @recency += 1
167
+ @facts.push fact
168
+ return fact
169
+ end
170
+
171
+ def retract_fact(fact)
172
+ i = @facts.index(fact)
173
+ raise 'The fact to remove does not exist!' unless i
174
+ existing_fact = @facts[i]
175
+ @facts.delete_at(i)
176
+ return existing_fact
180
177
  end
181
178
 
182
179
  def print
@@ -281,17 +278,22 @@ module Ruleby
281
278
  end
282
279
 
283
280
  private
284
- def fact_helper(object, sign=:plus, &block)
285
- f = Core::Fact.new object, sign
281
+ def fact_helper(object, sign=:plus)
282
+ f = Core::Fact.new object
286
283
  yield f if block_given?
287
- assert_fact f
284
+ sign==:plus ? assert_fact(f) : retract_fact(f)
288
285
  f
289
286
  end
290
287
 
291
288
  def assert_fact(fact)
292
289
  wm_fact = @working_memory.assert_fact fact
293
290
  @root.assert_fact wm_fact if @root != nil
294
- end
291
+ end
292
+
293
+ def retract_fact(fact)
294
+ wm_fact = @working_memory.retract_fact fact
295
+ @root.retract_fact wm_fact if @root != nil
296
+ end
295
297
  end
296
298
  end
297
299
  end
@@ -34,14 +34,16 @@ module Ruleby
34
34
  @terminal_nodes.push terminal_node
35
35
  end
36
36
 
37
- # When a new fact is added to working memory, or an existing one is removed
38
- # this method is called. It finds any nodes that depend on it, and updates
39
- # them accordingly.
37
+ # When a new fact is added to working memory this method is called. It finds any nodes that depend on it, and
38
+ # updates them accordingly.
40
39
  def assert_fact(fact)
41
- @type_node and fact.token == :plus ? @type_node.assert(fact) : @type_node.retract(fact)
42
- @inherit_nodes.each do |node|
43
- fact.token == :plus ? node.assert(fact) : node.retract(fact)
44
- end
40
+ @type_node.assert(fact) if @type_node
41
+ @inherit_nodes.each {|node| node.assert(fact) }
42
+ end
43
+
44
+ def retract_fact(fact)
45
+ @type_node.retract(fact) if @type_node
46
+ @inherit_nodes.each {|node| node.retract(fact) }
45
47
  end
46
48
 
47
49
  # Increments the activation counter. This is just a pass-thru to the static
@@ -459,34 +461,67 @@ module Ruleby
459
461
  end
460
462
  end
461
463
  end
464
+
465
+ class AlphaMemoryNode < AtomNode
466
+ def initialize(bucket, atom)
467
+ super(bucket, atom)
468
+ @memory = Hash.new(DEFAULT)
469
+ end
470
+
471
+ def retract(assertable)
472
+ forget(assertable)
473
+ super
474
+ end
475
+
476
+ def remember(assertable)
477
+ @memory[assertable.fact.object_id]
478
+ end
479
+
480
+ def memorized?(assertable)
481
+ @memory[assertable.fact.object_id] != DEFAULT
482
+ end
483
+
484
+ def memorize(assertable, value)
485
+ @memory[assertable.fact.object_id] = value
486
+ end
487
+
488
+ def forget(assertable)
489
+ @memory.delete(assertable.fact.object_id)
490
+ end
491
+ end
462
492
 
463
493
  # This node class is used for matching properties of a fact.
464
- class PropertyNode < AtomNode
494
+ class PropertyNode < AlphaMemoryNode
465
495
  def assert(assertable)
466
- begin
467
- v = assertable.fact.object.send(@atom.slot)
468
- assertable.add_tag(@atom.tag, v)
469
- rescue NoMethodError => e
470
- @bucket.add_error Error.new(:no_method, :warn, {
471
- :object => assertable.fact.object.to_s,
472
- :method => e.name,
473
- :message => e.message
474
- })
475
- return
476
- end
477
- begin
478
- if @atom.proc.arity == 1
479
- super if @atom.proc.call(v)
480
- else
481
- super if @atom.proc.call(v, @atom.value)
496
+ unless memorized?(assertable)
497
+ begin
498
+ v = assertable.fact.object.send(@atom.slot)
499
+ assertable.add_tag(@atom.tag, v)
500
+ begin
501
+ if @atom.proc.arity == 1
502
+ r = @atom.proc.call(v)
503
+ else
504
+ r = @atom.proc.call(v, @atom.value)
505
+ end
506
+ memorize(assertable, r)
507
+ super if r
508
+ rescue Exception => e
509
+ @bucket.add_error Error.new(:proc_call, :error, {
510
+ :object => assertable.fact.object.to_s,
511
+ :method => @atom.slot,
512
+ :value => v.to_s,
513
+ :message => e.message
514
+ })
515
+ end
516
+ rescue NoMethodError => e
517
+ @bucket.add_error Error.new(:no_method, :warn, {
518
+ :object => assertable.fact.object.to_s,
519
+ :method => e.name,
520
+ :message => e.message
521
+ })
482
522
  end
483
- rescue Exception => e
484
- @bucket.add_error Error.new(:proc_call, :error, {
485
- :object => assertable.fact.object.to_s,
486
- :method => @atom.slot,
487
- :value => v.to_s,
488
- :message => e.message
489
- })
523
+ else
524
+ super if remember(assertable)
490
525
  end
491
526
  end
492
527
  end
@@ -501,10 +536,13 @@ module Ruleby
501
536
  end
502
537
 
503
538
  # This node class is used conditions that are simply a function, which returns true or false.
504
- class FunctionNode < AtomNode
539
+ class FunctionNode < AlphaMemoryNode
505
540
  def assert(assertable)
506
541
  begin
507
- super if @atom.proc.call(assertable.fact.object, *@atom.arguments)
542
+ unless memorized?(assertable)
543
+ memorize(assertable, @atom.proc.call(assertable.fact.object, *@atom.arguments))
544
+ end
545
+ super if remember(assertable)
508
546
  rescue Exception => e
509
547
  @bucket.add_error Error.new(:proc_call, :error, {
510
548
  :object => fact.object.to_s,
@@ -546,7 +584,7 @@ module Ruleby
546
584
  :message => e.message
547
585
  })
548
586
  end
549
- return MatchResult.new
587
+ MatchResult.new
550
588
  end
551
589
  end
552
590
 
@@ -611,7 +649,7 @@ module Ruleby
611
649
  class CollectNode < BaseBridgeNode
612
650
  def initialize(bucket, pattern)
613
651
  super
614
- @collection_memory = Fact.new([], :internal)
652
+ @collection_memory = Fact.new([])
615
653
  @should_modify = false
616
654
  # not really sure what to do about this. might just need to handle nil's
617
655
  # using a puts a limit on how many facts can be asserted before this feature breaks
@@ -1001,5 +1039,9 @@ module Ruleby
1001
1039
  end
1002
1040
  end
1003
1041
 
1042
+ class MemoryDefault
1043
+ end
1044
+ DEFAULT = MemoryDefault.new
1045
+
1004
1046
  end
1005
1047
  end