diff --git a/pkg/yqlib/doc/operators/divide.md b/pkg/yqlib/doc/operators/divide.md new file mode 100644 index 00000000..34242728 --- /dev/null +++ b/pkg/yqlib/doc/operators/divide.md @@ -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 +``` + diff --git a/pkg/yqlib/doc/operators/modulo.md b/pkg/yqlib/doc/operators/modulo.md new file mode 100644 index 00000000..b7ecd7e0 --- /dev/null +++ b/pkg/yqlib/doc/operators/modulo.md @@ -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 +``` + diff --git a/pkg/yqlib/lexer_participle.go b/pkg/yqlib/lexer_participle.go index e6d4d58f..18025f44 100644 --- a/pkg/yqlib/lexer_participle.go +++ b/pkg/yqlib/lexer_participle.go @@ -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}, diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index a24d18f2..5fc503ae 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -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} diff --git a/pkg/yqlib/operator_divide.go b/pkg/yqlib/operator_divide.go new file mode 100644 index 00000000..c3d5eb23 --- /dev/null +++ b/pkg/yqlib/operator_divide.go @@ -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 +} diff --git a/pkg/yqlib/operator_divide_test.go b/pkg/yqlib/operator_divide_test.go new file mode 100644 index 00000000..fc7297a3 --- /dev/null +++ b/pkg/yqlib/operator_divide_test.go @@ -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) +} diff --git a/pkg/yqlib/operator_modulo.go b/pkg/yqlib/operator_modulo.go new file mode 100644 index 00000000..ecb9b406 --- /dev/null +++ b/pkg/yqlib/operator_modulo.go @@ -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 +} diff --git a/pkg/yqlib/operator_modulo_test.go b/pkg/yqlib/operator_modulo_test.go new file mode 100644 index 00000000..ee1c9101 --- /dev/null +++ b/pkg/yqlib/operator_modulo_test.go @@ -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) +}