From 39e383812ead725d1332e485a515ec4598b4615e Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Sat, 12 Dec 2020 12:15:19 +0100 Subject: [PATCH] added bimap --- bimap/bimap.go | 143 ++++++++++++++++++++ bimap/bimap_test.go | 308 ++++++++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 10 ++ 4 files changed, 462 insertions(+) create mode 100644 bimap/bimap.go create mode 100644 bimap/bimap_test.go diff --git a/bimap/bimap.go b/bimap/bimap.go new file mode 100644 index 0000000..d992a39 --- /dev/null +++ b/bimap/bimap.go @@ -0,0 +1,143 @@ +package bimap + +// Package bimap provides a threadsafe bidirectional map + +import "sync" + +// BiMap is a bi-directional hashmap that is thread safe and supports immutability +type BiMap struct { + s sync.RWMutex + immutable bool + forward map[interface{}]interface{} + inverse map[interface{}]interface{} +} + +// NewBiMap returns a an empty, mutable, biMap +func NewBiMap() *BiMap { + return &BiMap{forward: make(map[interface{}]interface{}), inverse: make(map[interface{}]interface{}), immutable: false} +} + +// Insert puts a key and value into the BiMap, provided its mutable. Also creates the reverse mapping from value to key. +func (b *BiMap) Insert(k interface{}, v interface{}) { + b.s.RLock() + if b.immutable { + panic("Cannot modify immutable map") + } + b.s.RUnlock() + + b.s.Lock() + defer b.s.Unlock() + b.forward[k] = v + b.inverse[v] = k +} + +// Exists checks whether or not a key exists in the BiMap +func (b *BiMap) Exists(k interface{}) bool { + b.s.RLock() + defer b.s.RUnlock() + _, ok := b.forward[k] + return ok +} + +// ExistsInverse checks whether or not a value exists in the BiMap +func (b *BiMap) ExistsInverse(k interface{}) bool { + b.s.RLock() + defer b.s.RUnlock() + + _, ok := b.inverse[k] + return ok +} + +// Get returns the value for a given key in the BiMap and whether or not the element was present. +func (b *BiMap) Get(k interface{}) (interface{}, bool) { + if !b.Exists(k) { + return "", false + } + b.s.RLock() + defer b.s.RUnlock() + return b.forward[k], true + +} + +// GetInverse returns the key for a given value in the BiMap and whether or not the element was present. +func (b *BiMap) GetInverse(v interface{}) (interface{}, bool) { + if !b.ExistsInverse(v) { + return "", false + } + b.s.RLock() + defer b.s.RUnlock() + return b.inverse[v], true + +} + +// Delete removes a key-value pair from the BiMap for a given key. Returns if the key doesn't exist +func (b *BiMap) Delete(k interface{}) { + b.s.RLock() + if b.immutable { + panic("Cannot modify immutable map") + } + b.s.RUnlock() + + if !b.Exists(k) { + return + } + val, _ := b.Get(k) + b.s.Lock() + defer b.s.Unlock() + delete(b.forward, k) + delete(b.inverse, val) +} + +// DeleteInverse emoves a key-value pair from the BiMap for a given value. Returns if the value doesn't exist +func (b *BiMap) DeleteInverse(v interface{}) { + b.s.RLock() + if b.immutable { + panic("Cannot modify immutable map") + } + b.s.RUnlock() + + if !b.ExistsInverse(v) { + return + } + + key, _ := b.GetInverse(v) + b.s.Lock() + defer b.s.Unlock() + delete(b.inverse, v) + delete(b.forward, key) + +} + +// Size returns the number of elements in the bimap +func (b *BiMap) Size() int { + b.s.RLock() + defer b.s.RUnlock() + return len(b.forward) +} + +// MakeImmutable freezes the BiMap preventing any further write actions from taking place +func (b *BiMap) MakeImmutable() { + b.s.Lock() + defer b.s.Unlock() + b.immutable = true +} + +// GetInverseMap returns a regular go map mapping from the BiMap's values to its keys +func (b *BiMap) GetInverseMap() map[interface{}]interface{} { + return b.inverse +} + +// GetForwardMap returns a regular go map mapping from the BiMap's keys to its values +func (b *BiMap) GetForwardMap() map[interface{}]interface{} { + return b.forward +} + +// Lock manually locks the BiMap's mutex +func (b *BiMap) Lock() { + b.s.Lock() +} + +// Unlock manually unlocks the BiMap's mutex +func (b *BiMap) Unlock() { + b.s.Unlock() +} diff --git a/bimap/bimap_test.go b/bimap/bimap_test.go new file mode 100644 index 0000000..bbdebaa --- /dev/null +++ b/bimap/bimap_test.go @@ -0,0 +1,308 @@ +package bimap + +import ( + "reflect" + "runtime/debug" + "testing" + + "github.com/epiclabs-io/ut" +) + +const key = "key" +const value = "value" + +// isEmpty gets whether the specified object is considered empty or not. +func isEmpty(object interface{}) bool { + + // get nil case out of the way + if object == nil { + return true + } + + objValue := reflect.ValueOf(object) + + switch objValue.Kind() { + // collection types are empty when they have no element + case reflect.Array, reflect.Chan, reflect.Map, reflect.Slice: + return objValue.Len() == 0 + // pointers are empty if nil or if the value they point to is empty + case reflect.Ptr: + if objValue.IsNil() { + return true + } + deref := objValue.Elem().Interface() + return isEmpty(deref) + // for all other types, compare against the zero value + default: + zero := reflect.Zero(objValue.Type()) + return reflect.DeepEqual(object, zero.Interface()) + } +} + +// didPanic returns true if the function passed to it panics. Otherwise, it returns false. +func didPanic(f func()) (bool, interface{}, string) { + + didPanic := false + var message interface{} + var stack string + func() { + + defer func() { + if message = recover(); message != nil { + didPanic = true + stack = string(debug.Stack()) + } + }() + + // call the target function + f() + + }() + + return didPanic, message, stack + +} + +func TestNewBiMap(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + + actual := NewBiMap() + expected := &BiMap{forward: make(map[interface{}]interface{}), inverse: make(map[interface{}]interface{})} + t.Equals(expected, actual) +} + +func TestBiMap_Insert(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + actual := NewBiMap() + actual.Insert(key, value) + + fwdExpected := make(map[interface{}]interface{}) + invExpected := make(map[interface{}]interface{}) + fwdExpected[key] = value + invExpected[value] = key + expected := &BiMap{forward: fwdExpected, inverse: invExpected} + + t.Equals(expected, actual) +} + +func TestBiMap_Exists(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + actual := NewBiMap() + + actual.Insert(key, value) + t.Assert(!actual.Exists("ARBITARY_KEY"), "Key should not exist") + t.Assert(actual.Exists(key), "Inserted key should exist") +} + +func TestBiMap_InverseExists(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + actual := NewBiMap() + + actual.Insert(key, value) + t.Assert(!actual.ExistsInverse("ARBITARY_VALUE"), "Value should not exist") + t.Assert(actual.ExistsInverse(value), "Inserted value should exist") +} + +func TestBiMap_Get(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + actual := NewBiMap() + + actual.Insert(key, value) + + actualVal, ok := actual.Get(key) + + t.Assert(ok, "It should return true") + t.Equals(value, actualVal) + + actualVal, ok = actual.Get(value) + + t.Assert(!ok, "It should return false") + t.Assert(isEmpty(actualVal), "Actual val should be empty") +} + +func TestBiMap_GetInverse(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + actual := NewBiMap() + + actual.Insert(key, value) + + actualKey, ok := actual.GetInverse(value) + + t.Assert(ok, "It should return true") + t.Equals(key, actualKey) + + actualKey, ok = actual.Get(value) + + t.Assert(!ok, "It should return false") + t.Assert(isEmpty(actualKey), "Actual key should be empty") +} + +func TestBiMap_Size(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + actual := NewBiMap() + + t.Equals(0, actual.Size()) + + actual.Insert(key, value) + + t.Equals(1, actual.Size()) +} + +func TestBiMap_Delete(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + actual := NewBiMap() + dummyKey := "DummyKey" + dummyVal := "DummyVal" + actual.Insert(key, value) + actual.Insert(dummyKey, dummyVal) + + t.Equals(2, actual.Size()) + + actual.Delete(dummyKey) + + fwdExpected := make(map[interface{}]interface{}) + invExpected := make(map[interface{}]interface{}) + fwdExpected[key] = value + invExpected[value] = key + + expected := &BiMap{forward: fwdExpected, inverse: invExpected} + + t.Equals(1, actual.Size()) + t.Equals(expected, actual) + + actual.Delete(dummyKey) + + t.Equals(1, actual.Size()) + t.Equals(expected, actual) +} + +func TestBiMap_InverseDelete(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + actual := NewBiMap() + dummyKey := "DummyKey" + dummyVal := "DummyVal" + actual.Insert(key, value) + actual.Insert(dummyKey, dummyVal) + + t.Equals(2, actual.Size()) + + actual.DeleteInverse(dummyVal) + + fwdExpected := make(map[interface{}]interface{}) + invExpected := make(map[interface{}]interface{}) + fwdExpected[key] = value + invExpected[value] = key + + expected := &BiMap{forward: fwdExpected, inverse: invExpected} + + t.Equals(1, actual.Size()) + t.Equals(expected, actual) + + actual.DeleteInverse(dummyVal) + + t.Equals(1, actual.Size()) + t.Equals(expected, actual) +} + +func TestBiMap_WithVaryingType(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + actual := NewBiMap() + dummyKey := "Dummy key" + dummyVal := 3 + + actual.Insert(dummyKey, dummyVal) + + res, _ := actual.Get(dummyKey) + resVal, _ := actual.GetInverse(dummyVal) + t.Equals(dummyVal, res) + t.Equals(dummyKey, resVal) + +} + +func TestBiMap_MakeImmutable(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + actual := NewBiMap() + dummyKey := "Dummy key" + dummyVal := 3 + + actual.Insert(dummyKey, dummyVal) + + actual.MakeImmutable() + + panicked, _, _ := didPanic(func() { + actual.Delete(dummyKey) + }) + t.Assert(panicked, "It should panic on a mutation operation") + + val, _ := actual.Get(dummyKey) + + t.Equals(dummyVal, val) + + panicked, _, _ = didPanic(func() { + actual.DeleteInverse(dummyVal) + }) + t.Assert(panicked, "It should panic on a mutation operation") + + key, _ := actual.GetInverse(dummyVal) + + t.Equals(dummyKey, key) + + size := actual.Size() + + t.Equals(1, size) + + panicked, _, _ = didPanic(func() { + actual.Insert("New", 1) + }) + t.Assert(panicked, "It should panic on a mutation operation") + + size = actual.Size() + + t.Equals(1, size) + +} + +func TestBiMap_GetForwardMap(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + actual := NewBiMap() + dummyKey := "Dummy key" + dummyVal := 42 + + forwardMap := make(map[interface{}]interface{}) + forwardMap[dummyKey] = dummyVal + + actual.Insert(dummyKey, dummyVal) + + actualForwardMap := actual.GetForwardMap() + eq := reflect.DeepEqual(actualForwardMap, forwardMap) + t.Assert(eq, "Forward maps should be equal") +} + +func TestBiMap_GetInverseMap(tx *testing.T) { + t := ut.BeginTest(tx, false) + defer t.FinishTest() + actual := NewBiMap() + dummyKey := "Dummy key" + dummyVal := 42 + + inverseMap := make(map[interface{}]interface{}) + inverseMap[dummyVal] = dummyKey + + actual.Insert(dummyKey, dummyVal) + + actualInverseMap := actual.GetInverseMap() + eq := reflect.DeepEqual(actualInverseMap, inverseMap) + t.Assert(eq, "Inverse maps should be equal") +} diff --git a/go.mod b/go.mod index 2156a18..89badf6 100644 --- a/go.mod +++ b/go.mod @@ -8,4 +8,5 @@ require ( github.com/epiclabs-io/ut v0.0.0-20190416122157-8da7fe4b4947 github.com/goburrow/modbus v0.1.0 github.com/goburrow/serial v0.1.0 // indirect + github.com/stretchr/testify v1.6.1 ) diff --git a/go.sum b/go.sum index c4bb734..8a491b1 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +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/eclipse/paho.mqtt.golang v1.3.0 h1:MU79lqr3FKNKbSrGN7d7bNYqh8MwWW7Zcx0iG+VIw9I= github.com/eclipse/paho.mqtt.golang v1.3.0/go.mod h1:eTzb4gxwwyWpqBUHGQZ4ABAV7+Jgm1PklsYT/eo8Hcc= github.com/epiclabs-io/diff3 v0.0.0-20181217103619-05282cece609 h1:KHcpmcC/8cnCDXDm6SaCTajWF/vyUbBE1ovA27xYYEY= @@ -10,9 +12,17 @@ github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U= golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +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=