diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 1e07a02..f2d7f4b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,10 +1,16 @@ ### 4.6.0 +<<<<<<< repo-assist/improve-isempty-tryhead-except-2471744-646c6897389cdfcb +* Added `AsyncSeq.isEmpty` — returns `true` if the sequence contains no elements; short-circuits after the first element, mirroring `Seq.isEmpty`. +* Added `AsyncSeq.tryHead` — returns the first element as `option`, or `None` if the sequence is empty, mirroring `Seq.tryHead` (equivalent to the existing `AsyncSeq.tryFirst`). +* Added `AsyncSeq.except` — returns a new sequence excluding all elements present in a given collection, mirroring `Seq.except`. +======= * Added `AsyncSeq.findIndex` — returns the index of the first element satisfying a predicate; raises `KeyNotFoundException` if no match, mirroring `Seq.findIndex`. * Added `AsyncSeq.tryFindIndex` — returns the index of the first element satisfying a predicate as `option`, or `None` if not found, mirroring `Seq.tryFindIndex`. * Added `AsyncSeq.findIndexAsync` — async-predicate variant of `AsyncSeq.findIndex`; raises `KeyNotFoundException` if no match. * Added `AsyncSeq.tryFindIndexAsync` — async-predicate variant of `AsyncSeq.tryFindIndex`; returns `option`. * Added `AsyncSeq.sortWith` — sorts the sequence using a custom comparison function, returning an array, mirroring `Seq.sortWith`. +>>>>>>> main ### 4.5.0 diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index 5dba884..c15e802 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1066,6 +1066,13 @@ module AsyncSeq = | None -> return raise (System.InvalidOperationException("The input sequence was empty.")) | Some v -> return v } + let tryHead (source : AsyncSeq<'T>) = tryFirst source + + let isEmpty (source : AsyncSeq<'T>) = async { + use ie = source.GetEnumerator() + let! v = ie.MoveNext() + return v.IsNone } + let last (source : AsyncSeq<'T>) = async { let! result = tryLast source match result with @@ -1421,6 +1428,10 @@ module AsyncSeq = let filter f (source : AsyncSeq<'T>) = filterAsync (f >> async.Return) source + let except (excluded : seq<'T>) (source : AsyncSeq<'T>) : AsyncSeq<'T> = + let s = System.Collections.Generic.HashSet(excluded) + source |> filter (fun x -> not (s.Contains(x))) + #if !FABLE_COMPILER let iterAsyncParallel (f:'a -> Async) (s:AsyncSeq<'a>) : Async = async { use mb = MailboxProcessor.Start (ignore >> async.Return) diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index 16d6ab0..7be6bf8 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -179,6 +179,14 @@ module AsyncSeq = /// Raises InvalidOperationException if the sequence is empty. val head : source:AsyncSeq<'T> -> Async<'T> + /// Asynchronously returns the first element of the asynchronous sequence as an option, + /// or None if the sequence is empty. Mirrors Seq.tryHead. + val tryHead : source:AsyncSeq<'T> -> Async<'T option> + + /// Asynchronously returns true if the asynchronous sequence contains no elements, false otherwise. + /// Short-circuits after the first element. Mirrors Seq.isEmpty. + val isEmpty : source:AsyncSeq<'T> -> Async + /// Asynchronously returns the only element of the asynchronous sequence. /// Raises InvalidOperationException if the sequence is empty or contains more than one element. val exactlyOne : source:AsyncSeq<'T> -> Async<'T> @@ -396,6 +404,10 @@ module AsyncSeq = /// and processes the input element immediately. val filter : predicate:('T -> bool) -> source:AsyncSeq<'T> -> AsyncSeq<'T> + /// Returns a new asynchronous sequence containing only elements that are not present + /// in the given excluded collection. Uses a HashSet for O(1) lookup. Mirrors Seq.except. + val except : excluded:seq<'T> -> source:AsyncSeq<'T> -> AsyncSeq<'T> when 'T : equality + /// 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 39ba79d..74c43c7 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -3275,6 +3275,57 @@ let ``AsyncSeq.tryItem returns None on empty sequence`` () = let result = AsyncSeq.tryItem 0 AsyncSeq.empty |> Async.RunSynchronously Assert.AreEqual(None, result) +// ===== isEmpty ===== + +[] +let ``AsyncSeq.isEmpty returns true for empty sequence`` () = + let result = AsyncSeq.isEmpty AsyncSeq.empty |> Async.RunSynchronously + Assert.True(result) + +[] +let ``AsyncSeq.isEmpty returns false for non-empty sequence`` () = + let source = asyncSeq { yield 1; yield 2 } + let result = AsyncSeq.isEmpty source |> Async.RunSynchronously + Assert.False(result) + +[] +let ``AsyncSeq.isEmpty returns false for singleton`` () = + let result = AsyncSeq.isEmpty (AsyncSeq.singleton 42) |> Async.RunSynchronously + Assert.False(result) + +// ===== tryHead ===== + +[] +let ``AsyncSeq.tryHead returns Some for non-empty sequence`` () = + let source = asyncSeq { yield 42; yield 99 } + let result = AsyncSeq.tryHead source |> Async.RunSynchronously + Assert.AreEqual(Some 42, result) + +[] +let ``AsyncSeq.tryHead returns None for empty sequence`` () = + let result = AsyncSeq.tryHead AsyncSeq.empty |> Async.RunSynchronously + Assert.AreEqual(None, result) + +// ===== except ===== + +[] +let ``AsyncSeq.except removes excluded elements`` () = + let source = asyncSeq { yield 1; yield 2; yield 3; yield 4; yield 5 } + let result = AsyncSeq.except [2; 4] source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| 1; 3; 5 |], result) + +[] +let ``AsyncSeq.except with empty excluded returns all elements`` () = + let source = asyncSeq { yield 1; yield 2; yield 3 } + let result = AsyncSeq.except [] source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([| 1; 2; 3 |], result) + +[] +let ``AsyncSeq.except with all excluded returns empty sequence`` () = + let source = asyncSeq { yield 1; yield 2; yield 3 } + let result = AsyncSeq.except [1; 2; 3] source |> AsyncSeq.toArrayAsync |> Async.RunSynchronously + Assert.AreEqual([||], result) + // ===== findIndex / tryFindIndex / findIndexAsync / tryFindIndexAsync ===== []