diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 50edbb7..be845e4 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,9 @@ +### 4.5.0 + +* Added `AsyncSeq.last` — returns the last element of the sequence; raises `InvalidOperationException` if empty, mirroring `Seq.last`. +* Added `AsyncSeq.item` — returns the element at the specified index; raises `ArgumentException` if out of bounds, mirroring `Seq.item`. +* Added `AsyncSeq.tryItem` — returns the element at the specified index as `option`, or `None` if the index is out of bounds, mirroring `Seq.tryItem`. + ### 4.4.0 * Added `AsyncSeq.findAsync` — async-predicate variant of `AsyncSeq.find`; raises `KeyNotFoundException` if no match, mirroring `Seq.find`. diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index a18c74d..554cf07 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1066,6 +1066,32 @@ module AsyncSeq = | None -> return raise (System.InvalidOperationException("The input sequence was empty.")) | Some v -> return v } + let last (source : AsyncSeq<'T>) = async { + let! result = tryLast source + match result with + | None -> return raise (System.InvalidOperationException("The input sequence was empty.")) + | Some v -> return v } + + let tryItem (index : int) (source : AsyncSeq<'T>) = async { + if index < 0 then return None + else + use ie = source.GetEnumerator() + let! first = ie.MoveNext() + let b = ref first + let i = ref 0 + while b.Value.IsSome && !i < index do + let! next = ie.MoveNext() + b := next + i := !i + 1 + if !i = index then return b.Value + else return None } + + let item (index : int) (source : AsyncSeq<'T>) = async { + let! result = tryItem index source + match result with + | None -> return raise (System.ArgumentException(sprintf "The input sequence has an insufficient number of elements. index = %d" index)) + | Some v -> return v } + let exactlyOne (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 a1cae85..4c84a41 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -155,6 +155,18 @@ module AsyncSeq = /// given asynchronous sequence (or None if the sequence is empty). val tryLast : source:AsyncSeq<'T> -> Async<'T option> + /// Asynchronously returns the last element of the asynchronous sequence. + /// Raises InvalidOperationException if the sequence is empty, mirroring Seq.last. + val last : source:AsyncSeq<'T> -> Async<'T> + + /// Asynchronously returns the element at the specified index in the asynchronous sequence. + /// Raises ArgumentException if the index is out of bounds, mirroring Seq.item. + val item : index:int -> source:AsyncSeq<'T> -> Async<'T> + + /// Asynchronously returns the element at the specified index in the asynchronous sequence, + /// or None if the index is out of bounds, mirroring Seq.tryItem. + val tryItem : index:int -> source:AsyncSeq<'T> -> Async<'T option> + /// Asynchronously returns the first element that was generated by the /// given asynchronous sequence (or the specified default value). val firstOrDefault : ``default``:'T -> source:AsyncSeq<'T> -> Async<'T> diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index 33fc9d1..88ce7f1 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3209,3 +3209,68 @@ let ``AsyncSeq.forallAsync returns true on empty sequence`` () = |> AsyncSeq.forallAsync (fun _ -> async { return false }) |> Async.RunSynchronously Assert.IsTrue(result) + +// ===== last ===== + +[] +let ``AsyncSeq.last returns last element`` () = + let source = asyncSeq { yield 1; yield 2; yield 3 } + let result = AsyncSeq.last source |> Async.RunSynchronously + Assert.AreEqual(3, result) + +[] +let ``AsyncSeq.last on singleton returns that element`` () = + let result = AsyncSeq.last (AsyncSeq.singleton 42) |> Async.RunSynchronously + Assert.AreEqual(42, result) + +[] +let ``AsyncSeq.last raises on empty sequence`` () = + Assert.Throws(fun () -> + AsyncSeq.last AsyncSeq.empty |> Async.RunSynchronously |> ignore) |> ignore + +// ===== item ===== + +[] +let ``AsyncSeq.item returns element at index 0`` () = + let source = asyncSeq { yield 10; yield 20; yield 30 } + let result = AsyncSeq.item 0 source |> Async.RunSynchronously + Assert.AreEqual(10, result) + +[] +let ``AsyncSeq.item returns element at index 2`` () = + let source = asyncSeq { yield 10; yield 20; yield 30 } + let result = AsyncSeq.item 2 source |> Async.RunSynchronously + Assert.AreEqual(30, result) + +[] +let ``AsyncSeq.item raises when index out of bounds`` () = + Assert.Throws(fun () -> + AsyncSeq.item 5 (AsyncSeq.ofSeq [1;2;3]) |> Async.RunSynchronously |> ignore) |> ignore + +[] +let ``AsyncSeq.item raises when index negative`` () = + Assert.Throws(fun () -> + AsyncSeq.item -1 (AsyncSeq.ofSeq [1;2;3]) |> Async.RunSynchronously |> ignore) |> ignore + +// ===== tryItem ===== + +[] +let ``AsyncSeq.tryItem returns Some for valid index`` () = + let source = asyncSeq { yield 10; yield 20; yield 30 } + let result = AsyncSeq.tryItem 1 source |> Async.RunSynchronously + Assert.AreEqual(Some 20, result) + +[] +let ``AsyncSeq.tryItem returns None for out-of-bounds index`` () = + let result = AsyncSeq.tryItem 10 (AsyncSeq.ofSeq [1;2;3]) |> Async.RunSynchronously + Assert.AreEqual(None, result) + +[] +let ``AsyncSeq.tryItem returns None for negative index`` () = + let result = AsyncSeq.tryItem -1 (AsyncSeq.ofSeq [1;2;3]) |> Async.RunSynchronously + Assert.AreEqual(None, result) + +[] +let ``AsyncSeq.tryItem returns None on empty sequence`` () = + let result = AsyncSeq.tryItem 0 AsyncSeq.empty |> Async.RunSynchronously + Assert.AreEqual(None, result)