Skip to content

Commit 5fd6062

Browse files
authored
Merge branch 'main' into repo-assist/feat-replicate-zip3-2026-03-6cd05ed182dd3f33
2 parents 28f77d1 + 25abafe commit 5fd6062

File tree

12 files changed

+1203
-17
lines changed

12 files changed

+1203
-17
lines changed

.github/workflows/test-report.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
- completed
1212
jobs:
1313
test-report-release:
14-
runs-on: windows-latest
14+
runs-on: ubuntu-latest
1515
steps:
1616
- uses: dorny/test-reporter@v2
1717
with:
@@ -21,7 +21,7 @@ jobs:
2121
reporter: dotnet-trx # Format of test results
2222

2323
test-report-debug:
24-
runs-on: windows-latest
24+
runs-on: ubuntu-latest
2525
steps:
2626
- uses: dorny/test-reporter@v2
2727
with:

README.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -211,15 +211,16 @@ The `TaskSeq` project already has a wide array of functions and functionalities,
211211
- [ ] `average` / `averageBy`, `sum` and related
212212
- [x] `forall` / `forallAsync` (see [#240])
213213
- [x] `skip` / `drop` / `truncate` / `take` (see [#209])
214-
- [ ] `chunkBySize` / `windowed`
214+
- [x] `chunkBySize` / `windowed` (see [#258])
215215
- [ ] `compareWith`
216216
- [ ] `distinct`
217217
- [ ] `exists2` / `map2` / `fold2` / `iter2` and related '2'-functions
218218
- [ ] `mapFold`
219-
- [ ] `pairwise` / `allpairs` / `permute` / `distinct` / `distinctBy`
219+
- [x] `pairwise` (see [#293])
220+
- [ ] `allpairs` / `permute` / `distinct` / `distinctBy`
220221
- [x] `replicate`
221-
- [ ] `reduce` / `scan`
222-
- [ ] `unfold`
222+
- [x] `reduce` / `scan` (see [#299], [#296])
223+
- [x] `unfold` (see [#300])
223224
- [x] Publish package on Nuget, **DONE, PUBLISHED SINCE: 7 November 2022**. See https://www.nuget.org/packages/FSharp.Control.TaskSeq
224225
- [x] Make `TaskSeq` interoperable with `Task` by expanding the latter with a `for .. in .. do` that acceps task sequences
225226
- [x] Add to/from functions to seq, list, array
@@ -263,7 +264,7 @@ This is what has been implemented so far, is planned or skipped:
263264
| ✅ [#67][] | | | `box` | |
264265
| ✅ [#67][] | | | `unbox` | |
265266
| ✅ [#23][] | `choose` | `choose` | `chooseAsync` | |
266-
| | `chunkBySize` | `chunkBySize` | | |
267+
| ✅ [#258][] | `chunkBySize` | `chunkBySize` | | |
267268
| ✅ [#11][] | `collect` | `collect` | `collectAsync` | |
268269
| ✅ [#11][] | | `collectSeq` | `collectSeqAsync` | |
269270
| | `compareWith` | `compareWith` | `compareWithAsync` | |
@@ -332,17 +333,17 @@ This is what has been implemented so far, is planned or skipped:
332333
| ✅ [#2][] | | `ofTaskArray` | | |
333334
| ✅ [#2][] | | `ofTaskList` | | |
334335
| ✅ [#2][] | | `ofTaskSeq` | | |
335-
| | `pairwise` | `pairwise` | | |
336+
| ✅ [#293][] | `pairwise` | `pairwise` | | |
336337
| | `permute` | `permute` | `permuteAsync` | |
337338
| ✅ [#23][] | `pick` | `pick` | `pickAsync` | |
338339
| &#x1f6ab; | `readOnly` | | | [note #3](#note3 "The motivation for 'readOnly' in 'Seq' is that a cast from a mutable array or list to a 'seq<_>' is valid and can be cast back, leading to a mutable sequence. Since 'TaskSeq' doesn't implement 'IEnumerable<_>', such casts are not possible.") |
339-
| | `reduce` | `reduce` | `reduceAsync` | |
340+
| &#x2705; [#299][] | `reduce` | `reduce` | `reduceAsync` | |
340341
| &#x1f6ab; | `reduceBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") |
341342
| &#x2705; [#236][]| `removeAt` | `removeAt` | | |
342343
| &#x2705; [#236][]| `removeManyAt` | `removeManyAt` | | |
343344
| &#x2705; | `replicate` | `replicate` | | |
344345
| &#x2753; | `rev` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
345-
| | `scan` | `scan` | `scanAsync` | |
346+
| &#x2705; [#296][] | `scan` | `scan` | `scanAsync` | |
346347
| &#x1f6ab; | `scanBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") |
347348
| &#x2705; [#90][] | `singleton` | `singleton` | | |
348349
| &#x2705; [#209][]| `skip` | `skip` | | |
@@ -378,10 +379,10 @@ This is what has been implemented so far, is planned or skipped:
378379
| &#x2705; [#23][] | `tryLast` | `tryLast` | | |
379380
| &#x2705; [#23][] | `tryPick` | `tryPick` | `tryPickAsync` | |
380381
| &#x2705; [#76][] | | `tryTail` | | |
381-
| | `unfold` | `unfold` | `unfoldAsync` | |
382+
| &#x2705; [#300][] | `unfold` | `unfold` | `unfoldAsync` | |
382383
| &#x2705; [#236][]| `updateAt` | `updateAt` | | |
383384
| &#x2705; [#217][]| `where` | `where` | `whereAsync` | |
384-
| | `windowed` | `windowed` | | |
385+
| &#x2705; [#258][] | `windowed` | `windowed` | | |
385386
| &#x2705; [#2][] | `zip` | `zip` | | |
386387
| &#x2705; | `zip3` | `zip3` | | |
387388
| | | `zip4` | | |
@@ -653,6 +654,11 @@ module TaskSeq =
653654
[#237]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/237
654655
[#236]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/236
655656
[#240]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/240
657+
[#258]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/258
658+
[#293]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/293
659+
[#296]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/296
660+
[#299]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/299
661+
[#300]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/300
656662

657663
[issues]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues
658664
[nuget]: https://www.nuget.org/packages/FSharp.Control.TaskSeq/

global.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"sdk": {
3-
"version": "10.0.103",
3+
"version": "10.0.102",
44
"rollForward": "minor"
55
}
66
}

release-notes.txt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11

22
Release notes:
33

4-
0.5.0
5-
- update engineering to .NET 9/10
4+
0.6.0
65
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
76
- adds TaskSeq.pairwise, #289
7+
- adds TaskSeq.mapFold and TaskSeq.mapFoldAsync
8+
- adds TaskSeq.sum, sumBy, sumByAsync, average, averageBy, averageByAsync
9+
- adds TaskSeq.reduce and TaskSeq.reduceAsync, #289
10+
- adds TaskSeq.unfold and TaskSeq.unfoldAsync, #289
11+
- adds TaskSeq.chunkBySize (closes #258) and TaskSeq.windowed, #289
812
- fixes: CancellationToken passed to GetAsyncEnumerator is now honored in MoveNextAsync, #179
913

14+
0.5.0
15+
- update engineering to .NET 9/10
16+
1017
0.4.0
1118
- overhaul all doc comments, add exceptions, improve IDE quick-info experience, #136, #220, #234
1219
- new surface area functions, fixes #208:

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
<Compile Include="TaskSeq.Find.Tests.fs" />
2828
<Compile Include="TaskSeq.Fold.Tests.fs" />
2929
<Compile Include="TaskSeq.Scan.Tests.fs" />
30+
<Compile Include="TaskSeq.MapFold.Tests.fs" />
3031
<Compile Include="TaskSeq.Reduce.Tests.fs" />
3132
<Compile Include="TaskSeq.Forall.Tests.fs" />
3233
<Compile Include="TaskSeq.Head.Tests.fs" />
@@ -41,6 +42,7 @@
4142
<Compile Include="TaskSeq.Length.Tests.fs" />
4243
<Compile Include="TaskSeq.Map.Tests.fs" />
4344
<Compile Include="TaskSeq.MaxMin.Tests.fs" />
45+
<Compile Include="TaskSeq.SumBy.Tests.fs" />
4446
<Compile Include="TaskSeq.OfXXX.Tests.fs" />
4547
<Compile Include="TaskSeq.Pick.Tests.fs" />
4648
<Compile Include="TaskSeq.RemoveAt.Tests.fs" />
@@ -54,6 +56,8 @@
5456
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
5557
<Compile Include="TaskSeq.UpdateAt.Tests.fs" />
5658
<Compile Include="TaskSeq.Zip.Tests.fs" />
59+
<Compile Include="TaskSeq.ChunkBySize.Tests.fs" />
60+
<Compile Include="TaskSeq.Windowed.Tests.fs" />
5761
<Compile Include="TaskSeq.Tests.CE.fs" />
5862
<Compile Include="TaskSeq.StateTransitionBug.Tests.CE.fs" />
5963
<Compile Include="TaskSeq.StateTransitionBug-delayed.Tests.CE.fs" />
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
module TaskSeq.Tests.ChunkBySize
2+
3+
open Xunit
4+
open FsUnit.Xunit
5+
6+
open FSharp.Control
7+
8+
//
9+
// TaskSeq.chunkBySize
10+
//
11+
12+
module EmptySeq =
13+
[<Fact>]
14+
let ``TaskSeq-chunkBySize with null source raises`` () = assertNullArg <| fun () -> TaskSeq.chunkBySize 1 null
15+
16+
[<Fact>]
17+
let ``TaskSeq-chunkBySize with zero raises ArgumentException before awaiting`` () =
18+
fun () -> TaskSeq.empty<int> |> TaskSeq.chunkBySize 0 |> ignore // throws eagerly, before enumeration
19+
|> should throw typeof<System.ArgumentException>
20+
21+
[<Fact>]
22+
let ``TaskSeq-chunkBySize with negative raises ArgumentException before awaiting`` () =
23+
fun () -> TaskSeq.empty<int> |> TaskSeq.chunkBySize -1 |> ignore
24+
|> should throw typeof<System.ArgumentException>
25+
26+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
27+
let ``TaskSeq-chunkBySize on empty sequence yields empty`` variant =
28+
Gen.getEmptyVariant variant
29+
|> TaskSeq.chunkBySize 1
30+
|> verifyEmpty
31+
32+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
33+
let ``TaskSeq-chunkBySize(99) on empty sequence yields empty`` variant =
34+
Gen.getEmptyVariant variant
35+
|> TaskSeq.chunkBySize 99
36+
|> verifyEmpty
37+
38+
module Immutable =
39+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
40+
let ``TaskSeq-chunkBySize preserves all elements in order`` variant = task {
41+
do!
42+
Gen.getSeqImmutable variant
43+
|> TaskSeq.chunkBySize 3
44+
|> TaskSeq.collect TaskSeq.ofArray
45+
|> verify1To10
46+
}
47+
48+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
49+
let ``TaskSeq-chunkBySize(2) returns 5 chunks of 2 for a 10-element sequence`` variant = task {
50+
let! chunks =
51+
Gen.getSeqImmutable variant
52+
|> TaskSeq.chunkBySize 2
53+
|> TaskSeq.toArrayAsync
54+
55+
chunks
56+
|> should equal [| [| 1; 2 |]; [| 3; 4 |]; [| 5; 6 |]; [| 7; 8 |]; [| 9; 10 |] |]
57+
}
58+
59+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
60+
let ``TaskSeq-chunkBySize(5) returns 2 full chunks for a 10-element sequence`` variant = task {
61+
let! chunks =
62+
Gen.getSeqImmutable variant
63+
|> TaskSeq.chunkBySize 5
64+
|> TaskSeq.toArrayAsync
65+
66+
chunks |> should equal [| [| 1..5 |]; [| 6..10 |] |]
67+
}
68+
69+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
70+
let ``TaskSeq-chunkBySize(1) returns each element as its own array`` variant = task {
71+
let! chunks =
72+
Gen.getSeqImmutable variant
73+
|> TaskSeq.chunkBySize 1
74+
|> TaskSeq.toArrayAsync
75+
76+
chunks |> Array.length |> should equal 10
77+
78+
chunks
79+
|> Array.iteri (fun i chunk -> chunk |> should equal [| i + 1 |])
80+
}
81+
82+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
83+
let ``TaskSeq-chunkBySize last chunk contains remainder when sequence does not divide evenly`` variant = task {
84+
// 10 elements with chunk size 3 → chunks [1;2;3] [4;5;6] [7;8;9] [10]
85+
let! chunks =
86+
Gen.getSeqImmutable variant
87+
|> TaskSeq.chunkBySize 3
88+
|> TaskSeq.toArrayAsync
89+
90+
chunks |> Array.length |> should equal 4
91+
chunks |> Array.last |> should equal [| 10 |]
92+
}
93+
94+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
95+
let ``TaskSeq-chunkBySize larger than sequence returns single chunk with all elements`` variant = task {
96+
let! chunks =
97+
Gen.getSeqImmutable variant
98+
|> TaskSeq.chunkBySize 11
99+
|> TaskSeq.toArrayAsync
100+
101+
chunks |> Array.length |> should equal 1
102+
chunks.[0] |> should equal [| 1..10 |]
103+
}
104+
105+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
106+
let ``TaskSeq-chunkBySize equal to sequence length returns single full chunk`` variant = task {
107+
let! chunks =
108+
Gen.getSeqImmutable variant
109+
|> TaskSeq.chunkBySize 10
110+
|> TaskSeq.toArrayAsync
111+
112+
chunks |> Array.length |> should equal 1
113+
chunks.[0] |> should equal [| 1..10 |]
114+
}
115+
116+
[<Fact>]
117+
let ``TaskSeq-chunkBySize each chunk array is independent - modifying one does not affect others`` () = task {
118+
let! chunks =
119+
taskSeq { yield! [ 1..6 ] }
120+
|> TaskSeq.chunkBySize 3
121+
|> TaskSeq.toArrayAsync
122+
123+
// Mutate the first chunk
124+
chunks.[0].[0] <- 99
125+
126+
// The second chunk must be unaffected
127+
chunks.[1] |> should equal [| 4; 5; 6 |]
128+
chunks.[0].[0] |> should equal 99
129+
}
130+
131+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
132+
let ``TaskSeq-chunkBySize remainder sizes`` variant = task {
133+
let verifyLastChunkSize chunkSize expectedLast =
134+
Gen.getSeqImmutable variant
135+
|> TaskSeq.chunkBySize chunkSize
136+
|> TaskSeq.toArrayAsync
137+
|> Task.map (Array.last >> Array.length >> should equal expectedLast)
138+
139+
do! verifyLastChunkSize 3 1 // 10 mod 3 = 1
140+
do! verifyLastChunkSize 4 2 // 10 mod 4 = 2
141+
do! verifyLastChunkSize 6 4 // 10 mod 6 = 4
142+
do! verifyLastChunkSize 7 3 // 10 mod 7 = 3
143+
do! verifyLastChunkSize 9 1 // 10 mod 9 = 1
144+
}
145+
146+
module SideEffects =
147+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
148+
let ``TaskSeq-chunkBySize gets all items`` variant =
149+
Gen.getSeqWithSideEffect variant
150+
|> TaskSeq.chunkBySize 5
151+
|> TaskSeq.toArrayAsync
152+
|> Task.map (should equal [| [| 1..5 |]; [| 6..10 |] |])
153+
154+
[<Fact>]
155+
let ``TaskSeq-chunkBySize executes side-effects from empty source`` () = task {
156+
let mutable sideEffects = 0
157+
158+
let ts = taskSeq {
159+
sideEffects <- sideEffects + 1
160+
sideEffects <- sideEffects + 1
161+
}
162+
163+
do! ts |> TaskSeq.chunkBySize 1 |> consumeTaskSeq
164+
do! ts |> TaskSeq.chunkBySize 3 |> consumeTaskSeq
165+
sideEffects |> should equal 4
166+
}
167+
168+
[<Fact>]
169+
let ``TaskSeq-chunkBySize executes all source side-effects`` () = task {
170+
let mutable sideEffects = 0
171+
172+
let ts = taskSeq {
173+
sideEffects <- sideEffects + 1
174+
yield 1
175+
sideEffects <- sideEffects + 1
176+
yield 2
177+
sideEffects <- sideEffects + 1 // executed even after last yield
178+
}
179+
180+
do! ts |> TaskSeq.chunkBySize 2 |> consumeTaskSeq
181+
sideEffects |> should equal 3
182+
}
183+
184+
[<Fact>]
185+
let ``TaskSeq-chunkBySize propagates exception from source`` () =
186+
let items = taskSeq {
187+
yield 1
188+
yield 2
189+
failwith "boom"
190+
yield 3
191+
}
192+
193+
fun () -> items |> TaskSeq.chunkBySize 2 |> consumeTaskSeq
194+
|> should throwAsyncExact typeof<System.Exception>

0 commit comments

Comments
 (0)