Skip to content

Commit b16eb97

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 4cd941f commit b16eb97

File tree

2 files changed

+75
-0
lines changed

2 files changed

+75
-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
)
@@ -182,3 +184,27 @@ func RemoveNils[T any](slice []*T) []*T {
182184
return ptr == nil
183185
})
184186
}
187+
188+
// IterateOrderedMap implements iter.Seq2 to iterate over a map in the key's order.
189+
//
190+
// This function returns a func yielding key-value-pairs from a given map in the order of their keys, if their type
191+
// is cmp.Ordered.
192+
//
193+
// Please note that currently - being at Go 1.22 - rangefuncs are still an experimental feature and cannot be directly
194+
// used unless compiled with `GOEXPERIMENT=rangefunc`. However, they can still be invoked normally.
195+
// https://go.dev/wiki/RangefuncExperiment
196+
func IterateOrderedMap[K cmp.Ordered, V any](m map[K]V) func(func(K, V) bool) {
197+
keys := make([]K, 0, len(m))
198+
for key := range m {
199+
keys = append(keys, key)
200+
}
201+
slices.Sort(keys)
202+
203+
return func(yield func(K, V) bool) {
204+
for _, key := range keys {
205+
if !yield(key, m[key]) {
206+
return
207+
}
208+
}
209+
}
210+
}

utils/utils_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,52 @@ func TestRemoveNils(t *testing.T) {
7575
})
7676
}
7777
}
78+
79+
func TestIterateOrderedMap(t *testing.T) {
80+
tests := []struct {
81+
name string
82+
in map[int]string
83+
outKeys []int
84+
}{
85+
{"empty", map[int]string{}, nil},
86+
{"single", map[int]string{1: "foo"}, []int{1}},
87+
{"few-numbers", map[int]string{1: "a", 2: "b", 3: "c"}, []int{1, 2, 3}},
88+
{
89+
"1k-numbers",
90+
func() map[int]string {
91+
m := make(map[int]string)
92+
for i := 0; i < 1000; i++ {
93+
m[i] = "foo"
94+
}
95+
return m
96+
}(),
97+
func() []int {
98+
keys := make([]int, 1000)
99+
for i := 0; i < 1000; i++ {
100+
keys[i] = i
101+
}
102+
return keys
103+
}(),
104+
},
105+
}
106+
107+
for _, tt := range tests {
108+
t.Run(tt.name, func(t *testing.T) {
109+
var outKeys []int
110+
111+
// Either run with GOEXPERIMENT=rangefunc or wait for rangefuncs to land in the next Go release.
112+
// for k, _ := range IterateOrderedMap(tt.in) {
113+
// outKeys = append(outKeys, k)
114+
// }
115+
116+
// In the meantime, it can be invoked as follows.
117+
IterateOrderedMap(tt.in)(func(k int, v string) bool {
118+
assert.Equal(t, tt.in[k], v)
119+
outKeys = append(outKeys, k)
120+
return true
121+
})
122+
123+
assert.Equal(t, tt.outKeys, outKeys)
124+
})
125+
}
126+
}

0 commit comments

Comments
 (0)