Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "minor"
"version": "10.0.100",
"rollForward": "latestPatch"
}
}
1 change: 1 addition & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<Compile Include="TaskSeq.Length.Tests.fs" />
<Compile Include="TaskSeq.Map.Tests.fs" />
<Compile Include="TaskSeq.MaxMin.Tests.fs" />
<Compile Include="TaskSeq.SumBy.Tests.fs" />
<Compile Include="TaskSeq.OfXXX.Tests.fs" />
<Compile Include="TaskSeq.Pick.Tests.fs" />
<Compile Include="TaskSeq.RemoveAt.Tests.fs" />
Expand Down
218 changes: 218 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.SumBy.Tests.fs
Original file line number Diff line number Diff line change
@@ -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 =
[<Fact>]
let ``Null source is invalid for sum`` () =
assertNullArg
<| fun () -> TaskSeq.sum (null: System.Collections.Generic.IAsyncEnumerable<int>)

[<Fact>]
let ``Null source is invalid for sumBy`` () =
assertNullArg
<| fun () -> TaskSeq.sumBy id (null: System.Collections.Generic.IAsyncEnumerable<int>)

[<Fact>]
let ``Null source is invalid for sumByAsync`` () =
assertNullArg
<| fun () -> TaskSeq.sumByAsync (id >> Task.fromResult) (null: System.Collections.Generic.IAsyncEnumerable<int>)

[<Fact>]
let ``Null source is invalid for average`` () =
assertNullArg
<| fun () -> TaskSeq.average (null: System.Collections.Generic.IAsyncEnumerable<float>)

[<Fact>]
let ``Null source is invalid for averageBy`` () =
assertNullArg
<| fun () -> TaskSeq.averageBy float (null: System.Collections.Generic.IAsyncEnumerable<int>)

[<Fact>]
let ``Null source is invalid for averageByAsync`` () =
assertNullArg
<| fun () -> TaskSeq.averageByAsync (float >> Task.fromResult) (null: System.Collections.Generic.IAsyncEnumerable<int>)

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-sum returns zero on empty`` variant = task {
let! result = Gen.getEmptyVariant variant |> TaskSeq.sum
result |> should equal 0
}

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-sumBy returns zero on empty`` variant = task {
let! result = Gen.getEmptyVariant variant |> TaskSeq.sumBy id
result |> should equal 0
}

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-sumByAsync returns zero on empty`` variant = task {
let! result =
Gen.getEmptyVariant variant
|> TaskSeq.sumByAsync Task.fromResult

result |> should equal 0
}

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-average raises on empty`` variant =
fun () ->
Gen.getEmptyVariant variant
|> TaskSeq.map float
|> TaskSeq.average
|> Task.ignore

|> should throwAsyncExact typeof<System.ArgumentException>

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-averageBy raises on empty`` variant =
fun () ->
Gen.getEmptyVariant variant
|> TaskSeq.averageBy float
|> Task.ignore

|> should throwAsyncExact typeof<System.ArgumentException>

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-averageByAsync raises on empty`` variant =
fun () ->
Gen.getEmptyVariant variant
|> TaskSeq.averageByAsync (float >> Task.fromResult)
|> Task.ignore

|> should throwAsyncExact typeof<System.ArgumentException>

module Immutable =
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-sumBy returns sum of id 1..10`` variant = task {
let! result = Gen.getSeqImmutable variant |> TaskSeq.sumBy id
result |> should equal 55
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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
}

[<Fact>]
let ``TaskSeq-sum works with a single element`` () = task {
let! result = TaskSeq.singleton 42 |> TaskSeq.sum
result |> should equal 42
}

[<Fact>]
let ``TaskSeq-average works with a single element`` () = task {
let! result = TaskSeq.singleton 42.0 |> TaskSeq.average
result |> should (equalWithin 0.001) 42.0
}

[<Fact>]
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
}

[<Fact>]
let ``TaskSeq-sum works with int64`` () = task {
let! result = TaskSeq.ofSeq [ 1L; 2L; 3L; 4L; 5L ] |> TaskSeq.sum

result |> should equal 15L
}

[<Fact>]
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 =
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-sum iterates exactly once`` variant = task {
let ts = Gen.getSeqWithSideEffect variant
let! result = ts |> TaskSeq.sum
result |> should equal 55
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-sumBy iterates exactly once`` variant = task {
let ts = Gen.getSeqWithSideEffect variant
let! result = ts |> TaskSeq.sumBy id
result |> should equal 55
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
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
}
102 changes: 102 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}


[<Sealed; AbstractClass>]
type TaskSeq private () =
Expand Down Expand Up @@ -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
Expand Down
Loading