Skip to content

Commit 441f1e6

Browse files
github-actions[bot]Repo AssistCopilot
authored
[Repo Assist] Add AsyncSeq.distinct, distinctBy, distinctByAsync, countBy, countByAsync, exactlyOne, tryExactlyOne (#249)
* Add AsyncSeq.distinct, distinctBy, distinctByAsync, countBy, countByAsync, exactlyOne, tryExactlyOne Seven new combinators: - distinct: removes all duplicates (unlike distinctUntilChanged which only removes consecutive ones) - distinctBy / distinctByAsync: distinct by key projection (sync and async variants) - countBy / countByAsync: count elements grouped by key (sync and async variants) - exactlyOne: returns the single element or raises InvalidOperationException - tryExactlyOne: returns Some if exactly one element, None otherwise 14 tests added, all 234 tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger CI checks --------- Co-authored-by: Repo Assist <repo-assist@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 842dd3c commit 441f1e6

File tree

3 files changed

+182
-0
lines changed

3 files changed

+182
-0
lines changed

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,28 @@ module AsyncSeq =
10601060
| None -> return def
10611061
| Some v -> return v }
10621062

1063+
let exactlyOne (source : AsyncSeq<'T>) = async {
1064+
use ie = source.GetEnumerator()
1065+
let! first = ie.MoveNext()
1066+
match first with
1067+
| None -> return raise (System.InvalidOperationException("The input sequence was empty."))
1068+
| Some v ->
1069+
let! second = ie.MoveNext()
1070+
match second with
1071+
| None -> return v
1072+
| Some _ -> return raise (System.InvalidOperationException("The input sequence contains more than one element.")) }
1073+
1074+
let tryExactlyOne (source : AsyncSeq<'T>) = async {
1075+
use ie = source.GetEnumerator()
1076+
let! first = ie.MoveNext()
1077+
match first with
1078+
| None -> return None
1079+
| Some v ->
1080+
let! second = ie.MoveNext()
1081+
match second with
1082+
| None -> return Some v
1083+
| Some _ -> return None }
1084+
10631085
let scanAsync f (state:'TState) (source : AsyncSeq<'T>) = asyncSeq {
10641086
yield state
10651087
let z = ref state
@@ -1252,6 +1274,22 @@ module AsyncSeq =
12521274
return LanguagePrimitives.DivideByInt sum count
12531275
}
12541276

1277+
let countByAsync (projection : 'T -> Async<'Key>) (source : AsyncSeq<'T>) : Async<('Key * int) array> =
1278+
async {
1279+
let! dict =
1280+
source |> foldAsync (fun (d : System.Collections.Generic.Dictionary<'Key,int>) v ->
1281+
async {
1282+
let! k = projection v
1283+
let mutable cnt = 0
1284+
if d.TryGetValue(k, &cnt) then d.[k] <- cnt + 1
1285+
else d.[k] <- 1
1286+
return d
1287+
}) (System.Collections.Generic.Dictionary<'Key,int>())
1288+
return dict |> Seq.map (fun kv -> kv.Key, kv.Value) |> Seq.toArray }
1289+
1290+
let countBy (projection : 'T -> 'Key) (source : AsyncSeq<'T>) : Async<('Key * int) array> =
1291+
countByAsync (projection >> async.Return) source
1292+
12551293
let scan f (state:'State) (source : AsyncSeq<'T>) =
12561294
scanAsync (fun st v -> f st v |> async.Return) state source
12571295

@@ -1997,6 +2035,19 @@ module AsyncSeq =
19972035
let distinctUntilChanged (s:AsyncSeq<'T>) : AsyncSeq<'T> =
19982036
distinctUntilChangedWith ((=)) s
19992037

2038+
let distinctByAsync (projection : 'T -> Async<'Key>) (source : AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq {
2039+
let seen = System.Collections.Generic.HashSet<'Key>()
2040+
for v in source do
2041+
let! k = projection v
2042+
if seen.Add(k) then
2043+
yield v }
2044+
2045+
let distinctBy (projection : 'T -> 'Key) (source : AsyncSeq<'T>) : AsyncSeq<'T> =
2046+
distinctByAsync (projection >> async.Return) source
2047+
2048+
let distinct (source : AsyncSeq<'T>) : AsyncSeq<'T> =
2049+
distinctBy id source
2050+
20002051
let getIterator (s:AsyncSeq<'T>) : (unit -> Async<'T option>) =
20012052
let curr = s.GetEnumerator()
20022053
fun () -> curr.MoveNext()

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,14 @@ module AsyncSeq =
163163
/// given asynchronous sequence (or None if the sequence is empty).
164164
val tryFirst : source:AsyncSeq<'T> -> Async<'T option>
165165

166+
/// Asynchronously returns the only element of the asynchronous sequence.
167+
/// Raises InvalidOperationException if the sequence is empty or contains more than one element.
168+
val exactlyOne : source:AsyncSeq<'T> -> Async<'T>
169+
170+
/// Asynchronously returns the only element of the asynchronous sequence, or None if the
171+
/// sequence is empty or contains more than one element.
172+
val tryExactlyOne : source:AsyncSeq<'T> -> Async<'T option>
173+
166174
/// Aggregates the elements of the input asynchronous sequence using the
167175
/// specified 'aggregation' function. The result is an asynchronous
168176
/// sequence of intermediate aggregation result.
@@ -276,6 +284,14 @@ module AsyncSeq =
276284
and ^U : (static member DivideByInt : ^U * int -> ^U)
277285
and ^U : (static member Zero : ^U)
278286

287+
/// Asynchronously count the elements of the input asynchronous sequence grouped by the result of the given asynchronous key projection.
288+
/// Returns an array of (key, count) pairs.
289+
val countByAsync : projection:('T -> Async<'Key>) -> source:AsyncSeq<'T> -> Async<('Key * int) array> when 'Key : equality
290+
291+
/// Asynchronously count the elements of the input asynchronous sequence grouped by the result of the given key projection.
292+
/// Returns an array of (key, count) pairs.
293+
val countBy : projection:('T -> 'Key) -> source:AsyncSeq<'T> -> Async<('Key * int) array> when 'Key : equality
294+
279295
/// Asynchronously determine if the sequence contains the given value
280296
val contains : value:'T -> source:AsyncSeq<'T> -> Async<bool> when 'T : equality
281297

@@ -594,6 +610,18 @@ module AsyncSeq =
594610
/// Returns an async sequence which contains no contiguous duplicate elements.
595611
val distinctUntilChanged : source:AsyncSeq<'T> -> AsyncSeq<'T> when 'T : equality
596612

613+
/// Returns an async sequence containing only distinct elements, determined by the given asynchronous key projection.
614+
/// Elements are compared using structural equality on the projected key.
615+
val distinctByAsync : projection:('T -> Async<'Key>) -> source:AsyncSeq<'T> -> AsyncSeq<'T> when 'Key : equality
616+
617+
/// Returns an async sequence containing only distinct elements, determined by the given key projection.
618+
/// Elements are compared using structural equality on the projected key.
619+
val distinctBy : projection:('T -> 'Key) -> source:AsyncSeq<'T> -> AsyncSeq<'T> when 'Key : equality
620+
621+
/// Returns an async sequence containing only distinct elements.
622+
/// Elements are compared using structural equality.
623+
val distinct : source:AsyncSeq<'T> -> AsyncSeq<'T> when 'T : equality
624+
597625
#if FABLE_COMPILER
598626
[<System.Obsolete("Use .GetEnumerator directly") >]
599627
#endif

tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2907,3 +2907,106 @@ let ``AsyncSeq.chunkByAsync groups consecutive equal keys`` () =
29072907
} |> Async.RunSynchronously
29082908

29092909

2910+
2911+
// ===== distinct / distinctBy / distinctByAsync =====
2912+
2913+
[<Test>]
2914+
let ``AsyncSeq.distinct removes all duplicates`` () =
2915+
let source = asyncSeq { yield 1; yield 2; yield 1; yield 3; yield 2 }
2916+
let result = AsyncSeq.distinct source |> AsyncSeq.toListSynchronously
2917+
Assert.AreEqual([1; 2; 3], result)
2918+
2919+
[<Test>]
2920+
let ``AsyncSeq.distinct empty sequence returns empty`` () =
2921+
let result = AsyncSeq.distinct AsyncSeq.empty<int> |> AsyncSeq.toListSynchronously
2922+
Assert.AreEqual([], result)
2923+
2924+
[<Test>]
2925+
let ``AsyncSeq.distinct all unique elements returns all`` () =
2926+
let source = asyncSeq { yield 1; yield 2; yield 3 }
2927+
let result = AsyncSeq.distinct source |> AsyncSeq.toListSynchronously
2928+
Assert.AreEqual([1; 2; 3], result)
2929+
2930+
[<Test>]
2931+
let ``AsyncSeq.distinctBy removes duplicates by key`` () =
2932+
let source = asyncSeq { yield (1, "a"); yield (2, "b"); yield (1, "c") }
2933+
let result = AsyncSeq.distinctBy fst source |> AsyncSeq.toListSynchronously
2934+
Assert.AreEqual([(1, "a"); (2, "b")], result)
2935+
2936+
[<Test>]
2937+
let ``AsyncSeq.distinctByAsync removes duplicates by async key`` () =
2938+
async {
2939+
let source = asyncSeq { yield 1; yield 2; yield 1; yield 3 }
2940+
let result = AsyncSeq.distinctByAsync (fun x -> async { return x % 2 }) source |> AsyncSeq.toListSynchronously
2941+
Assert.AreEqual([1; 2], result)
2942+
} |> Async.RunSynchronously
2943+
2944+
// ===== countBy / countByAsync =====
2945+
2946+
[<Test>]
2947+
let ``AsyncSeq.countBy counts elements by key`` () =
2948+
async {
2949+
let source = asyncSeq { yield 1; yield 2; yield 1; yield 3; yield 2; yield 2 }
2950+
let result = AsyncSeq.countBy id source |> Async.RunSynchronously
2951+
let sorted = result |> Array.sortBy fst
2952+
Assert.AreEqual([| (1, 2); (2, 3); (3, 1) |], sorted)
2953+
} |> Async.RunSynchronously
2954+
2955+
[<Test>]
2956+
let ``AsyncSeq.countBy empty sequence returns empty array`` () =
2957+
async {
2958+
let result = AsyncSeq.countBy id AsyncSeq.empty<int> |> Async.RunSynchronously
2959+
Assert.AreEqual([||], result)
2960+
} |> Async.RunSynchronously
2961+
2962+
[<Test>]
2963+
let ``AsyncSeq.countByAsync counts elements by async key`` () =
2964+
async {
2965+
let source = asyncSeq { yield 1; yield 2; yield 3; yield 4 }
2966+
let result = AsyncSeq.countByAsync (fun x -> async { return x % 2 }) source |> Async.RunSynchronously
2967+
let sorted = result |> Array.sortBy fst
2968+
Assert.AreEqual([| (0, 2); (1, 2) |], sorted)
2969+
} |> Async.RunSynchronously
2970+
2971+
// ===== exactlyOne / tryExactlyOne =====
2972+
2973+
[<Test>]
2974+
let ``AsyncSeq.exactlyOne returns single element`` () =
2975+
async {
2976+
let source = asyncSeq { yield 42 }
2977+
let result = AsyncSeq.exactlyOne source |> Async.RunSynchronously
2978+
Assert.AreEqual(42, result)
2979+
} |> Async.RunSynchronously
2980+
2981+
[<Test>]
2982+
let ``AsyncSeq.exactlyOne raises on empty sequence`` () =
2983+
Assert.Throws<InvalidOperationException>(fun () ->
2984+
AsyncSeq.exactlyOne AsyncSeq.empty<int> |> Async.RunSynchronously |> ignore) |> ignore
2985+
2986+
[<Test>]
2987+
let ``AsyncSeq.exactlyOne raises on sequence with more than one element`` () =
2988+
Assert.Throws<InvalidOperationException>(fun () ->
2989+
asyncSeq { yield 1; yield 2 } |> AsyncSeq.exactlyOne |> Async.RunSynchronously |> ignore) |> ignore
2990+
2991+
[<Test>]
2992+
let ``AsyncSeq.tryExactlyOne returns Some for single element`` () =
2993+
async {
2994+
let source = asyncSeq { yield 42 }
2995+
let result = AsyncSeq.tryExactlyOne source |> Async.RunSynchronously
2996+
Assert.AreEqual(Some 42, result)
2997+
} |> Async.RunSynchronously
2998+
2999+
[<Test>]
3000+
let ``AsyncSeq.tryExactlyOne returns None for empty sequence`` () =
3001+
async {
3002+
let result = AsyncSeq.tryExactlyOne AsyncSeq.empty<int> |> Async.RunSynchronously
3003+
Assert.AreEqual(None, result)
3004+
} |> Async.RunSynchronously
3005+
3006+
[<Test>]
3007+
let ``AsyncSeq.tryExactlyOne returns None for sequence with more than one element`` () =
3008+
async {
3009+
let source = asyncSeq { yield 1; yield 2 }
3010+
let result = AsyncSeq.tryExactlyOne source |> Async.RunSynchronously
3011+
Assert.AreEqual(None, result)
3012+
} |> Async.RunSynchronously

0 commit comments

Comments
 (0)