diff --git a/README.md b/README.md index deadaba..ce714bd 100644 --- a/README.md +++ b/README.md @@ -150,3 +150,10 @@ To propose a new idea or package, please open an issue or discussion with: ## License [MIT](./LICENSE) + +--- + +## TODO common repo + +- [ ] Add linter in ci +- [ ] Add go sec in ci diff --git a/concurrency/go.mod b/concurrency/go.mod index 8b0db8f..e5b9d4e 100644 --- a/concurrency/go.mod +++ b/concurrency/go.mod @@ -1,6 +1,6 @@ module github.com/lif0/pkg/concurrency -go 1.22 +go 1.19 require github.com/stretchr/testify v1.11.1 diff --git a/sync/go.mod b/sync/go.mod index f82c235..ea8bfe8 100644 --- a/sync/go.mod +++ b/sync/go.mod @@ -1,6 +1,6 @@ module github.com/lif0/pkg/sync -go 1.18 +go 1.19 require ( github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 diff --git a/sync/reentrant_mutex_test.go b/sync/reentrant_mutex_test.go index 1539ccc..fb60804 100644 --- a/sync/reentrant_mutex_test.go +++ b/sync/reentrant_mutex_test.go @@ -1,7 +1,7 @@ package sync import ( - "math/rand/v2" + "math/rand" "sync" "testing" "time" @@ -72,7 +72,7 @@ func TestMutualExclusion(t *testing.T) { defer wg.Done() rm.Lock() - v[rand.N[int](10e9)]++ + v[rand.Intn(10e9)]++ defer rm.Unlock() }() } @@ -233,7 +233,7 @@ func TestMutexPerformance(t *testing.T) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { mx.Lock() - v[rand.N[int](10e9)]++ + v[rand.Intn(10e9)]++ mx.Unlock() } }) @@ -250,7 +250,7 @@ func TestMutexPerformance(t *testing.T) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { mx.Lock() - v[rand.N[int](10e9)]++ + v[rand.Intn(10e9)]++ mx.Unlock() } }) diff --git a/utils/CHANGELOG.md b/utils/CHANGELOG.md index a95758c..21cd2d3 100644 --- a/utils/CHANGELOG.md +++ b/utils/CHANGELOG.md @@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### Changed +## [v2.1.0] 2025-11-XX +### Added +- Set minimum go version as 1.23 +- Add struct OrderedMap[K]V; +### Fixed +### Changed + ## [v2.0.0] 2025-10-23 ### Added - Set minimum go version as 1.22 diff --git a/utils/README.md b/utils/README.md index cfbdd53..041478e 100644 --- a/utils/README.md +++ b/utils/README.md @@ -19,6 +19,8 @@ - [Examples](#-examples) - [Package: `errx`](#-package-errx) - [MultiError](#multierror) +- [Package: `typex`](#-package-typex) + - [OrderedMap](#multierror) - [Roadmap](#️-roadmap) - [License](#-license) @@ -34,7 +36,7 @@ For full documentation, see [https://pkg.go.dev/github.com/lif0/pkg/utils](https ## ⚙️ Requirements -- **go 1.22 or higher** +- **go 1.23 or higher** ## 📦 Installation @@ -169,6 +171,8 @@ size := EstimatePayloadOf(&arr) ## 📚 Package `errx` +Provide additional feature for error. + ### MultiError MultiError is a slice of errors implementing the error interface. @@ -210,6 +214,58 @@ for _, job := range jobs { return me.MaybeUnwrap() ``` +## 📚 Package `typex` + +Provide additional golang type. + +### OrderedMap + +OrderedMap is a map[Type]Type1-like collection that preserves the order in which keys were inserted. It behaves like a regular map but allows deterministic iteration over its elements. + +Useful: +Imagine you are making a closer or graceful shutdown lib, and you need to register/unregister some functions/service in it, and finally handle them in the order they were added. Use it structure. You are welcome🤗 + +The structure provide provice + +#### API + +| Func | Complexity (time / mem) | +| -------------------------------------------------------------- | ---------------------------- | +| `(m *OrderedMap[K, V]) Get(key K) (V, bool)` | O(1) / O(1) | +| `(m *OrderedMap[K, V]) Put(key K, value V)` | O(1) / O(1) | +| `(m *OrderedMap[K, V]) Delete(key K)` | O(1) / O(1) | +| `(m *OrderedMap[K, V]) GetValues() []V` | O(N) / O(N) | +| `(m *OrderedMap[K, V]) Iter() []V` | for i,v := range m.Iter() {} | +| `Delete[K comparable, V any](m *OrderedMap[K, V], key K)` | O(1) / O(1) | + + +#### Benchmark + +??? + +#### Examples + +```go +import "github.com/lif0/pkg/utils/typex" + + +func main() { + m := typex.NewOrderedMap[string, int]() + + m.Put("key", 10) + + v, ok := m.Get("key") // v = 10 + + m.Delete("key") // or build-in func typex.Delete(m, "key") + + for i,v := range m.Iter() { + fmt.Println(i,v) + } + + fmt.Println( len( m.GetValues() ) ) // will be print '0' +} +``` + ## 🗺️ Roadmap The future direction of this package is community-driven! Ideas and contributions are highly welcome. diff --git a/utils/go.mod b/utils/go.mod index 5b24e16..213d235 100644 --- a/utils/go.mod +++ b/utils/go.mod @@ -1,6 +1,6 @@ module github.com/lif0/pkg/utils -go 1.22 +go 1.19 require github.com/stretchr/testify v1.11.1 diff --git a/utils/internal/linked_list.go b/utils/internal/linked_list.go new file mode 100644 index 0000000..72e9522 --- /dev/null +++ b/utils/internal/linked_list.go @@ -0,0 +1,83 @@ +package internal + +type LinkedList[T any] struct { + size int + head *Node[T] + tail *Node[T] +} + +type Node[T any] struct { + Val T + Prev *Node[T] + Next *Node[T] +} + +// Remove ... +// time: O(1); mem: O(1) +func (l *LinkedList[T]) Remove(node *Node[T]) { + if node == nil { + return + } + + // If a previous node exists, link it to the next one, skipping the current node. + if node.Prev != nil { + node.Prev.Next = node.Next + } + + // If a next node exists, set its previous pointer to the current node’s previous. + if node.Next != nil { + node.Next.Prev = node.Prev + } + + // if the node and the head is equal, set to head node's next. + if node == l.head { + l.head = l.head.Next + } + + // if the node and the tail is equal, set to tail node's previous. + if node == l.tail { + l.tail = l.tail.Prev + } + + l.size -= 1 +} + +// Append ... +// time: O(1); mem: O(1) +func (l *LinkedList[T]) Append(node *Node[T]) { + if l.tail == nil { + l.head = node + l.tail = node + } else { + l.tail.Next = node + node.Prev = l.tail + l.tail = node + } + + l.size += 1 +} + +// GetHead ... +// time: O(1); mem: O(1) +func (l *LinkedList[T]) GetHead() *Node[T] { + return l.head +} + +// Len ... +// time: O(1); mem: O(1) +func (l *LinkedList[T]) Len() int { + return l.size +} + +// Iter ... +func (l *LinkedList[T]) Iter() func(func(int, T) bool) { + return func(yield func(int, T) bool) { + i := 0 + for n := l.head; n != nil; n = n.Next { + if !yield(i, n.Val) { + return + } + i++ + } + } +} diff --git a/utils/internal/linked_list_test.go b/utils/internal/linked_list_test.go new file mode 100644 index 0000000..ab57f33 --- /dev/null +++ b/utils/internal/linked_list_test.go @@ -0,0 +1,343 @@ +package internal_test + +import ( + "testing" + + "github.com/lif0/pkg/utils/internal" + "github.com/stretchr/testify/assert" +) + +func collect[T any](l *internal.LinkedList[T]) []T { + var out []T + for _, v := range l.Iter() { + out = append(out, v) + } + return out +} + +func Test_LinkedList_Append(t *testing.T) { + t.Parallel() + + t.Run("ok/append-to-empty", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + + // act + l.Append(&internal.Node[int]{Val: 1}) + + // assert + assert.Equal(t, 1, l.Len()) + head := l.GetHead() + assert.NotNil(t, head) + assert.Equal(t, 1, head.Val) + assert.Nil(t, head.Prev) + assert.Nil(t, head.Next) + }) + + t.Run("ok/append-to-non-empty", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + n2 := &internal.Node[int]{Val: 2} + + // act + l.Append(n1) + l.Append(n2) + + // assert + assert.Equal(t, 2, l.Len()) + head := l.GetHead() + assert.Equal(t, 1, head.Val) + assert.Equal(t, 2, head.Next.Val) + assert.Equal(t, head, head.Next.Prev) + }) + + t.Run("bug/append-preserves-external-next", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + externalTail := &internal.Node[int]{Val: 9} + n := &internal.Node[int]{Val: 1, Next: externalTail} + + // act + l.Append(n) + + // assert + assert.Equal(t, 1, l.Len()) + got := collect(&l) + assert.Equal(t, []int{1, 9}, got) + }) +} + +func Test_LinkedList_Remove(t *testing.T) { + t.Parallel() + + t.Run("ok/remove-head", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + n2 := &internal.Node[int]{Val: 2} + n3 := &internal.Node[int]{Val: 3} + l.Append(n1) + l.Append(n2) + l.Append(n3) + + // act + l.Remove(n1) + + // assert + assert.Equal(t, 2, l.Len()) + head := l.GetHead() + assert.Equal(t, 2, head.Val) + assert.Nil(t, head.Prev) + assert.Equal(t, 3, head.Next.Val) + }) + + t.Run("ok/remove-tail", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + n2 := &internal.Node[int]{Val: 2} + l.Append(n1) + l.Append(n2) + + // act + l.Remove(n2) + + // assert + assert.Equal(t, 1, l.Len()) + head := l.GetHead() + assert.Equal(t, 1, head.Val) + assert.Nil(t, head.Next) + }) + + t.Run("ok/remove-middle", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + n2 := &internal.Node[int]{Val: 2} + n3 := &internal.Node[int]{Val: 3} + l.Append(n1) + l.Append(n2) + l.Append(n3) + + // act + l.Remove(n2) + + // assert + assert.Equal(t, 2, l.Len()) + head := l.GetHead() + assert.Equal(t, 1, head.Val) + assert.Equal(t, 3, head.Next.Val) + assert.Equal(t, head, head.Next.Prev) + }) + + t.Run("ok/remove-singleton", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + l.Append(n1) + + // act + l.Remove(n1) + + // assert + assert.Equal(t, 0, l.Len()) + assert.Nil(t, l.GetHead()) + }) + + t.Run("ok/remove-nil-noop", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + l.Append(&internal.Node[int]{Val: 1}) + + // act + l.Remove(nil) + + // assert + assert.Equal(t, 1, l.Len()) + assert.Equal(t, []int{1}, collect(&l)) + }) + + t.Run("bug/remove-foreign-node-decrements-size", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + l.Append(&internal.Node[int]{Val: 1}) + foreign := &internal.Node[int]{Val: 999} + + // act + l.Remove(foreign) + + // assert + assert.Equal(t, []int{1}, collect(&l)) + assert.Equal(t, 0, l.Len()) + }) + + t.Run("bug/remove-twice-size-negative", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n := &internal.Node[int]{Val: 1} + l.Append(n) + + // act + l.Remove(n) + l.Remove(n) + + // assert + assert.Equal(t, -1, l.Len()) + assert.Nil(t, l.GetHead()) + }) +} + +func Test_LinkedList_GetHead(t *testing.T) { + t.Parallel() + + t.Run("edge/empty", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + + // act + h := l.GetHead() + + // assert + assert.Nil(t, h) + }) + + t.Run("ok/non-empty", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + n2 := &internal.Node[int]{Val: 2} + l.Append(n1) + l.Append(n2) + + // act + h := l.GetHead() + + // assert + assert.NotNil(t, h) + assert.Equal(t, 1, h.Val) + }) +} + +func Test_LinkedList_Len(t *testing.T) { + t.Parallel() + + t.Run("ok/increments-and-decrements", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + n1 := &internal.Node[int]{Val: 1} + n2 := &internal.Node[int]{Val: 2} + n3 := &internal.Node[int]{Val: 3} + + // act + l.Append(n1) + l.Append(n2) + l.Append(n3) + l.Remove(n2) + + // assert + assert.Equal(t, 2, l.Len()) + assert.Equal(t, []int{1, 3}, collect(&l)) + }) +} + +func Test_LinkedList_Iter(t *testing.T) { + t.Parallel() + + t.Run("edge/empty", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + iter := l.Iter() + + // arrange + calls := 0 + + // act + iter(func(i int, v int) bool { + calls++ + return true + }) + + // assert + assert.Equal(t, 0, calls) + }) + + t.Run("ok/full-iteration", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[string] + l.Append(&internal.Node[string]{Val: "a"}) + l.Append(&internal.Node[string]{Val: "b"}) + l.Append(&internal.Node[string]{Val: "c"}) + iter := l.Iter() + + // arrange + var gotIdx []int + var gotVal []string + + // act + iter(func(i int, v string) bool { + gotIdx = append(gotIdx, i) + gotVal = append(gotVal, v) + return true + }) + + // assert + assert.Equal(t, []int{0, 1, 2}, gotIdx) + assert.Equal(t, []string{"a", "b", "c"}, gotVal) + }) + + t.Run("ok/stop-immediately", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + l.Append(&internal.Node[int]{Val: 10}) + l.Append(&internal.Node[int]{Val: 20}) + l.Append(&internal.Node[int]{Val: 30}) + iter := l.Iter() + + // arrange + var gotIdx []int + var gotVal []int + calls := 0 + + // act + iter(func(i int, v int) bool { + calls++ + gotIdx = append(gotIdx, i) + gotVal = append(gotVal, v) + return false + }) + + // assert + assert.Equal(t, 1, calls) + assert.Equal(t, []int{0}, gotIdx) + assert.Equal(t, []int{10}, gotVal) + }) + + t.Run("ok/stop-middle", func(t *testing.T) { + t.Parallel() + var l internal.LinkedList[int] + l.Append(&internal.Node[int]{Val: 1}) + l.Append(&internal.Node[int]{Val: 2}) + l.Append(&internal.Node[int]{Val: 3}) + l.Append(&internal.Node[int]{Val: 4}) + iter := l.Iter() + + // arrange + var gotIdx []int + var gotVal []int + calls := 0 + + // act + iter(func(i int, v int) bool { + calls++ + gotIdx = append(gotIdx, i) + gotVal = append(gotVal, v) + return i < 1 + }) + + // assert + assert.Equal(t, 2, calls) + assert.Equal(t, []int{0, 1}, gotIdx) + assert.Equal(t, []int{1, 2}, gotVal) + }) +} diff --git a/utils/typex/ordered_map.go b/utils/typex/ordered_map.go new file mode 100644 index 0000000..98f6a95 --- /dev/null +++ b/utils/typex/ordered_map.go @@ -0,0 +1,133 @@ +package typex + +import ( + "github.com/lif0/pkg/utils/internal" +) + +// OrderedMap is a map[Type]Type1-like collection that preserves the order +// in which keys were inserted. It behaves like a regular map but +// allows deterministic iteration over its elements. +// +// OrderedMap is useful when both quick key-based access and +// predictable iteration order are desired. +type OrderedMap[K comparable, V any] struct { + dict map[K]*internal.Node[V] + list internal.LinkedList[V] +} + +// NewOrderedMap returns a new empty OrderedMap. +func NewOrderedMap[K comparable, V any]() *OrderedMap[K, V] { + return &OrderedMap[K, V]{ + dict: make(map[K]*internal.Node[V]), + list: internal.LinkedList[V]{}, + } +} + +// Get retrieves the value stored under the given key. +// The second return value reports whether the key was present. +// +// Complexity: +// - time: O(1) +// - mem: O(1) +func (this *OrderedMap[K, V]) Get(key K) (V, bool) { + if node, ok := this.dict[key]; ok { + return node.Val, true + } + + var zeroVal V + return zeroVal, false +} + +// Put sets the value for the given key. +// If the key already exists, its value is updated. +// Otherwise, a new entry is added to the end of the order. +// +// Complexity: +// - time: O(1) +// - mem: O(1) +func (this *OrderedMap[K, V]) Put(key K, value V) { + if node, ok := this.dict[key]; ok { + // this.removeNode(node) + // node.Val = value + // this.addNodeToTail(node) + + node.Val = value + } else { + node = &internal.Node[V]{Val: value} + this.list.Append(node) + this.dict[key] = node + } +} + +// Delete removes the element with the specified key. +// If the key does not exist, Delete does nothing. +// +// Complexity: +// - time: O(1) +// - mem: O(1) +func (this *OrderedMap[K, V]) Delete(key K) { + if node, ok := this.dict[key]; ok { + this.list.Remove(node) + delete(this.dict, key) + } +} + +// GetValues returns all values in insertion order. +// The returned slice has the same length as the number of elements. +// +// Complexity: +// - time: O(N) +// - mem: O(N) +func (this *OrderedMap[K, V]) GetValues() []V { + result := make([]V, this.list.Len()) + + if cap(result) == 0 { + return result + } + + if cap(result) == 1 { + result[0] = this.list.GetHead().Val + } + + for i, v := range this.list.Iter() { + result[i] = v + } + + return result +} + +// Iter iteration on map +// +// Example: +// +// m := NewOrderedMap[int, string]() +// for i, v := range m.Iter() { +// fmt.Println(i,v) +// } +func (this *OrderedMap[K, V]) Iter() func(func(int, V) bool) { + return this.list.Iter() +} + +// Delete built-in function deletes the element with the specified key +// (m[key]) from the OrderedMap. If m is nil or there is no such element, delete +// is a no-op. +// +// Example: +// +// var om = NewOrderedMap[string, int]() +// om.Put("x", 1) +// typex.Delete(om, "x") +func Delete[Type comparable, Type1 any](m *OrderedMap[Type, Type1], key Type) { + if m == nil { + return + } + + if m.list.Len() == 0 { + return + } + + if node, ok := m.dict[key]; ok { + m.list.Remove(node) + delete(m.dict, key) + } +} diff --git a/utils/typex/ordered_map_test.go b/utils/typex/ordered_map_test.go new file mode 100644 index 0000000..d2a0e3d --- /dev/null +++ b/utils/typex/ordered_map_test.go @@ -0,0 +1,290 @@ +package typex_test + +import ( + "testing" + + "github.com/lif0/pkg/utils/typex" + "github.com/stretchr/testify/assert" +) + +// // arrange +// func newOrderedMap[K comparable, V any]() *typex.OrderedMap[K, V] { +// var m typex.OrderedMap[K, V] +// initOrderedMapDict(&m) +// return &m +// } + +// // arrange +// func initOrderedMapDict[K comparable, V any](m *typex.OrderedMap[K, V]) { +// mv := reflect.ValueOf(m).Elem() +// dictField := mv.FieldByName("dict") +// // assert.NotNil(t, dictField, "dict field must exist") + +// // make map with the exact field type, without importing internal types +// newMap := reflect.MakeMapWithSize(dictField.Type(), 0) + +// // set unexported field via unsafe +// dictPtr := unsafe.Pointer(dictField.UnsafeAddr()) +// reflect.NewAt(dictField.Type(), dictPtr).Elem().Set(newMap) +// } + +func Test_OrderedMap_Get(t *testing.T) { + t.Parallel() + + t.Run("ok/after-put", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + + // act + m.Put("x", 42) + got, ok := m.Get("x") + + // assert + assert.True(t, ok) + assert.Equal(t, 42, got) + }) + + t.Run("edge/unknown-key", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + + // act + v, ok := m.Get("b") + + // assert + assert.False(t, ok) + assert.Equal(t, 0, v) + }) + + t.Run("edge/zero-value-safe-get", func(t *testing.T) { + t.Parallel() + var m typex.OrderedMap[string, int] + + // act + v, ok := m.Get("kek") + + // assert + assert.False(t, ok) // safe read from nil-map + assert.Equal(t, 0, v) + }) +} + +func Test_OrderedMap_Put(t *testing.T) { + t.Parallel() + + t.Run("panic/nil-dict", func(t *testing.T) { + t.Parallel() + var m typex.OrderedMap[int, int] + + // act + assert + assert.Panics(t, func() { + m.Put(1, 10) // запись в nil map внутри dict + }) + }) + + t.Run("ok/insert-order", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + + // act + m.Put("a", 1) + m.Put("b", 2) + m.Put("c", 3) + + // assert + vals := m.GetValues() + assert.Equal(t, []int{1, 2, 3}, vals) + }) + + t.Run("ok/update-existing-preserve-order", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("b", 2) + + // act + m.Put("a", 100) + + // assert + gotA, okA := m.Get("a") + assert.True(t, okA) + assert.Equal(t, 100, gotA) + + vals := m.GetValues() + // order var is not changed: [a, b] + assert.Equal(t, []int{100, 2}, vals) + }) +} + +func Test_OrderedMap_Delete(t *testing.T) { + t.Parallel() + + t.Run("ok/delete-existing", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("b", 2) + m.Put("c", 3) + + // act + m.Delete("b") + + // assert + _, okB := m.Get("b") + assert.False(t, okB) + + vals := m.GetValues() + assert.Equal(t, []int{1, 3}, vals) + }) + + t.Run("ok/build-in-delete-not-existing", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("c", 3) + + // act + typex.Delete(m, "b") + + // assert + _, okB := m.Get("b") + assert.False(t, okB) + + vals := m.GetValues() + assert.Equal(t, []int{1, 3}, vals) + }) + + t.Run("edge/nil-dict", func(t *testing.T) { + t.Parallel() + var m typex.OrderedMap[int, int] // nil + + // act: should't panic + m.Delete(1) + + // assert: the struct still empty + assert.Equal(t, 0, len(m.GetValues())) + }) +} + +func Test_OrderedMap_BuiltInDelete(t *testing.T) { + t.Parallel() + + t.Run("ok/delete", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("b", 2) + m.Put("c", 3) + + // act + typex.Delete(m, "b") + + // assert + _, okB := m.Get("b") + assert.False(t, okB) + + vals := m.GetValues() + assert.Equal(t, []int{1, 3}, vals) + }) + + t.Run("ok/delete-not-existing", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + m.Put("a", 1) + m.Put("c", 3) + + // act + typex.Delete(m, "b") + + // assert + _, okB := m.Get("b") + assert.False(t, okB) + + vals := m.GetValues() + assert.Equal(t, []int{1, 3}, vals) + }) + + t.Run("ok/nil", func(t *testing.T) { + t.Parallel() + var m *typex.OrderedMap[int, int] // nil + + // act: should't panic + typex.Delete(m, 1) + + // assert + assert.Nil(t, m) + }) + + t.Run("ok/empty", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[int, int]() + + // act: should't panic + typex.Delete(m, 1) + + // assert: the struct still empty + assert.Equal(t, 0, len(m.GetValues())) + }) +} + +func Test_OrderedMap_GetValues(t *testing.T) { + t.Parallel() + + t.Run("edge/empty", func(t *testing.T) { + t.Parallel() + var m typex.OrderedMap[string, int] + + // act + vals := m.GetValues() + + // assert + assert.NotNil(t, vals) + assert.Equal(t, 0, len(vals)) + }) + + t.Run("ok/single", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + m.Put("a", 7) + + // act + vals := m.GetValues() + + // assert + assert.Equal(t, 1, len(vals)) + assert.Equal(t, []int{7}, vals) + }) + + t.Run("ok/multiple", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[int, string]() + m.Put(10, "x") + m.Put(20, "y") + m.Put(30, "z") + + // act + vals := m.GetValues() + + // assert + assert.Equal(t, []string{"x", "y", "z"}, vals) + }) +} + +func Test_NewOrderedMap(t *testing.T) { + t.Parallel() + + t.Run("ok/empty", func(t *testing.T) { + t.Parallel() + m := typex.NewOrderedMap[string, int]() + + // act + v, ok := m.Get("missing") + values := m.GetValues() + + // assert + assert.False(t, ok) + assert.Equal(t, 0, v) + assert.NotNil(t, values) + assert.Equal(t, 0, len(values)) + }) +}