diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 184a300..1f42477 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,7 @@ ### 4.11.0 +* Added `AsyncSeq.tryFindBack` / `AsyncSeq.tryFindBackAsync` — return the last element in the sequence satisfying a (sync or async) predicate, or `None` if no such element exists. The entire sequence is consumed. +* Added `AsyncSeq.findBack` / `AsyncSeq.findBackAsync` — like `tryFindBack`/`tryFindBackAsync` but raise `KeyNotFoundException` when no matching element is found. Mirrors `Array.findBack` and `List.findBack`. * Performance: `mapiAsync` — replaced `asyncSeq`-builder + `collect` implementation with a direct optimised enumerator (`OptimizedMapiAsyncEnumerator`), eliminating `collect` overhead and bringing per-element cost in line with `mapAsync`. Benchmarks added in `AsyncSeqMapiBenchmarks`. * Design parity with FSharp.Control.TaskSeq (#277, batch 2): * Added `AsyncSeq.tryTail` — returns `None` if the sequence is empty; otherwise returns `Some` of the tail. Safe counterpart to `tail`. Mirrors `TaskSeq.tryTail`. diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index 07d339f..5cc60e1 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1337,6 +1337,30 @@ module AsyncSeq = let findAsync f (source : AsyncSeq<'T>) = source |> pickAsync (fun v -> async { let! b = f v in return if b then Some v else None }) + let tryFindBackAsync (predicate: 'T -> Async) (source: AsyncSeq<'T>) : Async<'T option> = async { + use ie = source.GetEnumerator() + let! move = ie.MoveNext() + let mutable b = move + let mutable result = None + while b.IsSome do + let! ok = predicate b.Value + if ok then result <- b + let! next = ie.MoveNext() + b <- next + return result } + + let tryFindBack (predicate: 'T -> bool) (source: AsyncSeq<'T>) : Async<'T option> = + tryFindBackAsync (predicate >> async.Return) source + + let findBackAsync (predicate: 'T -> Async) (source: AsyncSeq<'T>) : Async<'T> = async { + let! result = tryFindBackAsync predicate source + match result with + | None -> return raise (System.Collections.Generic.KeyNotFoundException("An element satisfying the predicate was not found in the collection.")) + | Some v -> return v } + + let findBack (predicate: 'T -> bool) (source: AsyncSeq<'T>) : Async<'T> = + findBackAsync (predicate >> async.Return) source + let tryFindIndex f (source : AsyncSeq<'T>) = async { use ie = source.GetEnumerator() let! first = ie.MoveNext() diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index c3c266f..95308c6 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -377,6 +377,22 @@ module AsyncSeq = /// Raises KeyNotFoundException if no matching element is found. val findAsync : predicate:('T -> Async) -> source:AsyncSeq<'T> -> Async<'T> + /// Returns the last element in the sequence for which the given async predicate returns true, + /// or None if no such element exists. The entire sequence is consumed. + val tryFindBackAsync : predicate:('T -> Async) -> source:AsyncSeq<'T> -> Async<'T option> + + /// Returns the last element in the sequence for which the given predicate returns true, + /// or None if no such element exists. The entire sequence is consumed. + val tryFindBack : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<'T option> + + /// Returns the last element in the sequence for which the given async predicate returns true. + /// Raises KeyNotFoundException if no such element exists. + val findBackAsync : predicate:('T -> Async) -> source:AsyncSeq<'T> -> Async<'T> + + /// Returns the last element in the sequence for which the given predicate returns true. + /// Raises KeyNotFoundException if no such element exists. + val findBack : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async<'T> + /// Asynchronously find the index of the first value in a sequence for which the predicate returns true. /// Returns None if no matching element is found. val tryFindIndex : 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 42e8d6b..9947964 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -4179,3 +4179,87 @@ let ``AsyncSeq.partitionAsync splits by async predicate`` () = |> Async.RunSynchronously Assert.AreEqual([|2;4;6|], trues) Assert.AreEqual([|1;3;5|], falses) + +// ===== tryFindBack / findBack / tryFindBackAsync / findBackAsync ===== + +[] +let ``AsyncSeq.tryFindBack returns last matching element`` () = + let result = + AsyncSeq.ofSeq [1; 3; 5; 2; 4; 6; 7] + |> AsyncSeq.tryFindBack (fun x -> x % 2 = 0) + |> Async.RunSynchronously + Assert.AreEqual(Some 6, result) + +[] +let ``AsyncSeq.tryFindBack returns None when no element matches`` () = + let result = + AsyncSeq.ofSeq [1; 3; 5] + |> AsyncSeq.tryFindBack (fun x -> x % 2 = 0) + |> Async.RunSynchronously + Assert.AreEqual(None, result) + +[] +let ``AsyncSeq.tryFindBack returns None for empty sequence`` () = + let result = + AsyncSeq.empty + |> AsyncSeq.tryFindBack (fun _ -> true) + |> Async.RunSynchronously + Assert.AreEqual(None, result) + +[] +let ``AsyncSeq.tryFindBack returns last element when all match`` () = + let result = + AsyncSeq.ofSeq [10; 20; 30] + |> AsyncSeq.tryFindBack (fun _ -> true) + |> Async.RunSynchronously + Assert.AreEqual(Some 30, result) + +[] +let ``AsyncSeq.findBack returns last matching element`` () = + let result = + AsyncSeq.ofSeq [2; 4; 1; 3; 6; 5] + |> AsyncSeq.findBack (fun x -> x % 2 = 0) + |> Async.RunSynchronously + Assert.AreEqual(6, result) + +[] +let ``AsyncSeq.findBack raises KeyNotFoundException when no match`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [1; 3; 5] + |> AsyncSeq.findBack (fun x -> x % 2 = 0) + |> Async.RunSynchronously + |> ignore) + |> ignore + +[] +let ``AsyncSeq.tryFindBackAsync returns last element satisfying async predicate`` () = + let result = + AsyncSeq.ofSeq [1; 2; 3; 4; 5] + |> AsyncSeq.tryFindBackAsync (fun x -> async { return x < 4 }) + |> Async.RunSynchronously + Assert.AreEqual(Some 3, result) + +[] +let ``AsyncSeq.tryFindBackAsync returns None when nothing matches`` () = + let result = + AsyncSeq.ofSeq [1; 2; 3] + |> AsyncSeq.tryFindBackAsync (fun x -> async { return x > 99 }) + |> Async.RunSynchronously + Assert.AreEqual(None, result) + +[] +let ``AsyncSeq.findBackAsync returns last matching element`` () = + let result = + AsyncSeq.ofSeq [10; 20; 5; 15; 30] + |> AsyncSeq.findBackAsync (fun x -> async { return x > 10 }) + |> Async.RunSynchronously + Assert.AreEqual(30, result) + +[] +let ``AsyncSeq.findBackAsync raises KeyNotFoundException when no match`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [1; 2; 3] + |> AsyncSeq.findBackAsync (fun x -> async { return x > 99 }) + |> Async.RunSynchronously + |> ignore) + |> ignore