From 02f2ded09eed8d0e8d08bc93ab4c8c416ec30ab8 Mon Sep 17 00:00:00 2001 From: Elliot Chance Date: Fri, 15 Nov 2019 08:15:01 +1100 Subject: [PATCH] Basic functions Basic functions include Set, Get, Delete, Len and Keys. --- .gitignore | 1 + README.md | 24 +++ go.mod | 5 + go.sum | 11 ++ orderedmap.go | 78 ++++++++++ orderedmap_test.go | 379 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 498 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 orderedmap.go create mode 100644 orderedmap_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a09c56d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.idea diff --git a/README.md b/README.md new file mode 100644 index 0000000..8eb98a1 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# 🔃 github.com/elliotchance/orderedmap [![GoDoc](https://godoc.org/github.com/elliotchance/orderedmap?status.svg)](https://godoc.org/github.com/elliotchance/orderedmap) + +The `orderedmap` package provides a high performance ordered map in Go: + +```go +m := orderedmap.NewOrderedMap() + +m.Set("foo", "bar") +m.Set("qux", 1.23) +m.Set(123, true) + +m.Delete("qux") + +for _, key := range m.Keys() { + value, _:= m.Get(key) + fmt.Println(key, value) +} +``` + +Internally an `*OrderedMap` uses a combination of a map and linked list to +maintain amortized O(1) for `Set`, `Get`, `Delete` and `Len`. + +See the full documentation at +[https://godoc.org/github.com/elliotchance/orderedmap](https://godoc.org/github.com/elliotchance/orderedmap). diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8ee070e --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/elliotchance/orderedmap + +go 1.12 + +require github.com/stretchr/testify v1.4.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8fdee58 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +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.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/orderedmap.go b/orderedmap.go new file mode 100644 index 0000000..02ea7f0 --- /dev/null +++ b/orderedmap.go @@ -0,0 +1,78 @@ +package orderedmap + +import "container/list" + +type orderedMapElement struct { + key, value interface{} +} + +type OrderedMap struct { + kv map[interface{}]*list.Element + ll *list.List +} + +func NewOrderedMap() *OrderedMap { + return &OrderedMap{ + kv: make(map[interface{}]*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) Get(key interface{}) (interface{}, bool) { + value, ok := m.kv[key] + if ok { + return value.Value.(*orderedMapElement).value, true + } + + return nil, false +} + +// 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) Set(key, value interface{}) bool { + _, didExist := m.kv[key] + + if !didExist { + element := m.ll.PushBack(&orderedMapElement{key, value}) + m.kv[key] = element + } else { + m.kv[key].Value.(*orderedMapElement).value = value + } + + return !didExist +} + +// Len returns the number of elements in the map. +func (m *OrderedMap) 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) Keys() (keys []interface{}) { + keys = make([]interface{}, m.Len()) + + element := m.ll.Front() + for i := 0; element != nil; i++ { + keys[i] = element.Value.(*orderedMapElement).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) Delete(key interface{}) (didDelete bool) { + element, ok := m.kv[key] + if ok { + m.ll.Remove(element) + delete(m.kv, key) + } + + return ok +} diff --git a/orderedmap_test.go b/orderedmap_test.go new file mode 100644 index 0000000..ad58dd1 --- /dev/null +++ b/orderedmap_test.go @@ -0,0 +1,379 @@ +package orderedmap_test + +import ( + "fmt" + "github.com/elliotchance/orderedmap" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestNewOrderedMap(t *testing.T) { + m := orderedmap.NewOrderedMap() + assert.IsType(t, &orderedmap.OrderedMap{}, m) +} + +func TestGet(t *testing.T) { + t.Run("ReturnsNotOKIfStringKeyDoesntExist", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + _, ok := m.Get("foo") + assert.False(t, ok) + }) + + t.Run("ReturnsNotOKIfNonStringKeyDoesntExist", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + _, ok := m.Get(123) + assert.False(t, ok) + }) + + t.Run("ReturnsOKIfKeyExists", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set("foo", "bar") + _, ok := m.Get("foo") + assert.True(t, ok) + }) + + t.Run("ReturnsValueForKey", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set("foo", "bar") + value, _ := m.Get("foo") + assert.Equal(t, "bar", value) + }) + + t.Run("ReturnsDynamicValueForKey", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set("foo", "baz") + value, _ := m.Get("foo") + assert.Equal(t, "baz", value) + }) + + t.Run("KeyDoesntExistOnNonEmptyMap", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set("foo", "baz") + _, ok := m.Get("bar") + assert.False(t, ok) + }) + + t.Run("ValueForKeyDoesntExistOnNonEmptyMap", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set("foo", "baz") + value, _ := m.Get("bar") + assert.Nil(t, value) + }) + + t.Run("Performance", func(t *testing.T) { + if testing.Short() { + t.Skip("performance test skipped in short mode") + } + + res1 := testing.Benchmark(benchmarkOrderedMap_Get(1)) + res4 := testing.Benchmark(benchmarkOrderedMap_Get(4)) + + // O(1) would mean that res4 should take about the same time as res1, + // because we are accessing the same amount of elements, just on + // different sized maps. + + assert.InDelta(t, + res1.NsPerOp(), res4.NsPerOp(), + 0.5*float64(res1.NsPerOp())) + }) +} + +func TestSet(t *testing.T) { + t.Run("ReturnsTrueIfStringKeyIsNew", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + ok := m.Set("foo", "bar") + assert.True(t, ok) + }) + + t.Run("ReturnsTrueIfNonStringKeyIsNew", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + ok := m.Set(123, "bar") + assert.True(t, ok) + }) + + t.Run("ValueCanBeNonString", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + ok := m.Set(123, true) + assert.True(t, ok) + }) + + t.Run("ReturnsFalseIfKeyIsNotNew", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set("foo", "bar") + ok := m.Set("foo", "bar") + assert.False(t, ok) + }) + + t.Run("SetThreeDifferentKeys", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set("foo", "bar") + m.Set("baz", "qux") + ok := m.Set("quux", "corge") + assert.True(t, ok) + }) + + t.Run("Performance", func(t *testing.T) { + if testing.Short() { + t.Skip("performance test skipped in short mode") + } + + res1 := testing.Benchmark(benchmarkOrderedMap_Set(1)) + res4 := testing.Benchmark(benchmarkOrderedMap_Set(4)) + + // O(1) would mean that res4 should take about 4 times longer than res1 + // because we are doing 4 times the amount of Set operations. Allow for + // a wide margin, but not too wide that it would permit the inflection + // to O(n^2). + + assert.InDelta(t, + 4*res1.NsPerOp(), res4.NsPerOp(), + 2*float64(res1.NsPerOp())) + }) +} + +func TestLen(t *testing.T) { + t.Run("EmptyMapIsZeroLen", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + assert.Equal(t, 0, m.Len()) + }) + + t.Run("SingleElementIsLenOne", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set(123, true) + assert.Equal(t, 1, m.Len()) + }) + + t.Run("ThreeElements", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set(1, true) + m.Set(2, true) + m.Set(3, true) + assert.Equal(t, 3, m.Len()) + }) + + t.Run("Performance", func(t *testing.T) { + if testing.Short() { + t.Skip("performance test skipped in short mode") + } + + res1 := testing.Benchmark(benchmarkOrderedMap_Len(1)) + res4 := testing.Benchmark(benchmarkOrderedMap_Len(4)) + + // O(1) would mean that res4 should take about the same time as res1, + // because we are accessing the same amount of elements, just on + // different sized maps. + + assert.InDelta(t, + res1.NsPerOp(), res4.NsPerOp(), + 0.5*float64(res1.NsPerOp())) + }) +} + +func TestKeys(t *testing.T) { + t.Run("EmptyMap", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + assert.Empty(t, m.Keys()) + }) + + t.Run("OneElement", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set(1, true) + assert.Equal(t, []interface{}{1}, m.Keys()) + }) + + t.Run("RetainsOrder", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + for i := 1; i < 10; i++ { + m.Set(i, true) + } + assert.Equal(t, + []interface{}{1, 2, 3, 4, 5, 6, 7, 8, 9}, + m.Keys()) + }) + + t.Run("ReplacingKeyDoesntChangeOrder", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set("foo", true) + m.Set("bar", true) + m.Set("foo", false) + assert.Equal(t, + []interface{}{"foo", "bar"}, + m.Keys()) + }) + + t.Run("KeysAfterDelete", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set("foo", true) + m.Set("bar", true) + m.Delete("foo") + assert.Equal(t, []interface{}{"bar"}, m.Keys()) + }) + + t.Run("Performance", func(t *testing.T) { + if testing.Short() { + t.Skip("performance test skipped in short mode") + } + + res1 := testing.Benchmark(benchmarkOrderedMap_Keys(1)) + res4 := testing.Benchmark(benchmarkOrderedMap_Keys(4)) + + // O(1) would mean that res4 should take about 4 times longer than res1 + // because we are doing 4 times the amount of Set/Delete operations. + // Allow for a wide margin, but not too wide that it would permit the + // inflection to O(n^2). + + assert.InDelta(t, + 4*res1.NsPerOp(), res4.NsPerOp(), + 2*float64(res1.NsPerOp())) + }) +} + +func TestDelete(t *testing.T) { + t.Run("KeyDoesntExistReturnsFalse", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + assert.False(t, m.Delete("foo")) + }) + + t.Run("KeyDoesExist", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + m.Set("foo", nil) + assert.True(t, m.Delete("foo")) + }) + + t.Run("KeyNoLongerExists", func(t *testing.T) { + m := orderedmap.NewOrderedMap() + 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() + m.Set("foo", nil) + m.Set("bar", nil) + m.Delete("foo") + _, exists := m.Get("bar") + assert.True(t, exists) + }) + + t.Run("Performance", func(t *testing.T) { + if testing.Short() { + t.Skip("performance test skipped in short mode") + } + + res1 := testing.Benchmark(benchmarkOrderedMap_Delete(1)) + res4 := testing.Benchmark(benchmarkOrderedMap_Delete(4)) + + // O(1) would mean that res4 should take about 4 times longer than res1 + // because we are doing 4 times the amount of Set/Delete operations. + // Allow for a wide margin, but not too wide that it would permit the + // inflection to O(n^2). + + assert.InDelta(t, + 4*res1.NsPerOp(), res4.NsPerOp(), + 2*float64(res1.NsPerOp())) + }) +} + +func benchmarkOrderedMap_Set(multiplier int) func(b *testing.B) { + return func(b *testing.B) { + m := orderedmap.NewOrderedMap() + for i := 0; i < b.N*multiplier; i++ { + m.Set(i, true) + } + } +} + +func BenchmarkOrderedMap_Set(b *testing.B) { + benchmarkOrderedMap_Set(1)(b) +} + +func benchmarkOrderedMap_Get(multiplier int) func(b *testing.B) { + m := orderedmap.NewOrderedMap() + for i := 0; i < 1000*multiplier; i++ { + m.Set(i, true) + } + + return func(b *testing.B) { + for i := 0; i < b.N; i++ { + m.Get(1000 * multiplier % b.N) + } + } +} + +func BenchmarkOrderedMap_Get(b *testing.B) { + benchmarkOrderedMap_Get(1)(b) +} + +var tempInt int + +func benchmarkOrderedMap_Len(multiplier int) func(b *testing.B) { + m := orderedmap.NewOrderedMap() + for i := 0; i < 1000*multiplier; i++ { + m.Set(i, true) + } + + return func(b *testing.B) { + var temp int + for i := 0; i < b.N; i++ { + temp = m.Len() + } + + // prevent compiler from optimising Len away. + tempInt = temp + } +} + +func BenchmarkOrderedMap_Len(b *testing.B) { + benchmarkOrderedMap_Len(1)(b) +} + +func benchmarkOrderedMap_Delete(multiplier int) func(b *testing.B) { + return func(b *testing.B) { + m := orderedmap.NewOrderedMap() + for i := 0; i < b.N*multiplier; i++ { + m.Set(i, true) + } + + for i := 0; i < b.N; i++ { + m.Delete(b.N) + } + } +} + +func BenchmarkOrderedMap_Delete(b *testing.B) { + benchmarkOrderedMap_Delete(1)(b) +} + +func benchmarkOrderedMap_Keys(multiplier int) func(b *testing.B) { + m := orderedmap.NewOrderedMap() + for i := 0; i < 1000*multiplier; i++ { + m.Set(i, true) + } + + return func(b *testing.B) { + for i := 0; i < b.N; i++ { + m.Keys() + } + } +} + +func BenchmarkOrderedMap_Keys(b *testing.B) { + benchmarkOrderedMap_Keys(1)(b) +} + +func ExampleNewOrderedMap() { + m := orderedmap.NewOrderedMap() + + m.Set("foo", "bar") + m.Set("qux", 1.23) + m.Set(123, true) + + m.Delete("qux") + + for _, key := range m.Keys() { + value, _ := m.Get(key) + fmt.Println(key, value) + } +}