From 5f75e72cd71aa3a31cab42b02804c29a513cfe61 Mon Sep 17 00:00:00 2001 From: Repo Assist Date: Mon, 23 Feb 2026 20:07:02 +0000 Subject: [PATCH] Add AsyncSeq.min, max, minBy, maxBy, minByAsync, maxByAsync Six new aggregation combinators mirroring Seq.min/max/minBy/maxBy: - minByAsync / maxByAsync: find element with min/max async-projected key - minBy / maxBy: synchronous projection variants - min / max: compare elements directly (require 'T : comparison) All raise InvalidOperationException on empty sequences, matching Seq behaviour. Includes 8 tests covering correctness, async projections, and empty-sequence errors. Fixes shadowing of Operators.max in bufferByTime by qualifying the call. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/FSharp.Control.AsyncSeq/AsyncSeq.fs | 42 ++++++++++++++++- src/FSharp.Control.AsyncSeq/AsyncSeq.fsi | 18 +++++++ .../AsyncSeqTests.fs | 47 +++++++++++++++++++ 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs index 909cc135..001b9002 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fs +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fs @@ -1151,6 +1151,46 @@ module AsyncSeq = let inline sum (source : AsyncSeq<'T>) : Async<'T> = (LanguagePrimitives.GenericZero, source) ||> fold (+) + let minByAsync (projection: 'T -> Async<'Key>) (source: AsyncSeq<'T>) : Async<'T> = + async { + let! result = + source |> foldAsync (fun (acc: ('T * 'Key) option) v -> + async { + let! k = projection v + match acc with + | None -> return Some (v, k) + | Some (_, ak) -> return if k < ak then Some (v, k) else acc + }) None + match result with + | None -> return raise (System.InvalidOperationException("The input sequence was empty.")) + | Some (v, _) -> return v } + + let minBy (projection: 'T -> 'Key) (source: AsyncSeq<'T>) : Async<'T> = + minByAsync (projection >> async.Return) source + + let maxByAsync (projection: 'T -> Async<'Key>) (source: AsyncSeq<'T>) : Async<'T> = + async { + let! result = + source |> foldAsync (fun (acc: ('T * 'Key) option) v -> + async { + let! k = projection v + match acc with + | None -> return Some (v, k) + | Some (_, ak) -> return if k > ak then Some (v, k) else acc + }) None + match result with + | None -> return raise (System.InvalidOperationException("The input sequence was empty.")) + | Some (v, _) -> return v } + + let maxBy (projection: 'T -> 'Key) (source: AsyncSeq<'T>) : Async<'T> = + maxByAsync (projection >> async.Return) source + + let min (source: AsyncSeq<'T>) : Async<'T> = + minBy id source + + let max (source: AsyncSeq<'T>) : Async<'T> = + maxBy id source + let scan f (state:'State) (source : AsyncSeq<'T>) = scanAsync (fun st v -> f st v |> async.Return) state source @@ -1686,7 +1726,7 @@ module AsyncSeq = | Some rem -> async.Return rem | None -> Async.StartChildAsTask(ie.MoveNext()) let t = Stopwatch.GetTimestamp() - let! time = Async.StartChildAsTask(Async.Sleep (max 0 rt)) + let! time = Async.StartChildAsTask(Async.Sleep (Operators.max 0 rt)) let! moveOr = Async.chooseTasks move time let delta = int ((Stopwatch.GetTimestamp() - t) * 1000L / Stopwatch.Frequency) match moveOr with diff --git a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi index f801cc45..08ca19fe 100644 --- a/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi +++ b/src/FSharp.Control.AsyncSeq/AsyncSeq.fsi @@ -211,6 +211,24 @@ module AsyncSeq = when ^T : (static member ( + ) : ^T * ^T -> ^T) and ^T : (static member Zero : ^T) + /// Asynchronously find the element with the minimum projected value. Raises InvalidOperationException if the sequence is empty. + val minByAsync : projection:('T -> Async<'Key>) -> source:AsyncSeq<'T> -> Async<'T> when 'Key : comparison + + /// Asynchronously find the element with the minimum projected value. Raises InvalidOperationException if the sequence is empty. + val minBy : projection:('T -> 'Key) -> source:AsyncSeq<'T> -> Async<'T> when 'Key : comparison + + /// Asynchronously find the element with the maximum projected value. Raises InvalidOperationException if the sequence is empty. + val maxByAsync : projection:('T -> Async<'Key>) -> source:AsyncSeq<'T> -> Async<'T> when 'Key : comparison + + /// Asynchronously find the element with the maximum projected value. Raises InvalidOperationException if the sequence is empty. + val maxBy : projection:('T -> 'Key) -> source:AsyncSeq<'T> -> Async<'T> when 'Key : comparison + + /// Asynchronously find the minimum element. Raises InvalidOperationException if the sequence is empty. + val min : source:AsyncSeq<'T> -> Async<'T> when 'T : comparison + + /// Asynchronously find the maximum element. Raises InvalidOperationException if the sequence is empty. + val max : source:AsyncSeq<'T> -> Async<'T> when 'T : comparison + /// Asynchronously determine if the sequence contains the given value val contains : value:'T -> source:AsyncSeq<'T> -> Async when 'T : equality diff --git a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs index fcf55b2f..d7f32098 100644 --- a/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs +++ b/tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs @@ -211,6 +211,53 @@ let ``AsyncSeq.sum works``() = let expected = ls |> List.sum Assert.True((expected = actual)) +[] +let ``AsyncSeq.min returns minimum element``() = + for i in 1 .. 10 do + let ls = [ 1 .. i ] |> List.rev + let actual = AsyncSeq.ofSeq ls |> AsyncSeq.min |> Async.RunSynchronously + Assert.AreEqual(1, actual) + +[] +let ``AsyncSeq.min raises on empty sequence``() = + Assert.Throws(fun () -> + AsyncSeq.empty |> AsyncSeq.min |> Async.RunSynchronously |> ignore) |> ignore + +[] +let ``AsyncSeq.max returns maximum element``() = + for i in 1 .. 10 do + let ls = [ 1 .. i ] + let actual = AsyncSeq.ofSeq ls |> AsyncSeq.max |> Async.RunSynchronously + Assert.AreEqual(i, actual) + +[] +let ``AsyncSeq.max raises on empty sequence``() = + Assert.Throws(fun () -> + AsyncSeq.empty |> AsyncSeq.max |> Async.RunSynchronously |> ignore) |> ignore + +[] +let ``AsyncSeq.minBy returns element with minimum projected value``() = + let ls = [ ("b", 2); ("a", 1); ("c", 3) ] + let actual = AsyncSeq.ofSeq ls |> AsyncSeq.minBy snd |> Async.RunSynchronously + Assert.AreEqual(("a", 1), actual) + +[] +let ``AsyncSeq.maxBy returns element with maximum projected value``() = + let ls = [ ("b", 2); ("a", 1); ("c", 3) ] + let actual = AsyncSeq.ofSeq ls |> AsyncSeq.maxBy snd |> Async.RunSynchronously + Assert.AreEqual(("c", 3), actual) + +[] +let ``AsyncSeq.minByAsync uses async projection``() = + let ls = [ 3; 1; 4; 1; 5; 9 ] + let actual = AsyncSeq.ofSeq ls |> AsyncSeq.minByAsync (fun x -> async.Return x) |> Async.RunSynchronously + Assert.AreEqual(1, actual) + +[] +let ``AsyncSeq.maxByAsync uses async projection``() = + let ls = [ 3; 1; 4; 1; 5; 9 ] + let actual = AsyncSeq.ofSeq ls |> AsyncSeq.maxByAsync (fun x -> async.Return x) |> Async.RunSynchronously + Assert.AreEqual(9, actual) [] let ``AsyncSeq.length works``() =