From 47ca46fdf5d06754924271d00fc6bc3273dc215a Mon Sep 17 00:00:00 2001 From: jpfourny Date: Sun, 21 Jan 2024 14:19:32 -0500 Subject: [PATCH] Adapters for Go 1.22 range-over-func, refinements to iterator and numeric ranging. --- examples/stream/map_test.go | 6 +-- pkg/pair/pair.go | 30 ++++++------ pkg/stream/aggregate.go | 2 +- pkg/stream/combine.go | 33 +------------ pkg/stream/combine_test.go | 36 -------------- pkg/stream/generate.go | 26 +++++++++- pkg/stream/generate_test.go | 12 +++++ pkg/stream/iterate.go | 79 +++++++++++++++++++++++-------- pkg/stream/iterate_test.go | 74 +++++++++++++++++++++++++---- pkg/stream/mapper/convert.go | 20 ++++---- pkg/stream/mapper/convert_test.go | 44 ++++++++--------- pkg/stream/match.go | 14 +++--- pkg/stream/rangefunc.go | 39 +++++++++++++++ pkg/stream/rangefunc_test.go | 63 ++++++++++++++++++++++++ 14 files changed, 323 insertions(+), 155 deletions(-) create mode 100644 pkg/stream/rangefunc.go create mode 100644 pkg/stream/rangefunc_test.go diff --git a/examples/stream/map_test.go b/examples/stream/map_test.go index a4fb4aa..f4c62be 100644 --- a/examples/stream/map_test.go +++ b/examples/stream/map_test.go @@ -103,11 +103,11 @@ func TestMapPairReverse(t *testing.T) { // ("bar", 2) } -func TestMapNumberToBool(t *testing.T) { +func TestMapNumToBool(t *testing.T) { // Given stream of numbers, map each number to whether it is even. s := stream.Map( stream.Of(0, 2, 0), - mapper2.NumberToBool[int, bool](), + mapper2.NumToBool[int, bool](), ) stream.ForEach(s, func(b bool) { fmt.Println(b) @@ -122,7 +122,7 @@ func TestMapBoolNumber(t *testing.T) { // Given stream of bools, map each bool to 0 if false and 1 if true. s := stream.Map( stream.Of(false, true, false), - mapper2.BoolToNumber[bool, int](), + mapper2.BoolToNum[bool, int](), ) stream.ForEach(s, func(i int) { fmt.Println(i) diff --git a/pkg/pair/pair.go b/pkg/pair/pair.go index cc032e0..b960404 100644 --- a/pkg/pair/pair.go +++ b/pkg/pair/pair.go @@ -4,24 +4,33 @@ import ( "fmt" ) -// Pair represents a pair of two values. -// It is a generic type that takes two type parameters A and B. +// Zero returns a new Pair with default zero values for the type parameters A and B. +func Zero[A, B any]() Pair[A, B] { + return Pair[A, B]{} +} + +// Of creates a new Pair with the provided values for the first and second elements. +func Of[A, B any](first A, second B) Pair[A, B] { + return Pair[A, B]{first: first, second: second} +} + +// Pair represents a pair of values of type A and B. type Pair[A, B any] struct { first A second B } -// First returns the first element of a Pair. +// First returns the first element of type A. func (p Pair[A, B]) First() A { return p.first } -// Second returns the second element of a Pair. +// Second returns the second element of type B. func (p Pair[A, B]) Second() B { return p.second } -// Explode returns the first and second elements of a Pair. +// Explode returns the first and second elements together as a tuple. func (p Pair[A, B]) Explode() (A, B) { return p.first, p.second } @@ -35,14 +44,3 @@ func (p Pair[A, B]) Reverse() Pair[B, A] { func (p Pair[A, B]) String() string { return fmt.Sprintf("(%#v, %#v)", p.first, p.second) } - -// Zero returns a new Pair with default zero values for the type parameters A and B. -func Zero[A, B any]() Pair[A, B] { - return Pair[A, B]{} -} - -// Of creates a new Pair with the provided values for the first and second elements. -// The type parameters A and B determine the types of the first and second elements, respectively. -func Of[A, B any](first A, second B) Pair[A, B] { - return Pair[A, B]{first: first, second: second} -} diff --git a/pkg/stream/aggregate.go b/pkg/stream/aggregate.go index 2b7abd5..87811b7 100644 --- a/pkg/stream/aggregate.go +++ b/pkg/stream/aggregate.go @@ -56,7 +56,7 @@ func Reduce[E any](s Stream[E], reduce Reducer[E]) optional.Optional[E] { // n2 := stream.Sum[float64](stream.Of(1, 2, 3)) // 6.0 (float64) func Sum[R, E constraint.RealNumber](s Stream[E]) R { return Reduce( - Map(s, mapper.NumberToNumber[E, R]()), + Map(s, mapper.NumToNum[E, R]()), reducer.Sum[R](), ).OrElseZero() } diff --git a/pkg/stream/combine.go b/pkg/stream/combine.go index 9a90633..f4d4ff0 100644 --- a/pkg/stream/combine.go +++ b/pkg/stream/combine.go @@ -107,9 +107,7 @@ func CombineOrDiscard[E1, E2, F any](s1 Stream[E1], s2 Stream[E2], combine Optio // s := stream.Zip(stream.Of(1, 2, 3), stream.Of("foo", "bar")) // out := stream.DebugString(s) // "<(1, foo), (2, bar)>" func Zip[E, F any](s1 Stream[E], s2 Stream[F]) Stream[pair.Pair[E, F]] { - return Combine(s1, s2, func(e E, f F) pair.Pair[E, F] { - return pair.Of(e, f) - }) + return Combine(s1, s2, pair.Of[E, F]) } // ZipWithIndex returns a stream that pairs each element in the input stream with its index, starting at the given offset. @@ -120,32 +118,5 @@ func Zip[E, F any](s1 Stream[E], s2 Stream[F]) Stream[pair.Pair[E, F]] { // s := stream.ZipWithIndex(stream.Of("foo", "bar"), 1) // out := stream.DebugString(s) // "<(foo, 1), (bar, 2)>" func ZipWithIndex[E any, I constraint.Integer](s Stream[E], offset I) Stream[pair.Pair[E, I]] { - return Combine( - s, - Range[I](offset, pred.True[I](), mapper.Increment[I](1)), - func(e E, i I) pair.Pair[E, I] { - return pair.Of(e, i) - }, - ) -} - -// KeyExtractor represents a function that extracts a key of type K from a value of type E. -type KeyExtractor[E, K any] func(e E) K - -// ZipWithKey returns a stream that pairs each element in the input stream with the key extracted from the element using the given key extractor. -// The resulting stream will have the same number of elements as the input stream. -// -// Example usage: -// -// s := stream.ZipWithKey( -// stream.Of("foo", "bar"), -// func(s string) string { -// return strings.ToUpper(s) -// }, -// ) -// out := stream.DebugString(s) // "<(FOO, foo), (BAR, bar)>" -func ZipWithKey[K, E any](s Stream[E], ke KeyExtractor[E, K]) Stream[pair.Pair[K, E]] { - return Map(s, func(e E) pair.Pair[K, E] { - return pair.Of(ke(e), e) - }) + return Zip(s, Walk(offset, pred.True[I](), mapper.Increment[I](1))) } diff --git a/pkg/stream/combine_test.go b/pkg/stream/combine_test.go index fce2ada..5d08112 100644 --- a/pkg/stream/combine_test.go +++ b/pkg/stream/combine_test.go @@ -153,39 +153,3 @@ func TestZipWithIndex(t *testing.T) { assert.ElementsMatch(t, got, want) }) } - -func TestZipWithKey(t *testing.T) { - t.Run("empty", func(t *testing.T) { - s := ZipWithKey(Empty[int](), func(e int) int { - return e * 10 - }) - got := CollectSlice(s) - var want []pair.Pair[int, int] - assert.ElementsMatch(t, got, want) - }) - - t.Run("non-empty", func(t *testing.T) { - s := ZipWithKey(Of(1, 2, 3), func(e int) int { - return e * 10 - }) - got := CollectSlice(s) - want := []pair.Pair[int, int]{ - pair.Of(10, 1), - pair.Of(20, 2), - pair.Of(30, 3), - } - assert.ElementsMatch(t, got, want) - }) - - t.Run("limited", func(t *testing.T) { - s := ZipWithKey(Of(1, 2, 3), func(e int) int { - return e * 10 - }) - got := CollectSlice(Limit(s, 2)) // Stops stream after 2 elements. - want := []pair.Pair[int, int]{ - pair.Of(10, 1), - pair.Of(20, 2), - } - assert.ElementsMatch(t, got, want) - }) -} diff --git a/pkg/stream/generate.go b/pkg/stream/generate.go index ce26576..b80a6ca 100644 --- a/pkg/stream/generate.go +++ b/pkg/stream/generate.go @@ -1,10 +1,11 @@ package stream -import "math/rand" +import ( + "math/rand" +) // Generator represents a function that produces an infinite sequence of elements of type E. // Used by the Generate function. -// Example: Random numbers. type Generator[E any] func() E // Generate returns an infinite stream that produces elements by invoking the given generator function. @@ -23,6 +24,27 @@ func Generate[E any](next Generator[E]) Stream[E] { } } +// Repeat returns an infinite stream that always produces the given element. +// +// Example usage: +// +// s := stream.Repeat(1) +// out := stream.DebugString(s) // "<1, 1, 1, 1, 1, 1, 1, 1, 1, 1, ...>" +func Repeat[E any](e E) Stream[E] { + return Generate(func() E { return e }) +} + +// RepeatN returns a stream that produces the given element n times. +// If n is less than or equal to zero, the stream will be empty. +// +// Example usage: +// +// s := stream.RepeatN(1, 3) +// out := stream.DebugString(s) // "<1, 1, 1>" +func RepeatN[E any](e E, n int64) Stream[E] { + return Limit(Repeat(e), n) +} + // RandomInt returns a stream that produces pseudo-random, non-negative int values using the given rand.Source. func RandomInt(source rand.Source) Stream[int] { rnd := rand.New(source) diff --git a/pkg/stream/generate_test.go b/pkg/stream/generate_test.go index d9946c2..0e916b9 100644 --- a/pkg/stream/generate_test.go +++ b/pkg/stream/generate_test.go @@ -12,6 +12,18 @@ func TestGenerate(t *testing.T) { assert.ElementsMatch(t, out, []int{1, 1, 1, 1, 1}) } +func TestRepeat(t *testing.T) { + s := Repeat(1) + out := CollectSlice(Limit(s, 5)) // Limit to 5 elements for testing. + assert.ElementsMatch(t, out, []int{1, 1, 1, 1, 1}) +} + +func TestRepeatN(t *testing.T) { + s := RepeatN(1, 3) + out := CollectSlice(s) + assert.ElementsMatch(t, out, []int{1, 1, 1}) +} + func TestRandomInt(t *testing.T) { s := RandomInt(rand.NewSource(0)) // Seed with 0 for testing - should produce the same sequence every time. out := CollectSlice[int](Limit(s, 5)) // Limit to 5 elements for testing. diff --git a/pkg/stream/iterate.go b/pkg/stream/iterate.go index 32f1c80..af0c548 100644 --- a/pkg/stream/iterate.go +++ b/pkg/stream/iterate.go @@ -1,19 +1,44 @@ package stream -import "github.com/jpfourny/papaya/pkg/optional" +import ( + "github.com/jpfourny/papaya/pkg/constraint" + "github.com/jpfourny/papaya/pkg/optional" + "github.com/jpfourny/papaya/pkg/stream/mapper" + "github.com/jpfourny/papaya/pkg/stream/pred" +) -// Iterator represents a function that produces a (typically finite) sequence of elements of type E. -// It is similar to Generator, but it can return an empty optional.Optional to signal the end of the sequence. -// Used by the Iterate function. -type Iterator[E any] func() optional.Optional[E] +// Iterate returns a stream of elements produced by the given iterator function. +// When the iterator function returns false, the stream ends. +// +// Example usage: +// +// i := 0 +// s := stream.Iterate(func() (int, bool) { +// if i < 3 { +// i++ +// return i, true +// } +// return 0, false +// }) +// out := stream.DebugString(s) // "<1, 2, 3>" +func Iterate[E any](next func() (E, bool)) Stream[E] { + return func(yield Consumer[E]) bool { + for e, ok := next(); ok; e, ok = next() { + if !yield(e) { + return false + } + } + return true + } +} -// Iterate returns a stream of elements produced by the given Iterator. -// When the Iterator returns an empty optional.Optional, the stream ends. +// IterateOptional returns a stream of elements produced by the given iterator function. +// When the iterator function returns an empty optional.Optional, the stream ends. // // Example usage: // // i := 0 -// s := stream.Iterate(func() optional.Optional[int] { +// s := stream.IterateOptional(func() optional.Optional[int] { // if i < 3 { // i++ // return optional.Of(i) @@ -21,7 +46,7 @@ type Iterator[E any] func() optional.Optional[E] // return optional.Empty[int]() // }) // out := stream.DebugString(s) // "<1, 2, 3>" -func Iterate[E any](next Iterator[E]) Stream[E] { +func IterateOptional[E any](next func() optional.Optional[E]) Stream[E] { return func(yield Consumer[E]) bool { for e := next(); e.Present(); e = next() { if !yield(e.Get()) { @@ -32,21 +57,37 @@ func Iterate[E any](next Iterator[E]) Stream[E] { } } -// Range returns a stream that produces elements over a range beginning at `start`, advanced by the `next` function, and ending when `cond` predicate returns false. +// Walk returns a stream that walks elements beginning at `start`, advanced by the `advance` function, and ending when `cond` predicate returns false. // // Example usage: // -// s := stream.Range(1, pred.LessThanOrEqual(5), mapper.Increment(2)) +// s := stream.Walk(1, pred.LessThanOrEqual(5), mapper.Increment(2)) // out := stream.DebugString(s) // "<1, 3, 5>" -func Range[E any](start E, cond Predicate[E], next Mapper[E, E]) Stream[E] { - e := start - return Iterate(func() (result optional.Optional[E]) { - if cond(e) { - result = optional.Of(e) - e = next(e) - } else { - result = optional.Empty[E]() +func Walk[E any](start E, cond Predicate[E], advance Mapper[E, E]) Stream[E] { + next := start + return Iterate(func() (e E, ok bool) { + if cond(next) { + e, ok = next, true + next = advance(next) } return }) } + +// Interval returns a stream of real-number type N from the half-open interval `[start, end)` using the given step size. +// If the step size is negative, then the stream will be decreasing; otherwise, it will be increasing. +// +// Example usage: +// +// s := stream.Interval(0, 5, 1) +// out := stream.DebugString(s) // "<0, 1, 2, 3, 4>" +// s := stream.Interval(0, 5, 2) +// out := stream.DebugString(s) // "<0, 2, 4>" +// s = stream.Interval(5, 0, -2) +// out = stream.DebugString(s) // "<5, 3, 1>" +func Interval[N constraint.RealNumber](start, end, step N) Stream[N] { + if step < 0 { + return Walk[N](start, pred.GreaterThan(end), mapper.Increment(step)) + } + return Walk[N](start, pred.LessThan(end), mapper.Increment(step)) +} diff --git a/pkg/stream/iterate_test.go b/pkg/stream/iterate_test.go index ea0cc12..9ec8cc9 100644 --- a/pkg/stream/iterate_test.go +++ b/pkg/stream/iterate_test.go @@ -10,11 +10,38 @@ import ( ) func TestIterate(t *testing.T) { + t.Run("empty", func(t *testing.T) { + iter := func() (int, bool) { + return 0, false + } + s := Iterate(iter) + got := CollectSlice(s) + var want []int + assert.ElementsMatch(t, got, want) + }) + + t.Run("non-empty", func(t *testing.T) { + i := 0 + iter := func() (int, bool) { + if i < 3 { + i++ + return i, true + } + return 0, false + } + s := Iterate(iter) + got := CollectSlice(s) + want := []int{1, 2, 3} + assert.ElementsMatch(t, got, want) + }) +} + +func TestIterateOptional(t *testing.T) { t.Run("empty", func(t *testing.T) { iter := func() optional.Optional[int] { return optional.Empty[int]() } - s := Iterate(iter) + s := IterateOptional(iter) got := CollectSlice(s) var want []int assert.ElementsMatch(t, got, want) @@ -29,32 +56,63 @@ func TestIterate(t *testing.T) { } return optional.Empty[int]() } - s := Iterate(iter) + s := IterateOptional(iter) got := CollectSlice(s) want := []int{1, 2, 3} assert.ElementsMatch(t, got, want) }) + + t.Run("limited", func(t *testing.T) { + i := 0 + iter := func() optional.Optional[int] { + if i < 3 { + i++ + return optional.Of(i) + } + return optional.Empty[int]() + } + s := Limit(IterateOptional(iter), 2) + got := CollectSlice(s) + want := []int{1, 2} + assert.ElementsMatch(t, got, want) + }) } -func TestRange(t *testing.T) { +func TestWalk(t *testing.T) { t.Run("empty", func(t *testing.T) { - s := Range(0, pred.LessThan(0), mapper.Increment(1)) + s := Walk(0, pred.LessThan(0), mapper.Increment(1)) got := CollectSlice(s) var want []int assert.ElementsMatch(t, got, want) }) t.Run("non-empty", func(t *testing.T) { - s := Range(1, pred.LessThanOrEqual(5), mapper.Increment(2)) + s := Walk(1, pred.LessThanOrEqual(5), mapper.Increment(2)) got := CollectSlice(s) want := []int{1, 3, 5} assert.ElementsMatch(t, got, want) }) +} + +func TestInterval(t *testing.T) { + t.Run("empty", func(t *testing.T) { + s := Interval(0, 0, 1) + got := CollectSlice(s) + var want []int + assert.ElementsMatch(t, got, want) + }) - t.Run("limited", func(t *testing.T) { - s := Range(1, pred.LessThanOrEqual(5), mapper.Increment(2)) - got := CollectSlice(Limit(s, 2)) // Stops stream after 2 elements. + t.Run("non-empty-increasing", func(t *testing.T) { + s := Interval(1, 5, 2) + got := CollectSlice(s) want := []int{1, 3} assert.ElementsMatch(t, got, want) }) + + t.Run("non-empty-decreasing", func(t *testing.T) { + s := Interval(5, 1, -2) + got := CollectSlice(s) + want := []int{5, 3} + assert.ElementsMatch(t, got, want) + }) } diff --git a/pkg/stream/mapper/convert.go b/pkg/stream/mapper/convert.go index 2dd9ba0..b35184a 100644 --- a/pkg/stream/mapper/convert.go +++ b/pkg/stream/mapper/convert.go @@ -9,9 +9,9 @@ func BoolToBool[E constraint.Boolean, F constraint.Boolean]() func(E) F { } } -// BoolToNumber returns a function that accepts a value of boolean type E and returns a value of real number type F. +// BoolToNum returns a function that accepts a value of boolean type E and returns a value of real-number type F. // A value of true is converted to 1, and a value of false is converted to 0. -func BoolToNumber[E constraint.Boolean, F constraint.RealNumber]() func(E) F { +func BoolToNum[E constraint.Boolean, F constraint.RealNumber]() func(E) F { return func(e E) F { if e { return 1 @@ -20,31 +20,31 @@ func BoolToNumber[E constraint.Boolean, F constraint.RealNumber]() func(E) F { } } -// NumberToBool returns a function that accepts a value of real number type E and returns a value of boolean type F. +// NumToBool returns a function that accepts a value of real-number type E and returns a value of boolean type F. // A value of 0 is converted to false, and any other value is converted to true. -func NumberToBool[E constraint.RealNumber, F constraint.Boolean]() func(E) F { +func NumToBool[E constraint.RealNumber, F constraint.Boolean]() func(E) F { return func(e E) F { return F(e != 0) } } -// NumberToNumber returns a function that accepts a value of real number type E and returns a value of real number type F. +// NumToNum returns a function that accepts a value of real-number type E and returns a value of real-number type F. // The value is converted using standard type conversion rules. -func NumberToNumber[E constraint.RealNumber, F constraint.RealNumber]() func(E) F { +func NumToNum[E constraint.RealNumber, F constraint.RealNumber]() func(E) F { return func(e E) F { return F(e) } } -// StringToString returns a function that accepts a value of string type E and returns a value of string type F. -func StringToString[E constraint.String, F constraint.String]() func(E) F { +// ComplexToComplex returns a function that accepts a value of complex-number type E and returns a value of complex-number type F. +func ComplexToComplex[E constraint.Complex, F constraint.Complex]() func(E) F { return func(e E) F { return F(e) } } -// ComplexToComplex returns a function that accepts a value of complex number type E and returns a value of complex number type F. -func ComplexToComplex[E constraint.Complex, F constraint.Complex]() func(E) F { +// StringToString returns a function that accepts a value of string type E and returns a value of string type F. +func StringToString[E constraint.String, F constraint.String]() func(E) F { return func(e E) F { return F(e) } diff --git a/pkg/stream/mapper/convert_test.go b/pkg/stream/mapper/convert_test.go index 90679b1..09ec355 100644 --- a/pkg/stream/mapper/convert_test.go +++ b/pkg/stream/mapper/convert_test.go @@ -56,40 +56,40 @@ func TestStringToString(t *testing.T) { type testInt int -func TestNumberToNumber(t *testing.T) { +func TestNumToNum(t *testing.T) { t.Run("int-to-float64", func(t *testing.T) { - m := NumberToNumber[int, float64]() + m := NumToNum[int, float64]() got := m(42) want := float64(42) if got != want { - t.Errorf("NumberToNumber()(42) = %#v; want %#v", got, want) + t.Errorf("NumToNum()(42) = %#v; want %#v", got, want) } }) t.Run("float64-to-int", func(t *testing.T) { - m := NumberToNumber[float64, int]() + m := NumToNum[float64, int]() got := m(42) want := 42 if got != want { - t.Errorf("NumberToNumber()(42) = %#v; want %#v", got, want) + t.Errorf("NumToNum()(42) = %#v; want %#v", got, want) } }) t.Run("int-to-testInt", func(t *testing.T) { - m := NumberToNumber[int, testInt]() + m := NumToNum[int, testInt]() got := m(42) want := testInt(42) if got != want { - t.Errorf("NumberToNumber()(42) = %#v; want %#v", got, want) + t.Errorf("NumToNum()(42) = %#v; want %#v", got, want) } }) t.Run("testInt-to-int", func(t *testing.T) { - m := NumberToNumber[testInt, int]() + m := NumToNum[testInt, int]() got := m(42) want := 42 if got != want { - t.Errorf("NumberToNumber()(42) = %#v; want %#v", got, want) + t.Errorf("NumToNum()(42) = %#v; want %#v", got, want) } }) } @@ -114,58 +114,58 @@ func TestComplexToComplex(t *testing.T) { }) } -func TestBoolToNumber(t *testing.T) { +func TestBoolToNum(t *testing.T) { t.Run("bool-to-int", func(t *testing.T) { - m := BoolToNumber[bool, int]() + m := BoolToNum[bool, int]() got := m(true) want := 1 if got != want { - t.Errorf("BoolToNumber()(true) = %#v; want %#v", got, want) + t.Errorf("BoolToNum()(true) = %#v; want %#v", got, want) } got = m(false) want = 0 if got != want { - t.Errorf("BoolToNumber()(false) = %#v; want %#v", got, want) + t.Errorf("BoolToNum()(false) = %#v; want %#v", got, want) } }) t.Run("bool-to-testInt", func(t *testing.T) { - m := BoolToNumber[bool, testInt]() + m := BoolToNum[bool, testInt]() got := m(true) want := testInt(1) if got != want { - t.Errorf("BoolToNumber()(true) = %#v; want %#v", got, want) + t.Errorf("BoolToNum()(true) = %#v; want %#v", got, want) } got = m(false) want = 0 if got != want { - t.Errorf("BoolToNumber()(false) = %#v; want %#v", got, want) + t.Errorf("BoolToNum()(false) = %#v; want %#v", got, want) } }) } func TestNumberToBool(t *testing.T) { t.Run("int-to-bool", func(t *testing.T) { - m := NumberToBool[int, bool]() + m := NumToBool[int, bool]() got := m(42) if got != true { - t.Errorf("NumberToBool()(42) = %#v; want %#v", got, true) + t.Errorf("NumToBool()(42) = %#v; want %#v", got, true) } got = m(0) if got != false { - t.Errorf("NumberToBool()(0) = %#v; want %#v", got, false) + t.Errorf("NumToBool()(0) = %#v; want %#v", got, false) } }) t.Run("testInt-to-bool", func(t *testing.T) { - m := NumberToBool[testInt, bool]() + m := NumToBool[testInt, bool]() got := m(42) if got != true { - t.Errorf("NumberToBool()(42) = %#v; want %#v", got, true) + t.Errorf("NumToBool()(42) = %#v; want %#v", got, true) } got = m(0) if got != false { - t.Errorf("NumberToBool()(0) = %#v; want %#v", got, false) + t.Errorf("NumToBool()(0) = %#v; want %#v", got, false) } }) } diff --git a/pkg/stream/match.go b/pkg/stream/match.go index 5aecd0d..5eb8c72 100644 --- a/pkg/stream/match.go +++ b/pkg/stream/match.go @@ -2,7 +2,7 @@ package stream import ( "github.com/jpfourny/papaya/pkg/cmp" - pred2 "github.com/jpfourny/papaya/pkg/stream/pred" + "github.com/jpfourny/papaya/pkg/stream/pred" ) // AnyMatch returns true if any element in the stream matches the given Predicate. @@ -64,7 +64,7 @@ func NoneMatch[E any](s Stream[E], p Predicate[E]) bool { // out := stream.Contains(stream.Of(1, 2, 3), 2) // true // out = stream.Contains(stream.Of(1, 2, 3), 4) // false func Contains[E comparable](s Stream[E], e E) bool { - return AnyMatch(s, pred2.Equal(e)) + return AnyMatch(s, pred.Equal(e)) } // ContainsBy returns true if the stream contains the given element; false otherwise. @@ -75,7 +75,7 @@ func Contains[E comparable](s Stream[E], e E) bool { // out := stream.ContainsBy(stream.Of(1, 2, 3), 2, cmp.Natural[int]()) // true // out = stream.ContainsBy(stream.Of(1, 2, 3), 4, cmp.Natural[int]()) // false func ContainsBy[E any](s Stream[E], compare cmp.Comparer[E], e E) bool { - return AnyMatch(s, pred2.EqualBy(e, compare)) + return AnyMatch(s, pred.EqualBy(e, compare)) } // ContainsAny returns true if the stream contains any of the given elements; false otherwise. @@ -86,7 +86,7 @@ func ContainsBy[E any](s Stream[E], compare cmp.Comparer[E], e E) bool { // out := stream.ContainsAny(stream.Of(1, 2, 3), 2, 4) // true // out = stream.ContainsAny(stream.Of(1, 2, 3), 4, 5) // false func ContainsAny[E comparable](s Stream[E], es ...E) bool { - return AnyMatch(s, pred2.In(es...)) + return AnyMatch(s, pred.In(es...)) } // ContainsAnyBy returns true if the stream contains any of the given elements; false otherwise. @@ -97,7 +97,7 @@ func ContainsAny[E comparable](s Stream[E], es ...E) bool { // out := stream.ContainsAnyBy(stream.Of(1, 2, 3), cmp.Natural[int](), 2, 4) // true // out = stream.ContainsAnyBy(stream.Of(1, 2, 3), cmp.Natural[int](), 4, 5) // false func ContainsAnyBy[E any](s Stream[E], compare cmp.Comparer[E], es ...E) bool { - return AnyMatch(s, pred2.InBy(compare, es...)) + return AnyMatch(s, pred.InBy(compare, es...)) } // ContainsAll returns true if the stream contains all the given elements; false otherwise. @@ -141,7 +141,7 @@ func ContainsAllBy[E any](s Stream[E], compare cmp.Comparer[E], es ...E) bool { // out := stream.ContainsNone(stream.Of(1, 2, 3), 4, 5) // true // out = stream.ContainsNone(stream.Of(1, 2, 3), 2, 4) // false func ContainsNone[E comparable](s Stream[E], es ...E) bool { - return NoneMatch(s, pred2.In(es...)) + return NoneMatch(s, pred.In(es...)) } // ContainsNoneBy returns true if the stream contains none of the given elements; false otherwise. @@ -152,5 +152,5 @@ func ContainsNone[E comparable](s Stream[E], es ...E) bool { // out := stream.ContainsNoneBy(stream.Of(1, 2, 3), cmp.Natural[int](), 4, 5) // true // out = stream.ContainsNoneBy(stream.Of(1, 2, 3), cmp.Natural[int](), 2, 4) // false func ContainsNoneBy[E any](s Stream[E], compare cmp.Comparer[E], es ...E) bool { - return NoneMatch(s, pred2.InBy(compare, es...)) + return NoneMatch(s, pred.InBy(compare, es...)) } diff --git a/pkg/stream/rangefunc.go b/pkg/stream/rangefunc.go new file mode 100644 index 0000000..4b25ed7 --- /dev/null +++ b/pkg/stream/rangefunc.go @@ -0,0 +1,39 @@ +package stream + +import "github.com/jpfourny/papaya/pkg/pair" + +// FromRangeFunc adapts a range function of parameter type E to a Stream of the same type. +// This exists for compatibility with the range-over-func support in Go (1.22+). +func FromRangeFunc[E any](f func(func(E) bool) bool) Stream[E] { + return func(yield Consumer[E]) bool { + return f(yield) + } +} + +// FromRangeBiFunc converts a range bi-function of parameter types K and V into a Stream of `pair.Pair` with the same types. +// This exists for compatibility with the range-over-func support in Go (1.22+). +func FromRangeBiFunc[K, V any](f func(func(K, V) bool) bool) Stream[pair.Pair[K, V]] { + return func(yield Consumer[pair.Pair[K, V]]) bool { + return f(func(k K, v V) bool { + return yield(pair.Of(k, v)) + }) + } +} + +// ToRangeFunc converts the given Stream of type E into a range function of the same parameter type. +// This exists for compatibility with the range-over-func support in Go (1.22+). +func ToRangeFunc[E any](s Stream[E]) func(func(E) bool) bool { + return func(yield func(E) bool) bool { + return s(yield) + } +} + +// ToRangeBiFunc converts the given Stream of `pair.Pair` with types K and V into a range bi-function with the same parameter types. +// This exists for compatibility with the range-over-func support in Go (1.22+). +func ToRangeBiFunc[K, V any](s Stream[pair.Pair[K, V]]) func(func(K, V) bool) bool { + return func(yield func(K, V) bool) bool { + return s(func(p pair.Pair[K, V]) bool { + return yield(p.Explode()) + }) + } +} diff --git a/pkg/stream/rangefunc_test.go b/pkg/stream/rangefunc_test.go new file mode 100644 index 0000000..1316997 --- /dev/null +++ b/pkg/stream/rangefunc_test.go @@ -0,0 +1,63 @@ +package stream + +import ( + "github.com/jpfourny/papaya/internal/assert" + "github.com/jpfourny/papaya/pkg/pair" + "testing" +) + +func TestFromRangeFunc(t *testing.T) { + f := func(yield func(int) bool) bool { + return yield(1) && yield(2) && yield(3) + } + s := FromRangeFunc(f) + got := CollectSlice(s) + want := []int{1, 2, 3} + assert.ElementsMatch(t, got, want) +} + +func TestFromRangeBiFunc(t *testing.T) { + f := func(yield func(int, int) bool) bool { + return yield(1, 2) && yield(3, 4) && yield(5, 6) + } + s := FromRangeBiFunc(f) + got := CollectSlice(s) + want := []pair.Pair[int, int]{ + pair.Of(1, 2), + pair.Of(3, 4), + pair.Of(5, 6), + } + assert.ElementsMatch(t, got, want) +} + +func TestToRangeFunc(t *testing.T) { + s := Of(1, 2, 3) + f := ToRangeFunc(s) + got := make([]int, 0, 3) + want := []int{1, 2, 3} + f(func(e int) bool { + got = append(got, e) + return true + }) + assert.ElementsMatch(t, got, want) +} + +func TestToRangeBiFunc(t *testing.T) { + s := Of( + pair.Of(1, 2), + pair.Of(3, 4), + pair.Of(5, 6), + ) + f := ToRangeBiFunc(s) + got := make([]pair.Pair[int, int], 0, 3) + want := []pair.Pair[int, int]{ + pair.Of(1, 2), + pair.Of(3, 4), + pair.Of(5, 6), + } + f(func(k, v int) bool { + got = append(got, pair.Of(k, v)) + return true + }) + assert.ElementsMatch(t, got, want) +}