mirror of
https://github.com/mikefarah/yq.git
synced 2025-01-30 02:55:40 +00:00
Comparison op WIP
This commit is contained in:
parent
897604142f
commit
7213ea6bc2
101
pkg/yqlib/compare_operators.go
Normal file
101
pkg/yqlib/compare_operators.go
Normal file
@ -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")
|
||||||
|
}
|
236
pkg/yqlib/compare_operators_test.go
Normal file
236
pkg/yqlib/compare_operators_test.go
Normal file
@ -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)
|
||||||
|
}
|
115
pkg/yqlib/doc/operators/compare.md
Normal file
115
pkg/yqlib/doc/operators/compare.md
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
|
||||||
|
{% hint style="warning" %}
|
||||||
|
Note that versions prior to 4.18 require the 'eval/e' command to be specified. 
|
||||||
|
|
||||||
|
`yq e <exp> <file>`
|
||||||
|
{% 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
|
||||||
|
```
|
||||||
|
|
@ -509,6 +509,13 @@ func initLexer() (*lex.Lexer, error) {
|
|||||||
|
|
||||||
lexer.Add([]byte(`\s*==\s*`), opToken(equalsOpType))
|
lexer.Add([]byte(`\s*==\s*`), opToken(equalsOpType))
|
||||||
lexer.Add([]byte(`\s*!=\s*`), opToken(notEqualsOpType))
|
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(`\s*=\s*`), assignOpToken(false))
|
||||||
|
|
||||||
lexer.Add([]byte(`del`), opToken(deleteChildOpType))
|
lexer.Add([]byte(`del`), opToken(deleteChildOpType))
|
||||||
|
@ -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 alternativeOpType = &operationType{Type: "ALTERNATIVE", NumArgs: 2, Precedence: 42, Handler: alternativeOperator}
|
||||||
|
|
||||||
var equalsOpType = &operationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: equalsOperator}
|
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
|
//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}
|
var createMapOpType = &operationType{Type: "CREATE_MAP", NumArgs: 2, Precedence: 15, Handler: createMapOperator}
|
||||||
|
Loading…
Reference in New Issue
Block a user