Skip to content

Commit 1d0c637

Browse files
authored
Merge pull request #221 from fsprojects/implement-max-min-maxby-minby
Implement `max|min`, `maxBy|minBy` and `maxByAsync|minByAsync`
2 parents 06bc268 + 2e9541c commit 1d0c637

File tree

8 files changed

+488
-10
lines changed

8 files changed

+488
-10
lines changed

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,10 @@ This is what has been implemented so far, is planned or skipped:
289289
| 🚫 | `mapFoldBack` | | | [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.") |
290290
| ✅ [#2][] | `mapi` | `mapi` | `mapiAsync` | |
291291
| | `mapi2` | `mapi2` | `mapi2Async` | |
292-
| | `max` | `max` | | |
293-
| | `maxBy` | `maxBy` | `maxByAsync` | |
294-
| | `min` | `min` | | |
295-
| | `minBy` | `minBy` | `minByAsync` | |
292+
| ✅ [#221][]| `max` | `max` | | |
293+
| ✅ [#221][]| `maxBy` | `maxBy` | `maxByAsync` | |
294+
| ✅ [#221][]| `min` | `min` | | |
295+
| ✅ [#221][]| `minBy` | `minBy` | `minByAsync` | |
296296
| ✅ [#2][] | `ofArray` | `ofArray` | | |
297297
| ✅ [#2][] | | `ofAsyncArray` | | |
298298
| ✅ [#2][] | | `ofAsyncList` | | |
@@ -511,7 +511,12 @@ module TaskSeq =
511511
val mapAsync: mapper: ('T -> #Task<'U>) -> source: TaskSeq<'T> -> TaskSeq<'U>
512512
val mapi: mapper: (int -> 'T -> 'U) -> source: TaskSeq<'T> -> TaskSeq<'U>
513513
val mapiAsync: mapper: (int -> 'T -> #Task<'U>) -> source: TaskSeq<'T> -> TaskSeq<'U>
514-
val ofArray: source: 'T[] -> TaskSeq<'T>
514+
val max: source: TaskSeq<'T> -> Task<'T> when 'T: comparison
515+
val max: source: TaskSeq<'T> -> Task<'T> when 'T: comparison
516+
val maxBy: projection: ('T -> 'U) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison
517+
val minBy: projection: ('T -> 'U) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison
518+
val maxByAsync: projection: ('T -> #Task<'U>) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison
519+
val minByAsync: projection: ('T -> #Task<'U>) -> source: TaskSeq<'T> -> Task<'T> when 'U: comparison val ofArray: source: 'T[] -> TaskSeq<'T>
515520
val ofAsyncArray: source: Async<'T> array -> TaskSeq<'T>
516521
val ofAsyncList: source: Async<'T> list -> TaskSeq<'T>
517522
val ofAsyncSeq: source: seq<Async<'T>> -> TaskSeq<'T>
@@ -604,6 +609,7 @@ module TaskSeq =
604609
[#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209
605610
[#217]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/217
606611
[#219]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/219
612+
[#221]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/221
607613

608614
[issues]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues
609615
[nuget]: https://www.nuget.org/packages/FSharp.Control.TaskSeq/

assets/nuget-package-readme.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,10 @@ This is what has been implemented so far, is planned or skipped:
169169
| &#x1f6ab; | `mapFoldBack` | | | [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.") |
170170
| &#x2705; [#2][] | `mapi` | `mapi` | `mapiAsync` | |
171171
| | `mapi2` | `mapi2` | `mapi2Async` | |
172-
| | `max` | `max` | | |
173-
| | `maxBy` | `maxBy` | `maxByAsync` | |
174-
| | `min` | `min` | | |
175-
| | `minBy` | `minBy` | `minByAsync` | |
172+
| &#x2705; [#221][]| `max` | `max` | | |
173+
| &#x2705; [#221][]| `maxBy` | `maxBy` | `maxByAsync` | |
174+
| &#x2705; [#221][]| `min` | `min` | | |
175+
| &#x2705; [#221][]| `minBy` | `minBy` | `minByAsync` | |
176176
| &#x2705; [#2][] | `ofArray` | `ofArray` | | |
177177
| &#x2705; [#2][] | | `ofAsyncArray` | | |
178178
| &#x2705; [#2][] | | `ofAsyncList` | | |
@@ -309,3 +309,4 @@ _The motivation for `readOnly` in `Seq` is that a cast from a mutable array or l
309309
[#209]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/209
310310
[#217]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/217
311311
[#219]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/219
312+
[#221]: https://github.com/fsprojects/FSharp.Control.TaskSeq/issues/221

release-notes.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Release notes:
77
* TaskSeq.truncate, drop, #209
88
* TaskSeq.where, whereAsync, #217
99
* TaskSeq.skipWhile, skipWhileInclusive, skipWhileAsync, skipWhileInclusiveAsync, #219
10+
* TaskSeq.max, min, maxBy, minBy, maxByAsync, minByAsync, #221
1011

1112
- Performance: less thread hops with 'StartImmediateAsTask' instead of 'StartAsTask', fixes #135
1213
- BINARY INCOMPATIBILITY: 'TaskSeq' module is now static members on 'TaskSeq<_>', fixes #184

src/FSharp.Control.TaskSeq.Test/FSharp.Control.TaskSeq.Test.fsproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
<Compile Include="TaskSeq.Last.Tests.fs" />
3333
<Compile Include="TaskSeq.Length.Tests.fs" />
3434
<Compile Include="TaskSeq.Map.Tests.fs" />
35+
<Compile Include="TaskSeq.MaxMin.Tests.fs" />
3536
<Compile Include="TaskSeq.OfXXX.Tests.fs" />
3637
<Compile Include="TaskSeq.Pick.Tests.fs" />
3738
<Compile Include="TaskSeq.Singleton.Tests.fs" />
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
module TaskSeq.Tests.MaxMin
2+
3+
open System
4+
5+
open Xunit
6+
open FsUnit.Xunit
7+
8+
open FSharp.Control
9+
10+
//
11+
// TaskSeq.max
12+
// TaskSeq.min
13+
// TaskSeq.maxBy
14+
// TaskSeq.minBy
15+
// TaskSeq.maxByAsync
16+
// TaskSeq.minByAsync
17+
//
18+
19+
type MinMax =
20+
| Max = 0
21+
| Min = 1
22+
| MaxBy = 2
23+
| MinBy = 3
24+
| MaxByAsync = 4
25+
| MinByAsync = 5
26+
27+
module MinMax =
28+
let getFunction =
29+
function
30+
| MinMax.Max -> TaskSeq.max
31+
| MinMax.Min -> TaskSeq.min
32+
| MinMax.MaxBy -> TaskSeq.maxBy id
33+
| MinMax.MinBy -> TaskSeq.minBy id
34+
| MinMax.MaxByAsync -> TaskSeq.maxByAsync Task.fromResult
35+
| MinMax.MinByAsync -> TaskSeq.minByAsync Task.fromResult
36+
| _ -> failwith "impossible"
37+
38+
let getByFunction =
39+
function
40+
| MinMax.MaxBy -> TaskSeq.maxBy
41+
| MinMax.MinBy -> TaskSeq.minBy
42+
| MinMax.MaxByAsync -> fun by -> TaskSeq.maxByAsync (by >> Task.fromResult)
43+
| MinMax.MinByAsync -> fun by -> TaskSeq.minByAsync (by >> Task.fromResult)
44+
| _ -> failwith "impossible"
45+
46+
let getAll () =
47+
[ MinMax.Max; MinMax.Min; MinMax.MaxBy; MinMax.MinBy; MinMax.MaxByAsync; MinMax.MinByAsync ]
48+
|> List.map getFunction
49+
50+
let getAllMin () =
51+
[ MinMax.Min; MinMax.MinBy; MinMax.MinByAsync ]
52+
|> List.map getFunction
53+
54+
let getAllMax () =
55+
[ MinMax.Max; MinMax.MaxBy; MinMax.MaxByAsync ]
56+
|> List.map getFunction
57+
58+
let isMin =
59+
function
60+
| MinMax.Min
61+
| MinMax.MinBy
62+
| MinMax.MinByAsync -> true
63+
| _ -> false
64+
65+
let isMax = isMin >> not
66+
67+
68+
type AllMinMaxFunctions() as this =
69+
inherit TheoryData<MinMax>()
70+
71+
do
72+
this.Add MinMax.Max
73+
this.Add MinMax.Min
74+
this.Add MinMax.MaxBy
75+
this.Add MinMax.MinBy
76+
this.Add MinMax.MaxByAsync
77+
this.Add MinMax.MinByAsync
78+
79+
type JustMin() as this =
80+
inherit TheoryData<MinMax>()
81+
82+
do
83+
this.Add MinMax.Min
84+
this.Add MinMax.MinBy
85+
this.Add MinMax.MinByAsync
86+
87+
type JustMax() as this =
88+
inherit TheoryData<MinMax>()
89+
90+
do
91+
this.Add MinMax.Max
92+
this.Add MinMax.MaxBy
93+
this.Add MinMax.MaxByAsync
94+
95+
type JustMinMaxBy() as this =
96+
inherit TheoryData<MinMax>()
97+
98+
do
99+
this.Add MinMax.MaxBy
100+
this.Add MinMax.MinBy
101+
this.Add MinMax.MaxByAsync
102+
this.Add MinMax.MinByAsync
103+
104+
module EmptySeq =
105+
[<Theory; ClassData(typeof<AllMinMaxFunctions>)>]
106+
let ``Null source raises ArgumentNullException`` (minMaxType: MinMax) =
107+
let minMax = MinMax.getFunction minMaxType
108+
109+
assertNullArg <| fun () -> minMax (null: TaskSeq<int>)
110+
111+
[<Theory; ClassData(typeof<TestEmptyVariants>)>]
112+
let ``Empty sequence raises ArgumentException`` variant =
113+
let test minMax =
114+
let data = Gen.getEmptyVariant variant
115+
116+
fun () -> minMax data |> Task.ignore
117+
|> should throwAsyncExact typeof<ArgumentException>
118+
119+
for minMax in MinMax.getAll () do
120+
test minMax
121+
122+
module Functionality =
123+
[<Fact>]
124+
let ``TaskSeq-max should return maximum`` () = task {
125+
let ts = [ 'A' .. 'Z' ] |> TaskSeq.ofList
126+
let! max = TaskSeq.max ts
127+
max |> should equal 'Z'
128+
}
129+
130+
[<Fact>]
131+
let ``TaskSeq-maxBy should return maximum of input, not the projection`` () = task {
132+
let ts = [ 'A' .. 'Z' ] |> TaskSeq.ofList
133+
let! max = TaskSeq.maxBy id ts
134+
max |> should equal 'Z'
135+
136+
let ts = [ 1..10 ] |> TaskSeq.ofList
137+
let! max = TaskSeq.maxBy (~-) ts
138+
max |> should equal 1 // as negated, -1 is highest, should not return projection, but original
139+
}
140+
141+
[<Fact>]
142+
let ``TaskSeq-maxByAsync should return maximum of input, not the projection`` () = task {
143+
let ts = [ 'A' .. 'Z' ] |> TaskSeq.ofList
144+
let! max = TaskSeq.maxByAsync Task.fromResult ts
145+
max |> should equal 'Z'
146+
147+
let ts = [ 1..10 ] |> TaskSeq.ofList
148+
let! max = TaskSeq.maxByAsync (fun x -> Task.fromResult -x) ts
149+
max |> should equal 1 // as negated, -1 is highest, should not return projection, but original
150+
}
151+
152+
[<Fact>]
153+
let ``TaskSeq-min should return minimum`` () = task {
154+
let ts = [ 'A' .. 'Z' ] |> TaskSeq.ofList
155+
let! min = TaskSeq.min ts
156+
min |> should equal 'A'
157+
}
158+
159+
[<Fact>]
160+
let ``TaskSeq-minBy should return minimum of input, not the projection`` () = task {
161+
let ts = [ 'A' .. 'Z' ] |> TaskSeq.ofList
162+
let! min = TaskSeq.minBy id ts
163+
min |> should equal 'A'
164+
165+
let ts = [ 1..10 ] |> TaskSeq.ofList
166+
let! min = TaskSeq.minBy (~-) ts
167+
min |> should equal 10 // as negated, -10 is lowest, should not return projection, but original
168+
}
169+
170+
[<Fact>]
171+
let ``TaskSeq-minByAsync should return minimum of input, not the projection`` () = task {
172+
let ts = [ 'A' .. 'Z' ] |> TaskSeq.ofList
173+
let! min = TaskSeq.minByAsync Task.fromResult ts
174+
min |> should equal 'A'
175+
176+
let ts = [ 1..10 ] |> TaskSeq.ofList
177+
let! min = TaskSeq.minByAsync (fun x -> Task.fromResult -x) ts
178+
min |> should equal 10 // as negated, 1 is highest, should not return projection, but original
179+
}
180+
181+
182+
module Immutable =
183+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
184+
let ``TaskSeq-max, maxBy, maxByAsync returns maximum`` variant = task {
185+
let ts = Gen.getSeqImmutable variant
186+
187+
for max in MinMax.getAllMax () do
188+
let! max = max ts
189+
max |> should equal 10
190+
}
191+
192+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
193+
let ``TaskSeq-min, minBy, minByAsync returns minimum`` variant = task {
194+
let ts = Gen.getSeqImmutable variant
195+
196+
for min in MinMax.getAllMin () do
197+
let! min = min ts
198+
min |> should equal 1
199+
}
200+
201+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
202+
let ``TaskSeq-maxBy, maxByAsync returns maximum after projection`` variant = task {
203+
let ts = Gen.getSeqImmutable variant
204+
let! max = ts |> TaskSeq.maxBy (fun x -> -x)
205+
max |> should equal 1 // because -1 maps to item '1'
206+
}
207+
208+
209+
[<Theory; ClassData(typeof<TestImmTaskSeq>)>]
210+
let ``TaskSeq-minBy, minByAsync returns minimum after projection`` variant = task {
211+
let ts = Gen.getSeqImmutable variant
212+
let! min = ts |> TaskSeq.minBy (fun x -> -x)
213+
min |> should equal 10 // because -10 maps to item 10
214+
}
215+
216+
module SideSeffects =
217+
[<Fact>]
218+
let ``TaskSeq-max, maxBy, maxByAsync prove we execute after-effects`` () = task {
219+
let mutable i = 0
220+
221+
let ts = taskSeq {
222+
i <- i + 1
223+
i <- i + 1
224+
yield i // 2
225+
i <- i + 1
226+
yield i // 3
227+
yield i + 1 // 4
228+
i <- i + 1 // we should get here
229+
}
230+
231+
do! ts |> TaskSeq.max |> Task.map (should equal 4)
232+
do! ts |> TaskSeq.maxBy (~-) |> Task.map (should equal 6) // next iteration & negation "-6" maps to "6"
233+
234+
do!
235+
ts
236+
|> TaskSeq.maxByAsync Task.fromResult
237+
|> Task.map (should equal 12) // no negation
238+
239+
i |> should equal 12
240+
}
241+
242+
[<Fact>]
243+
let ``TaskSeq-min, minBy, minByAsync prove we execute after-effects test`` () = task {
244+
let mutable i = 0
245+
246+
let ts = taskSeq {
247+
i <- i + 1
248+
i <- i + 1
249+
yield i // 2
250+
i <- i + 1
251+
yield i // 3
252+
yield i + 1 // 4
253+
i <- i + 1 // we should get here
254+
}
255+
256+
do! ts |> TaskSeq.min |> Task.map (should equal 2)
257+
do! ts |> TaskSeq.minBy (~-) |> Task.map (should equal 8) // next iteration & negation
258+
259+
do!
260+
ts
261+
|> TaskSeq.minByAsync Task.fromResult
262+
|> Task.map (should equal 10) // no negation
263+
264+
i |> should equal 12
265+
}
266+
267+
268+
[<Theory; ClassData(typeof<JustMax>)>]
269+
let ``TaskSeq-max with sequence that changes length`` (minMax: MinMax) = task {
270+
let max = MinMax.getFunction minMax
271+
let mutable i = 0
272+
273+
let ts = taskSeq {
274+
i <- i + 10
275+
yield! [ 1..i ]
276+
}
277+
278+
do! max ts |> Task.map (should equal 10)
279+
do! max ts |> Task.map (should equal 20) // mutable state dangers!!
280+
do! max ts |> Task.map (should equal 30) // id
281+
}
282+
283+
[<Theory; ClassData(typeof<JustMin>)>]
284+
let ``TaskSeq-min with sequence that changes length`` (minMax: MinMax) = task {
285+
let min = MinMax.getFunction minMax
286+
let mutable i = 0
287+
288+
let ts = taskSeq {
289+
i <- i + 10
290+
yield! [ 1..i ]
291+
}
292+
293+
do! min ts |> Task.map (should equal 1)
294+
do! min ts |> Task.map (should equal 1) // same min after changing state
295+
do! min ts |> Task.map (should equal 1) // id
296+
}
297+
298+
[<Theory; ClassData(typeof<JustMinMaxBy>)>]
299+
let ``TaskSeq-minBy, maxBy with sequence that changes length`` (minMax: MinMax) = task {
300+
let mutable i = 0
301+
302+
let ts = taskSeq {
303+
i <- i + 10
304+
yield! [ 1..i ]
305+
}
306+
307+
let test minMaxFn v =
308+
if MinMax.isMin minMax then
309+
// this ensures the "min" version behaves like the "max" version
310+
minMaxFn (~-) ts |> Task.map (should equal v)
311+
else
312+
minMaxFn id ts |> Task.map (should equal v)
313+
314+
do! test (MinMax.getByFunction minMax) 10
315+
do! test (MinMax.getByFunction minMax) 20
316+
do! test (MinMax.getByFunction minMax) 30
317+
}

0 commit comments

Comments
 (0)