Skip to content

Commit e1169e4

Browse files
authored
Merge branch 'main' into repo-assist/improve-xmldoc-returns-tags-20260316-57d9cdc0fc84d55c
2 parents bde085c + a4b36b7 commit e1169e4

13 files changed

+1709
-1
lines changed

release-notes.txt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@
22
Release notes:
33

44
1.0.0
5+
- adds TaskSeq.distinctUntilChangedWith and TaskSeq.distinctUntilChangedWithAsync, #345
56
- adds TaskSeq.withCancellation, #167
7+
- adds TaskSeq.replicateInfinite, replicateInfiniteAsync, replicateUntilNoneAsync, #345
8+
- adds TaskSeq.firstOrDefault, lastOrDefault, #345
9+
- adds TaskSeq.splitAt, #345
10+
- adds TaskSeq.zipWith, zipWithAsync, zipWith3, zipWithAsync3, #345
11+
- adds TaskSeq.chunkBy, chunkByAsync, #345
12+
- adds TaskSeq.threadState, threadStateAsync, #345
613
- adds docs/ with fsdocs-based documentation site covering generating, transforming, consuming, combining and advanced operations
714
- docs: adds missing XML <returns> documentation tags to singleton, isEmpty, length, lengthOrMax, lengthBy, and lengthByAsync
15+
- test: adds 70 new tests to TaskSeq.Fold.Tests.fs covering call-count assertions, folder-not-called-on-empty, ordering, null initial state, and fold/foldAsync equivalence
816

917
0.7.0
1018
- performance: TaskSeq.exists, existsAsync, contains no longer allocate an intermediate Option value

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,23 @@
5050
<Compile Include="TaskSeq.RemoveAt.Tests.fs" />
5151
<Compile Include="TaskSeq.Singleton.Tests.fs" />
5252
<Compile Include="TaskSeq.Replicate.Tests.fs" />
53+
<Compile Include="TaskSeq.ReplicateInfinite.Tests.fs" />
5354
<Compile Include="TaskSeq.Skip.Tests.fs" />
5455
<Compile Include="TaskSeq.SkipWhile.Tests.fs" />
56+
<Compile Include="TaskSeq.SplitAt.Tests.fs" />
5557
<Compile Include="TaskSeq.Tail.Tests.fs" />
5658
<Compile Include="TaskSeq.Take.Tests.fs" />
5759
<Compile Include="TaskSeq.TakeWhile.Tests.fs" />
5860
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
5961
<Compile Include="TaskSeq.UpdateAt.Tests.fs" />
6062
<Compile Include="TaskSeq.Zip.Tests.fs" />
63+
<Compile Include="TaskSeq.ZipWith.Tests.fs" />
6164
<Compile Include="TaskSeq.CompareWith.Tests.fs" />
6265
<Compile Include="TaskSeq.ChunkBySize.Tests.fs" />
66+
<Compile Include="TaskSeq.ChunkBy.Tests.fs" />
6367
<Compile Include="TaskSeq.Windowed.Tests.fs" />
68+
<Compile Include="TaskSeq.FirstLastDefault.Tests.fs" />
69+
<Compile Include="TaskSeq.ThreadState.Tests.fs" />
6470
<Compile Include="TaskSeq.Tests.CE.fs" />
6571
<Compile Include="TaskSeq.StateTransitionBug.Tests.CE.fs" />
6672
<Compile Include="TaskSeq.StateTransitionBug-delayed.Tests.CE.fs" />
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
module TaskSeq.Tests.ChunkBy
2+
3+
open Xunit
4+
open FsUnit.Xunit
5+
6+
open FSharp.Control
7+
8+
//
9+
// TaskSeq.chunkBy
10+
// TaskSeq.chunkByAsync
11+
//
12+
13+
module EmptySeq =
14+
[<Fact>]
15+
let ``Null source is invalid`` () =
16+
assertNullArg
17+
<| fun () -> TaskSeq.chunkBy id (null: TaskSeq<int>)
18+
19+
assertNullArg
20+
<| fun () -> TaskSeq.chunkByAsync (fun x -> Task.fromResult x) (null: TaskSeq<int>)
21+
22+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
23+
let ``TaskSeq-chunkBy on empty gives empty`` variant =
24+
Gen.getEmptyVariant variant
25+
|> TaskSeq.chunkBy id
26+
|> verifyEmpty
27+
28+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
29+
let ``TaskSeq-chunkByAsync on empty gives empty`` variant =
30+
Gen.getEmptyVariant variant
31+
|> TaskSeq.chunkByAsync (fun x -> Task.fromResult x)
32+
|> verifyEmpty
33+
34+
35+
module Functionality =
36+
[<Fact>]
37+
let ``TaskSeq-chunkBy groups consecutive equal elements`` () = task {
38+
let ts = taskSeq { yield! [ 1; 1; 2; 2; 2; 3 ] }
39+
let! result = TaskSeq.chunkBy id ts |> TaskSeq.toArrayAsync
40+
result |> should haveLength 3
41+
result[0] |> should equal (1, [| 1; 1 |])
42+
result[1] |> should equal (2, [| 2; 2; 2 |])
43+
result[2] |> should equal (3, [| 3 |])
44+
}
45+
46+
[<Fact>]
47+
let ``TaskSeq-chunkBy with all same key yields one chunk`` () = task {
48+
let ts = taskSeq { yield! [ 5; 5; 5; 5 ] }
49+
let! result = TaskSeq.chunkBy id ts |> TaskSeq.toArrayAsync
50+
result |> should haveLength 1
51+
result[0] |> should equal (5, [| 5; 5; 5; 5 |])
52+
}
53+
54+
[<Fact>]
55+
let ``TaskSeq-chunkBy with all different keys yields singleton chunks`` () = task {
56+
let ts = taskSeq { yield! [ 1..5 ] }
57+
let! result = TaskSeq.chunkBy id ts |> TaskSeq.toArrayAsync
58+
result |> should haveLength 5
59+
60+
result
61+
|> Array.iteri (fun i (k, arr) ->
62+
k |> should equal (i + 1)
63+
arr |> should equal [| i + 1 |])
64+
}
65+
66+
[<Fact>]
67+
let ``TaskSeq-chunkBy with singleton source yields one chunk`` () = task {
68+
let ts = TaskSeq.singleton 42
69+
let! result = TaskSeq.chunkBy id ts |> TaskSeq.toArrayAsync
70+
result |> should haveLength 1
71+
result[0] |> should equal (42, [| 42 |])
72+
}
73+
74+
[<Fact>]
75+
let ``TaskSeq-chunkBy uses projection key, not element`` () = task {
76+
let ts = taskSeq {
77+
yield "a1"
78+
yield "a2"
79+
yield "b1"
80+
yield "b2"
81+
yield "a3"
82+
}
83+
84+
let! result =
85+
TaskSeq.chunkBy (fun (s: string) -> s[0]) ts
86+
|> TaskSeq.toArrayAsync
87+
88+
result |> should haveLength 3
89+
let k0, arr0 = result[0]
90+
k0 |> should equal 'a'
91+
arr0 |> should equal [| "a1"; "a2" |]
92+
let k1, arr1 = result[1]
93+
k1 |> should equal 'b'
94+
arr1 |> should equal [| "b1"; "b2" |]
95+
let k2, arr2 = result[2]
96+
k2 |> should equal 'a'
97+
arr2 |> should equal [| "a3" |]
98+
}
99+
100+
[<Fact>]
101+
let ``TaskSeq-chunkBy does not merge non-consecutive equal keys`` () = task {
102+
// Key alternates: 1, 2, 1, 2 — should produce 4 chunks not 2
103+
let ts = taskSeq { yield! [ 1; 2; 1; 2 ] }
104+
let! result = TaskSeq.chunkBy id ts |> TaskSeq.toArrayAsync
105+
result |> should haveLength 4
106+
}
107+
108+
[<Fact>]
109+
let ``TaskSeq-chunkByAsync groups consecutive by async key`` () = task {
110+
let ts = taskSeq { yield! [ 1; 1; 2; 3; 3 ] }
111+
112+
let! result =
113+
TaskSeq.chunkByAsync (fun x -> Task.fromResult (x % 2 = 0)) ts
114+
|> TaskSeq.toArrayAsync
115+
// odd, even, odd -> 3 chunks
116+
result |> should haveLength 3
117+
let k0, arr0 = result[0]
118+
k0 |> should equal false
119+
arr0 |> should equal [| 1; 1 |]
120+
let k1, arr1 = result[1]
121+
k1 |> should equal true
122+
arr1 |> should equal [| 2 |]
123+
let k2, arr2 = result[2]
124+
k2 |> should equal false
125+
arr2 |> should equal [| 3; 3 |]
126+
}
127+
128+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
129+
let ``TaskSeq-chunkBy all elements same key as variants`` variant = task {
130+
let ts = Gen.getSeqImmutable variant
131+
let! result = TaskSeq.chunkBy (fun _ -> 0) ts |> TaskSeq.toArrayAsync
132+
result |> should haveLength 1
133+
let _, arr = result[0]
134+
arr |> should haveLength 10
135+
}

src/FSharp.Control.TaskSeq.Test/TaskSeq.DistinctUntilChanged.Tests.fs

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ open FsUnit.Xunit
66
open FSharp.Control
77

88
//
9-
// TaskSeq.distinctUntilChanged
9+
// TaskSeq.distinctUntilChanged / distinctUntilChangedWith / distinctUntilChangedWithAsync
1010
//
1111

1212

@@ -23,6 +23,34 @@ module EmptySeq =
2323
|> Task.map (List.isEmpty >> should be True)
2424
}
2525

26+
[<Fact>]
27+
let ``TaskSeq-distinctUntilChangedWith with null source raises`` () =
28+
assertNullArg
29+
<| fun () -> TaskSeq.distinctUntilChangedWith (fun _ _ -> false) null
30+
31+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
32+
let ``TaskSeq-distinctUntilChangedWith has no effect on empty`` variant = task {
33+
do!
34+
Gen.getEmptyVariant variant
35+
|> TaskSeq.distinctUntilChangedWith (fun _ _ -> false)
36+
|> TaskSeq.toListAsync
37+
|> Task.map (List.isEmpty >> should be True)
38+
}
39+
40+
[<Fact>]
41+
let ``TaskSeq-distinctUntilChangedWithAsync with null source raises`` () =
42+
assertNullArg
43+
<| fun () -> TaskSeq.distinctUntilChangedWithAsync (fun _ _ -> task { return false }) null
44+
45+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
46+
let ``TaskSeq-distinctUntilChangedWithAsync has no effect on empty`` variant = task {
47+
do!
48+
Gen.getEmptyVariant variant
49+
|> TaskSeq.distinctUntilChangedWithAsync (fun _ _ -> task { return false })
50+
|> TaskSeq.toListAsync
51+
|> Task.map (List.isEmpty >> should be True)
52+
}
53+
2654
module Functionality =
2755
[<Fact>]
2856
let ``TaskSeq-distinctUntilChanged should return no consecutive duplicates`` () = task {
@@ -90,6 +118,129 @@ module Functionality =
90118
xs |> should equal [ 1..10 ]
91119
}
92120

121+
[<Fact>]
122+
let ``TaskSeq-distinctUntilChangedWith with structural equality comparer behaves like distinctUntilChanged`` () = task {
123+
let ts =
124+
[ 'A'; 'A'; 'B'; 'Z'; 'C'; 'C'; 'Z'; 'C'; 'D'; 'D'; 'D'; 'Z' ]
125+
|> TaskSeq.ofList
126+
127+
let! xs =
128+
ts
129+
|> TaskSeq.distinctUntilChangedWith (=)
130+
|> TaskSeq.toListAsync
131+
132+
xs
133+
|> List.map string
134+
|> String.concat ""
135+
|> should equal "ABZCZCDZ"
136+
}
137+
138+
[<Fact>]
139+
let ``TaskSeq-distinctUntilChangedWith with always-true comparer returns only first element`` () = task {
140+
let! xs =
141+
taskSeq { yield! [ 1; 2; 3; 4; 5 ] }
142+
|> TaskSeq.distinctUntilChangedWith (fun _ _ -> true)
143+
|> TaskSeq.toListAsync
144+
145+
xs |> should equal [ 1 ]
146+
}
147+
148+
[<Fact>]
149+
let ``TaskSeq-distinctUntilChangedWith with always-false comparer returns all elements`` () = task {
150+
let! xs =
151+
taskSeq { yield! [ 1; 1; 2; 2; 3 ] }
152+
|> TaskSeq.distinctUntilChangedWith (fun _ _ -> false)
153+
|> TaskSeq.toListAsync
154+
155+
xs |> should equal [ 1; 1; 2; 2; 3 ]
156+
}
157+
158+
[<Fact>]
159+
let ``TaskSeq-distinctUntilChangedWith can use custom projection for equality`` () = task {
160+
// Treat values as equal if their absolute difference is <= 1
161+
let closeEnough a b = abs (a - b) <= 1
162+
163+
let! xs =
164+
taskSeq { yield! [ 10; 11; 9; 20; 21; 5 ] }
165+
|> TaskSeq.distinctUntilChangedWith closeEnough
166+
|> TaskSeq.toListAsync
167+
168+
// 10≈11 skip; 11≈9 skip (|11-9|=2? no, |11-9|=2>1, so keep 9); 9 vs 20 keep; 20≈21 skip; 21 vs 5 keep
169+
// Wait: |10-11|=1 skip 11; |10-9|=1 skip 9; 10 vs 20 keep 20; |20-21|=1 skip 21; 20 vs 5 keep 5
170+
xs |> should equal [ 10; 20; 5 ]
171+
}
172+
173+
[<Fact>]
174+
let ``TaskSeq-distinctUntilChangedWith with single element returns singleton`` () = task {
175+
let! xs =
176+
taskSeq { yield 99 }
177+
|> TaskSeq.distinctUntilChangedWith (fun _ _ -> true)
178+
|> TaskSeq.toListAsync
179+
180+
xs |> should equal [ 99 ]
181+
}
182+
183+
[<Fact>]
184+
let ``TaskSeq-distinctUntilChangedWith case-insensitive string comparison`` () = task {
185+
let! xs =
186+
taskSeq { yield! [ "Hello"; "hello"; "HELLO"; "World"; "world" ] }
187+
|> TaskSeq.distinctUntilChangedWith (fun a b -> System.String.Compare(a, b, System.StringComparison.OrdinalIgnoreCase) = 0)
188+
|> TaskSeq.toListAsync
189+
190+
xs |> should equal [ "Hello"; "World" ]
191+
}
192+
193+
[<Fact>]
194+
let ``TaskSeq-distinctUntilChangedWithAsync with structural equality behaves like distinctUntilChanged`` () = task {
195+
let ts = [ 1; 1; 2; 3; 3; 4 ] |> TaskSeq.ofList
196+
197+
let! xs =
198+
ts
199+
|> TaskSeq.distinctUntilChangedWithAsync (fun a b -> task { return a = b })
200+
|> TaskSeq.toListAsync
201+
202+
xs |> should equal [ 1; 2; 3; 4 ]
203+
}
204+
205+
[<Fact>]
206+
let ``TaskSeq-distinctUntilChangedWithAsync with always-true async comparer returns only first element`` () = task {
207+
let! xs =
208+
taskSeq { yield! [ 10; 20; 30 ] }
209+
|> TaskSeq.distinctUntilChangedWithAsync (fun _ _ -> task { return true })
210+
|> TaskSeq.toListAsync
211+
212+
xs |> should equal [ 10 ]
213+
}
214+
215+
[<Fact>]
216+
let ``TaskSeq-distinctUntilChangedWithAsync with always-false async comparer returns all elements`` () = task {
217+
let! xs =
218+
taskSeq { yield! [ 5; 5; 5 ] }
219+
|> TaskSeq.distinctUntilChangedWithAsync (fun _ _ -> task { return false })
220+
|> TaskSeq.toListAsync
221+
222+
xs |> should equal [ 5; 5; 5 ]
223+
}
224+
225+
[<Fact>]
226+
let ``TaskSeq-distinctUntilChangedWithAsync can perform async work in comparer`` () = task {
227+
let mutable comparerCallCount = 0
228+
229+
let asyncComparer a b = task {
230+
comparerCallCount <- comparerCallCount + 1
231+
return a = b
232+
}
233+
234+
let! xs =
235+
taskSeq { yield! [ 1; 1; 2; 2; 3 ] }
236+
|> TaskSeq.distinctUntilChangedWithAsync asyncComparer
237+
|> TaskSeq.toListAsync
238+
239+
xs |> should equal [ 1; 2; 3 ]
240+
// comparer called for each pair of consecutive elements (4 pairs for 5 elements)
241+
comparerCallCount |> should equal 4
242+
}
243+
93244
module SideEffects =
94245
[<Fact>]
95246
let ``TaskSeq-distinctUntilChanged consumes every element exactly once`` () = task {
@@ -132,3 +283,41 @@ module SideEffects =
132283

133284
xs |> should equal [ 1..10 ]
134285
}
286+
287+
[<Fact>]
288+
let ``TaskSeq-distinctUntilChangedWith consumes every element exactly once`` () = task {
289+
let mutable count = 0
290+
291+
let ts = taskSeq {
292+
for i in 1..5 do
293+
count <- count + 1
294+
yield i
295+
}
296+
297+
let! xs =
298+
ts
299+
|> TaskSeq.distinctUntilChangedWith (fun a b -> a = b)
300+
|> TaskSeq.toListAsync
301+
302+
count |> should equal 5
303+
xs |> should equal [ 1; 2; 3; 4; 5 ]
304+
}
305+
306+
[<Fact>]
307+
let ``TaskSeq-distinctUntilChangedWithAsync consumes every element exactly once`` () = task {
308+
let mutable count = 0
309+
310+
let ts = taskSeq {
311+
for i in 1..5 do
312+
count <- count + 1
313+
yield i
314+
}
315+
316+
let! xs =
317+
ts
318+
|> TaskSeq.distinctUntilChangedWithAsync (fun a b -> task { return a = b })
319+
|> TaskSeq.toListAsync
320+
321+
count |> should equal 5
322+
xs |> should equal [ 1; 2; 3; 4; 5 ]
323+
}

0 commit comments

Comments
 (0)