Skip to content
Merged
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
1 change: 1 addition & 0 deletions release-notes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Release notes:

Unreleased
- adds TaskSeq.tryMax and TaskSeq.tryMin: safe variants of TaskSeq.max and TaskSeq.min that return None instead of raising ArgumentException when the input sequence is empty
- test: rename `SideEffect` module to `SideEffects` in TaskSeq.Concat.Tests.fs, TaskSeq.Delay.Tests.fs, and TaskSeq.Item.Tests.fs for consistency with the rest of the test suite (50+ files already use the plural form)
- test: add SideEffects module to TaskSeq.Using.Tests.fs; 7 new tests verify Dispose/DisposeAsync call counts, re-iteration semantics, and early-termination disposal for use and use! CE bindings
- perf: pairwise, distinctUntilChanged, distinctUntilChangedWith, distinctUntilChangedWithAsync now use explicit enumerator + while! instead of ValueOption tracking + for-in loop, eliminating per-element struct match overhead
Expand Down
86 changes: 86 additions & 0 deletions src/FSharp.Control.TaskSeq.Test/TaskSeq.MaxMin.Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ open FSharp.Control
//
// TaskSeq.max
// TaskSeq.min
// TaskSeq.tryMax
// TaskSeq.tryMin
// TaskSeq.maxBy
// TaskSeq.minBy
// TaskSeq.maxByAsync
Expand Down Expand Up @@ -316,3 +318,87 @@ module SideEffects =
do! test (MinMax.getByFunction minMax) 20
do! test (MinMax.getByFunction minMax) 30
}


module TryMaxMin =
[<Fact>]
let ``TaskSeq-tryMax returns None for null source`` () =
assertNullArg
<| fun () -> TaskSeq.tryMax (null: TaskSeq<int>)

[<Fact>]
let ``TaskSeq-tryMin returns None for null source`` () =
assertNullArg
<| fun () -> TaskSeq.tryMin (null: TaskSeq<int>)

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-tryMax returns None on empty`` variant = task {
let! result = Gen.getEmptyVariant variant |> TaskSeq.tryMax
result |> should equal None
}

[<Theory; ClassData(typeof<TestEmptyVariants>)>]
let ``TaskSeq-tryMin returns None on empty`` variant = task {
let! result = Gen.getEmptyVariant variant |> TaskSeq.tryMin
result |> should equal None
}

[<Fact>]
let ``TaskSeq-tryMax returns Some for singleton`` () = task {
let! result = TaskSeq.singleton 42 |> TaskSeq.tryMax
result |> should equal (Some 42)
}

[<Fact>]
let ``TaskSeq-tryMin returns Some for singleton`` () = task {
let! result = TaskSeq.singleton 42 |> TaskSeq.tryMin
result |> should equal (Some 42)
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-tryMax returns Some max of sequence`` variant = task {
let! result = Gen.getSeqImmutable variant |> TaskSeq.tryMax
result |> should equal (Some 10)
}

[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
let ``TaskSeq-tryMin returns Some min of sequence`` variant = task {
let! result = Gen.getSeqImmutable variant |> TaskSeq.tryMin
result |> should equal (Some 1)
}

[<Fact>]
let ``TaskSeq-tryMax and max agree on non-empty sequence`` () = task {
let ts = TaskSeq.ofList [ 3; 1; 4; 1; 5; 9; 2; 6 ]
let! viaMax = ts |> TaskSeq.max
let ts2 = TaskSeq.ofList [ 3; 1; 4; 1; 5; 9; 2; 6 ]
let! viaTryMax = ts2 |> TaskSeq.tryMax
viaTryMax |> should equal (Some viaMax)
}

[<Fact>]
let ``TaskSeq-tryMin and min agree on non-empty sequence`` () = task {
let ts = TaskSeq.ofList [ 3; 1; 4; 1; 5; 9; 2; 6 ]
let! viaMin = ts |> TaskSeq.min
let ts2 = TaskSeq.ofList [ 3; 1; 4; 1; 5; 9; 2; 6 ]
let! viaTryMin = ts2 |> TaskSeq.tryMin
viaTryMin |> should equal (Some viaMin)
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-tryMax re-iteration reflects side effects`` variant = task {
let ts = Gen.getSeqWithSideEffect variant
let! first = ts |> TaskSeq.tryMax
let! second = ts |> TaskSeq.tryMax
first |> should equal (Some 10)
second |> should equal (Some 20)
}

[<Theory; ClassData(typeof<TestSideEffectTaskSeq>)>]
let ``TaskSeq-tryMin re-iteration reflects side effects`` variant = task {
let ts = Gen.getSeqWithSideEffect variant
let! first = ts |> TaskSeq.tryMin
let! second = ts |> TaskSeq.tryMin
first |> should equal (Some 1)
second |> should equal (Some 11)
}
2 changes: 2 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ type TaskSeq private () =

static member max source = Internal.maxMin max source
static member min source = Internal.maxMin min source
static member tryMax source = Internal.tryMaxMin max source
static member tryMin source = Internal.tryMaxMin min source
static member maxBy projection source = Internal.maxMinBy (<) projection source // looks like 'less than', is 'greater than'
static member minBy projection source = Internal.maxMinBy (>) projection source
static member maxByAsync projection source = Internal.maxMinByAsync (<) projection source // looks like 'less than', is 'greater than'
Expand Down
20 changes: 20 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeq.fsi
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,26 @@ type TaskSeq =
/// <exception cref="T:ArgumentException">Thrown when the input task sequence is empty.</exception>
static member min: source: TaskSeq<'T> -> Task<'T> when 'T: comparison

/// <summary>
/// Returns <c>Some</c> with the greatest of all elements of the task sequence, compared via <see cref="Operators.max" />,
/// or <c>None</c> if the sequence is empty. For sequences that should never be empty, prefer <see cref="TaskSeq.max" />.
/// </summary>
///
/// <param name="source">The input task sequence.</param>
/// <returns>The largest element of the sequence wrapped in <c>Some</c>, or <c>None</c> if the sequence is empty.</returns>
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
static member tryMax: source: TaskSeq<'T> -> Task<'T option> when 'T: comparison

/// <summary>
/// Returns <c>Some</c> with the smallest of all elements of the task sequence, compared via <see cref="Operators.min" />,
/// or <c>None</c> if the sequence is empty. For sequences that should never be empty, prefer <see cref="TaskSeq.min" />.
/// </summary>
///
/// <param name="source">The input task sequence.</param>
/// <returns>The smallest element of the sequence wrapped in <c>Some</c>, or <c>None</c> if the sequence is empty.</returns>
/// <exception cref="T:ArgumentNullException">Thrown when the input task sequence is null.</exception>
static member tryMin: source: TaskSeq<'T> -> Task<'T option> when 'T: comparison

/// <summary>
/// Returns the greatest of all elements of the task sequence, compared via <see cref="Operators.max" />
/// on the result of applying the function <paramref name="projection" /> to each element.
Expand Down
18 changes: 18 additions & 0 deletions src/FSharp.Control.TaskSeq/TaskSeqInternal.fs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,24 @@ module internal TaskSeqInternal =
return acc
}

let inline tryMaxMin ([<InlineIfLambda>] maxOrMin) (source: TaskSeq<_>) =
checkNonNull (nameof source) source

task {
use e = source.GetAsyncEnumerator CancellationToken.None
let! hasFirst = e.MoveNextAsync()

if not hasFirst then
return None
else
let mutable acc = e.Current

while! e.MoveNextAsync() do
acc <- maxOrMin e.Current acc

return Some acc
}

// 'compare' is either `<` or `>` (i.e, less-than, greater-than resp.)
let inline maxMinBy ([<InlineIfLambda>] compare) ([<InlineIfLambda>] projection) (source: TaskSeq<_>) =
checkNonNull (nameof source) source
Expand Down
Loading