Skip to content

Commit 49f1691

Browse files
authored
Merge pull request #3 from fgm/orderedmap
Implement slice-based orderedmap.
2 parents 85e584c + 56d7ce9 commit 49f1691

8 files changed

Lines changed: 240 additions & 10 deletions

File tree

.idea/runConfigurations/Bench_all.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33
[![codecov](https://codecov.io/gh/fgm/container/branch/main/graph/badge.svg?token=8YYX1B720M)](https://codecov.io/gh/fgm/container)
44
[![Go Report Card](https://goreportcard.com/badge/github.com/fgm/container)](https://goreportcard.com/report/github.com/fgm/container)
55

6-
This module contains minimal type-safe Queue and Stack implementations using
7-
Go 1.18 generics.
6+
This module contains minimal type-safe Ordered Map, Queue and Stack implementations
7+
using Go 1.18 generics.
88

99
## Contents
1010

1111
See the available types by underlying storage
1212

13-
| Type | Slice | List | List+sync.Pool | List+int. pool | Recommended |
14-
|:-----:|:-----:|:----:|:--------------:|:--------------:|:--------------------:|
15-
| Queue | Y | Y | Y | Y | Slice with size hint |
16-
| Stack | Y | Y | Y | Y | Slice with size hint |
13+
| Type | Slice | List | List+sync.Pool | List+int. pool | Recommended |
14+
|------------|:-----:|:----:|:--------------:|:--------------:|----------------------|
15+
| OrderedMap | Y | | | | Slice with size hint |
16+
| Queue | Y | Y | Y | Y | Slice with size hint |
17+
| Stack | Y | Y | Y | Y | Slice with size hint |
1718

1819
**CAVEAT**: All of these implementations are unsafe for concurrent execution,
1920
so they need protection in concurrency situations.
@@ -27,12 +28,26 @@ See [BENCHARKS.md](BENCHMARKS.md) for details.
2728

2829
## Use
2930

30-
See complete listing in [`cmd/example.go`](cmd/example.go)
31+
See complete listings in:
32+
33+
- [`cmd/orderedmap/example.go`](cmd/orderedmap/example.go)
34+
- [`cmd/queuestack/example.go`](cmd/queuestack/example.go)
35+
36+
### Ordered Map
37+
38+
```go
39+
om := orderedmap.NewSlice[Key,Value](sizeHint)
40+
om.Store(k, v)
41+
om.Range(func(k K, v V) bool { fmt.Println(k, v); return true })
42+
v, loaded := om.Load(k)
43+
if !loaded { fmt.Printf("No entry for key %v\n", k)}
44+
om.Delete(k) // Idempotent: does not fail on nonexistent keys.
45+
```
3146

3247
### Queues
3348

3449
```go
35-
q := queue.NewSliceQueue[Element](sizeHint) // resp. NewListQueue
50+
q := queue.NewSliceQueue[Element](sizeHint)
3651
q.Enqueue(e)
3752
if lq, ok := q.(container.Countable); ok {
3853
fmt.Printf("elements in queue: %d\n", lq.Len())
@@ -46,7 +61,7 @@ for i := 0; i < 2; i++ {
4661
### Stacks
4762

4863
```go
49-
s := stack.NewSliceStack[Element](sizeHint) // resp. NewListStack
64+
s := stack.NewSliceStack[Element](sizeHint)
5065
s.Push(e)
5166
if ls, ok := s.(container.Countable); ok {
5267
fmt.Printf("elements in stack: %d\n", ls.Len())

cmd/orderedmap/main.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
7+
"github.com/fgm/container/orderedmap"
8+
)
9+
10+
type in struct {
11+
key string
12+
value int
13+
}
14+
15+
func main() {
16+
const size = 8
17+
input := make([]in, size)
18+
for i := 1; i <= size; i++ {
19+
input[i-1] = in{key: strconv.Itoa(i), value: i}
20+
}
21+
22+
fmt.Println("Go map:")
23+
m := make(map[string]int, size)
24+
for _, e := range input {
25+
m[e.key] = e.value
26+
}
27+
delete(m, "5")
28+
m["1"] = 11
29+
for k, v := range m {
30+
fmt.Println(k, v)
31+
}
32+
33+
fmt.Println("OrderedMap:")
34+
var om = orderedmap.NewSlice[string, int](size)
35+
for _, e := range input {
36+
om.Store(e.key, e.value)
37+
}
38+
om.Delete("5")
39+
om.Store("1", 11)
40+
41+
om.Range(func(k string, v int) bool {
42+
fmt.Println(k, v)
43+
return true
44+
})
45+
}

orderedmap/slice.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package orderedmap
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/fgm/container"
7+
)
8+
9+
type Slice[K comparable, V any] struct {
10+
order []K
11+
store map[K]V
12+
}
13+
14+
// mustIndexOf may only be used with a key known to be present in the map.
15+
func (s Slice[K, V]) mustIndexOf(k K) int {
16+
for i, ck := range s.order {
17+
if ck == k {
18+
return i
19+
}
20+
}
21+
// Should never happen: someone probably caused a race condition.
22+
panic(fmt.Errorf("structure inconsistency: key %v not found", k))
23+
}
24+
25+
func (s *Slice[K, V]) Delete(k K) {
26+
_, loaded := s.store[k]
27+
if !loaded {
28+
return
29+
}
30+
delete(s.store, k)
31+
index := s.mustIndexOf(k)
32+
s.order = append(s.order[:index], s.order[index+1:]...)
33+
}
34+
35+
func (s Slice[K, V]) Load(key K) (V, bool) {
36+
v, loaded := s.store[key]
37+
return v, loaded
38+
}
39+
40+
func (s Slice[K, V]) Range(f func(key K, value V) bool) {
41+
for _, k := range s.order {
42+
v, loaded := s.store[k]
43+
if !loaded {
44+
panic(fmt.Errorf("structure inconsistency: key %v not found", k))
45+
}
46+
if !f(k, v) {
47+
break
48+
}
49+
}
50+
}
51+
52+
func (s *Slice[K, V]) Store(k K, v V) {
53+
_, loaded := s.store[k]
54+
if !loaded {
55+
s.order = append(s.order, k)
56+
s.store[k] = v
57+
return
58+
}
59+
60+
index := s.mustIndexOf(k)
61+
s.order = append(s.order[:index], s.order[index+1:]...)
62+
s.order = append(s.order, k)
63+
s.store[k] = v
64+
}
65+
66+
func NewSlice[K comparable, V any](sizeHint int) container.OrderedMap[K, V] {
67+
s := &Slice[K, V]{
68+
order: make([]K, 0, sizeHint),
69+
store: make(map[K]V, sizeHint),
70+
}
71+
72+
return s
73+
}

orderedmap/slice_opaque_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package orderedmap
2+
3+
import (
4+
"strconv"
5+
"testing"
6+
7+
"github.com/google/go-cmp/cmp"
8+
)
9+
10+
type in struct {
11+
key string
12+
value int
13+
}
14+
15+
func TestSlice_Range(t *testing.T) {
16+
const size = 8
17+
input := make([]in, size)
18+
for i := 1; i <= size; i++ {
19+
input[i-1] = in{key: strconv.Itoa(i), value: i}
20+
}
21+
expectedKeys := []string{"2", "3", "4", "6", "7", "8", "1"}
22+
expectedVals := []int{2, 3, 4, 6, 7, 8, 11}
23+
24+
var om = NewSlice[string, int](size)
25+
for _, e := range input {
26+
om.Store(e.key, e.value)
27+
}
28+
om.Delete("5")
29+
om.Store("1", 11)
30+
31+
var keys = make([]string, 0, size)
32+
var vals = make([]int, 0, size)
33+
om.Range(func(k string, v int) bool {
34+
keys = append(keys, k)
35+
vals = append(vals, v)
36+
return k != "1" // This is the last key because we added it after Delete.
37+
})
38+
if !cmp.Equal(keys, expectedKeys) {
39+
t.Fatalf("Failed keys comparison:%s", cmp.Diff(keys, expectedKeys))
40+
}
41+
if !cmp.Equal(vals, expectedVals) {
42+
t.Fatalf("Failed values comparison:%s", cmp.Diff(vals, expectedVals))
43+
}
44+
}
45+
46+
func TestSlice_Store_Load_Delete(t *testing.T) {
47+
const one = "one"
48+
var om = NewSlice[string, int](1)
49+
om.Store(one, 1)
50+
zero, loaded := om.Load("zero")
51+
if loaded {
52+
t.Fatalf("unexpected load success for missing key %s, value is %v", "zero", zero)
53+
}
54+
_, loaded = om.Load(one)
55+
if !loaded {
56+
t.Fatal("unexpected load failure for present key")
57+
}
58+
om.Delete(one)
59+
om.Delete(one) // Ensure multiple deletes do not cause an error
60+
actual, loaded := om.Load(one)
61+
if loaded {
62+
t.Fatalf("unexpected load success for missing key %s, value is %v", one, actual)
63+
}
64+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package orderedmap
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestSlice_mustIndexOf(t *testing.T) {
8+
// Catch expected panic
9+
defer func() { _ = recover() }()
10+
11+
var om = NewSlice[string, int](1).(*Slice[string, int])
12+
om.store["one"] = 1
13+
om.mustIndexOf("one")
14+
t.Fatalf("mustIndexOf did not panic on missing order key")
15+
}
16+
17+
func TestSlice_Range_inconsistent(t *testing.T) {
18+
// Catch expected panic
19+
defer func() { _ = recover() }()
20+
21+
var om = NewSlice[string, int](1).(*Slice[string, int])
22+
om.Store("one", 1)
23+
delete(om.store, "one")
24+
om.Range(func(_ string, _ int) bool { return false })
25+
t.Fatalf("Range did not panic on missing map entry")
26+
}

types.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
package container
22

3+
type OrderedMap[K comparable, V any] interface {
4+
Delete(key K)
5+
Load(key K) (value V, loaded bool)
6+
Range(func(key K, value V) bool)
7+
Store(key K, value V)
8+
}
9+
310
// Queue is generic queue with no concurrency guarantees.
411
// Instantiate by queue.New<implementation>Queue(sizeHint).
512
// The size hint MAY be used by some implementations to optimize storage.

0 commit comments

Comments
 (0)