Repository: gohugoio/hugo (language: go)

Issue:
Sorting is not stable
Hi,

I'm trying to sort a slice of dicts by two different fields but when calling sort twice, it seems the order is not kept stable. Here is a reproducible example. 
```go
{{ $values := slice (dict "a" 1 "b" 2) (dict "a" 3 "b" 1) (dict "a" 2 "b" 0) (dict "a" 1 "b" 0) (dict "a" 3 "b" 1) (dict "a" 2 "b" 2) (dict "a" 2 "b" 1) (dict "a" 0 "b" 3) (dict "a" 3 "b" 3) (dict "a" 0 "b" 0) (dict "a" 0 "b" 0) (dict "a" 2 "b" 0) (dict "a" 1 "b" 2) (dict "a" 1 "b" 1) (dict "a" 3 "b" 0) (dict "a" 2 "b" 0) (dict "a" 3 "b" 0) (dict "a" 3 "b" 0) (dict "a" 3 "b" 0) (dict "a" 3 "b" 1) }}
{{ sort (sort $values "b") "a" }}
```
which outputs the following:
<pre>
[map[a:0 b:0] map[a:0 b:0] map[a:0 b:3] map[a:1 b:2] map[a:1 b:2] map[a:1 b:1] map[a:1 b:0] map[a:2 b:0] map[a:2 b:2] map[a:2 b:0] map[a:2 b:0] map[a:2 b:1] map[a:3 b:1] map[a:3 b:1] map[a:3 b:1] map[a:3 b:0] map[a:3 b:0] map[a:3 b:0] map[a:3 b:3] map[a:3 b:0]] 
</pre>

For each unique value of "a", the values of "b" should be sorted. In this example, we can see for a=1, the values of b are in this order: 2, 2, 1, 0.

Do you know some workaround?

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.98.0-165d299cde259c8b801abadc6d3405a229e449f6+extended linux/amd64 BuildDate=2022-04-28T10:23:30Z VendorInfo=gohugoio
</pre>

### Does this issue reproduce with the latest release?

Yes.





Discussion / hints:
If I understand correctly the code base, this line https://github.com/gohugoio/hugo/blob/f2946da9e806c2bafbdd26707fe339db79bd980b/tpl/collections/sort.go#L180 should be change to `sort.Stable(p)` to make the sorting stable.


Given this map:

```
{{ $m := slice
  (dict "a" 1 "b" 2)
  (dict "a" 2 "b" 2)
  (dict "a" 1 "b" 1)
  (dict "a" 2 "b" 1)
}}
```

You currently have to do something like this:

```
{{ $s := slice }}
{{ range $m }}
  {{ $s = $s | append .a }}
{{ end }}

{{ range ($s | uniq | sort) }}
  {{ range sort (where $m "a" .) "b" }}
    {{ range $k, $v := . }}
      {{ $k }}: {{ $v }}<br>
    {{ end }}
  {{ end }}
{{ end }}
```

It would be great if you could do this instead:

```
{{ range sort (sort $m "a") "b" }}
  {{ range $k, $v := . }}
    {{ $k }}: {{ $v }}<br>
  {{ end }}
{{ end }}
```

Fix patch:
diff --git a/tpl/collections/sort.go b/tpl/collections/sort.go
index ce7f27771c4..a0c2f815b19 100644
--- a/tpl/collections/sort.go
+++ b/tpl/collections/sort.go
@@ -177,9 +177,9 @@ func (p pairList) Less(i, j int) bool {
 // sorts a pairList and returns a slice of sorted values
 func (p pairList) sort() any {
 	if p.SortAsc {
-		sort.Sort(p)
+		sort.Stable(p)
 	} else {
-		sort.Sort(sort.Reverse(p))
+		sort.Stable(sort.Reverse(p))
 	}
 	sorted := reflect.MakeSlice(p.SliceType, len(p.Pairs), len(p.Pairs))
 	for i, v := range p.Pairs {


Test patch:
diff --git a/tpl/collections/integration_test.go b/tpl/collections/integration_test.go
index d767c384c64..225eab9faca 100644
--- a/tpl/collections/integration_test.go
+++ b/tpl/collections/integration_test.go
@@ -43,3 +43,33 @@ baseURL = 'http://example.com/'
 	[foo foo foo]
 `)
 }
+
+// Issue 9865
+func TestSortStable(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+-- layouts/index.html --
+{{ $values := slice (dict "a" 1 "b" 2) (dict "a" 3 "b" 1) (dict "a" 2 "b" 0) (dict "a" 1 "b" 0) (dict "a" 3 "b" 1) (dict "a" 2 "b" 2) (dict "a" 2 "b" 1) (dict "a" 0 "b" 3) (dict "a" 3 "b" 3) (dict "a" 0 "b" 0) (dict "a" 0 "b" 0) (dict "a" 2 "b" 0) (dict "a" 1 "b" 2) (dict "a" 1 "b" 1) (dict "a" 3 "b" 0) (dict "a" 2 "b" 0) (dict "a" 3 "b" 0) (dict "a" 3 "b" 0) (dict "a" 3 "b" 0) (dict "a" 3 "b" 1) }}
+Asc:  {{ sort (sort $values "b" "asc") "a" "asc" }}
+Desc: {{ sort (sort $values "b" "desc") "a" "desc" }}
+
+  `
+
+	for i := 0; i < 4; i++ {
+
+		b := hugolib.NewIntegrationTestBuilder(
+			hugolib.IntegrationTestConfig{
+				T:           t,
+				TxtarString: files,
+			},
+		).Build()
+
+		b.AssertFileContent("public/index.html", `
+Asc:  [map[a:0 b:0] map[a:0 b:0] map[a:0 b:3] map[a:1 b:0] map[a:1 b:1] map[a:1 b:2] map[a:1 b:2] map[a:2 b:0] map[a:2 b:0] map[a:2 b:0] map[a:2 b:1] map[a:2 b:2] map[a:3 b:0] map[a:3 b:0] map[a:3 b:0] map[a:3 b:0] map[a:3 b:1] map[a:3 b:1] map[a:3 b:1] map[a:3 b:3]]
+Desc: [map[a:3 b:3] map[a:3 b:1] map[a:3 b:1] map[a:3 b:1] map[a:3 b:0] map[a:3 b:0] map[a:3 b:0] map[a:3 b:0] map[a:2 b:2] map[a:2 b:1] map[a:2 b:0] map[a:2 b:0] map[a:2 b:0] map[a:1 b:2] map[a:1 b:2] map[a:1 b:1] map[a:1 b:0] map[a:0 b:3] map[a:0 b:0] map[a:0 b:0]]
+`)
+
+	}
+}