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.
@@ -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,12 @@
1
+ module BooleanSimplifier
2
+ def version
3
+ "1.0.0"
4
+ end
5
+
6
+ def simplify(string)
7
+ expression = ExpressionParser.parse(string)
8
+ ExpressionSimplifier.simplify(expression).to_s
9
+ end
10
+
11
+ extend self
12
+ end
@@ -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,14 @@
1
+ class Conjunction < Expression
2
+
3
+ def initialize(a, b)
4
+ @parts = [a, b]
5
+ end
6
+
7
+ def to_s
8
+ a = bracket(parts[0], Disjunction)
9
+ b = bracket(parts[1], Disjunction)
10
+
11
+ "#{a} && #{b}"
12
+ end
13
+
14
+ end
@@ -0,0 +1,11 @@
1
+ class Disjunction < Expression
2
+
3
+ def initialize(a, b)
4
+ @parts = [a, b]
5
+ end
6
+
7
+ def to_s
8
+ parts.join(" || ")
9
+ end
10
+
11
+ end
@@ -0,0 +1,12 @@
1
+ class Negation < Expression
2
+
3
+ def initialize(expression)
4
+ @parts = [expression]
5
+ end
6
+
7
+ def to_s
8
+ expr = bracket(parts[0], Conjunction, Disjunction)
9
+ "!#{expr}"
10
+ end
11
+
12
+ 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,7 @@
1
+ module Rule
2
+ def collection
3
+ @collection ||= []
4
+ end
5
+
6
+ extend self
7
+ 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,12 @@
1
+ module Idempotence
2
+ def simplify(expr)
3
+ if expr.respond_to?(:parts) && expr.parts[0] == expr.parts[1]
4
+ expr.parts[0]
5
+ else
6
+ expr
7
+ end
8
+ end
9
+
10
+ extend self
11
+ Rule.collection << self
12
+ 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: []