Skip to content

Commit

Permalink
Added support for generics (#26)
Browse files Browse the repository at this point in the history
Generics (in Go 1.18) are an ideal case for this library. It is provided
through a new version v2. If you need to support Go 1.17 or below you can
continue to use v1.
  • Loading branch information
elliotchance committed Apr 9, 2022
1 parent 1bd6a21 commit da74303
Show file tree
Hide file tree
Showing 7 changed files with 547 additions and 58 deletions.
73 changes: 15 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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 |
37 changes: 37 additions & 0 deletions v2/element.go
Original file line number Diff line number Diff line change
@@ -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())
}
67 changes: 67 additions & 0 deletions v2/element_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
11 changes: 11 additions & 0 deletions v2/go.mod
Original file line number Diff line number Diff line change
@@ -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
)
12 changes: 12 additions & 0 deletions v2/go.sum
Original file line number Diff line number Diff line change
@@ -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=
154 changes: 154 additions & 0 deletions v2/orderedmap.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit da74303

Please sign in to comment.