diff --git a/pkg/yqlib/doc/Add.md b/pkg/yqlib/doc/Add.md index bb994943..ab48df88 100644 --- a/pkg/yqlib/doc/Add.md +++ b/pkg/yqlib/doc/Add.md @@ -1,9 +1,9 @@ Add behaves differently according to the type of the LHS: - arrays: concatenate -- number scalars: arithmetic addition (soon) -- string scalars: concatenate (soon) +- number scalars: arithmetic addition +- string scalars: concatenate -Use `+=` as append assign for things like increment. `.a += .x` is equivalent to running `.a |= . + .x`. +Use `+=` as append assign for things like increment. Note that `.a += .x` is equivalent to running `.a = .a + .x`. ## Concatenate and assign arrays Given a sample.yml file of: @@ -131,3 +131,85 @@ b: - 4 ``` +## String concatenation +Given a sample.yml file of: +```yaml +a: cat +b: meow +``` +then +```bash +yq eval '.a = .a + .b' sample.yml +``` +will output +```yaml +a: catmeow +b: meow +``` + +## Relative string concatenation +Given a sample.yml file of: +```yaml +a: cat +b: meow +``` +then +```bash +yq eval '.a += .b' sample.yml +``` +will output +```yaml +a: catmeow +b: meow +``` + +## Number addition - 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.9 +``` +then +```bash +yq eval '.a = .a + .b' sample.yml +``` +will output +```yaml +a: 7.9 +b: 4.9 +``` + +## Number addition - 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: 7 +b: 4 +``` + +## Increment number +Given a sample.yml file of: +```yaml +a: 3 +``` +then +```bash +yq eval '.a += 1' sample.yml +``` +will output +```yaml +a: 4 +``` + diff --git a/pkg/yqlib/doc/headers/Add.md b/pkg/yqlib/doc/headers/Add.md index 96cf7246..dcd16a1e 100644 --- a/pkg/yqlib/doc/headers/Add.md +++ b/pkg/yqlib/doc/headers/Add.md @@ -1,6 +1,6 @@ Add behaves differently according to the type of the LHS: - arrays: concatenate -- number scalars: arithmetic addition (soon) -- string scalars: concatenate (soon) +- number scalars: arithmetic addition +- string scalars: concatenate -Use `+=` as append assign for things like increment. `.a += .x` is equivalent to running `.a |= . + .x`. +Use `+=` as append assign for things like increment. Note that `.a += .x` is equivalent to running `.a = .a + .x`. diff --git a/pkg/yqlib/operator_add.go b/pkg/yqlib/operator_add.go index 0fba7736..a397d8f1 100644 --- a/pkg/yqlib/operator_add.go +++ b/pkg/yqlib/operator_add.go @@ -4,21 +4,22 @@ import ( "fmt" "container/list" + "strconv" yaml "gopkg.in/yaml.v3" ) -func createSelfAddOp(rhs *PathTreeNode) *PathTreeNode { +func createAddOp(lhs *PathTreeNode, rhs *PathTreeNode) *PathTreeNode { return &PathTreeNode{Operation: &Operation{OperationType: Add}, - Lhs: &PathTreeNode{Operation: &Operation{OperationType: SelfReference}}, + Lhs: lhs, Rhs: rhs} } func AddAssignOperator(d *dataTreeNavigator, matchingNodes *list.List, pathNode *PathTreeNode) (*list.List, error) { assignmentOp := &Operation{OperationType: Assign} - assignmentOp.UpdateAssign = true + assignmentOp.UpdateAssign = false - assignmentOpNode := &PathTreeNode{Operation: assignmentOp, Lhs: pathNode.Lhs, Rhs: createSelfAddOp(pathNode.Rhs)} + assignmentOpNode := &PathTreeNode{Operation: assignmentOp, Lhs: pathNode.Lhs, Rhs: createAddOp(pathNode.Lhs, pathNode.Rhs)} return d.GetMatchingNodes(matchingNodes, assignmentOpNode) } @@ -63,7 +64,48 @@ func add(d *dataTreeNavigator, lhs *CandidateNode, rhs *CandidateNode) (*Candida target.Node.Tag = "!!seq" target.Node.Content = append(lhsNode.Content, toNodes(rhs)...) case yaml.ScalarNode: - return nil, fmt.Errorf("Scalars not yet supported for addition") + 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 addScalars(target, lhsNode, rhs.Node) + } + + return target, nil +} + +func addScalars(target *CandidateNode, lhs *yaml.Node, rhs *yaml.Node) (*CandidateNode, error) { + + if lhs.Tag == "!!str" { + target.Node.Tag = "!!str" + target.Node.Value = lhs.Value + rhs.Value + } 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 + } + sum := lhsNum + rhsNum + target.Node.Tag = "!!int" + target.Node.Value = fmt.Sprintf("%v", sum) + } 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 + } + sum := lhsNum + rhsNum + target.Node.Tag = "!!float" + target.Node.Value = fmt.Sprintf("%v", sum) + } else { + return nil, fmt.Errorf("%v cannot be added to %v", lhs.Tag, rhs.Tag) } return target, nil diff --git a/pkg/yqlib/operator_add_test.go b/pkg/yqlib/operator_add_test.go index ea116831..f4b3def7 100644 --- a/pkg/yqlib/operator_add_test.go +++ b/pkg/yqlib/operator_add_test.go @@ -61,6 +61,48 @@ var addOperatorScenarios = []expressionScenario{ "D0, P[], (doc)::{a: [1, 2, 3, 4], b: [3, 4]}\n", }, }, + { + description: "String concatenation", + document: `{a: cat, b: meow}`, + expression: `.a = .a + .b`, + expected: []string{ + "D0, P[], (doc)::{a: catmeow, b: meow}\n", + }, + }, + { + description: "Relative string concatenation", + document: `{a: cat, b: meow}`, + expression: `.a += .b`, + expected: []string{ + "D0, P[], (doc)::{a: catmeow, b: meow}\n", + }, + }, + { + description: "Number addition - float", + subdescription: "If the lhs or rhs are floats then the expression will be calculated with floats.", + document: `{a: 3, b: 4.9}`, + expression: `.a = .a + .b`, + expected: []string{ + "D0, P[], (doc)::{a: 7.9, b: 4.9}\n", + }, + }, + { + description: "Number addition - 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: 7, b: 4}\n", + }, + }, + { + description: "Increment number", + document: `{a: 3}`, + expression: `.a += 1`, + expected: []string{ + "D0, P[], (doc)::{a: 4}\n", + }, + }, } func TestAddOperatorScenarios(t *testing.T) { diff --git a/pkg/yqlib/operator_collect_test.go b/pkg/yqlib/operator_collect_test.go index 4be76f81..f4a18e6b 100644 --- a/pkg/yqlib/operator_collect_test.go +++ b/pkg/yqlib/operator_collect_test.go @@ -14,10 +14,10 @@ var collectOperatorScenarios = []expressionScenario{ }, }, { - skipDoc: true, - document: "{a: apple}\n---\n{b: frog}", - - expression: `[.]`, + skipDoc: true, + document: "{a: apple}\n---\n{b: frog}", + + expression: `[.]`, expected: []string{ "D0, P[], (!!seq)::- {a: apple}\n- {b: frog}\n", },