Skip to content

Commit 5f75e72

Browse files
Repo AssistCopilot
authored andcommitted
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 <[email protected]>
1 parent 44f16f0 commit 5f75e72

3 files changed

Lines changed: 106 additions & 1 deletion

File tree

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,46 @@ module AsyncSeq =
11511151
let inline sum (source : AsyncSeq<'T>) : Async<'T> =
11521152
(LanguagePrimitives.GenericZero, source) ||> fold (+)
11531153

1154+
let minByAsync (projection: 'T -> Async<'Key>) (source: AsyncSeq<'T>) : Async<'T> =
1155+
async {
1156+
let! result =
1157+
source |> foldAsync (fun (acc: ('T * 'Key) option) v ->
1158+
async {
1159+
let! k = projection v
1160+
match acc with
1161+
| None -> return Some (v, k)
1162+
| Some (_, ak) -> return if k < ak then Some (v, k) else acc
1163+
}) None
1164+
match result with
1165+
| None -> return raise (System.InvalidOperationException("The input sequence was empty."))
1166+
| Some (v, _) -> return v }
1167+
1168+
let minBy (projection: 'T -> 'Key) (source: AsyncSeq<'T>) : Async<'T> =
1169+
minByAsync (projection >> async.Return) source
1170+
1171+
let maxByAsync (projection: 'T -> Async<'Key>) (source: AsyncSeq<'T>) : Async<'T> =
1172+
async {
1173+
let! result =
1174+
source |> foldAsync (fun (acc: ('T * 'Key) option) v ->
1175+
async {
1176+
let! k = projection v
1177+
match acc with
1178+
| None -> return Some (v, k)
1179+
| Some (_, ak) -> return if k > ak then Some (v, k) else acc
1180+
}) None
1181+
match result with
1182+
| None -> return raise (System.InvalidOperationException("The input sequence was empty."))
1183+
| Some (v, _) -> return v }
1184+
1185+
let maxBy (projection: 'T -> 'Key) (source: AsyncSeq<'T>) : Async<'T> =
1186+
maxByAsync (projection >> async.Return) source
1187+
1188+
let min (source: AsyncSeq<'T>) : Async<'T> =
1189+
minBy id source
1190+
1191+
let max (source: AsyncSeq<'T>) : Async<'T> =
1192+
maxBy id source
1193+
11541194
let scan f (state:'State) (source : AsyncSeq<'T>) =
11551195
scanAsync (fun st v -> f st v |> async.Return) state source
11561196

@@ -1686,7 +1726,7 @@ module AsyncSeq =
16861726
| Some rem -> async.Return rem
16871727
| None -> Async.StartChildAsTask(ie.MoveNext())
16881728
let t = Stopwatch.GetTimestamp()
1689-
let! time = Async.StartChildAsTask(Async.Sleep (max 0 rt))
1729+
let! time = Async.StartChildAsTask(Async.Sleep (Operators.max 0 rt))
16901730
let! moveOr = Async.chooseTasks move time
16911731
let delta = int ((Stopwatch.GetTimestamp() - t) * 1000L / Stopwatch.Frequency)
16921732
match moveOr with

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,24 @@ module AsyncSeq =
211211
when ^T : (static member ( + ) : ^T * ^T -> ^T)
212212
and ^T : (static member Zero : ^T)
213213

214+
/// Asynchronously find the element with the minimum projected value. Raises InvalidOperationException if the sequence is empty.
215+
val minByAsync : projection:('T -> Async<'Key>) -> source:AsyncSeq<'T> -> Async<'T> when 'Key : comparison
216+
217+
/// Asynchronously find the element with the minimum projected value. Raises InvalidOperationException if the sequence is empty.
218+
val minBy : projection:('T -> 'Key) -> source:AsyncSeq<'T> -> Async<'T> when 'Key : comparison
219+
220+
/// Asynchronously find the element with the maximum projected value. Raises InvalidOperationException if the sequence is empty.
221+
val maxByAsync : projection:('T -> Async<'Key>) -> source:AsyncSeq<'T> -> Async<'T> when 'Key : comparison
222+
223+
/// Asynchronously find the element with the maximum projected value. Raises InvalidOperationException if the sequence is empty.
224+
val maxBy : projection:('T -> 'Key) -> source:AsyncSeq<'T> -> Async<'T> when 'Key : comparison
225+
226+
/// Asynchronously find the minimum element. Raises InvalidOperationException if the sequence is empty.
227+
val min : source:AsyncSeq<'T> -> Async<'T> when 'T : comparison
228+
229+
/// Asynchronously find the maximum element. Raises InvalidOperationException if the sequence is empty.
230+
val max : source:AsyncSeq<'T> -> Async<'T> when 'T : comparison
231+
214232
/// Asynchronously determine if the sequence contains the given value
215233
val contains : value:'T -> source:AsyncSeq<'T> -> Async<bool> when 'T : equality
216234

tests/FSharp.Control.AsyncSeq.Tests/AsyncSeqTests.fs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,53 @@ let ``AsyncSeq.sum works``() =
211211
let expected = ls |> List.sum
212212
Assert.True((expected = actual))
213213

214+
[<Test>]
215+
let ``AsyncSeq.min returns minimum element``() =
216+
for i in 1 .. 10 do
217+
let ls = [ 1 .. i ] |> List.rev
218+
let actual = AsyncSeq.ofSeq ls |> AsyncSeq.min |> Async.RunSynchronously
219+
Assert.AreEqual(1, actual)
220+
221+
[<Test>]
222+
let ``AsyncSeq.min raises on empty sequence``() =
223+
Assert.Throws<System.InvalidOperationException>(fun () ->
224+
AsyncSeq.empty<int> |> AsyncSeq.min |> Async.RunSynchronously |> ignore) |> ignore
225+
226+
[<Test>]
227+
let ``AsyncSeq.max returns maximum element``() =
228+
for i in 1 .. 10 do
229+
let ls = [ 1 .. i ]
230+
let actual = AsyncSeq.ofSeq ls |> AsyncSeq.max |> Async.RunSynchronously
231+
Assert.AreEqual(i, actual)
232+
233+
[<Test>]
234+
let ``AsyncSeq.max raises on empty sequence``() =
235+
Assert.Throws<System.InvalidOperationException>(fun () ->
236+
AsyncSeq.empty<int> |> AsyncSeq.max |> Async.RunSynchronously |> ignore) |> ignore
237+
238+
[<Test>]
239+
let ``AsyncSeq.minBy returns element with minimum projected value``() =
240+
let ls = [ ("b", 2); ("a", 1); ("c", 3) ]
241+
let actual = AsyncSeq.ofSeq ls |> AsyncSeq.minBy snd |> Async.RunSynchronously
242+
Assert.AreEqual(("a", 1), actual)
243+
244+
[<Test>]
245+
let ``AsyncSeq.maxBy returns element with maximum projected value``() =
246+
let ls = [ ("b", 2); ("a", 1); ("c", 3) ]
247+
let actual = AsyncSeq.ofSeq ls |> AsyncSeq.maxBy snd |> Async.RunSynchronously
248+
Assert.AreEqual(("c", 3), actual)
249+
250+
[<Test>]
251+
let ``AsyncSeq.minByAsync uses async projection``() =
252+
let ls = [ 3; 1; 4; 1; 5; 9 ]
253+
let actual = AsyncSeq.ofSeq ls |> AsyncSeq.minByAsync (fun x -> async.Return x) |> Async.RunSynchronously
254+
Assert.AreEqual(1, actual)
255+
256+
[<Test>]
257+
let ``AsyncSeq.maxByAsync uses async projection``() =
258+
let ls = [ 3; 1; 4; 1; 5; 9 ]
259+
let actual = AsyncSeq.ofSeq ls |> AsyncSeq.maxByAsync (fun x -> async.Return x) |> Async.RunSynchronously
260+
Assert.AreEqual(9, actual)
214261

215262
[<Test>]
216263
let ``AsyncSeq.length works``() =

0 commit comments

Comments
 (0)