Skip to content

Commit f2f8be5

Browse files
github-actions[bot]Copilotdsyme
authored
[Repo Assist] Add AsyncSeq.last, item, tryItem (#259)
* Add AsyncSeq.last, item, tryItem - AsyncSeq.last: returns the last element; raises InvalidOperationException if empty, mirroring Seq.last - AsyncSeq.item: returns element at given index; raises ArgumentException if out of bounds, mirroring Seq.item - AsyncSeq.tryItem: returns element at given index as option, or None if out of bounds, mirroring Seq.tryItem 11 new tests; 267 total pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * ci: trigger CI checks --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.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> Co-authored-by: Don Syme <dsyme@users.noreply.github.com>
1 parent 0fdcba1 commit f2f8be5

File tree

4 files changed

+109
-0
lines changed

4 files changed

+109
-0
lines changed

RELEASE_NOTES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
### 4.5.0
2+
3+
* Added `AsyncSeq.last` — returns the last element of the sequence; raises `InvalidOperationException` if empty, mirroring `Seq.last`.
4+
* Added `AsyncSeq.item` — returns the element at the specified index; raises `ArgumentException` if out of bounds, mirroring `Seq.item`.
5+
* Added `AsyncSeq.tryItem` — returns the element at the specified index as `option`, or `None` if the index is out of bounds, mirroring `Seq.tryItem`.
6+
17
### 4.4.0
28

39
* Added `AsyncSeq.findAsync` — async-predicate variant of `AsyncSeq.find`; raises `KeyNotFoundException` if no match, mirroring `Seq.find`.

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1066,6 +1066,32 @@ module AsyncSeq =
10661066
| None -> return raise (System.InvalidOperationException("The input sequence was empty."))
10671067
| Some v -> return v }
10681068

1069+
let last (source : AsyncSeq<'T>) = async {
1070+
let! result = tryLast source
1071+
match result with
1072+
| None -> return raise (System.InvalidOperationException("The input sequence was empty."))
1073+
| Some v -> return v }
1074+
1075+
let tryItem (index : int) (source : AsyncSeq<'T>) = async {
1076+
if index < 0 then return None
1077+
else
1078+
use ie = source.GetEnumerator()
1079+
let! first = ie.MoveNext()
1080+
let b = ref first
1081+
let i = ref 0
1082+
while b.Value.IsSome && !i < index do
1083+
let! next = ie.MoveNext()
1084+
b := next
1085+
i := !i + 1
1086+
if !i = index then return b.Value
1087+
else return None }
1088+
1089+
let item (index : int) (source : AsyncSeq<'T>) = async {
1090+
let! result = tryItem index source
1091+
match result with
1092+
| None -> return raise (System.ArgumentException(sprintf "The input sequence has an insufficient number of elements. index = %d" index))
1093+
| Some v -> return v }
1094+
10691095
let exactlyOne (source : AsyncSeq<'T>) = async {
10701096
use ie = source.GetEnumerator()
10711097
let! first = ie.MoveNext()

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,18 @@ module AsyncSeq =
155155
/// given asynchronous sequence (or None if the sequence is empty).
156156
val tryLast : source:AsyncSeq<'T> -> Async<'T option>
157157

158+
/// Asynchronously returns the last element of the asynchronous sequence.
159+
/// Raises InvalidOperationException if the sequence is empty, mirroring Seq.last.
160+
val last : source:AsyncSeq<'T> -> Async<'T>
161+
162+
/// Asynchronously returns the element at the specified index in the asynchronous sequence.
163+
/// Raises ArgumentException if the index is out of bounds, mirroring Seq.item.
164+
val item : index:int -> source:AsyncSeq<'T> -> Async<'T>
165+
166+
/// Asynchronously returns the element at the specified index in the asynchronous sequence,
167+
/// or None if the index is out of bounds, mirroring Seq.tryItem.
168+
val tryItem : index:int -> source:AsyncSeq<'T> -> Async<'T option>
169+
158170
/// Asynchronously returns the first element that was generated by the
159171
/// given asynchronous sequence (or the specified default value).
160172
val firstOrDefault : ``default``:'T -> source:AsyncSeq<'T> -> Async<'T>

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

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3209,3 +3209,68 @@ let ``AsyncSeq.forallAsync returns true on empty sequence`` () =
32093209
|> AsyncSeq.forallAsync (fun _ -> async { return false })
32103210
|> Async.RunSynchronously
32113211
Assert.IsTrue(result)
3212+
3213+
// ===== last =====
3214+
3215+
[<Test>]
3216+
let ``AsyncSeq.last returns last element`` () =
3217+
let source = asyncSeq { yield 1; yield 2; yield 3 }
3218+
let result = AsyncSeq.last source |> Async.RunSynchronously
3219+
Assert.AreEqual(3, result)
3220+
3221+
[<Test>]
3222+
let ``AsyncSeq.last on singleton returns that element`` () =
3223+
let result = AsyncSeq.last (AsyncSeq.singleton 42) |> Async.RunSynchronously
3224+
Assert.AreEqual(42, result)
3225+
3226+
[<Test>]
3227+
let ``AsyncSeq.last raises on empty sequence`` () =
3228+
Assert.Throws<InvalidOperationException>(fun () ->
3229+
AsyncSeq.last AsyncSeq.empty<int> |> Async.RunSynchronously |> ignore) |> ignore
3230+
3231+
// ===== item =====
3232+
3233+
[<Test>]
3234+
let ``AsyncSeq.item returns element at index 0`` () =
3235+
let source = asyncSeq { yield 10; yield 20; yield 30 }
3236+
let result = AsyncSeq.item 0 source |> Async.RunSynchronously
3237+
Assert.AreEqual(10, result)
3238+
3239+
[<Test>]
3240+
let ``AsyncSeq.item returns element at index 2`` () =
3241+
let source = asyncSeq { yield 10; yield 20; yield 30 }
3242+
let result = AsyncSeq.item 2 source |> Async.RunSynchronously
3243+
Assert.AreEqual(30, result)
3244+
3245+
[<Test>]
3246+
let ``AsyncSeq.item raises when index out of bounds`` () =
3247+
Assert.Throws<ArgumentException>(fun () ->
3248+
AsyncSeq.item 5 (AsyncSeq.ofSeq [1;2;3]) |> Async.RunSynchronously |> ignore) |> ignore
3249+
3250+
[<Test>]
3251+
let ``AsyncSeq.item raises when index negative`` () =
3252+
Assert.Throws<ArgumentException>(fun () ->
3253+
AsyncSeq.item -1 (AsyncSeq.ofSeq [1;2;3]) |> Async.RunSynchronously |> ignore) |> ignore
3254+
3255+
// ===== tryItem =====
3256+
3257+
[<Test>]
3258+
let ``AsyncSeq.tryItem returns Some for valid index`` () =
3259+
let source = asyncSeq { yield 10; yield 20; yield 30 }
3260+
let result = AsyncSeq.tryItem 1 source |> Async.RunSynchronously
3261+
Assert.AreEqual(Some 20, result)
3262+
3263+
[<Test>]
3264+
let ``AsyncSeq.tryItem returns None for out-of-bounds index`` () =
3265+
let result = AsyncSeq.tryItem 10 (AsyncSeq.ofSeq [1;2;3]) |> Async.RunSynchronously
3266+
Assert.AreEqual(None, result)
3267+
3268+
[<Test>]
3269+
let ``AsyncSeq.tryItem returns None for negative index`` () =
3270+
let result = AsyncSeq.tryItem -1 (AsyncSeq.ofSeq [1;2;3]) |> Async.RunSynchronously
3271+
Assert.AreEqual(None, result)
3272+
3273+
[<Test>]
3274+
let ``AsyncSeq.tryItem returns None on empty sequence`` () =
3275+
let result = AsyncSeq.tryItem 0 AsyncSeq.empty<int> |> Async.RunSynchronously
3276+
Assert.AreEqual(None, result)

0 commit comments

Comments
 (0)