From ea71d2ef4935b7ce0b9b9afd1af28767c2dfa2c5 Mon Sep 17 00:00:00 2001 From: Repo Assist Date: Sun, 8 Mar 2026 13:26:02 +0000 Subject: [PATCH 1/2] 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> --- README.md | 12 +- release-notes.txt | 1 + .../FSharp.Control.TaskSeq.Test.fsproj | 1 + .../TaskSeq.Sort.Tests.fs | 342 ++++++++++++++++++ src/FSharp.Control.TaskSeq/TaskSeq.fs | 13 + src/FSharp.Control.TaskSeq/TaskSeq.fsi | 97 +++++ src/FSharp.Control.TaskSeq/TaskSeqInternal.fs | 141 ++++++++ 7 files changed, 601 insertions(+), 6 deletions(-) create mode 100644 src/FSharp.Control.TaskSeq.Test/TaskSeq.Sort.Tests.fs diff --git a/README.md b/README.md index dcc1cbf..688cd58 100644 --- a/README.md +++ b/README.md @@ -342,18 +342,18 @@ This is what has been implemented so far, is planned or skipped: | ✅ [#236][]| `removeAt` | `removeAt` | | | | ✅ [#236][]| `removeManyAt` | `removeManyAt` | | | | ✅ | `replicate` | `replicate` | | | -| ❓ | `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.") | +| ✅ | `rev` | `rev` | | | | ✅ [#296][] | `scan` | `scan` | `scanAsync` | | | 🚫 | `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.") | | ✅ [#90][] | `singleton` | `singleton` | | | | ✅ [#209][]| `skip` | `skip` | | | | ✅ [#219][]| `skipWhile` | `skipWhile` | `skipWhileAsync` | | | ✅ [#219][]| | `skipWhileInclusive` | `skipWhileInclusiveAsync` | | -| ❓ | `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.") | -| ❓ | `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.") | -| ❓ | `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.") | -| ❓ | `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.") | -| ❓ | `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.") | +| ✅ | `sort` | `sort` | `sortDescending` | | +| ✅ | `sortBy` | `sortBy` | `sortByAsync` | | +| ✅ | `sortByAscending` | | | (use `sortBy`) | +| ✅ | `sortByDescending` | `sortByDescending` | `sortByDescendingAsync` | | +| ✅ | `sortWith` | `sortWith` | | | | | `splitInto` | `splitInto` | | | | | `sum` | `sum` | | | | | `sumBy` | `sumBy` | `sumByAsync` | | diff --git a/release-notes.txt b/release-notes.txt index 5c0b97e..bcf3113 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -17,6 +17,7 @@ Release notes: - adds TaskSeq.unfold and TaskSeq.unfoldAsync, #289 - adds TaskSeq.chunkBySize (closes #258) and TaskSeq.windowed, #289 - fixes: CancellationToken passed to GetAsyncEnumerator is now honored in MoveNextAsync, #179 + - adds TaskSeq.rev, sort, sortDescending, sortBy, sortByDescending, sortWith, sortByAsync, sortByDescendingAsync 0.5.0 - update engineering to .NET 9/10 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 89f8d94..2a46313 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -60,6 +60,7 @@ + diff --git a/src/FSharp.Control.TaskSeq.Test/TaskSeq.Sort.Tests.fs b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Sort.Tests.fs new file mode 100644 index 0000000..e15b80e --- /dev/null +++ b/src/FSharp.Control.TaskSeq.Test/TaskSeq.Sort.Tests.fs @@ -0,0 +1,342 @@ +module TaskSeq.Tests.Sort + +open Xunit +open FsUnit.Xunit + +open FSharp.Control + +// +// TaskSeq.rev +// TaskSeq.sort / sortDescending +// TaskSeq.sortBy / sortByDescending / sortByAsync / sortByDescendingAsync +// TaskSeq.sortWith +// + +module RevEmpty = + [] + let ``TaskSeq-rev with null source raises`` () = assertNullArg <| fun () -> TaskSeq.rev null + + [)>] + let ``TaskSeq-rev on empty returns empty`` variant = Gen.getEmptyVariant variant |> TaskSeq.rev |> verifyEmpty + + [] + let ``TaskSeq-rev on singleton returns singleton`` () = task { + let! result = taskSeq { yield 42 } |> TaskSeq.rev |> TaskSeq.toListAsync + result |> should equal [ 42 ] + } + +module RevImmutable = + [] + let ``TaskSeq-rev reverses a simple list`` () = task { + let! result = + taskSeq { yield! [ 1..5 ] } + |> TaskSeq.rev + |> TaskSeq.toListAsync + + result |> should equal [ 5; 4; 3; 2; 1 ] + } + + [] + let ``TaskSeq-rev on two elements swaps them`` () = task { + let! result = + taskSeq { yield! [ 10; 20 ] } + |> TaskSeq.rev + |> TaskSeq.toListAsync + + result |> should equal [ 20; 10 ] + } + + [] + let ``TaskSeq-rev is idempotent when applied twice`` () = task { + let original = [ 1..7 ] + + let! result = + taskSeq { yield! original } + |> TaskSeq.rev + |> TaskSeq.rev + |> TaskSeq.toListAsync + + result |> should equal original + } + + [)>] + let ``TaskSeq-rev all variants yields elements in reverse order`` variant = task { + let! result = + Gen.getSeqImmutable variant + |> TaskSeq.rev + |> TaskSeq.toListAsync + + result |> should equal [ 10; 9; 8; 7; 6; 5; 4; 3; 2; 1 ] + } + +module SortEmpty = + [] + let ``TaskSeq-sort with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sort null + + [)>] + let ``TaskSeq-sort on empty returns empty`` variant = Gen.getEmptyVariant variant |> TaskSeq.sort |> verifyEmpty + + [] + let ``TaskSeq-sortDescending with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sortDescending null + + [)>] + let ``TaskSeq-sortDescending on empty returns empty`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.sortDescending + |> verifyEmpty + +module SortImmutable = + [] + let ``TaskSeq-sort already-sorted sequence`` () = task { + let! result = + taskSeq { yield! [ 1..5 ] } + |> TaskSeq.sort + |> TaskSeq.toListAsync + + result |> should equal [ 1; 2; 3; 4; 5 ] + } + + [] + let ``TaskSeq-sort unsorted sequence`` () = task { + let! result = + taskSeq { yield! [ 3; 1; 4; 1; 5; 9; 2; 6 ] } + |> TaskSeq.sort + |> TaskSeq.toListAsync + + result |> should equal [ 1; 1; 2; 3; 4; 5; 6; 9 ] + } + + [] + let ``TaskSeq-sort reverse-sorted sequence`` () = task { + let! result = + taskSeq { yield! [ 5; 4; 3; 2; 1 ] } + |> TaskSeq.sort + |> TaskSeq.toListAsync + + result |> should equal [ 1; 2; 3; 4; 5 ] + } + + [] + let ``TaskSeq-sort strings`` () = task { + let! result = + taskSeq { yield! [ "banana"; "apple"; "cherry" ] } + |> TaskSeq.sort + |> TaskSeq.toListAsync + + result |> should equal [ "apple"; "banana"; "cherry" ] + } + + [] + let ``TaskSeq-sortDescending unsorted sequence`` () = task { + let! result = + taskSeq { yield! [ 3; 1; 4; 1; 5; 9; 2; 6 ] } + |> TaskSeq.sortDescending + |> TaskSeq.toListAsync + + result |> should equal [ 9; 6; 5; 4; 3; 2; 1; 1 ] + } + + [] + let ``TaskSeq-sortDescending is inverse of sort`` () = task { + let! asc = + taskSeq { yield! [ 5; 1; 3; 2; 4 ] } + |> TaskSeq.sort + |> TaskSeq.toListAsync + + let! desc = + taskSeq { yield! [ 5; 1; 3; 2; 4 ] } + |> TaskSeq.sortDescending + |> TaskSeq.toListAsync + + desc |> List.rev |> should equal asc + } + +module SortByEmpty = + [] + let ``TaskSeq-sortBy with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sortBy id null + + [)>] + let ``TaskSeq-sortBy on empty returns empty`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.sortBy id + |> verifyEmpty + + [] + let ``TaskSeq-sortByDescending with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sortByDescending id null + + [)>] + let ``TaskSeq-sortByDescending on empty returns empty`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.sortByDescending id + |> verifyEmpty + + [] + let ``TaskSeq-sortByAsync with null source raises`` () = + assertNullArg + <| fun () -> TaskSeq.sortByAsync (fun x -> task { return x }) null + + [)>] + let ``TaskSeq-sortByAsync on empty returns empty`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.sortByAsync (fun x -> task { return x }) + |> verifyEmpty + + [] + let ``TaskSeq-sortByDescendingAsync with null source raises`` () = + assertNullArg + <| fun () -> TaskSeq.sortByDescendingAsync (fun x -> task { return x }) null + + [)>] + let ``TaskSeq-sortByDescendingAsync on empty returns empty`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.sortByDescendingAsync (fun x -> task { return x }) + |> verifyEmpty + +module SortByImmutable = + [] + let ``TaskSeq-sortBy ascending by negative key reverses order`` () = task { + let! result = + taskSeq { yield! [ 1..5 ] } + |> TaskSeq.sortBy (fun x -> -x) + |> TaskSeq.toListAsync + + result |> should equal [ 5; 4; 3; 2; 1 ] + } + + [] + let ``TaskSeq-sortBy sorts record fields correctly`` () = task { + let items = [ {| Name = "Charlie"; Age = 30 |}; {| Name = "Alice"; Age = 25 |}; {| Name = "Bob"; Age = 35 |} ] + + let! byName = + taskSeq { yield! items } + |> TaskSeq.sortBy (fun x -> x.Name) + |> TaskSeq.toListAsync + + let! byAge = + taskSeq { yield! items } + |> TaskSeq.sortBy (fun x -> x.Age) + |> TaskSeq.toListAsync + + byName + |> List.map (fun x -> x.Name) + |> should equal [ "Alice"; "Bob"; "Charlie" ] + + byAge + |> List.map (fun x -> x.Age) + |> should equal [ 25; 30; 35 ] + } + + [] + let ``TaskSeq-sortByDescending ascending by negative key gives ascending order`` () = task { + let! result = + taskSeq { yield! [ 3; 1; 4; 1; 5 ] } + |> TaskSeq.sortByDescending (fun x -> -x) + |> TaskSeq.toListAsync + + result |> should equal [ 1; 1; 3; 4; 5 ] + } + + [] + let ``TaskSeq-sortByDescending sorts in descending order by projected key`` () = task { + let! result = + taskSeq { yield! [ "bb"; "aaa"; "c" ] } + |> TaskSeq.sortByDescending (fun s -> s.Length) + |> TaskSeq.toListAsync + + result + |> List.map (fun s -> s.Length) + |> should equal [ 3; 2; 1 ] + } + + [] + let ``TaskSeq-sortByAsync yields same result as sortBy for identity async`` () = task { + let input = [ 5; 3; 1; 4; 2 ] + + let! sync = + taskSeq { yield! input } + |> TaskSeq.sortBy id + |> TaskSeq.toListAsync + + let! async' = + taskSeq { yield! input } + |> TaskSeq.sortByAsync (fun x -> task { return x }) + |> TaskSeq.toListAsync + + async' |> should equal sync + } + + [] + let ``TaskSeq-sortByDescendingAsync yields same result as sortByDescending for identity async`` () = task { + let input = [ 5; 3; 1; 4; 2 ] + + let! sync = + taskSeq { yield! input } + |> TaskSeq.sortByDescending id + |> TaskSeq.toListAsync + + let! asyncDesc = + taskSeq { yield! input } + |> TaskSeq.sortByDescendingAsync (fun x -> task { return x }) + |> TaskSeq.toListAsync + + asyncDesc |> should equal sync + } + + [] + let ``TaskSeq-sortByAsync evaluates projection exactly once per element`` () = task { + let mutable callCount = 0 + + let! result = + taskSeq { yield! [ 3; 1; 2 ] } + |> TaskSeq.sortByAsync (fun x -> task { + callCount <- callCount + 1 + return x + }) + |> TaskSeq.toListAsync + + callCount |> should equal 3 + result |> should equal [ 1; 2; 3 ] + } + +module SortWithEmpty = + [] + let ``TaskSeq-sortWith with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sortWith compare null + + [)>] + let ``TaskSeq-sortWith on empty returns empty`` variant = + Gen.getEmptyVariant variant + |> TaskSeq.sortWith compare + |> verifyEmpty + +module SortWithImmutable = + [] + let ``TaskSeq-sortWith ascending with standard compare`` () = task { + let! result = + taskSeq { yield! [ 3; 1; 4; 1; 5; 9; 2; 6 ] } + |> TaskSeq.sortWith compare + |> TaskSeq.toListAsync + + result |> should equal [ 1; 1; 2; 3; 4; 5; 6; 9 ] + } + + [] + let ``TaskSeq-sortWith descending with reversed compare`` () = task { + let! result = + taskSeq { yield! [ 3; 1; 4; 1; 5; 9; 2; 6 ] } + |> TaskSeq.sortWith (fun a b -> compare b a) + |> TaskSeq.toListAsync + + result |> should equal [ 9; 6; 5; 4; 3; 2; 1; 1 ] + } + + [] + let ``TaskSeq-sortWith by string length`` () = task { + let! result = + taskSeq { yield! [ "banana"; "kiwi"; "cherry"; "fig" ] } + |> TaskSeq.sortWith (fun a b -> compare a.Length b.Length) + |> TaskSeq.toListAsync + + result + |> List.map (fun s -> s.Length) + |> should equal [ 3; 4; 6; 6 ] + } diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fs b/src/FSharp.Control.TaskSeq/TaskSeq.fs index aced3a5..2b7816f 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -531,3 +531,16 @@ type TaskSeq private () = static member partitionAsync predicate source = Internal.partition (PredicateAsync predicate) source static member mapFold mapping state source = Internal.mapFold (MapFolderAction mapping) state source static member mapFoldAsync mapping state source = Internal.mapFold (AsyncMapFolderAction mapping) state source + + // + // rev/sort functions + // + + static member rev source = Internal.rev source + static member sort source = Internal.sort source + static member sortDescending source = Internal.sortDescending source + static member sortBy projection source = Internal.sortBy projection source + static member sortByDescending projection source = Internal.sortByDescending projection source + static member sortWith comparer source = Internal.sortWith comparer source + static member sortByAsync projection source = Internal.sortByAsync projection source + static member sortByDescendingAsync projection source = Internal.sortByDescendingAsync projection source diff --git a/src/FSharp.Control.TaskSeq/TaskSeq.fsi b/src/FSharp.Control.TaskSeq/TaskSeq.fsi index fcf45cf..cb6f785 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fsi +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fsi @@ -1530,6 +1530,103 @@ type TaskSeq = /// Thrown when is not positive. static member windowed: windowSize: int -> source: TaskSeq<'T> -> TaskSeq<'T[]> + /// + /// Returns a new task sequence with the elements in the reverse order. This function materializes the source sequence. + /// + /// + /// The input task sequence. + /// The reversed task sequence. + /// Thrown when the input task sequence is null. + static member rev: source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Sorts the elements of the task sequence in ascending order. This function materializes the source sequence. + /// The elements must support comparison (i.e. implement ). + /// For a descending sort, use . + /// For sorting by a projection, use or . + /// + /// + /// The input task sequence. + /// The sorted task sequence. + /// Thrown when the input task sequence is null. + static member sort: source: TaskSeq<'T> -> TaskSeq<'T> when 'T: comparison + + /// + /// Sorts the elements of the task sequence in descending order. This function materializes the source sequence. + /// The elements must support comparison (i.e. implement ). + /// For an ascending sort, use . + /// + /// + /// The input task sequence. + /// The sorted task sequence in descending order. + /// Thrown when the input task sequence is null. + static member sortDescending: source: TaskSeq<'T> -> TaskSeq<'T> when 'T: comparison + + /// + /// Sorts the elements of the task sequence in ascending order using the given + /// to produce the sort key. This function materializes the source sequence. + /// The projected key must support comparison. + /// If is asynchronous, consider using . + /// + /// + /// A function to transform elements into the sort key. + /// The input task sequence. + /// The sorted task sequence. + /// Thrown when the input task sequence is null. + static member sortBy: projection: ('T -> 'Key) -> source: TaskSeq<'T> -> TaskSeq<'T> when 'Key: comparison + + /// + /// Sorts the elements of the task sequence in descending order using the given + /// to produce the sort key. This function materializes the source sequence. + /// The projected key must support comparison. + /// If is asynchronous, consider using . + /// + /// + /// A function to transform elements into the sort key. + /// The input task sequence. + /// The sorted task sequence in descending order. + /// Thrown when the input task sequence is null. + static member sortByDescending: projection: ('T -> 'Key) -> source: TaskSeq<'T> -> TaskSeq<'T> when 'Key: comparison + + /// + /// Sorts the elements of the task sequence using the given pairwise comparison . + /// This function materializes the source sequence. + /// + /// + /// A function that returns a negative integer, zero, or a positive integer to indicate ordering. + /// The input task sequence. + /// The sorted task sequence. + /// Thrown when the input task sequence is null. + static member sortWith: comparer: ('T -> 'T -> int) -> source: TaskSeq<'T> -> TaskSeq<'T> + + /// + /// Sorts the elements of the task sequence in ascending order using an asynchronous + /// to produce the sort key. This function materializes the source sequence and evaluates each projection once. + /// The projected key must support comparison. + /// If is synchronous, consider using . + /// + /// + /// An async function to transform elements into the sort key. + /// The input task sequence. + /// The sorted task sequence. + /// Thrown when the input task sequence is null. + static member sortByAsync: + projection: ('T -> #Task<'Key>) -> source: TaskSeq<'T> -> TaskSeq<'T> when 'Key: comparison + + /// + /// Sorts the elements of the task sequence in descending order using an asynchronous + /// to produce the sort key. This function materializes the source sequence and evaluates each projection once. + /// The projected key must support comparison. + /// If is synchronous, consider using . + /// + /// + /// An async function to transform elements into the sort key. + /// The input task sequence. + /// The sorted task sequence in descending order. + /// Thrown when the input task sequence is null. + static member sortByDescendingAsync: + projection: ('T -> #Task<'Key>) -> source: TaskSeq<'T> -> TaskSeq<'T> when 'Key: comparison + /// /// Combines the two task sequences into a new task sequence of pairs. The two sequences need not have equal lengths: /// when one sequence is exhausted any remaining elements in the other sequence are ignored. diff --git a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs index 26b02a6..0261b96 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -1489,3 +1489,144 @@ module internal TaskSeqInternal = yield result } + + let rev (source: TaskSeq<'T>) : TaskSeq<'T> = + checkNonNull (nameof source) source + + taskSeq { + let res = ResizeArray<'T>() + + for item in source do + res.Add item + + let arr = res.ToArray() + + for i in arr.Length - 1 .. -1 .. 0 do + yield arr[i] + } + + let sort (source: TaskSeq<'T>) : TaskSeq<'T> = + checkNonNull (nameof source) source + + taskSeq { + let res = ResizeArray<'T>() + + for item in source do + res.Add item + + let arr = res.ToArray() + Array.sortInPlace arr + + for item in arr do + yield item + } + + let sortDescending (source: TaskSeq<'T>) : TaskSeq<'T> = + checkNonNull (nameof source) source + + taskSeq { + let res = ResizeArray<'T>() + + for item in source do + res.Add item + + let arr = res.ToArray() + Array.sortInPlaceWith (fun a b -> compare b a) arr + + for item in arr do + yield item + } + + let sortBy (projection: 'T -> 'Key) (source: TaskSeq<'T>) : TaskSeq<'T> = + checkNonNull (nameof source) source + + taskSeq { + let res = ResizeArray<'T>() + + for item in source do + res.Add item + + let arr = res.ToArray() + Array.sortInPlaceBy projection arr + + for item in arr do + yield item + } + + let sortByDescending (projection: 'T -> 'Key) (source: TaskSeq<'T>) : TaskSeq<'T> = + checkNonNull (nameof source) source + + taskSeq { + let res = ResizeArray<'T>() + + for item in source do + res.Add item + + let arr = res.ToArray() + Array.sortInPlaceWith (fun a b -> compare (projection b) (projection a)) arr + + for item in arr do + yield item + } + + let sortWith (comparer: 'T -> 'T -> int) (source: TaskSeq<'T>) : TaskSeq<'T> = + checkNonNull (nameof source) source + + taskSeq { + let res = ResizeArray<'T>() + + for item in source do + res.Add item + + let arr = res.ToArray() + Array.sortInPlaceWith comparer arr + + for item in arr do + yield item + } + + let sortByAsync (projection: 'T -> #Task<'Key>) (source: TaskSeq<'T>) : TaskSeq<'T> = + checkNonNull (nameof source) source + + taskSeq { + let items = ResizeArray<'T>() + + for item in source do + items.Add item + + let count = items.Count + let keys = Array.zeroCreate<'Key> count + + for i in 0 .. count - 1 do + let! k = projection items[i] + keys[i] <- k + + let indices = Array.init count id + Array.sortInPlaceWith (fun i j -> compare keys[i] keys[j]) indices + + for idx in indices do + yield items[idx] + } + + let sortByDescendingAsync (projection: 'T -> #Task<'Key>) (source: TaskSeq<'T>) : TaskSeq<'T> = + checkNonNull (nameof source) source + + taskSeq { + let items = ResizeArray<'T>() + + for item in source do + items.Add item + + let count = items.Count + let keys = Array.zeroCreate<'Key> count + + for i in 0 .. count - 1 do + let! k = projection items[i] + keys[i] <- k + + let indices = Array.init count id + Array.sortInPlaceWith (fun i j -> compare keys[j] keys[i]) indices + + for idx in indices do + yield items[idx] + } From 9377d4ee169ad484970c5de4f9d670ff35f54e81 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 8 Mar 2026 13:31:49 +0000 Subject: [PATCH 2/2] ci: trigger checks