diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 184a300..8e40b41 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -13,6 +13,13 @@ * Added `AsyncSeq.delay` — defers sequence creation to enumeration time by calling a factory function each time `GetAsyncEnumerator` is called. Mirrors `TaskSeq.delay`. * Added `AsyncSeq.collectAsync` — like `collect` but the mapping function is asynchronous (`'T -> Async>`). Mirrors `TaskSeq.collectAsync`. * Added `AsyncSeq.partition` / `AsyncSeq.partitionAsync` — splits a sequence into two arrays using a (optionally async) predicate; the first array contains matching elements, the second non-matching. Mirrors `TaskSeq.partition` / `TaskSeq.partitionAsync`. + * 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. * Tests: added 14 new unit tests covering previously untested functions — `AsyncSeq.indexed`, `AsyncSeq.iteriAsync`, `AsyncSeq.tryLast`, `AsyncSeq.replicateUntilNoneAsync`, and `AsyncSeq.reduceAsync` (empty-sequence edge case). ### 4.10.0 diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index 07d339f..7e05620 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1645,6 +1645,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) @@ -2059,6 +2087,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<_> = if count < 0 then invalidArg "count" "must be non-negative" AsyncSeqImpl(fun () -> new OptimizedSkipEnumerator<'T>(source.GetEnumerator(), count) :> IAsyncSeqEnumerator<'T>) :> AsyncSeq<'T> diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index c3c266f..d70b46b 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -419,6 +419,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 + /// Asynchronously returns the number of elements in the sequence for which the predicate returns true. val lengthBy : predicate:('T -> bool) -> source:AsyncSeq<'T> -> Async @@ -468,6 +473,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 42e8d6b..9ca7bef 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3823,6 +3823,79 @@ let ``AsyncSeq.withCancellation with cancelled token raises OperationCanceledExc |> 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 + // ===== indexed ===== [] @@ -3931,6 +4004,118 @@ let ``AsyncSeq.replicateUntilNoneAsync generates elements until None`` () = |> 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) + [] let ``AsyncSeq.replicateUntilNoneAsync returns empty for immediate None`` () = let result =