From f4b4978e62e0983e8b015c7838030fbd2a745cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Schw=C3=B6rer?= Date: Sat, 30 May 2026 00:07:10 +0200 Subject: [PATCH] v0.0.643 OrderedMap --- dataext/orderedMap.go | 164 +++++++++++++++++ dataext/orderedMap_test.go | 358 +++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 2 + goextVersion.go | 4 +- 5 files changed, 527 insertions(+), 3 deletions(-) create mode 100644 dataext/orderedMap.go create mode 100644 dataext/orderedMap_test.go diff --git a/dataext/orderedMap.go b/dataext/orderedMap.go new file mode 100644 index 0000000..e1a29f8 --- /dev/null +++ b/dataext/orderedMap.go @@ -0,0 +1,164 @@ +package dataext + +import ( + "fmt" + "iter" +) + +// OrderedMap is like a normal map[TKey, TVal] - but its elements stay in order +type OrderedMap[TKey comparable, TVal any] struct { + m map[TKey]*TVal + a []TKey +} + +func NewOrderedMap[TKey comparable, TVal any](cap int) *OrderedMap[TKey, TVal] { + return &OrderedMap[TKey, TVal]{ + m: make(map[TKey]*TVal, cap), + a: make([]TKey, 0, cap), + } +} + +func (o *OrderedMap[TKey, TVal]) Get(key TKey) (TVal, bool) { + v, ok := o.m[key] + if ok { + return *v, ok + } + + return *new(TVal), false +} + +func (o *OrderedMap[TKey, TVal]) GetOrNil(key TKey) *TVal { + v, ok := o.m[key] + if ok { + return v + } + + return nil +} + +func (o *OrderedMap[TKey, TVal]) GetOrDefault(key TKey, defaultVal TVal) TVal { + v, ok := o.m[key] + if ok { + return *v + } + + return defaultVal +} + +// Add adds the new value to the map +// At the end of the map-ordering (even if key already exists, its then "moved" to the end) +// returns true if key already existed +func (o *OrderedMap[TKey, TVal]) Add(key TKey, val TVal) bool { + if _, ok := o.m[key]; ok { + o.remFromArray(key) + o.m[key] = &val + o.a = append(o.a, key) + return true + } + + o.m[key] = &val + o.a = append(o.a, key) + return false +} + +// AddOrReplace adds the new value to the map +// Normally at the end of the map, but if teh key already exists, its only replaced +// returns true if key already existed +func (o *OrderedMap[TKey, TVal]) AddOrReplace(key TKey, val TVal) bool { + if _, ok := o.m[key]; ok { + o.m[key] = &val + return true + } + + o.m[key] = &val + o.a = append(o.a, key) + return false +} + +func (o *OrderedMap[TKey, TVal]) Remove(key TKey) bool { + if _, ok := o.m[key]; ok { + o.remFromArray(key) + delete(o.m, key) + return true + } + + return false +} + +func (o *OrderedMap[TKey, TVal]) Iterate() iter.Seq[TVal] { + return func(yield func(TVal) bool) { + for _, v := range o.a { + if !yield(*o.m[v]) { + return + } + } + } +} + +func (o *OrderedMap[TKey, TVal]) IterateKeys() iter.Seq[TKey] { + return func(yield func(TKey) bool) { + for _, v := range o.a { + if !yield(v) { + return + } + } + } +} + +func (o *OrderedMap[TKey, TVal]) Array() []TVal { + res := make([]TVal, len(o.a)) + for i, v := range o.a { + res[i] = *o.m[v] + } + + return res +} + +func (o *OrderedMap[TKey, TVal]) Keys() []TKey { + res := make([]TKey, len(o.a)) + for i, v := range o.a { + res[i] = v + } + + return res +} + +func (o *OrderedMap[TKey, TVal]) Clear() { + mapCap := max(len(o.m), cap(o.a)) + o.m = make(map[TKey]*TVal, mapCap) + o.a = make([]TKey, 0, mapCap) +} + +func (o *OrderedMap[TKey, TVal]) Size() int { + return len(o.a) +} + +func (o *OrderedMap[TKey, TVal]) Capacity() int { + return min(cap(o.a), len(o.m)) +} + +func (o *OrderedMap[TKey, TVal]) Contains(key TKey) bool { + _, ok := o.m[key] + return ok +} + +func (o *OrderedMap[TKey, TVal]) IndexOf(key TKey) int { + for i, v := range o.a { + if v == key { + return i + } + } + + return -1 +} + +func (o *OrderedMap[TKey, TVal]) remFromArray(key TKey) { + for i, v := range o.a { + if v == key { + o.a = append(o.a[:i], o.a[i+1:]...) + return + } + } + + panic(fmt.Sprintf("Failed to remove key from OrderedMap -- key '%v' not found", key)) +} diff --git a/dataext/orderedMap_test.go b/dataext/orderedMap_test.go new file mode 100644 index 0000000..4777149 --- /dev/null +++ b/dataext/orderedMap_test.go @@ -0,0 +1,358 @@ +package dataext + +import ( + "slices" + "testing" + + "git.blackforestbytes.com/BlackForestBytes/goext/tst" +) + +func TestOrderedMapNew(t *testing.T) { + m := NewOrderedMap[string, int](4) + + tst.AssertEqual(t, m.Size(), 0) + tst.AssertArrayEqual(t, m.Keys(), []string{}) + tst.AssertArrayEqual(t, m.Array(), []int{}) +} + +func TestOrderedMapAddAndGet(t *testing.T) { + m := NewOrderedMap[string, int](0) + + tst.AssertFalse(t, m.Add("a", 1)) + tst.AssertFalse(t, m.Add("b", 2)) + tst.AssertFalse(t, m.Add("c", 3)) + + tst.AssertEqual(t, m.Size(), 3) + + v, ok := m.Get("a") + tst.AssertTrue(t, ok) + tst.AssertEqual(t, v, 1) + + v, ok = m.Get("b") + tst.AssertTrue(t, ok) + tst.AssertEqual(t, v, 2) + + v, ok = m.Get("c") + tst.AssertTrue(t, ok) + tst.AssertEqual(t, v, 3) + + v, ok = m.Get("missing") + tst.AssertFalse(t, ok) + tst.AssertEqual(t, v, 0) +} + +func TestOrderedMapOrderPreserved(t *testing.T) { + m := NewOrderedMap[string, int](0) + + m.Add("first", 1) + m.Add("second", 2) + m.Add("third", 3) + m.Add("fourth", 4) + + tst.AssertArrayEqual(t, m.Keys(), []string{"first", "second", "third", "fourth"}) + tst.AssertArrayEqual(t, m.Array(), []int{1, 2, 3, 4}) +} + +func TestOrderedMapAddMovesExistingToEnd(t *testing.T) { + m := NewOrderedMap[string, int](0) + + m.Add("a", 1) + m.Add("b", 2) + m.Add("c", 3) + + tst.AssertTrue(t, m.Add("a", 10)) + + tst.AssertArrayEqual(t, m.Keys(), []string{"b", "c", "a"}) + tst.AssertArrayEqual(t, m.Array(), []int{2, 3, 10}) + + v, ok := m.Get("a") + tst.AssertTrue(t, ok) + tst.AssertEqual(t, v, 10) +} + +func TestOrderedMapAddOrReplaceKeepsOrder(t *testing.T) { + m := NewOrderedMap[string, int](0) + + m.Add("a", 1) + m.Add("b", 2) + m.Add("c", 3) + + tst.AssertTrue(t, m.AddOrReplace("b", 99)) + + tst.AssertArrayEqual(t, m.Keys(), []string{"a", "b", "c"}) + tst.AssertArrayEqual(t, m.Array(), []int{1, 99, 3}) + + v, ok := m.Get("b") + tst.AssertTrue(t, ok) + tst.AssertEqual(t, v, 99) +} + +func TestOrderedMapAddOrReplaceNew(t *testing.T) { + m := NewOrderedMap[string, int](0) + + tst.AssertFalse(t, m.AddOrReplace("a", 1)) + tst.AssertFalse(t, m.AddOrReplace("b", 2)) + + tst.AssertArrayEqual(t, m.Keys(), []string{"a", "b"}) + tst.AssertArrayEqual(t, m.Array(), []int{1, 2}) +} + +func TestOrderedMapGetOrNil(t *testing.T) { + m := NewOrderedMap[string, int](0) + m.Add("a", 42) + + v := m.GetOrNil("a") + if v == nil { + t.Errorf("expected non-nil pointer") + return + } + tst.AssertEqual(t, *v, 42) + + v = m.GetOrNil("missing") + if v != nil { + t.Errorf("expected nil pointer") + } +} + +func TestOrderedMapGetOrDefault(t *testing.T) { + m := NewOrderedMap[string, int](0) + m.Add("a", 42) + + tst.AssertEqual(t, m.GetOrDefault("a", -1), 42) + tst.AssertEqual(t, m.GetOrDefault("missing", -1), -1) +} + +func TestOrderedMapRemove(t *testing.T) { + m := NewOrderedMap[string, int](0) + + m.Add("a", 1) + m.Add("b", 2) + m.Add("c", 3) + + tst.AssertTrue(t, m.Remove("b")) + + tst.AssertEqual(t, m.Size(), 2) + tst.AssertArrayEqual(t, m.Keys(), []string{"a", "c"}) + tst.AssertArrayEqual(t, m.Array(), []int{1, 3}) + + _, ok := m.Get("b") + tst.AssertFalse(t, ok) + tst.AssertFalse(t, m.Contains("b")) +} + +func TestOrderedMapRemoveMissing(t *testing.T) { + m := NewOrderedMap[string, int](0) + + m.Add("a", 1) + tst.AssertFalse(t, m.Remove("missing")) + + tst.AssertEqual(t, m.Size(), 1) + tst.AssertArrayEqual(t, m.Keys(), []string{"a"}) +} + +func TestOrderedMapRemoveFirstAndLast(t *testing.T) { + m := NewOrderedMap[string, int](0) + + m.Add("a", 1) + m.Add("b", 2) + m.Add("c", 3) + m.Add("d", 4) + + tst.AssertTrue(t, m.Remove("a")) + tst.AssertTrue(t, m.Remove("d")) + + tst.AssertArrayEqual(t, m.Keys(), []string{"b", "c"}) + tst.AssertArrayEqual(t, m.Array(), []int{2, 3}) +} + +func TestOrderedMapContains(t *testing.T) { + m := NewOrderedMap[string, int](0) + m.Add("a", 1) + + tst.AssertTrue(t, m.Contains("a")) + tst.AssertFalse(t, m.Contains("b")) + + m.Remove("a") + tst.AssertFalse(t, m.Contains("a")) +} + +func TestOrderedMapIndexOf(t *testing.T) { + m := NewOrderedMap[string, int](0) + m.Add("a", 1) + m.Add("b", 2) + m.Add("c", 3) + + tst.AssertEqual(t, m.IndexOf("a"), 0) + tst.AssertEqual(t, m.IndexOf("b"), 1) + tst.AssertEqual(t, m.IndexOf("c"), 2) + tst.AssertEqual(t, m.IndexOf("missing"), -1) + + m.Add("a", 10) // moves to end + tst.AssertEqual(t, m.IndexOf("a"), 2) + tst.AssertEqual(t, m.IndexOf("b"), 0) + tst.AssertEqual(t, m.IndexOf("c"), 1) +} + +func TestOrderedMapClear(t *testing.T) { + m := NewOrderedMap[string, int](0) + m.Add("a", 1) + m.Add("b", 2) + m.Add("c", 3) + + m.Clear() + + tst.AssertEqual(t, m.Size(), 0) + tst.AssertArrayEqual(t, m.Keys(), []string{}) + tst.AssertArrayEqual(t, m.Array(), []int{}) + tst.AssertFalse(t, m.Contains("a")) + + m.Add("x", 99) + tst.AssertEqual(t, m.Size(), 1) + tst.AssertArrayEqual(t, m.Keys(), []string{"x"}) +} + +func TestOrderedMapSize(t *testing.T) { + m := NewOrderedMap[string, int](0) + tst.AssertEqual(t, m.Size(), 0) + + m.Add("a", 1) + tst.AssertEqual(t, m.Size(), 1) + + m.Add("b", 2) + tst.AssertEqual(t, m.Size(), 2) + + m.Add("a", 10) // replaces, size stays + tst.AssertEqual(t, m.Size(), 2) + + m.AddOrReplace("b", 20) // replaces, size stays + tst.AssertEqual(t, m.Size(), 2) + + m.Remove("a") + tst.AssertEqual(t, m.Size(), 1) +} + +func TestOrderedMapIterate(t *testing.T) { + m := NewOrderedMap[string, int](0) + m.Add("a", 1) + m.Add("b", 2) + m.Add("c", 3) + + got := make([]int, 0) + for v := range m.Iterate() { + got = append(got, v) + } + + tst.AssertArrayEqual(t, got, []int{1, 2, 3}) +} + +func TestOrderedMapIterateBreak(t *testing.T) { + m := NewOrderedMap[string, int](0) + m.Add("a", 1) + m.Add("b", 2) + m.Add("c", 3) + + got := make([]int, 0) + for v := range m.Iterate() { + got = append(got, v) + if v == 2 { + break + } + } + + tst.AssertArrayEqual(t, got, []int{1, 2}) +} + +func TestOrderedMapIterateKeys(t *testing.T) { + m := NewOrderedMap[string, int](0) + m.Add("a", 1) + m.Add("b", 2) + m.Add("c", 3) + + got := make([]string, 0) + for k := range m.IterateKeys() { + got = append(got, k) + } + + tst.AssertArrayEqual(t, got, []string{"a", "b", "c"}) +} + +func TestOrderedMapIterateKeysBreak(t *testing.T) { + m := NewOrderedMap[string, int](0) + m.Add("a", 1) + m.Add("b", 2) + m.Add("c", 3) + + got := make([]string, 0) + for k := range m.IterateKeys() { + got = append(got, k) + if k == "b" { + break + } + } + + tst.AssertArrayEqual(t, got, []string{"a", "b"}) +} + +func TestOrderedMapArrayIsCopy(t *testing.T) { + m := NewOrderedMap[string, int](0) + m.Add("a", 1) + m.Add("b", 2) + + arr := m.Array() + arr[0] = 999 + + tst.AssertArrayEqual(t, m.Array(), []int{1, 2}) +} + +func TestOrderedMapKeysIsCopy(t *testing.T) { + m := NewOrderedMap[string, int](0) + m.Add("a", 1) + m.Add("b", 2) + + keys := m.Keys() + keys[0] = "zzz" + + tst.AssertArrayEqual(t, m.Keys(), []string{"a", "b"}) +} + +func TestOrderedMapIntKey(t *testing.T) { + m := NewOrderedMap[int, string](0) + m.Add(3, "three") + m.Add(1, "one") + m.Add(2, "two") + + tst.AssertArrayEqual(t, m.Keys(), []int{3, 1, 2}) + tst.AssertArrayEqual(t, m.Array(), []string{"three", "one", "two"}) + + v, ok := m.Get(1) + tst.AssertTrue(t, ok) + tst.AssertEqual(t, v, "one") +} + +func TestOrderedMapStress(t *testing.T) { + m := NewOrderedMap[int, int](0) + + for i := 0; i < 100; i++ { + m.Add(i, i*10) + } + tst.AssertEqual(t, m.Size(), 100) + + for i := 0; i < 100; i++ { + v, ok := m.Get(i) + tst.AssertTrue(t, ok) + tst.AssertEqual(t, v, i*10) + } + + for i := 0; i < 50; i++ { + m.Remove(i * 2) + } + tst.AssertEqual(t, m.Size(), 50) + + expected := make([]int, 0, 50) + for i := 0; i < 50; i++ { + expected = append(expected, i*2+1) + } + keys := m.Keys() + slices.Sort(keys) + tst.AssertArrayEqual(t, keys, expected) +} diff --git a/go.mod b/go.mod index 6f78d14..2fa0cd0 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/klauspost/compress v1.18.6 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-colorable v0.1.15 // indirect github.com/mattn/go-isatty v0.0.22 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/go.sum b/go.sum index 53979ee..a29d99d 100644 --- a/go.sum +++ b/go.sum @@ -74,6 +74,8 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY= +github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs= github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4= github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4= diff --git a/goextVersion.go b/goextVersion.go index f6fc23f..60fa30f 100644 --- a/goextVersion.go +++ b/goextVersion.go @@ -1,5 +1,5 @@ package goext -const GoextVersion = "0.0.642" +const GoextVersion = "0.0.643" -const GoextVersionTimestamp = "2026-05-27T17:18:10+0200" +const GoextVersionTimestamp = "2026-05-30T00:07:10+0200"