Added any_c and all_c operators

This commit is contained in:
Mike Farah 2021-05-14 15:01:44 +10:00
parent 8e14b3b393
commit f4392f8658
9 changed files with 127 additions and 71 deletions

View File

@ -4,8 +4,11 @@ The `or` and `and` operators take two parameters and return a boolean result.
`any` will return `true` if there are any `true` values in a array sequence, and `all` will return true if _all_ elements in an array are true. `any` will return `true` if there are any `true` values in a array sequence, and `all` will return true if _all_ elements in an array are true.
`any_c(condition)` and `all_c(condition)` are like `any` and `all` but they take a condition expression that is used against each element to determine if it's `true`. Note: in `jq` you can simply pass a condition to `any` or `all` and it simply works - `yq` isn't that clever..yet
These are most commonly used with the `select` operator to filter particular nodes. These are most commonly used with the `select` operator to filter particular nodes.
## OR example
## `or` example
Running Running
```bash ```bash
yq eval --null-input 'true or false' yq eval --null-input 'true or false'
@ -15,7 +18,7 @@ will output
true true
``` ```
## AND example ## `and` example
Running Running
```bash ```bash
yq eval --null-input 'true and false' yq eval --null-input 'true and false'
@ -47,7 +50,7 @@ will output
b: fly b: fly
``` ```
## ANY returns true if any boolean in a given array is true ## `any` returns true if any boolean in a given array is true
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
- false - false
@ -62,22 +65,7 @@ will output
true true
``` ```
## ANY returns true if any boolean in a given array is true ## `any` returns false for an empty array
Given a sample.yml file of:
```yaml
- false
- true
```
then
```bash
yq eval 'any' sample.yml
```
will output
```yaml
true
```
## ANY returns false for an empty array
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
[] []
@ -91,7 +79,27 @@ will output
false false
``` ```
## ALL returns true if all booleans in a given array are true ## `any_c` returns true if any element in the array is true for the given condition.
Given a sample.yml file of:
```yaml
a:
- rad
- awesome
b:
- meh
- whatever
```
then
```bash
yq eval '.[] |= any_c(. == "awesome")' sample.yml
```
will output
```yaml
a: true
b: false
```
## `all` returns true if all booleans in a given array are true
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
- true - true
@ -106,7 +114,7 @@ will output
true true
``` ```
## ANY returns true for an empty array ## `all` returns true for an empty array
Given a sample.yml file of: Given a sample.yml file of:
```yaml ```yaml
[] []
@ -120,6 +128,26 @@ will output
true true
``` ```
## `all_c` returns true if all elements in the array are true for the given condition.
Given a sample.yml file of:
```yaml
a:
- rad
- awesome
b:
- meh
- 12
```
then
```bash
yq eval '.[] |= all_c(tag == "!!str")' sample.yml
```
will output
```yaml
a: true
b: false
```
## Not true is false ## Not true is false
Running Running
```bash ```bash

View File

@ -4,4 +4,6 @@ The `or` and `and` operators take two parameters and return a boolean result.
`any` will return `true` if there are any `true` values in a array sequence, and `all` will return true if _all_ elements in an array are true. `any` will return `true` if there are any `true` values in a array sequence, and `all` will return true if _all_ elements in an array are true.
`any_c(condition)` and `all_c(condition)` are like `any` and `all` but they take a condition expression that is used against each element to determine if it's `true`. Note: in `jq` you can simply pass a condition to `any` or `all` and it simply works - `yq` isn't that clever..yet
These are most commonly used with the `select` operator to filter particular nodes. These are most commonly used with the `select` operator to filter particular nodes.

View File

@ -277,7 +277,9 @@ func initLexer() (*lex.Lexer, error) {
lexer.Add([]byte(`sub`), opToken(subStringOpType)) lexer.Add([]byte(`sub`), opToken(subStringOpType))
lexer.Add([]byte(`any`), opToken(anyOpType)) lexer.Add([]byte(`any`), opToken(anyOpType))
lexer.Add([]byte(`any_c`), opToken(anyConditionOpType))
lexer.Add([]byte(`all`), opToken(allOpType)) lexer.Add([]byte(`all`), opToken(allOpType))
lexer.Add([]byte(`all_c`), opToken(allConditionOpType))
lexer.Add([]byte(`split`), opToken(splitStringOpType)) lexer.Add([]byte(`split`), opToken(splitStringOpType))
lexer.Add([]byte(`keys`), opToken(keysOpType)) lexer.Add([]byte(`keys`), opToken(keysOpType))

View File

@ -63,6 +63,8 @@ var collectOpType = &operationType{Type: "COLLECT", NumArgs: 0, Precedence: 50,
var anyOpType = &operationType{Type: "ANY", NumArgs: 0, Precedence: 50, Handler: anyOperator} var anyOpType = &operationType{Type: "ANY", NumArgs: 0, Precedence: 50, Handler: anyOperator}
var allOpType = &operationType{Type: "ALL", NumArgs: 0, Precedence: 50, Handler: allOperator} var allOpType = &operationType{Type: "ALL", NumArgs: 0, Precedence: 50, Handler: allOperator}
var anyConditionOpType = &operationType{Type: "ANY_CONDITION", NumArgs: 1, Precedence: 50, Handler: anyOperator}
var allConditionOpType = &operationType{Type: "ALL_CONDITION", NumArgs: 1, Precedence: 50, Handler: allOperator}
var toEntriesOpType = &operationType{Type: "TO_ENTRIES", NumArgs: 0, Precedence: 50, Handler: toEntriesOperator} var toEntriesOpType = &operationType{Type: "TO_ENTRIES", NumArgs: 0, Precedence: 50, Handler: toEntriesOperator}
var fromEntriesOpType = &operationType{Type: "FROM_ENTRIES", NumArgs: 0, Precedence: 50, Handler: fromEntriesOperator} var fromEntriesOpType = &operationType{Type: "FROM_ENTRIES", NumArgs: 0, Precedence: 50, Handler: fromEntriesOperator}

View File

@ -3,10 +3,11 @@ package yqlib
import ( import (
"container/list" "container/list"
"fmt" "fmt"
yaml "gopkg.in/yaml.v3" yaml "gopkg.in/yaml.v3"
) )
func isTruthyNode(node * yaml.Node) (bool, error) { func isTruthyNode(node *yaml.Node) (bool, error) {
value := true value := true
if node.Tag == "!!null" { if node.Tag == "!!null" {
return false, nil return false, nil
@ -64,9 +65,24 @@ func performBoolOp(op boolOp) func(d *dataTreeNavigator, context Context, lhs *C
} }
} }
func findBoolean(wantBool bool, sequenceNode * yaml.Node) (bool, error) { func findBoolean(wantBool bool, d *dataTreeNavigator, context Context, expressionNode *ExpressionNode, sequenceNode *yaml.Node) (bool, error) {
for _, node := range sequenceNode.Content { for _, node := range sequenceNode.Content {
if expressionNode != nil {
//need to evaluate the expression against the node
candidate := &CandidateNode{Node: node}
rhs, err := d.GetMatchingNodes(context.SingleChildContext(candidate), expressionNode)
if err != nil {
return false, err
}
if rhs.MatchingNodes.Len() > 0 {
node = rhs.MatchingNodes.Front().Value.(*CandidateNode).Node
} else {
// no results found, ignore this entry
continue
}
}
truthy, err := isTruthyNode(node) truthy, err := isTruthyNode(node)
if err != nil { if err != nil {
return false, err return false, err
@ -87,7 +103,7 @@ func allOperator(d *dataTreeNavigator, context Context, expressionNode *Expressi
if candidateNode.Kind != yaml.SequenceNode { if candidateNode.Kind != yaml.SequenceNode {
return Context{}, fmt.Errorf("any only supports arrays, was %v", candidateNode.Tag) return Context{}, fmt.Errorf("any only supports arrays, was %v", candidateNode.Tag)
} }
booleanResult, err := findBoolean(false, candidateNode) booleanResult, err := findBoolean(false, d, context, expressionNode.Rhs, candidateNode)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }
@ -106,7 +122,7 @@ func anyOperator(d *dataTreeNavigator, context Context, expressionNode *Expressi
if candidateNode.Kind != yaml.SequenceNode { if candidateNode.Kind != yaml.SequenceNode {
return Context{}, fmt.Errorf("any only supports arrays, was %v", candidateNode.Tag) return Context{}, fmt.Errorf("any only supports arrays, was %v", candidateNode.Tag)
} }
booleanResult, err := findBoolean(true, candidateNode) booleanResult, err := findBoolean(true, d, context, expressionNode.Rhs, candidateNode)
if err != nil { if err != nil {
return Context{}, err return Context{}, err
} }

View File

@ -6,7 +6,7 @@ import (
var booleanOperatorScenarios = []expressionScenario{ var booleanOperatorScenarios = []expressionScenario{
{ {
description: "OR example", description: "`or` example",
expression: `true or false`, expression: `true or false`,
expected: []string{ expected: []string{
"D0, P[], (!!bool)::true\n", "D0, P[], (!!bool)::true\n",
@ -29,7 +29,7 @@ var booleanOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
description: "AND example", description: "`and` example",
expression: `true and false`, expression: `true and false`,
expected: []string{ expected: []string{
"D0, P[], (!!bool)::false\n", "D0, P[], (!!bool)::false\n",
@ -44,7 +44,7 @@ var booleanOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
description: "ANY returns true if any boolean in a given array is true", description: "`any` returns true if any boolean in a given array is true",
document: `[false, true]`, document: `[false, true]`,
expression: "any", expression: "any",
expected: []string{ expected: []string{
@ -52,21 +52,21 @@ var booleanOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
description: "ANY returns true if any boolean in a given array is true", description: "`any` returns false for an empty array",
document: `[false, true]`,
expression: "any",
expected: []string{
"D0, P[], (!!bool)::true\n",
},
},
{
description: "ANY returns false for an empty array",
document: `[]`, document: `[]`,
expression: "any", expression: "any",
expected: []string{ expected: []string{
"D0, P[], (!!bool)::false\n", "D0, P[], (!!bool)::false\n",
}, },
}, },
{
description: "`any_c` returns true if any element in the array is true for the given condition.",
document: "a: [rad, awesome]\nb: [meh, whatever]",
expression: `.[] |= any_c(. == "awesome")`,
expected: []string{
"D0, P[], (doc)::a: true\nb: false\n",
},
},
{ {
skipDoc: true, skipDoc: true,
document: `[false, false]`, document: `[false, false]`,
@ -76,7 +76,7 @@ var booleanOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
description: "ALL returns true if all booleans in a given array are true", description: "`all` returns true if all booleans in a given array are true",
document: `[true, true]`, document: `[true, true]`,
expression: "all", expression: "all",
expected: []string{ expected: []string{
@ -92,13 +92,21 @@ var booleanOperatorScenarios = []expressionScenario{
}, },
}, },
{ {
description: "ANY returns true for an empty array", description: "`all` returns true for an empty array",
document: `[]`, document: `[]`,
expression: "all", expression: "all",
expected: []string{ expected: []string{
"D0, P[], (!!bool)::true\n", "D0, P[], (!!bool)::true\n",
}, },
}, },
{
description: "`all_c` returns true if all elements in the array are true for the given condition.",
document: "a: [rad, awesome]\nb: [meh, 12]",
expression: `.[] |= all_c(tag == "!!str")`,
expected: []string{
"D0, P[], (doc)::a: true\nb: false\n",
},
},
{ {
skipDoc: true, skipDoc: true,
expression: `false or false`, expression: `false or false`,

View File

@ -25,8 +25,7 @@ var entriesOperatorScenarios = []expressionScenario{
description: "to_entries null", description: "to_entries null",
document: `null`, document: `null`,
expression: `to_entries`, expression: `to_entries`,
expected: []string{ expected: []string{},
},
}, },
{ {
description: "from_entries map", description: "from_entries map",

View File

@ -1,10 +1,11 @@
package yqlib package yqlib
import ( import (
"github.com/elliotchance/orderedmap"
"container/list" "container/list"
yaml "gopkg.in/yaml.v3"
"fmt" "fmt"
"github.com/elliotchance/orderedmap"
yaml "gopkg.in/yaml.v3"
) )
func unique(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { func unique(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
@ -19,7 +20,6 @@ func uniqueBy(d *dataTreeNavigator, context Context, expressionNode *ExpressionN
log.Debugf("-- uniqueBy Operator") log.Debugf("-- uniqueBy Operator")
var results = list.New() var results = list.New()
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() { for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode) candidate := el.Value.(*CandidateNode)
candidateNode := unwrapDoc(candidate.Node) candidateNode := unwrapDoc(candidate.Node)

View File

@ -39,7 +39,6 @@ var uniqueOperatorScenarios = []expressionScenario{
"D0, P[], (!!seq)::- {name: harry, pet: cat}\n- {name: billy, pet: dog}\n", "D0, P[], (!!seq)::- {name: harry, pet: cat}\n- {name: billy, pet: dog}\n",
}, },
}, },
} }
func TestUniqueOperatorScenarios(t *testing.T) { func TestUniqueOperatorScenarios(t *testing.T) {