Skip to content

Commit 240d26b

Browse files
github-actions[bot]Repo AssistCopilotdsyme
authored
Add AsyncSeq.min, max, minBy, maxBy, minByAsync, maxByAsync (#243)
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: Repo Assist <repo-assist@github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Don Syme <dsyme@users.noreply.github.com>
1 parent f067122 commit 240d26b

File tree

3 files changed

+107
-1
lines changed

3 files changed

+107
-1
lines changed

src/FSharp.Control.AsyncSeq/AsyncSeq.fs

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

1188+
let minByAsync (projection: 'T -> Async<'Key>) (source: AsyncSeq<'T>) : Async<'T> =
1189+
async {
1190+
let! result =
1191+
source |> foldAsync (fun (acc: ('T * 'Key) option) v ->
1192+
async {
1193+
let! k = projection v
1194+
match acc with
1195+
| None -> return Some (v, k)
1196+
| Some (_, ak) -> return if k < ak then Some (v, k) else acc
1197+
}) None
1198+
match result with
1199+
| None -> return raise (System.InvalidOperationException("The input sequence was empty."))
1200+
| Some (v, _) -> return v }
1201+
1202+
let minBy (projection: 'T -> 'Key) (source: AsyncSeq<'T>) : Async<'T> =
1203+
minByAsync (projection >> async.Return) source
1204+
1205+
let maxByAsync (projection: 'T -> Async<'Key>) (source: AsyncSeq<'T>) : Async<'T> =
1206+
async {
1207+
let! result =
1208+
source |> foldAsync (fun (acc: ('T * 'Key) option) v ->
1209+
async {
1210+
let! k = projection v
1211+
match acc with
1212+
| None -> return Some (v, k)
1213+
| Some (_, ak) -> return if k > ak then Some (v, k) else acc
1214+
}) None
1215+
match result with
1216+
| None -> return raise (System.InvalidOperationException("The input sequence was empty."))
1217+
| Some (v, _) -> return v }
1218+
1219+
let maxBy (projection: 'T -> 'Key) (source: AsyncSeq<'T>) : Async<'T> =
1220+
maxByAsync (projection >> async.Return) source
1221+
1222+
let min (source: AsyncSeq<'T>) : Async<'T> =
1223+
minBy id source
1224+
1225+
let max (source: AsyncSeq<'T>) : Async<'T> =
1226+
maxBy id source
1227+
11881228
let inline sumBy (projection : 'T -> ^U) (source : AsyncSeq<'T>) : Async<^U> =
11891229
fold (fun s x -> s + projection x) LanguagePrimitives.GenericZero source
11901230

@@ -1747,7 +1787,7 @@ module AsyncSeq =
17471787
| Some rem -> async.Return rem
17481788
| None -> Async.StartChildAsTask(ie.MoveNext())
17491789
let t = Stopwatch.GetTimestamp()
1750-
let! time = Async.StartChildAsTask(Async.Sleep (max 0 rt))
1790+
let! time = Async.StartChildAsTask(Async.Sleep (Operators.max 0 rt))
17511791
let! moveOr = Async.chooseTasks move time
17521792
let delta = int ((Stopwatch.GetTimestamp() - t) * 1000L / Stopwatch.Frequency)
17531793
match moveOr with

src/FSharp.Control.AsyncSeq/AsyncSeq.fsi

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

230+
/// Asynchronously find the element with the minimum projected value. Raises InvalidOperationException if the sequence is empty.
231+
val minByAsync : projection:('T -> Async<'Key>) -> source:AsyncSeq<'T> -> Async<'T> when 'Key : comparison
232+
233+
/// Asynchronously find the element with the minimum projected value. Raises InvalidOperationException if the sequence is empty.
234+
val minBy : projection:('T -> 'Key) -> source:AsyncSeq<'T> -> Async<'T> when 'Key : comparison
235+
236+
/// Asynchronously find the element with the maximum projected value. Raises InvalidOperationException if the sequence is empty.
237+
val maxByAsync : projection:('T -> Async<'Key>) -> source:AsyncSeq<'T> -> Async<'T> when 'Key : comparison
238+
239+
/// Asynchronously find the element with the maximum projected value. Raises InvalidOperationException if the sequence is empty.
240+
val maxBy : projection:('T -> 'Key) -> source:AsyncSeq<'T> -> Async<'T> when 'Key : comparison
241+
242+
/// Asynchronously find the minimum element. Raises InvalidOperationException if the sequence is empty.
243+
val min : source:AsyncSeq<'T> -> Async<'T> when 'T : comparison
244+
245+
/// Asynchronously find the maximum element. Raises InvalidOperationException if the sequence is empty.
246+
val max : source:AsyncSeq<'T> -> Async<'T> when 'T : comparison
247+
230248
/// Asynchronously sum the mapped elements of an asynchronous sequence using a synchronous projection.
231249
val inline sumBy : projection:('T -> ^U) -> source:AsyncSeq<'T> -> Async< ^U>
232250
when ^U : (static member ( + ) : ^U * ^U -> ^U)

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,54 @@ 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)
261+
214262
[<Test>]
215263
let ``AsyncSeq.sumBy works``() =
216264
for i in 0 .. 10 do

0 commit comments

Comments
 (0)