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
45 changes: 41 additions & 4 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func CompareKeys[K comparable, V any](a, b map[K]V) ([]K, []K) {

// MissingKeys returns the keys that are in a but not b
func MissingKeys[K comparable, V any](a, b map[K]V) []K {
// Pre-allocate with capacity of a since that's the maximum possible size
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why so pessimistic? :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment was written by Claude 3.7 Sonnet. I think it's correct though.

Copy link
Collaborator

@dncohen dncohen May 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but if a has 100 keys and b has all of them, you're building a map with capacity 100 to store nothing.

onlyA := make([]K, 0, len(a))
for k := range a {
if _, ok := b[k]; !ok {
Expand All @@ -35,12 +36,19 @@ func MissingKeys[K comparable, V any](a, b map[K]V) []K {
return onlyA
}

// EqualKeys checks if two maps have exactly the same keys.
// Returns true if both maps contain the same set of keys, regardless of values.
func EqualKeys[K comparable, V any](a, b map[K]V) bool {
if len(a) != len(b) {
return false
}
if len(MissingKeys(a, b)) != 0 {
return false
// We don't need to check if there are extras in b not in a because
// we checked that there are an equal number of keys so all we have
// to check is that all of a is in b
for k := range a {
if _, ok := b[k]; !ok {
return false
}
}
return true
}
Expand All @@ -49,25 +57,48 @@ func CopyMap[K comparable, V any](m map[K]V) map[K]V {
if m == nil {
return nil
}
newM := make(map[K]V)
// Pre-allocate with capacity of m
newM := make(map[K]V, len(m))
for k, v := range m {
newM[k] = v
}
return newM
}

// CopyMapSubset creates a new map containing only the specified keys from the original map
func CopyMapSubset[K comparable, V any](m map[K]V, keys []K) map[K]V {
if m == nil {
return nil
}

if len(m) == 0 {
return make(map[K]V)
}

// Pre-allocate with capacity of keys since that's the maximum possible size
result := make(map[K]V, len(keys))
for _, k := range keys {
if v, ok := m[k]; ok {
result[k] = v
}
}
return result
}

// Merge copies b onto a, overriding any common keys:
// a is modified and returned.
func Merge[K comparable, V any](a, b map[K]V) map[K]V {
if a == nil {
return b
return CopyMap(b)
}
for k, v := range b {
a[k] = v
}
return a
}

// AllKeys returns true if all keys in the map satisfy the given filter function.
// Returns true for empty maps (vacuous truth).
func AllKeys[K comparable, V any](m map[K]V, filter func(K) bool) bool {
for k := range m {
if !filter(k) {
Expand All @@ -77,6 +108,8 @@ func AllKeys[K comparable, V any](m map[K]V, filter func(K) bool) bool {
return true
}

// AnyKey returns true if at least one key in the map satisfies the given filter function.
// Returns false for empty maps.
func AnyKey[K comparable, V any](m map[K]V, filter func(K) bool) bool {
for k := range m {
if filter(k) {
Expand All @@ -86,6 +119,8 @@ func AnyKey[K comparable, V any](m map[K]V, filter func(K) bool) bool {
return false
}

// AllValues returns true if all values in the map satisfy the given filter function.
// Returns true for empty maps (vacuous truth).
func AllValues[K comparable, V any](m map[K]V, filter func(V) bool) bool {
for _, v := range m {
if !filter(v) {
Expand All @@ -95,6 +130,8 @@ func AllValues[K comparable, V any](m map[K]V, filter func(V) bool) bool {
return true
}

// AnyValue returns true if at least one value in the map satisfies the given filter function.
// Returns false for empty maps.
func AnyValue[K comparable, V any](m map[K]V, filter func(V) bool) bool {
for _, v := range m {
if filter(v) {
Expand Down
113 changes: 113 additions & 0 deletions map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,119 @@ func TestCopyMap(t *testing.T) {
})
}

func TestCopyMapSubset(t *testing.T) {
t.Parallel()

t.Run("copies specified keys", func(t *testing.T) {
t.Parallel()

original := map[string]int{
"a": 1,
"b": 2,
"c": 3,
"d": 4,
}

keys := []string{"a", "c"}
subset := generic.CopyMapSubset(original, keys)

t.Log("Should copy only specified keys from the original map")
assert.Len(t, subset, 2)
assert.Equal(t, 1, subset["a"])
assert.Equal(t, 3, subset["c"])
assert.NotContains(t, subset, "b")
assert.NotContains(t, subset, "d")
})

t.Run("ignores keys not in original map", func(t *testing.T) {
t.Parallel()

original := map[string]int{
"a": 1,
"b": 2,
}

keys := []string{"a", "c", "d"}
subset := generic.CopyMapSubset(original, keys)

t.Log("Should only copy keys that exist in the original map")
assert.Len(t, subset, 1)
assert.Equal(t, 1, subset["a"])
assert.NotContains(t, subset, "c")
assert.NotContains(t, subset, "d")
})

t.Run("returns empty map for nil input", func(t *testing.T) {
t.Parallel()

var original map[string]int = nil
keys := []string{"a", "b"}
subset := generic.CopyMapSubset(original, keys)

t.Log("Should return empty map when input map is nil")
assert.Nil(t, subset)
})

t.Run("handles empty keys", func(t *testing.T) {
t.Parallel()

original := map[string]int{
"a": 1,
"b": 2,
}

var keys []string = nil
subset := generic.CopyMapSubset(original, keys)

t.Log("Should return empty map when keys slice is nil")
assert.Empty(t, subset)
assert.NotNil(t, subset)

keys = []string{}
subset = generic.CopyMapSubset(original, keys)

t.Log("Should return empty map when keys slice is empty")
assert.Empty(t, subset)
assert.NotNil(t, subset)
})

t.Run("handles empty map", func(t *testing.T) {
t.Parallel()

original := make(map[string]int)
keys := []string{"a", "b"}
subset := generic.CopyMapSubset(original, keys)

t.Log("Should return empty map when input map is empty")
assert.Empty(t, subset)
assert.NotNil(t, subset)
})

t.Run("handles complex values", func(t *testing.T) {
t.Parallel()

type User struct {
Name string
Admin bool
}

original := map[string]User{
"a": {Name: "Alice", Admin: false},
"b": {Name: "Bob", Admin: true},
"c": {Name: "Charlie", Admin: false},
}

keys := []string{"a", "c"}
subset := generic.CopyMapSubset(original, keys)

t.Log("Should work with complex value types")
assert.Len(t, subset, 2)
assert.Equal(t, User{Name: "Alice", Admin: false}, subset["a"])
assert.Equal(t, User{Name: "Charlie", Admin: false}, subset["c"])
assert.NotContains(t, subset, "b")
})
}

func TestMerge(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 1 addition & 1 deletion set.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package generic

func ToSet[T comparable](slice []T) map[T]struct{} {
m := make(map[T]struct{})
m := make(map[T]struct{}, len(slice))
for _, item := range slice {
m[item] = struct{}{}
}
Expand Down
Loading