diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index c88d2e5..1956545 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,7 @@ +### 4.9.0 + +* Added `AsyncSeq.transpose` — transposes an async sequence of sequences, yielding each column as an array; buffers all rows before yielding; mirrors `Seq.transpose`. Raises `InvalidOperationException` if inner sequences have different lengths. + ### 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..0a502bf 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -2035,6 +2035,18 @@ module AsyncSeq = let! arr = toArrayAsync source for i in arr.Length - 1 .. -1 .. 0 do yield arr.[i] } + + /// Transposes the rows and columns of an async sequence of sequences. + /// Buffers the entire source sequence. Raises InvalidOperationException if inner sequences + /// have different lengths. Mirrors Seq.transpose. + let transpose (source: AsyncSeq>) : AsyncSeq<'T[]> = asyncSeq { + let! rows = toListAsync (source |> map Seq.toArray) + if not rows.IsEmpty then + let firstLen = rows.Head.Length + if rows |> List.exists (fun row -> row.Length <> firstLen) then + invalidOp "The input sequences have different lengths." + for col in 0 .. firstLen - 1 do + yield rows |> List.map (fun row -> row.[col]) |> List.toArray } #endif #if !FABLE_COMPILER diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index 5113dd6..6e23a09 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 the rows and columns of an async sequence of sequences, yielding each + /// column as an array. The entire source sequence is buffered before any column is yielded, + /// mirroring Seq.transpose. Raises InvalidOperationException if inner sequences have + /// different lengths. This function should not be used with large or infinite sequences. + val transpose : source:AsyncSeq> -> 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..fc1a0a0 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3662,3 +3662,69 @@ let ``AsyncSeq.insertAt raises ArgumentException when index exceeds length`` () |> AsyncSeq.toArrayAsync |> Async.RunSynchronously |> ignore) |> ignore + +// ===== transpose ===== + +[] +let ``AsyncSeq.transpose basic 2x3 matrix`` () = + let source = + asyncSeq { + yield seq { yield 1; yield 2; yield 3 } + yield seq { yield 4; yield 5; yield 6 } + } + let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual(3, result.Length) + Assert.AreEqual([| 1; 4 |], result.[0]) + Assert.AreEqual([| 2; 5 |], result.[1]) + Assert.AreEqual([| 3; 6 |], result.[2]) + +[] +let ``AsyncSeq.transpose empty outer sequence yields empty`` () = + let result = + AsyncSeq.empty> + |> AsyncSeq.transpose + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([||], result) + +[] +let ``AsyncSeq.transpose single row returns one column per element`` () = + let source = asyncSeq { yield seq { yield 1; yield 2; yield 3 } } + let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual(3, result.Length) + Assert.AreEqual([| 1 |], result.[0]) + Assert.AreEqual([| 2 |], result.[1]) + Assert.AreEqual([| 3 |], result.[2]) + +[] +let ``AsyncSeq.transpose single column returns one row per element`` () = + let source = + asyncSeq { + yield seq { yield 1 } + yield seq { yield 2 } + yield seq { yield 3 } + } + let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual(1, result.Length) + Assert.AreEqual([| 1; 2; 3 |], result.[0]) + +[] +let ``AsyncSeq.transpose of singleton rows yields one column`` () = + let source = asyncSeq { yield seq { yield 7 }; yield seq { yield 8 } } + let result = AsyncSeq.transpose source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual(1, result.Length) + Assert.AreEqual([| 7; 8 |], result.[0]) + +[] +let ``AsyncSeq.transpose raises InvalidOperationException for jagged input`` () = + let source = + asyncSeq { + yield seq { yield 1; yield 2 } + yield seq { yield 3 } + } + Assert.Throws(fun () -> + AsyncSeq.transpose source + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + |> ignore) + |> ignore