From 38f7e249474efa22e69fd92a487af728195b6ae1 Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Tue, 14 Apr 2026 16:56:56 +0200 Subject: [PATCH] structs: add tests and documentation --- pkg/structs/structs.go | 285 ++++++++++++++++++++---------------- pkg/structs/structs_test.go | 179 +++++++++++++++------- 2 files changed, 289 insertions(+), 175 deletions(-) diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go index 6131817532..57c55592fd 100644 --- a/pkg/structs/structs.go +++ b/pkg/structs/structs.go @@ -2,6 +2,7 @@ package structs import ( + "iter" "maps" "slices" @@ -17,6 +18,29 @@ func CopyOrZeroValue[T any](s *T) *T { return cp } +// Create an iterator from a slice, iterating over every single element of the slice, in order. +func Seq[T any](s []T) iter.Seq[T] { + return func(yield func(T) bool) { + for _, elem := range s { + if !yield(elem) { + return + } + } + } +} + +// Create an iterator from a slice that yields the position and the value, +// iterating over every single element of the slice, in order. +func Seq2[T any](s []T) iter.Seq2[int, T] { + return func(yield func(int, T) bool) { + for i, elem := range s { + if !yield(i, elem) { + return + } + } + } +} + // Returns a copy of an array with a unique set of elements. // // Element order is retained. @@ -34,6 +58,7 @@ func Uniq[T comparable](source []T) []T { return set } +// Returns a slice containing the keys of the map. func Keys[K comparable, V any](source map[K]V) []K { if source == nil { var zero []K @@ -42,6 +67,8 @@ func Keys[K comparable, V any](source map[K]V) []K { return slices.Collect(maps.Keys(source)) } +// Creates a map from a slice, using the indexer func to determine the key for each value, +// and the value being as-is. func Index[K comparable, V any](source []V, indexer func(V) K) map[K]V { if source == nil { var zero map[K]V @@ -55,6 +82,8 @@ func Index[K comparable, V any](source []V, indexer func(V) K) map[K]V { return result } +// Creates a slice from a slice, putting each value from the source slice through the +// mapper function to determine the value to store into the resulting slice. func Map[E any, R any](source []E, mapper func(E) R) []R { if source == nil { var zero []R @@ -67,67 +96,21 @@ func Map[E any, R any](source []E, mapper func(E) R) []R { return result } -func MapValues[K comparable, S any, T any](m map[K]S, mapper func(S) T) map[K]T { - r := make(map[K]T, len(m)) - for k, s := range m { - r[k] = mapper(s) - } - return r -} - -func MapValues2[K comparable, S any, T any](m map[K]S, mapper func(K, S) T) map[K]T { - r := make(map[K]T, len(m)) - for k, s := range m { - r[k] = mapper(k, s) - } - return r -} - -func MapKeys[S comparable, T comparable, V any](m map[S]V, mapper func(S) T) map[T]V { - r := make(map[T]V, len(m)) - for s, v := range m { - r[mapper(s)] = v - } - return r -} - -func MapKeys2[S comparable, T comparable, V any](m map[S]V, mapper func(S, V) T) map[T]V { - r := make(map[T]V, len(m)) - for s, v := range m { - r[mapper(s, v)] = v - } - return r -} - -func ToMap[E any, K comparable, V any](source []E, mapper func(E) (K, V)) map[K]V { - m := map[K]V{} - for _, e := range source { - k, v := mapper(e) - m[k] = v - } - return m -} - -func ToBoolMap[E comparable](source []E) map[E]bool { - m := make(map[E]bool, len(source)) - for _, v := range source { - m[v] = true - } - return m -} - -func ToIntMap[E comparable](source []E) map[E]int { - m := make(map[E]int, len(source)) - for _, v := range source { - if e, ok := m[v]; ok { - m[v] = e + 1 - } else { - m[v] = 1 +// Wraps an iterator with a transformer function. +func MapSeq[A, B any](it iter.Seq[A], transformer func(A) B) iter.Seq[B] { + return func(yield func(b B) bool) { + for v := range it { + t := transformer(v) + if !yield(t) { + return + } } } - return m } +// Creates a slice from a slice, putting each value from the source slice through the +// mapper function to determine the value to store into the resulting slice, but skipping +// the result of the mapper function if it returns nil. func MapN[E any, R any](source []E, indexer func(E) *R) []R { if source == nil { var zero []R @@ -143,6 +126,99 @@ func MapN[E any, R any](source []E, indexer func(E) *R) []R { return result } +// Creates a slice from a slice, putting each value from the source slice through the +// mapper function to determine the value to store into the resulting slice, but skipping +// the result of the mapper function if it returns false as its second return value. +func MapO[E any, R any](source []E, indexer func(E) (R, bool)) []R { + if source == nil { + var zero []R + return zero + } + result := []R{} + for _, e := range source { + if value, keep := indexer(e); keep { + result = append(result, value) + } + } + return result +} + +// Creates a map from a map, keeping each key as-is, and using the mapper +// function to determine the value to store into the resulting map. +func MapValues[K comparable, S any, T any](m map[K]S, mapper func(S) T) map[K]T { + r := make(map[K]T, len(m)) + for k, s := range m { + r[k] = mapper(s) + } + return r +} + +// Creates a map from a map, keeping each key as-is, and using the mapper function +// that takes both the key and the value to determine the value to store into the resulting map. +func MapValues2[K comparable, S any, T any](m map[K]S, mapper func(K, S) T) map[K]T { + r := make(map[K]T, len(m)) + for k, s := range m { + r[k] = mapper(k, s) + } + return r +} + +// Creates a map from a map, keeping each value as-is, and using the mapper +// function to determine the key to store into the resulting map. +func MapKeys[S comparable, T comparable, V any](m map[S]V, mapper func(S) T) map[T]V { + r := make(map[T]V, len(m)) + for s, v := range m { + r[mapper(s)] = v + } + return r +} + +// Creates a map from a map, keeping each value as-is, and using the mapper function +// that takes both the key and the value to determine the key to store into the resulting map. +func MapKeys2[S comparable, T comparable, V any](m map[S]V, mapper func(S, V) T) map[T]V { + r := make(map[T]V, len(m)) + for s, v := range m { + r[mapper(s, v)] = v + } + return r +} + +// Creates a map from a slice, using the mapper function to determine the key and value +// pair to use for each slice element in the resulting map. +func ToMap[E any, K comparable, V any](source []E, mapper func(E) (K, V)) map[K]V { + m := map[K]V{} + for _, e := range source { + k, v := mapper(e) + m[k] = v + } + return m +} + +// Creates a map of booleans, using the values of the source slice as keys in the +// resulting map. +func ToBoolMap[E comparable](source []E) map[E]bool { + m := make(map[E]bool, len(source)) + for _, v := range source { + m[v] = true + } + return m +} + +// Creates a map of ints, using the values of the source slice as keys in the +// resulting map, and storing the number of occurences of every given value +// as the int value in the map. +func ToIntMap[E comparable](source []E) map[E]int { + m := make(map[E]int, len(source)) + for _, v := range source { + if e, ok := m[v]; ok { + m[v] = e + 1 + } else { + m[v] = 1 + } + } + return m +} + // Check whether two slices contain the same elements, ignoring order. func SameSlices[E comparable](x, y []E) bool { // https://stackoverflow.com/a/36000696 @@ -168,73 +244,9 @@ func SameSlices[E comparable](x, y []E) bool { return len(diff) == 0 } -func Missing[E comparable](expected, actual []E) []E { - missing := []E{} - actualIndex := ToBoolMap(actual) - for _, e := range expected { - if _, ok := actualIndex[e]; !ok { - missing = append(missing, e) - } - } - return missing -} - -func FirstKey[K comparable, V any](m map[K]V) (K, bool) { - for k := range m { - return k, true - } - var zero K - return zero, false -} - -func Any[E any](s []E, predicate func(E) bool) bool { - if len(s) < 1 { - return false - } - for _, e := range s { - if predicate(e) { - return true - } - } - return false -} - -func AnyKey[K comparable, V any](m map[K]V, predicate func(K) bool) bool { - if len(m) < 1 { - return false - } - for k := range m { - if predicate(k) { - return true - } - } - return false -} - -func AnyValue[K comparable, V any](m map[K]V, predicate func(V) bool) bool { - if len(m) < 1 { - return false - } - for _, v := range m { - if predicate(v) { - return true - } - } - return false -} - -func AnyItem[K comparable, V any](m map[K]V, predicate func(K, V) bool) bool { - if len(m) < 1 { - return false - } - for k, v := range m { - if predicate(k, v) { - return true - } - } - return false -} - +// Concatenate the elements of multiple slices into a single slice. +// +// Element order is preserved. func Concat[E any](arys ...[]E) []E { l := 0 for _, ary := range arys { @@ -251,7 +263,18 @@ func Concat[E any](arys ...[]E) []E { return r } +// Create a new slice from a slice, determining whether each element should +// be added to the new slice by passing it to the predicate function. +// +// When the predicate function returns true, the element is stored in the +// new slice. +// When the predicate functoin returns false, the element is skipped and not +// stored in the new slice. func Filter[E any](s []E, predicate func(E) bool) []E { + if s == nil { + var zero []E + return zero + } r := []E{} for _, e := range s { if predicate(e) { @@ -260,3 +283,17 @@ func Filter[E any](s []E, predicate func(E) bool) []E { } return r } + +// Wrap an iterator with a conditional/filtering predicate function. +func FilterSeq[T any](it iter.Seq[T], predicate func(T) bool) iter.Seq[T] { + return func(yield func(s T) bool) { + for v := range it { + b := predicate(v) + if b { + if !yield(v) { + return + } + } + } + } +} diff --git a/pkg/structs/structs_test.go b/pkg/structs/structs_test.go index f81de10ce1..a9087fe60f 100644 --- a/pkg/structs/structs_test.go +++ b/pkg/structs/structs_test.go @@ -2,6 +2,7 @@ package structs import ( "fmt" + "slices" "strings" "testing" @@ -52,7 +53,7 @@ func TestUniqWithInts(t *testing.T) { {[]int{1, 1, 1}, []int{1}}, } for i, tt := range tests { - t.Run(fmt.Sprintf("%d: testing %v", i+1, tt.input), func(t *testing.T) { + t.Run(fmt.Sprintf("%d: testing %v", i+1, tt.input), func(t *testing.T) { //NOSONAR result := Uniq(tt.input) assert.EqualValues(t, tt.expected, result) }) @@ -101,68 +102,84 @@ func TestKeys(t *testing.T) { } } -func TestMissing(t *testing.T) { +func TestIndex(t *testing.T) { tests := []struct { - source []string input []string - expected []string + indexer func(string) string + expected map[string]string }{ - {[]string{"a", "b", "c"}, []string{"c", "b", "a"}, []string{}}, - {[]string{"a", "b", "c"}, []string{"c", "b"}, []string{"a"}}, - {[]string{"a", "b", "c"}, []string{"c", "b", "a", "d"}, []string{}}, - {[]string{}, []string{"c", "b"}, []string{}}, - {[]string{"a", "b", "c"}, []string{}, []string{"a", "b", "c"}}, - {[]string{"a", "b", "b", "c"}, []string{"a", "b"}, []string{"c"}}, + { + []string{"un", "deux", "trois"}, + strings.ToUpper, + map[string]string{"UN": "un", "DEUX": "deux", "TROIS": "trois"}, + }, } for i, tt := range tests { - t.Run(fmt.Sprintf("%d: testing [%v] <-> [%v] == [%v]", i+1, strings.Join(tt.source, ", "), strings.Join(tt.input, ", "), strings.Join(tt.expected, ", ")), func(t *testing.T) { - result := Missing(tt.source, tt.input) + t.Run(fmt.Sprintf("%d: testing %v", i+1, tt.input), func(t *testing.T) { + result := Index(tt.input, tt.indexer) assert.Equal(t, tt.expected, result) }) } } -func TestAny(t *testing.T) { - always := func(s string) bool { return true } - never := func(s string) bool { return false } - assert.True(t, Any([]string{"a", "b", "c"}, always)) - assert.False(t, Any([]string{}, always)) - assert.False(t, Any(nil, always)) - assert.False(t, Any([]string{"a", "b", "c"}, never)) - assert.False(t, Any(nil, never)) +func TestMap(t *testing.T) { + tests := []struct { + input []string + mapper func(string) int + expected []int + }{ + { + nil, + func(s string) int { return len(s) }, + nil, + }, + { + []string{}, + func(s string) int { return len(s) }, + []int{}, + }, + { + []string{"un", "deux", "trois"}, + func(s string) int { return len(s) }, + []int{2, 4, 5}, + }, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("%d: testing %v", i+1, tt.input), func(t *testing.T) { + result := Map(tt.input, tt.mapper) + assert.Equal(t, tt.expected, result) + }) + } } -func TestAnyKey(t *testing.T) { - always := func(s string) bool { return true } - never := func(s string) bool { return false } - - assert.True(t, AnyKey(map[string]bool{"a": true, "b": false}, always)) - assert.False(t, AnyKey(map[string]bool{}, always)) - assert.False(t, AnyKey[string, bool](nil, always)) - assert.False(t, AnyKey(map[string]bool{"a": true, "b": false}, never)) - assert.False(t, AnyKey[string, bool](nil, never)) -} - -func TestAnyValue(t *testing.T) { - always := func(b bool) bool { return true } - never := func(b bool) bool { return false } - - assert.True(t, AnyValue(map[string]bool{"a": true, "b": false}, always)) - assert.False(t, AnyValue(map[string]bool{}, always)) - assert.False(t, AnyValue[string](nil, always)) - assert.False(t, AnyValue(map[string]bool{"a": true, "b": false}, never)) - assert.False(t, AnyValue[string](nil, never)) -} - -func TestAnyItem(t *testing.T) { - always := func(s string, b bool) bool { return true } - never := func(s string, b bool) bool { return false } - - assert.True(t, AnyItem(map[string]bool{"a": true, "b": false}, always)) - assert.False(t, AnyItem(map[string]bool{}, always)) - assert.False(t, AnyItem(nil, always)) - assert.False(t, AnyItem(map[string]bool{"a": true, "b": false}, never)) - assert.False(t, AnyItem(nil, never)) +func TestMapSeq(t *testing.T) { + tests := []struct { + input []string + mapper func(string) int + expected []int + }{ + { + nil, + func(s string) int { return len(s) }, + nil, + }, + { + []string{}, + func(s string) int { return len(s) }, + nil, + }, + { + []string{"un", "deux", "trois"}, + func(s string) int { return len(s) }, + []int{2, 4, 5}, + }, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("%d: testing %v", i+1, tt.input), func(t *testing.T) { + result := slices.Collect(MapSeq(Seq(tt.input), tt.mapper)) + assert.Equal(t, tt.expected, result) + }) + } } func TestConcat(t *testing.T) { @@ -171,3 +188,63 @@ func TestConcat(t *testing.T) { assert.Equal(t, []string{"a"}, Concat([]string{}, nil, []string{"a"})) assert.Equal(t, []string{}, Concat[string]()) } + +func TestFilter(t *testing.T) { + tests := []struct { + input []int + predicate func(int) bool + expected []int + }{ + { + nil, + func(i int) bool { return i%2 == 0 }, + nil, + }, + { + []int{}, + func(i int) bool { return i%2 == 0 }, + []int{}, + }, + { + []int{1, 2, 3, 4, 5}, + func(i int) bool { return i%2 == 0 }, + []int{2, 4}, + }, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("%d: testing %v", i+1, tt.input), func(t *testing.T) { + result := Filter(tt.input, tt.predicate) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestFilterSeq(t *testing.T) { + tests := []struct { + input []int + predicate func(int) bool + expected []int + }{ + { + nil, + func(i int) bool { return i%2 == 0 }, + nil, + }, + { + []int{}, + func(i int) bool { return i%2 == 0 }, + nil, + }, + { + []int{1, 2, 3, 4, 5}, + func(i int) bool { return i%2 == 0 }, + []int{2, 4}, + }, + } + for i, tt := range tests { + t.Run(fmt.Sprintf("%d: testing %v", i+1, tt.input), func(t *testing.T) { + result := slices.Collect(FilterSeq(Seq(tt.input), tt.predicate)) + assert.Equal(t, tt.expected, result) + }) + } +}