ruleby 0.9.b4 → 0.9.b7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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