From 74bc1a602803975a177fcc944da5c1adaba69328 Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Fri, 20 Mar 2026 14:16:36 -0400 Subject: [PATCH 1/9] Add the split, takeWhileIncluding, and skipUntilIncluding functions to all the other modules --- ReleaseNotes.md | 6 +- SafetyFirst.Specs/ArraySpec.fs | 112 ++++++++++++++++++- SafetyFirst.Specs/FSeqSpec.fs | 107 +++++++++++++++++- SafetyFirst.Specs/ListSpec.fs | 116 ++++++++++++++++++- SafetyFirst.Specs/SeqSpec.fs | 196 ++++++++++++++++++++++++++++++--- SafetyFirst/Array.fs | 60 +++++++++- SafetyFirst/FiniteSeqModule.fs | 127 +++++++++++++++++---- SafetyFirst/List.fs | 61 +++++++++- SafetyFirst/Seq.fs | 116 +++++++++++++------ 9 files changed, 813 insertions(+), 88 deletions(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 5d48541..1da5601 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -2,7 +2,11 @@ ## New features: -Adds a `splitPairwise` function for the List/Array/Seq/FSeq modules. +Adds new functions for the List/Array/Seq/FSeq modules: +- `splitPairwise` +- `split` +- `takeWhileIncluding` +- `skipUntilIncluding` ### InfiniteSeq InfiniteSeq has been reworked. It is now iterable as a regular sequence. When dealing with infinite sequences, a hang should not be considered a recoverable error with programmatic mitigation (other than possibly with a global exception handler), rather it should be considered a bug needing a fix. Therefore, InfiniteSeq is no longer designed to return a Result in the event of a hang - it's meant to throw an exception instead. Functions like `InfiniteSeq.item` now either crash for a hang or return the item without Result. Existing Result-returning functions like `item'` or Option-returning functions like `tryItem` still exist but are marked deprecated, and will be removed in version 6.0. If you still need the functionality to programmatically recover from a hang, then switch to a `try ... with :? InfiniteSequenceEvaluationHung ->` block. diff --git a/SafetyFirst.Specs/ArraySpec.fs b/SafetyFirst.Specs/ArraySpec.fs index fd66f4f..aca9c48 100644 --- a/SafetyFirst.Specs/ArraySpec.fs +++ b/SafetyFirst.Specs/ArraySpec.fs @@ -122,7 +122,6 @@ module Splitting = let toArrs xs = Seq.map Array.NonEmpty.toArray xs |> Array.ofSeq [] - let ``returns what the documentation says`` () = test @@ -172,9 +171,9 @@ module Splitting = @> [] - let ``splits pairwise properly for multiple types of inputs`` () = + let ``splits pairwise properly for multiple types of inputs`` () = let bigDiff i j = abs (i - j) > 5 - test + test <@ (Array.NonEmpty.splitPairwise (=) (Array.NonEmpty.singleton 0) |> toArrs) = [|[|0|]|] && @@ -187,4 +186,109 @@ module Splitting = && (Array.NonEmpty.splitPairwise (bigDiff) (Array.NonEmpty.create 1 [|2;12;13;23|]) |> toArrs) = [|[|1;2|]; [|12;13|]; [|23|]|] - @> \ No newline at end of file + @> + + [] + let ``split returns what the documentation says`` () = + test + <@ + (Array.split ((=) 100) [|1;2;3;100;100;4;100;5;6|] |> toArrs) + = [|[|1;2;3;100|];[|100|];[|4;100|];[|5;6|]|] + @> + + [] + let ``split handles empty and single element arrays`` () = + test + <@ + (Array.split ((=) 5) [||] |> toArrs) = [||] + && + (Array.split ((=) 5) [|0|] |> toArrs) = [|[|0|]|] + && + (Array.split ((=) 5) [|5|] |> toArrs) = [|[|5|]|] + && + (Array.split ((=) 5) [|5;5|] |> toArrs) = [|[|5|]; [|5|]|] + @> + + [] + let ``split splits properly for multiple types of inputs`` () = + test + <@ + (Array.split ((=) 5) [|0|] |> toArrs) = [|[|0|]|] + && + (Array.split ((=) 5) [|5|] |> toArrs) = [|[|5|]|] + && + (Array.split ((=) 5) [|0;5|] |> toArrs) = [|[|0; 5|]|] + && + (Array.split ((=) 5) [|5;5|] |> toArrs) = [|[|5|]; [|5|]|] + && + (Array.split ((=) 5) [|5;0|] |> toArrs) = [|[|5|]; [|0|]|] + && + (Array.split ((=) 5) [|5;0;0;5;5;0;5|] |> toArrs) = [|[|5|]; [|0;0;5|]; [|5|]; [|0;5|]|] + @> + +module TakeWhileIncluding = + [] + let ``NonEmpty.takeWhileIncluding returns through the first matching element`` () = + test + <@ + Array.NonEmpty.takeWhileIncluding ((=) 3) (Array.NonEmpty.create 1 [|2;3;4;5|]) + = Array.NonEmpty.create 1 [|2;3|] + && + Array.NonEmpty.takeWhileIncluding ((=) 1) (Array.NonEmpty.create 1 [|2;3;4;5|]) + = Array.NonEmpty.singleton 1 + && + Array.NonEmpty.takeWhileIncluding ((=) 99) (Array.NonEmpty.create 1 [|2;3|]) + = Array.NonEmpty.create 1 [|2;3|] + @> + + [] + let ``returns empty for empty input`` () = + test <@ Array.takeWhileIncluding (fun _ -> true) [||] = [||] @> + + [] + let ``returns through the first matching element`` () = + test <@ Array.takeWhileIncluding ((=) 3) [|1;2;3;4;5|] = [|1;2;3|] @> + + [] + let ``returns only the first element when it matches`` () = + test <@ Array.takeWhileIncluding ((=) 3) [|3;4;5|] = [|3|] @> + + [] + let ``stops at the first match even when multiple elements match`` () = + test <@ Array.takeWhileIncluding ((=) 3) [|1;3;3;3|] = [|1;3|] @> + + [] + let ``returns the full array when no element matches`` () = + test <@ Array.takeWhileIncluding ((=) 99) [|1;2;3|] = [|1;2;3|] @> + +module SkipUntilIncluding = + [] + let ``returns empty for empty input`` () = + test <@ Array.skipUntilIncluding (fun _ -> true) [||] = [||] @> + + [] + let ``returns elements after the first matching element`` () = + test <@ Array.skipUntilIncluding ((=) 3) [|1;2;3;4;5|] = [|4;5|] @> + + [] + let ``returns elements after the first element when it matches`` () = + test <@ Array.skipUntilIncluding ((=) 3) [|3;4;5|] = [|4;5|] @> + + [] + let ``stops skipping at the first match even when multiple elements match`` () = + test <@ Array.skipUntilIncluding ((=) 3) [|1;3;3;3|] = [|3;3|] @> + + [] + let ``returns empty when the match is the last element`` () = + test <@ Array.skipUntilIncluding ((=) 3) [|1;2;3|] = [||] @> + + [] + let ``returns empty when no element matches`` () = + test <@ Array.skipUntilIncluding ((=) 99) [|1;2;3|] = [||] @> + + [] + let ``takeWhileIncluding and skipUntilIncluding partition the array`` () = + let xs = [|1;2;3;4;5|] + let taken = Array.takeWhileIncluding ((=) 3) xs + let skipped = Array.skipUntilIncluding ((=) 3) xs + test <@ Array.append taken skipped = xs @> \ No newline at end of file diff --git a/SafetyFirst.Specs/FSeqSpec.fs b/SafetyFirst.Specs/FSeqSpec.fs index 3ac630b..db660c2 100644 --- a/SafetyFirst.Specs/FSeqSpec.fs +++ b/SafetyFirst.Specs/FSeqSpec.fs @@ -240,7 +240,7 @@ module Splitting = List.ofSeq <| Seq.map FSeq.NonEmpty.toList xs let toNonEmpty xs = - FSeq.NonEmpty.ofFSeq' (fseq xs) |> Result.expect + NonEmpty.assume (fseq xs) [] let ``returns what the documentation says`` () = @@ -352,6 +352,111 @@ module Splitting = && Seq.toList neSegments.[1] = [1;2;3;4] @> + [] + let ``split returns what the documentation says`` () = + test + <@ + (FSeq.split ((=) 100) (fseq [1;2;3;100;100;4;100;5;6]) |> ofNonEmpty) + = [[1;2;3;100];[100];[4;100];[5;6]] + @> + + [] + let ``split handles empty and single element sequences`` () = + test + <@ + (FSeq.split ((=) 5) (fseq []) |> ofNonEmpty) = [] + && + (FSeq.split ((=) 5) (fseq [0]) |> ofNonEmpty) = [[0]] + && + (FSeq.split ((=) 5) (fseq [5]) |> ofNonEmpty) = [[5]] + && + (FSeq.split ((=) 5) (fseq [5;5]) |> ofNonEmpty) = [[5]; [5]] + @> + + [] + let ``split splits properly for multiple types of inputs`` () = + test + <@ + (FSeq.split ((=) 5) (fseq [0]) |> ofNonEmpty) = [[0]] + && + (FSeq.split ((=) 5) (fseq [5]) |> ofNonEmpty) = [[5]] + && + (FSeq.split ((=) 5) (fseq [0;5]) |> ofNonEmpty) = [[0; 5]] + && + (FSeq.split ((=) 5) (fseq [5;5]) |> ofNonEmpty) = [[5]; [5]] + && + (FSeq.split ((=) 5) (fseq [5;0]) |> ofNonEmpty) = [[5]; [0]] + && + (FSeq.split ((=) 5) (fseq [5;0;0;5;5;0;5]) |> ofNonEmpty) = [[5]; [0;0;5]; [5]; [0;5]] + @> + +module TakeWhileIncluding = + [] + let ``NonEmpty.takeWhileIncluding returns through the first matching element`` () = + test + <@ + FSeq.NonEmpty.takeWhileIncluding ((=) 3) (Splitting.toNonEmpty [1;2;3;4;5]) + = Splitting.toNonEmpty [1;2;3] + && + FSeq.NonEmpty.takeWhileIncluding ((=) 1) (Splitting.toNonEmpty [1;2;3;4;5]) + = Splitting.toNonEmpty [1] + && + FSeq.NonEmpty.takeWhileIncluding ((=) 99) (Splitting.toNonEmpty [1;2;3]) + = Splitting.toNonEmpty [1;2;3] + @> + + [] + let ``returns empty for empty input`` () = + test <@ FSeq.takeWhileIncluding (fun _ -> true) (fseq []) = fseq [] @> + + [] + let ``returns through the first matching element`` () = + test <@ FSeq.takeWhileIncluding ((=) 3) (fseq [1;2;3;4;5]) = fseq [1;2;3] @> + + [] + let ``returns only the first element when it matches`` () = + test <@ FSeq.takeWhileIncluding ((=) 3) (fseq [3;4;5]) = fseq [3] @> + + [] + let ``stops at the first match even when multiple elements match`` () = + test <@ FSeq.takeWhileIncluding ((=) 3) (fseq [1;3;3;3]) = fseq [1;3] @> + + [] + let ``returns the full sequence when no element matches`` () = + test <@ FSeq.takeWhileIncluding ((=) 99) (fseq [1;2;3]) = fseq [1;2;3] @> + +module SkipUntilIncluding = + [] + let ``returns empty for empty input`` () = + test <@ FSeq.skipUntilIncluding (fun _ -> true) (fseq []) = fseq [] @> + + [] + let ``returns elements after the first matching element`` () = + test <@ FSeq.skipUntilIncluding ((=) 3) (fseq [1;2;3;4;5]) = fseq [4;5] @> + + [] + let ``returns elements after the first element when it matches`` () = + test <@ FSeq.skipUntilIncluding ((=) 3) (fseq [3;4;5]) = fseq [4;5] @> + + [] + let ``stops skipping at the first match even when multiple elements match`` () = + test <@ FSeq.skipUntilIncluding ((=) 3) (fseq [1;3;3;3]) = fseq [3;3] @> + + [] + let ``returns empty when the match is the last element`` () = + test <@ FSeq.skipUntilIncluding ((=) 3) (fseq [1;2;3]) = fseq [] @> + + [] + let ``returns empty when no element matches`` () = + test <@ FSeq.skipUntilIncluding ((=) 99) (fseq [1;2;3]) = fseq [] @> + + [] + let ``takeWhileIncluding and skipUntilIncluding partition the sequence`` () = + let xs = fseq [1;2;3;4;5] + let taken = FSeq.takeWhileIncluding ((=) 3) xs + let skipped = FSeq.skipUntilIncluding ((=) 3) xs + test <@ FSeq.append taken skipped = xs @> + module SafeFunctions = open SeqSpec diff --git a/SafetyFirst.Specs/ListSpec.fs b/SafetyFirst.Specs/ListSpec.fs index 7dca2ae..305a8ab 100644 --- a/SafetyFirst.Specs/ListSpec.fs +++ b/SafetyFirst.Specs/ListSpec.fs @@ -150,7 +150,6 @@ module Splitting = = [[0;1];[1;2;3;4];[4];[4;5]] @> - [] let ``splits empty and single element sequences`` () = test @@ -213,4 +212,117 @@ module Splitting = && (List.NonEmpty.splitPairwise splitOnDescent (List.NonEmpty.create 5 [3;1;2;4]) |> toLists) = [[5]; [3]; [1;2;4]] - @> \ No newline at end of file + @> + + [] + let ``split returns what the documentation says`` () = + test + <@ + (List.split ((=) 100) [1;2;3;100;100;4;100;5;6] |> toLists) + = [[1;2;3;100];[100];[4;100];[5;6]] + @> + + [] + let ``works when the start and end elements match the predicate`` () = + test + <@ + (List.split ((=) 100) [100; 100; 1; 2; 3; 100] |> toLists) + = [ [100]; [100]; [1;2;3;100] ] + @> + + [] + let ``split handles empty and single element lists`` () = + test + <@ + (List.split ((=) 5) [] |> toLists) = [] + && + (List.split ((=) 5) [0] |> toLists) = [[0]] + && + (List.split ((=) 5) [5] |> toLists) = [[5]] + && + (List.split ((=) 5) [5;5] |> toLists) = [[5]; [5]] + @> + + [] + let ``split splits properly for multiple types of inputs`` () = + test + <@ + (List.split ((=) 5) [0] |> toLists) = [[0]] + && + (List.split ((=) 5) [5] |> toLists) = [[5]] + && + (List.split ((=) 5) [0;5] |> toLists) = [[0; 5]] + && + (List.split ((=) 5) [5;5] |> toLists) = [[5]; [5]] + && + (List.split ((=) 5) [5;0] |> toLists) = [[5]; [0]] + && + (List.split ((=) 5) [5;0;0;5;5;0;5] |> toLists) = [[5]; [0;0;5]; [5]; [0;5]] + @> + + [] + let ``NonEmpty.takeWhileIncluding returns through the first matching element`` () = + test + <@ + List.NonEmpty.takeWhileIncluding ((=) 3) (List.NonEmpty.create 1 [2;3;4;5]) + = List.NonEmpty.create 1 [2;3] + && + List.NonEmpty.takeWhileIncluding ((=) 1) (List.NonEmpty.create 1 [2;3;4;5]) + = List.NonEmpty.singleton 1 + && + List.NonEmpty.takeWhileIncluding ((=) 99) (List.NonEmpty.create 1 [2;3]) + = List.NonEmpty.create 1 [2;3] + @> + +module TakeWhileIncluding = + [] + let ``returns empty for empty input`` () = + test <@ List.takeWhileIncluding (fun _ -> true) [] = [] @> + + [] + let ``returns through the first matching element`` () = + test <@ List.takeWhileIncluding ((=) 3) [1;2;3;4;5] = [1;2;3] @> + + [] + let ``returns only the first element when it matches`` () = + test <@ List.takeWhileIncluding ((=) 3) [3;4;5] = [3] @> + + [] + let ``stops at the first match even when multiple elements match`` () = + test <@ List.takeWhileIncluding ((=) 3) [1;3;3;3] = [1;3] @> + + [] + let ``returns the full list when no element matches`` () = + test <@ List.takeWhileIncluding ((=) 99) [1;2;3] = [1;2;3] @> + +module SkipUntilIncluding = + [] + let ``returns empty for empty input`` () = + test <@ List.skipUntilIncluding (fun _ -> true) [] = [] @> + + [] + let ``returns elements after the first matching element`` () = + test <@ List.skipUntilIncluding ((=) 3) [1;2;3;4;5] = [4;5] @> + + [] + let ``returns elements after the first element when it matches`` () = + test <@ List.skipUntilIncluding ((=) 3) [3;4;5] = [4;5] @> + + [] + let ``stops skipping at the first match even when multiple elements match`` () = + test <@ List.skipUntilIncluding ((=) 3) [1;3;3;3] = [3;3] @> + + [] + let ``returns empty when the match is the last element`` () = + test <@ List.skipUntilIncluding ((=) 3) [1;2;3] = [] @> + + [] + let ``returns empty when no element matches`` () = + test <@ List.skipUntilIncluding ((=) 99) [1;2;3] = [] @> + + [] + let ``takeWhileIncluding and skipUntilIncluding partition the list`` () = + let xs = [1;2;3;4;5] + let taken = List.takeWhileIncluding ((=) 3) xs + let skipped = List.skipUntilIncluding ((=) 3) xs + test <@ taken @ skipped = xs @> \ No newline at end of file diff --git a/SafetyFirst.Specs/SeqSpec.fs b/SafetyFirst.Specs/SeqSpec.fs index 3444b9e..3eb992e 100644 --- a/SafetyFirst.Specs/SeqSpec.fs +++ b/SafetyFirst.Specs/SeqSpec.fs @@ -200,20 +200,94 @@ let ``isHungAfter throws for finite sequences that exceed the limit`` () = raises <@ [1..11] |> Seq.isHungAfter 10 |> Seq.toList @> -module Splitting = - let toLists (xs:seq<#seq<_>>) = +module TakeWhileIncluding = + [] + let ``returns empty for empty input`` () = + test <@ Seq.takeWhileIncluding (fun _ -> true) Seq.empty |> Seq.toList = [] @> + + [] + let ``returns through the first matching element`` () = + test <@ Seq.takeWhileIncluding ((=) 3) [1;2;3;4;5] |> Seq.toList = [1;2;3] @> + + [] + let ``returns only the first element when it matches`` () = + test <@ Seq.takeWhileIncluding ((=) 3) [3;4;5] |> Seq.toList = [3] @> + + [] + let ``stops at the first match even when multiple elements match`` () = + test <@ Seq.takeWhileIncluding ((=) 3) [1;3;3;3] |> Seq.toList = [1;3] @> + + [] + let ``returns the full sequence when no element matches`` () = + test <@ Seq.takeWhileIncluding ((=) 99) [1;2;3] |> Seq.toList = [1;2;3] @> + + [] + let ``works with infinite sequences`` () = + // stops after finding the matching element rather than diverging + test <@ Seq.initInfinite id |> Seq.takeWhileIncluding ((=) 3) |> Seq.toList = [0;1;2;3] @> + + [] + let ``is lazy - does not evaluate past the matching element`` () = + let splitInfinite: seq<_> = InfiniteSeq.initBounded 3000 id + test <@ splitInfinite |> Seq.takeWhileIncluding ((=) 3) |> Seq.toList = [0;1;2;3] @> + +module SkipUntilIncluding = + [] + let ``returns empty for empty input`` () = + test <@ Seq.skipUntilIncluding (fun _ -> true) Seq.empty |> Seq.toList = [] @> + + [] + let ``returns elements after the first matching element`` () = + test <@ Seq.skipUntilIncluding ((=) 3) [1;2;3;4;5] |> Seq.toList = [4;5] @> + + [] + let ``returns elements after the first element when it matches`` () = + test <@ Seq.skipUntilIncluding ((=) 3) [3;4;5] |> Seq.toList = [4;5] @> + + [] + let ``stops skipping at the first match even when multiple elements match`` () = + test <@ Seq.skipUntilIncluding ((=) 3) [1;3;3;3] |> Seq.toList = [3;3] @> + + [] + let ``returns empty when the match is the last element`` () = + test <@ Seq.skipUntilIncluding ((=) 3) [1;2;3] |> Seq.toList = [] @> + + [] + let ``returns empty when no element matches`` () = + test <@ Seq.skipUntilIncluding ((=) 99) [1;2;3] |> Seq.toList = [] @> + + [] + let ``works with infinite sequences`` () = + // yields the infinite tail after the matching element + test <@ Seq.initInfinite id |> Seq.skipUntilIncluding ((=) 3) |> Seq.take 4 |> Seq.toList = [4;5;6;7] @> + + [] + let ``takeWhileIncluding and skipWhileIncluding partition the sequence`` () = + let xs = [1;2;3;4;5] + let taken = Seq.takeWhileIncluding ((=) 3) xs |> Seq.toList + let skipped = Seq.skipUntilIncluding ((=) 3) xs |> Seq.toList + test <@ taken @ skipped = xs @> + +module Splitting = + let toLists (xs:seq<#seq<_>>) = Seq.toList <| Seq.map Seq.toList xs [] let ``returns what the documentation says`` () = - test + test + <@ + (Seq.split ((=) 100) [1;2;3;100;100;4;100;5;6] |> toLists) + = [[1;2;3;100];[100];[4;100];[5;6]] + @> + + test <@ (Seq.splitPairwise (=) [0;1;1;2;3;4;4;4;5] |> toLists) = [[0;1];[1;2;3;4];[4];[4;5]] @> - test + test <@ (Seq.NonEmpty.split ((=) 100) (Seq.NonEmpty.create 1 [2;3;100;100;4;100;5;6]) |> toLists) = [[1;2;3;100];[100];[4;100];[5;6]] @@ -226,34 +300,67 @@ module Splitting = [] let ``works with infinite lists`` () = - let infinite = Seq.append [0;1;1;2;3;4;4;4;5;0] (InfiniteSeq.initBounded 3000 id) - let neInfinite = NonEmpty.assume infinite - test + let splitInfinite = Seq.append [1;2;3;100;100;4;100;5;6] (InfiniteSeq.initBounded 3000 id) + let neSplitInfinite = NonEmpty.assume splitInfinite + test <@ - (Seq.splitPairwise (=) infinite |> Seq.truncate 4 |> toLists) + (Seq.split ((=) 100) splitInfinite |> Seq.truncate 3 |> toLists) + = [[1;2;3;100];[100];[4;100]] + @> + + test + <@ + (Seq.NonEmpty.split ((=) 100) neSplitInfinite |> Seq.truncate 3 |> toLists) + = [[1;2;3;100];[100];[4;100]] + @> + + let pairwiseInfinite = Seq.append [0;1;1;2;3;4;4;4;5;0] (InfiniteSeq.initBounded 3000 id) + let nePairwiseInfinite = NonEmpty.assume pairwiseInfinite + test + <@ + (Seq.splitPairwise (=) pairwiseInfinite |> Seq.truncate 4 |> toLists) = [[0;1];[1;2;3;4];[4];[4;5;0]] @> - test + test <@ - (Seq.NonEmpty.splitPairwise (=) neInfinite |> Seq.truncate 4 |> toLists) + (Seq.NonEmpty.splitPairwise (=) nePairwiseInfinite |> Seq.truncate 4 |> toLists) = [[0;1];[1;2;3;4];[4];[4;5;0]] @> [] let ``inner segments can be infinite`` () = + // [0; 1; 2; 3; ...]: first segment is [0; 1], then an infinite segment [2; 3; 4; ...] + // that never triggers the split again + let splitInfinite: seq<_> = InfiniteSeq.initBounded 3000 id + let neSplitInfinite = NonEmpty.assume splitInfinite + + test <@ (Seq.split ((=) 1) splitInfinite |> Seq.item 1 |> Seq.truncate 4 |> Seq.toList) = [2; 3; 4; 5] @> + test <@ (Seq.NonEmpty.split ((=) 1) neSplitInfinite |> Seq.item 1 |> Seq.truncate 4 |> Seq.toList) = [2; 3; 4; 5] @> + // [5; 5; 0; 1; 2; 3; ...]: one split at (5,5), then an infinite segment [5; 0; 1; 2; 3; ...] // with no equal adjacent pairs, so it never splits again - let infinite = Seq.append [5; 5] (InfiniteSeq.initBounded 3000 id) - let neInfinite = NonEmpty.assume infinite + let pairwiseInfinite = Seq.append [5; 5] (InfiniteSeq.initBounded 3000 id) + let nePairwiseInfinite = NonEmpty.assume pairwiseInfinite - test <@ (Seq.splitPairwise (=) infinite |> Seq.item 1 |> Seq.truncate 4 |> Seq.toList) = [5; 0; 1; 2] @> - test <@ (Seq.NonEmpty.splitPairwise (=) neInfinite |> Seq.item 1 |> Seq.truncate 4 |> Seq.toList) = [5; 0; 1; 2] @> + test <@ (Seq.splitPairwise (=) pairwiseInfinite |> Seq.item 1 |> Seq.truncate 4 |> Seq.toList) = [5; 0; 1; 2] @> + test <@ (Seq.NonEmpty.splitPairwise (=) nePairwiseInfinite |> Seq.item 1 |> Seq.truncate 4 |> Seq.toList) = [5; 0; 1; 2] @> [] let ``splits empty and single element sequences`` () = - test + test + <@ + (Seq.split ((=) 5) Seq.empty |> toLists) = [] + && + (Seq.split ((=) 5) [0] |> toLists) = [[0]] + && + (Seq.split ((=) 5) [5] |> toLists) = [[5]] + && + (Seq.split ((=) 5) [5; 5] |> toLists) = [[5]; [5]] + @> + + test <@ (Seq.splitPairwise (=) Seq.empty |> toLists) = [] && @@ -263,8 +370,25 @@ module Splitting = @> [] - let ``splits properly for multiple types of inputs`` () = - test + let ``splits properly for multiple types of inputs`` () = + test + <@ + (Seq.split ((=) 5) [] |> toLists) = [] + && + (Seq.split ((=) 5) [0] |> toLists) = [[0]] + && + (Seq.split ((=) 5) [5] |> toLists) = [[5]] + && + (Seq.split ((=) 5) [0;5] |> toLists) = [[0; 5]] + && + (Seq.split ((=) 5) [5;5] |> toLists) = [[5]; [5]] + && + (Seq.split ((=) 5) [5;0] |> toLists) = [[5]; [0]] + && + (Seq.split ((=) 5) [5;0;0;5;5;0;5] |> toLists) = [[5]; [0;0;5]; [5]; [0;5]] + @> + + test <@ (Seq.NonEmpty.split ((=) 5) (Seq.NonEmpty.singleton 0) |> toLists) = [[0]] && @@ -299,6 +423,12 @@ module Splitting = [] let ``inner segments remain valid after outer sequence is fully materialized`` () = + let splitSegments = Seq.split ((=) 100) [1;2;3;100;100;4;100;5;6] |> Seq.toList + test <@ splitSegments |> List.map Seq.toList = [[1;2;3;100];[100];[4;100];[5;6]] @> + + let neSplitSegments = Seq.NonEmpty.split ((=) 100) (Seq.NonEmpty.create 1 [2;3;100;100;4;100;5;6]) |> Seq.toList + test <@ neSplitSegments |> List.map Seq.toList = [[1;2;3;100];[100];[4;100];[5;6]] @> + let segments = Seq.splitPairwise (=) [0;1;1;2;3;4;4;4;5] |> Seq.toList test <@ segments |> List.map Seq.toList = [[0;1];[1;2;3;4];[4];[4;5]] @> @@ -307,6 +437,20 @@ module Splitting = [] let ``inner segments can be re-enumerated`` () = + let splitFirstSegment = Seq.split ((=) 100) [1;2;3;100;4;100] |> Seq.toList |> List.head + test + <@ + Seq.toList splitFirstSegment = [1;2;3;100] + && Seq.toList splitFirstSegment = [1;2;3;100] + @> + + let neSplitFirstSegment = Seq.NonEmpty.split ((=) 100) (Seq.NonEmpty.create 1 [2;3;100;4;100]) |> Seq.toList |> List.head + test + <@ + Seq.toList neSplitFirstSegment = [1;2;3;100] + && Seq.toList neSplitFirstSegment = [1;2;3;100] + @> + let firstSegment = Seq.splitPairwise (=) [0;1;1;2] |> Seq.toList |> List.head test <@ @@ -323,6 +467,24 @@ module Splitting = [] let ``inner segments can be consumed out of order`` () = + let splitSegments = Seq.split ((=) 5) [5;0;0;5;5;0;5] |> Seq.toArray + test + <@ + Seq.toList splitSegments.[2] = [5] + && Seq.toList splitSegments.[0] = [5] + && Seq.toList splitSegments.[3] = [0;5] + && Seq.toList splitSegments.[1] = [0;0;5] + @> + + let neSplitSegments = Seq.NonEmpty.split ((=) 5) (Seq.NonEmpty.create 5 [0;0;5;5;0;5]) |> Seq.toArray + test + <@ + Seq.toList neSplitSegments.[2] = [5] + && Seq.toList neSplitSegments.[0] = [5] + && Seq.toList neSplitSegments.[3] = [0;5] + && Seq.toList neSplitSegments.[1] = [0;0;5] + @> + let segments = Seq.splitPairwise (=) [0;1;1;2;3;4;4;4;5] |> Seq.toArray test <@ diff --git a/SafetyFirst/Array.fs b/SafetyFirst/Array.fs index 53de553..82e2ca3 100644 --- a/SafetyFirst/Array.fs +++ b/SafetyFirst/Array.fs @@ -640,6 +640,17 @@ let skipLenient count xs = /// let inline drop count xs = skipLenient count xs +/// +/// Returns the elements of the array after the first element for which the given function returns True, +/// discarding all elements up to and including the first match. +/// Like skipWhile, but also skips the element for which the predicate first returns True. +/// If the array is exhausted without finding a matching element, an empty array is returned. +/// +let skipUntilIncluding predicate (xs: _ array) = + match Array.tryFindIndex predicate xs with + | None -> [||] + | Some i -> xs.[i + 1..] + /// /// Splits an array into two arrays, at the given index. /// Returns an IndexOutOfBounds Error when split index exceeds @@ -723,6 +734,28 @@ let splitPairwise splitBetween (xs: array<_>) : array> = |] |] +/// +/// Splits an array at every occurrence of an element satisfying splitAfter. +/// The split occurs immediately after each element that satisfies splitAfter, +/// and the element satisfying splitAfter will be included as the last element of +/// the array preceeding the split. +/// For example: +/// +/// split ((=) 100) [|1;2;3;100;100;4;100;5;6|] +/// //returns [|[|1;2;3;100|];[|100|];[|4;100|];[|5;6|]|] +/// +/// +let split splitAfter (xs: _ array) : NonEmptyArray<_>[] = + [| + let mutable groupStart = 0 + for i in 0..xs.Length - 1 do + if splitAfter xs.[i] then + yield NonEmpty xs.[groupStart..i] + groupStart <- i + 1 + if groupStart < xs.Length then + yield NonEmpty xs.[groupStart..] + |] + /// /// Slices an array given a starting index and a count of elements to return. /// Returns an IndexOutOfBounds Error if either startIndex or count is negative, @@ -805,6 +838,16 @@ let inline take' count xs = takeSafe count xs /// let inline tryTake count xs = takeSafe count xs |> Result.toOption +/// +/// Returns the array through the first element for which the given function returns True. +/// Like takeWhile, but includes the element for which the predicate returns True. +/// If the array is exhausted without finding a matching element, the entire array is returned. +/// +let takeWhileIncluding predicate (xs: _ array) = + match Array.tryFindIndex predicate xs with + | None -> xs + | Some i -> xs.[..i] + /// /// Returns the transpose of the given sequence of arrays. Returns a DifferingLengths Error if /// the input arrays differ in length. @@ -1475,10 +1518,8 @@ module NonEmpty = /// //returns ([|[|1;2;3;100|];[|100|];[|4;100|];[|5;6|]|]) /// /// - let split splitAfter xs = - FSeq.NonEmpty.split splitAfter (toNonEmptyFSeq xs) - |> FSeq.NonEmpty.map FSeq.NonEmpty.toNonEmptyArray - |> FSeq.NonEmpty.toNonEmptyArray + let split splitAfter (NonEmpty xs : NonEmptyArray<_>) : NonEmptyArray> = + NonEmpty <| split splitAfter xs /// /// Splits an array between each pair of adjacent elements that satisfy splitBetween. @@ -1491,7 +1532,16 @@ module NonEmpty = let splitPairwise splitBetween (NonEmpty xs : NonEmptyArray<_>) : NonEmptyArray> = NonEmpty (splitPairwise splitBetween xs) - type ZipperExpression() = + /// + /// Returns the array through the first element for which the given function returns True. + /// Like takeWhile, but includes the element for which the predicate returns True. + /// Like find, but computes on-demand and returns the array of intermediary result through the found element. + /// If the array is exhausted without finding a matching element, the entire array is returned. + /// + let takeWhileIncluding predicate (NonEmpty xs : NonEmptyArray<_>) : NonEmptyArray<_> = + NonEmpty (takeWhileIncluding predicate xs) + + type ZipperExpression() = member inline this.MergeSources(t1, t2) = zipShortest t1 t2 diff --git a/SafetyFirst/FiniteSeqModule.fs b/SafetyFirst/FiniteSeqModule.fs index 37f3bda..6cebd4d 100644 --- a/SafetyFirst/FiniteSeqModule.fs +++ b/SafetyFirst/FiniteSeqModule.fs @@ -671,9 +671,27 @@ module FiniteSeq = /// Returns a sequence that, when iterated, skips elements of the underlying sequence while the /// given predicate returns True, and then yields the remaining elements of the sequence. /// - let skipWhile predicate (FSeq xs : FiniteSeq<_>) : FiniteSeq<_> = + let skipWhile predicate (FSeq xs : FiniteSeq<_>) : FiniteSeq<_> = fseq (Seq.skipWhile predicate xs) + /// + /// Returns the elements of the sequence after the first element for which the given function returns True, + /// discarding all elements up to and including the first match. + /// Like skipWhile, but also skips the element for which the predicate first returns True. + /// If the sequence is exhausted without finding a matching element, an empty sequence is returned. + /// + let skipUntilIncluding predicate (xs: FiniteSeq<_>) : FiniteSeq<_> = + fseq <| seq { + use e = (xs :> IEnumerable<_>).GetEnumerator() + let mutable startYielding = false + while e.MoveNext() do + if startYielding then + yield e.Current + + if predicate e.Current then + startYielding <- true + } + /// /// Splits the input sequence into at most count chunks. /// This function consumes the whole input sequence before yielding the first element of the result sequence. @@ -706,6 +724,34 @@ module FiniteSeq = /// let inline trySplitInto n xs = splitIntoSafe n xs |> Result.toOption + /// + /// Splits a sequence at every occurrence of an element satisfying splitAfter. + /// The split occurs immediately after each element that satisfies splitAfter, + /// and the element satisfying splitAfter will be included as the last element of + /// the sequence preceeding the split. + /// For example: + /// + /// split ((=) 100) [1;2;3;100;100;4;100;5;6] + /// //returns [[1;2;3;100];[100];[4;100];[5;6]] + /// + /// The outer sequence is lazy, but each inner segment is eagerly materialized when the outer + /// sequence advances to it. + /// + let split splitAfter (xs: FiniteSeq<_>) : FiniteSeq> = + fseq <| seq { + use mutable iter = xs :> IEnumerable<_> |> _.GetEnumerator() + let mutable keepGoing = iter.MoveNext() + while keepGoing do + yield + NonEmpty <| fseq [ + let mutable stop = false + while keepGoing && not stop do + yield iter.Current + stop <- splitAfter iter.Current + keepGoing <- iter.MoveNext() + ] + } + /// /// Splits a sequence between each pair of adjacent elements that satisfy splitBetween. /// For example: @@ -792,6 +838,21 @@ module FiniteSeq = let takeWhile predicate (FSeq xs : FiniteSeq<_>) : FiniteSeq<_> = fseq (Seq.takeWhile predicate xs) + /// + /// Returns the sequence through the first element for which the given function returns True. + /// Like takeWhile, but includes the element for which the predicate returns True. + /// If the sequence is exhausted without finding a matching element, the entire sequence is returned. + /// + let takeWhileIncluding predicate (xs: FiniteSeq<_>) : FiniteSeq<_> = + fseq <| seq { + use e = (xs :> IEnumerable<_>).GetEnumerator() + let mutable continueLoop = true + while continueLoop && e.MoveNext() do + yield e.Current + if predicate e.Current then + continueLoop <- false + } + /// /// Builds an array from the given collection. /// @@ -1585,6 +1646,14 @@ module FSeq = /// let inline skipWhile predicate (xs : _ fseq) : _ fseq = FiniteSeq.skipWhile predicate xs + /// + /// Returns the elements of the sequence after the first element for which the given function returns True, + /// discarding all elements up to and including the first match. + /// Like skipWhile, but also skips the element for which the predicate first returns True. + /// If the sequence is exhausted without finding a matching element, an empty sequence is returned. + /// + let inline skipUntilIncluding predicate (xs : _ fseq) : _ fseq = FiniteSeq.skipUntilIncluding predicate xs + /// /// Splits the input sequence into at most count chunks. /// This function consumes the whole input sequence before yielding the first element of the result sequence. @@ -1625,6 +1694,21 @@ module FSeq = /// let splitPairwise splitBetween (xs: FSeq<_>) : FSeq> = FiniteSeq.splitPairwise splitBetween xs + /// + /// Splits a sequence at every occurrence of an element satisfying splitAfter. + /// The split occurs immediately after each element that satisfies splitAfter, + /// and the element satisfying splitAfter will be included as the last element of + /// the sequence preceeding the split. + /// For example: + /// + /// split ((=) 100) (fseq [1;2;3;100;100;4;100;5;6]) + /// //returns [[1;2;3;100];[100];[4;100];[5;6]] + /// + /// The outer sequence is lazy, but each inner segment is eagerly materialized when the outer + /// sequence advances to it. + /// + let split splitAfter (xs: FSeq<_>) : FSeq> = FiniteSeq.split splitAfter xs + /// /// Returns the sum of the elements in the sequence. /// The elements are summed using the + operator and Zero property associated with the generated type. @@ -1643,6 +1727,13 @@ module FSeq = /// let inline takeWhile predicate (xs : _ fseq) : _ fseq = FiniteSeq.takeWhile predicate xs + /// + /// Returns the sequence through the first element for which the given function returns True. + /// Like takeWhile, but includes the element for which the predicate returns True. + /// If the sequence is exhausted without finding a matching element, the entire sequence is returned. + /// + let inline takeWhileIncluding predicate (xs : _ fseq) : _ fseq = FiniteSeq.takeWhileIncluding predicate xs + /// /// O(1). Return option the list corresponding to the remaining items in the sequence. /// Forces the evaluation of the first cell of the list if it is not already evaluated. @@ -2346,27 +2437,8 @@ module FSeq = /// // this implementation is faster than the version in Seq.NonEmpty, but is unsafe for infinite sequences // so this should be the default used for any finite sequence (inculding lists and arrays) - let split splitAfter xs = - let addToEnd xs x = appendR xs (singleton x) - let (++) = addToEnd - - let rec split' (input:'a fseq) startNewGroup (currentGroup:NonEmptyFSeq<'a>) (completedGroups:fseq>) = - match input with - | NotEmpty input -> - let (head, tail) = uncons input - - let newCurrentGroup, newCompletedGroups = - if not startNewGroup - then (fseq currentGroup ++ head, completedGroups) - else (singleton head, fseq (completedGroups ++ currentGroup)) - - split' tail (splitAfter head) newCurrentGroup newCompletedGroups - - | Empty -> - completedGroups ++ currentGroup - - let (head, tail) = uncons xs - split' tail (splitAfter head) (singleton head) (fseq []) + let split splitAfter (NonEmpty xs: NonEmptyFSeq<_>) : NonEmptyFSeq> = + NonEmpty (FiniteSeq.split splitAfter xs) /// /// Splits a sequence between each pair of adjacent elements that satisfy splitBetween. @@ -2381,7 +2453,16 @@ module FSeq = let splitPairwise splitBetween (NonEmpty xs) : NonEmptyFSeq> = NonEmpty (FiniteSeq.splitPairwise splitBetween xs) - type ZipperExpression() = + /// + /// Returns the sequence through the first element for which the given function returns True. + /// Like takeWhile, but includes the element for which the predicate returns True. + /// Like find, but computes on-demand and returns the sequence of intermediary result through the found element. + /// If the sequence is exhausted without finding a matching element, the entire sequence is returned. + /// + let takeWhileIncluding predicate (NonEmpty xs : NonEmptyFSeq<_>) : NonEmptyFSeq<_> = + NonEmpty (FiniteSeq.takeWhileIncluding predicate xs) + + type ZipperExpression() = member inline this.MergeSources(t1, t2) = zip t1 t2 diff --git a/SafetyFirst/List.fs b/SafetyFirst/List.fs index 66806de..9d565e8 100644 --- a/SafetyFirst/List.fs +++ b/SafetyFirst/List.fs @@ -644,6 +644,19 @@ let skipLenient count xs = /// let inline drop count xs = skipLenient count xs +/// +/// Returns the elements of the list after the first element for which the given function returns True, +/// discarding all elements up to and including the first match. +/// Like skipWhile, but also skips the element for which the predicate first returns True. +/// If the list is exhausted without finding a matching element, an empty list is returned. +/// +let rec skipUntilIncluding predicate xs = + match xs with + | [] -> [] + | head :: tail -> + if predicate head then tail + else skipUntilIncluding predicate tail + /// /// Splits a list into two lists, at the given index. /// Returns an IndexOutOfBounds Error when split index exceeds @@ -729,6 +742,30 @@ let splitPairwise splitBetween xs : List> = let (currGroup, completedGroups) = (NonEmpty [prev], []) split' input prev currGroup completedGroups +/// +/// Splits a list at every occurrence of an element satisfying splitAfter. +/// The split occurs immediately after each element that satisfies splitAfter, +/// and the element satisfying splitAfter will be included as the last element of +/// the list preceeding the split. +/// For example: +/// +/// split ((=) 100) [1;2;3;100;100;4;100;5;6] +/// //returns [[1;2;3;100];[100];[4;100];[5;6]] +/// +/// +let split splitAfter xs : NonEmptyList<_> list = + let rec split' splitBefore input (NonEmpty currGroupLst as currentGroup : NonEmptyList<_>) completedGroups = + match input with + | [] -> currentGroup :: completedGroups + | head :: tail -> + if splitBefore head + then split' splitBefore tail (NonEmpty [head]) (currentGroup :: completedGroups) + else split' splitBefore tail (NonEmpty (head :: currGroupLst)) completedGroups + + match List.rev xs, splitAfter with + | [], _ -> [] + | prev :: input, splitBefore -> split' splitBefore input (NonEmpty [prev]) [] + /// /// Returns a list that skips 1 element of the underlying list and then yields the /// remaining elements of the list. @@ -776,6 +813,19 @@ let inline take' count xs = takeSafe count xs /// let inline tryTake count xs = takeSafe count xs |> Result.toOption +/// +/// Returns the list through the first element for which the given function returns True. +/// Like takeWhile, but includes the element for which the predicate returns True. +/// If the list is exhausted without finding a matching element, the entire list is returned. +/// +let takeWhileIncluding predicate xs = + let rec take' acc = function + | [] -> List.rev acc + | head :: tail -> + if predicate head then List.rev (head :: acc) + else take' (head :: acc) tail + take' [] xs + /// /// Returns a list that yields sliding windows containing elements drawn from the input /// list. Each window is returned as a fresh list. @@ -1459,7 +1509,16 @@ module NonEmpty = let splitPairwise splitBetween (NonEmpty xs: NonEmptyList<_>) : NonEmptyList> = NonEmpty (splitPairwise splitBetween xs) - type ZipperExpression() = + /// + /// Returns the list through the first element for which the given function returns True. + /// Like takeWhile, but includes the element for which the predicate returns True. + /// Like find, but computes on-demand and returns the list of intermediary result through the found element. + /// If the list is exhausted without finding a matching element, the entire list is returned. + /// + let takeWhileIncluding predicate (NonEmpty xs: NonEmptyList<_>) : NonEmptyList<_> = + NonEmpty (takeWhileIncluding predicate xs) + + type ZipperExpression() = member inline this.MergeSources(t1, t2) = zipShortest t1 t2 diff --git a/SafetyFirst/Seq.fs b/SafetyFirst/Seq.fs index d9a4413..377a536 100644 --- a/SafetyFirst/Seq.fs +++ b/SafetyFirst/Seq.fs @@ -474,6 +474,27 @@ let skipLenient count (xs: _ seq) = yield e.Current } +/// +/// Returns a sequence that, when iterated, skips elements of the underlying sequence +/// up to and including the first element for which the given predicate returns True, +/// and then yields the remaining elements of the sequence. +/// Like skipWhile, but with an inverted predicate and +/// also skips the element for which the predicate first returns True. +/// If the sequence is exhausted without finding a matching element, an empty sequence is returned. +/// +let skipUntilIncluding predicate (xs: _ seq) = + seq { + use e = xs.GetEnumerator() + let mutable startYielding = false + while e.MoveNext() do + if startYielding then + yield e.Current + + if predicate e.Current then + startYielding <- true + } + + /// /// Returns a sequence that lazily skips N elements of the underlying sequence and then yields the /// remaining elements of the sequence. @@ -509,6 +530,57 @@ let inline trySplitInto count xs = splitIntoSafe count xs |> Result.toOption /// let splitIntoN (PositiveInt count) xs = Seq.splitInto count xs +/// +/// Returns the sequence through the first element for which the given function returns True. +/// Like takeWhile, but includes the element for which the predicate returns True. +/// Like find, but computes on-demand and returns the sequence of intermediary result through the found element. +/// If the sequence is exhausted without finding a matching element, the entire sequence is returned. +/// +let takeWhileIncluding predicate (xs: _ seq) = + seq { + use e = xs.GetEnumerator() + let mutable continueLoop = true + while continueLoop && e.MoveNext() do + yield e.Current + if predicate e.Current then + continueLoop <- false + } + + +/// +/// Splits a sequence at every occurrence of an element satisfying splitAfter. +/// The split occurs immediately after each element that satisfies splitAfter, +/// and the element satisfying splitAfter will be included as the last element of +/// the sequence preceeding the split. +/// For example: +/// +/// split ((=) 100) [1;2;3;100;100;4;100;5;6] +/// //returns ([[1;2;3;100];[100];[4;100];[5;6]]) +/// +/// Both the outer sequence and each inner segment are lazy. +/// Inner segments are safe to re-enumerate and consume in any order. +/// NOTE: Performance is O(N*K) where N is the number of elements and K is the number of +/// segments, due to re-traversal of the lazy chain at each level. If the source sequence +/// is expensive to evaluate, cache it with Seq.cache before calling this function. +/// If the source sequence is finite, you can achieve better performance by converting it to +/// a list or array first and using List.split or Array.split instead. +/// +let split splitAfter xs : seq> = + let takeGroup (NonEmpty xs: NonEmptySeq<_>) : NonEmptySeq<_> = + NonEmpty <| takeWhileIncluding splitAfter xs + + let rec split' xs = + seq { + match xs with + | Empty -> () + | NotEmpty neXs -> + yield takeGroup neXs + yield! split' (skipUntilIncluding splitAfter xs) + } + + split' xs + + /// /// Splits a sequence between each pair of adjacent elements that satisfy splitBetween. /// For example: @@ -835,6 +907,14 @@ module NonEmpty = /// let scan f initialState (NonEmpty xs) : NonEmptySeq<_> = NonEmpty (Seq.scan f initialState xs) + /// + /// Returns the sequence through the first element for which the given function returns True. + /// Like takeWhile, but includes the element for which the predicate returns True. + /// Like find, but computes on-demand and returns the sequence of intermediary result through the found element. + /// If the sequence is exhausted without finding a matching element, the entire sequence is returned. + /// + let takeWhileIncluding predicate (NonEmpty xs) : NonEmptySeq<_> = NonEmpty (takeWhileIncluding predicate xs) + /// /// Builds an array from the given collection. /// @@ -906,40 +986,8 @@ module NonEmpty = /// //returns ([[1;2;3;100];[100];[4;100];[5;6]]) /// /// - let split splitAfter xs : NonEmptySeq<_> = - let (++) (NonEmpty xs) ys = NonEmpty <| Seq.append xs ys - - let takeGroup input : NonEmptySeq<_> = - let rec takeGroup' input = - seq { - match input with - | SeqOneOrMore (head, tail) -> - if splitAfter head - then yield head - else - yield head - yield! takeGroup' tail - | _ -> () - } - - let head, tail = uncons input - if splitAfter head - then singleton head - else singleton head ++ takeGroup' tail - - let rec split' (xs:NonEmptySeq<_>) = - NonEmpty ( - seq { - yield takeGroup xs - - let subsequentElements = Seq.skipWhile (not << splitAfter) xs |> skipLenient 1 - match subsequentElements with - | Empty -> () - | NotEmpty elements -> yield! split' elements - } - ) - - split' (NonEmpty <| toSeq xs) + let split splitAfter (NonEmpty xs : NonEmptySeq<_>) : NonEmptySeq> = + NonEmpty <| split splitAfter xs /// /// Splits a sequence between each pair of adjacent elements that satisfy splitBetween. From 3fda0689b53a08a4a7198f83950c4babbc6848ef Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Fri, 20 Mar 2026 14:41:00 -0400 Subject: [PATCH 2/9] Add split to InfiniteSeq too --- SafetyFirst.Specs/InfiniteSeqSpec.fs | 39 ++++++++++++++++++++++++++++ SafetyFirst/Array.fs | 2 +- SafetyFirst/FiniteSeqModule.fs | 6 ++--- SafetyFirst/InfiniteSeq.fs | 12 +++------ SafetyFirst/List.fs | 8 +++--- SafetyFirst/Seq.fs | 4 +-- 6 files changed, 51 insertions(+), 20 deletions(-) diff --git a/SafetyFirst.Specs/InfiniteSeqSpec.fs b/SafetyFirst.Specs/InfiniteSeqSpec.fs index 9a2d205..030e494 100644 --- a/SafetyFirst.Specs/InfiniteSeqSpec.fs +++ b/SafetyFirst.Specs/InfiniteSeqSpec.fs @@ -360,6 +360,45 @@ let ``splitPairwise inner segments can be consumed out of order`` () = && Seq.toList segments.[1] = [1;2;3;4] @> +[] +let ``split returns what the documentation says`` () = + // Four 100s → four finite segments; everything after belongs to a 5th segment we don't read + let xs = InfiniteSeq.append [1;2;3;100;100;4;100;5;100;0] (InfiniteSeq.initBounded 100 id) + test + <@ + InfiniteSeq.split ((=) 100) xs |> InfiniteSeq.take 4 |> Seq.map Seq.toList |> Seq.toList + = [[1;2;3;100];[100];[4;100];[5;100]] + @> + +[] +let ``split inner segments can be infinite`` () = + // initBounded 100 id yields 0..99 with no 100s, so the second segment is infinite + let xs = InfiniteSeq.append [1;100] (InfiniteSeq.initBounded 100 id) + let secondSeg = InfiniteSeq.split ((=) 100) xs |> InfiniteSeq.take 2 |> Seq.toList |> List.item 1 + test <@ secondSeg |> Seq.truncate 4 |> Seq.toList = [0;1;2;3] @> + +[] +let ``split inner segments can be re-enumerated`` () = + let xs = InfiniteSeq.append [1;2;100;3] (InfiniteSeq.initBounded 100 id) + let firstSegment = InfiniteSeq.split ((=) 100) xs |> InfiniteSeq.take 1 |> Seq.head + test + <@ + Seq.toList firstSegment = [1;2;100] + && Seq.toList firstSegment = [1;2;100] + @> + +[] +let ``split inner segments can be consumed out of order`` () = + let xs = InfiniteSeq.append [1;2;3;100;100;4;100;5;100;0] (InfiniteSeq.initBounded 100 id) + let segments = InfiniteSeq.split ((=) 100) xs |> InfiniteSeq.take 4 |> Seq.toArray + test + <@ + Seq.toList segments.[2] = [4;100] + && Seq.toList segments.[0] = [1;2;3;100] + && Seq.toList segments.[3] = [5;100] + && Seq.toList segments.[1] = [100] + @> + // [] // let ``splits infinite sequences without hanging`` () = // let alwaysFalse (_:int) = false diff --git a/SafetyFirst/Array.fs b/SafetyFirst/Array.fs index 82e2ca3..82011c1 100644 --- a/SafetyFirst/Array.fs +++ b/SafetyFirst/Array.fs @@ -738,7 +738,7 @@ let splitPairwise splitBetween (xs: array<_>) : array> = /// Splits an array at every occurrence of an element satisfying splitAfter. /// The split occurs immediately after each element that satisfies splitAfter, /// and the element satisfying splitAfter will be included as the last element of -/// the array preceeding the split. +/// the array preceding the split. /// For example: /// /// split ((=) 100) [|1;2;3;100;100;4;100;5;6|] diff --git a/SafetyFirst/FiniteSeqModule.fs b/SafetyFirst/FiniteSeqModule.fs index 6cebd4d..bdd23d5 100644 --- a/SafetyFirst/FiniteSeqModule.fs +++ b/SafetyFirst/FiniteSeqModule.fs @@ -728,7 +728,7 @@ module FiniteSeq = /// Splits a sequence at every occurrence of an element satisfying splitAfter. /// The split occurs immediately after each element that satisfies splitAfter, /// and the element satisfying splitAfter will be included as the last element of - /// the sequence preceeding the split. + /// the sequence preceding the split. /// For example: /// /// split ((=) 100) [1;2;3;100;100;4;100;5;6] @@ -1698,7 +1698,7 @@ module FSeq = /// Splits a sequence at every occurrence of an element satisfying splitAfter. /// The split occurs immediately after each element that satisfies splitAfter, /// and the element satisfying splitAfter will be included as the last element of - /// the sequence preceeding the split. + /// the sequence preceding the split. /// For example: /// /// split ((=) 100) (fseq [1;2;3;100;100;4;100;5;6]) @@ -2428,7 +2428,7 @@ module FSeq = /// Splits a sequence at every occurrence of an element satisfying splitAfter. /// The split occurs immediately after each element that satisfies splitAfter, /// and the element satisfying splitAfter will be included as the last element of - /// the sequence preceeding the split. + /// the sequence preceding the split. /// For example: /// /// split ((=) 100) (FSeq.NonEmpty.create 1[2;3;100;100;4;100;5;6]) diff --git a/SafetyFirst/InfiniteSeq.fs b/SafetyFirst/InfiniteSeq.fs index 454d20e..a3fa1a6 100644 --- a/SafetyFirst/InfiniteSeq.fs +++ b/SafetyFirst/InfiniteSeq.fs @@ -525,25 +525,19 @@ module InfiniteSeq = let scan f initialState (InfiniteSeq xs) = InfiniteSeq (Seq.scan f initialState xs) - - // -- tests end here -- - - - - /// /// Splits a sequence at every occurrence of an element satisfying splitAfter. /// The split occurs immediately after each element that satisfies splitAfter, /// and the element satisfying splitAfter will be included as the last element of - /// the sequence preceeding the split. + /// the sequence preceding the split. /// For example: /// /// IniniteSeq.split ((=) 100) (seq {1;2;3;100;100;4;100;5;6;...}) /// //returns ([[1;2;3;100];[100];[4;100];[5;6];...]) /// /// - // let split splitAfter xs = - // InfiniteSeq (Seq.split splitAfter xs) + let split splitAfter xs = + InfiniteSeq (Seq.split splitAfter xs) /// /// Splits a sequence between each pair of adjacent elements that satisfy splitBetween. diff --git a/SafetyFirst/List.fs b/SafetyFirst/List.fs index 9d565e8..ccaf1a3 100644 --- a/SafetyFirst/List.fs +++ b/SafetyFirst/List.fs @@ -746,7 +746,7 @@ let splitPairwise splitBetween xs : List> = /// Splits a list at every occurrence of an element satisfying splitAfter. /// The split occurs immediately after each element that satisfies splitAfter, /// and the element satisfying splitAfter will be included as the last element of -/// the list preceeding the split. +/// the list preceding the split. /// For example: /// /// split ((=) 100) [1;2;3;100;100;4;100;5;6] @@ -1493,10 +1493,8 @@ module NonEmpty = /// //returns ([[1;2;3;100];[100];[4;100];[5;6]]) /// /// - let split splitAfter xs = - FSeq.NonEmpty.split splitAfter (toNonEmptyFSeq xs) - |> Seq.NonEmpty.map Seq.NonEmpty.toNonEmptyList - |> Seq.NonEmpty.toNonEmptyList + let split splitAfter (NonEmpty xs: NonEmptyList<_>) : NonEmptyList> = + NonEmpty (split splitAfter xs) /// /// Splits a list between each pair of adjacent elements that satisfy splitBetween. diff --git a/SafetyFirst/Seq.fs b/SafetyFirst/Seq.fs index 377a536..84d47f8 100644 --- a/SafetyFirst/Seq.fs +++ b/SafetyFirst/Seq.fs @@ -551,7 +551,7 @@ let takeWhileIncluding predicate (xs: _ seq) = /// Splits a sequence at every occurrence of an element satisfying splitAfter. /// The split occurs immediately after each element that satisfies splitAfter, /// and the element satisfying splitAfter will be included as the last element of -/// the sequence preceeding the split. +/// the sequence preceding the split. /// For example: /// /// split ((=) 100) [1;2;3;100;100;4;100;5;6] @@ -979,7 +979,7 @@ module NonEmpty = /// Splits a sequence at every occurrence of an element satisfying splitAfter. /// The split occurs immediately after each element that satisfies splitAfter, /// and the element satisfying splitAfter will be included as the last element of - /// the sequence preceeding the split. + /// the sequence preceding the split. /// For example: /// /// split ((=) 100) (Seq.NonEmpty.create 1[2;3;100;100;4;100;5;6]) From adfae53fce97f0eb2035a784e3f19d675542b2d1 Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Fri, 20 Mar 2026 14:41:42 -0400 Subject: [PATCH 3/9] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- SafetyFirst.Specs/SeqSpec.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SafetyFirst.Specs/SeqSpec.fs b/SafetyFirst.Specs/SeqSpec.fs index 3eb992e..128c00e 100644 --- a/SafetyFirst.Specs/SeqSpec.fs +++ b/SafetyFirst.Specs/SeqSpec.fs @@ -262,7 +262,7 @@ module SkipUntilIncluding = test <@ Seq.initInfinite id |> Seq.skipUntilIncluding ((=) 3) |> Seq.take 4 |> Seq.toList = [4;5;6;7] @> [] - let ``takeWhileIncluding and skipWhileIncluding partition the sequence`` () = + let ``takeWhileIncluding and skipUntilIncluding partition the sequence`` () = let xs = [1;2;3;4;5] let taken = Seq.takeWhileIncluding ((=) 3) xs |> Seq.toList let skipped = Seq.skipUntilIncluding ((=) 3) xs |> Seq.toList From 699426c5b59999db3580025338f092ac7f3aa9a2 Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Fri, 20 Mar 2026 15:24:25 -0400 Subject: [PATCH 4/9] Rename takeWhileIncluding to takeUntilIncluding --- ReleaseNotes.md | 2 +- SafetyFirst.Specs/ArraySpec.fs | 24 ++++++++++++------------ SafetyFirst.Specs/FSeqSpec.fs | 24 ++++++++++++------------ SafetyFirst.Specs/ListSpec.fs | 24 ++++++++++++------------ SafetyFirst.Specs/SeqSpec.fs | 20 ++++++++++---------- SafetyFirst/Array.fs | 15 +++++++++------ SafetyFirst/FiniteSeqModule.fs | 19 ++++++++++++------- SafetyFirst/List.fs | 15 +++++++++------ SafetyFirst/Seq.fs | 14 ++++++++------ 9 files changed, 85 insertions(+), 72 deletions(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 1da5601..a35d4b1 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -5,7 +5,7 @@ Adds new functions for the List/Array/Seq/FSeq modules: - `splitPairwise` - `split` -- `takeWhileIncluding` +- `takeUntilIncluding` - `skipUntilIncluding` ### InfiniteSeq diff --git a/SafetyFirst.Specs/ArraySpec.fs b/SafetyFirst.Specs/ArraySpec.fs index aca9c48..6b7d211 100644 --- a/SafetyFirst.Specs/ArraySpec.fs +++ b/SafetyFirst.Specs/ArraySpec.fs @@ -226,40 +226,40 @@ module Splitting = (Array.split ((=) 5) [|5;0;0;5;5;0;5|] |> toArrs) = [|[|5|]; [|0;0;5|]; [|5|]; [|0;5|]|] @> -module TakeWhileIncluding = +module takeUntilIncluding = [] - let ``NonEmpty.takeWhileIncluding returns through the first matching element`` () = + let ``NonEmpty.takeUntilIncluding returns through the first matching element`` () = test <@ - Array.NonEmpty.takeWhileIncluding ((=) 3) (Array.NonEmpty.create 1 [|2;3;4;5|]) + Array.NonEmpty.takeUntilIncluding ((=) 3) (Array.NonEmpty.create 1 [|2;3;4;5|]) = Array.NonEmpty.create 1 [|2;3|] && - Array.NonEmpty.takeWhileIncluding ((=) 1) (Array.NonEmpty.create 1 [|2;3;4;5|]) + Array.NonEmpty.takeUntilIncluding ((=) 1) (Array.NonEmpty.create 1 [|2;3;4;5|]) = Array.NonEmpty.singleton 1 && - Array.NonEmpty.takeWhileIncluding ((=) 99) (Array.NonEmpty.create 1 [|2;3|]) + Array.NonEmpty.takeUntilIncluding ((=) 99) (Array.NonEmpty.create 1 [|2;3|]) = Array.NonEmpty.create 1 [|2;3|] @> [] let ``returns empty for empty input`` () = - test <@ Array.takeWhileIncluding (fun _ -> true) [||] = [||] @> + test <@ Array.takeUntilIncluding (fun _ -> true) [||] = [||] @> [] let ``returns through the first matching element`` () = - test <@ Array.takeWhileIncluding ((=) 3) [|1;2;3;4;5|] = [|1;2;3|] @> + test <@ Array.takeUntilIncluding ((=) 3) [|1;2;3;4;5|] = [|1;2;3|] @> [] let ``returns only the first element when it matches`` () = - test <@ Array.takeWhileIncluding ((=) 3) [|3;4;5|] = [|3|] @> + test <@ Array.takeUntilIncluding ((=) 3) [|3;4;5|] = [|3|] @> [] let ``stops at the first match even when multiple elements match`` () = - test <@ Array.takeWhileIncluding ((=) 3) [|1;3;3;3|] = [|1;3|] @> + test <@ Array.takeUntilIncluding ((=) 3) [|1;3;3;3|] = [|1;3|] @> [] let ``returns the full array when no element matches`` () = - test <@ Array.takeWhileIncluding ((=) 99) [|1;2;3|] = [|1;2;3|] @> + test <@ Array.takeUntilIncluding ((=) 99) [|1;2;3|] = [|1;2;3|] @> module SkipUntilIncluding = [] @@ -287,8 +287,8 @@ module SkipUntilIncluding = test <@ Array.skipUntilIncluding ((=) 99) [|1;2;3|] = [||] @> [] - let ``takeWhileIncluding and skipUntilIncluding partition the array`` () = + let ``takeUntilIncluding and skipUntilIncluding partition the array`` () = let xs = [|1;2;3;4;5|] - let taken = Array.takeWhileIncluding ((=) 3) xs + let taken = Array.takeUntilIncluding ((=) 3) xs let skipped = Array.skipUntilIncluding ((=) 3) xs test <@ Array.append taken skipped = xs @> \ No newline at end of file diff --git a/SafetyFirst.Specs/FSeqSpec.fs b/SafetyFirst.Specs/FSeqSpec.fs index db660c2..2d0875d 100644 --- a/SafetyFirst.Specs/FSeqSpec.fs +++ b/SafetyFirst.Specs/FSeqSpec.fs @@ -390,40 +390,40 @@ module Splitting = (FSeq.split ((=) 5) (fseq [5;0;0;5;5;0;5]) |> ofNonEmpty) = [[5]; [0;0;5]; [5]; [0;5]] @> -module TakeWhileIncluding = +module takeUntilIncluding = [] - let ``NonEmpty.takeWhileIncluding returns through the first matching element`` () = + let ``NonEmpty.takeUntilIncluding returns through the first matching element`` () = test <@ - FSeq.NonEmpty.takeWhileIncluding ((=) 3) (Splitting.toNonEmpty [1;2;3;4;5]) + FSeq.NonEmpty.takeUntilIncluding ((=) 3) (Splitting.toNonEmpty [1;2;3;4;5]) = Splitting.toNonEmpty [1;2;3] && - FSeq.NonEmpty.takeWhileIncluding ((=) 1) (Splitting.toNonEmpty [1;2;3;4;5]) + FSeq.NonEmpty.takeUntilIncluding ((=) 1) (Splitting.toNonEmpty [1;2;3;4;5]) = Splitting.toNonEmpty [1] && - FSeq.NonEmpty.takeWhileIncluding ((=) 99) (Splitting.toNonEmpty [1;2;3]) + FSeq.NonEmpty.takeUntilIncluding ((=) 99) (Splitting.toNonEmpty [1;2;3]) = Splitting.toNonEmpty [1;2;3] @> [] let ``returns empty for empty input`` () = - test <@ FSeq.takeWhileIncluding (fun _ -> true) (fseq []) = fseq [] @> + test <@ FSeq.takeUntilIncluding (fun _ -> true) (fseq []) = fseq [] @> [] let ``returns through the first matching element`` () = - test <@ FSeq.takeWhileIncluding ((=) 3) (fseq [1;2;3;4;5]) = fseq [1;2;3] @> + test <@ FSeq.takeUntilIncluding ((=) 3) (fseq [1;2;3;4;5]) = fseq [1;2;3] @> [] let ``returns only the first element when it matches`` () = - test <@ FSeq.takeWhileIncluding ((=) 3) (fseq [3;4;5]) = fseq [3] @> + test <@ FSeq.takeUntilIncluding ((=) 3) (fseq [3;4;5]) = fseq [3] @> [] let ``stops at the first match even when multiple elements match`` () = - test <@ FSeq.takeWhileIncluding ((=) 3) (fseq [1;3;3;3]) = fseq [1;3] @> + test <@ FSeq.takeUntilIncluding ((=) 3) (fseq [1;3;3;3]) = fseq [1;3] @> [] let ``returns the full sequence when no element matches`` () = - test <@ FSeq.takeWhileIncluding ((=) 99) (fseq [1;2;3]) = fseq [1;2;3] @> + test <@ FSeq.takeUntilIncluding ((=) 99) (fseq [1;2;3]) = fseq [1;2;3] @> module SkipUntilIncluding = [] @@ -451,9 +451,9 @@ module SkipUntilIncluding = test <@ FSeq.skipUntilIncluding ((=) 99) (fseq [1;2;3]) = fseq [] @> [] - let ``takeWhileIncluding and skipUntilIncluding partition the sequence`` () = + let ``takeUntilIncluding and skipUntilIncluding partition the sequence`` () = let xs = fseq [1;2;3;4;5] - let taken = FSeq.takeWhileIncluding ((=) 3) xs + let taken = FSeq.takeUntilIncluding ((=) 3) xs let skipped = FSeq.skipUntilIncluding ((=) 3) xs test <@ FSeq.append taken skipped = xs @> diff --git a/SafetyFirst.Specs/ListSpec.fs b/SafetyFirst.Specs/ListSpec.fs index 305a8ab..f65fa99 100644 --- a/SafetyFirst.Specs/ListSpec.fs +++ b/SafetyFirst.Specs/ListSpec.fs @@ -261,39 +261,39 @@ module Splitting = @> [] - let ``NonEmpty.takeWhileIncluding returns through the first matching element`` () = + let ``NonEmpty.takeUntilIncluding returns through the first matching element`` () = test <@ - List.NonEmpty.takeWhileIncluding ((=) 3) (List.NonEmpty.create 1 [2;3;4;5]) + List.NonEmpty.takeUntilIncluding ((=) 3) (List.NonEmpty.create 1 [2;3;4;5]) = List.NonEmpty.create 1 [2;3] && - List.NonEmpty.takeWhileIncluding ((=) 1) (List.NonEmpty.create 1 [2;3;4;5]) + List.NonEmpty.takeUntilIncluding ((=) 1) (List.NonEmpty.create 1 [2;3;4;5]) = List.NonEmpty.singleton 1 && - List.NonEmpty.takeWhileIncluding ((=) 99) (List.NonEmpty.create 1 [2;3]) + List.NonEmpty.takeUntilIncluding ((=) 99) (List.NonEmpty.create 1 [2;3]) = List.NonEmpty.create 1 [2;3] @> -module TakeWhileIncluding = +module takeUntilIncluding = [] let ``returns empty for empty input`` () = - test <@ List.takeWhileIncluding (fun _ -> true) [] = [] @> + test <@ List.takeUntilIncluding (fun _ -> true) [] = [] @> [] let ``returns through the first matching element`` () = - test <@ List.takeWhileIncluding ((=) 3) [1;2;3;4;5] = [1;2;3] @> + test <@ List.takeUntilIncluding ((=) 3) [1;2;3;4;5] = [1;2;3] @> [] let ``returns only the first element when it matches`` () = - test <@ List.takeWhileIncluding ((=) 3) [3;4;5] = [3] @> + test <@ List.takeUntilIncluding ((=) 3) [3;4;5] = [3] @> [] let ``stops at the first match even when multiple elements match`` () = - test <@ List.takeWhileIncluding ((=) 3) [1;3;3;3] = [1;3] @> + test <@ List.takeUntilIncluding ((=) 3) [1;3;3;3] = [1;3] @> [] let ``returns the full list when no element matches`` () = - test <@ List.takeWhileIncluding ((=) 99) [1;2;3] = [1;2;3] @> + test <@ List.takeUntilIncluding ((=) 99) [1;2;3] = [1;2;3] @> module SkipUntilIncluding = [] @@ -321,8 +321,8 @@ module SkipUntilIncluding = test <@ List.skipUntilIncluding ((=) 99) [1;2;3] = [] @> [] - let ``takeWhileIncluding and skipUntilIncluding partition the list`` () = + let ``takeUntilIncluding and skipUntilIncluding partition the list`` () = let xs = [1;2;3;4;5] - let taken = List.takeWhileIncluding ((=) 3) xs + let taken = List.takeUntilIncluding ((=) 3) xs let skipped = List.skipUntilIncluding ((=) 3) xs test <@ taken @ skipped = xs @> \ No newline at end of file diff --git a/SafetyFirst.Specs/SeqSpec.fs b/SafetyFirst.Specs/SeqSpec.fs index 128c00e..aa14be7 100644 --- a/SafetyFirst.Specs/SeqSpec.fs +++ b/SafetyFirst.Specs/SeqSpec.fs @@ -200,36 +200,36 @@ let ``isHungAfter throws for finite sequences that exceed the limit`` () = raises <@ [1..11] |> Seq.isHungAfter 10 |> Seq.toList @> -module TakeWhileIncluding = +module takeUntilIncluding = [] let ``returns empty for empty input`` () = - test <@ Seq.takeWhileIncluding (fun _ -> true) Seq.empty |> Seq.toList = [] @> + test <@ Seq.takeUntilIncluding (fun _ -> true) Seq.empty |> Seq.toList = [] @> [] let ``returns through the first matching element`` () = - test <@ Seq.takeWhileIncluding ((=) 3) [1;2;3;4;5] |> Seq.toList = [1;2;3] @> + test <@ Seq.takeUntilIncluding ((=) 3) [1;2;3;4;5] |> Seq.toList = [1;2;3] @> [] let ``returns only the first element when it matches`` () = - test <@ Seq.takeWhileIncluding ((=) 3) [3;4;5] |> Seq.toList = [3] @> + test <@ Seq.takeUntilIncluding ((=) 3) [3;4;5] |> Seq.toList = [3] @> [] let ``stops at the first match even when multiple elements match`` () = - test <@ Seq.takeWhileIncluding ((=) 3) [1;3;3;3] |> Seq.toList = [1;3] @> + test <@ Seq.takeUntilIncluding ((=) 3) [1;3;3;3] |> Seq.toList = [1;3] @> [] let ``returns the full sequence when no element matches`` () = - test <@ Seq.takeWhileIncluding ((=) 99) [1;2;3] |> Seq.toList = [1;2;3] @> + test <@ Seq.takeUntilIncluding ((=) 99) [1;2;3] |> Seq.toList = [1;2;3] @> [] let ``works with infinite sequences`` () = // stops after finding the matching element rather than diverging - test <@ Seq.initInfinite id |> Seq.takeWhileIncluding ((=) 3) |> Seq.toList = [0;1;2;3] @> + test <@ Seq.initInfinite id |> Seq.takeUntilIncluding ((=) 3) |> Seq.toList = [0;1;2;3] @> [] let ``is lazy - does not evaluate past the matching element`` () = let splitInfinite: seq<_> = InfiniteSeq.initBounded 3000 id - test <@ splitInfinite |> Seq.takeWhileIncluding ((=) 3) |> Seq.toList = [0;1;2;3] @> + test <@ splitInfinite |> Seq.takeUntilIncluding ((=) 3) |> Seq.toList = [0;1;2;3] @> module SkipUntilIncluding = [] @@ -262,9 +262,9 @@ module SkipUntilIncluding = test <@ Seq.initInfinite id |> Seq.skipUntilIncluding ((=) 3) |> Seq.take 4 |> Seq.toList = [4;5;6;7] @> [] - let ``takeWhileIncluding and skipUntilIncluding partition the sequence`` () = + let ``takeUntilIncluding and skipUntilIncluding partition the sequence`` () = let xs = [1;2;3;4;5] - let taken = Seq.takeWhileIncluding ((=) 3) xs |> Seq.toList + let taken = Seq.takeUntilIncluding ((=) 3) xs |> Seq.toList let skipped = Seq.skipUntilIncluding ((=) 3) xs |> Seq.toList test <@ taken @ skipped = xs @> diff --git a/SafetyFirst/Array.fs b/SafetyFirst/Array.fs index 82011c1..67db3d4 100644 --- a/SafetyFirst/Array.fs +++ b/SafetyFirst/Array.fs @@ -840,10 +840,12 @@ let inline tryTake count xs = takeSafe count xs |> Result.toOption /// /// Returns the array through the first element for which the given function returns True. -/// Like takeWhile, but includes the element for which the predicate returns True. +/// Like takeWhile, but with an inverted predicate and +/// also includes the element for which the predicate first returns True. +/// Like find, but returns the array of intermediary result through the found element. /// If the array is exhausted without finding a matching element, the entire array is returned. /// -let takeWhileIncluding predicate (xs: _ array) = +let takeUntilIncluding predicate (xs: _ array) = match Array.tryFindIndex predicate xs with | None -> xs | Some i -> xs.[..i] @@ -1534,12 +1536,13 @@ module NonEmpty = /// /// Returns the array through the first element for which the given function returns True. - /// Like takeWhile, but includes the element for which the predicate returns True. - /// Like find, but computes on-demand and returns the array of intermediary result through the found element. + /// Like takeWhile, but with an inverted predicate and + /// also includes the element for which the predicate first returns True. + /// Like find, but returns the array of intermediary result through the found element. /// If the array is exhausted without finding a matching element, the entire array is returned. /// - let takeWhileIncluding predicate (NonEmpty xs : NonEmptyArray<_>) : NonEmptyArray<_> = - NonEmpty (takeWhileIncluding predicate xs) + let takeUntilIncluding predicate (NonEmpty xs : NonEmptyArray<_>) : NonEmptyArray<_> = + NonEmpty (takeUntilIncluding predicate xs) type ZipperExpression() = member inline this.MergeSources(t1, t2) = diff --git a/SafetyFirst/FiniteSeqModule.fs b/SafetyFirst/FiniteSeqModule.fs index bdd23d5..b2c81db 100644 --- a/SafetyFirst/FiniteSeqModule.fs +++ b/SafetyFirst/FiniteSeqModule.fs @@ -840,10 +840,12 @@ module FiniteSeq = /// /// Returns the sequence through the first element for which the given function returns True. - /// Like takeWhile, but includes the element for which the predicate returns True. + /// Like takeWhile, but with an inverted predicate and + /// also includes the element for which the predicate first returns True. + /// Like find, but computes on-demand and returns the sequence of intermediary result through the found element. /// If the sequence is exhausted without finding a matching element, the entire sequence is returned. /// - let takeWhileIncluding predicate (xs: FiniteSeq<_>) : FiniteSeq<_> = + let takeUntilIncluding predicate (xs: FiniteSeq<_>) : FiniteSeq<_> = fseq <| seq { use e = (xs :> IEnumerable<_>).GetEnumerator() let mutable continueLoop = true @@ -1729,10 +1731,12 @@ module FSeq = /// /// Returns the sequence through the first element for which the given function returns True. - /// Like takeWhile, but includes the element for which the predicate returns True. + /// Like takeWhile, but with an inverted predicate and + /// also includes the element for which the predicate first returns True. + /// Like find, but computes on-demand and returns the sequence of intermediary result through the found element. /// If the sequence is exhausted without finding a matching element, the entire sequence is returned. /// - let inline takeWhileIncluding predicate (xs : _ fseq) : _ fseq = FiniteSeq.takeWhileIncluding predicate xs + let inline takeUntilIncluding predicate (xs : _ fseq) : _ fseq = FiniteSeq.takeUntilIncluding predicate xs /// /// O(1). Return option the list corresponding to the remaining items in the sequence. @@ -2455,12 +2459,13 @@ module FSeq = /// /// Returns the sequence through the first element for which the given function returns True. - /// Like takeWhile, but includes the element for which the predicate returns True. + /// Like takeWhile, but with an inverted predicate and + /// also includes the element for which the predicate first returns True. /// Like find, but computes on-demand and returns the sequence of intermediary result through the found element. /// If the sequence is exhausted without finding a matching element, the entire sequence is returned. /// - let takeWhileIncluding predicate (NonEmpty xs : NonEmptyFSeq<_>) : NonEmptyFSeq<_> = - NonEmpty (FiniteSeq.takeWhileIncluding predicate xs) + let takeUntilIncluding predicate (NonEmpty xs : NonEmptyFSeq<_>) : NonEmptyFSeq<_> = + NonEmpty (FiniteSeq.takeUntilIncluding predicate xs) type ZipperExpression() = member inline this.MergeSources(t1, t2) = diff --git a/SafetyFirst/List.fs b/SafetyFirst/List.fs index ccaf1a3..2c75da6 100644 --- a/SafetyFirst/List.fs +++ b/SafetyFirst/List.fs @@ -815,10 +815,12 @@ let inline tryTake count xs = takeSafe count xs |> Result.toOption /// /// Returns the list through the first element for which the given function returns True. -/// Like takeWhile, but includes the element for which the predicate returns True. +/// Like takeWhile, but with an inverted predicate and +/// also includes the element for which the predicate first returns True. +/// Like find, but returns the list of intermediary result through the found element. /// If the list is exhausted without finding a matching element, the entire list is returned. /// -let takeWhileIncluding predicate xs = +let takeUntilIncluding predicate xs = let rec take' acc = function | [] -> List.rev acc | head :: tail -> @@ -1509,12 +1511,13 @@ module NonEmpty = /// /// Returns the list through the first element for which the given function returns True. - /// Like takeWhile, but includes the element for which the predicate returns True. - /// Like find, but computes on-demand and returns the list of intermediary result through the found element. + /// Like takeWhile, but with an inverted predicate and + /// also includes the element for which the predicate first returns True. + /// Like find, but returns the list of intermediary result through the found element. /// If the list is exhausted without finding a matching element, the entire list is returned. /// - let takeWhileIncluding predicate (NonEmpty xs: NonEmptyList<_>) : NonEmptyList<_> = - NonEmpty (takeWhileIncluding predicate xs) + let takeUntilIncluding predicate (NonEmpty xs: NonEmptyList<_>) : NonEmptyList<_> = + NonEmpty (takeUntilIncluding predicate xs) type ZipperExpression() = member inline this.MergeSources(t1, t2) = diff --git a/SafetyFirst/Seq.fs b/SafetyFirst/Seq.fs index 84d47f8..3fbbf1f 100644 --- a/SafetyFirst/Seq.fs +++ b/SafetyFirst/Seq.fs @@ -532,11 +532,12 @@ let splitIntoN (PositiveInt count) xs = Seq.splitInto count xs /// /// Returns the sequence through the first element for which the given function returns True. -/// Like takeWhile, but includes the element for which the predicate returns True. +/// Like takeWhile, but with an inverted predicate and +/// also includes the element for which the predicate first returns True. /// Like find, but computes on-demand and returns the sequence of intermediary result through the found element. /// If the sequence is exhausted without finding a matching element, the entire sequence is returned. /// -let takeWhileIncluding predicate (xs: _ seq) = +let takeUntilIncluding predicate (xs: _ seq) = seq { use e = xs.GetEnumerator() let mutable continueLoop = true @@ -567,7 +568,7 @@ let takeWhileIncluding predicate (xs: _ seq) = /// let split splitAfter xs : seq> = let takeGroup (NonEmpty xs: NonEmptySeq<_>) : NonEmptySeq<_> = - NonEmpty <| takeWhileIncluding splitAfter xs + NonEmpty <| takeUntilIncluding splitAfter xs let rec split' xs = seq { @@ -909,11 +910,12 @@ module NonEmpty = /// /// Returns the sequence through the first element for which the given function returns True. - /// Like takeWhile, but includes the element for which the predicate returns True. - /// Like find, but computes on-demand and returns the sequence of intermediary result through the found element. + /// Like takeWhile, but with an inverted predicate and + /// also includes the element for which the predicate first returns True. + /// Like find, but computes on-demand and returns the sequence of intermediary result through the found element. /// If the sequence is exhausted without finding a matching element, the entire sequence is returned. /// - let takeWhileIncluding predicate (NonEmpty xs) : NonEmptySeq<_> = NonEmpty (takeWhileIncluding predicate xs) + let takeUntilIncluding predicate (NonEmpty xs) : NonEmptySeq<_> = NonEmpty (takeUntilIncluding predicate xs) /// /// Builds an array from the given collection. From 96191c570557af31f3ab9c7553fe7e241b49270a Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Fri, 20 Mar 2026 15:25:59 -0400 Subject: [PATCH 5/9] Capitalize a few module names --- SafetyFirst.Specs/ArraySpec.fs | 2 +- SafetyFirst.Specs/FSeqSpec.fs | 2 +- SafetyFirst.Specs/ListSpec.fs | 2 +- SafetyFirst.Specs/SeqSpec.fs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/SafetyFirst.Specs/ArraySpec.fs b/SafetyFirst.Specs/ArraySpec.fs index 6b7d211..08bbfcf 100644 --- a/SafetyFirst.Specs/ArraySpec.fs +++ b/SafetyFirst.Specs/ArraySpec.fs @@ -226,7 +226,7 @@ module Splitting = (Array.split ((=) 5) [|5;0;0;5;5;0;5|] |> toArrs) = [|[|5|]; [|0;0;5|]; [|5|]; [|0;5|]|] @> -module takeUntilIncluding = +module TakeUntilIncluding = [] let ``NonEmpty.takeUntilIncluding returns through the first matching element`` () = test diff --git a/SafetyFirst.Specs/FSeqSpec.fs b/SafetyFirst.Specs/FSeqSpec.fs index 2d0875d..6cfc2f7 100644 --- a/SafetyFirst.Specs/FSeqSpec.fs +++ b/SafetyFirst.Specs/FSeqSpec.fs @@ -390,7 +390,7 @@ module Splitting = (FSeq.split ((=) 5) (fseq [5;0;0;5;5;0;5]) |> ofNonEmpty) = [[5]; [0;0;5]; [5]; [0;5]] @> -module takeUntilIncluding = +module TakeUntilIncluding = [] let ``NonEmpty.takeUntilIncluding returns through the first matching element`` () = test diff --git a/SafetyFirst.Specs/ListSpec.fs b/SafetyFirst.Specs/ListSpec.fs index f65fa99..6516a65 100644 --- a/SafetyFirst.Specs/ListSpec.fs +++ b/SafetyFirst.Specs/ListSpec.fs @@ -274,7 +274,7 @@ module Splitting = = List.NonEmpty.create 1 [2;3] @> -module takeUntilIncluding = +module TakeUntilIncluding = [] let ``returns empty for empty input`` () = test <@ List.takeUntilIncluding (fun _ -> true) [] = [] @> diff --git a/SafetyFirst.Specs/SeqSpec.fs b/SafetyFirst.Specs/SeqSpec.fs index aa14be7..f4a757a 100644 --- a/SafetyFirst.Specs/SeqSpec.fs +++ b/SafetyFirst.Specs/SeqSpec.fs @@ -200,7 +200,7 @@ let ``isHungAfter throws for finite sequences that exceed the limit`` () = raises <@ [1..11] |> Seq.isHungAfter 10 |> Seq.toList @> -module takeUntilIncluding = +module TakeUntilIncluding = [] let ``returns empty for empty input`` () = test <@ Seq.takeUntilIncluding (fun _ -> true) Seq.empty |> Seq.toList = [] @> From 6809ac9e332220c8332300bbdd78a2fe7662ba51 Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Fri, 20 Mar 2026 15:34:40 -0400 Subject: [PATCH 6/9] Add skipUntilIncluding to InfiniteSeq --- SafetyFirst.Specs/InfiniteSeqSpec.fs | 6 ++++++ SafetyFirst/InfiniteSeq.fs | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/SafetyFirst.Specs/InfiniteSeqSpec.fs b/SafetyFirst.Specs/InfiniteSeqSpec.fs index 030e494..2379f4a 100644 --- a/SafetyFirst.Specs/InfiniteSeqSpec.fs +++ b/SafetyFirst.Specs/InfiniteSeqSpec.fs @@ -108,6 +108,12 @@ let ``skipping does not hang`` () = illFormedList |> InfiniteSeq.skipWhile (fun i -> i < 10) |> take 5 |> Result.isError && wellFormedList |> InfiniteSeq.skipWhile (always true) |> take 1 |> Result.isError + && + wellFormedList |> InfiniteSeq.skipUntilIncluding (fun i -> i = 10) |> take 5 = Ok [11 .. 15] + && + illFormedList |> InfiniteSeq.skipUntilIncluding (fun i -> i = 10) |> take 5 |> Result.isError + && + wellFormedList |> InfiniteSeq.skipUntilIncluding (fun i -> i < 0) |> take 1 |> Result.isError @> [] diff --git a/SafetyFirst/InfiniteSeq.fs b/SafetyFirst/InfiniteSeq.fs index a3fa1a6..7243311 100644 --- a/SafetyFirst/InfiniteSeq.fs +++ b/SafetyFirst/InfiniteSeq.fs @@ -308,6 +308,15 @@ module InfiniteSeq = /// let skipWhile predicate (InfiniteSeq xs) = InfiniteSeq (Seq.skipWhile predicate xs) + /// + /// Returns a sequence that, when iterated, skips elements of the underlying sequence + /// up to and including the first element for which the given predicate returns True, + /// and then yields the remaining elements of the sequence. + /// Like skipWhile, but with an inverted predicate and + /// also skips the element for which the predicate first returns True. + /// + let skipUntilIncluding predicate (InfiniteSeq xs) = InfiniteSeq (Seq.skipUntilIncluding predicate xs) + /// /// Returns the first element of the sequence. /// This function can hang for an unbounded InfiniteSeq (created with initUnbounded or assume). From cc0f7849ac47572300eb0fec5fa5390544a90c6a Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Fri, 20 Mar 2026 15:38:29 -0400 Subject: [PATCH 7/9] Update the release notes --- ReleaseNotes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index a35d4b1..7a91b4f 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -16,14 +16,16 @@ New functions include: - `initUnbounded`: Create an "unsafe" InfiniteSeq that can hang if misused - `isHungAfter`: apply a new upper bound to any InfiniteSeq - `assume`: assume an existing seq is infinite -- `append`: prepend any seq to the front of an infinite seq +- `append`: prepend any seq to the front of an InfiniteSeq - `item`: same as `Seq.item`, but safe for infinite sequences (barring a hang) - `take`: same as `Seq.take`, but safe for infinite sequences (barring a hang) - `takeWhile`: same as `Seq.takeWhile`, but safe for infinite sequences (barring a hang) - `head`: same as `Seq.head`, but safe for infinite sequences (barring a hang) - `uncons`: same as `Seq.uncons`, but safe for infinite sequences (barring a hang) - `find`: same as `Seq.find`, but safe for infinite sequences (barring a hang) +- `skipUntilIncluding`: same as `Seq.skipUntilIncluding` - `splitPairwise`: same as `Seq.splitPairwise` +- `split`: same as `Seq.split` Also `Seq.isHungAfter` exists to take a potentially infinite seq that _isn't_ defined as an `InfiniteSeq` and apply an upper bound to consider the sequence hung if it produces more elements than some max number. From 4debfef4b93abc37f741ea6b53658eee2b9099fb Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Fri, 20 Mar 2026 16:31:01 -0400 Subject: [PATCH 8/9] Don't evaluate predicate unnecessarily when running skipUntilIncluding --- SafetyFirst/FiniteSeqModule.fs | 2 +- SafetyFirst/Seq.fs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SafetyFirst/FiniteSeqModule.fs b/SafetyFirst/FiniteSeqModule.fs index b2c81db..024819d 100644 --- a/SafetyFirst/FiniteSeqModule.fs +++ b/SafetyFirst/FiniteSeqModule.fs @@ -688,7 +688,7 @@ module FiniteSeq = if startYielding then yield e.Current - if predicate e.Current then + if not startYielding && predicate e.Current then startYielding <- true } diff --git a/SafetyFirst/Seq.fs b/SafetyFirst/Seq.fs index 3fbbf1f..2c1b64b 100644 --- a/SafetyFirst/Seq.fs +++ b/SafetyFirst/Seq.fs @@ -490,7 +490,7 @@ let skipUntilIncluding predicate (xs: _ seq) = if startYielding then yield e.Current - if predicate e.Current then + if not startYielding && predicate e.Current then startYielding <- true } From 5350f9af2d256039bef7ec58c22b5613326f529b Mon Sep 17 00:00:00 2001 From: nathan wilson Date: Fri, 20 Mar 2026 16:32:36 -0400 Subject: [PATCH 9/9] Spelling Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- SafetyFirst/FiniteSeqModule.fs | 2 +- SafetyFirst/InfiniteSeq.fs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/SafetyFirst/FiniteSeqModule.fs b/SafetyFirst/FiniteSeqModule.fs index 024819d..941d7f2 100644 --- a/SafetyFirst/FiniteSeqModule.fs +++ b/SafetyFirst/FiniteSeqModule.fs @@ -2440,7 +2440,7 @@ module FSeq = /// /// // this implementation is faster than the version in Seq.NonEmpty, but is unsafe for infinite sequences - // so this should be the default used for any finite sequence (inculding lists and arrays) + // so this should be the default used for any finite sequence (including lists and arrays) let split splitAfter (NonEmpty xs: NonEmptyFSeq<_>) : NonEmptyFSeq> = NonEmpty (FiniteSeq.split splitAfter xs) diff --git a/SafetyFirst/InfiniteSeq.fs b/SafetyFirst/InfiniteSeq.fs index 7243311..ab7b4ea 100644 --- a/SafetyFirst/InfiniteSeq.fs +++ b/SafetyFirst/InfiniteSeq.fs @@ -541,7 +541,7 @@ module InfiniteSeq = /// the sequence preceding the split. /// For example: /// - /// IniniteSeq.split ((=) 100) (seq {1;2;3;100;100;4;100;5;6;...}) + /// InfiniteSeq.split ((=) 100) (seq {1;2;3;100;100;4;100;5;6;...}) /// //returns ([[1;2;3;100];[100];[4;100];[5;6];...]) /// ///