From d4786212ea9d743f1861f0a3a379f023dbd95299 Mon Sep 17 00:00:00 2001 From: Matt Benson Date: Wed, 27 Mar 2024 15:51:46 -0500 Subject: [PATCH 1/2] min/max operators --- 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 +++++++++++++++++++++++++++++ 6 files changed, 203 insertions(+) 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/max.md b/pkg/yqlib/doc/operators/max.md new file mode 100644 index 00000000000..cbdddb6203f --- /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 00000000000..8455ae10e0f --- /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 2f65bddc63e..3c5f65a8930 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 df89247c6e6..b202be1dfa8 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 6e8a2e12e1c..44febf224c9 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 cc7d5fb8b54..ff5f0013ef3 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) +} From 6235d564a06e7a6fb41da678fdb73252c91c364f Mon Sep 17 00:00:00 2001 From: Matt Benson Date: Wed, 27 Mar 2024 17:45:04 -0500 Subject: [PATCH 2/2] min, max operator headers --- pkg/yqlib/doc/operators/headers/max.md | 3 +++ pkg/yqlib/doc/operators/headers/min.md | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 pkg/yqlib/doc/operators/headers/max.md create mode 100644 pkg/yqlib/doc/operators/headers/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 00000000000..1657dd1b02e --- /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 00000000000..785cac7a123 --- /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.