Skip to content

Commit 6f32989

Browse files
authored
Merge pull request #346 from fsprojects/repo-assist/fix-issue-345-distinctUntilChangedWith-8aef3b6a9525a64a
[Repo Assist] feat: add TaskSeq.distinctUntilChangedWith and distinctUntilChangedWithAsync
2 parents 8f3a22e + 16890ae commit 6f32989

File tree

5 files changed

+260
-1
lines changed

5 files changed

+260
-1
lines changed

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
1.0.0
5+
- adds TaskSeq.distinctUntilChangedWith and TaskSeq.distinctUntilChangedWithAsync, #345
56
- adds TaskSeq.withCancellation, #167
67
- adds docs/ with fsdocs-based documentation site covering generating, transforming, consuming, combining and advanced operations
78

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+
}

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,8 @@ type TaskSeq private () =
476476
static member distinctByAsync projection source = Internal.distinctByAsync projection source
477477

478478
static member distinctUntilChanged source = Internal.distinctUntilChanged source
479+
static member distinctUntilChangedWith comparer source = Internal.distinctUntilChangedWith comparer source
480+
static member distinctUntilChangedWithAsync comparer source = Internal.distinctUntilChangedWithAsync comparer source
479481
static member pairwise source = Internal.pairwise source
480482
static member chunkBySize chunkSize source = Internal.chunkBySize chunkSize source
481483
static member windowed windowSize source = Internal.windowed windowSize source

src/FSharp.Control.TaskSeq/TaskSeq.fsi

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1503,6 +1503,33 @@ type TaskSeq =
15031503
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequences is null.</exception>
15041504
static member distinctUntilChanged<'T when 'T: equality> : source: TaskSeq<'T> -> TaskSeq<'T>
15051505

1506+
/// <summary>
1507+
/// Returns a new task sequence without consecutive duplicate elements, using the supplied <paramref name="comparer" />
1508+
/// to determine equality of consecutive elements. The comparer returns <see langword="true" /> if two elements are
1509+
/// considered equal (and thus the second should be skipped).
1510+
/// </summary>
1511+
///
1512+
/// <param name="comparer">A function that returns <see langword="true" /> if two consecutive elements are equal.</param>
1513+
/// <param name="source">The input task sequence whose consecutive duplicates will be removed.</param>
1514+
/// <returns>A sequence without consecutive duplicate elements.</returns>
1515+
///
1516+
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
1517+
static member distinctUntilChangedWith: comparer: ('T -> 'T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T>
1518+
1519+
/// <summary>
1520+
/// Returns a new task sequence without consecutive duplicate elements, using the supplied async <paramref name="comparer" />
1521+
/// to determine equality of consecutive elements. The comparer returns <see langword="true" /> if two elements are
1522+
/// considered equal (and thus the second should be skipped).
1523+
/// </summary>
1524+
///
1525+
/// <param name="comparer">An async function that returns <see langword="true" /> if two consecutive elements are equal.</param>
1526+
/// <param name="source">The input task sequence whose consecutive duplicates will be removed.</param>
1527+
/// <returns>A sequence without consecutive duplicate elements.</returns>
1528+
///
1529+
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
1530+
static member distinctUntilChangedWithAsync:
1531+
comparer: ('T -> 'T -> #Task<bool>) -> source: TaskSeq<'T> -> TaskSeq<'T>
1532+
15061533
/// <summary>
15071534
/// Returns a task sequence of each element in the source paired with its successor.
15081535
/// The sequence is empty if the source has fewer than two elements.

src/FSharp.Control.TaskSeq/TaskSeqInternal.fs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,6 +1367,46 @@ module internal TaskSeqInternal =
13671367
maybePrevious <- ValueSome current
13681368
}
13691369

1370+
let distinctUntilChangedWith (comparer: 'T -> 'T -> bool) (source: TaskSeq<_>) =
1371+
checkNonNull (nameof source) source
1372+
1373+
taskSeq {
1374+
let mutable maybePrevious = ValueNone
1375+
1376+
for current in source do
1377+
match maybePrevious with
1378+
| ValueNone ->
1379+
yield current
1380+
maybePrevious <- ValueSome current
1381+
| ValueSome previous ->
1382+
if comparer previous current then
1383+
() // skip
1384+
else
1385+
yield current
1386+
maybePrevious <- ValueSome current
1387+
}
1388+
1389+
let distinctUntilChangedWithAsync (comparer: 'T -> 'T -> #Task<bool>) (source: TaskSeq<_>) =
1390+
checkNonNull (nameof source) source
1391+
1392+
taskSeq {
1393+
let mutable maybePrevious = ValueNone
1394+
1395+
for current in source do
1396+
match maybePrevious with
1397+
| ValueNone ->
1398+
yield current
1399+
maybePrevious <- ValueSome current
1400+
| ValueSome previous ->
1401+
let! areEqual = comparer previous current
1402+
1403+
if areEqual then
1404+
() // skip
1405+
else
1406+
yield current
1407+
maybePrevious <- ValueSome current
1408+
}
1409+
13701410
let pairwise (source: TaskSeq<_>) =
13711411
checkNonNull (nameof source) source
13721412

0 commit comments

Comments
 (0)