Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v2
with:
go-version: 1.18
go-version: 1.23

- name: Build
run: go build -v ./...
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@

# Dependency directories (remove the comment below to include it)
# vendor/

# OS specific files
.DS_Store
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ different kinds of maps.
Using the same interface, you can create and use a standard Go map, a map
that is safe for concurrency and/or a map that lets you order the keys in the map.

A Set class is included for quickly determining membership in a group.

## Example

```go
Expand All @@ -24,14 +26,13 @@ type myStdMap = StdMap[string, int]
func main() {
m := new(Map[string, int])

m.Merge(myStdMap{"b":2, "c":3})
m.Copy(myStdMap{"b":2, "c":3})
m.Set("a",1)

sum := 0
m.Range(func(k string, v int) bool {
for v := range m.All() {
sum += v
return true
})
}
fmt.Print(sum)
}

Expand Down
35 changes: 35 additions & 0 deletions equaler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package maps

// Equaler is the interface that implements an Equal function and that provides a way for the
// various MapI like objects to determine if they are equal.
//
// In particular, if your Map has
// non-comparible values, like a slice, but you would still like to call Equal() on that
// map, define an Equal function on the values to do the comparison. For example:
//
// type mySlice []int
//
// func (s mySlice) Equal(b any) bool {
// if s2, ok := b.(mySlice); ok {
// if len(s) == len(s2) {
// for i, v := range s2 {
// if s[i] != v {
// return false
// }
// }
// return true
// }
// }
// return false
// }
type Equaler interface {
Equal(a any) bool
}

func equalValues(a, b any) bool {
if e, ok := a.(Equaler); ok {
return e.Equal(b)
}

return a == b
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
module github.com/goradd/maps

go 1.18
go 1.23

require github.com/stretchr/testify v1.8.4
require github.com/stretchr/testify v1.9.0

require (
github.com/davecgh/go-spew v1.1.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
90 changes: 78 additions & 12 deletions map.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package maps

import (
"iter"
)

// Map is a go map that uses a standard set of functions shared with other Map-like types.
//
// The recommended way to create a Map is to first declare a concrete type alias, and then call
Expand All @@ -14,37 +18,47 @@ type Map[K comparable, V any] struct {
items StdMap[K, V]
}

// NewMap creates a new map that maps values of type K to values of type V.
// Pass in zero or more standard maps and the contents of those maps will be copied to the new Map.
func NewMap[K comparable, V any](sources ...map[K]V) *Map[K, V] {
m := new(Map[K, V])
for _, i := range sources {
m.Copy(Cast(i))
}
return m
}

// Clear resets the map to an empty map
func (m *Map[K, V]) Clear() {
m.items = nil
}

// Len returns the number of items in the map
func (m Map[K, V]) Len() int {
func (m *Map[K, V]) Len() int {
return m.items.Len()
}

// Range calls the given function for each key,value pair in the map.
// This is the same interface as sync.Map.Range().
// While its safe to call methods of the map from within the Range function, its discouraged.
// If you ever switch to one of the SafeMap maps, it will cause a deadlock.
func (m Map[K, V]) Range(f func(k K, v V) bool) {
func (m *Map[K, V]) Range(f func(k K, v V) bool) {
m.items.Range(f)
}

// Load returns the value based on its key, and a boolean indicating whether it exists in the map.
// This is the same interface as sync.Map.Load()
func (m Map[K, V]) Load(k K) (V, bool) {
func (m *Map[K, V]) Load(k K) (V, bool) {
return m.items.Load(k)
}

// Get returns the value for the given key. If the key does not exist, the zero value will be returned.
func (m Map[K, V]) Get(k K) V {
func (m *Map[K, V]) Get(k K) V {
return m.items.Get(k)
}

// Has returns true if the key exists.
func (m Map[K, V]) Has(k K) bool {
func (m *Map[K, V]) Has(k K) bool {
return m.items.Has(k)
}

Expand All @@ -54,12 +68,12 @@ func (m Map[K, V]) Delete(k K) V {
}

// Keys returns a new slice containing the keys of the map.
func (m Map[K, V]) Keys() []K {
func (m *Map[K, V]) Keys() []K {
return m.items.Keys()
}

// Values returns a new slice containing the values of the map.
func (m Map[K, V]) Values() []V {
func (m *Map[K, V]) Values() []V {
return m.items.Values()
}

Expand All @@ -73,23 +87,29 @@ func (m *Map[K, V]) Set(k K, v V) {
}

// Merge copies the items from in to the map, overwriting any conflicting keys.
// Deprecated: Call Copy instead.
func (m *Map[K, V]) Merge(in MapI[K, V]) {
m.Copy(in)
}

// Copy copies the items from in to the map, overwriting any conflicting keys.
func (m *Map[K, V]) Copy(in MapI[K, V]) {
if m.items == nil {
m.items = make(map[K]V, in.Len())
}
m.items.Merge(in)
m.items.Copy(in)
}

// Equal returns true if all the keys and values are equal.
//
// If the values are not comparable, you should implement the Equaler interface on the values.
// Otherwise, you will get a runtime panic.
func (m Map[K, V]) Equal(m2 MapI[K, V]) bool {
func (m *Map[K, V]) Equal(m2 MapI[K, V]) bool {
return m.items.Equal(m2)
}

// MarshalBinary implements the BinaryMarshaler interface to convert the map to a byte stream.
func (m Map[K, V]) MarshalBinary() ([]byte, error) {
func (m *Map[K, V]) MarshalBinary() ([]byte, error) {
return m.items.MarshalBinary()
}

Expand All @@ -105,7 +125,7 @@ func (m *Map[K, V]) UnmarshalBinary(data []byte) (err error) {
}

// MarshalJSON implements the json.Marshaler interface to convert the map into a JSON object.
func (m Map[K, V]) MarshalJSON() (out []byte, err error) {
func (m *Map[K, V]) MarshalJSON() (out []byte, err error) {
return m.items.MarshalJSON()
}

Expand All @@ -116,6 +136,52 @@ func (m *Map[K, V]) UnmarshalJSON(in []byte) (err error) {
}

// String returns the map as a string.
func (m Map[K, V]) String() string {
func (m *Map[K, V]) String() string {
return m.items.String()
}

// All returns an iterator over all the items in the map.
func (m *Map[K, V]) All() iter.Seq2[K, V] {
return m.items.All()
}

// KeysIter returns an iterator over all the keys in the map.
func (m *Map[K, V]) KeysIter() iter.Seq[K] {
return m.items.KeysIter()
}

// ValuesIter returns an iterator over all the values in the map.
func (m *Map[K, V]) ValuesIter() iter.Seq[V] {
return m.items.ValuesIter()
}

// Insert adds the values from seq to the map.
// Duplicate keys are overridden.
func (m *Map[K, V]) Insert(seq iter.Seq2[K, V]) {
if m.items == nil {
m.items = map[K]V{}
}

m.items.Insert(seq)
}

// CollectMap collects key-value pairs from seq into a new Map
// and returns it.
func CollectMap[K comparable, V any](seq iter.Seq2[K, V]) *Map[K, V] {
m := new(Map[K, V])
m.Insert(seq)
return m
}

// Clone returns a copy of the Map. This is a shallow clone:
// the new keys and values are set using ordinary assignment.
func (m *Map[K, V]) Clone() *Map[K, V] {
m1 := new(Map[K, V])
m1.items = m.items.Clone()
return m1
}

// DeleteFunc deletes any key/value pairs for which del returns true.
func (m *Map[K, V]) DeleteFunc(del func(K, V) bool) {
m.items.DeleteFunc(del)
}
15 changes: 15 additions & 0 deletions map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package maps
import (
"encoding/gob"
"fmt"
"github.com/stretchr/testify/assert"
"testing"
)

Expand All @@ -21,3 +22,17 @@ func ExampleMap_String() {
fmt.Print(m)
// Output: {"a":1, "b":2}
}

func ExampleCollectMap() {
m1 := StdMap[string, int]{"a": 1, "b": 2, "c": 3}
m2 := CollectMap(m1.All())
fmt.Println(m2.String())
// Output: {"a":1, "b":2, "c":3}
}

func TestMap_Clone(t *testing.T) {
m1 := StdMap[string, int]{"a": 1, "b": 2, "c": 3}
m2 := CollectMap(m1.All())
m3 := m2.Clone()
assert.True(t, m1.Equal(m3))
}
29 changes: 28 additions & 1 deletion mapi.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package maps

import "iter"

// MapI is the interface used by all the Map types.
type MapI[K comparable, V any] interface {
Setter[K, V]
Expand All @@ -13,7 +15,13 @@ type MapI[K comparable, V any] interface {
Values() []V
Merge(MapI[K, V])
Equal(MapI[K, V]) bool
Delete(k K) (v V)
Delete(k K) V
All() iter.Seq2[K, V]
KeysIter() iter.Seq[K]
ValuesIter() iter.Seq[V]
Insert(seq iter.Seq2[K, V])
DeleteFunc(del func(K, V) bool)
String() string
}

// Setter sets a value in a map.
Expand All @@ -30,3 +38,22 @@ type Getter[K comparable, V any] interface {
type Loader[K comparable, V any] interface {
Load(k K) (v V, ok bool)
}

// EqualFunc returns true if all the keys and values of the m1 and m2 are equal.
//
// The function eq is called on the values to determine equality. Keys are compared using ==.
// If one of the maps is a "safe" map, its more efficient to pass that map as m2.
func EqualFunc[K comparable, V1, V2 any](m1 MapI[K, V1], m2 MapI[K, V2], eq func(V1, V2) bool) bool {
if m1.Len() != m2.Len() {
return false
}
ret := true
m2.Range(func(k K, v V2) bool {
if !m1.Has(k) || !eq(m1.Get(k), v) {
ret = false
return false
}
return true
})
return ret
}
Loading