Skip to content

Commit 2561add

Browse files
[Repo Assist] Add AsyncSeq.findIndex, tryFindIndex, findIndexAsync, tryFindIndexAsync, sortWith (#261)
* Add AsyncSeq.findIndex, tryFindIndex, findIndexAsync, tryFindIndexAsync, sortWith Five new combinators mirroring standard F# module functions: - findIndex: returns 0-based index of first matching element; raises KeyNotFoundException if not found (mirrors Seq.findIndex) - tryFindIndex: returns Some index for first matching element, or None (mirrors Seq.tryFindIndex) - findIndexAsync: async-predicate variant of findIndex; raises KeyNotFoundException if not found - tryFindIndexAsync: async-predicate variant of tryFindIndex; returns option - sortWith: sorts sequence using custom comparison function, returning array (mirrors Seq.sortWith) 12 new tests added; 279 total pass. Co-authored-by: Copilot <[email protected]> * ci: trigger CI checks --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <[email protected]>
1 parent f2f8be5 commit 2561add

4 files changed

Lines changed: 156 additions & 0 deletions

File tree

RELEASE_NOTES.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
### 4.6.0
2+
3+
* Added `AsyncSeq.findIndex` — returns the index of the first element satisfying a predicate; raises `KeyNotFoundException` if no match, mirroring `Seq.findIndex`.
4+
* Added `AsyncSeq.tryFindIndex` — returns the index of the first element satisfying a predicate as `option`, or `None` if not found, mirroring `Seq.tryFindIndex`.
5+
* Added `AsyncSeq.findIndexAsync` — async-predicate variant of `AsyncSeq.findIndex`; raises `KeyNotFoundException` if no match.
6+
* Added `AsyncSeq.tryFindIndexAsync` — async-predicate variant of `AsyncSeq.tryFindIndex`; returns `option`.
7+
* Added `AsyncSeq.sortWith` — sorts the sequence using a custom comparison function, returning an array, mirroring `Seq.sortWith`.
8+
19
### 4.5.0
210

311
* Added `AsyncSeq.last` — returns the last element of the sequence; raises `InvalidOperationException` if empty, mirroring `Seq.last`.

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,46 @@ module AsyncSeq =
12091209
let findAsync f (source : AsyncSeq<'T>) =
12101210
source |> pickAsync (fun v -> async { let! b = f v in return if b then Some v else None })
12111211

1212+
let tryFindIndex f (source : AsyncSeq<'T>) = async {
1213+
use ie = source.GetEnumerator()
1214+
let! first = ie.MoveNext()
1215+
let b = ref first
1216+
let i = ref 0
1217+
while b.Value.IsSome && not (f b.Value.Value) do
1218+
let! next = ie.MoveNext()
1219+
b := next
1220+
i := !i + 1
1221+
return if b.Value.IsSome then Some !i else None }
1222+
1223+
let tryFindIndexAsync f (source : AsyncSeq<'T>) = async {
1224+
use ie = source.GetEnumerator()
1225+
let! first = ie.MoveNext()
1226+
let b = ref first
1227+
let i = ref 0
1228+
let mutable keepGoing = b.Value.IsSome
1229+
while keepGoing do
1230+
let! matches = f b.Value.Value
1231+
if matches then
1232+
keepGoing <- false
1233+
else
1234+
let! next = ie.MoveNext()
1235+
b := next
1236+
if b.Value.IsSome then i := !i + 1
1237+
else keepGoing <- false
1238+
return if b.Value.IsSome then Some !i else None }
1239+
1240+
let findIndex f (source : AsyncSeq<'T>) = async {
1241+
let! result = tryFindIndex f source
1242+
match result with
1243+
| None -> return raise (System.Collections.Generic.KeyNotFoundException("An element satisfying the predicate was not found in the collection."))
1244+
| Some i -> return i }
1245+
1246+
let findIndexAsync f (source : AsyncSeq<'T>) = async {
1247+
let! result = tryFindIndexAsync f source
1248+
match result with
1249+
| None -> return raise (System.Collections.Generic.KeyNotFoundException("An element satisfying the predicate was not found in the collection."))
1250+
| Some i -> return i }
1251+
12121252
let exists f (source : AsyncSeq<'T>) =
12131253
source |> tryFind f |> Async.map Option.isSome
12141254

@@ -1885,6 +1925,9 @@ module AsyncSeq =
18851925

18861926
let sortByDescending (projection:'T -> 'Key) (source:AsyncSeq<'T>) : array<'T> when 'Key : comparison =
18871927
toSortedSeq (Array.sortByDescending projection) source
1928+
1929+
let sortWith (comparer:'T -> 'T -> int) (source:AsyncSeq<'T>) : array<'T> =
1930+
toSortedSeq (Array.sortWith comparer) source
18881931
#endif
18891932

18901933
#if !FABLE_COMPILER

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,22 @@ module AsyncSeq =
343343
/// Raises KeyNotFoundException if no matching element is found.
344344
val findAsync : predicate:('T -> Async<bool>) -> source:AsyncSeq<'T> -> Async<'T>
345345

346+
/// Asynchronously find the index of the first value in a sequence for which the predicate returns true.
347+
/// Returns None if no matching element is found.
348+
val tryFindIndex : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<int option>
349+
350+
/// Asynchronously find the index of the first value in a sequence for which the async predicate returns true.
351+
/// Returns None if no matching element is found.
352+
val tryFindIndexAsync : predicate:('T -> Async<bool>) -> source:AsyncSeq<'T> -> Async<int option>
353+
354+
/// Asynchronously find the index of the first value in a sequence for which the predicate returns true.
355+
/// Raises KeyNotFoundException if no matching element is found.
356+
val findIndex : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<int>
357+
358+
/// Asynchronously find the index of the first value in a sequence for which the async predicate returns true.
359+
/// Raises KeyNotFoundException if no matching element is found.
360+
val findIndexAsync : predicate:('T -> Async<bool>) -> source:AsyncSeq<'T> -> Async<int>
361+
346362
/// Asynchronously determine if there is a value in the sequence for which the predicate returns true
347363
val exists : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<bool>
348364

@@ -602,6 +618,12 @@ module AsyncSeq =
602618
/// that sequence is iterated. As a result this function should not be used with
603619
/// large or infinite sequences.
604620
val sortByDescending : projection:('T -> 'Key) -> source:AsyncSeq<'T> -> array<'T> when 'Key : comparison
621+
622+
/// Sorts the given async sequence using the given comparison function and returns an array.
623+
/// This function returns an array that digests the whole initial sequence as soon as
624+
/// that sequence is iterated. As a result this function should not be used with
625+
/// large or infinite sequences.
626+
val sortWith : comparer:('T -> 'T -> int) -> source:AsyncSeq<'T> -> array<'T>
605627
#endif
606628

607629
/// Interleaves two async sequences of the same type into a resulting sequence. The provided

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

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3274,3 +3274,86 @@ let ``AsyncSeq.tryItem returns None for negative index`` () =
32743274
let ``AsyncSeq.tryItem returns None on empty sequence`` () =
32753275
let result = AsyncSeq.tryItem 0 AsyncSeq.empty<int> |> Async.RunSynchronously
32763276
Assert.AreEqual(None, result)
3277+
3278+
// ===== findIndex / tryFindIndex / findIndexAsync / tryFindIndexAsync =====
3279+
3280+
[<Test>]
3281+
let ``AsyncSeq.findIndex returns index of first matching element`` () =
3282+
for i in 0 .. 9 do
3283+
let ls = [ 1 .. 10 ]
3284+
let result = AsyncSeq.ofSeq ls |> AsyncSeq.findIndex (fun x -> x = i + 1) |> Async.RunSynchronously
3285+
Assert.AreEqual(i, result)
3286+
3287+
[<Test>]
3288+
let ``AsyncSeq.findIndex raises KeyNotFoundException when no match`` () =
3289+
Assert.Throws<System.Collections.Generic.KeyNotFoundException>(fun () ->
3290+
AsyncSeq.ofSeq [ 1; 2; 3 ] |> AsyncSeq.findIndex (fun x -> x = 99) |> Async.RunSynchronously |> ignore)
3291+
|> ignore
3292+
3293+
[<Test>]
3294+
let ``AsyncSeq.tryFindIndex returns Some index when found`` () =
3295+
let ls = [ 10; 20; 30; 40 ]
3296+
let result = AsyncSeq.ofSeq ls |> AsyncSeq.tryFindIndex (fun x -> x = 30) |> Async.RunSynchronously
3297+
Assert.AreEqual(Some 2, result)
3298+
3299+
[<Test>]
3300+
let ``AsyncSeq.tryFindIndex returns None when not found`` () =
3301+
let result = AsyncSeq.ofSeq [ 1; 2; 3 ] |> AsyncSeq.tryFindIndex (fun x -> x = 99) |> Async.RunSynchronously
3302+
Assert.AreEqual(None, result)
3303+
3304+
[<Test>]
3305+
let ``AsyncSeq.tryFindIndex returns Some 0 for first element`` () =
3306+
let result = AsyncSeq.ofSeq [ 5; 6; 7 ] |> AsyncSeq.tryFindIndex (fun x -> x = 5) |> Async.RunSynchronously
3307+
Assert.AreEqual(Some 0, result)
3308+
3309+
[<Test>]
3310+
let ``AsyncSeq.findIndexAsync returns index of first matching element`` () =
3311+
let ls = [ 1; 2; 3; 4; 5 ]
3312+
let result =
3313+
AsyncSeq.ofSeq ls
3314+
|> AsyncSeq.findIndexAsync (fun x -> async { return x = 3 })
3315+
|> Async.RunSynchronously
3316+
Assert.AreEqual(2, result)
3317+
3318+
[<Test>]
3319+
let ``AsyncSeq.findIndexAsync raises KeyNotFoundException when no match`` () =
3320+
Assert.Throws<System.Collections.Generic.KeyNotFoundException>(fun () ->
3321+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3322+
|> AsyncSeq.findIndexAsync (fun x -> async { return x = 99 })
3323+
|> Async.RunSynchronously |> ignore)
3324+
|> ignore
3325+
3326+
[<Test>]
3327+
let ``AsyncSeq.tryFindIndexAsync returns Some index when found`` () =
3328+
let result =
3329+
AsyncSeq.ofSeq [ 10; 20; 30 ]
3330+
|> AsyncSeq.tryFindIndexAsync (fun x -> async { return x = 20 })
3331+
|> Async.RunSynchronously
3332+
Assert.AreEqual(Some 1, result)
3333+
3334+
[<Test>]
3335+
let ``AsyncSeq.tryFindIndexAsync returns None when not found`` () =
3336+
let result =
3337+
AsyncSeq.ofSeq [ 1; 2; 3 ]
3338+
|> AsyncSeq.tryFindIndexAsync (fun x -> async { return x = 99 })
3339+
|> Async.RunSynchronously
3340+
Assert.AreEqual(None, result)
3341+
3342+
// ===== sortWith =====
3343+
3344+
[<Test>]
3345+
let ``AsyncSeq.sortWith sorts using custom comparer`` () =
3346+
let source = asyncSeq { yield 3; yield 1; yield 4; yield 1; yield 5 }
3347+
let result = AsyncSeq.sortWith compare source
3348+
Assert.AreEqual([| 1; 1; 3; 4; 5 |], result)
3349+
3350+
[<Test>]
3351+
let ``AsyncSeq.sortWith sorts descending with negated comparer`` () =
3352+
let source = asyncSeq { yield 3; yield 1; yield 4; yield 1; yield 5 }
3353+
let result = AsyncSeq.sortWith (fun a b -> compare b a) source
3354+
Assert.AreEqual([| 5; 4; 3; 1; 1 |], result)
3355+
3356+
[<Test>]
3357+
let ``AsyncSeq.sortWith returns empty array for empty sequence`` () =
3358+
let result = AsyncSeq.sortWith compare AsyncSeq.empty<int>
3359+
Assert.AreEqual([||], result)

0 commit comments

Comments
 (0)