diff --git a/release-notes.txt b/release-notes.txt index 7078840..0930bca 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -2,6 +2,7 @@ Release notes: 1.0.0 + - adds TaskSeq.findBack, findBackAsync, tryFindBack, tryFindBackAsync, findIndexBack, findIndexBackAsync, tryFindIndexBack, tryFindIndexBackAsync - fixes: TaskSeq.insertAt, insertManyAt, removeAt, removeManyAt, updateAt now raise ArgumentNullException (not NullReferenceException) when given a null source; insertManyAt also validates the values argument - refactor: simplify lengthBy and lengthBeforeMax to use while! and remove the redundant mutable 'go' and initial MoveNextAsync - adds TaskSeq.distinctUntilChangedWith and TaskSeq.distinctUntilChangedWithAsync, #345 diff --git a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj index 58856f1..cdd301a 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -25,6 +25,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.FindBack.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.FindBack.Tests.fs new file mode 100644 index 0000000..11921bb --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.FindBack.Tests.fs @@ -0,0 +1,345 @@ +module TaskSeq.Tests.FindBack + +open System.Collections.Generic + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// TaskSeq.findBack +// TaskSeq.findBackAsync +// TaskSeq.tryFindBack +// TaskSeq.tryFindBackAsync +// TaskSeq.findIndexBack +// TaskSeq.findIndexBackAsync +// TaskSeq.tryFindIndexBack +// TaskSeq.tryFindIndexBackAsync +// + +module EmptySeq = + [] + let ``Null source is invalid`` () = + assertNullArg + <| fun () -> TaskSeq.findBack (fun _ -> false) null + + assertNullArg + <| fun () -> TaskSeq.findBackAsync (fun _ -> Task.fromResult false) null + + assertNullArg + <| fun () -> TaskSeq.tryFindBack (fun _ -> false) null + + assertNullArg + <| fun () -> TaskSeq.tryFindBackAsync (fun _ -> Task.fromResult false) null + + assertNullArg + <| fun () -> TaskSeq.findIndexBack (fun _ -> false) null + + assertNullArg + <| fun () -> TaskSeq.findIndexBackAsync (fun _ -> Task.fromResult false) null + + assertNullArg + <| fun () -> TaskSeq.tryFindIndexBack (fun _ -> false) null + + assertNullArg + <| fun () -> TaskSeq.tryFindIndexBackAsync (fun _ -> Task.fromResult false) null + + [)>] + let ``TaskSeq-findBack raises KeyNotFoundException`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.findBack ((=) 12) + |> Task.ignore + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-findBackAsync raises KeyNotFoundException`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.findBackAsync (fun x -> task { return x = 12 }) + |> Task.ignore + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-tryFindBack returns None`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.tryFindBack ((=) 12) + |> Task.map (should be None') + + [)>] + let ``TaskSeq-tryFindBackAsync returns None`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.tryFindBackAsync (fun x -> task { return x = 12 }) + |> Task.map (should be None') + + [)>] + let ``TaskSeq-findIndexBack raises KeyNotFoundException`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.findIndexBack ((=) 12) + |> Task.ignore + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-findIndexBackAsync raises KeyNotFoundException`` variant = + fun () -> + Gen.getEmptyVariant variant + |> TaskSeq.findIndexBackAsync (fun x -> task { return x = 12 }) + |> Task.ignore + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-tryFindIndexBack returns None`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.tryFindIndexBack ((=) 12) + |> Task.map (should be None') + + [)>] + let ``TaskSeq-tryFindIndexBackAsync returns None`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.tryFindIndexBackAsync (fun x -> task { return x = 12 }) + |> Task.map (should be None') + +module Immutable = + [)>] + let ``TaskSeq-findBack sad path raises KeyNotFoundException`` variant = + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.findBack ((=) 0) // dummy tasks sequence starts at 1 + |> Task.ignore + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-findBackAsync sad path raises KeyNotFoundException`` variant = + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.findBackAsync (fun x -> task { return x = 0 }) // dummy tasks sequence starts at 1 + |> Task.ignore + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-findBack happy path returns last match`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findBack (fun x -> x < 6 && x > 3) // matches 4, 5 — last is 5 + |> Task.map (should equal 5) + + [)>] + let ``TaskSeq-findBackAsync happy path returns last match`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findBackAsync (fun x -> task { return x < 6 && x > 3 }) // matches 4, 5 — last is 5 + |> Task.map (should equal 5) + + [)>] + let ``TaskSeq-findBack happy path returns last item`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findBack (fun x -> x <= 10) // all items qualify; last is 10 + |> Task.map (should equal 10) + + [)>] + let ``TaskSeq-findBackAsync happy path returns last item`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findBackAsync (fun x -> task { return x <= 10 }) // all items qualify; last is 10 + |> Task.map (should equal 10) + + [)>] + let ``TaskSeq-findBack happy path returns only matching item`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findBack ((=) 7) // exactly one match + |> Task.map (should equal 7) + + [)>] + let ``TaskSeq-findIndexBack sad path raises KeyNotFoundException`` variant = + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.findIndexBack ((=) 0) // dummy tasks sequence starts at 1 + |> Task.ignore + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-findIndexBackAsync sad path raises KeyNotFoundException`` variant = + fun () -> + Gen.getSeqImmutable variant + |> TaskSeq.findIndexBackAsync (fun x -> task { return x = 0 }) // dummy tasks sequence starts at 1 + |> Task.ignore + + |> should throwAsyncExact typeof + + [)>] + let ``TaskSeq-findIndexBack happy path returns last matching index`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findIndexBack (fun x -> x < 6 && x > 3) // matches indices 3, 4 — last is 4 + |> Task.map (should equal 4) + + [)>] + let ``TaskSeq-findIndexBackAsync happy path returns last matching index`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findIndexBackAsync (fun x -> task { return x < 6 && x > 3 }) // matches indices 3, 4 — last is 4 + |> Task.map (should equal 4) + + [)>] + let ``TaskSeq-findIndexBack happy path returns last index when all match`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findIndexBack (fun x -> x <= 10) // all 10 items qualify; last index is 9 + |> Task.map (should equal 9) + + [)>] + let ``TaskSeq-findIndexBack happy path single match`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.findIndexBack ((=) 1) // value 1 is at index 0 + |> Task.map (should equal 0) + + [)>] + let ``TaskSeq-tryFindBack sad path returns None`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindBack ((=) 0) // dummy tasks sequence starts at 1 + |> Task.map (should be None') + + [)>] + let ``TaskSeq-tryFindBackAsync sad path returns None`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindBackAsync (fun x -> task { return x = 0 }) // dummy tasks sequence starts at 1 + |> Task.map (should be None') + + [)>] + let ``TaskSeq-tryFindBack happy path returns last match`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindBack (fun x -> x < 6 && x > 3) + |> Task.map (should equal (Some 5)) + + [)>] + let ``TaskSeq-tryFindBackAsync happy path returns last match`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindBackAsync (fun x -> task { return x < 6 && x > 3 }) + |> Task.map (should equal (Some 5)) + + [)>] + let ``TaskSeq-tryFindIndexBack sad path returns None`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindIndexBack ((=) 0) + |> Task.map (should be None') + + [)>] + let ``TaskSeq-tryFindIndexBackAsync sad path returns None`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindIndexBackAsync (fun x -> task { return x = 0 }) + |> Task.map (should be None') + + [)>] + let ``TaskSeq-tryFindIndexBack happy path returns last matching index`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindIndexBack (fun x -> x < 6 && x > 3) + |> Task.map (should equal (Some 4)) + + [)>] + let ``TaskSeq-tryFindIndexBackAsync happy path returns last matching index`` variant = + Gen.getSeqImmutable variant + |> TaskSeq.tryFindIndexBackAsync (fun x -> task { return x < 6 && x > 3 }) + |> Task.map (should equal (Some 4)) + +module SideEffects = + [)>] + let ``TaskSeq-findBack consumes the entire sequence`` variant = + Gen.getSeqWithSideEffect variant + |> TaskSeq.findBack (fun x -> x < 6 && x > 3) + |> Task.map (should equal 5) + + [)>] + let ``TaskSeq-tryFindBack consumes the entire sequence`` variant = + Gen.getSeqWithSideEffect variant + |> TaskSeq.tryFindBack (fun x -> x < 6 && x > 3) + |> Task.map (should equal (Some 5)) + + [)>] + let ``TaskSeq-findIndexBack consumes the entire sequence`` variant = + Gen.getSeqWithSideEffect variant + |> TaskSeq.findIndexBack (fun x -> x < 6 && x > 3) + |> Task.map (should equal 4) + + [)>] + let ``TaskSeq-tryFindIndexBack consumes the entire sequence`` variant = + Gen.getSeqWithSideEffect variant + |> TaskSeq.tryFindIndexBack (fun x -> x < 6 && x > 3) + |> Task.map (should equal (Some 4)) + + [] + let ``TaskSeq-findBack _specialcase_ unlike findBack, findBack always evaluates the full sequence`` () = task { + let mutable i = 0 + + let ts = taskSeq { + yield 42 + i <- i + 1 // side effect after the matching yield + yield 1 // an item after the match + } + + // findBack must find the LAST match so it evaluates everything + let! found = ts |> TaskSeq.findBack ((=) 42) + found |> should equal 42 + i |> should equal 1 // side effect WAS executed — findBack consumed all items + } + + [] + let ``TaskSeq-tryFindBack _specialcase_ always evaluates the full sequence`` () = task { + let mutable i = 0 + + let ts = taskSeq { + yield 42 + i <- i + 1 + yield 1 + } + + let! found = ts |> TaskSeq.tryFindBack ((=) 42) + found |> should equal (Some 42) + i |> should equal 1 // side effect WAS executed + } + + [] + let ``TaskSeq-findBack _specialcase_ returns the last of multiple matches`` () = task { + let ts = taskSeq { yield! [ 3; 5; 3; 7; 3 ] } + + let! found = ts |> TaskSeq.findBack ((=) 3) + found |> should equal 3 // value is 3 (last of three matches) + } + + [] + let ``TaskSeq-findIndexBack _specialcase_ returns the last matching index among multiple matches`` () = task { + let ts = taskSeq { yield! [ 3; 5; 3; 7; 3 ] } + + let! found = ts |> TaskSeq.findIndexBack ((=) 3) + found |> should equal 4 // index 4 is the last 3 + } + + [] + let ``TaskSeq-tryFindBack _specialcase_ returns last match when multiple exist`` () = task { + let ts = taskSeq { yield! [ 1; 2; 3; 4; 5; 4; 3; 2; 1 ] } + + let! found = ts |> TaskSeq.tryFindBack (fun x -> x > 3) + found |> should equal (Some 4) // last element > 3 is the 4 at index 5 + } + + [] + let ``TaskSeq-tryFindIndexBack _specialcase_ returns last matching index when multiple exist`` () = task { + let ts = taskSeq { yield! [ 1; 2; 3; 4; 5; 4; 3; 2; 1 ] } + + let! found = ts |> TaskSeq.tryFindIndexBack (fun x -> x > 3) + found |> should equal (Some 5) // last element > 3 is at index 5 + } + + [] + let ``TaskSeq-findIndexBack _specialcase_ single-element sequence`` () = task { + let ts = taskSeq { yield 42 } + + let! found = ts |> TaskSeq.findIndexBack ((=) 42) + found |> should equal 0 + } + + [] + let ``TaskSeq-tryFindBack _specialcase_ single-element no match returns None`` () = task { + let ts = taskSeq { yield 42 } + + let! found = ts |> TaskSeq.tryFindBack ((=) 99) + found |> should be None' + } diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index 9051ddc..c567c31 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -469,6 +469,11 @@ type TaskSeq private () = static member tryFindIndex predicate source = Internal.tryFindIndex (Predicate predicate) source static member tryFindIndexAsync predicate source = Internal.tryFindIndex (PredicateAsync predicate) source + static member tryFindBack predicate source = Internal.tryFindBack (Predicate predicate) source + static member tryFindBackAsync predicate source = Internal.tryFindBack (PredicateAsync predicate) source + static member tryFindIndexBack predicate source = Internal.tryFindIndexBack (Predicate predicate) source + static member tryFindIndexBackAsync predicate source = Internal.tryFindIndexBack (PredicateAsync predicate) source + static member insertAt index value source = Internal.insertAt index (One value) source static member insertManyAt index values source = Internal.insertAt index (Many values) source static member removeAt index source = Internal.removeAt index source @@ -524,6 +529,22 @@ type TaskSeq private () = Internal.tryFindIndex (PredicateAsync predicate) source |> Task.map (Option.defaultWith Internal.raiseNotFound) + static member findBack predicate source = + Internal.tryFindBack (Predicate predicate) source + |> Task.map (Option.defaultWith Internal.raiseNotFound) + + static member findBackAsync predicate source = + Internal.tryFindBack (PredicateAsync predicate) source + |> Task.map (Option.defaultWith Internal.raiseNotFound) + + static member findIndexBack predicate source = + Internal.tryFindIndexBack (Predicate predicate) source + |> Task.map (Option.defaultWith Internal.raiseNotFound) + + static member findIndexBackAsync predicate source = + Internal.tryFindIndexBack (PredicateAsync predicate) source + |> Task.map (Option.defaultWith Internal.raiseNotFound) + // // zip/unzip/fold etc functions // diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index f914048..7542081 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -1365,6 +1365,58 @@ type TaskSeq = /// Thrown when the input task sequence is null. static member tryFindIndexAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task + /// + /// Returns the last element for which the given function returns + /// . Returns if no such element exists. + /// The entire sequence is consumed. If is asynchronous, consider + /// using . + /// + /// + /// A function that evaluates to a when given an item in the sequence. + /// The input task sequence. + /// The last element for which the predicate returns , or . + /// Thrown when the input task sequence is null. + static member tryFindBack: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<'T option> + + /// + /// Returns the last element for which the given asynchronous function returns + /// . Returns if no such element exists. + /// The entire sequence is consumed. If is synchronous, consider + /// using . + /// + /// + /// An asynchronous function that evaluates to a when given an item in the sequence. + /// The input task sequence. + /// The last element for which the predicate returns , or . + /// Thrown when the input task sequence is null. + static member tryFindBackAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task<'T option> + + /// + /// Returns the index, starting from zero, of the last element for which the given function + /// returns . Returns if no such element exists. + /// The entire sequence is consumed. If is asynchronous, consider + /// using . + /// + /// + /// A function that evaluates to a when given an item in the sequence. + /// The input task sequence. + /// The last index for which the predicate returns , or . + /// Thrown when the input task sequence is null. + static member tryFindIndexBack: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task + + /// + /// Returns the index, starting from zero, of the last element for which the given asynchronous function + /// returns . Returns if no such element exists. + /// The entire sequence is consumed. If is synchronous, consider + /// using . + /// + /// + /// An asynchronous function that evaluates to a when given an item in the sequence. + /// The input task sequence. + /// The last index for which the predicate returns , or . + /// Thrown when the input task sequence is null. + static member tryFindIndexBackAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task + /// /// Applies the given function to successive elements, returning the first result where /// the function returns . Throws an exception if none is found. @@ -1443,6 +1495,58 @@ type TaskSeq = /// Thrown if no element returns when evaluated by the function. static member findIndexAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task + /// + /// Returns the last element for which the given function returns . + /// Throws an exception if none is found. The entire sequence is consumed. + /// If is asynchronous, consider using . + /// + /// + /// A function that evaluates to a when given an item in the sequence. + /// The input task sequence. + /// The last element for which the predicate returns . + /// Thrown when the input task sequence is null. + /// Thrown if no element returns when evaluated by the function. + static member findBack: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task<'T> + + /// + /// Returns the last element for which the given asynchronous function returns . + /// Throws an exception if none is found. The entire sequence is consumed. + /// If is synchronous, consider using . + /// + /// + /// An asynchronous function that evaluates to a when given an item in the sequence. + /// The input task sequence. + /// The last element for which the predicate returns . + /// Thrown when the input task sequence is null. + /// Thrown if no element returns when evaluated by the function. + static member findBackAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task<'T> + + /// + /// Returns the index, starting from zero, of the last element for which the given function + /// returns . Throws an exception if none is found. The entire sequence is consumed. + /// If is asynchronous, consider using . + /// + /// + /// A function that evaluates to a when given an item in the sequence. + /// The input task sequence. + /// The last index for which the predicate returns . + /// Thrown when the input task sequence is null. + /// Thrown if no element returns when evaluated by the function. + static member findIndexBack: predicate: ('T -> bool) -> source: TaskSeq<'T> -> Task + + /// + /// Returns the index, starting from zero, of the last element for which the given asynchronous function + /// returns . Throws an exception if none is found. The entire sequence is consumed. + /// If is synchronous, consider using . + /// + /// + /// An asynchronous function that evaluates to a when given an item in the sequence. + /// The input task sequence. + /// The last index for which the predicate returns . + /// Thrown when the input task sequence is null. + /// Thrown if no element returns when evaluated by the function. + static member findIndexBackAsync: predicate: ('T -> #Task) -> source: TaskSeq<'T> -> Task + /// /// Tests if the sequence contains the specified element. Returns /// if contains the specified element; diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index cc279c1..2f742b0 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -1036,6 +1036,75 @@ module internal TaskSeqInternal = if isFound then return Some index else return None } + let tryFindBack predicate (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + task { + use e = source.GetAsyncEnumerator CancellationToken.None + + let mutable go = true + let mutable foundItem = None + let! step = e.MoveNextAsync() + go <- step + + match predicate with + | Predicate predicate -> + while go do + if predicate e.Current then + foundItem <- Some e.Current + + let! step = e.MoveNextAsync() + go <- step + + | PredicateAsync predicate -> + while go do + let! predicateResult = predicate e.Current + + if predicateResult then + foundItem <- Some e.Current + + let! step = e.MoveNextAsync() + go <- step + + return foundItem + } + + let tryFindIndexBack predicate (source: TaskSeq<_>) = + checkNonNull (nameof source) source + + task { + use e = source.GetAsyncEnumerator CancellationToken.None + + let mutable go = true + let mutable foundIndex = None + let mutable index = 0 + let! step = e.MoveNextAsync() + go <- step + + match predicate with + | Predicate predicate -> + while go do + if predicate e.Current then + foundIndex <- Some index + + index <- index + 1 + let! step = e.MoveNextAsync() + go <- step + + | PredicateAsync predicate -> + while go do + let! predicateResult = predicate e.Current + + if predicateResult then + foundIndex <- Some index + + index <- index + 1 + let! step = e.MoveNextAsync() + go <- step + + return foundIndex + } + let choose chooser (source: TaskSeq<_>) = checkNonNull (nameof source) source