Skip to content

Commit ea71d2e

Browse files
Repo AssistCopilot
authored andcommitted
feat: add TaskSeq.rev, sort, sortDescending, sortBy, sortByDescending, sortWith, sortByAsync, sortByDescendingAsync
All eight functions materialize the source sequence before yielding results in the requested order. Keys for the async sortBy variants are evaluated exactly once per element. - rev: reverses the sequence - sort / sortDescending: sort by natural order - sortBy / sortByDescending: sort by synchronous projection - sortByAsync / sortByDescendingAsync: sort by asynchronous projection - sortWith: sort using a custom comparer 100 new tests added. README table updated; release-notes.txt updated. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent a057959 commit ea71d2e

File tree

7 files changed

+601
-6
lines changed

7 files changed

+601
-6
lines changed

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -342,18 +342,18 @@ This is what has been implemented so far, is planned or skipped:
342342
| &#x2705; [#236][]| `removeAt` | `removeAt` | | |
343343
| &#x2705; [#236][]| `removeManyAt` | `removeManyAt` | | |
344344
| &#x2705; | `replicate` | `replicate` | | |
345-
| &#x2753; | `rev` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
345+
| &#x2705; | `rev` | `rev` | | |
346346
| &#x2705; [#296][] | `scan` | `scan` | `scanAsync` | |
347347
| &#x1f6ab; | `scanBack` | | | [note #2](#note2 "Because of the async nature of TaskSeq sequences, iterating from the back would be bad practice. Instead, materialize the sequence to a list or array and then apply the 'Back' iterators.") |
348348
| &#x2705; [#90][] | `singleton` | `singleton` | | |
349349
| &#x2705; [#209][]| `skip` | `skip` | | |
350350
| &#x2705; [#219][]| `skipWhile` | `skipWhile` | `skipWhileAsync` | |
351351
| &#x2705; [#219][]| | `skipWhileInclusive` | `skipWhileInclusiveAsync` | |
352-
| &#x2753; | `sort` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
353-
| &#x2753; | `sortBy` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
354-
| &#x2753; | `sortByAscending` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
355-
| &#x2753; | `sortByDescending` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
356-
| &#x2753; | `sortWith` | | | [note #1](#note1 "These functions require a form of pre-materializing through 'TaskSeq.cache', similar to the approach taken in the corresponding 'Seq' functions. It doesn't make much sense to have a cached async sequence. However, 'AsyncSeq' does implement these, so we'll probably do so eventually as well.") |
352+
| &#x2705; | `sort` | `sort` | `sortDescending` | |
353+
| &#x2705; | `sortBy` | `sortBy` | `sortByAsync` | |
354+
| &#x2705; | `sortByAscending` | | | (use `sortBy`) |
355+
| &#x2705; | `sortByDescending` | `sortByDescending` | `sortByDescendingAsync` | |
356+
| &#x2705; | `sortWith` | `sortWith` | | |
357357
| | `splitInto` | `splitInto` | | |
358358
| | `sum` | `sum` | | |
359359
| | `sumBy` | `sumBy` | `sumByAsync` | |

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ Release notes:
1717
- adds TaskSeq.unfold and TaskSeq.unfoldAsync, #289
1818
- adds TaskSeq.chunkBySize (closes #258) and TaskSeq.windowed, #289
1919
- fixes: CancellationToken passed to GetAsyncEnumerator is now honored in MoveNextAsync, #179
20+
- adds TaskSeq.rev, sort, sortDescending, sortBy, sortByDescending, sortWith, sortByAsync, sortByDescendingAsync
2021

2122
0.5.0
2223
- update engineering to .NET 9/10

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
<Compile Include="TaskSeq.Zip.Tests.fs" />
6161
<Compile Include="TaskSeq.ChunkBySize.Tests.fs" />
6262
<Compile Include="TaskSeq.Windowed.Tests.fs" />
63+
<Compile Include="TaskSeq.Sort.Tests.fs" />
6364
<Compile Include="TaskSeq.Tests.CE.fs" />
6465
<Compile Include="TaskSeq.StateTransitionBug.Tests.CE.fs" />
6566
<Compile Include="TaskSeq.StateTransitionBug-delayed.Tests.CE.fs" />
Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
module TaskSeq.Tests.Sort
2+
3+
open Xunit
4+
open FsUnit.Xunit
5+
6+
open FSharp.Control
7+
8+
//
9+
// TaskSeq.rev
10+
// TaskSeq.sort / sortDescending
11+
// TaskSeq.sortBy / sortByDescending / sortByAsync / sortByDescendingAsync
12+
// TaskSeq.sortWith
13+
//
14+
15+
module RevEmpty =
16+
[<Fact>]
17+
let ``TaskSeq-rev with null source raises`` () = assertNullArg <| fun () -> TaskSeq.rev null
18+
19+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
20+
let ``TaskSeq-rev on empty returns empty`` variant = Gen.getEmptyVariant variant |> TaskSeq.rev |> verifyEmpty
21+
22+
[<Fact>]
23+
let ``TaskSeq-rev on singleton returns singleton`` () = task {
24+
let! result = taskSeq { yield 42 } |> TaskSeq.rev |> TaskSeq.toListAsync
25+
result |> should equal [ 42 ]
26+
}
27+
28+
module RevImmutable =
29+
[<Fact>]
30+
let ``TaskSeq-rev reverses a simple list`` () = task {
31+
let! result =
32+
taskSeq { yield! [ 1..5 ] }
33+
|> TaskSeq.rev
34+
|> TaskSeq.toListAsync
35+
36+
result |> should equal [ 5; 4; 3; 2; 1 ]
37+
}
38+
39+
[<Fact>]
40+
let ``TaskSeq-rev on two elements swaps them`` () = task {
41+
let! result =
42+
taskSeq { yield! [ 10; 20 ] }
43+
|> TaskSeq.rev
44+
|> TaskSeq.toListAsync
45+
46+
result |> should equal [ 20; 10 ]
47+
}
48+
49+
[<Fact>]
50+
let ``TaskSeq-rev is idempotent when applied twice`` () = task {
51+
let original = [ 1..7 ]
52+
53+
let! result =
54+
taskSeq { yield! original }
55+
|> TaskSeq.rev
56+
|> TaskSeq.rev
57+
|> TaskSeq.toListAsync
58+
59+
result |> should equal original
60+
}
61+
62+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
63+
let ``TaskSeq-rev all variants yields elements in reverse order`` variant = task {
64+
let! result =
65+
Gen.getSeqImmutable variant
66+
|> TaskSeq.rev
67+
|> TaskSeq.toListAsync
68+
69+
result |> should equal [ 10; 9; 8; 7; 6; 5; 4; 3; 2; 1 ]
70+
}
71+
72+
module SortEmpty =
73+
[<Fact>]
74+
let ``TaskSeq-sort with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sort null
75+
76+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
77+
let ``TaskSeq-sort on empty returns empty`` variant = Gen.getEmptyVariant variant |> TaskSeq.sort |> verifyEmpty
78+
79+
[<Fact>]
80+
let ``TaskSeq-sortDescending with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sortDescending null
81+
82+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
83+
let ``TaskSeq-sortDescending on empty returns empty`` variant =
84+
Gen.getEmptyVariant variant
85+
|> TaskSeq.sortDescending
86+
|> verifyEmpty
87+
88+
module SortImmutable =
89+
[<Fact>]
90+
let ``TaskSeq-sort already-sorted sequence`` () = task {
91+
let! result =
92+
taskSeq { yield! [ 1..5 ] }
93+
|> TaskSeq.sort
94+
|> TaskSeq.toListAsync
95+
96+
result |> should equal [ 1; 2; 3; 4; 5 ]
97+
}
98+
99+
[<Fact>]
100+
let ``TaskSeq-sort unsorted sequence`` () = task {
101+
let! result =
102+
taskSeq { yield! [ 3; 1; 4; 1; 5; 9; 2; 6 ] }
103+
|> TaskSeq.sort
104+
|> TaskSeq.toListAsync
105+
106+
result |> should equal [ 1; 1; 2; 3; 4; 5; 6; 9 ]
107+
}
108+
109+
[<Fact>]
110+
let ``TaskSeq-sort reverse-sorted sequence`` () = task {
111+
let! result =
112+
taskSeq { yield! [ 5; 4; 3; 2; 1 ] }
113+
|> TaskSeq.sort
114+
|> TaskSeq.toListAsync
115+
116+
result |> should equal [ 1; 2; 3; 4; 5 ]
117+
}
118+
119+
[<Fact>]
120+
let ``TaskSeq-sort strings`` () = task {
121+
let! result =
122+
taskSeq { yield! [ "banana"; "apple"; "cherry" ] }
123+
|> TaskSeq.sort
124+
|> TaskSeq.toListAsync
125+
126+
result |> should equal [ "apple"; "banana"; "cherry" ]
127+
}
128+
129+
[<Fact>]
130+
let ``TaskSeq-sortDescending unsorted sequence`` () = task {
131+
let! result =
132+
taskSeq { yield! [ 3; 1; 4; 1; 5; 9; 2; 6 ] }
133+
|> TaskSeq.sortDescending
134+
|> TaskSeq.toListAsync
135+
136+
result |> should equal [ 9; 6; 5; 4; 3; 2; 1; 1 ]
137+
}
138+
139+
[<Fact>]
140+
let ``TaskSeq-sortDescending is inverse of sort`` () = task {
141+
let! asc =
142+
taskSeq { yield! [ 5; 1; 3; 2; 4 ] }
143+
|> TaskSeq.sort
144+
|> TaskSeq.toListAsync
145+
146+
let! desc =
147+
taskSeq { yield! [ 5; 1; 3; 2; 4 ] }
148+
|> TaskSeq.sortDescending
149+
|> TaskSeq.toListAsync
150+
151+
desc |> List.rev |> should equal asc
152+
}
153+
154+
module SortByEmpty =
155+
[<Fact>]
156+
let ``TaskSeq-sortBy with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sortBy id null
157+
158+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
159+
let ``TaskSeq-sortBy on empty returns empty`` variant =
160+
Gen.getEmptyVariant variant
161+
|> TaskSeq.sortBy id
162+
|> verifyEmpty
163+
164+
[<Fact>]
165+
let ``TaskSeq-sortByDescending with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sortByDescending id null
166+
167+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
168+
let ``TaskSeq-sortByDescending on empty returns empty`` variant =
169+
Gen.getEmptyVariant variant
170+
|> TaskSeq.sortByDescending id
171+
|> verifyEmpty
172+
173+
[<Fact>]
174+
let ``TaskSeq-sortByAsync with null source raises`` () =
175+
assertNullArg
176+
<| fun () -> TaskSeq.sortByAsync (fun x -> task { return x }) null
177+
178+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
179+
let ``TaskSeq-sortByAsync on empty returns empty`` variant =
180+
Gen.getEmptyVariant variant
181+
|> TaskSeq.sortByAsync (fun x -> task { return x })
182+
|> verifyEmpty
183+
184+
[<Fact>]
185+
let ``TaskSeq-sortByDescendingAsync with null source raises`` () =
186+
assertNullArg
187+
<| fun () -> TaskSeq.sortByDescendingAsync (fun x -> task { return x }) null
188+
189+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
190+
let ``TaskSeq-sortByDescendingAsync on empty returns empty`` variant =
191+
Gen.getEmptyVariant variant
192+
|> TaskSeq.sortByDescendingAsync (fun x -> task { return x })
193+
|> verifyEmpty
194+
195+
module SortByImmutable =
196+
[<Fact>]
197+
let ``TaskSeq-sortBy ascending by negative key reverses order`` () = task {
198+
let! result =
199+
taskSeq { yield! [ 1..5 ] }
200+
|> TaskSeq.sortBy (fun x -> -x)
201+
|> TaskSeq.toListAsync
202+
203+
result |> should equal [ 5; 4; 3; 2; 1 ]
204+
}
205+
206+
[<Fact>]
207+
let ``TaskSeq-sortBy sorts record fields correctly`` () = task {
208+
let items = [ {| Name = "Charlie"; Age = 30 |}; {| Name = "Alice"; Age = 25 |}; {| Name = "Bob"; Age = 35 |} ]
209+
210+
let! byName =
211+
taskSeq { yield! items }
212+
|> TaskSeq.sortBy (fun x -> x.Name)
213+
|> TaskSeq.toListAsync
214+
215+
let! byAge =
216+
taskSeq { yield! items }
217+
|> TaskSeq.sortBy (fun x -> x.Age)
218+
|> TaskSeq.toListAsync
219+
220+
byName
221+
|> List.map (fun x -> x.Name)
222+
|> should equal [ "Alice"; "Bob"; "Charlie" ]
223+
224+
byAge
225+
|> List.map (fun x -> x.Age)
226+
|> should equal [ 25; 30; 35 ]
227+
}
228+
229+
[<Fact>]
230+
let ``TaskSeq-sortByDescending ascending by negative key gives ascending order`` () = task {
231+
let! result =
232+
taskSeq { yield! [ 3; 1; 4; 1; 5 ] }
233+
|> TaskSeq.sortByDescending (fun x -> -x)
234+
|> TaskSeq.toListAsync
235+
236+
result |> should equal [ 1; 1; 3; 4; 5 ]
237+
}
238+
239+
[<Fact>]
240+
let ``TaskSeq-sortByDescending sorts in descending order by projected key`` () = task {
241+
let! result =
242+
taskSeq { yield! [ "bb"; "aaa"; "c" ] }
243+
|> TaskSeq.sortByDescending (fun s -> s.Length)
244+
|> TaskSeq.toListAsync
245+
246+
result
247+
|> List.map (fun s -> s.Length)
248+
|> should equal [ 3; 2; 1 ]
249+
}
250+
251+
[<Fact>]
252+
let ``TaskSeq-sortByAsync yields same result as sortBy for identity async`` () = task {
253+
let input = [ 5; 3; 1; 4; 2 ]
254+
255+
let! sync =
256+
taskSeq { yield! input }
257+
|> TaskSeq.sortBy id
258+
|> TaskSeq.toListAsync
259+
260+
let! async' =
261+
taskSeq { yield! input }
262+
|> TaskSeq.sortByAsync (fun x -> task { return x })
263+
|> TaskSeq.toListAsync
264+
265+
async' |> should equal sync
266+
}
267+
268+
[<Fact>]
269+
let ``TaskSeq-sortByDescendingAsync yields same result as sortByDescending for identity async`` () = task {
270+
let input = [ 5; 3; 1; 4; 2 ]
271+
272+
let! sync =
273+
taskSeq { yield! input }
274+
|> TaskSeq.sortByDescending id
275+
|> TaskSeq.toListAsync
276+
277+
let! asyncDesc =
278+
taskSeq { yield! input }
279+
|> TaskSeq.sortByDescendingAsync (fun x -> task { return x })
280+
|> TaskSeq.toListAsync
281+
282+
asyncDesc |> should equal sync
283+
}
284+
285+
[<Fact>]
286+
let ``TaskSeq-sortByAsync evaluates projection exactly once per element`` () = task {
287+
let mutable callCount = 0
288+
289+
let! result =
290+
taskSeq { yield! [ 3; 1; 2 ] }
291+
|> TaskSeq.sortByAsync (fun x -> task {
292+
callCount <- callCount + 1
293+
return x
294+
})
295+
|> TaskSeq.toListAsync
296+
297+
callCount |> should equal 3
298+
result |> should equal [ 1; 2; 3 ]
299+
}
300+
301+
module SortWithEmpty =
302+
[<Fact>]
303+
let ``TaskSeq-sortWith with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sortWith compare null
304+
305+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
306+
let ``TaskSeq-sortWith on empty returns empty`` variant =
307+
Gen.getEmptyVariant variant
308+
|> TaskSeq.sortWith compare
309+
|> verifyEmpty
310+
311+
module SortWithImmutable =
312+
[<Fact>]
313+
let ``TaskSeq-sortWith ascending with standard compare`` () = task {
314+
let! result =
315+
taskSeq { yield! [ 3; 1; 4; 1; 5; 9; 2; 6 ] }
316+
|> TaskSeq.sortWith compare
317+
|> TaskSeq.toListAsync
318+
319+
result |> should equal [ 1; 1; 2; 3; 4; 5; 6; 9 ]
320+
}
321+
322+
[<Fact>]
323+
let ``TaskSeq-sortWith descending with reversed compare`` () = task {
324+
let! result =
325+
taskSeq { yield! [ 3; 1; 4; 1; 5; 9; 2; 6 ] }
326+
|> TaskSeq.sortWith (fun a b -> compare b a)
327+
|> TaskSeq.toListAsync
328+
329+
result |> should equal [ 9; 6; 5; 4; 3; 2; 1; 1 ]
330+
}
331+
332+
[<Fact>]
333+
let ``TaskSeq-sortWith by string length`` () = task {
334+
let! result =
335+
taskSeq { yield! [ "banana"; "kiwi"; "cherry"; "fig" ] }
336+
|> TaskSeq.sortWith (fun a b -> compare a.Length b.Length)
337+
|> TaskSeq.toListAsync
338+
339+
result
340+
|> List.map (fun s -> s.Length)
341+
|> should equal [ 3; 4; 6; 6 ]
342+
}

0 commit comments

Comments
 (0)