diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 5d48541..7a91b4f 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` +- `takeUntilIncluding` +- `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. @@ -12,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. diff --git a/SafetyFirst.Specs/ArraySpec.fs b/SafetyFirst.Specs/ArraySpec.fs index fd66f4f..08bbfcf 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 TakeUntilIncluding = + [] + let ``NonEmpty.takeUntilIncluding returns through the first matching element`` () = + test + <@ + Array.NonEmpty.takeUntilIncluding ((=) 3) (Array.NonEmpty.create 1 [|2;3;4;5|]) + = Array.NonEmpty.create 1 [|2;3|] + && + Array.NonEmpty.takeUntilIncluding ((=) 1) (Array.NonEmpty.create 1 [|2;3;4;5|]) + = Array.NonEmpty.singleton 1 + && + Array.NonEmpty.takeUntilIncluding ((=) 99) (Array.NonEmpty.create 1 [|2;3|]) + = Array.NonEmpty.create 1 [|2;3|] + @> + + [] + let ``returns empty for empty input`` () = + test <@ Array.takeUntilIncluding (fun _ -> true) [||] = [||] @> + + [] + let ``returns through the first matching element`` () = + test <@ Array.takeUntilIncluding ((=) 3) [|1;2;3;4;5|] = [|1;2;3|] @> + + [] + let ``returns only the first element when it matches`` () = + test <@ Array.takeUntilIncluding ((=) 3) [|3;4;5|] = [|3|] @> + + [] + let ``stops at the first match even when multiple elements match`` () = + test <@ Array.takeUntilIncluding ((=) 3) [|1;3;3;3|] = [|1;3|] @> + + [] + let ``returns the full array when no element matches`` () = + test <@ Array.takeUntilIncluding ((=) 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 ``takeUntilIncluding and skipUntilIncluding partition the array`` () = + let xs = [|1;2;3;4;5|] + 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 3ac630b..6cfc2f7 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 TakeUntilIncluding = + [] + let ``NonEmpty.takeUntilIncluding returns through the first matching element`` () = + test + <@ + FSeq.NonEmpty.takeUntilIncluding ((=) 3) (Splitting.toNonEmpty [1;2;3;4;5]) + = Splitting.toNonEmpty [1;2;3] + && + FSeq.NonEmpty.takeUntilIncluding ((=) 1) (Splitting.toNonEmpty [1;2;3;4;5]) + = Splitting.toNonEmpty [1] + && + FSeq.NonEmpty.takeUntilIncluding ((=) 99) (Splitting.toNonEmpty [1;2;3]) + = Splitting.toNonEmpty [1;2;3] + @> + + [] + let ``returns empty for empty input`` () = + test <@ FSeq.takeUntilIncluding (fun _ -> true) (fseq []) = fseq [] @> + + [] + let ``returns through the first matching element`` () = + 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.takeUntilIncluding ((=) 3) (fseq [3;4;5]) = fseq [3] @> + + [] + let ``stops at the first match even when multiple elements match`` () = + test <@ FSeq.takeUntilIncluding ((=) 3) (fseq [1;3;3;3]) = fseq [1;3] @> + + [] + let ``returns the full sequence when no element matches`` () = + test <@ FSeq.takeUntilIncluding ((=) 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 ``takeUntilIncluding and skipUntilIncluding partition the sequence`` () = + let xs = fseq [1;2;3;4;5] + let taken = FSeq.takeUntilIncluding ((=) 3) xs + let skipped = FSeq.skipUntilIncluding ((=) 3) xs + test <@ FSeq.append taken skipped = xs @> + module SafeFunctions = open SeqSpec diff --git a/SafetyFirst.Specs/InfiniteSeqSpec.fs b/SafetyFirst.Specs/InfiniteSeqSpec.fs index 9a2d205..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 @> [] @@ -360,6 +366,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.Specs/ListSpec.fs b/SafetyFirst.Specs/ListSpec.fs index 7dca2ae..6516a65 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.takeUntilIncluding returns through the first matching element`` () = + test + <@ + List.NonEmpty.takeUntilIncluding ((=) 3) (List.NonEmpty.create 1 [2;3;4;5]) + = List.NonEmpty.create 1 [2;3] + && + List.NonEmpty.takeUntilIncluding ((=) 1) (List.NonEmpty.create 1 [2;3;4;5]) + = List.NonEmpty.singleton 1 + && + List.NonEmpty.takeUntilIncluding ((=) 99) (List.NonEmpty.create 1 [2;3]) + = List.NonEmpty.create 1 [2;3] + @> + +module TakeUntilIncluding = + [] + let ``returns empty for empty input`` () = + test <@ List.takeUntilIncluding (fun _ -> true) [] = [] @> + + [] + let ``returns through the first matching element`` () = + test <@ List.takeUntilIncluding ((=) 3) [1;2;3;4;5] = [1;2;3] @> + + [] + let ``returns only the first element when it matches`` () = + test <@ List.takeUntilIncluding ((=) 3) [3;4;5] = [3] @> + + [] + let ``stops at the first match even when multiple elements match`` () = + test <@ List.takeUntilIncluding ((=) 3) [1;3;3;3] = [1;3] @> + + [] + let ``returns the full list when no element matches`` () = + test <@ List.takeUntilIncluding ((=) 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 ``takeUntilIncluding and skipUntilIncluding partition the list`` () = + let xs = [1;2;3;4;5] + 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 3444b9e..f4a757a 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 TakeUntilIncluding = + [] + let ``returns empty for empty input`` () = + test <@ Seq.takeUntilIncluding (fun _ -> true) Seq.empty |> Seq.toList = [] @> + + [] + let ``returns through the first matching element`` () = + 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.takeUntilIncluding ((=) 3) [3;4;5] |> Seq.toList = [3] @> + + [] + let ``stops at the first match even when multiple elements match`` () = + test <@ Seq.takeUntilIncluding ((=) 3) [1;3;3;3] |> Seq.toList = [1;3] @> + + [] + let ``returns the full sequence when no element matches`` () = + 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.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.takeUntilIncluding ((=) 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 ``takeUntilIncluding and skipUntilIncluding partition the sequence`` () = + let xs = [1;2;3;4;5] + let taken = Seq.takeUntilIncluding ((=) 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..67db3d4 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 preceding 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,18 @@ 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 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 takeUntilIncluding 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 +1520,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 +1534,17 @@ 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 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 takeUntilIncluding predicate (NonEmpty xs : NonEmptyArray<_>) : NonEmptyArray<_> = + NonEmpty (takeUntilIncluding 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..941d7f2 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 not startYielding && 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 preceding 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,23 @@ 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 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 takeUntilIncluding 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 +1648,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 +1696,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 preceding 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 +1729,15 @@ 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 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 takeUntilIncluding predicate (xs : _ fseq) : _ fseq = FiniteSeq.takeUntilIncluding 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. @@ -2337,7 +2432,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]) @@ -2345,28 +2440,9 @@ 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 []) + // 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) /// /// Splits a sequence between each pair of adjacent elements that satisfy splitBetween. @@ -2381,7 +2457,17 @@ 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 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 takeUntilIncluding predicate (NonEmpty xs : NonEmptyFSeq<_>) : NonEmptyFSeq<_> = + NonEmpty (FiniteSeq.takeUntilIncluding predicate xs) + + type ZipperExpression() = member inline this.MergeSources(t1, t2) = zip t1 t2 diff --git a/SafetyFirst/InfiniteSeq.fs b/SafetyFirst/InfiniteSeq.fs index 454d20e..ab7b4ea 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). @@ -525,25 +534,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;...}) + /// InfiniteSeq.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 66806de..2c75da6 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 preceding 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,21 @@ 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 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 takeUntilIncluding 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. @@ -1443,10 +1495,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. @@ -1459,7 +1509,17 @@ 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 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 takeUntilIncluding predicate (NonEmpty xs: NonEmptyList<_>) : NonEmptyList<_> = + NonEmpty (takeUntilIncluding 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..2c1b64b 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 not startYielding && 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,58 @@ 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 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 takeUntilIncluding 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 preceding 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 <| takeUntilIncluding 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 +908,15 @@ 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 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 takeUntilIncluding predicate (NonEmpty xs) : NonEmptySeq<_> = NonEmpty (takeUntilIncluding predicate xs) + /// /// Builds an array from the given collection. /// @@ -899,47 +981,15 @@ 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]) /// //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.