diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 7290415..55e0b31 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,7 @@ * Performance: Optimised `AsyncSeq.pairwise` to use a `hasPrev` flag and a direct `mutable` field instead of wrapping the previous element in `Some`. Previously, each iteration allocated a new `'T option` object on the heap; the new implementation eliminates that allocation entirely, reducing GC pressure for long sequences. * Bug fix: `AsyncSeq.splitAt` and `AsyncSeq.tryTail` now correctly dispose the underlying enumerator when an exception or cancellation occurs during the initial `MoveNext` call. Previously the enumerator could leak if the source sequence threw during the first few steps. +* Added `AsyncSeq.tryFindIndexBack` and `AsyncSeq.findIndexBack` — return the index of the last element satisfying a predicate. Mirrors `Array.tryFindIndexBack` / `Array.findIndexBack`. Async-predicate variants `tryFindIndexBackAsync` and `findIndexBackAsync` are also included. ### 4.16.0 diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index ddc5e71..706b026 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1460,6 +1460,45 @@ module AsyncSeq = | None -> return raise (System.Collections.Generic.KeyNotFoundException("An element satisfying the predicate was not found in the collection.")) | Some i -> return i } + let tryFindIndexBack f (source : AsyncSeq<'T>) = async { + use ie = source.GetEnumerator() + let! v = ie.MoveNext() + let mutable b = v + let mutable i = 0 + let mutable res = None + while b.IsSome do + if f b.Value then res <- Some i + let! next = ie.MoveNext() + b <- next + i <- i + 1 + return res } + + let tryFindIndexBackAsync f (source : AsyncSeq<'T>) = async { + use ie = source.GetEnumerator() + let! v = ie.MoveNext() + let mutable b = v + let mutable i = 0 + let mutable res = None + while b.IsSome do + let! matches = f b.Value + if matches then res <- Some i + let! next = ie.MoveNext() + b <- next + i <- i + 1 + return res } + + let findIndexBack f (source : AsyncSeq<'T>) = async { + let! result = tryFindIndexBack f source + match result with + | None -> return raise (System.Collections.Generic.KeyNotFoundException("An element satisfying the predicate was not found in the collection.")) + | Some i -> return i } + + let findIndexBackAsync f (source : AsyncSeq<'T>) = async { + let! result = tryFindIndexBackAsync f source + match result with + | None -> return raise (System.Collections.Generic.KeyNotFoundException("An element satisfying the predicate was not found in the collection.")) + | Some i -> return i } + let exists f (source : AsyncSeq<'T>) = source |> tryFind f |> Async.map Option.isSome diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index d8ffec4..d8b5b5f 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -414,6 +414,22 @@ module AsyncSeq = /// Raises KeyNotFoundException if no matching element is found. val findIndexAsync : predicate:('T -> Async) -> source:AsyncSeq<'T> -> Async + /// Asynchronously find the index of the last value in a sequence for which the predicate returns true. + /// Returns None if no matching element is found. + val tryFindIndexBack : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async + + /// Asynchronously find the index of the last value in a sequence for which the async predicate returns true. + /// Returns None if no matching element is found. + val tryFindIndexBackAsync : predicate:('T -> Async) -> source:AsyncSeq<'T> -> Async + + /// Asynchronously find the index of the last value in a sequence for which the predicate returns true. + /// Raises KeyNotFoundException if no matching element is found. + val findIndexBack : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async + + /// Asynchronously find the index of the last value in a sequence for which the async predicate returns true. + /// Raises KeyNotFoundException if no matching element is found. + val findIndexBackAsync : predicate:('T -> Async) -> source:AsyncSeq<'T> -> Async + /// Asynchronously determine if there is a value in the sequence for which the predicate returns true val exists : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index 24748af..5cbbd89 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3491,6 +3491,71 @@ let ``AsyncSeq.tryFindIndexAsync returns None when not found`` () = |> Async.RunSynchronously Assert.AreEqual(None, result) +// ===== tryFindIndexBack / findIndexBack / tryFindIndexBackAsync / findIndexBackAsync ===== + +[] +let ``AsyncSeq.tryFindIndexBack returns index of last matching element`` () = + let result = AsyncSeq.ofSeq [ 1; 2; 3; 2; 1 ] |> AsyncSeq.tryFindIndexBack (fun x -> x = 2) |> Async.RunSynchronously + Assert.AreEqual(Some 3, result) + +[] +let ``AsyncSeq.tryFindIndexBack returns None when no match`` () = + let result = AsyncSeq.ofSeq [ 1; 2; 3 ] |> AsyncSeq.tryFindIndexBack (fun x -> x = 99) |> Async.RunSynchronously + Assert.AreEqual(None, result) + +[] +let ``AsyncSeq.tryFindIndexBack returns None for empty sequence`` () = + let result = AsyncSeq.empty |> AsyncSeq.tryFindIndexBack (fun _ -> true) |> Async.RunSynchronously + Assert.AreEqual(None, result) + +[] +let ``AsyncSeq.tryFindIndexBack returns index of last element when all match`` () = + let result = AsyncSeq.ofSeq [ 1; 2; 3 ] |> AsyncSeq.tryFindIndexBack (fun _ -> true) |> Async.RunSynchronously + Assert.AreEqual(Some 2, result) + +[] +let ``AsyncSeq.findIndexBack returns index of last matching element`` () = + let result = AsyncSeq.ofSeq [ 10; 20; 30; 20; 10 ] |> AsyncSeq.findIndexBack (fun x -> x = 20) |> Async.RunSynchronously + Assert.AreEqual(3, result) + +[] +let ``AsyncSeq.findIndexBack raises KeyNotFoundException when no match`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [ 1; 2; 3 ] |> AsyncSeq.findIndexBack (fun x -> x = 99) |> Async.RunSynchronously |> ignore) + |> ignore + +[] +let ``AsyncSeq.tryFindIndexBackAsync returns index of last matching element`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3; 2; 1 ] + |> AsyncSeq.tryFindIndexBackAsync (fun x -> async { return x = 2 }) + |> Async.RunSynchronously + Assert.AreEqual(Some 3, result) + +[] +let ``AsyncSeq.tryFindIndexBackAsync returns None when no match`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.tryFindIndexBackAsync (fun x -> async { return x = 99 }) + |> Async.RunSynchronously + Assert.AreEqual(None, result) + +[] +let ``AsyncSeq.findIndexBackAsync returns index of last matching element`` () = + let result = + AsyncSeq.ofSeq [ 5; 4; 3; 4; 5 ] + |> AsyncSeq.findIndexBackAsync (fun x -> async { return x = 4 }) + |> Async.RunSynchronously + Assert.AreEqual(3, result) + +[] +let ``AsyncSeq.findIndexBackAsync raises KeyNotFoundException when no match`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.findIndexBackAsync (fun x -> async { return x = 99 }) + |> Async.RunSynchronously |> ignore) + |> ignore + // ===== tryFindBack / findBack / tryFindBackAsync / findBackAsync ===== []