boolean_simplifier 1.0.0
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.
- 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: []
|