Skip to content

Commit

Permalink
sort_by works on maps
Browse files Browse the repository at this point in the history
  • Loading branch information
mikefarah committed Jan 22, 2025
1 parent 1508f1f commit f202d06
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 36 deletions.
14 changes: 3 additions & 11 deletions examples/data1.yaml
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
# block comments come through
person: # neither do comments on maps
name: Mike Wazowski # comments on values appear
pets:
- cat # comments on array values appear
- dog # comments on array values appear
- things:
- frog
food: [pizza] # comments on arrays do not
emptyArray: []
emptyMap: []
Foo: 3
apple: 1
bar: 2
4 changes: 1 addition & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,4 @@ require (
golang.org/x/sys v0.28.0 // indirect
)

go 1.21.0

toolchain go1.22.5
go 1.23.0
23 changes: 23 additions & 0 deletions pkg/yqlib/candidate_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,29 @@ func (n *CandidateNode) SetParent(parent *CandidateNode) {
n.Parent = parent
}

type ValueVisitor func(*CandidateNode) error

func (n *CandidateNode) VisitValues(visitor ValueVisitor) error {
if n.Kind == MappingNode {
for i := 1; i < len(n.Content); i = i + 2 {
if err := visitor(n.Content[i]); err != nil {
return err
}
}
} else if n.Kind == SequenceNode {
for i := 0; i < len(n.Content); i = i + 1 {
if err := visitor(n.Content[i]); err != nil {
return err
}
}
}
return nil
}

func (n *CandidateNode) CanVisitValues() bool {
return n.Kind == MappingNode || n.Kind == SequenceNode
}

func (n *CandidateNode) AddKeyValueChild(rawKey *CandidateNode, rawValue *CandidateNode) (*CandidateNode, *CandidateNode) {
key := rawKey.Copy()
key.SetParent(n)
Expand Down
4 changes: 2 additions & 2 deletions pkg/yqlib/doc/operators/headers/sort-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@ diff file1.yml file2.yml

Note that `yq` does not yet consider anchors when sorting by keys - this may result in invalid yaml documents if you are using merge anchors.

For more advanced sorting, using `to_entries` to convert the map to an array, then sort/process the array as you like (e.g. using `sort_by`) and convert back to a map using `from_entries`.
See [here](https://mikefarah.gitbook.io/yq/operators/entries#custom-sort-map-keys) for an example.
For more advanced sorting, you can use the [sort_by](https://mikefarah.gitbook.io/yq/operators/sort) function on a map, and give it a custom function like `sort_by(key | downcase)`.

4 changes: 2 additions & 2 deletions pkg/yqlib/doc/operators/sort-keys.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ diff file1.yml file2.yml

Note that `yq` does not yet consider anchors when sorting by keys - this may result in invalid yaml documents if you are using merge anchors.

For more advanced sorting, using `to_entries` to convert the map to an array, then sort/process the array as you like (e.g. using `sort_by`) and convert back to a map using `from_entries`.
See [here](https://mikefarah.gitbook.io/yq/operators/entries#custom-sort-map-keys) for an example.
For more advanced sorting, you can use the [sort_by](https://mikefarah.gitbook.io/yq/operators/sort) function on a map, and give it a custom function like `sort_by(key | downcase)`.


## Sort keys of map
Given a sample.yml file of:
Expand Down
40 changes: 40 additions & 0 deletions pkg/yqlib/doc/operators/sort.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,46 @@ cool:
- c: banana
```
## Sort a map
Sorting a map, by default, will sort by the values
Given a sample.yml file of:
```yaml
y: b
z: a
x: c
```
then
```bash
yq 'sort' sample.yml
```
will output
```yaml
z: a
y: b
x: c
```
## Sort a map by keys
Use sort_by to sort a map using a custom function
Given a sample.yml file of:
```yaml
Y: b
z: a
x: c
```
then
```bash
yq 'sort_by(key | downcase)' sample.yml
```
will output
```yaml
x: c
Y: b
z: a
```
## Sort is stable
Note the order of the elements in unchanged when equal in sorting.
Expand Down
46 changes: 28 additions & 18 deletions pkg/yqlib/operator_sort.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,40 @@ func sortByOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
candidate := el.Value.(*CandidateNode)

if candidate.Kind != SequenceNode {
return context, fmt.Errorf("node at path [%v] is not an array (it's a %v)", candidate.GetNicePath(), candidate.Tag)
}

sortableArray := make(sortableNodeArray, len(candidate.Content))

for i, originalNode := range candidate.Content {

compareContext, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(originalNode), expressionNode.RHS)
if err != nil {
return Context{}, err
var sortableArray sortableNodeArray

if candidate.CanVisitValues() {
sortableArray = make(sortableNodeArray, 0)
visitor := func(valueNode *CandidateNode) error {
compareContext, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(valueNode), expressionNode.RHS)
if err != nil {
return err
}
sortableNode := sortableNode{Node: valueNode, CompareContext: compareContext, dateTimeLayout: context.GetDateTimeLayout()}
sortableArray = append(sortableArray, sortableNode)
return nil
}

sortableArray[i] = sortableNode{Node: originalNode, CompareContext: compareContext, dateTimeLayout: context.GetDateTimeLayout()}

if err := candidate.VisitValues(visitor); err != nil {
return context, err
}
} else {
return context, fmt.Errorf("node at path [%v] is not an array or map (it's a %v)", candidate.GetNicePath(), candidate.Tag)
}

sort.Stable(sortableArray)

sortedList := candidate.CreateReplacementWithComments(SequenceNode, "!!seq", candidate.Style)

for _, sortedNode := range sortableArray {
sortedList.AddChild(sortedNode.Node)
sortedList := candidate.CopyWithoutContent()
if candidate.Kind == MappingNode {
for _, sortedNode := range sortableArray {
sortedList.AddKeyValueChild(sortedNode.Node.Key, sortedNode.Node)
}
} else if candidate.Kind == SequenceNode {
for _, sortedNode := range sortableArray {
sortedList.AddChild(sortedNode.Node)
}
}

// convert array of value nodes back to map
results.PushBack(sortedList)
}
return context.ChildContext(results), nil
Expand Down
18 changes: 18 additions & 0 deletions pkg/yqlib/operator_sort_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ var sortByOperatorScenarios = []expressionScenario{
"D0, P[], (!!map)::cool: [{a: banana}, {b: banana}, {c: banana}]\n",
},
},
{
description: "Sort a map",
subdescription: "Sorting a map, by default this will sort by the values",
document: "y: b\nz: a\nx: c\n",
expression: `sort`,
expected: []string{
"D0, P[], (!!map)::z: a\ny: b\nx: c\n",
},
},
{
description: "Sort a map by keys",
subdescription: "Use sort_by to sort a map using a custom function",
document: "Y: b\nz: a\nx: c\n",
expression: `sort_by(key | downcase)`,
expected: []string{
"D0, P[], (!!map)::x: c\nY: b\nz: a\n",
},
},
{
description: "Sort is stable",
subdescription: "Note the order of the elements in unchanged when equal in sorting.",
Expand Down

0 comments on commit f202d06

Please sign in to comment.