mort666-wongi-engine 0.2.9
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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.hgignore +6 -0
- data/.hgtags +13 -0
- data/.ruby-gemset +1 -0
- data/.travis.yml +19 -0
- data/CHANGELOG.md +106 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +27 -0
- data/Rakefile +9 -0
- data/examples/ex01.rb +23 -0
- data/examples/ex02.rb +37 -0
- data/examples/graphviz.rb +16 -0
- data/examples/rdf.n3 +6 -0
- data/examples/rdf.rb +14 -0
- data/examples/timeline.rb +48 -0
- data/lib/wongi-engine.rb +36 -0
- data/lib/wongi-engine/alpha_memory.rb +60 -0
- data/lib/wongi-engine/beta.rb +11 -0
- data/lib/wongi-engine/beta/assignment_node.rb +40 -0
- data/lib/wongi-engine/beta/beta_memory.rb +49 -0
- data/lib/wongi-engine/beta/beta_node.rb +94 -0
- data/lib/wongi-engine/beta/filter_node.rb +48 -0
- data/lib/wongi-engine/beta/join_node.rb +140 -0
- data/lib/wongi-engine/beta/ncc_node.rb +67 -0
- data/lib/wongi-engine/beta/ncc_partner.rb +40 -0
- data/lib/wongi-engine/beta/neg_node.rb +115 -0
- data/lib/wongi-engine/beta/optional_node.rb +142 -0
- data/lib/wongi-engine/beta/or_node.rb +37 -0
- data/lib/wongi-engine/beta/production_node.rb +31 -0
- data/lib/wongi-engine/compiler.rb +115 -0
- data/lib/wongi-engine/core_ext.rb +63 -0
- data/lib/wongi-engine/data_overlay.rb +144 -0
- data/lib/wongi-engine/dsl.rb +132 -0
- data/lib/wongi-engine/dsl/action/base.rb +11 -0
- data/lib/wongi-engine/dsl/action/error_generator.rb +31 -0
- data/lib/wongi-engine/dsl/action/simple_action.rb +60 -0
- data/lib/wongi-engine/dsl/action/simple_collector.rb +52 -0
- data/lib/wongi-engine/dsl/action/statement_generator.rb +46 -0
- data/lib/wongi-engine/dsl/action/trace_action.rb +49 -0
- data/lib/wongi-engine/dsl/any_rule.rb +33 -0
- data/lib/wongi-engine/dsl/assuming.rb +31 -0
- data/lib/wongi-engine/dsl/builder.rb +44 -0
- data/lib/wongi-engine/dsl/clause/assign.rb +15 -0
- data/lib/wongi-engine/dsl/clause/fact.rb +71 -0
- data/lib/wongi-engine/dsl/clause/gen.rb +17 -0
- data/lib/wongi-engine/dsl/clause/generic.rb +38 -0
- data/lib/wongi-engine/dsl/generated.rb +43 -0
- data/lib/wongi-engine/dsl/ncc_subrule.rb +17 -0
- data/lib/wongi-engine/dsl/query.rb +24 -0
- data/lib/wongi-engine/dsl/rule.rb +84 -0
- data/lib/wongi-engine/enumerators.rb +21 -0
- data/lib/wongi-engine/error.rb +22 -0
- data/lib/wongi-engine/filter.rb +6 -0
- data/lib/wongi-engine/filter/asserting_test.rb +20 -0
- data/lib/wongi-engine/filter/equality_test.rb +36 -0
- data/lib/wongi-engine/filter/filter_test.rb +18 -0
- data/lib/wongi-engine/filter/greater_than_test.rb +36 -0
- data/lib/wongi-engine/filter/inequality_test.rb +36 -0
- data/lib/wongi-engine/filter/less_than_test.rb +36 -0
- data/lib/wongi-engine/graph.rb +73 -0
- data/lib/wongi-engine/network.rb +416 -0
- data/lib/wongi-engine/network/collectable.rb +42 -0
- data/lib/wongi-engine/network/debug.rb +85 -0
- data/lib/wongi-engine/ruleset.rb +74 -0
- data/lib/wongi-engine/template.rb +78 -0
- data/lib/wongi-engine/token.rb +114 -0
- data/lib/wongi-engine/version.rb +5 -0
- data/lib/wongi-engine/wme.rb +89 -0
- data/lib/wongi-engine/wme_match_data.rb +34 -0
- data/spec/beta_node_spec.rb +29 -0
- data/spec/bug_specs/issue_4_spec.rb +141 -0
- data/spec/dataset_spec.rb +27 -0
- data/spec/dsl_spec.rb +9 -0
- data/spec/filter_specs/assert_test_spec.rb +102 -0
- data/spec/filter_specs/less_test_spec.rb +41 -0
- data/spec/generation_spec.rb +116 -0
- data/spec/high_level_spec.rb +378 -0
- data/spec/network_spec.rb +182 -0
- data/spec/overlay_spec.rb +61 -0
- data/spec/rule_specs/any_rule_spec.rb +75 -0
- data/spec/rule_specs/assign_spec.rb +88 -0
- data/spec/rule_specs/assuming_spec.rb +66 -0
- data/spec/rule_specs/maybe_rule_spec.rb +101 -0
- data/spec/rule_specs/ncc_spec.rb +258 -0
- data/spec/rule_specs/negative_rule_spec.rb +105 -0
- data/spec/ruleset_spec.rb +54 -0
- data/spec/simple_action_spec.rb +40 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/wme_spec.rb +83 -0
- data/wongi-engine.gemspec +40 -0
- metadata +212 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
module Wongi::Engine
|
2
|
+
module NetworkParts
|
3
|
+
|
4
|
+
module Collectable
|
5
|
+
|
6
|
+
def collectors name = nil
|
7
|
+
@collectors ||= { }
|
8
|
+
if name
|
9
|
+
@collectors[name] ||= [ ]
|
10
|
+
else
|
11
|
+
@collectors
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def error_collectors
|
16
|
+
collectors :error
|
17
|
+
end
|
18
|
+
|
19
|
+
def add_collector collector, name
|
20
|
+
collectors( name ) << collector
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_error_collector
|
24
|
+
add_collector collector, :error
|
25
|
+
end
|
26
|
+
|
27
|
+
def collection name
|
28
|
+
collectors( name ).map( &:default_collect ).flatten.uniq
|
29
|
+
end
|
30
|
+
|
31
|
+
def errors
|
32
|
+
error_collectors.map( &:errors ).flatten
|
33
|
+
end
|
34
|
+
|
35
|
+
def collected_tokens name
|
36
|
+
collectors( name ).map { |collector| collector.production.tokens }.flatten
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module Wongi::Engine
|
2
|
+
|
3
|
+
module NetworkParts
|
4
|
+
|
5
|
+
module Debug
|
6
|
+
|
7
|
+
def full_wme_dump
|
8
|
+
@timeline.each_with_index do |slice, index|
|
9
|
+
puts "time #{ index - @timeline.length }"
|
10
|
+
slice.each do |key, alpha|
|
11
|
+
puts "\t#{alpha.template} -> [#{alpha.wmes.map(&:to_s).join ", "}]"
|
12
|
+
end
|
13
|
+
puts ""
|
14
|
+
end
|
15
|
+
puts "time 0"
|
16
|
+
alpha_hash.each do |key, alpha|
|
17
|
+
puts "\t#{alpha.template} -> [#{alpha.wmes.map(&:to_s).join ", "}]"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def full_dump io = $stdout
|
22
|
+
|
23
|
+
alpha_hash.each_value do |alpha|
|
24
|
+
io.puts "ALPHA #{alpha.template}"
|
25
|
+
alpha.wmes.each do |wme|
|
26
|
+
dump_wme wme, io
|
27
|
+
end
|
28
|
+
end
|
29
|
+
dump_beta beta_top, io
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def token_lineage token
|
36
|
+
result = []
|
37
|
+
while token.parent
|
38
|
+
result << token.parent
|
39
|
+
token = token.parent
|
40
|
+
end
|
41
|
+
result
|
42
|
+
end
|
43
|
+
|
44
|
+
def dump_wme wme, io
|
45
|
+
io.puts "\tWME: #{wme.object_id} #{wme}"
|
46
|
+
wme.tokens.each { |token| io.puts "\t\tTOKEN #{token.object_id}" }
|
47
|
+
io.puts "\tGENERATING:" unless wme.generating_tokens.empty?
|
48
|
+
wme.generating_tokens.each { |token| io.puts "\t\tTOKEN #{token.object_id}" }
|
49
|
+
end
|
50
|
+
|
51
|
+
def dump_beta beta, io
|
52
|
+
case beta
|
53
|
+
when BetaMemory
|
54
|
+
dump_beta_memory beta, io
|
55
|
+
when NccNode
|
56
|
+
dump_ncc beta, io
|
57
|
+
else
|
58
|
+
io.puts "BETA #{beta.object_id} #{beta.class} : TODO"
|
59
|
+
|
60
|
+
end
|
61
|
+
io.puts "\tCHILDREN: #{beta.children.map(&:object_id).join ", "}"
|
62
|
+
beta.children.each { |child| dump_beta child, io } unless beta.children.empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
def dump_beta_memory beta, io
|
66
|
+
io.puts "BETA MEMORY #{beta.object_id}"
|
67
|
+
beta.tokens.each { |token|
|
68
|
+
io.puts "\tTOKEN #{token.object_id} [#{token_lineage(token).map(&:object_id).map(&:to_s).join(" - ")}]"
|
69
|
+
token.wmes.each { |wme| io.puts "\t\tWME " + wme.object_id.to_s }
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def dump_ncc beta, io
|
74
|
+
io.puts "NCC #{beta.object_id}"
|
75
|
+
beta.tokens.each { |token|
|
76
|
+
io.puts "\tTOKEN #{token.object_id} [#{token_lineage(token).map(&:object_id).map(&:to_s).join(" - ")}]"
|
77
|
+
token.wmes.each { |wme| io.puts "\t\tWME " + wme.object_id.to_s }
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Wongi
|
2
|
+
module Engine
|
3
|
+
class Ruleset
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def [] name
|
8
|
+
raise Error, "undefined ruleset #{name}" unless rulesets.has_key?( name )
|
9
|
+
rulesets[ name ]
|
10
|
+
end
|
11
|
+
|
12
|
+
def register name, ruleset
|
13
|
+
raise Error, "ruleset #{name} already exists" if rulesets.has_key?( name )
|
14
|
+
rulesets[ name ] = ruleset
|
15
|
+
end
|
16
|
+
|
17
|
+
def rulesets
|
18
|
+
@rulesets ||= {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset
|
22
|
+
@rulesets = { }
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
def initialize name = nil
|
28
|
+
@rules = []
|
29
|
+
self.name( name ) if name
|
30
|
+
end
|
31
|
+
|
32
|
+
def inspect
|
33
|
+
"<Ruleset #{name}>"
|
34
|
+
end
|
35
|
+
|
36
|
+
def install rete
|
37
|
+
# puts "Installing ruleset #{name}"
|
38
|
+
@rules.each { |rule| rete << rule }
|
39
|
+
rescue StandardError => e
|
40
|
+
e1 = Error.new "error installing ruleset '#{name||'<unnamed>'}': #{e}"
|
41
|
+
e1.set_backtrace e.backtrace
|
42
|
+
raise e1
|
43
|
+
end
|
44
|
+
|
45
|
+
def name name = nil
|
46
|
+
if name && ! @name
|
47
|
+
self.class.register name, self
|
48
|
+
@name = name
|
49
|
+
end
|
50
|
+
@name
|
51
|
+
end
|
52
|
+
|
53
|
+
# def uri uri = nil
|
54
|
+
# @uri = uri if uri
|
55
|
+
# @uri
|
56
|
+
# end
|
57
|
+
|
58
|
+
def rule name, &definition
|
59
|
+
r = DSL::Rule.new name
|
60
|
+
r.instance_eval &definition
|
61
|
+
@rules << r
|
62
|
+
r
|
63
|
+
end
|
64
|
+
|
65
|
+
def query name, &definition
|
66
|
+
r = DSL::Query.new name
|
67
|
+
r.instance_eval &definition
|
68
|
+
@rules << r
|
69
|
+
r
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Wongi::Engine
|
2
|
+
|
3
|
+
Template = Struct.new( :subject, :predicate, :object ) do
|
4
|
+
|
5
|
+
def self.variable? thing
|
6
|
+
return false unless thing.is_a?(Symbol)
|
7
|
+
thing[0] >= 'A' && thing[0] <= 'Z'
|
8
|
+
end
|
9
|
+
|
10
|
+
# TODO: reintroduce Network#import when bringing back RDF support
|
11
|
+
|
12
|
+
def root?
|
13
|
+
subject == :_ && predicate == :_ && object == :_
|
14
|
+
end
|
15
|
+
|
16
|
+
def variables
|
17
|
+
[].tap do |a|
|
18
|
+
a << subject if Template.variable?( subject )
|
19
|
+
a << predicate if Template.variable?( predicate )
|
20
|
+
a << object if Template.variable?( object )
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def hash
|
25
|
+
@hash ||= [subject.hash, predicate.hash, object.hash].hash
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.hash_for *args
|
29
|
+
args.map( &:hash ).hash
|
30
|
+
end
|
31
|
+
|
32
|
+
def == other
|
33
|
+
other.is_a?(Template) && subject == other.subject && predicate == other.predicate && object == other.object
|
34
|
+
end
|
35
|
+
|
36
|
+
def =~ template
|
37
|
+
case template
|
38
|
+
when Template
|
39
|
+
( template.subject == :_ || template.subject == subject ) &&
|
40
|
+
( template.predicate == :_ || template.predicate == predicate ) &&
|
41
|
+
( template.object == :_ || template.object == object )
|
42
|
+
else
|
43
|
+
raise Error, "templates can only match other templates"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def inspect
|
48
|
+
"<~#{subject.inspect} #{predicate.inspect} #{object.inspect}>"
|
49
|
+
end
|
50
|
+
|
51
|
+
def to_s
|
52
|
+
inspect
|
53
|
+
end
|
54
|
+
|
55
|
+
def resolve!(token)
|
56
|
+
s = if Template.variable?(subject)
|
57
|
+
raise DefinitionError, "unbound variable #{subject} in token #{token}" unless token.has_var?(subject)
|
58
|
+
token[subject]
|
59
|
+
else
|
60
|
+
subject
|
61
|
+
end
|
62
|
+
p = if Template.variable?(predicate)
|
63
|
+
raise DefinitionError, "unbound variable #{predicate} in token #{token}" unless token.has_var?(predicate)
|
64
|
+
token[predicate]
|
65
|
+
else
|
66
|
+
predicate
|
67
|
+
end
|
68
|
+
o = if Template.variable?(object)
|
69
|
+
raise DefinitionError, "unbound variable #{object} in token #{token}" unless token.has_var?(object)
|
70
|
+
token[object]
|
71
|
+
else
|
72
|
+
object
|
73
|
+
end
|
74
|
+
[s, p, o]
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Wongi::Engine
|
2
|
+
|
3
|
+
class Token
|
4
|
+
|
5
|
+
include CoreExt
|
6
|
+
|
7
|
+
attr_reader :children
|
8
|
+
attr_reader :wme
|
9
|
+
attr_reader :node
|
10
|
+
attr_reader :overlay
|
11
|
+
attr_accessor :owner, :parent
|
12
|
+
attr_reader :neg_join_results
|
13
|
+
attr_reader :opt_join_results
|
14
|
+
attr_reader :ncc_results
|
15
|
+
attr_reader :generated_wmes
|
16
|
+
attr_predicate :optional
|
17
|
+
attr_predicate :deleted
|
18
|
+
|
19
|
+
def initialize node, token, wme, assignments
|
20
|
+
@node, @parent, @wme, @assignments = node, token, wme, assignments
|
21
|
+
@overlay = wme ? wme.overlay.highest(token.overlay) : (token ? token.overlay : node.rete.default_overlay)
|
22
|
+
@children = []
|
23
|
+
@deleted = false
|
24
|
+
@neg_join_results = []
|
25
|
+
@opt_join_results = []
|
26
|
+
@ncc_results = []
|
27
|
+
@generated_wmes = []
|
28
|
+
token.children << self if token
|
29
|
+
end
|
30
|
+
|
31
|
+
def ancestors
|
32
|
+
if parent
|
33
|
+
parent.ancestors.unshift parent
|
34
|
+
else
|
35
|
+
[]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def subst variable, value
|
40
|
+
@cached_assignments = nil
|
41
|
+
if @assignments.has_key? variable
|
42
|
+
@assignments[ variable ] = value
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def assignments
|
47
|
+
@cached_assignments ||= all_assignments
|
48
|
+
end
|
49
|
+
|
50
|
+
def [] var
|
51
|
+
if a = assignments[ var ]
|
52
|
+
a.respond_to?(:call) ? a.call( self ) : a
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def has_var? x
|
57
|
+
assignments.has_key? x
|
58
|
+
end
|
59
|
+
|
60
|
+
# TODO ignore assignments?
|
61
|
+
def duplicate? other
|
62
|
+
self.parent.equal?(other.parent) && @wme.equal?(other.wme) && self.assignments == other.assignments
|
63
|
+
end
|
64
|
+
|
65
|
+
def to_s
|
66
|
+
str = "TOKEN [ #{object_id} parent=#{parent ? parent.object_id : 'nil'} "
|
67
|
+
all_assignments.each_pair { |key, value| str << "#{key} => #{value} " }
|
68
|
+
str << "]"
|
69
|
+
str
|
70
|
+
end
|
71
|
+
|
72
|
+
def destroy
|
73
|
+
deleted!
|
74
|
+
end
|
75
|
+
|
76
|
+
def dispose!
|
77
|
+
parent.children.delete(self) if parent
|
78
|
+
neg_join_results.dup.each(&:unlink)
|
79
|
+
opt_join_results.dup.each(&:unlink)
|
80
|
+
@parent = nil
|
81
|
+
@wme = nil
|
82
|
+
end
|
83
|
+
|
84
|
+
# for neg feedback loop protection
|
85
|
+
def generated? wme
|
86
|
+
return true if generated_wmes.any? { |w| w == wme }
|
87
|
+
return children.any? { |t| t.generated? wme }
|
88
|
+
end
|
89
|
+
|
90
|
+
protected
|
91
|
+
|
92
|
+
def all_assignments
|
93
|
+
raise "Assignments is not a hash" unless @assignments.kind_of?( Hash )
|
94
|
+
if @parent
|
95
|
+
@parent.assignments.merge @assignments
|
96
|
+
else
|
97
|
+
@assignments
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
class FakeToken < Token
|
104
|
+
def initialize token, wme, assignments
|
105
|
+
@parent, @wme, @assignments = token, wme, assignments
|
106
|
+
@children = []
|
107
|
+
@neg_join_results = []
|
108
|
+
@opt_join_results = []
|
109
|
+
@ncc_results = []
|
110
|
+
@generated_wmes = []
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Wongi::Engine
|
2
|
+
|
3
|
+
WME = Struct.new( :subject, :predicate, :object ) do
|
4
|
+
|
5
|
+
include CoreExt
|
6
|
+
|
7
|
+
attr_reader :rete
|
8
|
+
|
9
|
+
attr_reader :generating_tokens
|
10
|
+
attr_reader :neg_join_results, :opt_join_results
|
11
|
+
attr_accessor :overlay
|
12
|
+
attr_predicate :deleted
|
13
|
+
attr_predicate :manual
|
14
|
+
|
15
|
+
def initialize s, p, o, r = nil
|
16
|
+
|
17
|
+
manual!
|
18
|
+
|
19
|
+
@deleted = false
|
20
|
+
@alphas = []
|
21
|
+
@generating_tokens = []
|
22
|
+
@neg_join_results = []
|
23
|
+
@opt_join_results = []
|
24
|
+
|
25
|
+
@rete = r
|
26
|
+
|
27
|
+
# TODO: reintroduce Network#import when bringing back RDF support
|
28
|
+
super( s, p, o )
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
def import_into r
|
33
|
+
self.class.new( subject, predicate, object, r ).tap do |wme|
|
34
|
+
wme.overlay = overlay
|
35
|
+
wme.manual = self.manual?
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def dup
|
40
|
+
self.class.new( subject, predicate, object, rete ).tap do |wme|
|
41
|
+
wme.overlay = overlay
|
42
|
+
wme.manual = self.manual?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def == other
|
47
|
+
subject == other.subject && predicate == other.predicate && object == other.object
|
48
|
+
end
|
49
|
+
|
50
|
+
def =~ template
|
51
|
+
raise Wongi::Engine::Error, "Cannot match a WME against a #{template.class}" unless Template === template
|
52
|
+
result = match_member( self.subject, template.subject ) & match_member( self.predicate, template.predicate ) & match_member( self.object, template.object )
|
53
|
+
if result.match?
|
54
|
+
result
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def generated?
|
59
|
+
!generating_tokens.empty?
|
60
|
+
end
|
61
|
+
|
62
|
+
def inspect
|
63
|
+
"{#{subject.inspect} #{predicate.inspect} #{object.inspect}}"
|
64
|
+
end
|
65
|
+
|
66
|
+
def to_s
|
67
|
+
inspect
|
68
|
+
end
|
69
|
+
|
70
|
+
def hash
|
71
|
+
@hash ||= [subject.hash, predicate.hash, object.hash].hash
|
72
|
+
end
|
73
|
+
|
74
|
+
protected
|
75
|
+
|
76
|
+
def match_member mine, theirs
|
77
|
+
result = WMEMatchData.new
|
78
|
+
if theirs == :_ || mine == theirs
|
79
|
+
result.match!
|
80
|
+
elsif Template.variable? theirs
|
81
|
+
result.match!
|
82
|
+
result[theirs] = mine
|
83
|
+
end
|
84
|
+
result
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|