diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 6f4908d..fad03de 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,14 @@ +### 4.11.0 + +* Added `AsyncSeq.insertManyAt` — inserts multiple values before the element at the given index. Mirrors `Seq.insertManyAt` and `TaskSeq.insertManyAt`. +* Added `AsyncSeq.removeManyAt` — removes a run of elements starting at the given index. Mirrors `Seq.removeManyAt` and `TaskSeq.removeManyAt`. +* Added `AsyncSeq.box` — boxes each element to `obj`. Mirrors `TaskSeq.box`. +* Added `AsyncSeq.unbox<'T>` — unboxes each `obj` element to `'T`. Mirrors `TaskSeq.unbox`. +* Added `AsyncSeq.cast<'T>` — dynamically casts each `obj` element to `'T`. Mirrors `TaskSeq.cast`. +* Added `AsyncSeq.lengthOrMax` — counts elements up to a maximum, avoiding full enumeration of long or infinite sequences. Mirrors `TaskSeq.lengthOrMax`. +* Note: `AsyncSeq.except` already accepts `seq<'T>` for the excluded collection, so no separate `exceptOfSeq` is needed. +* Part of ongoing design-parity work with FSharp.Control.TaskSeq (see #277). + ### 4.10.0 * Added `AsyncSeq.withCancellation` — returns a new `AsyncSeq` that passes the given `CancellationToken` to `GetAsyncEnumerator`, overriding whatever token would otherwise be supplied. Mirrors `TaskSeq.withCancellation` and is useful when consuming sequences from libraries (e.g. Entity Framework) that accept a cancellation token through `GetAsyncEnumerator`. Part of ongoing design-parity work with FSharp.Control.TaskSeq (see #277). diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index 642bab5..3ba6c18 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1528,6 +1528,34 @@ module AsyncSeq = elif i.Value < index then invalidArg "index" "The index is outside the range of elements in the collection." } + let insertManyAt (index : int) (values : seq<'T>) (source : AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq { + if index < 0 then invalidArg "index" "must be non-negative" + let i = ref 0 + for x in source do + if i.Value = index then yield! ofSeq values + yield x + i := i.Value + 1 + if i.Value = index then yield! ofSeq values + elif i.Value < index then + invalidArg "index" "The index is outside the range of elements in the collection." } + + let removeManyAt (index : int) (count : int) (source : AsyncSeq<'T>) : AsyncSeq<'T> = asyncSeq { + if index < 0 then invalidArg "index" "must be non-negative" + if count < 0 then invalidArg "count" "must be non-negative" + let i = ref 0 + for x in source do + if i.Value < index || i.Value >= index + count then yield x + i := i.Value + 1 } + + let box (source : AsyncSeq<'T>) : AsyncSeq = + map Microsoft.FSharp.Core.Operators.box source + + let unbox<'T> (source : AsyncSeq) : AsyncSeq<'T> = + map Microsoft.FSharp.Core.Operators.unbox source + + let cast<'T> (source : AsyncSeq) : AsyncSeq<'T> = + map Microsoft.FSharp.Core.Operators.unbox source + #if !FABLE_COMPILER let iterAsyncParallel (f:'a -> Async) (s:AsyncSeq<'a>) : Async = async { use mb = MailboxProcessor.Start (ignore >> async.Return) @@ -1914,6 +1942,12 @@ module AsyncSeq = let truncate count source = take count source + let lengthOrMax (max : int) (source : AsyncSeq<'T>) : Async = + async { + let! n = source |> take max |> length + return int n + } + let skip count (source : AsyncSeq<'T>) : AsyncSeq<_> = asyncSeq { if (count < 0) then invalidArg "count" "must be non-negative" use ie = source.GetEnumerator() diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index b17f019..aaefb64 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -395,6 +395,11 @@ module AsyncSeq = /// Asynchronously determine the number of elements in the sequence val length : source:AsyncSeq<'T> -> Async + /// Asynchronously counts elements up to a maximum. Returns the actual count if the sequence has + /// fewer than 'max' elements, otherwise returns 'max'. Avoids full enumeration of long sequences. + /// Mirrors TaskSeq.lengthOrMax. + val lengthOrMax : max:int -> source:AsyncSeq<'T> -> Async + /// Same as AsyncSeq.scanAsync, but the specified function is synchronous. val scan : folder:('State -> 'T -> 'State) -> state:'State -> source:AsyncSeq<'T> -> AsyncSeq<'State> @@ -431,6 +436,25 @@ module AsyncSeq = /// Raises ArgumentException if index is negative or greater than the sequence length. Mirrors Seq.insertAt. val insertAt : index:int -> value:'T -> source:AsyncSeq<'T> -> AsyncSeq<'T> + /// Returns a new asynchronous sequence with the given values inserted before the element at the specified index. + /// An index equal to the length of the sequence appends the values at the end. + /// Raises ArgumentException if index is negative or greater than the sequence length. Mirrors Seq.insertManyAt. + val insertManyAt : index:int -> values:seq<'T> -> source:AsyncSeq<'T> -> AsyncSeq<'T> + + /// Returns a new asynchronous sequence with 'count' elements removed starting at the specified index. + /// Raises ArgumentException if index or count is negative. Mirrors Seq.removeManyAt. + val removeManyAt : index:int -> count:int -> source:AsyncSeq<'T> -> AsyncSeq<'T> + + /// Returns a new asynchronous sequence where each element is boxed to type obj. + val box : source:AsyncSeq<'T> -> AsyncSeq + + /// Returns a new asynchronous sequence where each obj element is unboxed to type 'T. + val unbox<'T> : source:AsyncSeq -> AsyncSeq<'T> + + /// Returns a new asynchronous sequence where each obj element is dynamically cast to type 'T. + /// Raises InvalidCastException if an element cannot be cast. + val cast<'T> : source:AsyncSeq -> AsyncSeq<'T> + /// Creates an asynchronous sequence that lazily takes element from an /// input synchronous sequence and returns them one-by-one. val ofSeq : source:seq<'T> -> AsyncSeq<'T> diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index 6e39e77..eac59b5 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3719,3 +3719,191 @@ let ``AsyncSeq.withCancellation with cancelled token raises OperationCanceledExc |> Async.RunSynchronously |> ignore) |> ignore + +// ===== insertManyAt ===== + +[] +let ``AsyncSeq.insertManyAt inserts values at middle index`` () = + let result = + AsyncSeq.ofSeq [ 1; 4; 5 ] + |> AsyncSeq.insertManyAt 1 [ 2; 3 ] + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3; 4; 5 |], result) + +[] +let ``AsyncSeq.insertManyAt inserts at index 0 (prepend)`` () = + let result = + AsyncSeq.ofSeq [ 3; 4 ] + |> AsyncSeq.insertManyAt 0 [ 1; 2 ] + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3; 4 |], result) + +[] +let ``AsyncSeq.insertManyAt appends when index equals sequence length`` () = + let result = + AsyncSeq.ofSeq [ 1; 2 ] + |> AsyncSeq.insertManyAt 2 [ 3; 4 ] + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3; 4 |], result) + +[] +let ``AsyncSeq.insertManyAt with empty values is identity`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.insertManyAt 1 [] + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3 |], result) + +[] +let ``AsyncSeq.insertManyAt raises ArgumentException for negative index`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [ 1; 2 ] + |> AsyncSeq.insertManyAt -1 [ 0 ] + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously |> ignore) + |> ignore + +// ===== removeManyAt ===== + +[] +let ``AsyncSeq.removeManyAt removes elements at middle index`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3; 4; 5 ] + |> AsyncSeq.removeManyAt 1 3 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 5 |], result) + +[] +let ``AsyncSeq.removeManyAt removes from start`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3; 4 ] + |> AsyncSeq.removeManyAt 0 2 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 3; 4 |], result) + +[] +let ``AsyncSeq.removeManyAt count zero returns all elements`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.removeManyAt 1 0 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3 |], result) + +[] +let ``AsyncSeq.removeManyAt count greater than remaining removes to end`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.removeManyAt 1 10 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1 |], result) + +[] +let ``AsyncSeq.removeManyAt raises ArgumentException for negative index`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.removeManyAt -1 1 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously |> ignore) + |> ignore + +[] +let ``AsyncSeq.removeManyAt raises ArgumentException for negative count`` () = + Assert.Throws(fun () -> + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.removeManyAt 0 -1 + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously |> ignore) + |> ignore + +// ===== box / unbox / cast ===== + +[] +let ``AsyncSeq.box boxes each element to obj`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.box + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual(3, result.Length) + Assert.AreEqual(box 1, result.[0]) + Assert.AreEqual(box 2, result.[1]) + Assert.AreEqual(box 3, result.[2]) + +[] +let ``AsyncSeq.unbox unboxes each element`` () = + let result = + AsyncSeq.ofSeq [ box 1; box 2; box 3 ] + |> AsyncSeq.unbox + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3 |], result) + +[] +let ``AsyncSeq.cast casts each element`` () = + let result = + AsyncSeq.ofSeq [ box 1; box 2; box 3 ] + |> AsyncSeq.cast + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3 |], result) + +[] +let ``AsyncSeq.box then unbox roundtrips`` () = + let original = [| 10; 20; 30 |] + let result = + AsyncSeq.ofSeq original + |> AsyncSeq.box + |> AsyncSeq.unbox + |> AsyncSeq.toArrayAsync + |> Async.RunSynchronously + Assert.AreEqual(original, result) + +// ===== lengthOrMax ===== + +[] +let ``AsyncSeq.lengthOrMax returns length when sequence is shorter than max`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.lengthOrMax 10 + |> Async.RunSynchronously + Assert.AreEqual(3, result) + +[] +let ``AsyncSeq.lengthOrMax returns max when sequence is longer`` () = + let result = + AsyncSeq.ofSeq [ 1 .. 100 ] + |> AsyncSeq.lengthOrMax 5 + |> Async.RunSynchronously + Assert.AreEqual(5, result) + +[] +let ``AsyncSeq.lengthOrMax returns 0 for empty sequence`` () = + let result = + AsyncSeq.empty + |> AsyncSeq.lengthOrMax 5 + |> Async.RunSynchronously + Assert.AreEqual(0, result) + +[] +let ``AsyncSeq.lengthOrMax with max 0 returns 0`` () = + let result = + AsyncSeq.ofSeq [ 1; 2; 3 ] + |> AsyncSeq.lengthOrMax 0 + |> Async.RunSynchronously + Assert.AreEqual(0, result) + +[] +let ``AsyncSeq.lengthOrMax does not enumerate beyond max on infinite sequence`` () = + let result = + AsyncSeq.replicateInfinite 42 + |> AsyncSeq.lengthOrMax 7 + |> Async.RunSynchronously + Assert.AreEqual(7, result) diff --git a/version.props b/version.props index 989fd4a..7b59b9f 100644 --- a/version.props +++ b/version.props @@ -1,5 +1,5 @@ - 4.10.0 + 4.11.0