From 2636b9c86f683bdec05306c7374c2225ceb03704 Mon Sep 17 00:00:00 2001 From: Edwin Marrima Date: Thu, 12 Oct 2023 14:54:20 +0200 Subject: [PATCH 1/6] feat: constraint set theory comparison --- .idea/.gitignore | 8 ++++ .idea/go-version.iml | 9 ++++ .idea/modules.xml | 8 ++++ .idea/vcs.xml | 6 +++ constraint.go | 104 ++++++++++++++++++++++++++++++++++++++++++- constraint_test.go | 34 ++++++++++++++ 6 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/go-version.iml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/go-version.iml b/.idea/go-version.iml new file mode 100644 index 0000000..5e764c4 --- /dev/null +++ b/.idea/go-version.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e221e79 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/constraint.go b/constraint.go index da5d1ac..5a682b7 100644 --- a/constraint.go +++ b/constraint.go @@ -10,6 +10,7 @@ import ( // Constraint represents a single constraint for a version, such as // ">= 1.0". + type Constraint struct { f constraintFunc op operator @@ -27,6 +28,8 @@ type Constraints []*Constraint type constraintFunc func(v, c *Version) bool +type constraintComparison func(v1, v2 Constraint) bool + var constraintOperators map[string]constraintOperation type constraintOperation struct { @@ -36,6 +39,8 @@ type constraintOperation struct { var constraintRegexp *regexp.Regexp +var setTheoryOperators map[operator]constraintComparison + func init() { constraintOperators = map[string]constraintOperation{ "": {op: equal, f: constraintEqual}, @@ -47,7 +52,33 @@ func init() { "<=": {op: lessThanEqual, f: constraintLessThanEqual}, "~>": {op: pessimistic, f: constraintPessimistic}, } - + setTheoryOperators = map[operator]constraintComparison{ + greaterThan: func(v1, v2 Constraint) bool { + if !(v2.op == greaterThan || v2.op == greaterThanEqual) { + return false + } + return v1.check.LessThan(v2.check) + }, + greaterThanEqual: func(v1, v2 Constraint) bool { + if !(v2.op == greaterThan || v2.op == greaterThanEqual) { + return false + } + return v1.check.LessThanOrEqual(v2.check) + }, + lessThan: func(v1, v2 Constraint) bool { + if !(v2.op == lessThan || v2.op == lessThanEqual) { + return false + } + + return v1.check.GreaterThan(v2.check) + }, + lessThanEqual: func(v1, v2 Constraint) bool { + if !(v2.op == lessThan || v2.op == lessThanEqual) { + return false + } + return v1.check.GreaterThanOrEqual(v2.check) + }, + } ops := make([]string, 0, len(constraintOperators)) for k := range constraintOperators { ops = append(ops, regexp.QuoteMeta(k)) @@ -98,6 +129,63 @@ func (cs Constraints) Check(v *Version) bool { return true } +// EqualsLogical compares Constraints with other Constraints +// for equality. This represents a logical equivalence of compared +// constraints. +// e.g. '>0.1,>0.5' is logically equivalent to '>0.2' +// +// Missing operator is treated as equal to '=', whitespaces +// are ignored. +func (cs Constraints) EqualsLogical(c Constraints) bool { + // Loop through the constraints in the first set. + for _, c1 := range cs { + // Loop through the constraints in the second set. + for _, c2 := range c { + // If c1 is an "equal" constraint and it's not equal to c2, return false. + if c1.op == equal && !c1.Equals(c2) { + return false + } + // If c1 is a "pessimistic" constraint, check various conditions. + if c1.op == pessimistic { + // If c2 is an "equal" constraint and c1 passes the check, return true. + if c2.op == equal && c1.Check(c2.check) { + continue + } + // If both c1 and c2 are "pessimistic" constraints, compare their segments. + if c2.op == pessimistic { + c1seg := c1.check.Segments() + c2seg := c2.check.Segments() + // If the segments match and c2's third segment is greater or equal to c1's third segment, return true. + if c1seg[0] == c2seg[0] && c1seg[1] == c2seg[1] && c2seg[2] >= c1seg[2] { + continue + } + } + // If none of the above conditions are met, return false. + return false + } + // If c1 and c2 have valid operators, use the set theory operators to compare them. + v1, ok1 := setTheoryOperators[c1.op] + _, ok2 := setTheoryOperators[c2.op] + if ok1 && ok2 { + // If the set theory operator returns false, return false. + if !v1(*c1, *c2) { + return false + } + } + // If c1 has a valid operator and c2 is an "equal" constraint, check if c1 passes the check. + if ok1 && c2.op == equal { + if c1.Check(c2.check) { + continue + } + // If the check fails, return false. + return false + } + } + } + // If no false conditions are met, return true. + return true +} + // Equals compares Constraints with other Constraints // for equality. This may not represent logical equivalence // of compared constraints. @@ -105,7 +193,7 @@ func (cs Constraints) Check(v *Version) bool { // to '>0.2' it is *NOT* treated as equal. // // Missing operator is treated as equal to '=', whitespaces -// are ignored and constraints are sorted before comaparison. +// are ignored and constraints are sorted before comparison. func (cs Constraints) Equals(c Constraints) bool { if len(cs) != len(c) { return false @@ -114,7 +202,9 @@ func (cs Constraints) Equals(c Constraints) bool { // make copies to retain order of the original slices left := make(Constraints, len(cs)) copy(left, cs) + sort.Stable(left) + right := make(Constraints, len(c)) copy(right, c) sort.Stable(right) @@ -162,6 +252,15 @@ func (cs Constraints) String() string { func (c *Constraint) Check(v *Version) bool { return c.f(v, c.check) } +func (cs *Constraint) IsWithin(c Constraint) bool { + // if cs.set != nil && cs.set != nil { + // if cs.set.isNegativeInfinite && c.set.isNegativeInfinite { + // if cs + // return cs.check.GreaterThanOrEqual(c.check) + // } + // } + return false +} // Prerelease returns true if the version underlying this constraint // contains a prerelease field. @@ -175,6 +274,7 @@ func (c *Constraint) String() string { func parseSingle(v string) (*Constraint, error) { matches := constraintRegexp.FindStringSubmatch(v) + if matches == nil { return nil, fmt.Errorf("Malformed constraint: %s", v) } diff --git a/constraint_test.go b/constraint_test.go index 5338c8e..ccd6719 100644 --- a/constraint_test.go +++ b/constraint_test.go @@ -127,7 +127,41 @@ func TestConstraintPrerelease(t *testing.T) { } } } +func TestConstraintEqualsLogical(t *testing.T) { + cases := []struct { + leftConstraint string + rightConstraint string + expectedEqual bool + }{ + {"0.0.1", "0.0.1", true}, + {"5.0.1", "> 5.0.7", false}, + {"~> 5.0", "5.0.7", true}, + {"~> 5.0", "~> 5.0.2", true}, + {"~> 5.0.77", "~> 5.0.2", false}, + {"> 5.0.77", "5.0.55", false}, + {"> 5.0.77", "5.0.78", true}, + {"< 5.5.77", "5.0.78", true}, + {"< 5.5.77,<4.5.77", "4.5.76", true}, + {"<10.5.77,>4.5.77", "6.5.76", true}, + {">10.5.77,<4.5.77", "16.5.76", false}, + } + for _, tc := range cases { + leftCon, err := NewConstraint(tc.leftConstraint) + if err != nil { + t.Fatalf("err: %s", err) + } + rightCon, err := NewConstraint(tc.rightConstraint) + if err != nil { + t.Fatalf("err: %s", err) + } + actual := leftCon.EqualsLogical(rightCon) + if actual != tc.expectedEqual { + t.Fatalf("Constraints: %s vs %s\nExpected: %t\nActual: %t", + tc.leftConstraint, tc.rightConstraint, tc.expectedEqual, actual) + } + } +} func TestConstraintEqual(t *testing.T) { cases := []struct { leftConstraint string From 5b9b8a725a5e421daed4ecca7cac823cc7b73147 Mon Sep 17 00:00:00 2001 From: Edwin Marrima Date: Thu, 12 Oct 2023 14:56:29 +0200 Subject: [PATCH 2/6] wip: add gitignore --- .gitignore | 1 + .idea/.gitignore | 8 -------- .idea/go-version.iml | 9 --------- .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ 5 files changed, 1 insertion(+), 31 deletions(-) create mode 100644 .gitignore delete mode 100644 .idea/.gitignore delete mode 100644 .idea/go-version.iml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..723ef36 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/go-version.iml b/.idea/go-version.iml deleted file mode 100644 index 5e764c4..0000000 --- a/.idea/go-version.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index e221e79..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file From 1a20e1b5202b0a8430be26b52b78f380e27eaae4 Mon Sep 17 00:00:00 2001 From: Edwin Marrima Date: Thu, 12 Oct 2023 15:11:06 +0200 Subject: [PATCH 3/6] feat: update comments --- constraint.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/constraint.go b/constraint.go index 5a682b7..98e6df3 100644 --- a/constraint.go +++ b/constraint.go @@ -132,7 +132,8 @@ func (cs Constraints) Check(v *Version) bool { // EqualsLogical compares Constraints with other Constraints // for equality. This represents a logical equivalence of compared // constraints. -// e.g. '>0.1,>0.5' is logically equivalent to '>0.2' +// e.g. '>0.1,>0.5' and '=0.2' return true, '<10.5.77,>4.5.77' and '6.5.76' returns true +// '~> 5.0' and '~> 5.0.2' return true. // // Missing operator is treated as equal to '=', whitespaces // are ignored. From 13f7118527b3453e410f3b4c9d31f84f2d54837a Mon Sep 17 00:00:00 2001 From: Edwin Marrima Date: Thu, 12 Oct 2023 15:16:20 +0200 Subject: [PATCH 4/6] feat: update naming --- constraint.go | 13 ++----------- constraint_test.go | 2 +- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/constraint.go b/constraint.go index 98e6df3..21647f3 100644 --- a/constraint.go +++ b/constraint.go @@ -129,7 +129,7 @@ func (cs Constraints) Check(v *Version) bool { return true } -// EqualsLogical compares Constraints with other Constraints +// IsPartOfSets compares Constraints with other Constraints // for equality. This represents a logical equivalence of compared // constraints. // e.g. '>0.1,>0.5' and '=0.2' return true, '<10.5.77,>4.5.77' and '6.5.76' returns true @@ -137,7 +137,7 @@ func (cs Constraints) Check(v *Version) bool { // // Missing operator is treated as equal to '=', whitespaces // are ignored. -func (cs Constraints) EqualsLogical(c Constraints) bool { +func (cs Constraints) IsPartOfSets(c Constraints) bool { // Loop through the constraints in the first set. for _, c1 := range cs { // Loop through the constraints in the second set. @@ -253,15 +253,6 @@ func (cs Constraints) String() string { func (c *Constraint) Check(v *Version) bool { return c.f(v, c.check) } -func (cs *Constraint) IsWithin(c Constraint) bool { - // if cs.set != nil && cs.set != nil { - // if cs.set.isNegativeInfinite && c.set.isNegativeInfinite { - // if cs - // return cs.check.GreaterThanOrEqual(c.check) - // } - // } - return false -} // Prerelease returns true if the version underlying this constraint // contains a prerelease field. diff --git a/constraint_test.go b/constraint_test.go index ccd6719..0c06b4e 100644 --- a/constraint_test.go +++ b/constraint_test.go @@ -155,7 +155,7 @@ func TestConstraintEqualsLogical(t *testing.T) { t.Fatalf("err: %s", err) } - actual := leftCon.EqualsLogical(rightCon) + actual := leftCon.IsPartOfSets(rightCon) if actual != tc.expectedEqual { t.Fatalf("Constraints: %s vs %s\nExpected: %t\nActual: %t", tc.leftConstraint, tc.rightConstraint, tc.expectedEqual, actual) From b1edaea11d8b286f8c90a44e8c4063022a716e46 Mon Sep 17 00:00:00 2001 From: Edwin Marrima Date: Thu, 12 Oct 2023 15:17:52 +0200 Subject: [PATCH 5/6] fix: test name --- constraint_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constraint_test.go b/constraint_test.go index 0c06b4e..7afa45b 100644 --- a/constraint_test.go +++ b/constraint_test.go @@ -127,7 +127,7 @@ func TestConstraintPrerelease(t *testing.T) { } } } -func TestConstraintEqualsLogical(t *testing.T) { +func TestConstraintIsPartOfSets(t *testing.T) { cases := []struct { leftConstraint string rightConstraint string From 623acd80eb6eb5918998207a0cacfa6fd2ab723e Mon Sep 17 00:00:00 2001 From: Edwin Marrima Date: Thu, 12 Oct 2023 15:37:29 +0200 Subject: [PATCH 6/6] fix: update module name --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index f528555..e109675 100644 --- a/go.mod +++ b/go.mod @@ -1 +1 @@ -module github.com/hashicorp/go-version +module github.com/edwin-Marrima/go-version