diff --git a/Changes.md b/Changes.md index 3c9e0e3..bf31312 100644 --- a/Changes.md +++ b/Changes.md @@ -1,3 +1,9 @@ +WIP TBD + + * Added maps.MergeInPlace. + * Added slices.MapSlice and slices.MapMap. + * maps.Merge now allocates room equal to the size of all input maps in the output map. + v0.3.0 2023-07-13 * Added slices.FirstIndex and slices.GrepIndex. diff --git a/README.md b/README.md index e957237..01a55d8 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,8 @@ off-by-one errors, so the following ops are provided: * `Unshift(slice, value)` * `Concat(slice...)` * `Map(slice, mapper)` + * `MapSlice(slice, mapper)` + * `MapMap(slice, mapper)` * `Reduce(slice, reducer)` * `Reductions(slice, reducer)` * `ReduceAcc(slice, start, reducer)` @@ -87,6 +89,7 @@ There are a lot of common map operations that are missing from the built-in standard library: * `Merge(...maps)` + * `MergeInPlace(base, ...maps)` * `Diff(a, b)` ## Generic Sets diff --git a/maps/merge.go b/maps/merge.go index 1e1abb4..28f8308 100644 --- a/maps/merge.go +++ b/maps/merge.go @@ -1,17 +1,31 @@ package maps -import "github.com/zostay/go-std/generic" +import ( + "github.com/zostay/go-std/generic" +) -// Merge maps into a single map. Keys in maps later in the list will overwrite -// keys found in earlier maps. +// Merge maps many maps into a single map. Keys in maps later in the list will +// overwrite keys found in earlier maps. The returned map will be a newly +// allocated map. func Merge[K comparable, V any](maps ...map[K]V) map[K]V { - out := map[K]V{} + size := 0 for _, m := range maps { + size += len(m) + } + return MergeInPlace(make(map[K]V, size), maps...) +} + +// MergeInPlace maps many maps into a single map. The base map will be updated +// to include all the pairs in the merge maps. Each map will be itereated in +// order with values in later maps overwriting those in early maps. When +// complete, the base map is returned. +func MergeInPlace[K comparable, V any](base map[K]V, merge ...map[K]V) map[K]V { + for _, m := range merge { for k, v := range m { - out[k] = v + base[k] = v } } - return out + return base } // Diff returns three maps from two. The first map returned is a map of diff --git a/maps/merge_test.go b/maps/merge_test.go index 573453d..7a64496 100644 --- a/maps/merge_test.go +++ b/maps/merge_test.go @@ -28,6 +28,23 @@ func TestMerge(t *testing.T) { }, maps.Merge(a, b, c)) } +func TestMergeInPlace(t *testing.T) { + a := map[string]int{"a": 1, "b": 2} + b := map[string]int{"c": 3, "d": 4} + c := map[string]int{"a": 5, "b": 6, "c": 7} + + assert.Equal(t, map[string]int{}, maps.MergeInPlace(map[string]int{})) + assert.Equal(t, map[string]int{ + "a": 1, "b": 2, + }, maps.MergeInPlace(a)) + assert.Equal(t, map[string]int{ + "a": 1, "b": 2, "c": 3, "d": 4, + }, maps.MergeInPlace(a, b)) + assert.Equal(t, map[string]int{ + "a": 5, "b": 6, "c": 7, "d": 4, + }, maps.MergeInPlace(a, c)) +} + func TestDiff(t *testing.T) { a := map[string]int{"one": 1, "two": 2, "three": 3, "four": 4} b := map[string]int{"one": 5, "three": 6, "five": 7, "seven": 8} diff --git a/slices/map.go b/slices/map.go index 3e6c46a..8dd7071 100644 --- a/slices/map.go +++ b/slices/map.go @@ -1,6 +1,9 @@ package slices -import "github.com/zostay/go-std/generic" +import ( + "github.com/zostay/go-std/generic" + "github.com/zostay/go-std/maps" +) // Map transforms the input into the output value using the mapper function. func Map[In, Out any](in []In, mapper func(in In) Out) []Out { @@ -11,6 +14,30 @@ func Map[In, Out any](in []In, mapper func(in In) Out) []Out { return out } +// MapSlice transforms the input into an output slice using the mapper function. +// A single input value may map to multiple or zero output values. +func MapSlice[In, Out any](in []In, mapper func(in In) []Out) []Out { + out := make([]Out, 0, len(in)) + for _, t := range in { + out = append(out, mapper(t)...) + } + return out +} + +// MapMap transforms the input into an output map using the mapper function. +// This will allocate a master map. The values returned by each call to mapper +// for each item in the input argument named in will be added to that map. If +// keys are added later that have already been added to the map being built, the +// new values will overwrite the old. +func MapMap[In any, K comparable, V any](in []In, mapper func(in In) map[K]V) map[K]V { + out := make(map[K]V, len(in)) + for _, t := range in { + outMap := mapper(t) + out = maps.Merge(out, outMap) + } + return out +} + // Reduce collapses the input slice into a single value using the reducer // function. func Reduce[In, Out any](in []In, reducer func(u Out, t In) Out) Out { diff --git a/slices/map_test.go b/slices/map_test.go index 7899bc4..e6b86ed 100644 --- a/slices/map_test.go +++ b/slices/map_test.go @@ -45,6 +45,35 @@ func TestMap(t *testing.T) { assert.Equal(t, []string{}, ss) } +func TestMapSlice(t *testing.T) { + halveTheOdds := func(i int) []float64 { + if i%2 == 1 { + return []float64{float64(i) / 2.0} + } else { + return []float64{} + } + } + + in := []int{1, 2, 3} + ss := slices.MapSlice(in, halveTheOdds) + assert.Equal(t, []float64{0.5, 1.5}, ss) + ss = slices.MapSlice([]int{}, halveTheOdds) + assert.Equal(t, []float64{}, ss) +} + +func TestMapMap(t *testing.T) { + stringToInt := func(in int) map[string]int { + return map[string]int{ + strconv.Itoa(in): in, + } + } + in := []int{1, 2, 3} + ss := slices.MapMap(in, stringToInt) + assert.Equal(t, map[string]int{"1": 1, "2": 2, "3": 3}, ss) + ss = slices.MapMap([]int{}, stringToInt) + assert.Equal(t, map[string]int{}, ss) +} + func TestReduce(t *testing.T) { in := []int{1, 2, 3} s := slices.Reduce(in, subtract)