boolean_simplifier 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +36 -0
- data/lib/boolean_simplifier.rb +20 -0
- data/lib/boolean_simplifier/base.rb +12 -0
- data/lib/boolean_simplifier/expression.rb +22 -0
- data/lib/boolean_simplifier/expression/conjunction.rb +14 -0
- data/lib/boolean_simplifier/expression/disjunction.rb +11 -0
- data/lib/boolean_simplifier/expression/negation.rb +12 -0
- data/lib/boolean_simplifier/expression_parser.rb +16 -0
- data/lib/boolean_simplifier/expression_simplifier.rb +39 -0
- data/lib/boolean_simplifier/grammar/boolean_expression.treetop +71 -0
- data/lib/boolean_simplifier/rule.rb +7 -0
- data/lib/boolean_simplifier/rule/absorbtion.rb +28 -0
- data/lib/boolean_simplifier/rule/annihilator.rb +25 -0
- data/lib/boolean_simplifier/rule/complementation.rb +25 -0
- data/lib/boolean_simplifier/rule/de_morgans.rb +31 -0
- data/lib/boolean_simplifier/rule/distributivity.rb +31 -0
- data/lib/boolean_simplifier/rule/double_negation.rb +17 -0
- data/lib/boolean_simplifier/rule/idempotence.rb +12 -0
- data/lib/boolean_simplifier/rule/identity.rb +25 -0
- metadata +111 -0
data/README.md
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
## Boolean Simplifier
|
2
|
+
|
3
|
+
A hobbyist's attempt to simplify boolean expressions using a recursive, object-oriented strategy.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
puts BooleanSimplifier.simplify("a && a || !!b")
|
9
|
+
# a || b
|
10
|
+
|
11
|
+
puts BooleanSimplifier.simplify("a && !a")
|
12
|
+
# false
|
13
|
+
|
14
|
+
puts BooleanSimplifier.simplify("!a || !b && (true || false)")
|
15
|
+
# !(a && b)
|
16
|
+
```
|
17
|
+
|
18
|
+
## How it works
|
19
|
+
|
20
|
+
The boolean expression is parsed and an object graph is constructed. This graph consists of instances of Negation, Conjunction, Disjunction. It also contains literal strings.
|
21
|
+
|
22
|
+
A set of boolean simplification rules is applied to the expression and recursively to its subexpressions. These rules take an arbitrary expression as input and return a new expression if the rule applies. If not, the original expression is returned.
|
23
|
+
|
24
|
+
The base case for this recursive algorithm is when no improvement is made to the given expression after iterating through all simplification rules.
|
25
|
+
|
26
|
+
## Limitations
|
27
|
+
|
28
|
+
Ruby does not implement tail recursion. Therefore, you may run into a SystemStackError when simplifying large expressions.
|
29
|
+
|
30
|
+
I haven't figured out every simplification rule, or the most efficient way to apply them. Therefore, you may find that some expressions are only partially simplified.
|
31
|
+
|
32
|
+
## Contribution
|
33
|
+
|
34
|
+
Open an issue, or send a pull request. I'd appreciate all feedback.
|
35
|
+
|
36
|
+
[Twitter](https://twitter.com/cpatuzzo)
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require "treetop"
|
2
|
+
require "polyglot"
|
3
|
+
|
4
|
+
require "boolean_simplifier/base"
|
5
|
+
require "boolean_simplifier/expression"
|
6
|
+
require "boolean_simplifier/expression/conjunction"
|
7
|
+
require "boolean_simplifier/expression/disjunction"
|
8
|
+
require "boolean_simplifier/expression/negation"
|
9
|
+
require "boolean_simplifier/expression_parser"
|
10
|
+
require "boolean_simplifier/expression_simplifier"
|
11
|
+
require "boolean_simplifier/grammar/boolean_expression"
|
12
|
+
require "boolean_simplifier/rule"
|
13
|
+
require "boolean_simplifier/rule/absorbtion"
|
14
|
+
require "boolean_simplifier/rule/annihilator"
|
15
|
+
require "boolean_simplifier/rule/complementation"
|
16
|
+
require "boolean_simplifier/rule/de_morgans"
|
17
|
+
require "boolean_simplifier/rule/distributivity"
|
18
|
+
require "boolean_simplifier/rule/double_negation"
|
19
|
+
require "boolean_simplifier/rule/idempotence"
|
20
|
+
require "boolean_simplifier/rule/identity"
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Expression
|
2
|
+
|
3
|
+
attr_accessor :parts
|
4
|
+
|
5
|
+
def ==(other)
|
6
|
+
to_s == other.to_s
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_s
|
10
|
+
raise NotImplementedError
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def bracket(expression, *classes)
|
15
|
+
if classes.include?(expression.class)
|
16
|
+
"(#{expression})"
|
17
|
+
else
|
18
|
+
expression
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module ExpressionParser
|
2
|
+
def parse(string)
|
3
|
+
parser = BooleanExpressionParser.new
|
4
|
+
|
5
|
+
ast = parser.parse(string)
|
6
|
+
if ast.nil?
|
7
|
+
raise ParseError, parser.failure_reason
|
8
|
+
end
|
9
|
+
|
10
|
+
ast.to_expression
|
11
|
+
end
|
12
|
+
|
13
|
+
class ::ParseError < StandardError; end
|
14
|
+
|
15
|
+
extend self
|
16
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ExpressionSimplifier
|
2
|
+
def simplify(expression)
|
3
|
+
until_no_improvement(expression) do
|
4
|
+
simplify_parts(expression)
|
5
|
+
simplify_root(expression)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
def until_no_improvement(expression, &block)
|
11
|
+
begin
|
12
|
+
break if trivial?(expression)
|
13
|
+
previous = expression.dup
|
14
|
+
expression = yield
|
15
|
+
end until previous == expression
|
16
|
+
|
17
|
+
expression
|
18
|
+
end
|
19
|
+
|
20
|
+
def simplify_parts(expression)
|
21
|
+
expression.parts.each.with_index do |part, index|
|
22
|
+
expression.parts[index] = simplify(part)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def simplify_root(expression)
|
27
|
+
Rule.collection.each do |rule|
|
28
|
+
expression = rule.simplify(expression)
|
29
|
+
end
|
30
|
+
|
31
|
+
expression
|
32
|
+
end
|
33
|
+
|
34
|
+
def trivial?(expression)
|
35
|
+
!expression.respond_to?(:parts)
|
36
|
+
end
|
37
|
+
|
38
|
+
extend self
|
39
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
grammar BooleanExpression
|
2
|
+
|
3
|
+
rule expression
|
4
|
+
space disjunctive space {
|
5
|
+
def to_expression
|
6
|
+
disjunctive.to_expression
|
7
|
+
end
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
rule disjunctive
|
12
|
+
a:conjunctive space "||" space b:disjunctive {
|
13
|
+
def to_expression
|
14
|
+
Disjunction.new(a.to_expression, b.to_expression)
|
15
|
+
end
|
16
|
+
} /
|
17
|
+
conjunctive
|
18
|
+
end
|
19
|
+
|
20
|
+
rule conjunctive
|
21
|
+
a:negative space "&&" space b:conjunctive {
|
22
|
+
def to_expression
|
23
|
+
Conjunction.new(a.to_expression, b.to_expression)
|
24
|
+
end
|
25
|
+
} /
|
26
|
+
negative
|
27
|
+
end
|
28
|
+
|
29
|
+
rule negative
|
30
|
+
"!" negative {
|
31
|
+
def to_expression
|
32
|
+
Negation.new(negative.to_expression)
|
33
|
+
end
|
34
|
+
} /
|
35
|
+
primary
|
36
|
+
end
|
37
|
+
|
38
|
+
rule primary
|
39
|
+
boolean / variable / "(" expression ")" {
|
40
|
+
def to_expression
|
41
|
+
expression.to_expression
|
42
|
+
end
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
rule boolean
|
47
|
+
"true" {
|
48
|
+
def to_expression
|
49
|
+
true
|
50
|
+
end
|
51
|
+
} /
|
52
|
+
"false" {
|
53
|
+
def to_expression
|
54
|
+
false
|
55
|
+
end
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
rule variable
|
60
|
+
[a-zA-Z0-9_]+ {
|
61
|
+
def to_expression
|
62
|
+
text_value
|
63
|
+
end
|
64
|
+
}
|
65
|
+
end
|
66
|
+
|
67
|
+
rule space
|
68
|
+
" "*
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Absorbtion
|
2
|
+
def simplify(expr)
|
3
|
+
if expr.class == Conjunction
|
4
|
+
absorb_for_class(expr, Disjunction)
|
5
|
+
elsif expr.class == Disjunction
|
6
|
+
absorb_for_class(expr, Conjunction)
|
7
|
+
else
|
8
|
+
expr
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def absorb_for_class(expr, klass)
|
14
|
+
[[0, 1], [1, 0]].each do |a, b|
|
15
|
+
if expr.parts[a].class == klass && contains?(expr.parts[a], expr.parts[b])
|
16
|
+
return expr.parts[b]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
expr
|
20
|
+
end
|
21
|
+
|
22
|
+
def contains?(a, b)
|
23
|
+
a.parts[0] == b || a.parts[1] == b
|
24
|
+
end
|
25
|
+
|
26
|
+
extend self
|
27
|
+
Rule.collection << self
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Annihilator
|
2
|
+
def simplify(expr)
|
3
|
+
if expr.class == Conjunction
|
4
|
+
return_x_if_contains_x(expr, false)
|
5
|
+
elsif expr.class == Disjunction
|
6
|
+
return_x_if_contains_x(expr, true)
|
7
|
+
else
|
8
|
+
expr
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def return_x_if_contains_x(expr, x)
|
14
|
+
if expr.parts[0] == x
|
15
|
+
x
|
16
|
+
elsif expr.parts[1] == x
|
17
|
+
x
|
18
|
+
else
|
19
|
+
expr
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
extend self
|
24
|
+
Rule.collection << self
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Complementation
|
2
|
+
def simplify(expr)
|
3
|
+
if expr.class == Conjunction
|
4
|
+
return_x_if_complementary(expr, false)
|
5
|
+
elsif expr.class == Disjunction
|
6
|
+
return_x_if_complementary(expr, true)
|
7
|
+
else
|
8
|
+
expr
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def return_x_if_complementary(expr, x)
|
14
|
+
complementary_expressions?(expr) ? x : expr
|
15
|
+
end
|
16
|
+
|
17
|
+
def complementary_expressions?(expr)
|
18
|
+
[[0, 1], [1, 0]].any? do |a, b|
|
19
|
+
expr.parts[a] == Negation.new(expr.parts[b])
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
extend self
|
24
|
+
Rule.collection << self
|
25
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module DeMorgans
|
2
|
+
def simplify(expr)
|
3
|
+
if expr.class == Conjunction
|
4
|
+
apply_de_morgans(expr, Disjunction)
|
5
|
+
elsif expr.class == Disjunction
|
6
|
+
apply_de_morgans(expr, Conjunction)
|
7
|
+
else
|
8
|
+
expr
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def apply_de_morgans(expr, inner_class)
|
14
|
+
if both_negation?(expr)
|
15
|
+
Negation.new(
|
16
|
+
inner_class.new(
|
17
|
+
expr.parts[0].parts[0],
|
18
|
+
expr.parts[1].parts[0])
|
19
|
+
)
|
20
|
+
else
|
21
|
+
expr
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def both_negation?(expr)
|
26
|
+
[0, 1].all? { |i| expr.parts[i].class == Negation }
|
27
|
+
end
|
28
|
+
|
29
|
+
extend self
|
30
|
+
Rule.collection << self
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Distributivity
|
2
|
+
def simplify(expr)
|
3
|
+
if expr.class == Conjunction
|
4
|
+
distribute(expr, Conjunction, Disjunction)
|
5
|
+
elsif expr.class == Disjunction
|
6
|
+
distribute(expr, Disjunction, Conjunction)
|
7
|
+
else
|
8
|
+
expr
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def distribute(expr, inner, outer)
|
14
|
+
[[0, 1], [1, 0]].each do |a, b|
|
15
|
+
if expr.parts[a].class == outer && trivial?(expr.parts[b])
|
16
|
+
return outer.new(
|
17
|
+
inner.new(expr.parts[b], expr.parts[a].parts[0]),
|
18
|
+
inner.new(expr.parts[b], expr.parts[a].parts[1])
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
expr
|
23
|
+
end
|
24
|
+
|
25
|
+
def trivial?(expr)
|
26
|
+
!expr.respond_to?(:parts)
|
27
|
+
end
|
28
|
+
|
29
|
+
extend self
|
30
|
+
Rule.collection << self
|
31
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module DoubleNegation
|
2
|
+
def simplify(expr)
|
3
|
+
if negation?(expr) && negation?(expr.parts[0])
|
4
|
+
expr.parts[0].parts[0]
|
5
|
+
else
|
6
|
+
expr
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
def negation?(expr)
|
12
|
+
expr.class == Negation
|
13
|
+
end
|
14
|
+
|
15
|
+
extend self
|
16
|
+
Rule.collection << self
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Identity
|
2
|
+
def simplify(expr)
|
3
|
+
if expr.class == Conjunction
|
4
|
+
return_part_if_contains_x(expr, true)
|
5
|
+
elsif expr.class == Disjunction
|
6
|
+
return_part_if_contains_x(expr, false)
|
7
|
+
else
|
8
|
+
expr
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
def return_part_if_contains_x(expr, x)
|
14
|
+
if expr.parts[0] == x
|
15
|
+
expr.parts[1]
|
16
|
+
elsif expr.parts[1] == x
|
17
|
+
expr.parts[0]
|
18
|
+
else
|
19
|
+
expr
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
extend self
|
24
|
+
Rule.collection << self
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: boolean_simplifier
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chris Patuzzo
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-11-17 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: treetop
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: polyglot
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Simplify boolean expressions
|
63
|
+
email: chris@patuzzo.co.uk
|
64
|
+
executables: []
|
65
|
+
extensions: []
|
66
|
+
extra_rdoc_files: []
|
67
|
+
files:
|
68
|
+
- README.md
|
69
|
+
- lib/boolean_simplifier/base.rb
|
70
|
+
- lib/boolean_simplifier/expression/conjunction.rb
|
71
|
+
- lib/boolean_simplifier/expression/disjunction.rb
|
72
|
+
- lib/boolean_simplifier/expression/negation.rb
|
73
|
+
- lib/boolean_simplifier/expression.rb
|
74
|
+
- lib/boolean_simplifier/expression_parser.rb
|
75
|
+
- lib/boolean_simplifier/expression_simplifier.rb
|
76
|
+
- lib/boolean_simplifier/grammar/boolean_expression.treetop
|
77
|
+
- lib/boolean_simplifier/rule/absorbtion.rb
|
78
|
+
- lib/boolean_simplifier/rule/annihilator.rb
|
79
|
+
- lib/boolean_simplifier/rule/complementation.rb
|
80
|
+
- lib/boolean_simplifier/rule/de_morgans.rb
|
81
|
+
- lib/boolean_simplifier/rule/distributivity.rb
|
82
|
+
- lib/boolean_simplifier/rule/double_negation.rb
|
83
|
+
- lib/boolean_simplifier/rule/idempotence.rb
|
84
|
+
- lib/boolean_simplifier/rule/identity.rb
|
85
|
+
- lib/boolean_simplifier/rule.rb
|
86
|
+
- lib/boolean_simplifier.rb
|
87
|
+
homepage: https://github.com/tuzz/boolean_simplifier
|
88
|
+
licenses: []
|
89
|
+
post_install_message:
|
90
|
+
rdoc_options: []
|
91
|
+
require_paths:
|
92
|
+
- lib
|
93
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ! '>='
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: '0'
|
105
|
+
requirements: []
|
106
|
+
rubyforge_project:
|
107
|
+
rubygems_version: 1.8.23
|
108
|
+
signing_key:
|
109
|
+
specification_version: 3
|
110
|
+
summary: Boolean Simplifier
|
111
|
+
test_files: []
|