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 =