From daabd511ab15d01037c047697d75387f835cda22 Mon Sep 17 00:00:00 2001 From: Jonathan Duck Date: Wed, 31 Aug 2022 07:50:11 -0700 Subject: [PATCH 1/2] Replace interface with concrete type --- builders.go | 6 ++-- grid/rules.go | 4 +-- grid/samples_test.go | 12 +++---- primary.go | 63 +++++++++++++++-------------------- primary_test.go | 6 ++-- quine-mccluskey.go | 36 ++++++++++---------- quine-mccluskey_test.go | 32 +++++++++--------- smullyan/counting.go | 6 ++-- types.go | 74 +++++++++++------------------------------ 9 files changed, 98 insertions(+), 141 deletions(-) diff --git a/builders.go b/builders.go index 52280b0..d577ae5 100644 --- a/builders.go +++ b/builders.go @@ -3,7 +3,7 @@ package boolgebra // TermBuilder can be used to efficiently append ID ( or Not(ID)) into a big And type TermBuilder struct { isLitFalse bool - m minterm + m Term } // And append a variable to the current term @@ -12,7 +12,7 @@ func (t *TermBuilder) And(id string, val bool) { return } // nothing to do if t.m == nil { - t.m = make(minterm) + t.m = make(Term) } if prev, exists := t.m[id]; exists && prev != val { //attempt to do something like AND(x, !x) which is always false therefore the result will always be Lit(false) @@ -33,5 +33,5 @@ func (t *TermBuilder) Build() Expr { } res := t.m t.m = nil // destroy reference to m to avoid editing it anymore - return res + return Expr{res} } diff --git a/grid/rules.go b/grid/rules.go index 7dd7ea0..c337195 100644 --- a/grid/rules.go +++ b/grid/rules.go @@ -17,8 +17,8 @@ func P(name, value string) Expr { return ID(name + "=" + value) } // // Let's define 'R' an transitive and symetric relation in Values noted `\forall x,y \in Values xRy` // -// 1. `\forall g,h \in Groups² |g| = |h| \and g \inter h = \phi` -// 2. `\forall G \in Groups, \forall v \notin G \exists! w in G vRw` +// 1. `\forall g,h \in Groups² |g| = |h| \and g \inter h = \phi` +// 2. `\forall G \in Groups, \forall v \notin G \exists! w in G vRw` // // groups are defined by to position in the list func Rules(N int, values ...string) Expr { diff --git a/grid/samples_test.go b/grid/samples_test.go index f26e060..d414786 100644 --- a/grid/samples_test.go +++ b/grid/samples_test.go @@ -47,11 +47,11 @@ func ExampleSimplify_logic3x1() { result := Simplify(And(rules, Hint1, Hint2, Hint3)) - if result.Terms() > 1 { - fmt.Printf("There are %d solutions, that's too many\n", result.Terms()) + if len(result) > 1 { + fmt.Printf("There are %d solutions, that's too many\n", len(result)) fmt.Println(Factor(result)) } else { - fmt.Printf("There is %d solution.\n", result.Terms()) + fmt.Printf("There is %d solution.\n", len(result)) } //Output: // There is 1 solution. @@ -95,13 +95,13 @@ func ExampleSimplify_logic4x1() { P(Philippe, R6), ) - if result.Terms() > 1 { - fmt.Printf("There are %d solutions, that's too many\n", result.Terms()) + if len(result) > 1 { + fmt.Printf("There are %d solutions, that's too many\n", len(result)) deduction, rem := Factor(result) fmt.Println(deduction) fmt.Println(rem) } else { - fmt.Printf("There is %d solution.\n", result.Terms()) + fmt.Printf("There is %d solution.\n", len(result)) } //Output: // There is 1 solution. diff --git a/primary.go b/primary.go index 6f5811b..c1b73c3 100644 --- a/primary.go +++ b/primary.go @@ -6,35 +6,34 @@ package boolgebra // Lit returns an Expr equivalent to a boolean literal 'val' func Lit(val bool) Expr { if val { - return minterm{} // true is by definition an empty minterm ( neutral for product) + return Expr{Term{}} // true is by definition an empty minterm ( neutral for product) } else { - return expression{} // false is an empty expression (neutral for sum) + return Expr{} // false is an empty expression (neutral for sum) } } // ID returns an Expr equivalent to a single ID 'id' -func ID(id string) Expr { return minterm{id: true} } +func ID(id string) Expr { return Expr{Term{id: true}} } // Or return the conjunction of all the expression passed in parameter. // // By convention, if 'x' is empty it returns Lit(false). See https://en.wikipedia.org/wiki/Empty_sum func Or(x ...Expr) Expr { // start with the neutral of the Or i.e a false - res := make(expression, 0) + res := make(Expr, 0) // scan all terms, in all expr for _, exp := range x { - for i := 0; i < exp.Terms(); i++ { - t := exp.Term(i) - if t.Is(true) { - return t // + for _, t := range exp { + if t.isLiteral(true) { + return Expr{Term{}} } - if !t.Is(false) { // if this is the literal false, we can just skip it - res = append(res, t.(minterm)) + if !t.isLiteral(false) { // if this is the literal false, we can just skip it + res = append(res, t) } } } if len(res) == 1 { - return res[0] + return Expr{res[0]} } return res } @@ -62,26 +61,24 @@ func And(expressions ...Expr) Expr { // this is the only real case x, y := expressions[0], expressions[1] - if x.Is(false) || y.Is(false) { + if x.isLiteral(false) || y.isLiteral(false) { return Lit(false) } - if x.Is(true) { + if x.isLiteral(true) { return y } - if y.Is(true) { + if y.isLiteral(true) { return x } // general case - z := make(expression, 0, x.Terms()*y.Terms()) + z := make(Expr, 0, len(x)*len(y)) // this is the big one: all terms from x multiplied by terms from y - for i := 0; i < x.Terms(); i++ { - m := x.Term(i).(minterm) + for _, m := range x { product: - for j := 0; j < y.Terms(); j++ { - n := y.Term(j).(minterm) + for _, n := range y { // compute the real m && n , this is basically a merge of all IDs // there is one special case: A & A' = false @@ -93,7 +90,7 @@ func And(expressions ...Expr) Expr { } // basic merge - o := make(minterm) + o := make(Term) for k, v := range m { o[k] = v } @@ -113,12 +110,7 @@ func Not(x Expr) Expr { return x.Not() } // Simplify returns a simpler version of 'x' by applying simplification rules. func Simplify(x Expr) Expr { - switch e := x.(type) { - case expression: - return reduce(e) - default: - return x // unchanged - } + return reduce(x) } // Factor computes the greatest common factor between terms of x @@ -127,30 +119,27 @@ func Simplify(x Expr) Expr { // // x is currently a sum of terms, this function returns f and rem so that // -// x = And(f, rem) -// f.Terms() ==1 : it's a minterm -// +// x = And(f, rem) +// f.Terms() ==1 : it's a minterm func Factor(x Expr) (f, rem Expr) { - var res minterm - for i := 0; i < x.Terms(); i++ { - m := x.Term(i).(minterm) + var res Term + for i, m := range x { if i == 0 { // special case for the first one, need to init the thing res = m } res = inter(res, m) if len(res) == 0 { - return expression{res}, x // empty one + return Expr{res}, x // empty one } } // now for each minterm recompute the reminder - r := expression{} - for i := 0; i < x.Terms(); i++ { - m := x.Term(i).(minterm) + r := Expr{} + for _, m := range x { r = append(r, div(m, res)) } - return expression{res}, r + return Expr{res}, r } diff --git a/primary_test.go b/primary_test.go index 0f9945a..48128cf 100644 --- a/primary_test.go +++ b/primary_test.go @@ -13,10 +13,10 @@ func ExampleID() { // TesLit ensure that the basic true and false are working accordingly with Is(bool) func TestLit(t *testing.T) { - if !Lit(true).Is(true) { + if !Lit(true).isLiteral(true) { t.Error("Lit(true).Is(true) must be true") } - if !Lit(false).Is(false) { + if !Lit(false).isLiteral(false) { t.Error("Lit(false).Is(false) must be true") } } @@ -72,7 +72,7 @@ func ExampleOr() { } func truthTester(t *testing.T, label string, z Expr, expected bool) { - if !z.Is(expected) { + if !z.isLiteral(expected) { t.Errorf("%s: expected %v got %v", label, expected, z) } } diff --git a/quine-mccluskey.go b/quine-mccluskey.go index 0d0eb98..58160b6 100644 --- a/quine-mccluskey.go +++ b/quine-mccluskey.go @@ -2,15 +2,15 @@ package boolgebra // Quine-McCluskey is an algorithm to simplify a sum of prod. -//reduce combine together all minterms of x into prime implicants -func reduce(x expression) expression { +// reduce combine together all minterms of x into prime implicants +func reduce(x Expr) Expr { // to reduce we need to cluster minterms of x into number of non neg ID - var cluster [][]minterm // index minterm by their 1s. store them in a slice - var primes []minterm // also keep primes all together + var cluster [][]Term // index minterm by their 1s. store them in a slice + var primes []Term // also keep primes all together // fill the first cluster - cluster = make([][]minterm, 1+len(x.IDs())) + cluster = make([][]Term, 1+len(x.IDs())) for _, m := range x { ones := positives(m) cluster[ones] = append(cluster[ones], m) @@ -20,7 +20,7 @@ func reduce(x expression) expression { for !emptycluster { // the next cluster will become the current one soon, so we already set the bool to true, because we start with an empty one emptycluster = true // we start with an empty next one, let see if it get filled - next := make([][]minterm, len(cluster)) + next := make([][]Term, len(cluster)) // attempt all possible combinations. // a minterm with n Positive IDs, can only be combined with another one with n or n+1 ( or n-1 but combination is symetric so we don't care) @@ -94,12 +94,12 @@ func reduce(x expression) expression { cluster = next } //done, we now have all the prime implicant - return expression(primes) + return Expr(primes) } // appenunique behave like 'append' except for items in 'terms' that are present in 'set': they // are not appended in this case. -func appendunique(set []minterm, terms ...minterm) []minterm { +func appendunique(set []Term, terms ...Term) []Term { termsloop: for _, m := range terms { for _, x := range set { @@ -118,7 +118,7 @@ termsloop: // x and y must be identical but on exactly one identifier. // // the combined is then then intersection of x and y. -func combine(x, y minterm) (c minterm, ok bool) { +func combine(x, y Term) (c Term, ok bool) { // alg: find out the one and only one difference between x,y // so scan for differences and count. var d string // the identifier that is different (if diffs == 1)) @@ -154,7 +154,7 @@ func combine(x, y minterm) (c minterm, ok bool) { // build c accordingly then // x and y are guaranteed to be identical but on 'd' // so copy x but 'd' - c = make(minterm) + c = make(Term) for k, v := range x { if k != d { c[k] = v @@ -165,7 +165,7 @@ func combine(x, y minterm) (c minterm, ok bool) { } // equals return true if and only if m and n are both minterm, then they are semantically equals -func equals(m, n minterm) bool { +func equals(m, n Term) bool { if len(m) != len(n) { return false } @@ -178,7 +178,7 @@ func equals(m, n minterm) bool { } // positives returns the number of positive identifiers -func positives(m minterm) int { +func positives(m Term) int { count := 0 for _, v := range m { if v { @@ -188,9 +188,9 @@ func positives(m minterm) int { return count } -//inter computes the intersection of x inter y -func inter(x, y minterm) minterm { - res := make(minterm) +// inter computes the intersection of x inter y +func inter(x, y Term) Term { + res := make(Term) for k, v := range x { if w, exists := y[k]; exists && v == w { res[k] = v @@ -200,10 +200,10 @@ func inter(x, y minterm) minterm { } -//div computes x/y i.e z so that And(z,y) = x +// div computes x/y i.e z so that And(z,y) = x // can be seen as x removed from items in y -func div(x, y minterm) minterm { - res := make(minterm) +func div(x, y Term) Term { + res := make(Term) for k, v := range x { if w, exists := y[k]; !exists || v != w { res[k] = v diff --git a/quine-mccluskey_test.go b/quine-mccluskey_test.go index db023a9..34e257e 100644 --- a/quine-mccluskey_test.go +++ b/quine-mccluskey_test.go @@ -2,7 +2,7 @@ package boolgebra import "testing" -//TestReduce check that we get the prime correctly using the wikipedia examples +// TestReduce check that we get the prime correctly using the wikipedia examples func TestReduce(t *testing.T) { /* Number @@ -28,7 +28,7 @@ func TestReduce(t *testing.T) { m11 := m("ab'cd") m14 := m("abcd'") m15 := m("abcd") - x := expression{m4, m8, m9, m10, m12, m11, m14, m15} + x := Expr{m4, m8, m9, m10, m12, m11, m14, m15} //primes are //m4_12 := m("bc'd'") //m8_9_10_11 := m("ab'") @@ -39,9 +39,9 @@ func TestReduce(t *testing.T) { } -//newminterm creates a new minterm using ' at the end of the string to find out that its a neg -func m(x string) minterm { - res := make(minterm) +// newminterm creates a new minterm using ' at the end of the string to find out that its a neg +func m(x string) Term { + res := make(Term) for i, id := range x { nextisquote := i+1 < len(x) && x[i+1] == '\'' @@ -54,7 +54,7 @@ func m(x string) minterm { // TestPosLen make sure that we count the number of true correctly func TestPositives(t *testing.T) { - x := minterm{"A": true, "B": true, "C": false, "E": true} + x := Term{"A": true, "B": true, "C": false, "E": true} if positives(x) != 3 { t.Errorf("invalid minterm %v PosLen attribute got %v want 3", x, positives(x)) } @@ -63,13 +63,13 @@ func TestPositives(t *testing.T) { // TestMinterm_combine gold test some minterm combinations func TestCombine(t *testing.T) { - x := minterm{"A": true, "B": true, "C": false, "E": true} - var y, r, c minterm + x := Term{"A": true, "B": true, "C": false, "E": true} + var y, r, c Term var ok bool // 1,0 -> _ - y = minterm{"A": true, "B": true, "C": false, "E": false} - r = minterm{"A": true, "B": true, "C": false} + y = Term{"A": true, "B": true, "C": false, "E": false} + r = Term{"A": true, "B": true, "C": false} c, ok = combine(x, y) if !ok { t.Errorf("failed to combine(%v,%v)", x, y) @@ -78,8 +78,8 @@ func TestCombine(t *testing.T) { } // 1,_ -> _ - y = minterm{"A": true, "B": true, "C": false} - r = minterm{"A": true, "B": true, "C": false} + y = Term{"A": true, "B": true, "C": false} + r = Term{"A": true, "B": true, "C": false} c, ok = combine(x, y) if !ok { t.Errorf("failed to combine(%v,%v)", x, y) @@ -88,8 +88,8 @@ func TestCombine(t *testing.T) { } // 0,1 -> _ - y = minterm{"A": true, "B": true, "C": true, "E": true} - r = minterm{"A": true, "B": true, "E": true} + y = Term{"A": true, "B": true, "C": true, "E": true} + r = Term{"A": true, "B": true, "E": true} c, ok = combine(x, y) if !ok { t.Errorf("failed to combine(%v,%v)", x, y) @@ -98,8 +98,8 @@ func TestCombine(t *testing.T) { } // 0,_ -> _ - y = minterm{"A": true, "B": true, "E": true} - r = minterm{"A": true, "B": true, "E": true} + y = Term{"A": true, "B": true, "E": true} + r = Term{"A": true, "B": true, "E": true} c, ok = combine(x, y) if !ok { t.Errorf("failed to combine(%v,%v)", x, y) diff --git a/smullyan/counting.go b/smullyan/counting.go index d20e69b..ca423ad 100644 --- a/smullyan/counting.go +++ b/smullyan/counting.go @@ -1,7 +1,9 @@ package smullyan -import "github.com/etnz/permute" -import . "github.com/etnz/boolgebra" +import ( + . "github.com/etnz/boolgebra" + "github.com/etnz/permute" +) //counting.go holds the function relative to counting, like Exactly or AtMost diff --git a/types.go b/types.go index d18d743..830afa7 100644 --- a/types.go +++ b/types.go @@ -7,35 +7,23 @@ import ( ) type ( - // expression is boolean algebra expression as a sum of prod of minterms. + // Expr is boolean algebra Expr as a sum of prod of minterms. // As such it is a slice of minterms. It must be considered as a set // - // an empty expression is always false ( and this is the definition of false + // an empty Expr is always false ( and this is the definition of false // - expression []minterm + Expr []Term - //minterm is a product of identifier or their negation. For instance - // mintern "AB'D" <=> "A or Not(B) or D" is coded as minterm{ "A":true, "B":false, "D":true} + //Term is a product of identifiers or their negation. For instance + // mintern "AB'D" <=> "A or Not(B) or D" is coded as Term{ "A":true, "B":false, "D":true} // - // it is conventional that https://en.wikipedia.org/wiki/Empty_product the empty minterm is 1 the neutral for prod ( and for and too) + // it is conventional that https://en.wikipedia.org/wiki/Empty_product the empty Term is 1 the neutral for prod ( and for and too) // - minterm map[string]bool - - // Expr is the interface that all elements of a boolean algebra share - Expr interface { - String() string - // return the negation of receiver - Not() Expr - // Is return true if the Expr is literally equals to value - Is(val bool) bool - Terms() int - Term(i int) Expr - IDs() (ids map[string]struct{}) - } + Term map[string]bool ) // String return the literal representation (using primary functions) of the current expression. -func (x expression) String() string { +func (x Expr) String() string { if len(x) == 0 { return "Lit(false)" } @@ -54,8 +42,8 @@ func (x expression) String() string { return "Or(" + strings.Join(terms, ", ") + ")" } -//String return the literal representation (using primary functions) of the current minterm -func (m minterm) String() string { +// String return the literal representation (using primary functions) of the current minterm +func (m Term) String() string { if len(m) == 0 { return "Lit(true)" } @@ -81,7 +69,7 @@ func (m minterm) String() string { // NOT -func (x expression) Not() Expr { +func (x Expr) Not() Expr { factors := make([]Expr, 0, len(x)) for _, e := range x { factors = append(factors, e.Not()) @@ -89,16 +77,16 @@ func (x expression) Not() Expr { return And(factors...) } -func (m minterm) Not() Expr { - res := make(expression, 0, len(m)) +func (m Term) Not() Expr { + res := make(Expr, 0, len(m)) for k, v := range m { - res = append(res, minterm{string(k): !v}) + res = append(res, Term{string(k): !v}) } return res } -//Is return true if this expression is equals to val -func (x expression) Is(val bool) bool { +// Is return true if this expression is equals to val +func (x Expr) isLiteral(val bool) bool { if val { return len(x) == 1 && len(x[0]) == 0 } else { @@ -106,35 +94,13 @@ func (x expression) Is(val bool) bool { } } -//Is return true if this expression is equals to val -func (m minterm) Is(val bool) bool { +// Is return true if this expression is equals to val +func (m Term) isLiteral(val bool) bool { return val && len(m) == 0 } -//Terms retuns the number of terms in this expression -func (x expression) Terms() int { return len(x) } - -//Terms retuns the number of terms in this expression -func (m minterm) Terms() int { return 1 } - -//Term retuns the ith terms. Panic if out of bounds ( negative, or >= Terms()) -func (x expression) Term(i int) Expr { - if i < 0 || i >= x.Terms() { - panic("Term is not defined for this index value") - } - return x[i] -} - -//Term retuns the ith terms. Panic if out of bounds ( negative, or >= Terms()) -func (m minterm) Term(i int) Expr { - if i != 0 { - panic("Term is not defined for this index value") - } - return m -} - // IDs return the set of ID in this expression -func (x expression) IDs() (ids map[string]struct{}) { +func (x Expr) IDs() (ids map[string]struct{}) { ids = make(map[string]struct{}) for _, m := range x { for k := range m { @@ -145,7 +111,7 @@ func (x expression) IDs() (ids map[string]struct{}) { } // IDs return the set of ID in this expression -func (m minterm) IDs() (ids map[string]struct{}) { +func (m Term) IDs() (ids map[string]struct{}) { ids = make(map[string]struct{}) for k := range m { ids[k] = struct{}{} From 609544ab30cfa1439d5a4c27b2c44248fd04fc4c Mon Sep 17 00:00:00 2001 From: Jonathan Duck Date: Wed, 31 Aug 2022 10:44:42 -0700 Subject: [PATCH 2/2] Make term types generic --- builders.go | 16 +++++----- builders_test.go | 4 +-- go.mod | 2 +- grid/rules.go | 30 ++++++++++++------ primary.go | 43 ++++++++++++-------------- primary_test.go | 26 ++++++++-------- quine-mccluskey.go | 36 +++++++++++----------- quine-mccluskey_test.go | 30 +++++++++--------- secondary.go | 8 ++--- secondary_test.go | 4 +-- smullyan/counting.go | 24 +++++++-------- types.go | 67 ++++++++++++++++++++++++++++------------- 12 files changed, 160 insertions(+), 130 deletions(-) diff --git a/builders.go b/builders.go index d577ae5..62ecb6c 100644 --- a/builders.go +++ b/builders.go @@ -1,18 +1,18 @@ package boolgebra // TermBuilder can be used to efficiently append ID ( or Not(ID)) into a big And -type TermBuilder struct { +type TermBuilder[T comparable] struct { isLitFalse bool - m Term + m Term[T] } // And append a variable to the current term -func (t *TermBuilder) And(id string, val bool) { +func (t *TermBuilder[T]) And(id T, val bool) { if t.isLitFalse { return } // nothing to do if t.m == nil { - t.m = make(Term) + t.m = make(Term[T]) } if prev, exists := t.m[id]; exists && prev != val { //attempt to do something like AND(x, !x) which is always false therefore the result will always be Lit(false) @@ -24,14 +24,14 @@ func (t *TermBuilder) And(id string, val bool) { } // IsFalse returns true if the term under construction is already degenerated to False -func (t TermBuilder) IsFalse() bool { return t.isLitFalse } +func (t TermBuilder[T]) IsFalse() bool { return t.isLitFalse } -func (t *TermBuilder) Build() Expr { +func (t *TermBuilder[T]) Build() Expr[T] { if t.isLitFalse { t.isLitFalse = false // reset it - return Lit(false) + return Lit[T](false) } res := t.m t.m = nil // destroy reference to m to avoid editing it anymore - return Expr{res} + return Expr[T]{res} } diff --git a/builders_test.go b/builders_test.go index e4e2905..d26d8fc 100644 --- a/builders_test.go +++ b/builders_test.go @@ -11,7 +11,7 @@ import ( // This benchmark is to show that, in comparision with BenchmarkTermBuilder func BenchmarkAnd(b *testing.B) { - res := Lit(true) // neutral for And + res := Lit[string](true) // neutral for And // prepare 1000 different ids M := 1000 var ids []string @@ -36,7 +36,7 @@ func BenchmarkTermBuilder(b *testing.B) { for i := 0; i < M; i++ { ids = append(ids, "parameter_"+strconv.Itoa(i)) } - var t TermBuilder + var t TermBuilder[string] b.ResetTimer() for i := 0; i < b.N; i++ { t.And(ids[i%M], true) diff --git a/go.mod b/go.mod index 5e4e470..d3a3b7f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/etnz/boolgebra -go 1.12 +go 1.18 require ( github.com/etnz/permute v0.0.0-20160126210303-99613134c393 diff --git a/grid/rules.go b/grid/rules.go index c337195..45fbff4 100644 --- a/grid/rules.go +++ b/grid/rules.go @@ -8,20 +8,30 @@ import ( ) // creates an ID that means property name = value -// just for test, not safe for any kind of injection -func P(name, value string) Expr { return ID(name + "=" + value) } +func P[T comparable](name, value T) Expr[Pair[T]] { + return ID(Pair[T]{Key: name, Val: value}) +} + +type Pair[T comparable] struct { + Val T + Key T +} + +func (p Pair[T]) String() string { + return fmt.Sprintf("%v=%v", p.Key, p.Val) +} // Values is a list all possible values without repetition // // a Group is a subset of Values, Groups is the set of all defined Group, N is card(Groups) // -// Let's define 'R' an transitive and symetric relation in Values noted `\forall x,y \in Values xRy` +// Let's define 'R' an transitive and symmetric relation in Values noted `\forall x,y \in Values xRy` // // 1. `\forall g,h \in Groups² |g| = |h| \and g \inter h = \phi` // 2. `\forall G \in Groups, \forall v \notin G \exists! w in G vRw` // // groups are defined by to position in the list -func Rules(N int, values ...string) Expr { +func Rules[T comparable](N int, values ...T) Expr[Pair[T]] { if len(values)%N != 0 { panic(fmt.Sprintf("inconsistent number of values %d with number of groups %d (not divisible)", len(values), N)) @@ -30,7 +40,7 @@ func Rules(N int, values ...string) Expr { M := len(values) / N { // the following code is in a a block 'cause I don't keep anyting from here the index is purely local, and temporary - index := make(map[string]struct{}) // index values (that shall be unique) + index := make(map[T]struct{}) // index values (that shall be unique) for _, v := range values { index[v] = struct{}{} } @@ -82,16 +92,16 @@ func Rules(N int, values ...string) Expr { } //rules := make([]map[string]bool, 0) // the game rules will be an Or() of all possible solutions - rules := Lit(false) + rules := Lit[Pair[T]](false) for ok := true; ok; ok = next() { // loop over all columns x all permutations, and break when done - var tb TermBuilder // build a big AND expression + var tb TermBuilder[Pair[T]] // build a big AND expression // for the given solution, scan all possible ID ( "Paul is 6yo") wether it is true or not for i, v := range values { for j, w := range values { if i != j { - tb.And(v+"="+w, whois(i) == whois(j)) + tb.And(Pair[T]{v, w}, whois(i) == whois(j)) } } } @@ -104,9 +114,9 @@ func Rules(N int, values ...string) Expr { return rules } -func Solve(nbproperties int, values []string, hints ...Expr) Expr { +func Solve[T comparable](nbproperties int, values []T, hints ...Expr[Pair[T]]) Expr[Pair[T]] { - rules := []Expr{} + rules := []Expr[Pair[T]]{} rules = append(rules, Rules(nbproperties, values...)) rules = append(rules, hints...) return Simplify(And(rules...)) diff --git a/primary.go b/primary.go index c1b73c3..0b780f1 100644 --- a/primary.go +++ b/primary.go @@ -4,28 +4,28 @@ package boolgebra // expression/minterm types // Lit returns an Expr equivalent to a boolean literal 'val' -func Lit(val bool) Expr { +func Lit[T comparable](val bool) Expr[T] { if val { - return Expr{Term{}} // true is by definition an empty minterm ( neutral for product) + return Expr[T]{Term[T]{}} // true is by definition an empty minterm ( neutral for product) } else { - return Expr{} // false is an empty expression (neutral for sum) + return Expr[T]{} // false is an empty expression (neutral for sum) } } // ID returns an Expr equivalent to a single ID 'id' -func ID(id string) Expr { return Expr{Term{id: true}} } +func ID[T comparable](id T) Expr[T] { return Expr[T]{Term[T]{id: true}} } // Or return the conjunction of all the expression passed in parameter. // // By convention, if 'x' is empty it returns Lit(false). See https://en.wikipedia.org/wiki/Empty_sum -func Or(x ...Expr) Expr { +func Or[T comparable](x ...Expr[T]) Expr[T] { // start with the neutral of the Or i.e a false - res := make(Expr, 0) + res := make(Expr[T], 0) // scan all terms, in all expr for _, exp := range x { for _, t := range exp { if t.isLiteral(true) { - return Expr{Term{}} + return Expr[T]{Term[T]{}} } if !t.isLiteral(false) { // if this is the literal false, we can just skip it res = append(res, t) @@ -33,7 +33,7 @@ func Or(x ...Expr) Expr { } } if len(res) == 1 { - return Expr{res[0]} + return Expr[T]{res[0]} } return res } @@ -41,10 +41,10 @@ func Or(x ...Expr) Expr { // And returns the disjunction of all the expressions passed in parameters. // // By convention, if 'x' is empty it returns Lit(true). See https://en.wikipedia.org/wiki/Empty_product -func And(expressions ...Expr) Expr { +func And[T comparable](expressions ...Expr[T]) Expr[T] { if len(expressions) == 0 { - return Lit(true) // return the neutral of And operation by convention + return Lit[T](true) // return the neutral of And operation by convention } if len(expressions) == 1 { return expressions[0] // another common degenerated case @@ -62,7 +62,7 @@ func And(expressions ...Expr) Expr { x, y := expressions[0], expressions[1] if x.isLiteral(false) || y.isLiteral(false) { - return Lit(false) + return Lit[T](false) } if x.isLiteral(true) { @@ -73,7 +73,7 @@ func And(expressions ...Expr) Expr { } // general case - z := make(Expr, 0, len(x)*len(y)) + z := make(Expr[T], 0, len(x)*len(y)) // this is the big one: all terms from x multiplied by terms from y for _, m := range x { @@ -90,7 +90,7 @@ func And(expressions ...Expr) Expr { } // basic merge - o := make(Term) + o := make(Term[T]) for k, v := range m { o[k] = v } @@ -106,12 +106,7 @@ func And(expressions ...Expr) Expr { } // Not returns the negation of 'x'. -func Not(x Expr) Expr { return x.Not() } - -// Simplify returns a simpler version of 'x' by applying simplification rules. -func Simplify(x Expr) Expr { - return reduce(x) -} +func Not[T comparable](x Expr[T]) Expr[T] { return x.Not() } // Factor computes the greatest common factor between terms of x // @@ -121,8 +116,8 @@ func Simplify(x Expr) Expr { // // x = And(f, rem) // f.Terms() ==1 : it's a minterm -func Factor(x Expr) (f, rem Expr) { - var res Term +func Factor[T comparable](x Expr[T]) (f, rem Expr[T]) { + var res Term[T] for i, m := range x { if i == 0 { // special case for the first one, need to init the thing @@ -130,16 +125,16 @@ func Factor(x Expr) (f, rem Expr) { } res = inter(res, m) if len(res) == 0 { - return Expr{res}, x // empty one + return Expr[T]{res}, x // empty one } } // now for each minterm recompute the reminder - r := Expr{} + r := Expr[T]{} for _, m := range x { r = append(r, div(m, res)) } - return Expr{res}, r + return Expr[T]{res}, r } diff --git a/primary_test.go b/primary_test.go index 48128cf..b44b289 100644 --- a/primary_test.go +++ b/primary_test.go @@ -13,19 +13,19 @@ func ExampleID() { // TesLit ensure that the basic true and false are working accordingly with Is(bool) func TestLit(t *testing.T) { - if !Lit(true).isLiteral(true) { + if !Lit[string](true).isLiteral(true) { t.Error("Lit(true).Is(true) must be true") } - if !Lit(false).isLiteral(false) { + if !Lit[string](false).isLiteral(false) { t.Error("Lit(false).Is(false) must be true") } } func ExampleLit() { - A := Lit(true) + A := Lit[string](true) fmt.Println(A) - B := Lit(false) + B := Lit[string](false) fmt.Println(B) //Output: // Lit(true) @@ -34,8 +34,8 @@ func ExampleLit() { func ExampleNot() { fmt.Println(Not(ID("A"))) - fmt.Println(Not(Lit(true))) - fmt.Println(Not(Lit(false))) + fmt.Println(Not(Lit[string](true))) + fmt.Println(Not(Lit[string](false))) //Output: // Not("A") // Lit(false) @@ -46,8 +46,8 @@ func ExampleAnd() { A := ID("A") B := ID("B") C := ID("C") - fmt.Println(And(A, Lit(true))) - fmt.Println(And(A, Lit(false))) + fmt.Println(And(A, Lit[string](true))) + fmt.Println(And(A, Lit[string](false))) fmt.Println(And(A, B, C)) fmt.Println(And(A, Not(B))) //Output: @@ -62,8 +62,8 @@ func ExampleOr() { A := ID("A") B := ID("B") - fmt.Println(Or(A, Lit(true))) - fmt.Println(Or(A, Lit(false))) + fmt.Println(Or(A, Lit[string](true))) + fmt.Println(Or(A, Lit[string](false))) fmt.Println(Or(A, Not(B))) //Output: // Lit(true) @@ -71,7 +71,7 @@ func ExampleOr() { // Or("A", Not("B")) } -func truthTester(t *testing.T, label string, z Expr, expected bool) { +func truthTester(t *testing.T, label string, z Expr[string], expected bool) { if !z.isLiteral(expected) { t.Errorf("%s: expected %v got %v", label, expected, z) } @@ -79,8 +79,8 @@ func truthTester(t *testing.T, label string, z Expr, expected bool) { func Test_truthTables(t *testing.T) { - T := Lit(true) - F := Lit(false) + T := Lit[string](true) + F := Lit[string](false) //Not truthTester(t, "Not(F)", Not(F), true) diff --git a/quine-mccluskey.go b/quine-mccluskey.go index 58160b6..442a709 100644 --- a/quine-mccluskey.go +++ b/quine-mccluskey.go @@ -2,15 +2,15 @@ package boolgebra // Quine-McCluskey is an algorithm to simplify a sum of prod. -// reduce combine together all minterms of x into prime implicants -func reduce(x Expr) Expr { +// Simplify returns a simpler version of 'x' by applying simplification rules. +func Simplify[T comparable](x Expr[T]) Expr[T] { // to reduce we need to cluster minterms of x into number of non neg ID - var cluster [][]Term // index minterm by their 1s. store them in a slice - var primes []Term // also keep primes all together + var cluster [][]Term[T] // index minterm by their 1s. store them in a slice + var primes []Term[T] // also keep primes all together // fill the first cluster - cluster = make([][]Term, 1+len(x.IDs())) + cluster = make([][]Term[T], 1+len(x.IDs())) for _, m := range x { ones := positives(m) cluster[ones] = append(cluster[ones], m) @@ -20,7 +20,7 @@ func reduce(x Expr) Expr { for !emptycluster { // the next cluster will become the current one soon, so we already set the bool to true, because we start with an empty one emptycluster = true // we start with an empty next one, let see if it get filled - next := make([][]Term, len(cluster)) + next := make([][]Term[T], len(cluster)) // attempt all possible combinations. // a minterm with n Positive IDs, can only be combined with another one with n or n+1 ( or n-1 but combination is symetric so we don't care) @@ -94,12 +94,12 @@ func reduce(x Expr) Expr { cluster = next } //done, we now have all the prime implicant - return Expr(primes) + return Expr[T](primes) } // appenunique behave like 'append' except for items in 'terms' that are present in 'set': they // are not appended in this case. -func appendunique(set []Term, terms ...Term) []Term { +func appendunique[T comparable](set []Term[T], terms ...Term[T]) []Term[T] { termsloop: for _, m := range terms { for _, x := range set { @@ -118,11 +118,11 @@ termsloop: // x and y must be identical but on exactly one identifier. // // the combined is then then intersection of x and y. -func combine(x, y Term) (c Term, ok bool) { +func combine[T comparable](x, y Term[T]) (c Term[T], ok bool) { // alg: find out the one and only one difference between x,y // so scan for differences and count. - var d string // the identifier that is different (if diffs == 1)) - diffs := 0 // number of differences + var d T // the identifier that is different (if diffs == 1)) + diffs := 0 // number of differences for k, v := range x { w, exists := y[k] if !exists || v != w { @@ -154,7 +154,7 @@ func combine(x, y Term) (c Term, ok bool) { // build c accordingly then // x and y are guaranteed to be identical but on 'd' // so copy x but 'd' - c = make(Term) + c = make(Term[T]) for k, v := range x { if k != d { c[k] = v @@ -165,7 +165,7 @@ func combine(x, y Term) (c Term, ok bool) { } // equals return true if and only if m and n are both minterm, then they are semantically equals -func equals(m, n Term) bool { +func equals[T comparable](m, n Term[T]) bool { if len(m) != len(n) { return false } @@ -178,7 +178,7 @@ func equals(m, n Term) bool { } // positives returns the number of positive identifiers -func positives(m Term) int { +func positives[T comparable](m Term[T]) int { count := 0 for _, v := range m { if v { @@ -189,8 +189,8 @@ func positives(m Term) int { } // inter computes the intersection of x inter y -func inter(x, y Term) Term { - res := make(Term) +func inter[T comparable](x, y Term[T]) Term[T] { + res := make(Term[T]) for k, v := range x { if w, exists := y[k]; exists && v == w { res[k] = v @@ -202,8 +202,8 @@ func inter(x, y Term) Term { // div computes x/y i.e z so that And(z,y) = x // can be seen as x removed from items in y -func div(x, y Term) Term { - res := make(Term) +func div[T comparable](x, y Term[T]) Term[T] { + res := make(Term[T]) for k, v := range x { if w, exists := y[k]; !exists || v != w { res[k] = v diff --git a/quine-mccluskey_test.go b/quine-mccluskey_test.go index 34e257e..265e4a4 100644 --- a/quine-mccluskey_test.go +++ b/quine-mccluskey_test.go @@ -28,20 +28,20 @@ func TestReduce(t *testing.T) { m11 := m("ab'cd") m14 := m("abcd'") m15 := m("abcd") - x := Expr{m4, m8, m9, m10, m12, m11, m14, m15} + x := Expr[string]{m4, m8, m9, m10, m12, m11, m14, m15} //primes are //m4_12 := m("bc'd'") //m8_9_10_11 := m("ab'") //m8_10_12_14 := m("ad'") //m10_11_14_15 := m("ac") - y := reduce(x) + y := Simplify(x) t.Logf("reduced = %v", y) } // newminterm creates a new minterm using ' at the end of the string to find out that its a neg -func m(x string) Term { - res := make(Term) +func m(x string) Term[string] { + res := make(Term[string]) for i, id := range x { nextisquote := i+1 < len(x) && x[i+1] == '\'' @@ -54,7 +54,7 @@ func m(x string) Term { // TestPosLen make sure that we count the number of true correctly func TestPositives(t *testing.T) { - x := Term{"A": true, "B": true, "C": false, "E": true} + x := Term[string]{"A": true, "B": true, "C": false, "E": true} if positives(x) != 3 { t.Errorf("invalid minterm %v PosLen attribute got %v want 3", x, positives(x)) } @@ -63,13 +63,13 @@ func TestPositives(t *testing.T) { // TestMinterm_combine gold test some minterm combinations func TestCombine(t *testing.T) { - x := Term{"A": true, "B": true, "C": false, "E": true} - var y, r, c Term + x := Term[string]{"A": true, "B": true, "C": false, "E": true} + var y, r, c Term[string] var ok bool // 1,0 -> _ - y = Term{"A": true, "B": true, "C": false, "E": false} - r = Term{"A": true, "B": true, "C": false} + y = Term[string]{"A": true, "B": true, "C": false, "E": false} + r = Term[string]{"A": true, "B": true, "C": false} c, ok = combine(x, y) if !ok { t.Errorf("failed to combine(%v,%v)", x, y) @@ -78,8 +78,8 @@ func TestCombine(t *testing.T) { } // 1,_ -> _ - y = Term{"A": true, "B": true, "C": false} - r = Term{"A": true, "B": true, "C": false} + y = Term[string]{"A": true, "B": true, "C": false} + r = Term[string]{"A": true, "B": true, "C": false} c, ok = combine(x, y) if !ok { t.Errorf("failed to combine(%v,%v)", x, y) @@ -88,8 +88,8 @@ func TestCombine(t *testing.T) { } // 0,1 -> _ - y = Term{"A": true, "B": true, "C": true, "E": true} - r = Term{"A": true, "B": true, "E": true} + y = Term[string]{"A": true, "B": true, "C": true, "E": true} + r = Term[string]{"A": true, "B": true, "E": true} c, ok = combine(x, y) if !ok { t.Errorf("failed to combine(%v,%v)", x, y) @@ -98,8 +98,8 @@ func TestCombine(t *testing.T) { } // 0,_ -> _ - y = Term{"A": true, "B": true, "E": true} - r = Term{"A": true, "B": true, "E": true} + y = Term[string]{"A": true, "B": true, "E": true} + r = Term[string]{"A": true, "B": true, "E": true} c, ok = combine(x, y) if !ok { t.Errorf("failed to combine(%v,%v)", x, y) diff --git a/secondary.go b/secondary.go index 637757d..fc6f68d 100644 --- a/secondary.go +++ b/secondary.go @@ -6,7 +6,7 @@ package boolgebra // Impl returns the logical implication of A,B // // see https://en.wikipedia.org/wiki/Boolean_algebra#Secondary_operations -func Impl(A, B Expr) Expr { +func Impl[T comparable](A, B Expr[T]) Expr[T] { return Or(Not(A), B) } @@ -15,16 +15,16 @@ func Impl(A, B Expr) Expr { // It can also be the logical equivalence A <=> B. Both are in fact the same boolean function. // // see https://en.wikipedia.org/wiki/Boolean_algebra#Secondary_operations -func Eq(A, B Expr) Expr { return Or(And(Not(A), Not(B)), And(A, B)) } +func Eq[T comparable](A, B Expr[T]) Expr[T] { return Or(And(Not(A), Not(B)), And(A, B)) } // Xor returns the logical Xor // // see https://en.wikipedia.org/wiki/Boolean_algebra#Secondary_operations -func Xor(A, B Expr) Expr { return Or(And(A, Not(B)), And(Not(A), B)) } +func Xor[T comparable](A, B Expr[T]) Expr[T] { return Or(And(A, Not(B)), And(Not(A), B)) } // Neq returns the logical '!=' // // It is the same as Xor. // // see https://en.wikipedia.org/wiki/Boolean_algebra#Secondary_operations -func Neq(A, B Expr) Expr { return Xor(A, B) } +func Neq[T comparable](A, B Expr[T]) Expr[T] { return Xor(A, B) } diff --git a/secondary_test.go b/secondary_test.go index 2d48dbd..56bf263 100644 --- a/secondary_test.go +++ b/secondary_test.go @@ -4,8 +4,8 @@ import "testing" func Test_truthTables2(t *testing.T) { - T := Lit(true) - F := Lit(false) + T := Lit[string](true) + F := Lit[string](false) //XOr truthTester(t, "Xor(F,F)", Xor(F, F), false) diff --git a/smullyan/counting.go b/smullyan/counting.go index ca423ad..a5c8d6f 100644 --- a/smullyan/counting.go +++ b/smullyan/counting.go @@ -10,22 +10,22 @@ import ( // Exactly retuns an expression that is true if and only if exactly 'i' terms are True. // // This is the Or() of and And() of all the i-subsets of terms -func Exactly(i int, terms ...Expr) Expr { - return quantified(i, identity, Not, terms...) +func Exactly[T comparable](i int, terms ...Expr[T]) Expr[T] { + return quantified(i, identity[T], Not[T], terms...) } // AtMost retuns an expression that is true if and only if at most 'i' terms are True. -func AtMost(i int, terms ...Expr) Expr { - return quantified(i, truth, Not, terms...) +func AtMost[T comparable](i int, terms ...Expr[T]) Expr[T] { + return quantified(i, truth[T], Not[T], terms...) } // AtLeast retuns an expression that is true if and only if at least 'i' terms are True. -func AtLeast(i int, terms ...Expr) Expr { - return quantified(i, identity, truth, terms...) +func AtLeast[T comparable](i int, terms ...Expr[T]) Expr[T] { + return quantified(i, identity[T], truth[T], terms...) } -func identity(x Expr) Expr { return x } -func truth(x Expr) Expr { return Lit(true) } +func identity[T comparable](x Expr[T]) Expr[T] { return x } +func truth[T comparable](x Expr[T]) Expr[T] { return Lit[T](true) } // quantifier returns Or ( And( f(p,p')) )where: // p is a subset of terms, p' is the complement ( remain terms) @@ -35,13 +35,13 @@ func truth(x Expr) Expr { return Lit(true) } // - for Atmost: we just need to build those with p' // // therefore we defined two simple function fp(Expr) and fp' accordingly -func quantified(i int, f, g func(x Expr) Expr, terms ...Expr) Expr { +func quantified[T comparable](i int, f, g func(x Expr[T]) Expr[T], terms ...Expr[T]) Expr[T] { - p := subid(i) // the slice of indices starting at identify, always - var ors []Expr // all ands to be Or()ed + p := subid(i) // the slice of indices starting at identify, always + var ors []Expr[T] // all ands to be Or()ed for ok := true; ok; ok = nextsubset(p, len(terms)) { - res := make([]Expr, 0, len(terms)) + res := make([]Expr[T], 0, len(terms)) c := complement(p, len(terms)) for _, i := range p { diff --git a/types.go b/types.go index 830afa7..ad4d80e 100644 --- a/types.go +++ b/types.go @@ -1,6 +1,7 @@ package boolgebra import ( + "fmt" "sort" "strconv" "strings" @@ -12,18 +13,18 @@ type ( // // an empty Expr is always false ( and this is the definition of false // - Expr []Term + Expr[T comparable] []Term[T] //Term is a product of identifiers or their negation. For instance // mintern "AB'D" <=> "A or Not(B) or D" is coded as Term{ "A":true, "B":false, "D":true} // // it is conventional that https://en.wikipedia.org/wiki/Empty_product the empty Term is 1 the neutral for prod ( and for and too) // - Term map[string]bool + Term[T comparable] map[T]bool ) // String return the literal representation (using primary functions) of the current expression. -func (x Expr) String() string { +func (x Expr[T]) String() string { if len(x) == 0 { return "Lit(false)" } @@ -43,50 +44,74 @@ func (x Expr) String() string { } // String return the literal representation (using primary functions) of the current minterm -func (m Term) String() string { +func (m Term[T]) String() string { if len(m) == 0 { return "Lit(true)" } - var terms []string + terms := make([]T, 0, len(m)) for k := range m { terms = append(terms, k) } - sort.Strings(terms) + var sTerms []string + switch s := any(terms).(type) { + case []string: + sort.Strings(s) + sTerms = s + case []int: + sort.Ints(s) + default: + sTerms = make([]string, len(terms)) + for i := range terms { + sTerms[i] = fmt.Sprintf("%v", terms[i]) + } + + sort.Slice(sTerms, func(i, j int) bool { + return sTerms[i] < sTerms[j] + }) + } + if sTerms != nil { + sTerms = make([]string, len(terms)) + for i := range terms { + sTerms[i] = fmt.Sprintf("%v", terms[i]) + } + } + for i, t := range terms { + s := sTerms[i] if !m[t] { - terms[i] = "Not(" + strconv.Quote(t) + ")" + sTerms[i] = "Not(" + strconv.Quote(s) + ")" } else { - terms[i] = strconv.Quote(t) + sTerms[i] = strconv.Quote(s) } } if len(terms) == 1 { - return terms[0] + return sTerms[0] } else { - return "And(" + strings.Join(terms, ", ") + ")" + return "And(" + strings.Join(sTerms, ", ") + ")" } } // NOT -func (x Expr) Not() Expr { - factors := make([]Expr, 0, len(x)) +func (x Expr[T]) Not() Expr[T] { + factors := make([]Expr[T], 0, len(x)) for _, e := range x { factors = append(factors, e.Not()) } return And(factors...) } -func (m Term) Not() Expr { - res := make(Expr, 0, len(m)) +func (m Term[T]) Not() Expr[T] { + res := make(Expr[T], 0, len(m)) for k, v := range m { - res = append(res, Term{string(k): !v}) + res = append(res, Term[T]{k: !v}) } return res } // Is return true if this expression is equals to val -func (x Expr) isLiteral(val bool) bool { +func (x Expr[T]) isLiteral(val bool) bool { if val { return len(x) == 1 && len(x[0]) == 0 } else { @@ -95,13 +120,13 @@ func (x Expr) isLiteral(val bool) bool { } // Is return true if this expression is equals to val -func (m Term) isLiteral(val bool) bool { +func (m Term[T]) isLiteral(val bool) bool { return val && len(m) == 0 } // IDs return the set of ID in this expression -func (x Expr) IDs() (ids map[string]struct{}) { - ids = make(map[string]struct{}) +func (x Expr[T]) IDs() (ids map[T]struct{}) { + ids = make(map[T]struct{}) for _, m := range x { for k := range m { ids[k] = struct{}{} @@ -111,8 +136,8 @@ func (x Expr) IDs() (ids map[string]struct{}) { } // IDs return the set of ID in this expression -func (m Term) IDs() (ids map[string]struct{}) { - ids = make(map[string]struct{}) +func (m Term[T]) IDs() (ids map[T]struct{}) { + ids = make(map[T]struct{}) for k := range m { ids[k] = struct{}{} }