diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c88d2e5..9ad3cad 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,7 @@ +### 4.9.0 + +* Added `AsyncSeq.transpose` — transposes a sequence of sequences so each element of the result is an array of the i-th elements of all inner sequences; all inner sequences must have the same length and the entire source is buffered, mirroring `Seq.transpose`. + ### 4.8.0 * Added `AsyncSeq.mapFoldAsync` — maps each element using an asynchronous folder that also threads an accumulator state, returning both the array of results and the final state; mirrors `Seq.mapFold`. diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index bf536f4..69e98ba 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -2035,6 +2035,17 @@ module AsyncSeq = let! arr = toArrayAsync source for i in arr.Length - 1 .. -1 .. 0 do yield arr.[i] } + + let transpose (source: AsyncSeq<#seq<'T>>) : AsyncSeq<'T[]> = asyncSeq { + let! rows = toArrayAsync source + if rows.Length > 0 then + let rowArrays = rows |> Array.map (fun r -> (r :> seq<'T>) |> Seq.toArray) + let colCount = rowArrays.[0].Length + for row in rowArrays do + if row.Length <> colCount then + invalidArg "source" "All inner sequences must have the same length." + for c in 0 .. colCount - 1 do + yield [| for row in rowArrays -> row.[c] |] } #endif #if !FABLE_COMPILER diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index 5113dd6..248da0b 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -673,6 +673,12 @@ module AsyncSeq = /// sequence is buffered before yielding any elements, mirroring Seq.rev. /// This function should not be used with large or infinite sequences. val rev : source:AsyncSeq<'T> -> AsyncSeq<'T> + + /// Transposes a sequence of sequences: each element of the result is an array + /// of the i-th elements of all inner sequences, mirroring Seq.transpose. + /// All inner sequences must have the same length; the entire source is buffered. + /// This function should not be used with large or infinite sequences. + val transpose : source:AsyncSeq<#seq<'T>> -> AsyncSeq<'T[]> #endif /// Interleaves two async sequences of the same type into a resulting sequence. The provided diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index 17277e6..9ace089 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3662,3 +3662,44 @@ let ``AsyncSeq.insertAt raises ArgumentException when index exceeds length`` () |> AsyncSeq.toArrayAsync |> Async.RunSynchronously |> ignore) |> ignore + +// ── AsyncSeq.transpose ──────────────────────────────────────────────────────── + +[] +let ``AsyncSeq.transpose transposes rows and columns`` () = + let source = asyncSeq { yield [1; 2; 3]; yield [4; 5; 6] } + let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| [|1;4|]; [|2;5|]; [|3;6|] |], result) + +[] +let ``AsyncSeq.transpose single row`` () = + let source = asyncSeq { yield [10; 20; 30] } + let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| [|10|]; [|20|]; [|30|] |], result) + +[] +let ``AsyncSeq.transpose empty source returns empty`` () = + let result = AsyncSeq.transpose AsyncSeq.empty |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([||], result) + +[] +let ``AsyncSeq.transpose single column`` () = + let source = asyncSeq { yield [1]; yield [2]; yield [3] } + let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| [|1;2;3|] |], result) + +[] +let ``AsyncSeq.transpose square matrix`` () = + let source = asyncSeq { yield [1; 2]; yield [3; 4] } + let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| [|1;3|]; [|2;4|] |], result) + +[] +let ``AsyncSeq.transpose raises on mismatched row lengths`` () = + Assert.Throws(fun () -> + asyncSeq { yield [1; 2]; yield [3] } + |> AsyncSeq.transpose + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + |> ignore) + |> ignore