diff --git a/README.md b/README.md index d8b23491..a3c499e6 100644 --- a/README.md +++ b/README.md @@ -319,8 +319,8 @@ This is what has been implemented so far, is planned or skipped: | ✅ [#90][] | `singleton` | `singleton` | | | | ✅ [#209][]| `skip` | `skip` | | | | ✅ [#209][]| | `drop` | | | -| | `skipWhile` | `skipWhile` | `skipWhileAsync` | | -| | | `skipWhileInclusive` | `skipWhileInclusiveAsync` | | +| ✅ [#219][]| `skipWhile` | `skipWhile` | `skipWhileAsync` | | +| ✅ [#219][]| | `skipWhileInclusive` | `skipWhileInclusiveAsync` | | | ❓ | `sort` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | | ❓ | `sortBy` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | | ❓ | `sortByAscending` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | @@ -603,6 +603,7 @@ module TaskSeq = [#133]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/133 [#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209 [#217]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/217 +[#219]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/219 [issues]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues [nuget]: https://www.nuget.org/packages/FSharp.Control.TaskSeq/ diff --git a/assets/nuget-package-readme.md b/assets/nuget-package-readme.md index 1a7651e6..1e1a7f84 100644 --- a/assets/nuget-package-readme.md +++ b/assets/nuget-package-readme.md @@ -199,8 +199,8 @@ This is what has been implemented so far, is planned or skipped: | ✅ [#90][] | `singleton` | `singleton` | | | | ✅ [#209][]| `skip` | `skip` | | | | ✅ [#209][]| | `drop` | | | -| | `skipWhile` | `skipWhile` | `skipWhileAsync` | | -| | | `skipWhileInclusive` | `skipWhileInclusiveAsync` | | +| ✅ [#219][]| `skipWhile` | `skipWhile` | `skipWhileAsync` | | +| ✅ [#219][]| | `skipWhileInclusive` | `skipWhileInclusiveAsync` | | | ❓ | `sort` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | | ❓ | `sortBy` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | | ❓ | `sortByAscending` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") | @@ -308,3 +308,4 @@ _The motivation for `readOnly` in `Seq` is that a cast from a mutable array or l [#126]: https://github.com/fsprojects/FSharp.Control.TaskSeq/pull/126 [#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209 [#217]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/217 +[#219]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/219 diff --git a/release-notes.txt b/release-notes.txt index 06930db4..74096d35 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -3,9 +3,10 @@ Release notes: 0.4.x (unreleased) - overhaul all doc comments, add exceptions, improve IDE quick-info experience, #136 - new surface area functions, fixes #208: - * TaskSeq.take, TaskSeq.skip, #209 - * TaskSeq.truncate, TaskSeq.drop, #209 - * TaskSeq.where, TaskSeq.whereAsync, #217 + * TaskSeq.take, skip, #209 + * TaskSeq.truncate, drop, #209 + * TaskSeq.where, whereAsync, #217 + * TaskSeq.skipWhile, skipWhileInclusive, skipWhileAsync, skipWhileInclusiveAsync, #219 - Performance: less thread hops with 'StartImmediateAsTask' instead of 'StartAsTask', fixes #135 - BINARY INCOMPATIBILITY: 'TaskSeq' module is now static members on 'TaskSeq<_>', fixes #184 diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj index 16ba1b70..3536363d 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -36,6 +36,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.SkipWhile.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.SkipWhile.Tests.fs new file mode 100644 index 00000000..b0bf214e --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.SkipWhile.Tests.fs @@ -0,0 +1,322 @@ +module TaskSeq.Tests.skipWhile + +open System + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// TaskSeq.skipWhile +// TaskSeq.skipWhileAsync +// TaskSeq.skipWhileInclusive +// TaskSeq.skipWhileInclusiveAsync +// + +exception SideEffectPastEnd of string + + +module EmptySeq = + + // TaskSeq-skipWhile+A stands for: + // skipWhile + skipWhileAsync etc. + + [)>] + let ``TaskSeq-skipWhile+A has no effect`` variant = task { + do! + Gen.getEmptyVariant variant + |> TaskSeq.skipWhile ((=) 12) + |> verifyEmpty + + do! + Gen.getEmptyVariant variant + |> TaskSeq.skipWhileAsync ((=) 12 >> Task.fromResult) + |> verifyEmpty + } + + [)>] + let ``TaskSeq-skipWhileInclusive+A has no effect`` variant = task { + do! + Gen.getEmptyVariant variant + |> TaskSeq.skipWhileInclusive ((=) 12) + |> verifyEmpty + + do! + Gen.getEmptyVariant variant + |> TaskSeq.skipWhileInclusiveAsync ((=) 12 >> Task.fromResult) + |> verifyEmpty + } + +module Immutable = + + // TaskSeq-skipWhile+A stands for: + // skipWhile + skipWhileAsync etc. + + [)>] + let ``TaskSeq-skipWhile+A filters correctly`` variant = task { + // truth table for f(x) = x < 5 + // 1 2 3 4 5 6 7 8 9 10 + // T T T T F F F F F F (stops at first F) + // x x x x _ _ _ _ _ _ (skips exclusive) + // A B C D E F G H I J + + do! + Gen.getSeqImmutable variant + |> TaskSeq.skipWhile (fun x -> x < 5) + |> verifyDigitsAsString "EFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.skipWhileAsync (fun x -> task { return x < 5 }) + |> verifyDigitsAsString "EFGHIJ" + } + + [)>] + let ``TaskSeq-skipWhile+A does not skip first item when false`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.skipWhile ((=) 0) + |> verifyDigitsAsString "ABCDEFGHIJ" // all 10 remain! + + do! + Gen.getSeqImmutable variant + |> TaskSeq.skipWhileAsync ((=) 0 >> Task.fromResult) + |> verifyDigitsAsString "ABCDEFGHIJ" // all 10 remain! + } + + [)>] + let ``TaskSeq-skipWhileInclusive+A filters correctly`` variant = task { + // truth table for f(x) = x < 5 + // 1 2 3 4 5 6 7 8 9 10 + // T T T T F F F F F F (stops at first F) + // x x x x x _ _ _ _ _ (skips inclusively) + // A B C D E F G H I J + + do! + Gen.getSeqImmutable variant + |> TaskSeq.skipWhileInclusive (fun x -> x < 5) + |> verifyDigitsAsString "FGHIJ" // last 4 + + do! + Gen.getSeqImmutable variant + |> TaskSeq.skipWhileInclusiveAsync (fun x -> task { return x < 5 }) + |> verifyDigitsAsString "FGHIJ" + } + + + [)>] + let ``TaskSeq-skipWhileInclusive+A returns the empty sequence if always true`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.skipWhileInclusive (fun x -> x > -1) // always true + |> verifyEmpty + + do! + Gen.getSeqImmutable variant + |> TaskSeq.skipWhileInclusiveAsync (fun x -> Task.fromResult (x > -1)) + |> verifyEmpty + } + + [)>] + let ``TaskSeq-skipWhileInclusive+A always skips at least the first item`` variant = task { + do! + Gen.getSeqImmutable variant + |> TaskSeq.skipWhileInclusive ((=) 0) + |> verifyDigitsAsString "BCDEFGHIJ" + + do! + Gen.getSeqImmutable variant + |> TaskSeq.skipWhileInclusiveAsync ((=) 0 >> Task.fromResult) + |> verifyDigitsAsString "BCDEFGHIJ" + } + +module SideEffects = + [)>] + let ``TaskSeq-skipWhile+A filters correctly`` variant = task { + // truth table for f(x) = x < 6 + // 1 2 3 4 5 6 7 8 9 10 + // T T T T T F F F F F (stops at first F) + // x x x x x _ _ _ _ _ (skips exclusively) + // A B C D E F G H I J + + do! + Gen.getSeqWithSideEffect variant + |> TaskSeq.skipWhile (fun x -> x < 6) + |> verifyDigitsAsString "FGHIJ" + + do! + Gen.getSeqWithSideEffect variant + |> TaskSeq.skipWhileAsync (fun x -> task { return x < 6 }) + |> verifyDigitsAsString "FGHIJ" + } + + [)>] + let ``TaskSeq-skipWhileInclusive+A filters correctly`` variant = task { + // truth table for f(x) = x < 6 + // 1 2 3 4 5 6 7 8 9 10 + // T T T T T F F F F F (stops at first F) + // x x x x x x _ _ _ _ (skips inclusively) + // A B C D E F G H I J + + do! + Gen.getSeqWithSideEffect variant + |> TaskSeq.skipWhileInclusive (fun x -> x < 6) + |> verifyDigitsAsString "GHIJ" + + do! + Gen.getSeqWithSideEffect variant + |> TaskSeq.skipWhileInclusiveAsync (fun x -> task { return x < 6 }) + |> verifyDigitsAsString "GHIJ" + } + + [] + let ``TaskSeq-skipWhile and variants prove it reads the entire input stream`` () = + + let mutable x = 42 + + let items = taskSeq { + yield x + yield x * 2 + x <- x + 1 // we are proving we ALWAYS get here + } + + // this needs to be lifted from the task or it raises the infamous + // warning FS3511 on CI: This state machine is not statically compilable + let testSkipper skipper expected = task { + let! first = items |> skipper |> TaskSeq.toArrayAsync + return first |> should equal expected + } + + task { + do! testSkipper (TaskSeq.skipWhile ((=) 42)) [| 84 |] + x |> should equal 43 + + do! testSkipper (TaskSeq.skipWhileInclusive ((=) 43)) [||] + x |> should equal 44 + + do! testSkipper (TaskSeq.skipWhileAsync (fun x -> Task.fromResult (x = 44))) [| 88 |] + x |> should equal 45 + + do! testSkipper (TaskSeq.skipWhileInclusiveAsync (fun x -> Task.fromResult (x = 45))) [||] + x |> should equal 46 + } + + [] + let ``TaskSeq-skipWhile and variants prove side effects are properly executed`` () = + let mutable x = 41 + + let items = taskSeq { + x <- x + 1 + yield x + x <- x + 2 + yield x * 2 + x <- x + 200 // as previously proven, we should ALWAYS trigger this + } + + // this needs to be lifted from the task or it raises the infamous + // warning FS3511 on CI: This state machine is not statically compilable + let testSkipper skipper expected = task { + let! first = items |> skipper |> TaskSeq.toArrayAsync + return first |> should equal expected + } + + task { + do! testSkipper (TaskSeq.skipWhile ((=) 42)) [| 88 |] + x |> should equal 244 + + do! testSkipper (TaskSeq.skipWhileInclusive ((=) 245)) [||] + x |> should equal 447 + + do! testSkipper (TaskSeq.skipWhileAsync (fun x -> Task.fromResult (x = 448))) [| 900 |] + x |> should equal 650 + + do! testSkipper (TaskSeq.skipWhileInclusiveAsync (fun x -> Task.fromResult (x = 651))) [||] + x |> should equal 853 + } + + [)>] + let ``TaskSeq-skipWhile consumes the prefix of a longer sequence, with mutation`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + + let! first = + TaskSeq.skipWhile (fun x -> x < 5) ts + |> TaskSeq.toArrayAsync + + let expected = [| 5..10 |] + first |> should equal expected + + // side effect, reiterating causes it to resume from where we left it (minus the failing item) + // which means the original sequence has now changed due to the side effect + let! repeat = + TaskSeq.skipWhile (fun x -> x < 5) ts + |> TaskSeq.toArrayAsync + + repeat |> should not' (equal expected) + } + + [)>] + let ``TaskSeq-skipWhileInclusiveAsync consumes the prefix for a longer sequence, with mutation`` variant = task { + let ts = Gen.getSeqWithSideEffect variant + + let! first = + TaskSeq.skipWhileInclusiveAsync (fun x -> task { return x < 5 }) ts + |> TaskSeq.toArrayAsync + + let expected = [| 6..10 |] + first |> should equal expected + + // side effect, reiterating causes it to resume from where we left it (minus the failing item) + // which means the original sequence has now changed due to the side effect + let! repeat = + TaskSeq.skipWhileInclusiveAsync (fun x -> task { return x < 5 }) ts + |> TaskSeq.toArrayAsync + + repeat |> should not' (equal expected) + } + +module Other = + [] + let ``TaskSeq-skipWhileXXX should include all items after predicate fails`` () = task { + do! + [ 1; 2; 2; 3; 3; 2; 1 ] + |> TaskSeq.ofSeq + |> TaskSeq.skipWhile (fun x -> x <= 2) + |> verifyDigitsAsString "CCBA" + + do! + [ 1; 2; 2; 3; 3; 2; 1 ] + |> TaskSeq.ofSeq + |> TaskSeq.skipWhileInclusive (fun x -> x <= 2) + |> verifyDigitsAsString "CBA" + + do! + [ 1; 2; 2; 3; 3; 2; 1 ] + |> TaskSeq.ofSeq + |> TaskSeq.skipWhileAsync (fun x -> Task.fromResult (x <= 2)) + |> verifyDigitsAsString "CCBA" + + do! + [ 1; 2; 2; 3; 3; 2; 1 ] + |> TaskSeq.ofSeq + |> TaskSeq.skipWhileInclusiveAsync (fun x -> Task.fromResult (x <= 2)) + |> verifyDigitsAsString "CBA" + } + + [] + let ``TaskSeq-skipWhileXXX stops consuming after predicate fails`` () = + let testSkipper skipper = + fun () -> + seq { + yield! [ 1; 2; 2; 3; 3 ] + yield SideEffectPastEnd "Too far" |> raise + } + |> TaskSeq.ofSeq + |> skipper + |> consumeTaskSeq + |> should throwAsyncExact typeof + + testSkipper (TaskSeq.skipWhile (fun x -> x <= 2)) + testSkipper (TaskSeq.skipWhileInclusive (fun x -> x <= 2)) + testSkipper (TaskSeq.skipWhileAsync (fun x -> Task.fromResult (x <= 2))) + testSkipper (TaskSeq.skipWhileInclusiveAsync (fun x -> Task.fromResult (x <= 2))) diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs index a68e29ea..81fc3bf4 100644 --- a/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.TakeWhile.Tests.fs @@ -39,6 +39,10 @@ module With = res module EmptySeq = + + // TaskSeq-takeWhile+A stands for: + // takeWhile + takeWhileAsync etc. + [)>] let ``TaskSeq-takeWhile+A has no effect`` variant = task { do! @@ -124,23 +128,37 @@ module Immutable = module SideEffects = [)>] - let ``TaskSeq-takeWhile filters correctly`` variant = - Gen.getSeqWithSideEffect variant - |> TaskSeq.takeWhile condWithGuard - |> verifyDigitsAsString "ABCDE" + let ``TaskSeq-takeWhile+A filters correctly`` variant = task { + do! + Gen.getSeqWithSideEffect variant + |> TaskSeq.takeWhile condWithGuard + |> verifyDigitsAsString "ABCDE" + + do! + Gen.getSeqWithSideEffect variant + |> TaskSeq.takeWhileAsync (fun x -> task { return condWithGuard x }) + |> verifyDigitsAsString "ABCDE" + } [)>] - let ``TaskSeq-takeWhileAsync filters correctly`` variant = - Gen.getSeqWithSideEffect variant - |> TaskSeq.takeWhileAsync (fun x -> task { return condWithGuard x }) - |> verifyDigitsAsString "ABCDE" + let ``TaskSeq-takeWhileInclusive+A filters correctly`` variant = task { + do! + Gen.getSeqWithSideEffect variant + |> TaskSeq.takeWhileInclusive condWithGuard + |> verifyDigitsAsString "ABCDEF" + + do! + Gen.getSeqWithSideEffect variant + |> TaskSeq.takeWhileInclusiveAsync (fun x -> task { return condWithGuard x }) + |> verifyDigitsAsString "ABCDEF" + } [] [] [] [] [] - let ``TaskSeq-takeWhileXXX prove it does not read beyond the failing yield`` (inclusive, isAsync) = task { + let ``TaskSeq-takeWhile and variants prove it does not read beyond the failing yield`` (inclusive, isAsync) = task { let mutable x = 42 // for this test, the potential mutation should not actually occur let functionToTest = getFunction inclusive isAsync ((=) 42) @@ -165,7 +183,7 @@ module SideEffects = [] [] [] - let ``TaskSeq-takeWhileXXX prove side effects are executed`` (inclusive, isAsync) = task { + let ``TaskSeq-takeWhile and variants prove side effects are executed`` (inclusive, isAsync) = task { let mutable x = 41 let functionToTest = getFunction inclusive isAsync ((>) 50) @@ -180,6 +198,7 @@ module SideEffects = let expectedFirst = if inclusive then [| 42; 44 * 2 |] else [| 42 |] let expectedRepeat = if inclusive then [| 45; 47 * 2 |] else [| 45 |] + x |> should equal 41 let! first = items |> functionToTest |> TaskSeq.toArrayAsync x |> should equal 44 let! repeat = items |> functionToTest |> TaskSeq.toArrayAsync @@ -201,6 +220,7 @@ module SideEffects = first |> should equal expected // side effect, reiterating causes it to resume from where we left it (minus the failing item) + // which means the original sequence has now changed due to the side effect let! repeat = TaskSeq.takeWhile (fun x -> x < 5) ts |> TaskSeq.toArrayAsync @@ -220,6 +240,7 @@ module SideEffects = first |> should equal expected // side effect, reiterating causes it to resume from where we left it (minus the failing item) + // which means the original sequence has now changed due to the side effect let! repeat = TaskSeq.takeWhileInclusiveAsync (fun x -> task { return x < 5 }) ts |> TaskSeq.toArrayAsync @@ -233,7 +254,7 @@ module Other = [] [] [] - let ``TaskSeq-takeWhileXXX exclude all items after predicate fails`` (inclusive, isAsync) = + let ``TaskSeq-takeWhile and variants excludes all items after predicate fails`` (inclusive, isAsync) = let functionToTest = With.getFunction inclusive isAsync [ 1; 2; 2; 3; 3; 2; 1 ] @@ -246,7 +267,7 @@ module Other = [] [] [] - let ``TaskSeq-takeWhileXXX stops consuming after predicate fails`` (inclusive, isAsync) = + let ``TaskSeq-takeWhile and variants stops consuming after predicate fails`` (inclusive, isAsync) = let functionToTest = With.getFunction inclusive isAsync seq { diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index be9961f7..d3aedcb9 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -297,6 +297,10 @@ type TaskSeq private () = static member takeWhileAsync predicate source = Internal.takeWhile Exclusive (PredicateAsync predicate) source static member takeWhileInclusive predicate source = Internal.takeWhile Inclusive (Predicate predicate) source static member takeWhileInclusiveAsync predicate source = Internal.takeWhile Inclusive (PredicateAsync predicate) source + static member skipWhile predicate source = Internal.skipWhile Exclusive (Predicate predicate) source + static member skipWhileAsync predicate source = Internal.skipWhile Exclusive (PredicateAsync predicate) source + static member skipWhileInclusive predicate source = Internal.skipWhile Inclusive (Predicate predicate) source + static member skipWhileInclusiveAsync predicate source = Internal.skipWhile Inclusive (PredicateAsync predicate) source static member tryPick chooser source = Internal.tryPick (TryPick chooser) source static member tryPickAsync chooser source = Internal.tryPick (TryPickAsync chooser) source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index 68b8d4e2..c34895d2 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -830,10 +830,10 @@ type TaskSeq = static member takeWhile: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T> /// - /// Returns a sequence that, when iterated, yields elements of the underlying sequence while the + /// Returns a task sequence that, when iterated, yields elements of the underlying sequence while the /// given asynchronous function returns , and then returns no further elements. /// The first element where the predicate returns is not included in the resulting sequence - /// (see also ). + /// (see also ). /// If is synchronous, consider using . /// /// @@ -844,7 +844,7 @@ type TaskSeq = static member takeWhileAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> TaskSeq<'T> /// - /// Returns a sequence that, when iterated, yields elements of the underlying sequence until the given + /// Returns a task sequence that, when iterated, yields elements of the underlying sequence until the given /// function returns , returns that element /// and then returns no further elements (see also ). This function returns /// at least one element of a non-empty sequence, or the empty task sequence if the input is empty. @@ -858,9 +858,9 @@ type TaskSeq = static member takeWhileInclusive: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T> /// - /// Returns a sequence that, when iterated, yields elements of the underlying sequence until the given + /// Returns a task sequence that, when iterated, yields elements of the underlying sequence until the given /// asynchronous function returns , returns that element - /// and then returns no further elements (see also ). This function returns + /// and then returns no further elements (see also ). This function returns /// at least one element of a non-empty sequence, or the empty task sequence if the input is empty. /// If is synchronous, consider using . /// @@ -871,6 +871,62 @@ type TaskSeq = /// Thrown when the input task sequence is null. static member takeWhileInclusiveAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> TaskSeq<'T> + /// + /// Returns a task sequence that, when iterated, skips elements of the underlying sequence while the + /// given function returns , and then yields the remaining + /// elements. The first element where the predicate returns is returned, which means that this + /// function will skip 0 or more elements (see also ). + /// If is asynchronous, consider using . + /// + /// + /// A function that evaluates to false when no more items should be skipped. + /// The input task sequence. + /// The resulting task sequence. + /// Thrown when the input task sequence is null. + static member skipWhile: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Returns a task sequence that, when iterated, skips elements of the underlying sequence while the + /// given asynchronous function returns , and then yields the + /// remaining elements. The first element where the predicate returns is returned, which + /// means that this function will skip 0 or more elements (see also ). + /// If is synchronous, consider using . + /// + /// + /// An asynchronous function that evaluates to false when no more items should be skipped. + /// The input task sequence. + /// The resulting task sequence. + /// Thrown when the input task sequence is null. + static member skipWhileAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Returns a task sequence that, when iterated, skips elements of the underlying sequence until the given + /// function returns , also skips that element + /// and then yields the remaining elements (see also ). This function skips + /// at least one element of a non-empty sequence, or returns the empty task sequence if the input is empty. + /// If is asynchronous, consider using . + /// ` + /// + /// A function that evaluates to false when no more items should be skipped. + /// The input task sequence. + /// The resulting task sequence. + /// Thrown when the input task sequence is null. + static member skipWhileInclusive: predicate: ('T -> bool) -> source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Returns a task sequence that, when iterated, skips elements of the underlying sequence until the given + /// function returns , also skips that element + /// and then yields the remaining elements (see also ). This function skips + /// at least one element of a non-empty sequence, or returns the empty task sequence if the input is empty. + /// If is synchronous, consider using . + /// + /// + /// An asynchronous function that evaluates to false when no more items should be skipped. + /// The input task sequence. + /// The resulting task sequence. + /// Thrown when the input task sequence is null. + static member skipWhileInclusiveAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> TaskSeq<'T> + /// /// Applies the given function to successive elements, returning the first result where /// the function returns . diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index d6a33421..e59d50c3 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -13,9 +13,9 @@ type internal AsyncEnumStatus = [] type internal WhileKind = - /// The item under test is included even if false + /// The item under test is included (or skipped) even when the predicate returns false | Inclusive - /// The item under test is always excluded + /// The item under test is always excluded (or not skipped) | Exclusive [] @@ -648,9 +648,9 @@ module internal TaskSeqInternal = use e = source.GetAsyncEnumerator CancellationToken.None for _ in 1..count do - let! ok = e.MoveNextAsync() + let! hasMore = e.MoveNextAsync() - if not ok then + if not hasMore then raiseInsufficient () while! e.MoveNextAsync() do @@ -731,53 +731,135 @@ module internal TaskSeqInternal = taskSeq { use e = source.GetAsyncEnumerator CancellationToken.None - let! step = e.MoveNextAsync() - let mutable more = step + let! notEmpty = e.MoveNextAsync() + let mutable more = notEmpty match whileKind, predicate with - | Exclusive, Predicate predicate -> + | Exclusive, Predicate predicate -> // takeWhile while more do let value = e.Current more <- predicate value if more then + // yield ONLY if predicate is true yield value - let! ok = e.MoveNextAsync() - more <- ok + let! hasMore = e.MoveNextAsync() + more <- hasMore - | Inclusive, Predicate predicate -> + | Inclusive, Predicate predicate -> // takeWhileInclusive while more do let value = e.Current more <- predicate value + // yield regardless of result of predicate yield value if more then - let! ok = e.MoveNextAsync() - more <- ok + let! hasMore = e.MoveNextAsync() + more <- hasMore - | Exclusive, PredicateAsync predicate -> + | Exclusive, PredicateAsync predicate -> // takeWhileAsync while more do let value = e.Current let! passed = predicate value more <- passed if more then + // yield ONLY if predicate is true yield value - let! ok = e.MoveNextAsync() - more <- ok + let! hasMore = e.MoveNextAsync() + more <- hasMore - | Inclusive, PredicateAsync predicate -> + | Inclusive, PredicateAsync predicate -> // takeWhileInclusiveAsync while more do let value = e.Current let! passed = predicate value more <- passed + // yield regardless of predicate yield value if more then - let! ok = e.MoveNextAsync() - more <- ok + let! hasMore = e.MoveNextAsync() + more <- hasMore + } + + let skipWhile whileKind predicate (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + taskSeq { + use e = source.GetAsyncEnumerator CancellationToken.None + let! moveFirst = e.MoveNextAsync() + let mutable more = moveFirst + + match whileKind, predicate with + | Exclusive, Predicate predicate -> // skipWhile + while more && predicate e.Current do + let! hasMore = e.MoveNextAsync() + more <- hasMore + + if more then + // yield the last one where the predicate was false + // (this ensures we skip 0 or more) + yield e.Current + + while! e.MoveNextAsync() do // get the rest + yield e.Current + + | Inclusive, Predicate predicate -> // skipWhileInclusive + while more && predicate e.Current do + let! hasMore = e.MoveNextAsync() + more <- hasMore + + if more then + // yield the rest (this ensures we skip 1 or more) + while! e.MoveNextAsync() do + yield e.Current + + | Exclusive, PredicateAsync predicate -> // skipWhileAsync + let mutable cont = true + + if more then + let! hasMore = predicate e.Current + cont <- hasMore + + while more && cont do + let! moveNext = e.MoveNextAsync() + + if moveNext then + let! hasMore = predicate e.Current + cont <- hasMore + + more <- moveNext + + if more then + // yield the last one where the predicate was false + // (this ensures we skip 0 or more) + yield e.Current + + while! e.MoveNextAsync() do // get the rest + yield e.Current + + | Inclusive, PredicateAsync predicate -> // skipWhileInclusiveAsync + let mutable cont = true + + if more then + let! hasMore = predicate e.Current + cont <- hasMore + + while more && cont do + let! moveNext = e.MoveNextAsync() + + if moveNext then + let! hasMore = predicate e.Current + cont <- hasMore + + more <- moveNext + + if more then + // get the rest, this gives 1 or more semantics + while! e.MoveNextAsync() do + yield e.Current } // Consider turning using an F# version of this instead?