Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

constraint comparison #131

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.idea
96 changes: 94 additions & 2 deletions constraint.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

// Constraint represents a single constraint for a version, such as
// ">= 1.0".

type Constraint struct {
f constraintFunc
op operator
Expand All @@ -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 {
Expand All @@ -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},
Expand All @@ -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))
Expand Down Expand Up @@ -98,14 +129,72 @@ func (cs Constraints) Check(v *Version) bool {
return true
}

// 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
// '~> 5.0' and '~> 5.0.2' return true.
//
// Missing operator is treated as equal to '=', whitespaces
// are ignored.
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.
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.
// e.g. even though '>0.1,>0.2' is logically equivalent
// 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
Expand All @@ -114,7 +203,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)
Expand Down Expand Up @@ -175,6 +266,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)
}
Expand Down
34 changes: 34 additions & 0 deletions constraint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,41 @@ func TestConstraintPrerelease(t *testing.T) {
}
}
}
func TestConstraintIsPartOfSets(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.IsPartOfSets(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
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1 +1 @@
module github.com/hashicorp/go-version
module github.com/edwin-Marrima/go-version