diff --git a/pkg/yqlib/doc/Subtract.md b/pkg/yqlib/doc/Subtract.md index cf0f9baa..8f71d32e 100644 --- a/pkg/yqlib/doc/Subtract.md +++ b/pkg/yqlib/doc/Subtract.md @@ -1,4 +1,43 @@ +## Array subtraction +Running +```bash +yq eval --null-input '[1,2] - [2,3]' +``` +will output +```yaml +- 1 +``` + +## Array subtraction with nested array +Running +```bash +yq eval --null-input '[[1], 1, 2] - [[1], 3]' +``` +will output +```yaml +- 1 +- 2 +``` + +## Array subtraction with nested object +Note that order of the keys does not matter + +Given a sample.yml file of: +```yaml +- a: b + c: d +- a: b +``` +then +```bash +yq eval '. - [{"c": "d", "a": "b"}]' sample.yml +``` +will output +```yaml +- a: b +``` + ## Number subtraction - float If the lhs or rhs are floats then the expression will be calculated with floats. diff --git a/pkg/yqlib/lib.go b/pkg/yqlib/lib.go index 29054578..01d69fed 100644 --- a/pkg/yqlib/lib.go +++ b/pkg/yqlib/lib.go @@ -119,6 +119,64 @@ type Operation struct { UpdateAssign bool // used for assign ops, when true it means we evaluate the rhs given the lhs } +func recurseNodeArrayEqual(lhs *yaml.Node, rhs *yaml.Node) bool { + if len(lhs.Content) != len(rhs.Content) { + return false + } + + for index := 0; index < len(lhs.Content); index = index + 1 { + if !recursiveNodeEqual(lhs.Content[index], rhs.Content[index]) { + return false + } + } + return true +} + +func findInArray(array *yaml.Node, item *yaml.Node) int { + + for index := 0; index < len(array.Content); index = index + 1 { + if recursiveNodeEqual(array.Content[index], item) { + return index + } + } + return -1 +} + +func recurseNodeObjectEqual(lhs *yaml.Node, rhs *yaml.Node) bool { + if len(lhs.Content) != len(rhs.Content) { + return false + } + + for index := 0; index < len(lhs.Content); index = index + 2 { + key := lhs.Content[index] + value := lhs.Content[index+1] + + indexInRhs := findInArray(rhs, key) + + if indexInRhs == -1 || !recursiveNodeEqual(value, rhs.Content[indexInRhs+1]) { + return false + } + } + return true +} + +func recursiveNodeEqual(lhs *yaml.Node, rhs *yaml.Node) bool { + if lhs.Kind != rhs.Kind || lhs.Tag != rhs.Tag { + return false + } else if lhs.Tag == "!!null" { + return true + + } else if lhs.Kind == yaml.ScalarNode { + return lhs.Value == rhs.Value + } else if lhs.Kind == yaml.SequenceNode { + return recurseNodeArrayEqual(lhs, rhs) + } else if lhs.Kind == yaml.MappingNode { + return recurseNodeObjectEqual(lhs, rhs) + } + return false + +} + // yaml numbers can be hex encoded... func parseInt(numberString string) (string, int64, error) { if strings.HasPrefix(numberString, "0x") || diff --git a/pkg/yqlib/operator_subtract.go b/pkg/yqlib/operator_subtract.go index 6a8b6742..f58f8341 100644 --- a/pkg/yqlib/operator_subtract.go +++ b/pkg/yqlib/operator_subtract.go @@ -5,7 +5,7 @@ import ( "strconv" - yaml "gopkg.in/yaml.v3" + "gopkg.in/yaml.v3" ) func createSubtractOp(lhs *ExpressionNode, rhs *ExpressionNode) *ExpressionNode { @@ -24,6 +24,24 @@ func subtractOperator(d *dataTreeNavigator, context Context, expressionNode *Exp return crossFunction(d, context.ReadOnlyClone(), expressionNode, subtract, false) } +func subtractArray(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { + newLhsArray := make([]*yaml.Node, 0) + + for lindex := 0; lindex < len(lhs.Node.Content); lindex = lindex + 1 { + shouldInclude := true + for rindex := 0; rindex < len(rhs.Node.Content) && shouldInclude; rindex = rindex + 1 { + if recursiveNodeEqual(lhs.Node.Content[lindex], rhs.Node.Content[rindex]) { + shouldInclude = false + } + } + if shouldInclude { + newLhsArray = append(newLhsArray, lhs.Node.Content[lindex]) + } + } + lhs.Node.Content = newLhsArray + return lhs, nil +} + func subtract(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *CandidateNode) (*CandidateNode, error) { lhs.Node = unwrapDoc(lhs.Node) rhs.Node = unwrapDoc(rhs.Node) @@ -40,14 +58,13 @@ func subtract(d *dataTreeNavigator, context Context, lhs *CandidateNode, rhs *Ca 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)...) + if rhs.Node.Kind != yaml.SequenceNode { + return nil, fmt.Errorf("%v (%v) cannot be subtracted from %v", rhs.Node.Tag, rhs.Path, lhsNode.Tag) + } + return subtractArray(d, context, lhs, 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) + return nil, fmt.Errorf("%v (%v) cannot be subtracted from %v", rhs.Node.Tag, rhs.Path, lhsNode.Tag) } target.Node.Kind = yaml.ScalarNode target.Node.Style = lhsNode.Style diff --git a/pkg/yqlib/operator_subtract_test.go b/pkg/yqlib/operator_subtract_test.go index ba511f1d..09db0035 100644 --- a/pkg/yqlib/operator_subtract_test.go +++ b/pkg/yqlib/operator_subtract_test.go @@ -13,6 +13,51 @@ var subtractOperatorScenarios = []expressionScenario{ "D0, P[], (doc)::{}\n", }, }, + { + description: "Array subtraction", + expression: `[1,2] - [2,3]`, + expected: []string{ + "D0, P[], (!!seq)::- 1\n", + }, + }, + { + skipDoc: true, + expression: `[2,1,2,2] - [2,3]`, + expected: []string{ + "D0, P[], (!!seq)::- 1\n", + }, + }, + { + description: "Array subtraction with nested array", + expression: `[[1], 1, 2] - [[1], 3]`, + expected: []string{ + "D0, P[], (!!seq)::- 1\n- 2\n", + }, + }, + { + skipDoc: true, + expression: `[[1], 1, [[[2]]]] - [[1], [[[3]]]]`, + expected: []string{ + "D0, P[], (!!seq)::- 1\n- - - - 2\n", + }, + }, + { + description: "Array subtraction with nested object", + subdescription: `Note that order of the keys does not matter`, + document: `[{a: b, c: d}, {a: b}]`, + expression: `. - [{"c": "d", "a": "b"}]`, + expected: []string{ + "D0, P[], (!!seq)::[{a: b}]\n", + }, + }, + { + skipDoc: true, + document: `[{a: [1], c: d}, {a: [2], c: d}, {a: b}]`, + expression: `. - [{"c": "d", "a": [1]}]`, + expected: []string{ + "D0, P[], (!!seq)::[{a: [2], c: d}, {a: b}]\n", + }, + }, { description: "Number subtraction - float", subdescription: "If the lhs or rhs are floats then the expression will be calculated with floats.",