Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | |
Expand Down
1 change: 1 addition & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
<Compile Include="TaskSeq.CompareWith.Tests.fs" />
<Compile Include="TaskSeq.ChunkBySize.Tests.fs" />
<Compile Include="TaskSeq.Windowed.Tests.fs" />
<Compile Include="TaskSeq.Sort.Tests.fs" />
<Compile Include="TaskSeq.Tests.CE.fs" />
<Compile Include="TaskSeq.StateTransitionBug.Tests.CE.fs" />
<Compile Include="TaskSeq.StateTransitionBug-delayed.Tests.CE.fs" />
Expand Down
342 changes: 342 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.Sort.Tests.fs
Original file line number Diff line number Diff line change
@@ -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 =
[<Fact>]
let ``TaskSeq-rev with null source raises`` () = assertNullArg <| fun () -> TaskSeq.rev null

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-rev on empty returns empty`` variant = Gen.getEmptyVariant variant |> TaskSeq.rev |> verifyEmpty

[<Fact>]
let ``TaskSeq-rev on singleton returns singleton`` () = task {
let! result = taskSeq { yield 42 } |> TaskSeq.rev |> TaskSeq.toListAsync
result |> should equal [ 42 ]
}

module RevImmutable =
[<Fact>]
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 ]
}

[<Fact>]
let ``TaskSeq-rev on two elements swaps them`` () = task {
let! result =
taskSeq { yield! [ 10; 20 ] }
|> TaskSeq.rev
|> TaskSeq.toListAsync

result |> should equal [ 20; 10 ]
}

[<Fact>]
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
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
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 =
[<Fact>]
let ``TaskSeq-sort with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sort null

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-sort on empty returns empty`` variant = Gen.getEmptyVariant variant |> TaskSeq.sort |> verifyEmpty

[<Fact>]
let ``TaskSeq-sortDescending with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sortDescending null

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-sortDescending on empty returns empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.sortDescending
|> verifyEmpty

module SortImmutable =
[<Fact>]
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 ]
}

[<Fact>]
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 ]
}

[<Fact>]
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 ]
}

[<Fact>]
let ``TaskSeq-sort strings`` () = task {
let! result =
taskSeq { yield! [ "banana"; "apple"; "cherry" ] }
|> TaskSeq.sort
|> TaskSeq.toListAsync

result |> should equal [ "apple"; "banana"; "cherry" ]
}

[<Fact>]
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 ]
}

[<Fact>]
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 =
[<Fact>]
let ``TaskSeq-sortBy with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sortBy id null

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-sortBy on empty returns empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.sortBy id
|> verifyEmpty

[<Fact>]
let ``TaskSeq-sortByDescending with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sortByDescending id null

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-sortByDescending on empty returns empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.sortByDescending id
|> verifyEmpty

[<Fact>]
let ``TaskSeq-sortByAsync with null source raises`` () =
assertNullArg
<| fun () -> TaskSeq.sortByAsync (fun x -> task { return x }) null

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-sortByAsync on empty returns empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.sortByAsync (fun x -> task { return x })
|> verifyEmpty

[<Fact>]
let ``TaskSeq-sortByDescendingAsync with null source raises`` () =
assertNullArg
<| fun () -> TaskSeq.sortByDescendingAsync (fun x -> task { return x }) null

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-sortByDescendingAsync on empty returns empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.sortByDescendingAsync (fun x -> task { return x })
|> verifyEmpty

module SortByImmutable =
[<Fact>]
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 ]
}

[<Fact>]
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 ]
}

[<Fact>]
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 ]
}

[<Fact>]
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 ]
}

[<Fact>]
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
}

[<Fact>]
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
}

[<Fact>]
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 =
[<Fact>]
let ``TaskSeq-sortWith with null source raises`` () = assertNullArg <| fun () -> TaskSeq.sortWith compare null

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-sortWith on empty returns empty`` variant =
Gen.getEmptyVariant variant
|> TaskSeq.sortWith compare
|> verifyEmpty

module SortWithImmutable =
[<Fact>]
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 ]
}

[<Fact>]
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 ]
}

[<Fact>]
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 ]
}
Loading
Loading