Skip to content

Commit a0379c1

Browse files
committed
sort_by works on maps
1 parent 8bf425b commit a0379c1

File tree

8 files changed

+117
-36
lines changed

8 files changed

+117
-36
lines changed

examples/data1.yaml

+3-11
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
# block comments come through
2-
person: # neither do comments on maps
3-
name: Mike Wazowski # comments on values appear
4-
pets:
5-
- cat # comments on array values appear
6-
- dog # comments on array values appear
7-
- things:
8-
- frog
9-
food: [pizza] # comments on arrays do not
10-
emptyArray: []
11-
emptyMap: []
1+
Foo: 3
2+
apple: 1
3+
bar: 2

go.mod

+1-3
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,4 @@ require (
2929
golang.org/x/sys v0.28.0 // indirect
3030
)
3131

32-
go 1.21.0
33-
34-
toolchain go1.22.5
32+
go 1.23.0

pkg/yqlib/candidate_node.go

+23
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,29 @@ func (n *CandidateNode) SetParent(parent *CandidateNode) {
198198
n.Parent = parent
199199
}
200200

201+
type ValueVisitor func(*CandidateNode) error
202+
203+
func (n *CandidateNode) VisitValues(visitor ValueVisitor) error {
204+
if n.Kind == MappingNode {
205+
for i := 1; i < len(n.Content); i = i + 2 {
206+
if err := visitor(n.Content[i]); err != nil {
207+
return err
208+
}
209+
}
210+
} else if n.Kind == SequenceNode {
211+
for i := 0; i < len(n.Content); i = i + 1 {
212+
if err := visitor(n.Content[i]); err != nil {
213+
return err
214+
}
215+
}
216+
}
217+
return nil
218+
}
219+
220+
func (n *CandidateNode) CanVisitValues() bool {
221+
return n.Kind == MappingNode || n.Kind == SequenceNode
222+
}
223+
201224
func (n *CandidateNode) AddKeyValueChild(rawKey *CandidateNode, rawValue *CandidateNode) (*CandidateNode, *CandidateNode) {
202225
key := rawKey.Copy()
203226
key.SetParent(n)

pkg/yqlib/doc/operators/headers/sort-keys.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ diff file1.yml file2.yml
1212

1313
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.
1414

15-
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`.
16-
See [here](https://mikefarah.gitbook.io/yq/operators/entries#custom-sort-map-keys) for an example.
15+
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)`.
16+

pkg/yqlib/doc/operators/sort-keys.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ diff file1.yml file2.yml
1212

1313
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.
1414

15-
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`.
16-
See [here](https://mikefarah.gitbook.io/yq/operators/entries#custom-sort-map-keys) for an example.
15+
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)`.
16+
1717

1818
## Sort keys of map
1919
Given a sample.yml file of:

pkg/yqlib/doc/operators/sort.md

+40
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,46 @@ cool:
109109
- c: banana
110110
```
111111
112+
## Sort a map
113+
Sorting a map, by default, will sort by the values
114+
115+
Given a sample.yml file of:
116+
```yaml
117+
y: b
118+
z: a
119+
x: c
120+
```
121+
then
122+
```bash
123+
yq 'sort' sample.yml
124+
```
125+
will output
126+
```yaml
127+
z: a
128+
y: b
129+
x: c
130+
```
131+
132+
## Sort a map by keys
133+
Use sort_by to sort a map using a custom function
134+
135+
Given a sample.yml file of:
136+
```yaml
137+
Y: b
138+
z: a
139+
x: c
140+
```
141+
then
142+
```bash
143+
yq 'sort_by(key | downcase)' sample.yml
144+
```
145+
will output
146+
```yaml
147+
x: c
148+
Y: b
149+
z: a
150+
```
151+
112152
## Sort is stable
113153
Note the order of the elements in unchanged when equal in sorting.
114154

pkg/yqlib/operator_sort.go

+28-18
Original file line numberDiff line numberDiff line change
@@ -24,30 +24,40 @@ func sortByOperator(d *dataTreeNavigator, context Context, expressionNode *Expre
2424
for el := context.MatchingNodes.Front(); el != nil; el = el.Next() {
2525
candidate := el.Value.(*CandidateNode)
2626

27-
if candidate.Kind != SequenceNode {
28-
return context, fmt.Errorf("node at path [%v] is not an array (it's a %v)", candidate.GetNicePath(), candidate.Tag)
29-
}
30-
31-
sortableArray := make(sortableNodeArray, len(candidate.Content))
32-
33-
for i, originalNode := range candidate.Content {
34-
35-
compareContext, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(originalNode), expressionNode.RHS)
36-
if err != nil {
37-
return Context{}, err
27+
var sortableArray sortableNodeArray
28+
29+
if candidate.CanVisitValues() {
30+
sortableArray = make(sortableNodeArray, 0)
31+
visitor := func(valueNode *CandidateNode) error {
32+
compareContext, err := d.GetMatchingNodes(context.SingleReadonlyChildContext(valueNode), expressionNode.RHS)
33+
if err != nil {
34+
return err
35+
}
36+
sortableNode := sortableNode{Node: valueNode, CompareContext: compareContext, dateTimeLayout: context.GetDateTimeLayout()}
37+
sortableArray = append(sortableArray, sortableNode)
38+
return nil
3839
}
39-
40-
sortableArray[i] = sortableNode{Node: originalNode, CompareContext: compareContext, dateTimeLayout: context.GetDateTimeLayout()}
41-
40+
if err := candidate.VisitValues(visitor); err != nil {
41+
return context, err
42+
}
43+
} else {
44+
return context, fmt.Errorf("node at path [%v] is not an array or map (it's a %v)", candidate.GetNicePath(), candidate.Tag)
4245
}
4346

4447
sort.Stable(sortableArray)
4548

46-
sortedList := candidate.CreateReplacementWithComments(SequenceNode, "!!seq", candidate.Style)
47-
48-
for _, sortedNode := range sortableArray {
49-
sortedList.AddChild(sortedNode.Node)
49+
sortedList := candidate.CopyWithoutContent()
50+
if candidate.Kind == MappingNode {
51+
for _, sortedNode := range sortableArray {
52+
sortedList.AddKeyValueChild(sortedNode.Node.Key, sortedNode.Node)
53+
}
54+
} else if candidate.Kind == SequenceNode {
55+
for _, sortedNode := range sortableArray {
56+
sortedList.AddChild(sortedNode.Node)
57+
}
5058
}
59+
60+
// convert array of value nodes back to map
5161
results.PushBack(sortedList)
5262
}
5363
return context.ChildContext(results), nil

pkg/yqlib/operator_sort_test.go

+18
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,24 @@ var sortByOperatorScenarios = []expressionScenario{
8484
"D0, P[], (!!map)::cool: [{a: banana}, {b: banana}, {c: banana}]\n",
8585
},
8686
},
87+
{
88+
description: "Sort a map",
89+
subdescription: "Sorting a map, by default this will sort by the values",
90+
document: "y: b\nz: a\nx: c\n",
91+
expression: `sort`,
92+
expected: []string{
93+
"D0, P[], (!!map)::z: a\ny: b\nx: c\n",
94+
},
95+
},
96+
{
97+
description: "Sort a map by keys",
98+
subdescription: "Use sort_by to sort a map using a custom function",
99+
document: "Y: b\nz: a\nx: c\n",
100+
expression: `sort_by(key | downcase)`,
101+
expected: []string{
102+
"D0, P[], (!!map)::x: c\nY: b\nz: a\n",
103+
},
104+
},
87105
{
88106
description: "Sort is stable",
89107
subdescription: "Note the order of the elements in unchanged when equal in sorting.",

0 commit comments

Comments
 (0)