mirror of
https://github.com/mikefarah/yq.git
synced 2025-01-12 19:25:37 +00:00
Added divide and modulo operators (#1593)
* Added divide operator (#49) * Tidy up divide operator logic * Added modulo operator * Fix divide test typo * Add divide by zero test * Handle int modulo by 0 and add tests * Tidy up divide/modulo operator node creation * Fix linter errors
This commit is contained in:
parent
360a47fddc
commit
a466821e8f
56
pkg/yqlib/doc/operators/divide.md
Normal file
56
pkg/yqlib/doc/operators/divide.md
Normal file
@ -0,0 +1,56 @@
|
||||
|
||||
## String split
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: cat_meow
|
||||
b: _
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.c = .a / .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: cat_meow
|
||||
b: _
|
||||
c:
|
||||
- cat
|
||||
- meow
|
||||
```
|
||||
|
||||
## Number division
|
||||
The result during division is calculated as a float
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 12
|
||||
b: 2.5
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a = .a / .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 4.8
|
||||
b: 2.5
|
||||
```
|
||||
|
||||
## Number division by zero
|
||||
Dividing by zero results in +Inf or -Inf
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 1
|
||||
b: -1
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a = .a / 0 | .b = .b / 0' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: !!float +Inf
|
||||
b: !!float -Inf
|
||||
```
|
||||
|
72
pkg/yqlib/doc/operators/modulo.md
Normal file
72
pkg/yqlib/doc/operators/modulo.md
Normal file
@ -0,0 +1,72 @@
|
||||
|
||||
## Number modulo - int
|
||||
If the lhs and rhs are ints then the expression will be calculated with ints.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 13
|
||||
b: 2
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a = .a % .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: 1
|
||||
b: 2
|
||||
```
|
||||
|
||||
## Number modulo - float
|
||||
If the lhs or rhs are floats then the expression will be calculated with floats.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 12
|
||||
b: 2.5
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a = .a % .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: !!float 2
|
||||
b: 2.5
|
||||
```
|
||||
|
||||
## Number modulo - int by zero
|
||||
If the lhs is an int and rhs is a 0 the result is an error.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 1
|
||||
b: 0
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a = .a % .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```bash
|
||||
Error: cannot modulo by 0
|
||||
```
|
||||
|
||||
## Number modulo - float by zero
|
||||
If the lhs is a float and rhs is a 0 the result is NaN.
|
||||
|
||||
Given a sample.yml file of:
|
||||
```yaml
|
||||
a: 1.1
|
||||
b: 0
|
||||
```
|
||||
then
|
||||
```bash
|
||||
yq '.a = .a % .b' sample.yml
|
||||
```
|
||||
will output
|
||||
```yaml
|
||||
a: !!float NaN
|
||||
b: 0
|
||||
```
|
||||
|
@ -210,6 +210,10 @@ var participleYqRules = []*participleYqRule{
|
||||
{"MultiplyAssign", `\*=[\+|\?cdn]*`, multiplyWithPrefs(multiplyAssignOpType), 0},
|
||||
{"Multiply", `\*[\+|\?cdn]*`, multiplyWithPrefs(multiplyOpType), 0},
|
||||
|
||||
{"Divide", `\/`, opToken(divideOpType), 0},
|
||||
|
||||
{"Modulo", `%`, opToken(moduloOpType), 0},
|
||||
|
||||
{"AddAssign", `\+=`, opToken(addAssignOpType), 0},
|
||||
{"Add", `\+`, opToken(addOpType), 0},
|
||||
|
||||
|
@ -62,6 +62,10 @@ var assignAliasOpType = &operationType{Type: "ASSIGN_ALIAS", NumArgs: 2, Precede
|
||||
var multiplyOpType = &operationType{Type: "MULTIPLY", NumArgs: 2, Precedence: 42, Handler: multiplyOperator}
|
||||
var multiplyAssignOpType = &operationType{Type: "MULTIPLY_ASSIGN", NumArgs: 2, Precedence: 42, Handler: multiplyAssignOperator}
|
||||
|
||||
var divideOpType = &operationType{Type: "DIVIDE", NumArgs: 2, Precedence: 42, Handler: divideOperator}
|
||||
|
||||
var moduloOpType = &operationType{Type: "MODULO", NumArgs: 2, Precedence: 42, Handler: moduloOperator}
|
||||
|
||||
var addOpType = &operationType{Type: "ADD", NumArgs: 2, Precedence: 42, Handler: addOperator}
|
||||
var subtractOpType = &operationType{Type: "SUBTRACT", NumArgs: 2, Precedence: 42, Handler: subtractOperator}
|
||||
var alternativeOpType = &operationType{Type: "ALTERNATIVE", NumArgs: 2, Precedence: 42, Handler: alternativeOperator}
|
||||
|
78
pkg/yqlib/operator_divide.go
Normal file
78
pkg/yqlib/operator_divide.go
Normal file
@ -0,0 +1,78 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func divideOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
log.Debugf("Divide operator")
|
||||
|
||||
return crossFunction(d, context.ReadOnlyClone(), expressionNode, divide, false)
|
||||
}
|
||||
|
||||
func divide(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
||||
lhs.Node = unwrapDoc(lhs.Node)
|
||||
rhs.Node = unwrapDoc(rhs.Node)
|
||||
|
||||
lhsNode := lhs.Node
|
||||
|
||||
if lhsNode.Tag == "!!null" {
|
||||
return nil, fmt.Errorf("%v (%v) cannot be divided by %v (%v)", lhsNode.Tag, lhs.GetNicePath(), rhs.Node.Tag, rhs.GetNicePath())
|
||||
}
|
||||
|
||||
target := &yaml.Node{}
|
||||
|
||||
if lhsNode.Kind == yaml.ScalarNode && rhs.Node.Kind == yaml.ScalarNode {
|
||||
if err := divideScalars(target, lhsNode, rhs.Node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("%v (%v) cannot be divided by %v (%v)", lhsNode.Tag, lhs.GetNicePath(), rhs.Node.Tag, rhs.GetNicePath())
|
||||
}
|
||||
|
||||
return lhs.CreateReplacement(target), nil
|
||||
}
|
||||
|
||||
func divideScalars(target *yaml.Node, lhs *yaml.Node, rhs *yaml.Node) error {
|
||||
lhsTag := lhs.Tag
|
||||
rhsTag := guessTagFromCustomType(rhs)
|
||||
lhsIsCustom := false
|
||||
if !strings.HasPrefix(lhsTag, "!!") {
|
||||
// custom tag - we have to have a guess
|
||||
lhsTag = guessTagFromCustomType(lhs)
|
||||
lhsIsCustom = true
|
||||
}
|
||||
|
||||
if lhsTag == "!!str" && rhsTag == "!!str" {
|
||||
res := split(lhs.Value, rhs.Value)
|
||||
target.Kind = res.Kind
|
||||
target.Tag = res.Tag
|
||||
target.Content = res.Content
|
||||
} else if (lhsTag == "!!int" || lhsTag == "!!float") && (rhsTag == "!!int" || rhsTag == "!!float") {
|
||||
target.Kind = yaml.ScalarNode
|
||||
target.Style = lhs.Style
|
||||
|
||||
lhsNum, err := strconv.ParseFloat(lhs.Value, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rhsNum, err := strconv.ParseFloat(rhs.Value, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
quotient := lhsNum / rhsNum
|
||||
if lhsIsCustom {
|
||||
target.Tag = lhs.Tag
|
||||
} else {
|
||||
target.Tag = "!!float"
|
||||
}
|
||||
target.Value = fmt.Sprintf("%v", quotient)
|
||||
} else {
|
||||
return fmt.Errorf("%v cannot be divided by %v", lhsTag, rhsTag)
|
||||
}
|
||||
return nil
|
||||
}
|
130
pkg/yqlib/operator_divide_test.go
Normal file
130
pkg/yqlib/operator_divide_test.go
Normal file
@ -0,0 +1,130 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var divideOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
skipDoc: true,
|
||||
document: `[{a: foo_bar, b: _}, {a: 4, b: 2}]`,
|
||||
expression: ".[] | .a / .b",
|
||||
expected: []string{
|
||||
"D0, P[0 a], (!!seq)::- foo\n- bar\n",
|
||||
"D0, P[1 a], (!!float)::2\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: `{}`,
|
||||
expression: "(.a / .b) as $x | .",
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "String split",
|
||||
document: `{a: cat_meow, b: _}`,
|
||||
expression: `.c = .a / .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{a: cat_meow, b: _, c: [cat, meow]}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Number division",
|
||||
subdescription: "The result during division is calculated as a float",
|
||||
document: `{a: 12, b: 2.5}`,
|
||||
expression: `.a = .a / .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{a: 4.8, b: 2.5}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Number division by zero",
|
||||
subdescription: "Dividing by zero results in +Inf or -Inf",
|
||||
document: `{a: 1, b: -1}`,
|
||||
expression: `.a = .a / 0 | .b = .b / 0`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{a: !!float +Inf, b: !!float -Inf}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Custom types: that are really strings",
|
||||
document: "a: !horse cat_meow\nb: !goat _",
|
||||
expression: `.a = .a / .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: !horse\n - cat\n - meow\nb: !goat _\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Custom types: that are really numbers",
|
||||
document: "a: !horse 1.2\nb: !goat 2.3",
|
||||
expression: `.a = .a / .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: !horse 0.5217391304347826\nb: !goat 2.3\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: "a: 2\nb: !goat 2.3",
|
||||
expression: `.a = .a / .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: 0.8695652173913044\nb: !goat 2.3\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Custom types: that are really ints",
|
||||
document: "a: !horse 2\nb: !goat 3",
|
||||
expression: `.a = .a / .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: !horse 0.6666666666666666\nb: !goat 3\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Keep anchors",
|
||||
document: "a: &horse [1]",
|
||||
expression: `.a[1] = .a[0] / 2`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: &horse [1, 0.5]\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Divide int by string",
|
||||
document: "a: 123\nb: '2'",
|
||||
expression: `.a / .b`,
|
||||
expectedError: "!!int cannot be divided by !!str",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Divide string by int",
|
||||
document: "a: 2\nb: '123'",
|
||||
expression: `.b / .a`,
|
||||
expectedError: "!!str cannot be divided by !!int",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Divide map by int",
|
||||
document: "a: {\"a\":1}\nb: 2",
|
||||
expression: `.a / .b`,
|
||||
expectedError: "!!map (a) cannot be divided by !!int (b)",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Divide array by str",
|
||||
document: "a: [1,2]\nb: '2'",
|
||||
expression: `.a / .b`,
|
||||
expectedError: "!!seq (a) cannot be divided by !!str (b)",
|
||||
},
|
||||
}
|
||||
|
||||
func TestDivideOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range divideOperatorScenarios {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "divide", divideOperatorScenarios)
|
||||
}
|
93
pkg/yqlib/operator_modulo.go
Normal file
93
pkg/yqlib/operator_modulo.go
Normal file
@ -0,0 +1,93 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func moduloOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) {
|
||||
log.Debugf("Modulo operator")
|
||||
|
||||
return crossFunction(d, context.ReadOnlyClone(), expressionNode, modulo, false)
|
||||
}
|
||||
|
||||
func modulo(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) {
|
||||
lhs.Node = unwrapDoc(lhs.Node)
|
||||
rhs.Node = unwrapDoc(rhs.Node)
|
||||
|
||||
lhsNode := lhs.Node
|
||||
|
||||
if lhsNode.Tag == "!!null" {
|
||||
return nil, fmt.Errorf("%v (%v) cannot modulo by %v (%v)", lhsNode.Tag, lhs.GetNicePath(), rhs.Node.Tag, rhs.GetNicePath())
|
||||
}
|
||||
|
||||
target := &yaml.Node{}
|
||||
|
||||
if lhsNode.Kind == yaml.ScalarNode && rhs.Node.Kind == yaml.ScalarNode {
|
||||
if err := moduloScalars(target, lhsNode, rhs.Node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, fmt.Errorf("%v (%v) cannot modulo by %v (%v)", lhsNode.Tag, lhs.GetNicePath(), rhs.Node.Tag, rhs.GetNicePath())
|
||||
}
|
||||
|
||||
return lhs.CreateReplacement(target), nil
|
||||
}
|
||||
|
||||
func moduloScalars(target *yaml.Node, lhs *yaml.Node, rhs *yaml.Node) error {
|
||||
lhsTag := lhs.Tag
|
||||
rhsTag := guessTagFromCustomType(rhs)
|
||||
lhsIsCustom := false
|
||||
if !strings.HasPrefix(lhsTag, "!!") {
|
||||
// custom tag - we have to have a guess
|
||||
lhsTag = guessTagFromCustomType(lhs)
|
||||
lhsIsCustom = true
|
||||
}
|
||||
|
||||
if lhsTag == "!!int" && rhsTag == "!!int" {
|
||||
target.Kind = yaml.ScalarNode
|
||||
target.Style = lhs.Style
|
||||
|
||||
format, lhsNum, err := parseInt64(lhs.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, rhsNum, err := parseInt64(rhs.Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rhsNum == 0 {
|
||||
return fmt.Errorf("cannot modulo by 0")
|
||||
}
|
||||
remainder := lhsNum % rhsNum
|
||||
|
||||
target.Tag = lhs.Tag
|
||||
target.Value = fmt.Sprintf(format, remainder)
|
||||
} else if (lhsTag == "!!int" || lhsTag == "!!float") && (rhsTag == "!!int" || rhsTag == "!!float") {
|
||||
target.Kind = yaml.ScalarNode
|
||||
target.Style = lhs.Style
|
||||
|
||||
lhsNum, err := strconv.ParseFloat(lhs.Value, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rhsNum, err := strconv.ParseFloat(rhs.Value, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
remainder := math.Mod(lhsNum, rhsNum)
|
||||
if lhsIsCustom {
|
||||
target.Tag = lhs.Tag
|
||||
} else {
|
||||
target.Tag = "!!float"
|
||||
}
|
||||
target.Value = fmt.Sprintf("%v", remainder)
|
||||
} else {
|
||||
return fmt.Errorf("%v cannot modulo by %v", lhsTag, rhsTag)
|
||||
}
|
||||
return nil
|
||||
}
|
120
pkg/yqlib/operator_modulo_test.go
Normal file
120
pkg/yqlib/operator_modulo_test.go
Normal file
@ -0,0 +1,120 @@
|
||||
package yqlib
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var moduloOperatorScenarios = []expressionScenario{
|
||||
{
|
||||
skipDoc: true,
|
||||
document: `[{a: 2.5, b: 2}, {a: 2, b: 0.75}]`,
|
||||
expression: ".[] | .a % .b",
|
||||
expected: []string{
|
||||
"D0, P[0 a], (!!float)::0.5\n",
|
||||
"D0, P[1 a], (!!float)::0.5\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: `{}`,
|
||||
expression: "(.a / .b) as $x | .",
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Number modulo - int",
|
||||
subdescription: "If the lhs and rhs are ints then the expression will be calculated with ints.",
|
||||
document: `{a: 13, b: 2}`,
|
||||
expression: `.a = .a % .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{a: 1, b: 2}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Number modulo - float",
|
||||
subdescription: "If the lhs or rhs are floats then the expression will be calculated with floats.",
|
||||
document: `{a: 12, b: 2.5}`,
|
||||
expression: `.a = .a % .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{a: !!float 2, b: 2.5}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
description: "Number modulo - int by zero",
|
||||
subdescription: "If the lhs is an int and rhs is a 0 the result is an error.",
|
||||
document: `{a: 1, b: 0}`,
|
||||
expression: `.a = .a % .b`,
|
||||
expectedError: "cannot modulo by 0",
|
||||
},
|
||||
{
|
||||
description: "Number modulo - float by zero",
|
||||
subdescription: "If the lhs is a float and rhs is a 0 the result is NaN.",
|
||||
document: `{a: 1.1, b: 0}`,
|
||||
expression: `.a = .a % .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::{a: !!float NaN, b: 0}\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Custom types: that are really numbers",
|
||||
document: "a: !horse 333.975\nb: !goat 299.2",
|
||||
expression: `.a = .a % .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: !horse 34.775000000000034\nb: !goat 299.2\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
document: "a: 2\nb: !goat 2.3",
|
||||
expression: `.a = .a % .b`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: !!float 2\nb: !goat 2.3\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Keep anchors",
|
||||
document: "a: &horse [1]",
|
||||
expression: `.a[1] = .a[0] % 2`,
|
||||
expected: []string{
|
||||
"D0, P[], (doc)::a: &horse [1, 1]\n",
|
||||
},
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Modulo int by string",
|
||||
document: "a: 123\nb: '2'",
|
||||
expression: `.a % .b`,
|
||||
expectedError: "!!int cannot modulo by !!str",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Modulo string by int",
|
||||
document: "a: 2\nb: '123'",
|
||||
expression: `.b % .a`,
|
||||
expectedError: "!!str cannot modulo by !!int",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Modulo map by int",
|
||||
document: "a: {\"a\":1}\nb: 2",
|
||||
expression: `.a % .b`,
|
||||
expectedError: "!!map (a) cannot modulo by !!int (b)",
|
||||
},
|
||||
{
|
||||
skipDoc: true,
|
||||
description: "Modulo array by str",
|
||||
document: "a: [1,2]\nb: '2'",
|
||||
expression: `.a % .b`,
|
||||
expectedError: "!!seq (a) cannot modulo by !!str (b)",
|
||||
},
|
||||
}
|
||||
|
||||
func TestModuloOperatorScenarios(t *testing.T) {
|
||||
for _, tt := range moduloOperatorScenarios {
|
||||
testScenario(t, &tt)
|
||||
}
|
||||
documentOperatorScenarios(t, "modulo", moduloOperatorScenarios)
|
||||
}
|
Loading…
Reference in New Issue
Block a user