diff --git a/global.json b/global.json index badcd44..b24aad6 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "10.0.103", - "rollForward": "minor" + "version": "10.0.100", + "rollForward": "latestPatch" } } diff --git a/release-notes.txt b/release-notes.txt index 969a48c..274b5cf 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -5,6 +5,7 @@ Release notes: - update engineering to .NET 9/10 - adds TaskSeq.scan and TaskSeq.scanAsync, #289 - adds TaskSeq.pairwise, #289 + - adds TaskSeq.sum, sumBy, sumByAsync, average, averageBy, averageByAsync - adds TaskSeq.reduce and TaskSeq.reduceAsync, #289 - adds TaskSeq.unfold and TaskSeq.unfoldAsync, #289 - adds TaskSeq.chunkBySize (closes #258) and TaskSeq.windowed, #289 diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj index b9c5a60..79f6d67 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -41,6 +41,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.SumBy.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.SumBy.Tests.fs new file mode 100644 index 0000000..3c59300 --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.SumBy.Tests.fs @@ -0,0 +1,218 @@ +module TaskSeq.Tests.SumBy + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// TaskSeq.sum +// TaskSeq.sumBy +// TaskSeq.sumByAsync +// TaskSeq.average +// TaskSeq.averageBy +// TaskSeq.averageByAsync +// + +module EmptySeq = + [] + let ``Null source is invalid for sum`` () = + assertNullArg + <| fun () -> TaskSeq.sum (null: System.Collections.Generic.IAsyncEnumerable) + + [] + let ``Null source is invalid for sumBy`` () = + assertNullArg + <| fun () -> TaskSeq.sumBy id (null: System.Collections.Generic.IAsyncEnumerable) + + [] + let ``Null source is invalid for sumByAsync`` () = + assertNullArg + <| fun () -> TaskSeq.sumByAsync (id >> Task.fromResult) (null: System.Collections.Generic.IAsyncEnumerable) + + [] + let ``Null source is invalid for average`` () = + assertNullArg + <| fun () -> TaskSeq.average (null: System.Collections.Generic.IAsyncEnumerable) + + [] + let ``Null source is invalid for averageBy`` () = + assertNullArg + <| fun () -> TaskSeq.averageBy float (null: System.Collections.Generic.IAsyncEnumerable) + + [] + let ``Null source is invalid for averageByAsync`` () = + assertNullArg + <| fun () -> TaskSeq.averageByAsync (float >> Task.fromResult) (null: System.Collections.Generic.IAsyncEnumerable) + + [)>] + let ``TaskSeq-sum returns zero on empty`` variant = task { + let! result = Gen.getEmptyVariant variant |> TaskSeq.sum + result |> should equal 0 + } + + [)>] + let ``TaskSeq-sumBy returns zero on empty`` variant = task { + let! result = Gen.getEmptyVariant variant |> TaskSeq.sumBy id + result |> should equal 0 + } + + [)>] + let ``TaskSeq-sumByAsync returns zero on empty`` variant = task { + let! result = + Gen.getEmptyVariant variant + |> TaskSeq.sumByAsync Task.fromResult + + result |> should equal 0 + } + + [)>] + let ``TaskSeq-average raises on empty`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.map float + |> TaskSeq.average + |> Task.ignore + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-averageBy raises on empty`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.averageBy float + |> Task.ignore + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-averageByAsync raises on empty`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.averageByAsync (float >> Task.fromResult) + |> Task.ignore + + |> should throwAsyncExact typeof + +module Immutable = + [)>] + let ``TaskSeq-sum returns sum of 1..10`` variant = task { + // items are 1..10; sum = 55 + let! result = Gen.getSeqImmutable variant |> TaskSeq.sum + result |> should equal 55 + } + + [)>] + let ``TaskSeq-sumBy returns sum of id 1..10`` variant = task { + let! result = Gen.getSeqImmutable variant |> TaskSeq.sumBy id + result |> should equal 55 + } + + [)>] + let ``TaskSeq-sumBy with projection returns sum of doubled values`` variant = task { + // sum of 2*i for i in 1..10 = 2 * 55 = 110 + let! result = Gen.getSeqImmutable variant |> TaskSeq.sumBy ((*) 2) + result |> should equal 110 + } + + [)>] + let ``TaskSeq-sumByAsync with async projection returns sum`` variant = task { + let! result = + Gen.getSeqImmutable variant + |> TaskSeq.sumByAsync (fun x -> task { return x * 3 }) + + // 3 * 55 = 165 + result |> should equal 165 + } + + [)>] + let ``TaskSeq-average returns average of 1..10 as float`` variant = task { + // items are 1..10; average = 5.5 + let! result = + Gen.getSeqImmutable variant + |> TaskSeq.map float + |> TaskSeq.average + + result |> should (equalWithin 0.001) 5.5 + } + + [)>] + let ``TaskSeq-averageBy returns average of float projections`` variant = task { + // average of float values 1.0..10.0 = 5.5 + let! result = Gen.getSeqImmutable variant |> TaskSeq.averageBy float + result |> should (equalWithin 0.001) 5.5 + } + + [)>] + let ``TaskSeq-averageBy with custom projection returns correct average`` variant = task { + // sum of 2*i / count = 2 * 5.5 = 11.0 + let! result = + Gen.getSeqImmutable variant + |> TaskSeq.averageBy (float >> (*) 2.0) + + result |> should (equalWithin 0.001) 11.0 + } + + [)>] + let ``TaskSeq-averageByAsync with async projection returns correct average`` variant = task { + let! result = + Gen.getSeqImmutable variant + |> TaskSeq.averageByAsync (fun x -> task { return float x }) + + result |> should (equalWithin 0.001) 5.5 + } + + [] + let ``TaskSeq-sum works with a single element`` () = task { + let! result = TaskSeq.singleton 42 |> TaskSeq.sum + result |> should equal 42 + } + + [] + let ``TaskSeq-average works with a single element`` () = task { + let! result = TaskSeq.singleton 42.0 |> TaskSeq.average + result |> should (equalWithin 0.001) 42.0 + } + + [] + let ``TaskSeq-sumBy works with float projection`` () = task { + let! result = TaskSeq.ofSeq [ 1; 2; 3; 4; 5 ] |> TaskSeq.sumBy float + + result |> should (equalWithin 0.001) 15.0 + } + + [] + let ``TaskSeq-sum works with int64`` () = task { + let! result = TaskSeq.ofSeq [ 1L; 2L; 3L; 4L; 5L ] |> TaskSeq.sum + + result |> should equal 15L + } + + [] + let ``TaskSeq-average works with float32`` () = task { + let! result = TaskSeq.ofSeq [ 1.0f; 2.0f; 3.0f ] |> TaskSeq.average + + result |> should (equalWithin 0.001f) 2.0f + } + +module SideEffects = + [)>] + let ``TaskSeq-sum iterates exactly once`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let! result = ts |> TaskSeq.sum + result |> should equal 55 + } + + [)>] + let ``TaskSeq-sumBy iterates exactly once`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let! result = ts |> TaskSeq.sumBy id + result |> should equal 55 + } + + [)>] + let ``TaskSeq-averageBy iterates exactly once`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + let! result = ts |> TaskSeq.averageBy float + result |> should (equalWithin 0.001) 5.5 + } diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 361286d..26508e7 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -13,6 +13,107 @@ module TaskSeqExtensions = module TaskSeq = let empty<'T> = Internal.empty<'T> + let inline sum (source: TaskSeq< ^T >) : Task< ^T > = + if obj.ReferenceEquals(source, null) then + nullArg (nameof source) + + task { + use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None) + let mutable acc = Unchecked.defaultof< ^T> + + while! e.MoveNextAsync() do + acc <- acc + e.Current + + return acc + } + + let inline sumBy (projection: 'T -> ^U) (source: TaskSeq<'T>) : Task< ^U > = + if obj.ReferenceEquals(source, null) then + nullArg (nameof source) + + task { + use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None) + let mutable acc = Unchecked.defaultof< ^U> + + while! e.MoveNextAsync() do + acc <- acc + projection e.Current + + return acc + } + + let inline sumByAsync (projection: 'T -> Task< ^U >) (source: TaskSeq<'T>) : Task< ^U > = + if obj.ReferenceEquals(source, null) then + nullArg (nameof source) + + task { + use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None) + let mutable acc = Unchecked.defaultof< ^U> + + while! e.MoveNextAsync() do + let! value = projection e.Current + acc <- acc + value + + return acc + } + + let inline average (source: TaskSeq< ^T >) : Task< ^T > = + if obj.ReferenceEquals(source, null) then + nullArg (nameof source) + + task { + use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None) + let mutable acc = Unchecked.defaultof< ^T> + let mutable count = 0 + + while! e.MoveNextAsync() do + acc <- acc + e.Current + count <- count + 1 + + if count = 0 then + invalidArg (nameof source) "The input task sequence was empty." + + return LanguagePrimitives.DivideByInt acc count + } + + let inline averageBy (projection: 'T -> ^U) (source: TaskSeq<'T>) : Task< ^U > = + if obj.ReferenceEquals(source, null) then + nullArg (nameof source) + + task { + use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None) + let mutable acc = Unchecked.defaultof< ^U> + let mutable count = 0 + + while! e.MoveNextAsync() do + acc <- acc + projection e.Current + count <- count + 1 + + if count = 0 then + invalidArg (nameof source) "The input task sequence was empty." + + return LanguagePrimitives.DivideByInt acc count + } + + let inline averageByAsync (projection: 'T -> Task< ^U >) (source: TaskSeq<'T>) : Task< ^U > = + if obj.ReferenceEquals(source, null) then + nullArg (nameof source) + + task { + use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None) + let mutable acc = Unchecked.defaultof< ^U> + let mutable count = 0 + + while! e.MoveNextAsync() do + let! value = projection e.Current + acc <- acc + value + count <- count + 1 + + if count = 0 then + invalidArg (nameof source) "The input task sequence was empty." + + return LanguagePrimitives.DivideByInt acc count + } + [] type TaskSeq private () = @@ -166,6 +267,7 @@ type TaskSeq private () = static member minBy projection source = Internal.maxMinBy (>) projection source static member maxByAsync projection source = Internal.maxMinByAsync (<) projection source // looks like 'less than', is 'greater than' static member minByAsync projection source = Internal.maxMinByAsync (>) projection source + static member length source = Internal.lengthBy None source static member lengthOrMax max source = Internal.lengthBeforeMax max source static member lengthBy predicate source = Internal.lengthBy (Some(Predicate predicate)) source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index aed7d96..a608b9b 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -9,6 +9,90 @@ module TaskSeqExtensions = /// Initialize an empty task sequence. val empty<'T> : TaskSeq<'T> + /// + /// Returns the sum of all elements of the task sequence. The elements must support the + operator, + /// which is the case for all built-in numeric types. For sequences with a projection, use . + /// + /// + /// The input task sequence. + /// The sum of all elements in the sequence, starting from Unchecked.defaultof as zero. + /// Thrown when the input task sequence is null. + val inline sum: source: TaskSeq< ^T > -> Task< ^T > when ^T: (static member (+): ^T * ^T -> ^T) + + /// + /// Returns the sum of the results generated by applying the function to each element + /// of the task sequence. The result type must support the + operator, which is the case for all built-in numeric types. + /// If is asynchronous, consider using . + /// + /// + /// A function to transform items from the input sequence into summable values. + /// The input task sequence. + /// The sum of the projected values. + /// Thrown when the input task sequence is null. + val inline sumBy: + projection: ('T -> ^U) -> source: TaskSeq<'T> -> Task< ^U > when ^U: (static member (+): ^U * ^U -> ^U) + + /// + /// Returns the sum of the results generated by applying the asynchronous function to + /// each element of the task sequence. The result type must support the + operator, which is the case for all + /// built-in numeric types. + /// If is synchronous, consider using . + /// + /// + /// An async function to transform items from the input sequence into summable values. + /// The input task sequence. + /// The sum of the projected values. + /// Thrown when the input task sequence is null. + val inline sumByAsync: + projection: ('T -> Task< ^U >) -> source: TaskSeq<'T> -> Task< ^U > + when ^U: (static member (+): ^U * ^U -> ^U) + + /// + /// Returns the average of all elements of the task sequence. The elements must support the + operator + /// and DivideByInt, which is the case for all built-in F# floating-point types. + /// For sequences with a projection, consider using . + /// + /// + /// The input task sequence. + /// The average of the elements in the sequence. + /// Thrown when the input task sequence is null. + /// Thrown when the input task sequence is empty. + val inline average: + source: TaskSeq< ^T > -> Task< ^T > + when ^T: (static member (+): ^T * ^T -> ^T) and ^T: (static member DivideByInt: ^T * int -> ^T) + + /// + /// Returns the average of the results generated by applying the function to each element + /// of the task sequence. The result type must support the + operator and DivideByInt, which is the case + /// for all built-in F# floating-point types. + /// If is asynchronous, consider using . + /// + /// + /// A function to transform items from the input sequence into averageable values. + /// The input task sequence. + /// The average of the projected values. + /// Thrown when the input task sequence is null. + /// Thrown when the input task sequence is empty. + val inline averageBy: + projection: ('T -> ^U) -> source: TaskSeq<'T> -> Task< ^U > + when ^U: (static member (+): ^U * ^U -> ^U) and ^U: (static member DivideByInt: ^U * int -> ^U) + + /// + /// Returns the average of the results generated by applying the asynchronous function to + /// each element of the task sequence. The result type must support the + operator and DivideByInt, which + /// is the case for all built-in F# floating-point types. + /// If is synchronous, consider using . + /// + /// + /// An async function to transform items from the input sequence into averageable values. + /// The input task sequence. + /// The average of the projected values. + /// Thrown when the input task sequence is null. + /// Thrown when the input task sequence is empty. + val inline averageByAsync: + projection: ('T -> Task< ^U >) -> source: TaskSeq<'T> -> Task< ^U > + when ^U: (static member (+): ^U * ^U -> ^U) and ^U: (static member DivideByInt: ^U * int -> ^U) + [] type TaskSeq =