From 4d36a51dad26378b4a1a9b5506b6a25be72c9125 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Mar 2026 17:53:20 +0000 Subject: [PATCH 1/2] =?UTF-8?q?Add=20AsyncSeq.splitAt=20=E2=80=94=20splits?= =?UTF-8?q?=20at=20index,=20returning=20first=20N=20elements=20as=20array?= =?UTF-8?q?=20and=20remainder=20as=20AsyncSeq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors Seq.splitAt. Source is enumerated once; remainder is produced lazily. 6 new tests; 293/293 total pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- RELEASE_NOTES.md | 4 ++ src/FSharp.Control.AsyncSeq/AsyncSeq.fs | 29 ++++++++++++ src/FSharp.Control.AsyncSeq/AsyncSeq.fsi | 5 +++ .../AsyncSeqTests.fs | 44 +++++++++++++++++++ 4 files changed, 82 insertions(+) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 5b078ca..e53e33e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,7 @@ +### 4.7.0 + +* Added `AsyncSeq.splitAt` — splits a sequence at the given index, returning the first `count` elements as an array and the remaining elements as a new `AsyncSeq`. Mirrors `Seq.splitAt`. The source is enumerated once. + ### 4.6.0 * Added `AsyncSeq.isEmpty` — returns `true` if the sequence contains no elements; short-circuits after the first element, mirroring `Seq.isEmpty`. diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index c15e802..6392b1c 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1815,6 +1815,35 @@ module AsyncSeq = let tail (source : AsyncSeq<'T>) : AsyncSeq<'T> = skip 1 source + /// Splits an async sequence at the given index, returning the first `count` elements as an array + /// and the remaining elements as a new AsyncSeq. The source is enumerated once. + let splitAt (count: int) (source: AsyncSeq<'T>) : Async<'T array * AsyncSeq<'T>> = async { + if count < 0 then invalidArg "count" "must be non-negative" + let ie = source.GetEnumerator() + let ra = ResizeArray<'T>() + let! m = ie.MoveNext() + let b = ref m + while b.Value.IsSome && ra.Count < count do + ra.Add b.Value.Value + let! next = ie.MoveNext() + b := next + let first = ra.ToArray() + let rest = + if b.Value.IsNone then + ie.Dispose() + empty<'T> + else + let cur = ref b.Value + asyncSeq { + try + while cur.Value.IsSome do + yield cur.Value.Value + let! next = ie.MoveNext() + cur := next + finally + ie.Dispose() } + return first, rest } + let toArrayAsync (source : AsyncSeq<'T>) : Async<'T[]> = async { let ra = (new ResizeArray<_>()) use ie = source.GetEnumerator() diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index 7be6bf8..25a6c3b 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -586,6 +586,11 @@ module AsyncSeq = /// Returns an empty sequence if the source is empty. val tail : source:AsyncSeq<'T> -> AsyncSeq<'T> + /// Splits an async sequence at the given index. Returns an async computation that yields + /// the first `count` elements as an array and the remaining elements as a new AsyncSeq. + /// The source is enumerated once; the returned AsyncSeq lazily produces the remainder. + val splitAt : count:int -> source:AsyncSeq<'T> -> Async<'T array * AsyncSeq<'T>> + /// Creates an async computation which iterates the AsyncSeq and collects the output into an array. val toArrayAsync : source:AsyncSeq<'T> -> Async<'T []> diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index 74c43c7..457ce9d 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3408,3 +3408,47 @@ let ``AsyncSeq.sortWith sorts descending with negated comparer`` () = let ``AsyncSeq.sortWith returns empty array for empty sequence`` () = let result = AsyncSeq.sortWith compare AsyncSeq.empty Assert.AreEqual([||], result) + +[] +let ``AsyncSeq.splitAt splits a sequence at the given index`` () = + let source = asyncSeq { yield 1; yield 2; yield 3; yield 4; yield 5 } + let first, rest = AsyncSeq.splitAt 3 source |> Async.RunSynchronously + let restArr = rest |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3 |], first) + Assert.AreEqual([| 4; 5 |], restArr) + +[] +let ``AsyncSeq.splitAt with count=0 returns empty array and full rest`` () = + let source = asyncSeq { yield 10; yield 20 } + let first, rest = AsyncSeq.splitAt 0 source |> Async.RunSynchronously + let restArr = rest |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([||], first) + Assert.AreEqual([| 10; 20 |], restArr) + +[] +let ``AsyncSeq.splitAt with count >= length returns all elements in first and empty rest`` () = + let source = asyncSeq { yield 1; yield 2; yield 3 } + let first, rest = AsyncSeq.splitAt 10 source |> Async.RunSynchronously + let restArr = rest |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3 |], first) + Assert.AreEqual([||], restArr) + +[] +let ``AsyncSeq.splitAt on empty sequence returns empty first and empty rest`` () = + let first, rest = AsyncSeq.splitAt 3 AsyncSeq.empty |> Async.RunSynchronously + let restArr = rest |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([||], first) + Assert.AreEqual([||], restArr) + +[] +let ``AsyncSeq.splitAt with count equal to length returns all in first and empty rest`` () = + let source = asyncSeq { yield 7; yield 8; yield 9 } + let first, rest = AsyncSeq.splitAt 3 source |> Async.RunSynchronously + let restArr = rest |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| 7; 8; 9 |], first) + Assert.AreEqual([||], restArr) + +[] +let ``AsyncSeq.splitAt with negative count throws ArgumentException`` () = + Assert.Throws(fun () -> + AsyncSeq.splitAt -1 AsyncSeq.empty |> Async.RunSynchronously |> ignore) |> ignore From 30e91bb392a7500c830947235d4b7e4b8754c4bd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 5 Mar 2026 17:56:24 +0000 Subject: [PATCH 2/2] ci: trigger checks