Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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

0.4.0
- overhaul all doc comments, add exceptions, improve IDE quick-info experience, #136, #220, #234
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