Skip to content

Commit 7e7f134

Browse files
feat: add TaskSeq.findBack, findBackAsync, tryFindBack, tryFindBackAsync, findIndexBack, findIndexBackAsync, tryFindIndexBack, tryFindIndexBackAsync (273 tests)
These functions find the last matching element or index, mirroring Seq.findBack, Seq.tryFindBack, Seq.findIndexBack, and Seq.tryFindIndexBack from FSharp.Core. Unlike their forward counterparts, they consume the entire sequence to locate the last match; this is documented in the signatures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 852f4b1 commit 7e7f134

File tree

6 files changed

+541
-0
lines changed

6 files changed

+541
-0
lines changed

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
Release notes:
33

44
1.0.0
5+
- adds TaskSeq.findBack, findBackAsync, tryFindBack, tryFindBackAsync, findIndexBack, findIndexBackAsync, tryFindIndexBack, tryFindIndexBackAsync
56
- fixes: TaskSeq.insertAt, insertManyAt, removeAt, removeManyAt, updateAt now raise ArgumentNullException (not NullReferenceException) when given a null source; insertManyAt also validates the values argument
67
- refactor: simplify lengthBy and lengthBeforeMax to use while! and remove the redundant mutable 'go' and initial MoveNextAsync
78
- adds TaskSeq.distinctUntilChangedWith and TaskSeq.distinctUntilChangedWithAsync, #345

src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<Compile Include="TaskSeq.Exists.Tests.fs" />
2626
<Compile Include="TaskSeq.Filter.Tests.fs" />
2727
<Compile Include="TaskSeq.FindIndex.Tests.fs" />
28+
<Compile Include="TaskSeq.FindBack.Tests.fs" />
2829
<Compile Include="TaskSeq.Find.Tests.fs" />
2930
<Compile Include="TaskSeq.Fold.Tests.fs" />
3031
<Compile Include="TaskSeq.Scan.Tests.fs" />
Lines changed: 345 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,345 @@
1+
module TaskSeq.Tests.FindBack
2+
3+
open System.Collections.Generic
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
8+
open FSharp.Control
9+
10+
//
11+
// TaskSeq.findBack
12+
// TaskSeq.findBackAsync
13+
// TaskSeq.tryFindBack
14+
// TaskSeq.tryFindBackAsync
15+
// TaskSeq.findIndexBack
16+
// TaskSeq.findIndexBackAsync
17+
// TaskSeq.tryFindIndexBack
18+
// TaskSeq.tryFindIndexBackAsync
19+
//
20+
21+
module EmptySeq =
22+
[<Fact>]
23+
let ``Null source is invalid`` () =
24+
assertNullArg
25+
<| fun () -> TaskSeq.findBack (fun _ -> false) null
26+
27+
assertNullArg
28+
<| fun () -> TaskSeq.findBackAsync (fun _ -> Task.fromResult false) null
29+
30+
assertNullArg
31+
<| fun () -> TaskSeq.tryFindBack (fun _ -> false) null
32+
33+
assertNullArg
34+
<| fun () -> TaskSeq.tryFindBackAsync (fun _ -> Task.fromResult false) null
35+
36+
assertNullArg
37+
<| fun () -> TaskSeq.findIndexBack (fun _ -> false) null
38+
39+
assertNullArg
40+
<| fun () -> TaskSeq.findIndexBackAsync (fun _ -> Task.fromResult false) null
41+
42+
assertNullArg
43+
<| fun () -> TaskSeq.tryFindIndexBack (fun _ -> false) null
44+
45+
assertNullArg
46+
<| fun () -> TaskSeq.tryFindIndexBackAsync (fun _ -> Task.fromResult false) null
47+
48+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
49+
let ``TaskSeq-findBack raises KeyNotFoundException`` variant =
50+
fun () ->
51+
Gen.getEmptyVariant variant
52+
|> TaskSeq.findBack ((=) 12)
53+
|> Task.ignore
54+
|> should throwAsyncExact typeof<KeyNotFoundException>
55+
56+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
57+
let ``TaskSeq-findBackAsync raises KeyNotFoundException`` variant =
58+
fun () ->
59+
Gen.getEmptyVariant variant
60+
|> TaskSeq.findBackAsync (fun x -> task { return x = 12 })
61+
|> Task.ignore
62+
|> should throwAsyncExact typeof<KeyNotFoundException>
63+
64+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
65+
let ``TaskSeq-tryFindBack returns None`` variant =
66+
Gen.getEmptyVariant variant
67+
|> TaskSeq.tryFindBack ((=) 12)
68+
|> Task.map (should be None')
69+
70+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
71+
let ``TaskSeq-tryFindBackAsync returns None`` variant =
72+
Gen.getEmptyVariant variant
73+
|> TaskSeq.tryFindBackAsync (fun x -> task { return x = 12 })
74+
|> Task.map (should be None')
75+
76+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
77+
let ``TaskSeq-findIndexBack raises KeyNotFoundException`` variant =
78+
fun () ->
79+
Gen.getEmptyVariant variant
80+
|> TaskSeq.findIndexBack ((=) 12)
81+
|> Task.ignore
82+
|> should throwAsyncExact typeof<KeyNotFoundException>
83+
84+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
85+
let ``TaskSeq-findIndexBackAsync raises KeyNotFoundException`` variant =
86+
fun () ->
87+
Gen.getEmptyVariant variant
88+
|> TaskSeq.findIndexBackAsync (fun x -> task { return x = 12 })
89+
|> Task.ignore
90+
|> should throwAsyncExact typeof<KeyNotFoundException>
91+
92+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
93+
let ``TaskSeq-tryFindIndexBack returns None`` variant =
94+
Gen.getEmptyVariant variant
95+
|> TaskSeq.tryFindIndexBack ((=) 12)
96+
|> Task.map (should be None')
97+
98+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
99+
let ``TaskSeq-tryFindIndexBackAsync returns None`` variant =
100+
Gen.getEmptyVariant variant
101+
|> TaskSeq.tryFindIndexBackAsync (fun x -> task { return x = 12 })
102+
|> Task.map (should be None')
103+
104+
module Immutable =
105+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
106+
let ``TaskSeq-findBack sad path raises KeyNotFoundException`` variant =
107+
fun () ->
108+
Gen.getSeqImmutable variant
109+
|> TaskSeq.findBack ((=) 0) // dummy tasks sequence starts at 1
110+
|> Task.ignore
111+
112+
|> should throwAsyncExact typeof<KeyNotFoundException>
113+
114+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
115+
let ``TaskSeq-findBackAsync sad path raises KeyNotFoundException`` variant =
116+
fun () ->
117+
Gen.getSeqImmutable variant
118+
|> TaskSeq.findBackAsync (fun x -> task { return x = 0 }) // dummy tasks sequence starts at 1
119+
|> Task.ignore
120+
121+
|> should throwAsyncExact typeof<KeyNotFoundException>
122+
123+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
124+
let ``TaskSeq-findBack happy path returns last match`` variant =
125+
Gen.getSeqImmutable variant
126+
|> TaskSeq.findBack (fun x -> x < 6 && x > 3) // matches 4, 5 — last is 5
127+
|> Task.map (should equal 5)
128+
129+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
130+
let ``TaskSeq-findBackAsync happy path returns last match`` variant =
131+
Gen.getSeqImmutable variant
132+
|> TaskSeq.findBackAsync (fun x -> task { return x < 6 && x > 3 }) // matches 4, 5 — last is 5
133+
|> Task.map (should equal 5)
134+
135+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
136+
let ``TaskSeq-findBack happy path returns last item`` variant =
137+
Gen.getSeqImmutable variant
138+
|> TaskSeq.findBack (fun x -> x <= 10) // all items qualify; last is 10
139+
|> Task.map (should equal 10)
140+
141+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
142+
let ``TaskSeq-findBackAsync happy path returns last item`` variant =
143+
Gen.getSeqImmutable variant
144+
|> TaskSeq.findBackAsync (fun x -> task { return x <= 10 }) // all items qualify; last is 10
145+
|> Task.map (should equal 10)
146+
147+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
148+
let ``TaskSeq-findBack happy path returns only matching item`` variant =
149+
Gen.getSeqImmutable variant
150+
|> TaskSeq.findBack ((=) 7) // exactly one match
151+
|> Task.map (should equal 7)
152+
153+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
154+
let ``TaskSeq-findIndexBack sad path raises KeyNotFoundException`` variant =
155+
fun () ->
156+
Gen.getSeqImmutable variant
157+
|> TaskSeq.findIndexBack ((=) 0) // dummy tasks sequence starts at 1
158+
|> Task.ignore
159+
160+
|> should throwAsyncExact typeof<KeyNotFoundException>
161+
162+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
163+
let ``TaskSeq-findIndexBackAsync sad path raises KeyNotFoundException`` variant =
164+
fun () ->
165+
Gen.getSeqImmutable variant
166+
|> TaskSeq.findIndexBackAsync (fun x -> task { return x = 0 }) // dummy tasks sequence starts at 1
167+
|> Task.ignore
168+
169+
|> should throwAsyncExact typeof<KeyNotFoundException>
170+
171+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
172+
let ``TaskSeq-findIndexBack happy path returns last matching index`` variant =
173+
Gen.getSeqImmutable variant
174+
|> TaskSeq.findIndexBack (fun x -> x < 6 && x > 3) // matches indices 3, 4 — last is 4
175+
|> Task.map (should equal 4)
176+
177+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
178+
let ``TaskSeq-findIndexBackAsync happy path returns last matching index`` variant =
179+
Gen.getSeqImmutable variant
180+
|> TaskSeq.findIndexBackAsync (fun x -> task { return x < 6 && x > 3 }) // matches indices 3, 4 — last is 4
181+
|> Task.map (should equal 4)
182+
183+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
184+
let ``TaskSeq-findIndexBack happy path returns last index when all match`` variant =
185+
Gen.getSeqImmutable variant
186+
|> TaskSeq.findIndexBack (fun x -> x <= 10) // all 10 items qualify; last index is 9
187+
|> Task.map (should equal 9)
188+
189+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
190+
let ``TaskSeq-findIndexBack happy path single match`` variant =
191+
Gen.getSeqImmutable variant
192+
|> TaskSeq.findIndexBack ((=) 1) // value 1 is at index 0
193+
|> Task.map (should equal 0)
194+
195+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
196+
let ``TaskSeq-tryFindBack sad path returns None`` variant =
197+
Gen.getSeqImmutable variant
198+
|> TaskSeq.tryFindBack ((=) 0) // dummy tasks sequence starts at 1
199+
|> Task.map (should be None')
200+
201+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
202+
let ``TaskSeq-tryFindBackAsync sad path returns None`` variant =
203+
Gen.getSeqImmutable variant
204+
|> TaskSeq.tryFindBackAsync (fun x -> task { return x = 0 }) // dummy tasks sequence starts at 1
205+
|> Task.map (should be None')
206+
207+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
208+
let ``TaskSeq-tryFindBack happy path returns last match`` variant =
209+
Gen.getSeqImmutable variant
210+
|> TaskSeq.tryFindBack (fun x -> x < 6 && x > 3)
211+
|> Task.map (should equal (Some 5))
212+
213+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
214+
let ``TaskSeq-tryFindBackAsync happy path returns last match`` variant =
215+
Gen.getSeqImmutable variant
216+
|> TaskSeq.tryFindBackAsync (fun x -> task { return x < 6 && x > 3 })
217+
|> Task.map (should equal (Some 5))
218+
219+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
220+
let ``TaskSeq-tryFindIndexBack sad path returns None`` variant =
221+
Gen.getSeqImmutable variant
222+
|> TaskSeq.tryFindIndexBack ((=) 0)
223+
|> Task.map (should be None')
224+
225+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
226+
let ``TaskSeq-tryFindIndexBackAsync sad path returns None`` variant =
227+
Gen.getSeqImmutable variant
228+
|> TaskSeq.tryFindIndexBackAsync (fun x -> task { return x = 0 })
229+
|> Task.map (should be None')
230+
231+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
232+
let ``TaskSeq-tryFindIndexBack happy path returns last matching index`` variant =
233+
Gen.getSeqImmutable variant
234+
|> TaskSeq.tryFindIndexBack (fun x -> x < 6 && x > 3)
235+
|> Task.map (should equal (Some 4))
236+
237+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
238+
let ``TaskSeq-tryFindIndexBackAsync happy path returns last matching index`` variant =
239+
Gen.getSeqImmutable variant
240+
|> TaskSeq.tryFindIndexBackAsync (fun x -> task { return x < 6 && x > 3 })
241+
|> Task.map (should equal (Some 4))
242+
243+
module SideEffects =
244+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
245+
let ``TaskSeq-findBack consumes the entire sequence`` variant =
246+
Gen.getSeqWithSideEffect variant
247+
|> TaskSeq.findBack (fun x -> x < 6 && x > 3)
248+
|> Task.map (should equal 5)
249+
250+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
251+
let ``TaskSeq-tryFindBack consumes the entire sequence`` variant =
252+
Gen.getSeqWithSideEffect variant
253+
|> TaskSeq.tryFindBack (fun x -> x < 6 && x > 3)
254+
|> Task.map (should equal (Some 5))
255+
256+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
257+
let ``TaskSeq-findIndexBack consumes the entire sequence`` variant =
258+
Gen.getSeqWithSideEffect variant
259+
|> TaskSeq.findIndexBack (fun x -> x < 6 && x > 3)
260+
|> Task.map (should equal 4)
261+
262+
[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
263+
let ``TaskSeq-tryFindIndexBack consumes the entire sequence`` variant =
264+
Gen.getSeqWithSideEffect variant
265+
|> TaskSeq.tryFindIndexBack (fun x -> x < 6 && x > 3)
266+
|> Task.map (should equal (Some 4))
267+
268+
[<Fact>]
269+
let ``TaskSeq-findBack _specialcase_ unlike findBack, findBack always evaluates the full sequence`` () = task {
270+
let mutable i = 0
271+
272+
let ts = taskSeq {
273+
yield 42
274+
i <- i + 1 // side effect after the matching yield
275+
yield 1 // an item after the match
276+
}
277+
278+
// findBack must find the LAST match so it evaluates everything
279+
let! found = ts |> TaskSeq.findBack ((=) 42)
280+
found |> should equal 42
281+
i |> should equal 1 // side effect WAS executed — findBack consumed all items
282+
}
283+
284+
[<Fact>]
285+
let ``TaskSeq-tryFindBack _specialcase_ always evaluates the full sequence`` () = task {
286+
let mutable i = 0
287+
288+
let ts = taskSeq {
289+
yield 42
290+
i <- i + 1
291+
yield 1
292+
}
293+
294+
let! found = ts |> TaskSeq.tryFindBack ((=) 42)
295+
found |> should equal (Some 42)
296+
i |> should equal 1 // side effect WAS executed
297+
}
298+
299+
[<Fact>]
300+
let ``TaskSeq-findBack _specialcase_ returns the last of multiple matches`` () = task {
301+
let ts = taskSeq { yield! [ 3; 5; 3; 7; 3 ] }
302+
303+
let! found = ts |> TaskSeq.findBack ((=) 3)
304+
found |> should equal 3 // value is 3 (last of three matches)
305+
}
306+
307+
[<Fact>]
308+
let ``TaskSeq-findIndexBack _specialcase_ returns the last matching index among multiple matches`` () = task {
309+
let ts = taskSeq { yield! [ 3; 5; 3; 7; 3 ] }
310+
311+
let! found = ts |> TaskSeq.findIndexBack ((=) 3)
312+
found |> should equal 4 // index 4 is the last 3
313+
}
314+
315+
[<Fact>]
316+
let ``TaskSeq-tryFindBack _specialcase_ returns last match when multiple exist`` () = task {
317+
let ts = taskSeq { yield! [ 1; 2; 3; 4; 5; 4; 3; 2; 1 ] }
318+
319+
let! found = ts |> TaskSeq.tryFindBack (fun x -> x > 3)
320+
found |> should equal (Some 4) // last element > 3 is the 4 at index 5
321+
}
322+
323+
[<Fact>]
324+
let ``TaskSeq-tryFindIndexBack _specialcase_ returns last matching index when multiple exist`` () = task {
325+
let ts = taskSeq { yield! [ 1; 2; 3; 4; 5; 4; 3; 2; 1 ] }
326+
327+
let! found = ts |> TaskSeq.tryFindIndexBack (fun x -> x > 3)
328+
found |> should equal (Some 5) // last element > 3 is at index 5
329+
}
330+
331+
[<Fact>]
332+
let ``TaskSeq-findIndexBack _specialcase_ single-element sequence`` () = task {
333+
let ts = taskSeq { yield 42 }
334+
335+
let! found = ts |> TaskSeq.findIndexBack ((=) 42)
336+
found |> should equal 0
337+
}
338+
339+
[<Fact>]
340+
let ``TaskSeq-tryFindBack _specialcase_ single-element no match returns None`` () = task {
341+
let ts = taskSeq { yield 42 }
342+
343+
let! found = ts |> TaskSeq.tryFindBack ((=) 99)
344+
found |> should be None'
345+
}

src/FSharp.Control.TaskSeq/TaskSeq.fs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,11 @@ type TaskSeq private () =
469469
static member tryFindIndex predicate source = Internal.tryFindIndex (Predicate predicate) source
470470
static member tryFindIndexAsync predicate source = Internal.tryFindIndex (PredicateAsync predicate) source
471471

472+
static member tryFindBack predicate source = Internal.tryFindBack (Predicate predicate) source
473+
static member tryFindBackAsync predicate source = Internal.tryFindBack (PredicateAsync predicate) source
474+
static member tryFindIndexBack predicate source = Internal.tryFindIndexBack (Predicate predicate) source
475+
static member tryFindIndexBackAsync predicate source = Internal.tryFindIndexBack (PredicateAsync predicate) source
476+
472477
static member insertAt index value source = Internal.insertAt index (One value) source
473478
static member insertManyAt index values source = Internal.insertAt index (Many values) source
474479
static member removeAt index source = Internal.removeAt index source
@@ -524,6 +529,22 @@ type TaskSeq private () =
524529
Internal.tryFindIndex (PredicateAsync predicate) source
525530
|> Task.map (Option.defaultWith Internal.raiseNotFound)
526531

532+
static member findBack predicate source =
533+
Internal.tryFindBack (Predicate predicate) source
534+
|> Task.map (Option.defaultWith Internal.raiseNotFound)
535+
536+
static member findBackAsync predicate source =
537+
Internal.tryFindBack (PredicateAsync predicate) source
538+
|> Task.map (Option.defaultWith Internal.raiseNotFound)
539+
540+
static member findIndexBack predicate source =
541+
Internal.tryFindIndexBack (Predicate predicate) source
542+
|> Task.map (Option.defaultWith Internal.raiseNotFound)
543+
544+
static member findIndexBackAsync predicate source =
545+
Internal.tryFindIndexBack (PredicateAsync predicate) source
546+
|> Task.map (Option.defaultWith Internal.raiseNotFound)
547+
527548
//
528549
// zip/unzip/fold etc functions
529550
//

0 commit comments

Comments
 (0)