- Add TryMap, Filter, Reduce, KeyBy, GroupBy, Uniq, UniqBy, Partition and Find, plus the Set type built with SetOf (Has, Add, Slice). - Add unit tests at 100% statement coverage and a runnable example for every function and Set method. - Complete the module README: contracts, use cases, Pluck rationale, standard-library pointers and the full API reference. - docs/architecture/introduction.md: add a Utilities row for sieve. - docs/setup/coming-from-wintercms.md: map Laravel collection methods to sieve and narrow the not-provided row to the fluent collection object.
304 lines
7.7 KiB
Go
304 lines
7.7 KiB
Go
package sieve
|
|
|
|
import (
|
|
"errors"
|
|
"slices"
|
|
"strconv"
|
|
"testing"
|
|
)
|
|
|
|
type ids []int
|
|
|
|
type item struct {
|
|
ID int
|
|
Name string
|
|
}
|
|
|
|
func TestMap(t *testing.T) {
|
|
t.Run("nil input", func(t *testing.T) {
|
|
got := Map([]int(nil), func(v int) string { return strconv.Itoa(v) })
|
|
if got == nil || len(got) != 0 {
|
|
t.Fatalf("Map(nil) = %#v, want non-nil empty", got)
|
|
}
|
|
})
|
|
t.Run("order and call count", func(t *testing.T) {
|
|
in := []int{3, 1, 2}
|
|
snap := slices.Clone(in)
|
|
calls := 0
|
|
got := Map(in, func(v int) int { calls++; return v * 10 })
|
|
if !slices.Equal(got, []int{30, 10, 20}) {
|
|
t.Fatalf("Map = %v", got)
|
|
}
|
|
if calls != len(in) {
|
|
t.Fatalf("calls = %d, want %d", calls, len(in))
|
|
}
|
|
got[0] = 99
|
|
if !slices.Equal(in, snap) {
|
|
t.Fatalf("input changed: %v", in)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestTryMap(t *testing.T) {
|
|
t.Run("success", func(t *testing.T) {
|
|
got, err := TryMap([]string{"1", "2", "3"}, strconv.Atoi)
|
|
if err != nil || !slices.Equal(got, []int{1, 2, 3}) {
|
|
t.Fatalf("TryMap = %v, %v", got, err)
|
|
}
|
|
})
|
|
t.Run("nil input", func(t *testing.T) {
|
|
got, err := TryMap([]string(nil), strconv.Atoi)
|
|
if err != nil || got == nil || len(got) != 0 {
|
|
t.Fatalf("TryMap(nil) = %#v, %v", got, err)
|
|
}
|
|
})
|
|
sentinel := errors.New("boom")
|
|
for _, failAt := range []int{0, 2} {
|
|
t.Run("fails at "+strconv.Itoa(failAt), func(t *testing.T) {
|
|
calls := 0
|
|
got, err := TryMap([]int{0, 1, 2, 3}, func(v int) (int, error) {
|
|
calls++
|
|
if v == failAt {
|
|
return 0, sentinel
|
|
}
|
|
return v, nil
|
|
})
|
|
if got != nil {
|
|
t.Fatalf("result = %#v, want nil", got)
|
|
}
|
|
if err != sentinel || !errors.Is(err, sentinel) {
|
|
t.Fatalf("err = %v, want sentinel", err)
|
|
}
|
|
if calls != failAt+1 {
|
|
t.Fatalf("calls = %d, want %d", calls, failAt+1)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFilter(t *testing.T) {
|
|
even := func(v int) bool { return v%2 == 0 }
|
|
tests := []struct {
|
|
name string
|
|
in []int
|
|
keep func(int) bool
|
|
want []int
|
|
}{
|
|
{"nil input", nil, even, []int{}},
|
|
{"no matches", []int{1, 3}, even, []int{}},
|
|
{"some matches", []int{1, 2, 3, 4}, even, []int{2, 4}},
|
|
{"all match", []int{2, 4}, even, []int{2, 4}},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
snap := slices.Clone(tt.in)
|
|
got := Filter(tt.in, tt.keep)
|
|
if got == nil || !slices.Equal(got, tt.want) {
|
|
t.Fatalf("Filter = %#v, want %v", got, tt.want)
|
|
}
|
|
if len(got) > 0 {
|
|
got[0] = -1
|
|
}
|
|
if !slices.Equal(tt.in, snap) {
|
|
t.Fatalf("input changed: %v", tt.in)
|
|
}
|
|
})
|
|
}
|
|
t.Run("named type", func(t *testing.T) {
|
|
var got ids = Filter(ids{1, 2, 3}, even)
|
|
if !slices.Equal(got, ids{2}) {
|
|
t.Fatalf("Filter = %v", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestReduce(t *testing.T) {
|
|
sum := func(a, v int) int { return a + v }
|
|
if got := Reduce([]int{1, 2, 3}, 0, sum); got != 6 {
|
|
t.Fatalf("sum = %d", got)
|
|
}
|
|
if got := Reduce([]int(nil), 42, sum); got != 42 {
|
|
t.Fatalf("Reduce(nil) = %d, want init", got)
|
|
}
|
|
if got := Reduce([]string{"a", "b", "c"}, "", func(a, v string) string { return a + v }); got != "abc" {
|
|
t.Fatalf("concat = %q", got)
|
|
}
|
|
if got := Reduce([]int{1, 2}, "", func(a string, v int) string { return a + strconv.Itoa(v) }); got != "12" {
|
|
t.Fatalf("string accumulator = %q", got)
|
|
}
|
|
counts := Reduce([]int{1, 2, 1}, map[int]int{}, func(m map[int]int, v int) map[int]int { m[v]++; return m })
|
|
if counts[1] != 2 || counts[2] != 1 {
|
|
t.Fatalf("map accumulator = %v", counts)
|
|
}
|
|
}
|
|
|
|
func TestKeyBy(t *testing.T) {
|
|
in := []item{{1, "a"}, {2, "b"}, {1, "c"}}
|
|
calls := 0
|
|
got := KeyBy(in, func(it item) int { calls++; return it.ID })
|
|
if len(got) != 2 || got[1].Name != "c" || got[2].Name != "b" {
|
|
t.Fatalf("KeyBy = %v", got)
|
|
}
|
|
if calls != len(in) {
|
|
t.Fatalf("calls = %d", calls)
|
|
}
|
|
empty := KeyBy([]item(nil), func(it item) int { return it.ID })
|
|
if empty == nil || len(empty) != 0 {
|
|
t.Fatalf("KeyBy(nil) = %#v", empty)
|
|
}
|
|
}
|
|
|
|
func TestGroupBy(t *testing.T) {
|
|
in := ids{1, 2, 3, 4, 5}
|
|
snap := slices.Clone(in)
|
|
var got map[bool]ids = GroupBy(in, func(v int) bool { return v%2 == 0 })
|
|
if !slices.Equal(got[false], ids{1, 3, 5}) || !slices.Equal(got[true], ids{2, 4}) {
|
|
t.Fatalf("GroupBy = %v", got)
|
|
}
|
|
odd := got[false]
|
|
got[false] = append(got[false], 7)
|
|
got[false][0] = 100
|
|
odd[1] = 200
|
|
if !slices.Equal(in, snap) {
|
|
t.Fatalf("input changed: %v", in)
|
|
}
|
|
if !slices.Equal(got[true], ids{2, 4}) {
|
|
t.Fatalf("other group changed: %v", got[true])
|
|
}
|
|
empty := GroupBy([]int(nil), func(v int) int { return v })
|
|
if empty == nil || len(empty) != 0 {
|
|
t.Fatalf("GroupBy(nil) = %#v", empty)
|
|
}
|
|
}
|
|
|
|
func TestUniq(t *testing.T) {
|
|
in := []int{3, 1, 3, 2, 1}
|
|
snap := slices.Clone(in)
|
|
if got := Uniq(in); !slices.Equal(got, []int{3, 1, 2}) {
|
|
t.Fatalf("Uniq = %v", got)
|
|
}
|
|
if !slices.Equal(in, snap) {
|
|
t.Fatalf("input changed: %v", in)
|
|
}
|
|
distinct := []int{1, 2, 3}
|
|
got := Uniq(distinct)
|
|
got[0] = 9
|
|
if distinct[0] != 1 {
|
|
t.Fatalf("Uniq aliased its input")
|
|
}
|
|
empty := Uniq([]int(nil))
|
|
if empty == nil || len(empty) != 0 {
|
|
t.Fatalf("Uniq(nil) = %#v", empty)
|
|
}
|
|
var named ids = Uniq(ids{1, 1})
|
|
if !slices.Equal(named, ids{1}) {
|
|
t.Fatalf("Uniq(ids) = %v", named)
|
|
}
|
|
}
|
|
|
|
func TestUniqBy(t *testing.T) {
|
|
in := []item{{1, "a"}, {2, "b"}, {1, "c"}, {3, "d"}, {2, "e"}}
|
|
calls := 0
|
|
got := UniqBy(in, func(it item) int { calls++; return it.ID })
|
|
want := []item{{1, "a"}, {2, "b"}, {3, "d"}}
|
|
if !slices.Equal(got, want) {
|
|
t.Fatalf("UniqBy = %v", got)
|
|
}
|
|
if calls != len(in) {
|
|
t.Fatalf("calls = %d", calls)
|
|
}
|
|
empty := UniqBy([]item(nil), func(it item) int { return it.ID })
|
|
if empty == nil || len(empty) != 0 {
|
|
t.Fatalf("UniqBy(nil) = %#v", empty)
|
|
}
|
|
}
|
|
|
|
func TestPartition(t *testing.T) {
|
|
even := func(v int) bool { return v%2 == 0 }
|
|
matched, rest := Partition([]int{1, 2, 3, 4, 5}, even)
|
|
if !slices.Equal(matched, []int{2, 4}) || !slices.Equal(rest, []int{1, 3, 5}) {
|
|
t.Fatalf("Partition = %v, %v", matched, rest)
|
|
}
|
|
matched, rest = Partition([]int(nil), even)
|
|
if matched == nil || rest == nil || len(matched) != 0 || len(rest) != 0 {
|
|
t.Fatalf("Partition(nil) = %#v, %#v", matched, rest)
|
|
}
|
|
in := []int{2, 4}
|
|
matched, rest = Partition(in, even)
|
|
if rest == nil || len(rest) != 0 || !slices.Equal(matched, in) {
|
|
t.Fatalf("Partition(all) = %#v, %#v", matched, rest)
|
|
}
|
|
matched[0] = 9
|
|
if in[0] != 2 {
|
|
t.Fatalf("Partition aliased its input")
|
|
}
|
|
var m, r ids = Partition(ids{1, 2}, even)
|
|
if !slices.Equal(m, ids{2}) || !slices.Equal(r, ids{1}) {
|
|
t.Fatalf("Partition(ids) = %v, %v", m, r)
|
|
}
|
|
}
|
|
|
|
func TestFind(t *testing.T) {
|
|
calls := 0
|
|
got, ok := Find([]int{1, 4, 6, 8}, func(v int) bool { calls++; return v%2 == 0 })
|
|
if !ok || got != 4 {
|
|
t.Fatalf("Find = %d, %v", got, ok)
|
|
}
|
|
if calls != 2 {
|
|
t.Fatalf("calls = %d, want 2", calls)
|
|
}
|
|
got, ok = Find([]int{1, 3}, func(v int) bool { return v%2 == 0 })
|
|
if ok || got != 0 {
|
|
t.Fatalf("Find(no match) = %d, %v", got, ok)
|
|
}
|
|
s, ok := Find([]string(nil), func(string) bool { return true })
|
|
if ok || s != "" {
|
|
t.Fatalf("Find(nil) = %q, %v", s, ok)
|
|
}
|
|
}
|
|
|
|
func TestSet(t *testing.T) {
|
|
s := SetOf(1, 2, 2, 3)
|
|
if len(s) != 3 {
|
|
t.Fatalf("len = %d", len(s))
|
|
}
|
|
if !s.Has(2) || s.Has(4) {
|
|
t.Fatalf("Has wrong: %v", s)
|
|
}
|
|
s.Add(4, 5, 4)
|
|
if len(s) != 5 || !s.Has(5) {
|
|
t.Fatalf("Add wrong: %v", s)
|
|
}
|
|
delete(s, 1)
|
|
if s.Has(1) {
|
|
t.Fatalf("delete had no effect")
|
|
}
|
|
got := s.Slice()
|
|
slices.Sort(got)
|
|
if !slices.Equal(got, []int{2, 3, 4, 5}) {
|
|
t.Fatalf("Slice = %v", got)
|
|
}
|
|
|
|
empty := SetOf[string]()
|
|
if empty == nil {
|
|
t.Fatalf("SetOf() is nil")
|
|
}
|
|
empty.Add("a")
|
|
if !empty.Has("a") {
|
|
t.Fatalf("Add on SetOf() failed")
|
|
}
|
|
|
|
from := SetOf(ids{7, 7, 8}...)
|
|
if len(from) != 2 {
|
|
t.Fatalf("SetOf(ids...) = %v", from)
|
|
}
|
|
|
|
var zero Set[int]
|
|
if zero.Has(1) {
|
|
t.Fatalf("nil Set Has true")
|
|
}
|
|
if sl := zero.Slice(); sl == nil || len(sl) != 0 {
|
|
t.Fatalf("nil Set Slice = %#v", sl)
|
|
}
|
|
}
|