Added modulo operator

This commit is contained in:
TJ Miller 2023-03-09 14:51:23 -08:00
parent 880298de68
commit 0715486576
5 changed files with 243 additions and 0 deletions

View File

@ -0,0 +1,37 @@
## 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
```

View File

@ -212,6 +212,8 @@ var participleYqRules = []*participleYqRule{
{"Divide", `\/`, opToken(divideOpType), 0}, {"Divide", `\/`, opToken(divideOpType), 0},
{"Modulo", `%`, opToken(moduloOpType), 0},
{"AddAssign", `\+=`, opToken(addAssignOpType), 0}, {"AddAssign", `\+=`, opToken(addAssignOpType), 0},
{"Add", `\+`, opToken(addOpType), 0}, {"Add", `\+`, opToken(addOpType), 0},

View File

@ -64,6 +64,8 @@ var multiplyAssignOpType = &operationType{Type: "MULTIPLY_ASSIGN", NumArgs: 2, P
var divideOpType = &operationType{Type: "DIVIDE", NumArgs: 2, Precedence: 42, Handler: divideOperator} 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 addOpType = &operationType{Type: "ADD", NumArgs: 2, Precedence: 42, Handler: addOperator}
var subtractOpType = &operationType{Type: "SUBTRACT", NumArgs: 2, Precedence: 42, Handler: subtractOperator} var subtractOpType = &operationType{Type: "SUBTRACT", NumArgs: 2, Precedence: 42, Handler: subtractOperator}
var alternativeOpType = &operationType{Type: "ALTERNATIVE", NumArgs: 2, Precedence: 42, Handler: alternativeOperator} var alternativeOpType = &operationType{Type: "ALTERNATIVE", NumArgs: 2, Precedence: 42, Handler: alternativeOperator}

View File

@ -0,0 +1,98 @@
package yqlib
import (
"fmt"
"math"
"strconv"
"strings"
yaml "gopkg.in/yaml.v3"
)
func createModuloOp(lhs *ExpressionNode, rhs *ExpressionNode) *ExpressionNode {
return &ExpressionNode{Operation: &Operation{OperationType: moduloOpType},
LHS: lhs,
RHS: rhs}
}
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 := lhs.CreateReplacement(&yaml.Node{
Anchor: lhs.Node.Anchor,
})
if lhsNode.Kind == yaml.ScalarNode && rhs.Node.Kind == yaml.ScalarNode {
if err := moduloScalars(context, 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 target, nil
}
func moduloScalars(context Context, target *CandidateNode, 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.Node.Kind = yaml.ScalarNode
target.Node.Style = lhs.Style
format, lhsNum, err := parseInt64(lhs.Value)
if err != nil {
return err
}
_, rhsNum, err := parseInt64(rhs.Value)
if err != nil {
return err
}
remainder := lhsNum % rhsNum
target.Node.Tag = lhs.Tag
target.Node.Value = fmt.Sprintf(format, remainder)
} else if (lhsTag == "!!int" || lhsTag == "!!float") && (rhsTag == "!!int" || rhsTag == "!!float") {
target.Node.Kind = yaml.ScalarNode
target.Node.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.Node.Tag = lhs.Tag
} else {
target.Node.Tag = "!!float"
}
target.Node.Value = fmt.Sprintf("%v", remainder)
} else {
return fmt.Errorf("%v cannot modulo by %v", lhsTag, rhsTag)
}
return nil
}

View File

@ -0,0 +1,104 @@
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",
},
},
{
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)
}