Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
8 changed files
with
557 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |
Oops, something went wrong.