diff --git a/README.md b/README.md index 654caea..7fe7a35 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 8629e32..b2a4a1f 100644 --- a/release-notes.txt +++ b/release-notes.txt @@ -19,6 +19,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 8ea06c5..e10feb2 100644 --- a/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj +++ b/src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj @@ -61,6 +61,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 046cca2..f3127ef 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeq.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeq.fs @@ -533,3 +533,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 cd47d20..778e027 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 91937ec..8be436c 100644 --- a/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs +++ b/src/FSharp.Control.TaskSeq/TaskSeqInternal.fs @@ -1551,3 +1551,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] + }