From 101cf14b8cff36ff17c6ba52d71dced6cf12d6f5 Mon Sep 17 00:00:00 2001 From: Matt Benson Date: Fri, 29 Mar 2024 21:34:36 -0500 Subject: [PATCH] min/max operators (#1992) * min/max operators * min, max operator headers --- pkg/yqlib/doc/operators/headers/max.md | 3 ++ pkg/yqlib/doc/operators/headers/min.md | 3 ++ pkg/yqlib/doc/operators/max.md | 48 +++++++++++++++++++ pkg/yqlib/doc/operators/min.md | 48 +++++++++++++++++++ pkg/yqlib/lexer_participle.go | 3 ++ pkg/yqlib/operation.go | 2 + pkg/yqlib/operator_compare.go | 38 +++++++++++++++ pkg/yqlib/operators_compare_test.go | 64 ++++++++++++++++++++++++++ 8 files changed, 209 insertions(+) create mode 100644 pkg/yqlib/doc/operators/headers/max.md create mode 100644 pkg/yqlib/doc/operators/headers/min.md create mode 100644 pkg/yqlib/doc/operators/max.md create mode 100644 pkg/yqlib/doc/operators/min.md diff --git a/pkg/yqlib/doc/operators/headers/max.md b/pkg/yqlib/doc/operators/headers/max.md new file mode 100644 index 00000000..1657dd1b --- /dev/null +++ b/pkg/yqlib/doc/operators/headers/max.md @@ -0,0 +1,3 @@ +# Max + +Computes the maximum among an incoming sequence of scalar values. diff --git a/pkg/yqlib/doc/operators/headers/min.md b/pkg/yqlib/doc/operators/headers/min.md new file mode 100644 index 00000000..785cac7a --- /dev/null +++ b/pkg/yqlib/doc/operators/headers/min.md @@ -0,0 +1,3 @@ +# Min + +Computes the minimum among an incoming sequence of scalar values. diff --git a/pkg/yqlib/doc/operators/max.md b/pkg/yqlib/doc/operators/max.md new file mode 100644 index 00000000..cbdddb62 --- /dev/null +++ b/pkg/yqlib/doc/operators/max.md @@ -0,0 +1,48 @@ + +## Maximum int +Given a sample.yml file of: +```yaml +- 99 +- 16 +- 12 +- 6 +- 66 +``` +then +```bash +yq 'max' sample.yml +``` +will output +```yaml +99 +``` + +## Maximum string +Given a sample.yml file of: +```yaml +- foo +- bar +- baz +``` +then +```bash +yq 'max' sample.yml +``` +will output +```yaml +foo +``` + +## Maximum of empty +Given a sample.yml file of: +```yaml +[] +``` +then +```bash +yq 'max' sample.yml +``` +will output +```yaml +``` + diff --git a/pkg/yqlib/doc/operators/min.md b/pkg/yqlib/doc/operators/min.md new file mode 100644 index 00000000..8455ae10 --- /dev/null +++ b/pkg/yqlib/doc/operators/min.md @@ -0,0 +1,48 @@ + +## Minimum int +Given a sample.yml file of: +```yaml +- 99 +- 16 +- 12 +- 6 +- 66 +``` +then +```bash +yq 'min' sample.yml +``` +will output +```yaml +6 +``` + +## Minimum string +Given a sample.yml file of: +```yaml +- foo +- bar +- baz +``` +then +```bash +yq 'min' sample.yml +``` +will output +```yaml +bar +``` + +## Minimum of empty +Given a sample.yml file of: +```yaml +[] +``` +then +```bash +yq 'min' sample.yml +``` +will output +```yaml +``` + diff --git a/pkg/yqlib/lexer_participle.go b/pkg/yqlib/lexer_participle.go index 4b58d7ce..ef31e778 100644 --- a/pkg/yqlib/lexer_participle.go +++ b/pkg/yqlib/lexer_participle.go @@ -199,6 +199,9 @@ var participleYqRules = []*participleYqRule{ {"GreaterThan", `\s*>\s*`, opTokenWithPrefs(compareOpType, nil, compareTypePref{OrEqual: false, Greater: true}), 0}, {"LessThan", `\s*<\s*`, opTokenWithPrefs(compareOpType, nil, compareTypePref{OrEqual: false, Greater: false}), 0}, + simpleOp("min", minOpType), + simpleOp("max", maxOpType), + {"AssignRelative", `\|=[c]*`, assignOpToken(true), 0}, {"Assign", `=[c]*`, assignOpToken(false), 0}, diff --git a/pkg/yqlib/operation.go b/pkg/yqlib/operation.go index be1f6230..d79ac7a0 100644 --- a/pkg/yqlib/operation.go +++ b/pkg/yqlib/operation.go @@ -73,6 +73,8 @@ var equalsOpType = &operationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Ha var notEqualsOpType = &operationType{Type: "NOT_EQUALS", NumArgs: 2, Precedence: 40, Handler: notEqualsOperator} var compareOpType = &operationType{Type: "COMPARE", NumArgs: 2, Precedence: 40, Handler: compareOperator} +var minOpType = &operationType{Type: "MIN", NumArgs: 0, Precedence: 40, Handler: minOperator} +var maxOpType = &operationType{Type: "MAX", NumArgs: 0, Precedence: 40, Handler: maxOperator} // 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} diff --git a/pkg/yqlib/operator_compare.go b/pkg/yqlib/operator_compare.go index 6e8a2e12..44febf22 100644 --- a/pkg/yqlib/operator_compare.go +++ b/pkg/yqlib/operator_compare.go @@ -1,6 +1,7 @@ package yqlib import ( + "container/list" "fmt" "strconv" ) @@ -129,3 +130,40 @@ func compareScalars(context Context, prefs compareTypePref, lhs *CandidateNode, return false, fmt.Errorf("%v not yet supported for comparison", lhs.Tag) } + +func superlativeByComparison(d *dataTreeNavigator, context Context, prefs compareTypePref) (Context, error) { + fn := compare(prefs) + + var results = list.New() + + for seq := context.MatchingNodes.Front(); seq != nil; seq = seq.Next() { + splatted, err := splat(context.SingleChildContext(seq.Value.(*CandidateNode)), traversePreferences{}) + if err != nil { + return Context{}, err + } + result := splatted.MatchingNodes.Front() + if result != nil { + for el := result.Next(); el != nil; el = el.Next() { + cmp, err := fn(d, context, el.Value.(*CandidateNode), result.Value.(*CandidateNode)) + if err != nil { + return Context{}, err + } + if isTruthyNode(cmp) { + result = el + } + } + results.PushBack(result.Value) + } + } + return context.ChildContext(results), nil +} + +func minOperator(d *dataTreeNavigator, context Context, _ *ExpressionNode) (Context, error) { + log.Debug(("Min")) + return superlativeByComparison(d, context, compareTypePref{Greater: false}) +} + +func maxOperator(d *dataTreeNavigator, context Context, _ *ExpressionNode) (Context, error) { + log.Debug(("Max")) + return superlativeByComparison(d, context, compareTypePref{Greater: true}) +} diff --git a/pkg/yqlib/operators_compare_test.go b/pkg/yqlib/operators_compare_test.go index cc7d5fb8..ff5f0013 100644 --- a/pkg/yqlib/operators_compare_test.go +++ b/pkg/yqlib/operators_compare_test.go @@ -383,3 +383,67 @@ func TestCompareOperatorScenarios(t *testing.T) { } documentOperatorScenarios(t, "compare", compareOperatorScenarios) } + +var minOperatorScenarios = []expressionScenario{ + { + description: "Minimum int", + document: "[99, 16, 12, 6, 66]\n", + expression: `min`, + expected: []string{ + "D0, P[3], (!!int)::6\n", + }, + }, + { + description: "Minimum string", + document: "[foo, bar, baz]\n", + expression: `min`, + expected: []string{ + "D0, P[1], (!!str)::bar\n", + }, + }, + { + description: "Minimum of empty", + document: "[]\n", + expression: `min`, + expected: []string{}, + }, +} + +func TestMinOperatorScenarios(t *testing.T) { + for _, tt := range minOperatorScenarios { + testScenario(t, &tt) + } + documentOperatorScenarios(t, "min", minOperatorScenarios) +} + +var maxOperatorScenarios = []expressionScenario{ + { + description: "Maximum int", + document: "[99, 16, 12, 6, 66]\n", + expression: `max`, + expected: []string{ + "D0, P[0], (!!int)::99\n", + }, + }, + { + description: "Maximum string", + document: "[foo, bar, baz]\n", + expression: `max`, + expected: []string{ + "D0, P[0], (!!str)::foo\n", + }, + }, + { + description: "Maximum of empty", + document: "[]\n", + expression: `max`, + expected: []string{}, + }, +} + +func TestMaxOperatorScenarios(t *testing.T) { + for _, tt := range maxOperatorScenarios { + testScenario(t, &tt) + } + documentOperatorScenarios(t, "max", maxOperatorScenarios) +}