diff --git a/pkg/yqlib/doc/Subtract.md b/pkg/yqlib/doc/Subtract.md new file mode 100644 index 00000000..cf0f9baa --- /dev/null +++ b/pkg/yqlib/doc/Subtract.md @@ -0,0 +1,71 @@ + +## Number subtraction - float +If the lhs or rhs are floats then the expression will be calculated with floats. + +Given a sample.yml file of: +```yaml +a: 3 +b: 4.5 +``` +then +```bash +yq eval '.a = .a - .b' sample.yml +``` +will output +```yaml +a: -1.5 +b: 4.5 +``` + +## Number subtraction - float +If the lhs or rhs are floats then the expression will be calculated with floats. + +Given a sample.yml file of: +```yaml +a: 3 +b: 4.5 +``` +then +```bash +yq eval '.a = .a - .b' sample.yml +``` +will output +```yaml +a: -1.5 +b: 4.5 +``` + +## Number subtraction - int +If both the lhs and rhs are ints then the expression will be calculated with ints. + +Given a sample.yml file of: +```yaml +a: 3 +b: 4 +``` +then +```bash +yq eval '.a = .a - .b' sample.yml +``` +will output +```yaml +a: -1 +b: 4 +``` + +## Decrement numbers +Given a sample.yml file of: +```yaml +a: 3 +b: 5 +``` +then +```bash +yq eval '.[] -= 1' sample.yml +``` +will output +```yaml +a: 2 +b: 4 +``` + diff --git a/pkg/yqlib/expression_tokeniser.go b/pkg/yqlib/expression_tokeniser.go index fb22e6b8..1b2d8afd 100644 --- a/pkg/yqlib/expression_tokeniser.go +++ b/pkg/yqlib/expression_tokeniser.go @@ -325,6 +325,8 @@ func initLexer() (*lex.Lexer, error) { lexer.Add([]byte(`\*[\+|\?d]*`), multiplyWithPrefs()) lexer.Add([]byte(`\+`), opToken(addOpType)) lexer.Add([]byte(`\+=`), opToken(addAssignOpType)) + lexer.Add([]byte(`\-`), opToken(subtractOpType)) + lexer.Add([]byte(`\-=`), opToken(subtractAssignOpType)) lexer.Add([]byte(`\$[a-zA-Z_-0-9]+`), getVariableOpToken()) lexer.Add([]byte(`as`), opToken(assignVariableOpType)) diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index d855eb35..0d05f0a9 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -35,6 +35,7 @@ var pipeOpType = &operationType{Type: "PIPE", NumArgs: 2, Precedence: 30, Handle var assignOpType = &operationType{Type: "ASSIGN", NumArgs: 2, Precedence: 40, Handler: assignUpdateOperator} var addAssignOpType = &operationType{Type: "ADD_ASSIGN", NumArgs: 2, Precedence: 40, Handler: addAssignOperator} +var subtractAssignOpType = &operationType{Type: "SUBTRACT_ASSIGN", NumArgs: 2, Precedence: 40, Handler: subtractAssignOperator} var assignAttributesOpType = &operationType{Type: "ASSIGN_ATTRIBUTES", NumArgs: 2, Precedence: 40, Handler: assignAttributesOperator} var assignStyleOpType = &operationType{Type: "ASSIGN_STYLE", NumArgs: 2, Precedence: 40, Handler: assignStyleOperator} @@ -46,6 +47,7 @@ var assignAliasOpType = &operationType{Type: "ASSIGN_ALIAS", NumArgs: 2, Precede var multiplyOpType = &operationType{Type: "MULTIPLY", NumArgs: 2, Precedence: 42, Handler: multiplyOperator} 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} var equalsOpType = &operationType{Type: "EQUALS", NumArgs: 2, Precedence: 40, Handler: equalsOperator} diff --git a/pkg/yqlib/operator_subtract.go b/pkg/yqlib/operator_subtract.go new file mode 100644 index 00000000..a5ef6965 --- /dev/null +++ b/pkg/yqlib/operator_subtract.go @@ -0,0 +1,97 @@ +package yqlib + +import ( + "fmt" + + "strconv" + + yaml "gopkg.in/yaml.v3" +) + +func createSubtractOp(lhs *ExpressionNode, rhs *ExpressionNode) *ExpressionNode { + return &ExpressionNode{Operation: &Operation{OperationType: subtractOpType}, + Lhs: lhs, + Rhs: rhs} +} + +func subtractAssignOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + assignmentOp := &Operation{OperationType: assignOpType} + assignmentOp.UpdateAssign = true + selfExpression := &ExpressionNode{Operation: &Operation{OperationType: selfReferenceOpType}} + assignmentOpNode := &ExpressionNode{Operation: assignmentOp, Lhs: expressionNode.Lhs, Rhs: createSubtractOp(selfExpression, expressionNode.Rhs)} + return d.GetMatchingNodes(context, assignmentOpNode) +} + +func subtractOperator(d *dataTreeNavigator, context Context, expressionNode *ExpressionNode) (Context, error) { + log.Debugf("Subtract operator") + + return crossFunction(d, context, expressionNode, subtract) +} + +func subtract(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 lhs.CreateChild(nil, rhs.Node), nil + } + + target := lhs.CreateChild(nil, &yaml.Node{}) + + switch lhsNode.Kind { + case yaml.MappingNode: + return nil, fmt.Errorf("Maps not yet supported for subtraction") + case yaml.SequenceNode: + return nil, fmt.Errorf("Sequences not yet supported for subtraction") + // target.Node.Kind = yaml.SequenceNode + // target.Node.Style = lhsNode.Style + // target.Node.Tag = "!!seq" + // target.Node.Content = append(lhsNode.Content, toNodes(rhs)...) + case yaml.ScalarNode: + if rhs.Node.Kind != yaml.ScalarNode { + return nil, fmt.Errorf("%v (%v) cannot be added to a %v", rhs.Node.Tag, rhs.Path, lhsNode.Tag) + } + target.Node.Kind = yaml.ScalarNode + target.Node.Style = lhsNode.Style + return subtractScalars(target, lhsNode, rhs.Node) + } + + return target, nil +} + +func subtractScalars(target *CandidateNode, lhs *yaml.Node, rhs *yaml.Node) (*CandidateNode, error) { + + if lhs.Tag == "!!str" { + return nil, fmt.Errorf("strings cannot be subtracted") + } else if lhs.Tag == "!!int" && rhs.Tag == "!!int" { + lhsNum, err := strconv.Atoi(lhs.Value) + if err != nil { + return nil, err + } + rhsNum, err := strconv.Atoi(rhs.Value) + if err != nil { + return nil, err + } + result := lhsNum - rhsNum + target.Node.Tag = "!!int" + target.Node.Value = fmt.Sprintf("%v", result) + } else if (lhs.Tag == "!!int" || lhs.Tag == "!!float") && (rhs.Tag == "!!int" || rhs.Tag == "!!float") { + lhsNum, err := strconv.ParseFloat(lhs.Value, 64) + if err != nil { + return nil, err + } + rhsNum, err := strconv.ParseFloat(rhs.Value, 64) + if err != nil { + return nil, err + } + result := lhsNum - rhsNum + target.Node.Tag = "!!float" + target.Node.Value = fmt.Sprintf("%v", result) + } else { + return nil, fmt.Errorf("%v cannot be added to %v", lhs.Tag, rhs.Tag) + } + + return target, nil +} diff --git a/pkg/yqlib/operator_subtract_test.go b/pkg/yqlib/operator_subtract_test.go new file mode 100644 index 00000000..a78d633d --- /dev/null +++ b/pkg/yqlib/operator_subtract_test.go @@ -0,0 +1,50 @@ +package yqlib + +import ( + "testing" +) + +var subtractOperatorScenarios = []expressionScenario{ + { + description: "Number subtraction - float", + subdescription: "If the lhs or rhs are floats then the expression will be calculated with floats.", + document: `{a: 3, b: 4.5}`, + expression: `.a = .a - .b`, + expected: []string{ + "D0, P[], (doc)::{a: -1.5, b: 4.5}\n", + }, + }, + { + description: "Number subtraction - float", + subdescription: "If the lhs or rhs are floats then the expression will be calculated with floats.", + document: `{a: 3, b: 4.5}`, + expression: `.a = .a - .b`, + expected: []string{ + "D0, P[], (doc)::{a: -1.5, b: 4.5}\n", + }, + }, + { + description: "Number subtraction - int", + subdescription: "If both the lhs and rhs are ints then the expression will be calculated with ints.", + document: `{a: 3, b: 4}`, + expression: `.a = .a - .b`, + expected: []string{ + "D0, P[], (doc)::{a: -1, b: 4}\n", + }, + }, + { + description: "Decrement numbers", + document: `{a: 3, b: 5}`, + expression: `.[] -= 1`, + expected: []string{ + "D0, P[], (doc)::{a: 2, b: 4}\n", + }, + }, +} + +func TestSubtractOperatorScenarios(t *testing.T) { + for _, tt := range subtractOperatorScenarios { + testScenario(t, &tt) + } + documentScenarios(t, "Subtract", subtractOperatorScenarios) +}