Skip to content

Commit f330f2e

Browse files
committed
utils: Add IterateOrderedMap
This change was the result of investigating Go's new rangefunc experiment[0]. The utilization of this novel language feature - which can also be indirectly used in the absence of `GOEXPERIMENT=rangefunc` - ensures that the map is traversed in key order. [0]: https://go.dev/wiki/RangefuncExperiment Imported from Icinga/icinga-notifications@e371553
1 parent 6a29e59 commit f330f2e

File tree

2 files changed

+76
-0
lines changed

2 files changed

+76
-0
lines changed

utils/utils.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package utils
22

33
import (
4+
"cmp"
45
"context"
56
"crypto/sha1" // #nosec G505 -- Blocklisted import crypto/sha1
67
"fmt"
@@ -11,6 +12,7 @@ import (
1112
"net"
1213
"os"
1314
"path/filepath"
15+
"slices"
1416
"strings"
1517
"time"
1618
)
@@ -163,3 +165,27 @@ func PrintErrorThenExit(err error, exitCode int) {
163165
fmt.Fprintln(os.Stderr, err)
164166
os.Exit(exitCode)
165167
}
168+
169+
// IterateOrderedMap implements iter.Seq2 to iterate over a map in the key's order.
170+
//
171+
// This function returns a func yielding key-value-pairs from a given map in the order of their keys, if their type
172+
// is cmp.Ordered.
173+
//
174+
// Please note that currently - being at Go 1.22 - rangefuncs are still an experimental feature and cannot be directly
175+
// used unless compiled with `GOEXPERIMENT=rangefunc`. However, they can still be invoked normally.
176+
// https://go.dev/wiki/RangefuncExperiment
177+
func IterateOrderedMap[K cmp.Ordered, V any](m map[K]V) func(func(K, V) bool) {
178+
keys := make([]K, 0, len(m))
179+
for key := range m {
180+
keys = append(keys, key)
181+
}
182+
slices.Sort(keys)
183+
184+
return func(yield func(K, V) bool) {
185+
for _, key := range keys {
186+
if !yield(key, m[key]) {
187+
return
188+
}
189+
}
190+
}
191+
}

utils/utils_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package utils
22

33
import (
4+
"github.com/stretchr/testify/assert"
45
"github.com/stretchr/testify/require"
56
"testing"
67
)
@@ -52,3 +53,52 @@ func requireClosedEmpty(t *testing.T, ch <-chan int) {
5253
require.Fail(t, "receiving should not block")
5354
}
5455
}
56+
57+
func TestIterateOrderedMap(t *testing.T) {
58+
tests := []struct {
59+
name string
60+
in map[int]string
61+
outKeys []int
62+
}{
63+
{"empty", map[int]string{}, nil},
64+
{"single", map[int]string{1: "foo"}, []int{1}},
65+
{"few-numbers", map[int]string{1: "a", 2: "b", 3: "c"}, []int{1, 2, 3}},
66+
{
67+
"1k-numbers",
68+
func() map[int]string {
69+
m := make(map[int]string)
70+
for i := 0; i < 1000; i++ {
71+
m[i] = "foo"
72+
}
73+
return m
74+
}(),
75+
func() []int {
76+
keys := make([]int, 1000)
77+
for i := 0; i < 1000; i++ {
78+
keys[i] = i
79+
}
80+
return keys
81+
}(),
82+
},
83+
}
84+
85+
for _, tt := range tests {
86+
t.Run(tt.name, func(t *testing.T) {
87+
var outKeys []int
88+
89+
// Either run with GOEXPERIMENT=rangefunc or wait for rangefuncs to land in the next Go release.
90+
// for k, _ := range IterateOrderedMap(tt.in) {
91+
// outKeys = append(outKeys, k)
92+
// }
93+
94+
// In the meantime, it can be invoked as follows.
95+
IterateOrderedMap(tt.in)(func(k int, v string) bool {
96+
assert.Equal(t, tt.in[k], v)
97+
outKeys = append(outKeys, k)
98+
return true
99+
})
100+
101+
assert.Equal(t, tt.outKeys, outKeys)
102+
})
103+
}
104+
}

0 commit comments

Comments
 (0)