From 7213ea6bc227dbc27cf12c49b904adaa9e77825b Mon Sep 17 00:00:00 2001 From: Mike Farah Date: Thu, 17 Mar 2022 14:08:08 +1100 Subject: [PATCH] Comparison op WIP --- pkg/yqlib/compare_operators.go | 101 ++++++++++++ pkg/yqlib/compare_operators_test.go | 236 ++++++++++++++++++++++++++++ pkg/yqlib/doc/operators/compare.md | 115 ++++++++++++++ pkg/yqlib/expression_tokeniser.go | 7 + pkg/yqlib/lib.go | 4 +- 5 files changed, 462 insertions(+), 1 deletion(-) create mode 100644 pkg/yqlib/compare_operators.go create mode 100644 pkg/yqlib/compare_operators_test.go create mode 100644 pkg/yqlib/doc/operators/compare.md diff --git a/pkg/yqlib/compare_operators.go b/pkg/yqlib/compare_operators.go new file mode 100644 index 00000000..1d7e4804 --- /dev/null +++ b/pkg/yqlib/compare_operators.go @@ -0,0 +1,101 @@ +package yqlib + +import ( + "fmt" + "strconv" + + yaml "gopkg.in/yaml.v3" +) + +type compareTypePref struct { + OrEqual bool + Greater bool +} + +func compareOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + log.Debugf("-- compareOperator") + prefs := expressionNode.Operation.Preferences.(compareTypePref) + return crossFunction(d, context.ReadOnlyClone(), expressionNode, compare(prefs), true) +} + +func compare(prefs compareTypePref) func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { + return func(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { + log.Debugf("-- compare cross function") + if lhs == nil && rhs == nil { + owner := &CandidateNode{} + return createBooleanCandidate(owner, prefs.OrEqual), nil + } else if lhs == nil { + log.Debugf("lhs nil, but rhs is not") + return createBooleanCandidate(rhs, false), nil + } else if rhs == nil { + log.Debugf("rhs nil, but rhs is not") + return createBooleanCandidate(lhs, false), nil + } + + lhs.Node = unwrapDoc(lhs.Node) + rhs.Node = unwrapDoc(rhs.Node) + + switch lhs.Node.Kind { + case yaml.MappingNode: + return nil, fmt.Errorf("maps not yet supported for comparison") + case yaml.SequenceNode: + return nil, fmt.Errorf("arrays not yet supported for comparison") + default: + if rhs.Node.Kind != yaml.ScalarNode { + return nil, fmt.Errorf("%v (%v) cannot be subtracted from %v", rhs.Node.Tag, rhs.Path, lhs.Node.Tag) + } + target := lhs.CreateReplacement(&yaml.Node{}) + boolV, err := compareScalars(context, prefs, lhs.Node, rhs.Node) + + return createBooleanCandidate(target, boolV), err + } + } +} + +func compareScalars(context Context, prefs compareTypePref, lhs *yaml.Node, rhs *yaml.Node) (bool, error) { + lhsTag := guessTagFromCustomType(lhs) + rhsTag := guessTagFromCustomType(rhs) + + // isDateTime := lhs.Tag == "!!timestamp" + // // if the lhs is a string, it might be a timestamp in a custom format. + // if lhsTag == "!!str" && context.GetDateTimeLayout() != time.RFC3339 { + // _, err := time.Parse(context.GetDateTimeLayout(), lhs.Value) + // isDateTime = err == nil + // } + + if lhsTag == "!!int" && rhsTag == "!!int" { + _, lhsNum, err := parseInt(lhs.Value) + if err != nil { + return false, err + } + _, rhsNum, err := parseInt(rhs.Value) + if err != nil { + return false, err + } + + if prefs.OrEqual && lhsNum == rhsNum { + return true, nil + } + if prefs.Greater { + return lhsNum > rhsNum, nil + } + return lhsNum < rhsNum, nil + } else if (lhsTag == "!!int" || lhsTag == "!!float") && (rhsTag == "!!int" || rhsTag == "!!float") { + lhsNum, err := strconv.ParseFloat(lhs.Value, 64) + if err != nil { + return false, err + } + rhsNum, err := strconv.ParseFloat(rhs.Value, 64) + if err != nil { + return false, err + } + if prefs.OrEqual && lhsNum == rhsNum { + return true, nil + } + if prefs.Greater { + return lhsNum > rhsNum, nil + } + return lhsNum < rhsNum, nil + } + return false, fmt.Errorf("not yet supported") +} diff --git a/pkg/yqlib/compare_operators_test.go b/pkg/yqlib/compare_operators_test.go new file mode 100644 index 00000000..ff8b8040 --- /dev/null +++ b/pkg/yqlib/compare_operators_test.go @@ -0,0 +1,236 @@ +package yqlib + +import ( + "testing" +) + +var compareOperatorScenarios = []expressionScenario{ + // both null + { + description: "Both sides are null: > is false", + expression: ".a > .b", + expected: []string{ + "D0, P[], (!!bool)::false\n", + }, + }, + { + skipDoc: true, + expression: ".a < .b", + expected: []string{ + "D0, P[], (!!bool)::false\n", + }, + }, + { + description: "Both sides are null: >= is true", + expression: ".a >= .b", + expected: []string{ + "D0, P[], (!!bool)::true\n", + }, + }, + { + skipDoc: true, + expression: ".a <= .b", + expected: []string{ + "D0, P[], (!!bool)::true\n", + }, + }, + + // one null + { + description: "One side is null: > is false", + document: `a: 5`, + expression: ".a > .b", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + { + skipDoc: true, + document: `a: 5`, + expression: ".a < .b", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + { + description: "One side is null: >= is false", + document: `a: 5`, + expression: ".a >= .b", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + { + skipDoc: true, + document: `a: 5`, + expression: ".a <= .b", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + { + skipDoc: true, + document: `a: 5`, + expression: ".b <= .a", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + { + skipDoc: true, + document: `a: 5`, + expression: ".b < .a", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + + // ints, not equal + { + description: "Compare integers (>)", + document: "a: 5\nb: 4", + expression: ".a > .b", + expected: []string{ + "D0, P[a], (!!bool)::true\n", + }, + }, + { + skipDoc: true, + document: "a: 5\nb: 4", + expression: ".a < .b", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + { + description: "Compare integers (>=)", + document: "a: 5\nb: 4", + expression: ".a >= .b", + expected: []string{ + "D0, P[a], (!!bool)::true\n", + }, + }, + { + skipDoc: true, + document: "a: 5\nb: 4", + expression: ".a <= .b", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + + // ints, equal + { + description: "Compare equal numbers", + document: "a: 5\nb: 5", + expression: ".a > .b", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + { + skipDoc: true, + document: "a: 5\nb: 5", + expression: ".a < .b", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + { + description: "Compare equal numbers (>=)", + document: "a: 5\nb: 5", + expression: ".a >= .b", + expected: []string{ + "D0, P[a], (!!bool)::true\n", + }, + }, + { + skipDoc: true, + document: "a: 5\nb: 5", + expression: ".a <= .b", + expected: []string{ + "D0, P[a], (!!bool)::true\n", + }, + }, + + // floats, not equal + { + skipDoc: true, + document: "a: 5.2\nb: 4.1", + expression: ".a > .b", + expected: []string{ + "D0, P[a], (!!bool)::true\n", + }, + }, + { + skipDoc: true, + document: "a: 5.2\nb: 4.1", + expression: ".a < .b", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + { + skipDoc: true, + document: "a: 5.2\nb: 4.1", + expression: ".a >= .b", + expected: []string{ + "D0, P[a], (!!bool)::true\n", + }, + }, + { + skipDoc: true, + document: "a: 5.5\nb: 4.1", + expression: ".a <= .b", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + + // floats, equal + { + skipDoc: true, + document: "a: 5.5\nb: 5.5", + expression: ".a > .b", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + { + skipDoc: true, + document: "a: 5.5\nb: 5.5", + expression: ".a < .b", + expected: []string{ + "D0, P[a], (!!bool)::false\n", + }, + }, + { + skipDoc: true, + document: "a: 5.1\nb: 5.1", + expression: ".a >= .b", + expected: []string{ + "D0, P[a], (!!bool)::true\n", + }, + }, + { + skipDoc: true, + document: "a: 5.1\nb: 5.1", + expression: ".a <= .b", + expected: []string{ + "D0, P[a], (!!bool)::true\n", + }, + }, + + // strings, not equal + // strings, equal + + // datetime, not equal + // datetime, equal +} + +func TestCompareOperatorScenarios(t *testing.T) { + for _, tt := range compareOperatorScenarios { + testScenario(t, &tt) + } + documentOperatorScenarios(t, "compare", compareOperatorScenarios) +} diff --git a/pkg/yqlib/doc/operators/compare.md b/pkg/yqlib/doc/operators/compare.md new file mode 100644 index 00000000..24d7d538 --- /dev/null +++ b/pkg/yqlib/doc/operators/compare.md @@ -0,0 +1,115 @@ + +{% hint style="warning" %} +Note that versions prior to 4.18 require the 'eval/e' command to be specified. + +`yq e ` +{% endhint %} + +## Both sides are null: > is false +Running +```bash +yq --null-input '.a > .b' +``` +will output +```yaml +false +``` + +## Both sides are null: >= is true +Running +```bash +yq --null-input '.a >= .b' +``` +will output +```yaml +true +``` + +## One side is null: > is false +Given a sample.yml file of: +```yaml +a: 5 +``` +then +```bash +yq '.a > .b' sample.yml +``` +will output +```yaml +false +``` + +## One side is null: >= is false +Given a sample.yml file of: +```yaml +a: 5 +``` +then +```bash +yq '.a >= .b' sample.yml +``` +will output +```yaml +false +``` + +## Compare integers (>) +Given a sample.yml file of: +```yaml +a: 5 +b: 4 +``` +then +```bash +yq '.a > .b' sample.yml +``` +will output +```yaml +true +``` + +## Compare integers (>=) +Given a sample.yml file of: +```yaml +a: 5 +b: 4 +``` +then +```bash +yq '.a >= .b' sample.yml +``` +will output +```yaml +true +``` + +## Compare equal numbers +Given a sample.yml file of: +```yaml +a: 5 +b: 5 +``` +then +```bash +yq '.a > .b' sample.yml +``` +will output +```yaml +false +``` + +## Compare equal numbers (>=) +Given a sample.yml file of: +```yaml +a: 5 +b: 5 +``` +then +```bash +yq '.a >= .b' sample.yml +``` +will output +```yaml +true +``` + diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go index ded0bd7e..8c62c1c7 100644 --- a/pkg/yqlib/expression_tokeniser.go +++ b/pkg/yqlib/expression_tokeniser.go @@ -509,6 +509,13 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`\s*==\s*`), opToken(equalsOpType)) lexer.Add([]byte(`\s*!=\s*`), opToken(notEqualsOpType)) + + lexer.Add([]byte(`\s*>=\s*`), opTokenWithPrefs(compareOpType, nil, compareTypePref{OrEqual: true, Greater: true})) + lexer.Add([]byte(`\s*>\s*`), opTokenWithPrefs(compareOpType, nil, compareTypePref{OrEqual: false, Greater: true})) + + lexer.Add([]byte(`\s*<=\s*`), opTokenWithPrefs(compareOpType, nil, compareTypePref{OrEqual: true, Greater: false})) + lexer.Add([]byte(`\s*<\s*`), opTokenWithPrefs(compareOpType, nil, compareTypePref{OrEqual: false, Greater: false})) + lexer.Add([]byte(`\s*=\s*`), assignOpToken(false)) lexer.Add([]byte(`del`), opToken(deleteChildOpType)) diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 3c25aafd..097a58c2 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -72,7 +72,9 @@ var subtractOpType = &operationType{Type: "SUBTRACT", NumArgs: 2, Precedence: 42 var alternativeOpType = &operationType{Type: "ALTERNATIVE", NumArgs: 2, Precedence: 42, Handler: alternativeOperator} var equalsOpType = &operationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: equalsOperator} -var notEqualsOpType = &operationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: notEqualsOperator} +var notEqualsOpType = &operationType{Type: "NOT_EQUALS", NumArgs: 2, Precedence: 40, Handler: notEqualsOperator} + +var compareOpType = &operationType{Type: "COMPARE", NumArgs: 2, Precedence: 40, Handler: compareOperator} //createmap needs to be above union, as we use union to build the components of the objects var createMapOpType = &operationType{Type: "CREATE_MAP", NumArgs: 2, Precedence: 15, Handler: createMapOperator}