Skip to content

Commit 83b1340

Browse files
authored
Merge pull request #318 from fsprojects/repo-assist/feat-comparewith-2026-03-4fd44af764428d65
[Repo Assist] feat: add TaskSeq.compareWith and TaskSeq.compareWithAsync (58 tests)
2 parents 3ad4684 + f29a403 commit 83b1340

File tree

7 files changed

+342
-3
lines changed

7 files changed

+342
-3
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ The `TaskSeq` project already has a wide array of functions and functionalities,
212212
- [x] `forall` / `forallAsync` (see [#240])
213213
- [x] `skip` / `drop` / `truncate` / `take` (see [#209])
214214
- [x] `chunkBySize` / `windowed` (see [#258])
215-
- [ ] `compareWith`
215+
- [x] `compareWith` / `compareWithAsync`
216216
- [ ] `distinct`
217217
- [ ] `exists2` / `map2` / `fold2` / `iter2` and related '2'-functions
218218
- [ ] `mapFold`
@@ -267,7 +267,7 @@ This is what has been implemented so far, is planned or skipped:
267267
| ✅ [#258][] | `chunkBySize` | `chunkBySize` | | |
268268
| ✅ [#11][] | `collect` | `collect` | `collectAsync` | |
269269
| ✅ [#11][] | | `collectSeq` | `collectSeqAsync` | |
270-
| | `compareWith` | `compareWith` | `compareWithAsync` | |
270+
| ✅ | `compareWith` | `compareWith` | `compareWithAsync` | |
271271
| ✅ [#69][] | `concat` | `concat` | | |
272272
| ✅ [#237][]| `concat` (list) | `concat` (list) | | |
273273
| ✅ [#237][]| `concat` (array) | `concat` (array) | | |

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Release notes:
33

44
0.6.0
5+
- adds TaskSeq.compareWith and TaskSeq.compareWithAsync
56
- adds TaskSeq.scan and TaskSeq.scanAsync, #289
67
- adds TaskSeq.pairwise, #289
78
- adds TaskSeq.groupBy and TaskSeq.groupByAsync, #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
@@ -58,6 +58,7 @@
5858
<Compile Include="TaskSeq.ToXXX.Tests.fs" />
5959
<Compile Include="TaskSeq.UpdateAt.Tests.fs" />
6060
<Compile Include="TaskSeq.Zip.Tests.fs" />
61+
<Compile Include="TaskSeq.CompareWith.Tests.fs" />
6162
<Compile Include="TaskSeq.ChunkBySize.Tests.fs" />
6263
<Compile Include="TaskSeq.Windowed.Tests.fs" />
6364
<Compile Include="TaskSeq.Tests.CE.fs" />
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
module TaskSeq.Tests.CompareWith
2+
3+
open Xunit
4+
open FsUnit.Xunit
5+
6+
open FSharp.Control
7+
8+
//
9+
// TaskSeq.compareWith
10+
// TaskSeq.compareWithAsync
11+
//
12+
13+
let inline sign x =
14+
if x < 0 then -1
15+
elif x > 0 then 1
16+
else 0
17+
18+
module EmptySeq =
19+
[<Fact>]
20+
let ``Null source1 is invalid`` () =
21+
assertNullArg
22+
<| fun () -> TaskSeq.compareWith compare null TaskSeq.empty<int>
23+
24+
assertNullArg
25+
<| fun () -> TaskSeq.compareWithAsync (fun a b -> Task.fromResult (compare a b)) null TaskSeq.empty<int>
26+
27+
[<Fact>]
28+
let ``Null source2 is invalid`` () =
29+
assertNullArg
30+
<| fun () -> TaskSeq.compareWith compare TaskSeq.empty<int> null
31+
32+
assertNullArg
33+
<| fun () -> TaskSeq.compareWithAsync (fun a b -> Task.fromResult (compare a b)) TaskSeq.empty<int> null
34+
35+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
36+
let ``TaskSeq-compareWith of two empty sequences is 0`` variant = task {
37+
let empty = Gen.getEmptyVariant variant
38+
let! result = TaskSeq.compareWith compare empty TaskSeq.empty<int>
39+
result |> should equal 0
40+
}
41+
42+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
43+
let ``TaskSeq-compareWithAsync of two empty sequences is 0`` variant = task {
44+
let empty = Gen.getEmptyVariant variant
45+
let! result = TaskSeq.compareWithAsync (fun a b -> Task.fromResult (compare a b)) empty TaskSeq.empty<int>
46+
result |> should equal 0
47+
}
48+
49+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
50+
let ``TaskSeq-compareWith: empty source1 is less than non-empty source2`` variant = task {
51+
let empty = Gen.getEmptyVariant variant
52+
let! result = TaskSeq.compareWith compare empty (TaskSeq.singleton 1)
53+
result |> should equal -1
54+
}
55+
56+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
57+
let ``TaskSeq-compareWith: non-empty source1 is greater than empty source2`` variant = task {
58+
let empty = Gen.getEmptyVariant variant
59+
let! result = TaskSeq.compareWith compare (TaskSeq.singleton 1) empty
60+
result |> should equal 1
61+
}
62+
63+
64+
module Immutable =
65+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
66+
let ``TaskSeq-compareWith: equal sequences return 0`` variant = task {
67+
let src1 = Gen.getSeqImmutable variant
68+
let src2 = Gen.getSeqImmutable variant
69+
let! result = TaskSeq.compareWith compare src1 src2
70+
result |> should equal 0
71+
}
72+
73+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
74+
let ``TaskSeq-compareWithAsync: equal sequences return 0`` variant = task {
75+
let src1 = Gen.getSeqImmutable variant
76+
let src2 = Gen.getSeqImmutable variant
77+
let! result = TaskSeq.compareWithAsync (fun a b -> Task.fromResult (compare a b)) src1 src2
78+
result |> should equal 0
79+
}
80+
81+
[<Fact>]
82+
let ``TaskSeq-compareWith: first element differs`` () = task {
83+
let src1 = taskSeq {
84+
1
85+
2
86+
3
87+
}
88+
89+
let src2 = taskSeq {
90+
2
91+
2
92+
3
93+
}
94+
95+
let! result = TaskSeq.compareWith compare src1 src2
96+
sign result |> should equal -1
97+
}
98+
99+
[<Fact>]
100+
let ``TaskSeq-compareWith: last element differs`` () = task {
101+
let src1 = taskSeq {
102+
1
103+
2
104+
4
105+
}
106+
107+
let src2 = taskSeq {
108+
1
109+
2
110+
3
111+
}
112+
113+
let! result = TaskSeq.compareWith compare src1 src2
114+
sign result |> should equal 1
115+
}
116+
117+
[<Fact>]
118+
let ``TaskSeq-compareWith: source1 shorter returns negative`` () = task {
119+
let src1 = taskSeq {
120+
1
121+
2
122+
}
123+
124+
let src2 = taskSeq {
125+
1
126+
2
127+
3
128+
}
129+
130+
let! result = TaskSeq.compareWith compare src1 src2
131+
result |> should equal -1
132+
}
133+
134+
[<Fact>]
135+
let ``TaskSeq-compareWith: source2 shorter returns positive`` () = task {
136+
let src1 = taskSeq {
137+
1
138+
2
139+
3
140+
}
141+
142+
let src2 = taskSeq {
143+
1
144+
2
145+
}
146+
147+
let! result = TaskSeq.compareWith compare src1 src2
148+
result |> should equal 1
149+
}
150+
151+
[<Fact>]
152+
let ``TaskSeq-compareWith: uses custom comparer result sign`` () = task {
153+
// comparer returns a large number, not just -1/0/1
154+
let bigCompare (a: int) (b: int) = (a - b) * 100
155+
156+
let src1 = taskSeq {
157+
1
158+
2
159+
3
160+
}
161+
162+
let src2 = taskSeq {
163+
1
164+
2
165+
5
166+
}
167+
168+
let! result = TaskSeq.compareWith bigCompare src1 src2
169+
// 3 compared to 5 gives (3-5)*100 = -200, which is negative
170+
result |> should be (lessThan 0)
171+
}
172+
173+
[<Fact>]
174+
let ``TaskSeq-compareWith: stops at first non-zero comparison`` () = task {
175+
let mutable callCount = 0
176+
177+
let countingCompare a b =
178+
callCount <- callCount + 1
179+
compare a b
180+
181+
let src1 = taskSeq {
182+
1
183+
99
184+
99
185+
99
186+
}
187+
188+
let src2 = taskSeq {
189+
2
190+
99
191+
99
192+
99
193+
}
194+
195+
let! result = TaskSeq.compareWith countingCompare src1 src2
196+
sign result |> should equal -1
197+
// Should stop after first comparison
198+
callCount |> should equal 1
199+
}
200+
201+
[<Fact>]
202+
let ``TaskSeq-compareWithAsync: async comparer works`` () = task {
203+
let src1 = taskSeq {
204+
1
205+
2
206+
3
207+
}
208+
209+
let src2 = taskSeq {
210+
1
211+
2
212+
4
213+
}
214+
215+
let! result =
216+
TaskSeq.compareWithAsync
217+
(fun a b -> task {
218+
// simulate async work with a yield point
219+
return compare a b
220+
})
221+
src1
222+
src2
223+
224+
sign result |> should equal -1
225+
}
226+
227+
[<Fact>]
228+
let ``TaskSeq-compareWithAsync: async comparer works correctly`` () = task {
229+
let src1 = taskSeq {
230+
10
231+
20
232+
30
233+
}
234+
235+
let src2 = taskSeq {
236+
10
237+
20
238+
30
239+
}
240+
241+
let! result = TaskSeq.compareWithAsync (fun a b -> Task.fromResult (compare a b)) src1 src2
242+
result |> should equal 0
243+
}

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,8 @@ type TaskSeq private () =
512512

513513
static member zip source1 source2 = Internal.zip source1 source2
514514
static member zip3 source1 source2 source3 = Internal.zip3 source1 source2 source3
515+
static member compareWith comparer source1 source2 = Internal.compareWith comparer source1 source2
516+
static member compareWithAsync comparer source1 source2 = Internal.compareWithAsync comparer source1 source2
515517
static member fold folder state source = Internal.fold (FolderAction folder) state source
516518
static member foldAsync folder state source = Internal.fold (AsyncFolderAction folder) state source
517519
static member scan folder state source = Internal.scan (FolderAction folder) state source

src/FSharp.Control.TaskSeq/TaskSeq.fsi

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1555,7 +1555,37 @@ type TaskSeq =
15551555
source1: TaskSeq<'T1> -> source2: TaskSeq<'T2> -> source3: TaskSeq<'T3> -> TaskSeq<'T1 * 'T2 * 'T3>
15561556

15571557
/// <summary>
1558-
/// argument of type <typeref name="'State" /> through the computation. If the input function is <paramref name="f" /> and the elements are <paramref name="i0...iN" />
1558+
/// Applies a comparer function to corresponding elements of two task sequences, returning the result of the
1559+
/// first comparison that is non-zero, or zero if all compared elements are equal. The sequences are compared
1560+
/// element by element until one of them is exhausted; if one sequence is shorter than the other, it is considered
1561+
/// less than the longer sequence.
1562+
/// If the comparer function <paramref name="comparer" /> is asynchronous, consider using <see cref="TaskSeq.compareWithAsync" />.
1563+
/// </summary>
1564+
///
1565+
/// <param name="comparer">A function that compares an element from the first sequence with one from the second, returning an integer (negative = less than, zero = equal, positive = greater than).</param>
1566+
/// <param name="source1">The first input task sequence.</param>
1567+
/// <param name="source2">The second input task sequence.</param>
1568+
/// <returns>A task returning the first non-zero comparison result, or zero if all elements compare equal and the sequences have equal length.</returns>
1569+
/// <exception cref="T:ArgumentNullException">Thrown when either input task sequence is null.</exception>
1570+
static member compareWith: comparer: ('T -> 'T -> int) -> source1: TaskSeq<'T> -> source2: TaskSeq<'T> -> Task<int>
1571+
1572+
/// <summary>
1573+
/// Applies an asynchronous comparer function to corresponding elements of two task sequences, returning the result of
1574+
/// the first comparison that is non-zero, or zero if all compared elements are equal. The sequences are compared
1575+
/// element by element until one of them is exhausted; if one sequence is shorter than the other, it is considered
1576+
/// less than the longer sequence.
1577+
/// If the comparer function <paramref name="comparer" /> is synchronous, consider using <see cref="TaskSeq.compareWith" />.
1578+
/// </summary>
1579+
///
1580+
/// <param name="comparer">An asynchronous function that compares an element from the first sequence with one from the second, returning an integer (negative = less than, zero = equal, positive = greater than).</param>
1581+
/// <param name="source1">The first input task sequence.</param>
1582+
/// <param name="source2">The second input task sequence.</param>
1583+
/// <returns>A task returning the first non-zero comparison result, or zero if all elements compare equal and the sequences have equal length.</returns>
1584+
/// <exception cref="T:ArgumentNullException">Thrown when either input task sequence is null.</exception>
1585+
static member compareWithAsync:
1586+
comparer: ('T -> 'T -> #Task<int>) -> source1: TaskSeq<'T> -> source2: TaskSeq<'T> -> Task<int>
1587+
1588+
15591589
/// then computes<paramref name="f (... (f s i0)...) iN" />.
15601590
/// If the accumulator function <paramref name="folder" /> is asynchronous, consider using <see cref="TaskSeq.foldAsync" />.
15611591
/// argument of type <paramref name="'State" /> through the computation. If the input function is <paramref name="f" /> and the elements are <paramref name="i0...iN" />

src/FSharp.Control.TaskSeq/TaskSeqInternal.fs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,68 @@ module internal TaskSeqInternal =
585585
go <- step1 && step2 && step3
586586
}
587587

588+
let compareWith (comparer: 'T -> 'T -> int) (source1: TaskSeq<'T>) (source2: TaskSeq<'T>) =
589+
checkNonNull (nameof source1) source1
590+
checkNonNull (nameof source2) source2
591+
592+
task {
593+
use e1 = source1.GetAsyncEnumerator CancellationToken.None
594+
use e2 = source2.GetAsyncEnumerator CancellationToken.None
595+
let mutable result = 0
596+
let! step1 = e1.MoveNextAsync()
597+
let! step2 = e2.MoveNextAsync()
598+
let mutable has1 = step1
599+
let mutable has2 = step2
600+
601+
while result = 0 && (has1 || has2) do
602+
match has1, has2 with
603+
| false, _ -> result <- -1 // source1 is shorter: less than
604+
| _, false -> result <- 1 // source2 is shorter: greater than
605+
| true, true ->
606+
let cmp = comparer e1.Current e2.Current
607+
608+
if cmp <> 0 then
609+
result <- cmp
610+
else
611+
let! s1 = e1.MoveNextAsync()
612+
let! s2 = e2.MoveNextAsync()
613+
has1 <- s1
614+
has2 <- s2
615+
616+
return result
617+
}
618+
619+
let compareWithAsync (comparer: 'T -> 'T -> #Task<int>) (source1: TaskSeq<'T>) (source2: TaskSeq<'T>) =
620+
checkNonNull (nameof source1) source1
621+
checkNonNull (nameof source2) source2
622+
623+
task {
624+
use e1 = source1.GetAsyncEnumerator CancellationToken.None
625+
use e2 = source2.GetAsyncEnumerator CancellationToken.None
626+
let mutable result = 0
627+
let! step1 = e1.MoveNextAsync()
628+
let! step2 = e2.MoveNextAsync()
629+
let mutable has1 = step1
630+
let mutable has2 = step2
631+
632+
while result = 0 && (has1 || has2) do
633+
match has1, has2 with
634+
| false, _ -> result <- -1 // source1 is shorter: less than
635+
| _, false -> result <- 1 // source2 is shorter: greater than
636+
| true, true ->
637+
let! cmp = comparer e1.Current e2.Current
638+
639+
if cmp <> 0 then
640+
result <- cmp
641+
else
642+
let! s1 = e1.MoveNextAsync()
643+
let! s2 = e2.MoveNextAsync()
644+
has1 <- s1
645+
has2 <- s2
646+
647+
return result
648+
}
649+
588650
let collect (binder: _ -> #IAsyncEnumerable<_>) (source: TaskSeq<_>) =
589651
checkNonNull (nameof source) source
590652

0 commit comments

Comments
 (0)