Skip to content

Commit

Permalink
Merge pull request #3 from fgm/orderedmap
Browse files Browse the repository at this point in the history
Implement slice-based orderedmap.
  • Loading branch information
fgm authored May 12, 2022
2 parents 85e584c + 56d7ce9 commit 49f1691
Show file tree
Hide file tree
Showing 8 changed files with 240 additions and 10 deletions.
2 changes: 1 addition & 1 deletion .idea/runConfigurations/Bench_all.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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

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

## Contents

See the available types by underlying storage

| Type | Slice | List | List+sync.Pool | List+int. pool | Recommended |
|:-----:|:-----:|:----:|:--------------:|:--------------:|:--------------------:|
| Queue | Y | Y | Y | Y | Slice with size hint |
| Stack | Y | Y | Y | Y | Slice with size hint |
| Type | Slice | List | List+sync.Pool | List+int. pool | Recommended |
|------------|:-----:|:----:|:--------------:|:--------------:|----------------------|
| OrderedMap | Y | | | | Slice with size hint |
| Queue | Y | Y | Y | Y | Slice with size hint |
| Stack | Y | Y | Y | Y | Slice with size hint |

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

## Use

See complete listing in [`cmd/example.go`](cmd/example.go)
See complete listings in:

- [`cmd/orderedmap/example.go`](cmd/orderedmap/example.go)
- [`cmd/queuestack/example.go`](cmd/queuestack/example.go)

### Ordered Map

```go
om := orderedmap.NewSlice[Key,Value](sizeHint)
om.Store(k, v)
om.Range(func(k K, v V) bool { fmt.Println(k, v); return true })
v, loaded := om.Load(k)
if !loaded { fmt.Printf("No entry for key %v\n", k)}
om.Delete(k) // Idempotent: does not fail on nonexistent keys.
```

### Queues

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

```go
s := stack.NewSliceStack[Element](sizeHint) // resp. NewListStack
s := stack.NewSliceStack[Element](sizeHint)
s.Push(e)
if ls, ok := s.(container.Countable); ok {
fmt.Printf("elements in stack: %d\n", ls.Len())
Expand Down
45 changes: 45 additions & 0 deletions cmd/orderedmap/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package main

import (
"fmt"
"strconv"

"github.com/fgm/container/orderedmap"
)

type in struct {
key string
value int
}

func main() {
const size = 8
input := make([]in, size)
for i := 1; i <= size; i++ {
input[i-1] = in{key: strconv.Itoa(i), value: i}
}

fmt.Println("Go map:")
m := make(map[string]int, size)
for _, e := range input {
m[e.key] = e.value
}
delete(m, "5")
m["1"] = 11
for k, v := range m {
fmt.Println(k, v)
}

fmt.Println("OrderedMap:")
var om = orderedmap.NewSlice[string, int](size)
for _, e := range input {
om.Store(e.key, e.value)
}
om.Delete("5")
om.Store("1", 11)

om.Range(func(k string, v int) bool {
fmt.Println(k, v)
return true
})
}
File renamed without changes.
73 changes: 73 additions & 0 deletions orderedmap/slice.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package orderedmap

import (
"fmt"

"github.com/fgm/container"
)

type Slice[K comparable, V any] struct {
order []K
store map[K]V
}

// mustIndexOf may only be used with a key known to be present in the map.
func (s Slice[K, V]) mustIndexOf(k K) int {
for i, ck := range s.order {
if ck == k {
return i
}
}
// Should never happen: someone probably caused a race condition.
panic(fmt.Errorf("structure inconsistency: key %v not found", k))
}

func (s *Slice[K, V]) Delete(k K) {
_, loaded := s.store[k]
if !loaded {
return
}
delete(s.store, k)
index := s.mustIndexOf(k)
s.order = append(s.order[:index], s.order[index+1:]...)
}

func (s Slice[K, V]) Load(key K) (V, bool) {
v, loaded := s.store[key]
return v, loaded
}

func (s Slice[K, V]) Range(f func(key K, value V) bool) {
for _, k := range s.order {
v, loaded := s.store[k]
if !loaded {
panic(fmt.Errorf("structure inconsistency: key %v not found", k))
}
if !f(k, v) {
break
}
}
}

func (s *Slice[K, V]) Store(k K, v V) {
_, loaded := s.store[k]
if !loaded {
s.order = append(s.order, k)
s.store[k] = v
return
}

index := s.mustIndexOf(k)
s.order = append(s.order[:index], s.order[index+1:]...)
s.order = append(s.order, k)
s.store[k] = v
}

func NewSlice[K comparable, V any](sizeHint int) container.OrderedMap[K, V] {
s := &Slice[K, V]{
order: make([]K, 0, sizeHint),
store: make(map[K]V, sizeHint),
}

return s
}
64 changes: 64 additions & 0 deletions orderedmap/slice_opaque_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package orderedmap

import (
"strconv"
"testing"

"github.com/google/go-cmp/cmp"
)

type in struct {
key string
value int
}

func TestSlice_Range(t *testing.T) {
const size = 8
input := make([]in, size)
for i := 1; i <= size; i++ {
input[i-1] = in{key: strconv.Itoa(i), value: i}
}
expectedKeys := []string{"2", "3", "4", "6", "7", "8", "1"}
expectedVals := []int{2, 3, 4, 6, 7, 8, 11}

var om = NewSlice[string, int](size)
for _, e := range input {
om.Store(e.key, e.value)
}
om.Delete("5")
om.Store("1", 11)

var keys = make([]string, 0, size)
var vals = make([]int, 0, size)
om.Range(func(k string, v int) bool {
keys = append(keys, k)
vals = append(vals, v)
return k != "1" // This is the last key because we added it after Delete.
})
if !cmp.Equal(keys, expectedKeys) {
t.Fatalf("Failed keys comparison:%s", cmp.Diff(keys, expectedKeys))
}
if !cmp.Equal(vals, expectedVals) {
t.Fatalf("Failed values comparison:%s", cmp.Diff(vals, expectedVals))
}
}

func TestSlice_Store_Load_Delete(t *testing.T) {
const one = "one"
var om = NewSlice[string, int](1)
om.Store(one, 1)
zero, loaded := om.Load("zero")
if loaded {
t.Fatalf("unexpected load success for missing key %s, value is %v", "zero", zero)
}
_, loaded = om.Load(one)
if !loaded {
t.Fatal("unexpected load failure for present key")
}
om.Delete(one)
om.Delete(one) // Ensure multiple deletes do not cause an error
actual, loaded := om.Load(one)
if loaded {
t.Fatalf("unexpected load success for missing key %s, value is %v", one, actual)
}
}
26 changes: 26 additions & 0 deletions orderedmap/slice_transparent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package orderedmap

import (
"testing"
)

func TestSlice_mustIndexOf(t *testing.T) {
// Catch expected panic
defer func() { _ = recover() }()

var om = NewSlice[string, int](1).(*Slice[string, int])
om.store["one"] = 1
om.mustIndexOf("one")
t.Fatalf("mustIndexOf did not panic on missing order key")
}

func TestSlice_Range_inconsistent(t *testing.T) {
// Catch expected panic
defer func() { _ = recover() }()

var om = NewSlice[string, int](1).(*Slice[string, int])
om.Store("one", 1)
delete(om.store, "one")
om.Range(func(_ string, _ int) bool { return false })
t.Fatalf("Range did not panic on missing map entry")
}
7 changes: 7 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
package container

type OrderedMap[K comparable, V any] interface {
Delete(key K)
Load(key K) (value V, loaded bool)
Range(func(key K, value V) bool)
Store(key K, value V)
}

// Queue is generic queue with no concurrency guarantees.
// Instantiate by queue.New<implementation>Queue(sizeHint).
// The size hint MAY be used by some implementations to optimize storage.
Expand Down

0 comments on commit 49f1691

Please sign in to comment.