Skip to content

Commit 75402ae

Browse files
authored
Merge pull request #304 from fsprojects/repo-assist/feat-sumby-averageby-2026-03-8d8d6841f6117a18
[Repo Assist] feat: add TaskSeq.sum, sumBy, sumByAsync, average, averageBy, averageByAsync
2 parents 7871669 + d496ec9 commit 75402ae

File tree

6 files changed

+408
-2
lines changed

6 files changed

+408
-2
lines changed

global.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "10.0.103",
4-
"rollForward": "minor"
3+
"version": "10.0.100",
4+
"rollForward": "latestPatch"
55
}
66
}

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Release notes:
55
- update engineering to .NET 9/10
66
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
77
- adds TaskSeq.pairwise, #289
8+
- adds TaskSeq.sum, sumBy, sumByAsync, average, averageBy, averageByAsync
89
- adds TaskSeq.reduce and TaskSeq.reduceAsync, #289
910
- adds TaskSeq.unfold and TaskSeq.unfoldAsync, #289
1011
- adds TaskSeq.chunkBySize (closes #258) and TaskSeq.windowed, #289

src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
<Compile Include="TaskSeq.Length.Tests.fs" />
4242
<Compile Include="TaskSeq.Map.Tests.fs" />
4343
<Compile Include="TaskSeq.MaxMin.Tests.fs" />
44+
<Compile Include="TaskSeq.SumBy.Tests.fs" />
4445
<Compile Include="TaskSeq.OfXXX.Tests.fs" />
4546
<Compile Include="TaskSeq.Pick.Tests.fs" />
4647
<Compile Include="TaskSeq.RemoveAt.Tests.fs" />
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
module TaskSeq.Tests.SumBy
2+
3+
open Xunit
4+
open FsUnit.Xunit
5+
6+
open FSharp.Control
7+
8+
//
9+
// TaskSeq.sum
10+
// TaskSeq.sumBy
11+
// TaskSeq.sumByAsync
12+
// TaskSeq.average
13+
// TaskSeq.averageBy
14+
// TaskSeq.averageByAsync
15+
//
16+
17+
module EmptySeq =
18+
[<Fact>]
19+
let ``Null source is invalid for sum`` () =
20+
assertNullArg
21+
<| fun () -> TaskSeq.sum (null: System.Collections.Generic.IAsyncEnumerable<int>)
22+
23+
[<Fact>]
24+
let ``Null source is invalid for sumBy`` () =
25+
assertNullArg
26+
<| fun () -> TaskSeq.sumBy id (null: System.Collections.Generic.IAsyncEnumerable<int>)
27+
28+
[<Fact>]
29+
let ``Null source is invalid for sumByAsync`` () =
30+
assertNullArg
31+
<| fun () -> TaskSeq.sumByAsync (id >> Task.fromResult) (null: System.Collections.Generic.IAsyncEnumerable<int>)
32+
33+
[<Fact>]
34+
let ``Null source is invalid for average`` () =
35+
assertNullArg
36+
<| fun () -> TaskSeq.average (null: System.Collections.Generic.IAsyncEnumerable<float>)
37+
38+
[<Fact>]
39+
let ``Null source is invalid for averageBy`` () =
40+
assertNullArg
41+
<| fun () -> TaskSeq.averageBy float (null: System.Collections.Generic.IAsyncEnumerable<int>)
42+
43+
[<Fact>]
44+
let ``Null source is invalid for averageByAsync`` () =
45+
assertNullArg
46+
<| fun () -> TaskSeq.averageByAsync (float >> Task.fromResult) (null: System.Collections.Generic.IAsyncEnumerable<int>)
47+
48+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
49+
let ``TaskSeq-sum returns zero on empty`` variant = task {
50+
let! result = Gen.getEmptyVariant variant |> TaskSeq.sum
51+
result |> should equal 0
52+
}
53+
54+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
55+
let ``TaskSeq-sumBy returns zero on empty`` variant = task {
56+
let! result = Gen.getEmptyVariant variant |> TaskSeq.sumBy id
57+
result |> should equal 0
58+
}
59+
60+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
61+
let ``TaskSeq-sumByAsync returns zero on empty`` variant = task {
62+
let! result =
63+
Gen.getEmptyVariant variant
64+
|> TaskSeq.sumByAsync Task.fromResult
65+
66+
result |> should equal 0
67+
}
68+
69+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
70+
let ``TaskSeq-average raises on empty`` variant =
71+
fun () ->
72+
Gen.getEmptyVariant variant
73+
|> TaskSeq.map float
74+
|> TaskSeq.average
75+
|> Task.ignore
76+
77+
|> should throwAsyncExact typeof<System.ArgumentException>
78+
79+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
80+
let ``TaskSeq-averageBy raises on empty`` variant =
81+
fun () ->
82+
Gen.getEmptyVariant variant
83+
|> TaskSeq.averageBy float
84+
|> Task.ignore
85+
86+
|> should throwAsyncExact typeof<System.ArgumentException>
87+
88+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
89+
let ``TaskSeq-averageByAsync raises on empty`` variant =
90+
fun () ->
91+
Gen.getEmptyVariant variant
92+
|> TaskSeq.averageByAsync (float >> Task.fromResult)
93+
|> Task.ignore
94+
95+
|> should throwAsyncExact typeof<System.ArgumentException>
96+
97+
module Immutable =
98+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
99+
let ``TaskSeq-sum returns sum of 1..10`` variant = task {
100+
// items are 1..10; sum = 55
101+
let! result = Gen.getSeqImmutable variant |> TaskSeq.sum
102+
result |> should equal 55
103+
}
104+
105+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
106+
let ``TaskSeq-sumBy returns sum of id 1..10`` variant = task {
107+
let! result = Gen.getSeqImmutable variant |> TaskSeq.sumBy id
108+
result |> should equal 55
109+
}
110+
111+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
112+
let ``TaskSeq-sumBy with projection returns sum of doubled values`` variant = task {
113+
// sum of 2*i for i in 1..10 = 2 * 55 = 110
114+
let! result = Gen.getSeqImmutable variant |> TaskSeq.sumBy ((*) 2)
115+
result |> should equal 110
116+
}
117+
118+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
119+
let ``TaskSeq-sumByAsync with async projection returns sum`` variant = task {
120+
let! result =
121+
Gen.getSeqImmutable variant
122+
|> TaskSeq.sumByAsync (fun x -> task { return x * 3 })
123+
124+
// 3 * 55 = 165
125+
result |> should equal 165
126+
}
127+
128+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
129+
let ``TaskSeq-average returns average of 1..10 as float`` variant = task {
130+
// items are 1..10; average = 5.5
131+
let! result =
132+
Gen.getSeqImmutable variant
133+
|> TaskSeq.map float
134+
|> TaskSeq.average
135+
136+
result |> should (equalWithin 0.001) 5.5
137+
}
138+
139+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
140+
let ``TaskSeq-averageBy returns average of float projections`` variant = task {
141+
// average of float values 1.0..10.0 = 5.5
142+
let! result = Gen.getSeqImmutable variant |> TaskSeq.averageBy float
143+
result |> should (equalWithin 0.001) 5.5
144+
}
145+
146+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
147+
let ``TaskSeq-averageBy with custom projection returns correct average`` variant = task {
148+
// sum of 2*i / count = 2 * 5.5 = 11.0
149+
let! result =
150+
Gen.getSeqImmutable variant
151+
|> TaskSeq.averageBy (float >> (*) 2.0)
152+
153+
result |> should (equalWithin 0.001) 11.0
154+
}
155+
156+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
157+
let ``TaskSeq-averageByAsync with async projection returns correct average`` variant = task {
158+
let! result =
159+
Gen.getSeqImmutable variant
160+
|> TaskSeq.averageByAsync (fun x -> task { return float x })
161+
162+
result |> should (equalWithin 0.001) 5.5
163+
}
164+
165+
[<Fact>]
166+
let ``TaskSeq-sum works with a single element`` () = task {
167+
let! result = TaskSeq.singleton 42 |> TaskSeq.sum
168+
result |> should equal 42
169+
}
170+
171+
[<Fact>]
172+
let ``TaskSeq-average works with a single element`` () = task {
173+
let! result = TaskSeq.singleton 42.0 |> TaskSeq.average
174+
result |> should (equalWithin 0.001) 42.0
175+
}
176+
177+
[<Fact>]
178+
let ``TaskSeq-sumBy works with float projection`` () = task {
179+
let! result = TaskSeq.ofSeq [ 1; 2; 3; 4; 5 ] |> TaskSeq.sumBy float
180+
181+
result |> should (equalWithin 0.001) 15.0
182+
}
183+
184+
[<Fact>]
185+
let ``TaskSeq-sum works with int64`` () = task {
186+
let! result = TaskSeq.ofSeq [ 1L; 2L; 3L; 4L; 5L ] |> TaskSeq.sum
187+
188+
result |> should equal 15L
189+
}
190+
191+
[<Fact>]
192+
let ``TaskSeq-average works with float32`` () = task {
193+
let! result = TaskSeq.ofSeq [ 1.0f; 2.0f; 3.0f ] |> TaskSeq.average
194+
195+
result |> should (equalWithin 0.001f) 2.0f
196+
}
197+
198+
module SideEffects =
199+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
200+
let ``TaskSeq-sum iterates exactly once`` variant = task {
201+
let ts = Gen.getSeqWithSideEffect variant
202+
let! result = ts |> TaskSeq.sum
203+
result |> should equal 55
204+
}
205+
206+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
207+
let ``TaskSeq-sumBy iterates exactly once`` variant = task {
208+
let ts = Gen.getSeqWithSideEffect variant
209+
let! result = ts |> TaskSeq.sumBy id
210+
result |> should equal 55
211+
}
212+
213+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
214+
let ``TaskSeq-averageBy iterates exactly once`` variant = task {
215+
let ts = Gen.getSeqWithSideEffect variant
216+
let! result = ts |> TaskSeq.averageBy float
217+
result |> should (equalWithin 0.001) 5.5
218+
}

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,107 @@ module TaskSeqExtensions =
1313
module TaskSeq =
1414
let empty<'T> = Internal.empty<'T>
1515

16+
let inline sum (source: TaskSeq< ^T >) : Task< ^T > =
17+
if obj.ReferenceEquals(source, null) then
18+
nullArg (nameof source)
19+
20+
task {
21+
use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None)
22+
let mutable acc = Unchecked.defaultof< ^T>
23+
24+
while! e.MoveNextAsync() do
25+
acc <- acc + e.Current
26+
27+
return acc
28+
}
29+
30+
let inline sumBy (projection: 'T -> ^U) (source: TaskSeq<'T>) : Task< ^U > =
31+
if obj.ReferenceEquals(source, null) then
32+
nullArg (nameof source)
33+
34+
task {
35+
use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None)
36+
let mutable acc = Unchecked.defaultof< ^U>
37+
38+
while! e.MoveNextAsync() do
39+
acc <- acc + projection e.Current
40+
41+
return acc
42+
}
43+
44+
let inline sumByAsync (projection: 'T -> Task< ^U >) (source: TaskSeq<'T>) : Task< ^U > =
45+
if obj.ReferenceEquals(source, null) then
46+
nullArg (nameof source)
47+
48+
task {
49+
use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None)
50+
let mutable acc = Unchecked.defaultof< ^U>
51+
52+
while! e.MoveNextAsync() do
53+
let! value = projection e.Current
54+
acc <- acc + value
55+
56+
return acc
57+
}
58+
59+
let inline average (source: TaskSeq< ^T >) : Task< ^T > =
60+
if obj.ReferenceEquals(source, null) then
61+
nullArg (nameof source)
62+
63+
task {
64+
use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None)
65+
let mutable acc = Unchecked.defaultof< ^T>
66+
let mutable count = 0
67+
68+
while! e.MoveNextAsync() do
69+
acc <- acc + e.Current
70+
count <- count + 1
71+
72+
if count = 0 then
73+
invalidArg (nameof source) "The input task sequence was empty."
74+
75+
return LanguagePrimitives.DivideByInt acc count
76+
}
77+
78+
let inline averageBy (projection: 'T -> ^U) (source: TaskSeq<'T>) : Task< ^U > =
79+
if obj.ReferenceEquals(source, null) then
80+
nullArg (nameof source)
81+
82+
task {
83+
use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None)
84+
let mutable acc = Unchecked.defaultof< ^U>
85+
let mutable count = 0
86+
87+
while! e.MoveNextAsync() do
88+
acc <- acc + projection e.Current
89+
count <- count + 1
90+
91+
if count = 0 then
92+
invalidArg (nameof source) "The input task sequence was empty."
93+
94+
return LanguagePrimitives.DivideByInt acc count
95+
}
96+
97+
let inline averageByAsync (projection: 'T -> Task< ^U >) (source: TaskSeq<'T>) : Task< ^U > =
98+
if obj.ReferenceEquals(source, null) then
99+
nullArg (nameof source)
100+
101+
task {
102+
use e = source.GetAsyncEnumerator(System.Threading.CancellationToken.None)
103+
let mutable acc = Unchecked.defaultof< ^U>
104+
let mutable count = 0
105+
106+
while! e.MoveNextAsync() do
107+
let! value = projection e.Current
108+
acc <- acc + value
109+
count <- count + 1
110+
111+
if count = 0 then
112+
invalidArg (nameof source) "The input task sequence was empty."
113+
114+
return LanguagePrimitives.DivideByInt acc count
115+
}
116+
16117

17118
[<Sealed; AbstractClass>]
18119
type TaskSeq private () =
@@ -166,6 +267,7 @@ type TaskSeq private () =
166267
static member minBy projection source = Internal.maxMinBy (>) projection source
167268
static member maxByAsync projection source = Internal.maxMinByAsync (<) projection source // looks like 'less than', is 'greater than'
168269
static member minByAsync projection source = Internal.maxMinByAsync (>) projection source
270+
169271
static member length source = Internal.lengthBy None source
170272
static member lengthOrMax max source = Internal.lengthBeforeMax max source
171273
static member lengthBy predicate source = Internal.lengthBy (Some(Predicate predicate)) source

0 commit comments

Comments
 (0)