diff --git a/README.md b/README.md index a3c129c..519faaf 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,30 @@ # 🔃 github.com/elliotchance/orderedmap [![GoDoc](https://godoc.org/github.com/elliotchance/orderedmap?status.svg)](https://godoc.org/github.com/elliotchance/orderedmap) [![Build Status](https://travis-ci.org/elliotchance/orderedmap.svg?branch=master)](https://travis-ci.org/elliotchance/orderedmap) -## Installation - -```bash -go get -u github.com/elliotchance/orderedmap -``` - ## Basic Usage An `*OrderedMap` is a high performance ordered map that maintains amortized O(1) for `Set`, `Get`, `Delete` and `Len`: ```go -m := orderedmap.NewOrderedMap() +import "github.com/elliotchance/orderedmap/v2" -m.Set("foo", "bar") -m.Set("qux", 1.23) -m.Set(123, true) +func main() { + m := orderedmap.NewOrderedMap[string, any]() -m.Delete("qux") + m.Set("foo", "bar") + m.Set("qux", 1.23) + m.Set("123", true) + + m.Delete("qux") +} ``` -Internally an `*OrderedMap` uses the composite type [map](https://go.dev/blog/maps) combined with a [linked list](https://pkg.go.dev/container/list) to maintain the order. +*Note: v2 requires Go v1.18 for generics.* If you need to support Go 1.17 or +below, you can use v1. + +Internally an `*OrderedMap` uses the composite type +[map](https://go.dev/blog/maps) combined with a +[linked list](https://pkg.go.dev/container/list) to maintain the order. ## Iterating @@ -54,49 +57,3 @@ beyond the first or last item. If the map is changing while the iteration is in-flight it may produce unexpected behavior. - -## Performance - -CPU: Intel(R) Core(TM) i5-8250U CPU @ 1.60GHz - -RAM: 8GB - -System: Windows 10 - -```shell -$go test -benchmem -run=^$ github.com/elliotchance/orderedmap -bench BenchmarkAll -``` - -map[int]bool - -| | map | orderedmap | -| ------- | ------------------- | ------------------- | -| set | 198 ns/op, 44 B/op | 722 ns/op, 211 B/op | -| get | 18 ns/op, 0 B/op | 37.3 ns/op, 0 B/op | -| delete | 888 ns/op, 211 B/op | 280 ns/op, 44 B/op | -| Iterate | 206 ns/op, 44 B/op | 693 ns/op, 259 B/op | - -map[string]bool(PS : Use strconv.Itoa()) - -| | map | orderedmap | -| ----------- | ------------------- | ----------------------- | -| set | 421 ns/op, 86 B/op | 1048 ns/op, 243 B/op | -| get | 81.1 ns/op, 2 B/op | 97.8 ns/op, 2 B/op | -| delete | 737 ns/op, 122 B/op | 1188 ns/op, 251 B/op | -| Iterate all | 14706 ns/op, 1 B/op | 52671 ns/op, 16391 B/op | - -Big map[int]bool (10000000 keys) - -| | map | orderedmap | -| ----------- | -------------------------------- | ------------------------------- | -| set all | 1.834559 s/op, 423.9470291 MB/op | 7.5564667 s/op, 1784.1483 MB/op | -| get all | 2.6367878 s/op, 423.9698 MB/op | 9.0232475 s/op, 1784.1086 MB/op | -| Iterate all | 1.9526784 s/op, 423.9042 MB/op | 8.2495265 s/op, 1936.7619 MB/op | - -Big map[string]bool (10000000 keys) - -| | map | orderedmap | -| ----------- | --------------------------------- | ----------------------------------- | -| set all | 4.8893923 s/op, 921.33435 MB/op | 10.4405527 s/op, 2089.0144 MB/op | -| get all | 7.122791 s/op, 997.3802643 MB/op | 13.2613692 s/op, 2165.09521 MB/op | -| Iterate all | 5.1688922 s/op, 921.4619293 MB/op | 12.6623711 s/op, 2241.5272064 MB/op | diff --git a/v2/element.go b/v2/element.go new file mode 100644 index 0000000..57d6222 --- /dev/null +++ b/v2/element.go @@ -0,0 +1,37 @@ +package orderedmap + +import ( + "container/list" + "golang.org/x/exp/constraints" +) + +type Element[K constraints.Ordered, V any] struct { + Key K + Value V + + element *list.Element +} + +func newElement[K constraints.Ordered, V any](e *list.Element) *Element[K, V] { + if e == nil { + return nil + } + + element := e.Value.(*orderedMapElement[K, V]) + + return &Element[K, V]{ + element: e, + Key: element.key, + Value: element.value, + } +} + +// Next returns the next element, or nil if it finished. +func (e *Element[K, V]) Next() *Element[K, V] { + return newElement[K, V](e.element.Next()) +} + +// Prev returns the previous element, or nil if it finished. +func (e *Element[K, V]) Prev() *Element[K, V] { + return newElement[K, V](e.element.Prev()) +} diff --git a/v2/element_test.go b/v2/element_test.go new file mode 100644 index 0000000..100d026 --- /dev/null +++ b/v2/element_test.go @@ -0,0 +1,67 @@ +package orderedmap_test + +import ( + "github.com/elliotchance/orderedmap/v2" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestElement_Key(t *testing.T) { + t.Run("Front", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, string]() + m.Set(1, "foo") + m.Set(2, "bar") + assert.Equal(t, 1, m.Front().Key) + }) + + t.Run("Back", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, string]() + m.Set(1, "foo") + m.Set(2, "bar") + assert.Equal(t, 2, m.Back().Key) + }) +} + +func TestElement_Value(t *testing.T) { + t.Run("Front", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, string]() + m.Set(1, "foo") + m.Set(2, "bar") + assert.Equal(t, "foo", m.Front().Value) + }) + + t.Run("Back", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, string]() + m.Set(1, "foo") + m.Set(2, "bar") + assert.Equal(t, "bar", m.Back().Value) + }) +} + +func TestElement_Next(t *testing.T) { + m := orderedmap.NewOrderedMap[int, string]() + m.Set(1, "foo") + m.Set(2, "bar") + m.Set(3, "baz") + + var results []any + for el := m.Front(); el != nil; el = el.Next() { + results = append(results, el.Key, el.Value) + } + + assert.Equal(t, []any{1, "foo", 2, "bar", 3, "baz"}, results) +} + +func TestElement_Prev(t *testing.T) { + m := orderedmap.NewOrderedMap[int, string]() + m.Set(1, "foo") + m.Set(2, "bar") + m.Set(3, "baz") + + var results []any + for el := m.Back(); el != nil; el = el.Prev() { + results = append(results, el.Key, el.Value) + } + + assert.Equal(t, []any{3, "baz", 2, "bar", 1, "foo"}, results) +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..20dded0 --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,11 @@ +module github.com/elliotchance/orderedmap/v2 + +go 1.18 + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.7.1 // indirect + golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/v2/go.sum b/v2/go.sum new file mode 100644 index 0000000..23a78ea --- /dev/null +++ b/v2/go.sum @@ -0,0 +1,12 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705 h1:ba9YlqfDGTTQ5aZ2fwOoQ1hf32QySyQkR6ODGDzHlnE= +golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v2/orderedmap.go b/v2/orderedmap.go new file mode 100644 index 0000000..f5c2f64 --- /dev/null +++ b/v2/orderedmap.go @@ -0,0 +1,154 @@ +package orderedmap + +import ( + "container/list" + "golang.org/x/exp/constraints" +) + +type orderedMapElement[K constraints.Ordered, V any] struct { + key K + value V +} + +type OrderedMap[K constraints.Ordered, V any] struct { + kv map[K]*list.Element + ll *list.List +} + +func NewOrderedMap[K constraints.Ordered, V any]() *OrderedMap[K, V] { + return &OrderedMap[K, V]{ + kv: make(map[K]*list.Element), + ll: list.New(), + } +} + +// Get returns the value for a key. If the key does not exist, the second return +// parameter will be false and the value will be nil. +func (m *OrderedMap[K, V]) Get(key K) (value V, ok bool) { + v, ok := m.kv[key] + if ok { + value = v.Value.(*orderedMapElement[K, V]).value + } + + return +} + +// Set will set (or replace) a value for a key. If the key was new, then true +// will be returned. The returned value will be false if the value was replaced +// (even if the value was the same). +func (m *OrderedMap[K, V]) Set(key K, value V) bool { + _, didExist := m.kv[key] + + if !didExist { + element := m.ll.PushBack(&orderedMapElement[K, V]{key, value}) + m.kv[key] = element + } else { + m.kv[key].Value.(*orderedMapElement[K, V]).value = value + } + + return !didExist +} + +// GetOrDefault returns the value for a key. If the key does not exist, returns +// the default value instead. +func (m *OrderedMap[K, V]) GetOrDefault(key K, defaultValue V) V { + if value, ok := m.kv[key]; ok { + return value.Value.(*orderedMapElement[K, V]).value + } + + return defaultValue +} + +// GetElement returns the element for a key. If the key does not exist, the +// pointer will be nil. +func (m *OrderedMap[K, V]) GetElement(key K) *Element[K, V] { + value, ok := m.kv[key] + if ok { + element := value.Value.(*orderedMapElement[K, V]) + return &Element[K, V]{ + element: value, + Key: element.key, + Value: element.value, + } + } + + return nil +} + +// Len returns the number of elements in the map. +func (m *OrderedMap[K, V]) Len() int { + return len(m.kv) +} + +// Keys returns all of the keys in the order they were inserted. If a key was +// replaced it will retain the same position. To ensure most recently set keys +// are always at the end you must always Delete before Set. +func (m *OrderedMap[K, V]) Keys() (keys []K) { + keys = make([]K, m.Len()) + + element := m.ll.Front() + for i := 0; element != nil; i++ { + keys[i] = element.Value.(*orderedMapElement[K, V]).key + element = element.Next() + } + + return keys +} + +// Delete will remove a key from the map. It will return true if the key was +// removed (the key did exist). +func (m *OrderedMap[K, V]) Delete(key K) (didDelete bool) { + element, ok := m.kv[key] + if ok { + m.ll.Remove(element) + delete(m.kv, key) + } + + return ok +} + +// Front will return the element that is the first (oldest Set element). If +// there are no elements this will return nil. +func (m *OrderedMap[K, V]) Front() *Element[K, V] { + front := m.ll.Front() + if front == nil { + return nil + } + + element := front.Value.(*orderedMapElement[K, V]) + + return &Element[K, V]{ + element: front, + Key: element.key, + Value: element.value, + } +} + +// Back will return the element that is the last (most recent Set element). If +// there are no elements this will return nil. +func (m *OrderedMap[K, V]) Back() *Element[K, V] { + back := m.ll.Back() + if back == nil { + return nil + } + + element := back.Value.(*orderedMapElement[K, V]) + + return &Element[K, V]{ + element: back, + Key: element.key, + Value: element.value, + } +} + +// Copy returns a new OrderedMap with the same elements. +// Using Copy while there are concurrent writes may mangle the result. +func (m *OrderedMap[K, V]) Copy() *OrderedMap[K, V] { + m2 := NewOrderedMap[K, V]() + + for el := m.Front(); el != nil; el = el.Next() { + m2.Set(el.Key, el.Value) + } + + return m2 +} diff --git a/v2/orderedmap_test.go b/v2/orderedmap_test.go new file mode 100644 index 0000000..34f26b0 --- /dev/null +++ b/v2/orderedmap_test.go @@ -0,0 +1,251 @@ +package orderedmap_test + +import ( + "testing" + + "github.com/elliotchance/orderedmap/v2" + "github.com/stretchr/testify/assert" +) + +func TestNewOrderedMap(t *testing.T) { + m := orderedmap.NewOrderedMap[int, string]() + assert.IsType(t, &orderedmap.OrderedMap[int, string]{}, m) +} + +func TestGet(t *testing.T) { + t.Run("ReturnsNotOKIfStringKeyDoesntExist", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + _, ok := m.Get("foo") + assert.False(t, ok) + }) + + t.Run("ReturnsNotOKIfNonStringKeyDoesntExist", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, string]() + _, ok := m.Get(123) + assert.False(t, ok) + }) + + t.Run("ReturnsOKIfKeyExists", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + m.Set("foo", "bar") + _, ok := m.Get("foo") + assert.True(t, ok) + }) + + t.Run("ReturnsValueForKey", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + m.Set("foo", "bar") + value, _ := m.Get("foo") + assert.Equal(t, "bar", value) + }) + + t.Run("ReturnsDynamicValueForKey", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + m.Set("foo", "baz") + value, _ := m.Get("foo") + assert.Equal(t, "baz", value) + }) + + t.Run("KeyDoesntExistOnNonEmptyMap", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + m.Set("foo", "baz") + _, ok := m.Get("bar") + assert.False(t, ok) + }) + + t.Run("ValueForKeyDoesntExistOnNonEmptyMap", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + m.Set("foo", "baz") + value, _ := m.Get("bar") + assert.Empty(t, value) + }) +} + +func TestSet(t *testing.T) { + t.Run("ReturnsTrueIfStringKeyIsNew", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + ok := m.Set("foo", "bar") + assert.True(t, ok) + }) + + t.Run("ReturnsTrueIfNonStringKeyIsNew", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, string]() + ok := m.Set(123, "bar") + assert.True(t, ok) + }) + + t.Run("ValueCanBeNonString", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, bool]() + ok := m.Set(123, true) + assert.True(t, ok) + }) + + t.Run("ReturnsFalseIfKeyIsNotNew", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + m.Set("foo", "bar") + ok := m.Set("foo", "bar") + assert.False(t, ok) + }) + + t.Run("SetThreeDifferentKeys", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + m.Set("foo", "bar") + m.Set("baz", "qux") + ok := m.Set("quux", "corge") + assert.True(t, ok) + }) +} + +func TestLen(t *testing.T) { + t.Run("EmptyMapIsZeroLen", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + assert.Equal(t, 0, m.Len()) + }) + + t.Run("SingleElementIsLenOne", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, bool]() + m.Set(123, true) + assert.Equal(t, 1, m.Len()) + }) + + t.Run("ThreeElements", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, bool]() + m.Set(1, true) + m.Set(2, true) + m.Set(3, true) + assert.Equal(t, 3, m.Len()) + }) +} + +func TestKeys(t *testing.T) { + t.Run("EmptyMap", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, bool]() + assert.Empty(t, m.Keys()) + }) + + t.Run("OneElement", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, bool]() + m.Set(1, true) + assert.Equal(t, []int{1}, m.Keys()) + }) + + t.Run("RetainsOrder", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, bool]() + for i := 1; i < 10; i++ { + m.Set(i, true) + } + assert.Equal(t, + []int{1, 2, 3, 4, 5, 6, 7, 8, 9}, + m.Keys()) + }) + + t.Run("ReplacingKeyDoesntChangeOrder", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, bool]() + m.Set("foo", true) + m.Set("bar", true) + m.Set("foo", false) + assert.Equal(t, + []string{"foo", "bar"}, + m.Keys()) + }) + + t.Run("KeysAfterDelete", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, bool]() + m.Set("foo", true) + m.Set("bar", true) + m.Delete("foo") + assert.Equal(t, []string{"bar"}, m.Keys()) + }) +} + +func TestDelete(t *testing.T) { + t.Run("KeyDoesntExistReturnsFalse", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + assert.False(t, m.Delete("foo")) + }) + + t.Run("KeyDoesExist", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, any]() + m.Set("foo", nil) + assert.True(t, m.Delete("foo")) + }) + + t.Run("KeyNoLongerExists", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, any]() + m.Set("foo", nil) + m.Delete("foo") + _, exists := m.Get("foo") + assert.False(t, exists) + }) + + t.Run("KeyDeleteIsIsolated", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, any]() + m.Set("foo", nil) + m.Set("bar", nil) + m.Delete("foo") + _, exists := m.Get("bar") + assert.True(t, exists) + }) +} + +func TestOrderedMap_Front(t *testing.T) { + t.Run("NilOnEmptyMap", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, bool]() + assert.Nil(t, m.Front()) + }) + + t.Run("NilOnEmptyMap", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, bool]() + m.Set(1, true) + assert.NotNil(t, m.Front()) + }) +} + +func TestOrderedMap_Back(t *testing.T) { + t.Run("NilOnEmptyMap", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, bool]() + assert.Nil(t, m.Back()) + }) + + t.Run("NilOnEmptyMap", func(t *testing.T) { + m := orderedmap.NewOrderedMap[int, bool]() + m.Set(1, true) + assert.NotNil(t, m.Back()) + }) +} + +func TestOrderedMap_Copy(t *testing.T) { + t.Run("ReturnsEqualButNotSame", func(t *testing.T) { + key, value := 1, "a value" + m := orderedmap.NewOrderedMap[int, string]() + m.Set(key, value) + + m2 := m.Copy() + m2.Set(key, "a different value") + + assert.Equal(t, m.Len(), m2.Len(), "not all elements are copied") + assert.Equal(t, value, m.GetElement(key).Value) + }) +} + +func TestGetElement(t *testing.T) { + t.Run("ReturnsElementForKey", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + m.Set("foo", "bar") + + var results []any + element := m.GetElement("foo") + if element != nil { + results = append(results, element.Key, element.Value) + } + + assert.Equal(t, []any{"foo", "bar"}, results) + }) + + t.Run("ElementForKeyDoesntExistOnNonEmptyMap", func(t *testing.T) { + m := orderedmap.NewOrderedMap[string, string]() + m.Set("foo", "baz") + element := m.GetElement("bar") + assert.Nil(t, element) + }) +}