From a928f4a4cfdab42213b286b4db05652ff568d05f Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Mon, 30 Mar 2020 21:45:10 +0100 Subject: [PATCH 01/29] Rename unexported isgreater in reduceddim --- base/reducedim.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/base/reducedim.jl b/base/reducedim.jl index c83c2c71cb3e6..e9ca4f03dac7b 100644 --- a/base/reducedim.jl +++ b/base/reducedim.jl @@ -1001,7 +1001,7 @@ function _findmin(A, region) end end -isgreater(a, b) = isless(b,a) +_isgreater(a, b) = isless(b,a) """ findmax!(rval, rind, A) -> (maxval, index) @@ -1012,7 +1012,7 @@ dimensions of `rval` and `rind`, and store the results in `rval` and `rind`. """ function findmax!(rval::AbstractArray, rind::AbstractArray, A::AbstractArray; init::Bool=true) - findminmax!(isgreater, init && !isempty(A) ? fill!(rval, first(A)) : rval, fill!(rind,zero(eltype(keys(A)))), A) + findminmax!(_isgreater, init && !isempty(A) ? fill!(rval, first(A)) : rval, fill!(rind,zero(eltype(keys(A)))), A) end """ @@ -1045,7 +1045,7 @@ function _findmax(A, region) end similar(A, ri), zeros(eltype(keys(A)), ri) else - findminmax!(isgreater, fill!(similar(A, ri), first(A)), + findminmax!(_isgreater, fill!(similar(A, ri), first(A)), zeros(eltype(keys(A)), ri), A) end end From b2f11117d1578cbe498e9c802f3238b2e2071554 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Mon, 30 Mar 2020 18:10:03 +0100 Subject: [PATCH 02/29] Add isgreater Defines a descending total order where unorderable values and missing are last. This makes defining min, argmin, etc, simpler. --- NEWS.md | 1 + base/exports.jl | 1 + base/operators.jl | 24 +++++++++++++++++++++++- test/operators.jl | 8 ++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 48aebc9344980..e4d394e29cc02 100644 --- a/NEWS.md +++ b/NEWS.md @@ -28,6 +28,7 @@ Build system changes New library functions --------------------- +* New function `isgreater(a, b)` defines a descending total order where unorderable values and missing are ordered smaller than any regular value. New library features -------------------- diff --git a/base/exports.jl b/base/exports.jl index 0c157c45d2052..628ca3473ecb9 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -657,6 +657,7 @@ export isequal, ismutable, isless, + isgreater, ifelse, objectid, sizeof, diff --git a/base/operators.jl b/base/operators.jl index c2645910bf039..3ca937512dc2d 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -141,7 +141,7 @@ is defined, it is expected to satisfy the following: `isless(x, y) && isless(y, z)` implies `isless(x, z)`. Values that are normally unordered, such as `NaN`, -are ordered in an arbitrary but consistent fashion. +are ordered after regular values. [`missing`](@ref) values are ordered last. This is the default comparison used by [`sort`](@ref). @@ -168,6 +168,28 @@ isless(x::AbstractFloat, y::AbstractFloat) = (!isnan(x) & (isnan(y) | signless(x isless(x::Real, y::AbstractFloat) = (!isnan(x) & (isnan(y) | signless(x, y))) | (x < y) isless(x::AbstractFloat, y::Real ) = (!isnan(x) & (isnan(y) | signless(x, y))) | (x < y) +""" + isgreater(x, y) + +Test whether `x` is greater than `y`, according to a fixed total order. +`isgreater` is defined in terms of `isless`, but is not the opposite of that function. + +`isless` defines a fixed total order that ascends with unorderable values (such as `NaN`) and +[`missing`](@ref) ordered last (biggest). +`isgreater` defines a fixed total order that descends with unorderable values +and `missing` ordered last (smallest). + +Values that are normally unordered, such as `NaN`, +are ordered after regular values. +[`missing`](@ref) values are ordered last. + +# Implementation +Types should usually not implement this function. Instead, implement `isless`. +""" +isgreater(a, b) = _is_unorderable(a) || _is_unorderable(b) ? isless(a, b) : isless(b, a) +_is_unorderable(x) = !isa(x == x, Bool) || x != x + + function ==(T::Type, S::Type) @_pure_meta diff --git a/test/operators.jl b/test/operators.jl index 08f0b0179c81c..747742d9ee10e 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -83,6 +83,14 @@ import Base.< @test isless('a','b') +@testset "isgreater" begin + @test !isgreater(missing, 1) + @test isgreater(5, 1) + @test !isgreater(1, 5) + @test isgreater(1, missing) + @test isgreater(1, NaN) +end + @testset "vectorized comparisons between numbers" begin @test 1 .!= 2 @test 1 .== 1 From 8767109caee6f9795773a4c17c38d307c9de08cb Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Mon, 30 Mar 2020 18:14:26 +0100 Subject: [PATCH 03/29] Add 2-arg versions of findmax/min, argmax/min Fixes #27613. Related: #27639, #27612, #34674. Thanks to @tkf, @StefanKarpinski and @drewrobson for their assistance with this PR. --- NEWS.md | 1 + base/array.jl | 134 --------------------------------- base/reduce.jl | 196 +++++++++++++++++++++++++++++++++++++++++++++++++ test/reduce.jl | 32 ++++++++ 4 files changed, 229 insertions(+), 134 deletions(-) diff --git a/NEWS.md b/NEWS.md index e4d394e29cc02..efe95c213b236 100644 --- a/NEWS.md +++ b/NEWS.md @@ -29,6 +29,7 @@ New library functions --------------------- * New function `isgreater(a, b)` defines a descending total order where unorderable values and missing are ordered smaller than any regular value. +* Two argument methods `findmax(f, domain)`, `argmax(f, domain)` and the corresponding `min` versions ([#27613]). New library features -------------------- diff --git a/base/array.jl b/base/array.jl index aac35d6049a77..07c1fb248c57b 100644 --- a/base/array.jl +++ b/base/array.jl @@ -2201,140 +2201,6 @@ findall(x::Bool) = x ? [1] : Vector{Int}() findall(testf::Function, x::Number) = testf(x) ? [1] : Vector{Int}() findall(p::Fix2{typeof(in)}, x::Number) = x in p.x ? [1] : Vector{Int}() -""" - findmax(itr) -> (x, index) - -Return the maximum element of the collection `itr` and its index or key. -If there are multiple maximal elements, then the first one will be returned. -If any data element is `NaN`, this element is returned. -The result is in line with `max`. - -The collection must not be empty. - -# Examples -```jldoctest -julia> findmax([8,0.1,-9,pi]) -(8.0, 1) - -julia> findmax([1,7,7,6]) -(7, 2) - -julia> findmax([1,7,7,NaN]) -(NaN, 4) -``` -""" -findmax(a) = _findmax(a, :) - -function _findmax(a, ::Colon) - p = pairs(a) - y = iterate(p) - if y === nothing - throw(ArgumentError("collection must be non-empty")) - end - (mi, m), s = y - i = mi - while true - y = iterate(p, s) - y === nothing && break - m != m && break - (i, ai), s = y - if ai != ai || isless(m, ai) - m = ai - mi = i - end - end - return (m, mi) -end - -""" - findmin(itr) -> (x, index) - -Return the minimum element of the collection `itr` and its index or key. -If there are multiple minimal elements, then the first one will be returned. -If any data element is `NaN`, this element is returned. -The result is in line with `min`. - -The collection must not be empty. - -# Examples -```jldoctest -julia> findmin([8,0.1,-9,pi]) -(-9.0, 3) - -julia> findmin([7,1,1,6]) -(1, 2) - -julia> findmin([7,1,1,NaN]) -(NaN, 4) -``` -""" -findmin(a) = _findmin(a, :) - -function _findmin(a, ::Colon) - p = pairs(a) - y = iterate(p) - if y === nothing - throw(ArgumentError("collection must be non-empty")) - end - (mi, m), s = y - i = mi - while true - y = iterate(p, s) - y === nothing && break - m != m && break - (i, ai), s = y - if ai != ai || isless(ai, m) - m = ai - mi = i - end - end - return (m, mi) -end - -""" - argmax(itr) - -Return the index or key of the maximum element in a collection. -If there are multiple maximal elements, then the first one will be returned. - -The collection must not be empty. - -# Examples -```jldoctest -julia> argmax([8,0.1,-9,pi]) -1 - -julia> argmax([1,7,7,6]) -2 - -julia> argmax([1,7,7,NaN]) -4 -``` -""" -argmax(a) = findmax(a)[2] - -""" - argmin(itr) - -Return the index or key of the minimum element in a collection. -If there are multiple minimal elements, then the first one will be returned. - -The collection must not be empty. - -# Examples -```jldoctest -julia> argmin([8,0.1,-9,pi]) -3 - -julia> argmin([7,1,1,6]) -2 - -julia> argmin([7,1,1,NaN]) -4 -``` -""" -argmin(a) = findmin(a)[2] - # similar to Matlab's ismember """ indexin(a, b) diff --git a/base/reduce.jl b/base/reduce.jl index 8ea928669ab9e..4ccb037688205 100644 --- a/base/reduce.jl +++ b/base/reduce.jl @@ -762,6 +762,202 @@ Inf """ minimum(a; kw...) = mapreduce(identity, min, a; kw...) +## findmax, findmin, argmax & argmin + +""" + findmax(f, domain) -> (f(x), x) + findmax(f) + +Returns a pair of a value in the codomain (outputs of `f`) and the corresponding +value in the `domain` (inputs to `f`) such that `f(x)` is maximised. If there +are multiple maximal points, then the first one will be returned. + +When `domain` is provided it may be any iterable and must not be empty. + +When `domain` is omitted, `f` must have an implicit domain. In particular, if +`f` is an indexable collection, it is interpreted as a function mapping keys +(domain) to values (codomain), i.e. `findmax(itr)` returns the maximal element +of the collection `itr` and its index. + +Values are compared with `isless`. + +# Examples + +```jldoctest +julia> findmax(identity, 5:9) +(9, 9) + +julia> findmax(-, 1:10) +(-1, 1) + +julia> findmax(cos, 0:π/2:2π) +(1.0, 0.0) + +julia> findmax([8,0.1,-9,pi]) +(8.0, 1) + +julia> findmax([1,7,7,6]) +(7, 2) + +julia> findmax([1,7,7,NaN]) +(NaN, 4) +``` + +""" +findmax(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmax, domain) +_rf_findmax((fm, m), (fx, x)) = isless(fm, fx) ? (fx, x) : (fm, m) + +""" + findmin(f, domain) -> (f(x), x) + findmin(f) + +Returns a pair of a value in the codomain (outputs of `f`) and the corresponding +value in the `domain` (inputs to `f`) such that `f(x)` is minimised. If there +are multiple minimal points, then the first one will be returned. + +When `domain` is provided it may be any iterable and must not be empty. + +When `domain` is omitted, `f` must have an implicit domain. In particular, if +`f` is an indexable collection, it is interpreted as a function mapping keys +(domain) to values (codomain), i.e. `findmin(itr)` returns the minimal element +of the collection `itr` and its index. + +Values are compared with `isgreater`. + +# Examples + +```jldoctest +julia> findmin(identity, 5:9) +(5, 5) + +julia> findmin(-, 1:10) +(-10, 10) + +julia> findmin(cos, 0:π/2:2π) +(-1.0, 3.141592653589793) + +julia> findmin([8,0.1,-9,pi]) +(-9, 3) + +julia> findmin([1,7,7,6]) +(1, 1) + +julia> findmin([1,7,7,NaN]) +(NaN, 4) +``` + +""" +findmin(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmin, domain) +_rf_findmin((fm, m), (fx, x)) = isgreater(fm, fx) ? (fx, x) : (fm, m) + +findmax(a) = _findmax(a, :) + +function _findmax(a, ::Colon) + p = pairs(a) + y = iterate(p) + if y === nothing + throw(ArgumentError("collection must be non-empty")) + end + (mi, m), s = y + i = mi + while true + y = iterate(p, s) + y === nothing && break + m != m && break + (i, ai), s = y + if ai != ai || isless(m, ai) + m = ai + mi = i + end + end + return (m, mi) +end + +findmin(a) = _findmin(a, :) + +function _findmin(a, ::Colon) + p = pairs(a) + y = iterate(p) + if y === nothing + throw(ArgumentError("collection must be non-empty")) + end + (mi, m), s = y + i = mi + while true + y = iterate(p, s) + y === nothing && break + m != m && break + (i, ai), s = y + if ai != ai || isless(ai, m) + m = ai + mi = i + end + end + return (m, mi) +end + +""" + argmax(f, domain) + argmax(f) + +Return a value `x` in the domain of `f` for which `f(x)` is maximised. +If there are multiple maximal values for `f(x)` then the first one will be found. + +When `domain` is provided it may be any iterable and must not be empty. + +When `domain` is omitted, `f` must have an implicit domain. In particular, if +`f` is an indexable collection, it is interpreted as a function mapping keys +(domain) to values (codomain), i.e. `argmax(itr)` returns the index of the +maximal element in `itr`. + +Values are compared with `isless`. + +# Examples +```jldoctest +julia> argmax([8,0.1,-9,pi]) +1 + +julia> argmax([1,7,7,6]) +2 + +julia> argmax([1,7,7,NaN]) +4 +``` +""" +argmax(f, domain) = findmax(f, domain)[2] +argmax(f) = findmax(f)[2] + +""" + argmin(f, domain) + argmin(f) + +Return a value `x` in the domain of `f` for which `f(x)` is minimised. +If there are multiple minimal values for `f(x)` then the first one will be found. + +When `domain` is provided it may be any iterable and must not be empty. + +When `domain` is omitted, `f` must have an implicit domain. In particular, if +`f` is an indexable collection, it is interpreted as a function mapping keys +(domain) to values (codomain), i.e. `argmin(itr)` returns the index of the +minimal element in `itr`. + +Values are compared with `isgreater`. + +# Examples +```jldoctest +julia> argmin([8,0.1,-9,pi]) +3 + +julia> argmin([7,1,1,6]) +2 + +julia> argmin([7,1,1,NaN]) +4 +``` +""" +argmin(f, domain) = findmin(f, domain)[2] +argmin(f) = findmin(f)[2] + ## all & any """ diff --git a/test/reduce.jl b/test/reduce.jl index 4f9fc33403282..6cbe01a5de1cf 100644 --- a/test/reduce.jl +++ b/test/reduce.jl @@ -387,6 +387,38 @@ A = circshift(reshape(1:24,2,3,4), (0,1,1)) end end +# findmin, findmax, argmin, argmax + +@testset "findmin(f, domain)" begin + @test findmin(-, 1:10) == (-10, 10) + @test findmin(identity, [1, 2, 3, missing]) === (missing, missing) + @test findmin(identity, [1, NaN, 3, missing]) === (missing, missing) + @test findmin(identity, [1, missing, NaN, 3]) === (missing, missing) + @test findmin(identity, [1, NaN, 3]) === (NaN, NaN) + @test findmin(identity, [1, 3, NaN]) === (NaN, NaN) + @test all(findmin(cos, 0:π/2:2π) .≈ (-1.0, π)) +end + +@testset "findmax(f, domain)" begin + @test findmax(-, 1:10) == (-1, 1) + @test findmax(identity, [1, 2, 3, missing]) === (missing, missing) + @test findmax(identity, [1, NaN, 3, missing]) === (missing, missing) + @test findmax(identity, [1, missing, NaN, 3]) === (missing, missing) + @test findmax(identity, [1, NaN, 3]) === (NaN, NaN) + @test findmax(identity, [1, 3, NaN]) === (NaN, NaN) + @test findmax(cos, 0:π/2:2π) == (1.0, 0.0) +end + +@testset "argmin(f, domain)" begin + @test argmin(-, 1:10) == 10 + @test argmin(sum, Iterators.product(1:5, 1:5)) == (1, 1) +end + +@testset "argmax(f, domain)" begin + @test argmax(-, 1:10) == 1 + @test argmax(sum, Iterators.product(1:5, 1:5)) == (5, 5) +end + # any & all @test @inferred any([]) == false From 72c459d57ed272855f32d46f93dcd8b92d88151f Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Tue, 31 Mar 2020 20:40:50 +0100 Subject: [PATCH 04/29] Clarify description in NEWS.md --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index efe95c213b236..98b844e548eec 100644 --- a/NEWS.md +++ b/NEWS.md @@ -28,7 +28,7 @@ Build system changes New library functions --------------------- -* New function `isgreater(a, b)` defines a descending total order where unorderable values and missing are ordered smaller than any regular value. +* New function `isgreater(a, b)` defines a descending total order where `missing` and normally unordered values, such as `NaN`, are ordered smaller than any regular value. * Two argument methods `findmax(f, domain)`, `argmax(f, domain)` and the corresponding `min` versions ([#27613]). New library features From 1eb223b76f6a7908e7c6ce7ae8bb3046ee678089 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Tue, 31 Mar 2020 21:01:59 +0100 Subject: [PATCH 05/29] More doctest examples for argmin/max --- base/reduce.jl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/base/reduce.jl b/base/reduce.jl index 4ccb037688205..28b2fe1a4d528 100644 --- a/base/reduce.jl +++ b/base/reduce.jl @@ -914,6 +914,12 @@ Values are compared with `isless`. # Examples ```jldoctest +julia> argmax(abs, -10:5) +-10 + +julia> argmax(cos, 0:π/2:2π) +0 + julia> argmax([8,0.1,-9,pi]) 1 @@ -945,6 +951,15 @@ Values are compared with `isgreater`. # Examples ```jldoctest +julia> argmin(sign, -10:5) +-10 + +julia> argmin(x -> -x^3 + x^2 - 10, -5:5) +5 + +julia> argmin(acos, 0:0.1:1) +1.0 + julia> argmin([8,0.1,-9,pi]) 3 From fc0dbab029bac123b86283fc94f33166a5ccd665 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Tue, 31 Mar 2020 21:15:31 +0100 Subject: [PATCH 06/29] Clarify `isgreater` docstring --- base/operators.jl | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/base/operators.jl b/base/operators.jl index 3ca937512dc2d..811dca419de1e 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -174,17 +174,14 @@ isless(x::AbstractFloat, y::Real ) = (!isnan(x) & (isnan(y) | signless(x Test whether `x` is greater than `y`, according to a fixed total order. `isgreater` is defined in terms of `isless`, but is not the opposite of that function. -`isless` defines a fixed total order that ascends with unorderable values (such as `NaN`) and -[`missing`](@ref) ordered last (biggest). -`isgreater` defines a fixed total order that descends with unorderable values -and `missing` ordered last (smallest). - -Values that are normally unordered, such as `NaN`, -are ordered after regular values. -[`missing`](@ref) values are ordered last. +`isless` defines a fixed total order that ascends, while `isgreater` defines a +fixed total order that descends. Both order values that are normally unordered, +such as `NaN`, after all regular values and [`missing`](@ref) last. So for +`isless` these values are biggest and for `isgreater` these values are +smallest. # Implementation -Types should usually not implement this function. Instead, implement `isless`. +Types should not usually implement this function. Instead, implement `isless`. """ isgreater(a, b) = _is_unorderable(a) || _is_unorderable(b) ? isless(a, b) : isless(b, a) _is_unorderable(x) = !isa(x == x, Bool) || x != x From 9231741ccdebf813ba3a1246fed8a33178aa0eae Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Tue, 31 Mar 2020 21:20:32 +0100 Subject: [PATCH 07/29] Update `isgreater` from review - Explicitly cache `x == x` to save the compiler some work. - Rename _is_unorderable to !_is_reflexive. --- base/operators.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/base/operators.jl b/base/operators.jl index 811dca419de1e..7b8274b5658e1 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -183,8 +183,10 @@ smallest. # Implementation Types should not usually implement this function. Instead, implement `isless`. """ -isgreater(a, b) = _is_unorderable(a) || _is_unorderable(b) ? isless(a, b) : isless(b, a) -_is_unorderable(x) = !isa(x == x, Bool) || x != x +isgreater(x, y) = _is_reflexive(x) && _is_reflexive(y) ? isless(y, x) : isless(x, y) +_is_reflexive(x) = let eq = x == x + isa(eq, Bool) && eq +end From 4a851795c5ace1e4cc8d881ea9decbc413b71f5e Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Sat, 21 Nov 2020 15:37:17 +0000 Subject: [PATCH 08/29] Simplify _is_reflexive and rename _is_reflexive_eq Co-authored-by: Jameson Nash --- base/operators.jl | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/base/operators.jl b/base/operators.jl index 7b8274b5658e1..6f61580db66cb 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -183,12 +183,8 @@ smallest. # Implementation Types should not usually implement this function. Instead, implement `isless`. """ -isgreater(x, y) = _is_reflexive(x) && _is_reflexive(y) ? isless(y, x) : isless(x, y) -_is_reflexive(x) = let eq = x == x - isa(eq, Bool) && eq -end - - +isgreater(x, y) = _is_reflexive_eq(x) && _is_reflexive_eq(y) ? isless(y, x) : isless(x, y) +_is_reflexive_eq(x) = (x == x) === true function ==(T::Type, S::Type) @_pure_meta From 31a38a040a8aa1c2164ff7fc105fa1b1332701fd Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Sat, 21 Nov 2020 15:39:55 +0000 Subject: [PATCH 09/29] unexport isgreater --- NEWS.md | 1 - base/exports.jl | 1 - test/operators.jl | 12 +++++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/NEWS.md b/NEWS.md index 98b844e548eec..862dcb8bdddfc 100644 --- a/NEWS.md +++ b/NEWS.md @@ -28,7 +28,6 @@ Build system changes New library functions --------------------- -* New function `isgreater(a, b)` defines a descending total order where `missing` and normally unordered values, such as `NaN`, are ordered smaller than any regular value. * Two argument methods `findmax(f, domain)`, `argmax(f, domain)` and the corresponding `min` versions ([#27613]). New library features diff --git a/base/exports.jl b/base/exports.jl index 628ca3473ecb9..0c157c45d2052 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -657,7 +657,6 @@ export isequal, ismutable, isless, - isgreater, ifelse, objectid, sizeof, diff --git a/test/operators.jl b/test/operators.jl index 747742d9ee10e..9c81a6616cfc7 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -84,11 +84,13 @@ import Base.< @test isless('a','b') @testset "isgreater" begin - @test !isgreater(missing, 1) - @test isgreater(5, 1) - @test !isgreater(1, 5) - @test isgreater(1, missing) - @test isgreater(1, NaN) + let isgreater = Base.isgreater + @test !isgreater(missing, 1) + @test isgreater(5, 1) + @test !isgreater(1, 5) + @test isgreater(1, missing) + @test isgreater(1, NaN) + end end @testset "vectorized comparisons between numbers" begin From 6cc600ec7cab96795a3d903802d1710a8e5f2286 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Sat, 21 Nov 2020 15:43:47 +0000 Subject: [PATCH 10/29] Use 2-arg findmax/min for findmax/min(a) --- base/reduce.jl | 44 ++------------------------------------------ 1 file changed, 2 insertions(+), 42 deletions(-) diff --git a/base/reduce.jl b/base/reduce.jl index 28b2fe1a4d528..715852141a814 100644 --- a/base/reduce.jl +++ b/base/reduce.jl @@ -851,50 +851,10 @@ findmin(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmin, domain) _rf_findmin((fm, m), (fx, x)) = isgreater(fm, fx) ? (fx, x) : (fm, m) findmax(a) = _findmax(a, :) - -function _findmax(a, ::Colon) - p = pairs(a) - y = iterate(p) - if y === nothing - throw(ArgumentError("collection must be non-empty")) - end - (mi, m), s = y - i = mi - while true - y = iterate(p, s) - y === nothing && break - m != m && break - (i, ai), s = y - if ai != ai || isless(m, ai) - m = ai - mi = i - end - end - return (m, mi) -end +_findmax(a, ::Colon) = findmax(idx -> a[idx], keys(a)) findmin(a) = _findmin(a, :) - -function _findmin(a, ::Colon) - p = pairs(a) - y = iterate(p) - if y === nothing - throw(ArgumentError("collection must be non-empty")) - end - (mi, m), s = y - i = mi - while true - y = iterate(p, s) - y === nothing && break - m != m && break - (i, ai), s = y - if ai != ai || isless(ai, m) - m = ai - mi = i - end - end - return (m, mi) -end +_findmin(a, ::Colon) = findmin(idx -> a[idx], keys(a)) """ argmax(f, domain) From 4c6923a5752eb84c834c8e55175d22e462be82de Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Sat, 21 Nov 2020 16:36:51 +0000 Subject: [PATCH 11/29] Fix doctest --- base/reduce.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/reduce.jl b/base/reduce.jl index 715852141a814..08dc205310fa6 100644 --- a/base/reduce.jl +++ b/base/reduce.jl @@ -837,7 +837,7 @@ julia> findmin(cos, 0:π/2:2π) (-1.0, 3.141592653589793) julia> findmin([8,0.1,-9,pi]) -(-9, 3) +(-9.0, 3) julia> findmin([1,7,7,6]) (1, 1) From 7c7cfbec0a0005b66679f58880a7d1807db4ec21 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Sat, 21 Nov 2020 17:33:51 +0000 Subject: [PATCH 12/29] Add example showing that first value wins --- base/reduce.jl | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/base/reduce.jl b/base/reduce.jl index 08dc205310fa6..685a86bdfb9c3 100644 --- a/base/reduce.jl +++ b/base/reduce.jl @@ -790,16 +790,19 @@ julia> findmax(identity, 5:9) julia> findmax(-, 1:10) (-1, 1) +julia> findmax(first, [(1, :a), (2, :b), (2, :c)]) +(2, (2, :b)) + julia> findmax(cos, 0:π/2:2π) (1.0, 0.0) -julia> findmax([8,0.1,-9,pi]) +julia> findmax([8, 0.1, -9, pi]) (8.0, 1) -julia> findmax([1,7,7,6]) +julia> findmax([1, 7, 7, 6]) (7, 2) -julia> findmax([1,7,7,NaN]) +julia> findmax([1, 7, 7, NaN]) (NaN, 4) ``` @@ -833,16 +836,19 @@ julia> findmin(identity, 5:9) julia> findmin(-, 1:10) (-10, 10) +julia> findmin(first, [(1, :a), (1, :b), (2, :c)]) +(1, (1, :a)) + julia> findmin(cos, 0:π/2:2π) (-1.0, 3.141592653589793) -julia> findmin([8,0.1,-9,pi]) +julia> findmin([8, 0.1, -9, pi]) (-9.0, 3) -julia> findmin([1,7,7,6]) +julia> findmin([1, 7, 7, 6]) (1, 1) -julia> findmin([1,7,7,NaN]) +julia> findmin([1, 7, 7, NaN]) (NaN, 4) ``` From 8356986067a46a2dd60220e0acc01cf0bf20c25b Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Sat, 21 Nov 2020 20:35:04 +0000 Subject: [PATCH 13/29] Fix more doctests --- base/reduce.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/base/reduce.jl b/base/reduce.jl index 685a86bdfb9c3..cf435bc5b7bd0 100644 --- a/base/reduce.jl +++ b/base/reduce.jl @@ -884,15 +884,15 @@ julia> argmax(abs, -10:5) -10 julia> argmax(cos, 0:π/2:2π) -0 +0.0 -julia> argmax([8,0.1,-9,pi]) +julia> argmax([8, 0.1, -9, pi]) 1 -julia> argmax([1,7,7,6]) +julia> argmax([1, 7, 7, 6]) 2 -julia> argmax([1,7,7,NaN]) +julia> argmax([1, 7, 7, NaN]) 4 ``` """ @@ -926,13 +926,13 @@ julia> argmin(x -> -x^3 + x^2 - 10, -5:5) julia> argmin(acos, 0:0.1:1) 1.0 -julia> argmin([8,0.1,-9,pi]) +julia> argmin([8, 0.1, -9, pi]) 3 -julia> argmin([7,1,1,6]) +julia> argmin([7, 1, 1, 6]) 2 -julia> argmin([7,1,1,NaN]) +julia> argmin([7, 1, 1, NaN]) 4 ``` """ From 8061ce834350871ca5e14e59d098dc7d97fccc65 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Tue, 24 Nov 2020 23:33:43 +0000 Subject: [PATCH 14/29] omit unnecessary let Co-authored-by: Takafumi Arakaki --- test/operators.jl | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/operators.jl b/test/operators.jl index 9c81a6616cfc7..6ba41c2ace2a3 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -84,13 +84,12 @@ import Base.< @test isless('a','b') @testset "isgreater" begin - let isgreater = Base.isgreater - @test !isgreater(missing, 1) - @test isgreater(5, 1) - @test !isgreater(1, 5) - @test isgreater(1, missing) - @test isgreater(1, NaN) - end + isgreater = Base.isgreater + @test !isgreater(missing, 1) + @test isgreater(5, 1) + @test !isgreater(1, 5) + @test isgreater(1, missing) + @test isgreater(1, NaN) end @testset "vectorized comparisons between numbers" begin From 96433cfe2417c915aeaf5fe883d271cfa73fe65f Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Wed, 2 Dec 2020 14:44:06 +0000 Subject: [PATCH 15/29] define isgreater to be compatible with min --- base/operators.jl | 51 +++++++++++++++++++++++++++++++++++++---------- test/operators.jl | 16 +++++++++------ 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/base/operators.jl b/base/operators.jl index 6f61580db66cb..d6ecf72ebaa72 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -171,20 +171,51 @@ isless(x::AbstractFloat, y::Real ) = (!isnan(x) & (isnan(y) | signless(x """ isgreater(x, y) -Test whether `x` is greater than `y`, according to a fixed total order. -`isgreater` is defined in terms of `isless`, but is not the opposite of that function. +Not the inverse of `isless`! Test whether `x` is greater than `y`, according to +a fixed total order compatible with `min`. -`isless` defines a fixed total order that ascends, while `isgreater` defines a -fixed total order that descends. Both order values that are normally unordered, -such as `NaN`, after all regular values and [`missing`](@ref) last. So for -`isless` these values are biggest and for `isgreater` these values are -smallest. +Defined with `isless`, this function is usually `isless(y, x)`, but `NaN` and +[`missing`](@ref) are ordered as smaller than any ordinary value with `missing` +smaller than `NaN`. + +So `isless` defines an ascending total order with `NaN` and `missing` as the +largest values and `isgreater` defines a descending total order with `NaN` and +`missing` as the smallest values. + +!!! note + + Like `min`, `isgreater` orders containers (tuples, vectors, etc) + lexigraphically with `isless(y, x)` rather than recursively with itself: + + ```jldoctest + julia> isgreater(1, NaN) # 1 is greater than NaN + true + + julia> isgreater((1,), (NaN,)) # But (1,) is not greater than (NaN,) + false + + julia> sort([1, 2, 3, NaN]; lt=isgreater) + 4-element Vector{Float64}: + 3.0 + 2.0 + 1.0 + NaN + + julia> sort(tuple.([1, 2, 3, NaN]); lt=isgreater) + 4-element Vector{Tuple{Float64}}: + (NaN,) + (3.0,) + (2.0,) + (1.0,) + ``` # Implementation -Types should not usually implement this function. Instead, implement `isless`. +This is unexported. Types should not usually implement this function. Instead, implement `isless`. """ -isgreater(x, y) = _is_reflexive_eq(x) && _is_reflexive_eq(y) ? isless(y, x) : isless(x, y) -_is_reflexive_eq(x) = (x == x) === true +isgreater(x, y) = is_poisoning(x) || is_poisoning(y) ? isless(x, y) : isless(y, x) +is_poisoning(x) = false +is_poisoning(x::AbstractFloat) = isnan(x) +is_poisoning(x::Missing) = true function ==(T::Type, S::Type) @_pure_meta diff --git a/test/operators.jl b/test/operators.jl index 6ba41c2ace2a3..6e50e283172a4 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -84,12 +84,16 @@ import Base.< @test isless('a','b') @testset "isgreater" begin - isgreater = Base.isgreater - @test !isgreater(missing, 1) - @test isgreater(5, 1) - @test !isgreater(1, 5) - @test isgreater(1, missing) - @test isgreater(1, NaN) + # isgreater should be compatible with min. + min1(a, b) = isgreater(a, b) ? b : a + # min promotes numerical arguments to the same type, but our quick min1 + # doesn't, so use float test values instead of ints. + values = (1.0, 5.0, NaN, missing, Inf) + for a in values, b in values + @test min(a, b) === min1(a, b) + @test min((a,), (b,)) === min1((a,), (b,)) + @test all(min([a], [b]) .=== min1([a], [b])) + end end @testset "vectorized comparisons between numbers" begin From 91a825fd01f7213c4c2f1093b26bbe2bac0146eb Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Wed, 2 Dec 2020 15:22:38 +0000 Subject: [PATCH 16/29] Fix doctests --- base/operators.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/operators.jl b/base/operators.jl index d6ecf72ebaa72..dcb90d534d4e9 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -188,20 +188,20 @@ largest values and `isgreater` defines a descending total order with `NaN` and lexigraphically with `isless(y, x)` rather than recursively with itself: ```jldoctest - julia> isgreater(1, NaN) # 1 is greater than NaN + julia> Base.isgreater(1, NaN) # 1 is greater than NaN true - julia> isgreater((1,), (NaN,)) # But (1,) is not greater than (NaN,) + julia> Base.isgreater((1,), (NaN,)) # But (1,) is not greater than (NaN,) false - julia> sort([1, 2, 3, NaN]; lt=isgreater) + julia> sort([1, 2, 3, NaN]; lt=Base.isgreater) 4-element Vector{Float64}: 3.0 2.0 1.0 NaN - julia> sort(tuple.([1, 2, 3, NaN]); lt=isgreater) + julia> sort(tuple.([1, 2, 3, NaN]); lt=Base.isgreater) 4-element Vector{Tuple{Float64}}: (NaN,) (3.0,) From 51c1d07f5e11b43373d9049d683422dc85d1619d Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Wed, 2 Dec 2020 17:21:05 +0000 Subject: [PATCH 17/29] typo --- test/operators.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/operators.jl b/test/operators.jl index 6e50e283172a4..acf1636947806 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -85,7 +85,7 @@ import Base.< @testset "isgreater" begin # isgreater should be compatible with min. - min1(a, b) = isgreater(a, b) ? b : a + min1(a, b) = Base.isgreater(a, b) ? b : a # min promotes numerical arguments to the same type, but our quick min1 # doesn't, so use float test values instead of ints. values = (1.0, 5.0, NaN, missing, Inf) From f3dac8cbb29ed133bebea0a98a25e41809506337 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Sun, 6 Dec 2020 13:01:13 +0000 Subject: [PATCH 18/29] Use pairs() in findmax(a) It's about as fast as working on indexes for arrays, and should be faster for other collections. Thanks again to @tkf. --- base/reduce.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/reduce.jl b/base/reduce.jl index cf435bc5b7bd0..77ba2ebe7dd11 100644 --- a/base/reduce.jl +++ b/base/reduce.jl @@ -810,6 +810,9 @@ julia> findmax([1, 7, 7, NaN]) findmax(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmax, domain) _rf_findmax((fm, m), (fx, x)) = isless(fm, fx) ? (fx, x) : (fm, m) +findmax(a) = _findmax(a, :) +_findmax(a, ::Colon) = mapfoldl( ((k, v),) -> (v, k), _rf_findmax, pairs(a) ) + """ findmin(f, domain) -> (f(x), x) findmin(f) @@ -856,11 +859,8 @@ julia> findmin([1, 7, 7, NaN]) findmin(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmin, domain) _rf_findmin((fm, m), (fx, x)) = isgreater(fm, fx) ? (fx, x) : (fm, m) -findmax(a) = _findmax(a, :) -_findmax(a, ::Colon) = findmax(idx -> a[idx], keys(a)) - findmin(a) = _findmin(a, :) -_findmin(a, ::Colon) = findmin(idx -> a[idx], keys(a)) +_findmin(a, ::Colon) = mapfoldl( ((k, v),) -> (v, k), _rf_findmin, pairs(a) ) """ argmax(f, domain) From 9ed02b2128efc23c13f35d15928dbd98d066b542 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Sun, 6 Dec 2020 18:40:23 +0000 Subject: [PATCH 19/29] Make findmax(A; dims) consistent with findmax(A) again findmax(A; dims) and friends can handle `missing` now, too. --- base/reducedim.jl | 28 +++++++++++++++++----------- test/reducedim.jl | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/base/reducedim.jl b/base/reducedim.jl index e9ca4f03dac7b..1a05208ea0ce9 100644 --- a/base/reducedim.jl +++ b/base/reducedim.jl @@ -144,10 +144,18 @@ for (f1, f2, initval) in ((:min, :max, :Inf), (:max, :min, :(-Inf))) # otherwise use the min/max of the first slice as initial value v0 = mapreduce(f, $f2, A1) - # but NaNs need to be avoided as initial values - v0 = v0 != v0 ? typeof(v0)($initval) : v0 - T = _realtype(f, promote_union(eltype(A))) + + # but NaNs and missing need to be avoided as initial values + if (v0 != v0) === true + v0 = typeof(v0)($initval) + elseif ismissing(v0) + # If it's a union type, pick the initval from the other type. + if typeof(T) == Union + v0 = (T.a == Missing ? T.b : T.a)($initval) + end + end + Tr = v0 isa T ? T : typeof(v0) return reducedim_initarray(A, region, v0, Tr) end @@ -926,7 +934,7 @@ function findminmax!(f, Rval, Rind, A::AbstractArray{T,N}) where {T,N} for i in axes(A,1) k, kss = y::Tuple tmpAv = A[i,IA] - if tmpRi == zi || (tmpRv == tmpRv && (tmpAv != tmpAv || f(tmpAv, tmpRv))) + if tmpRi == zi || f(tmpRv, tmpAv) tmpRv = tmpAv tmpRi = k end @@ -943,7 +951,7 @@ function findminmax!(f, Rval, Rind, A::AbstractArray{T,N}) where {T,N} tmpAv = A[i,IA] tmpRv = Rval[i,IR] tmpRi = Rind[i,IR] - if tmpRi == zi || (tmpRv == tmpRv && (tmpAv != tmpAv || f(tmpAv, tmpRv))) + if tmpRi == zi || f(tmpRv, tmpAv) Rval[i,IR] = tmpAv Rind[i,IR] = k end @@ -963,7 +971,7 @@ dimensions of `rval` and `rind`, and store the results in `rval` and `rind`. """ function findmin!(rval::AbstractArray, rind::AbstractArray, A::AbstractArray; init::Bool=true) - findminmax!(isless, init && !isempty(A) ? fill!(rval, first(A)) : rval, fill!(rind,zero(eltype(keys(A)))), A) + findminmax!(isgreater, init && !isempty(A) ? fill!(rval, first(A)) : rval, fill!(rind,zero(eltype(keys(A)))), A) end """ @@ -996,13 +1004,11 @@ function _findmin(A, region) end (similar(A, ri), zeros(eltype(keys(A)), ri)) else - findminmax!(isless, fill!(similar(A, ri), first(A)), + findminmax!(isgreater, fill!(similar(A, ri), first(A)), zeros(eltype(keys(A)), ri), A) end end -_isgreater(a, b) = isless(b,a) - """ findmax!(rval, rind, A) -> (maxval, index) @@ -1012,7 +1018,7 @@ dimensions of `rval` and `rind`, and store the results in `rval` and `rind`. """ function findmax!(rval::AbstractArray, rind::AbstractArray, A::AbstractArray; init::Bool=true) - findminmax!(_isgreater, init && !isempty(A) ? fill!(rval, first(A)) : rval, fill!(rind,zero(eltype(keys(A)))), A) + findminmax!(isless, init && !isempty(A) ? fill!(rval, first(A)) : rval, fill!(rind,zero(eltype(keys(A)))), A) end """ @@ -1045,7 +1051,7 @@ function _findmax(A, region) end similar(A, ri), zeros(eltype(keys(A)), ri) else - findminmax!(_isgreater, fill!(similar(A, ri), first(A)), + findminmax!(isless, fill!(similar(A, ri), first(A)), zeros(eltype(keys(A)), ri), A) end end diff --git a/test/reducedim.jl b/test/reducedim.jl index b6229634b6006..8fd48ac236a4d 100644 --- a/test/reducedim.jl +++ b/test/reducedim.jl @@ -219,6 +219,26 @@ for (tup, rval, rind) in [((1,), [5.0 5.0 6.0], [CartesianIndex(2,1) CartesianIn @test isequal(maximum!(copy(rval), A, init=false), rval) end +@testset "missing in findmin/findmax" begin + B = [1.0 missing NaN; + 5.0 NaN missing] + for (tup, rval, rind) in [(1, [5.0 missing missing], [CartesianIndex(2, 1) CartesianIndex(1, 2) CartesianIndex(2, 3)]), + (2, [missing; missing], [CartesianIndex(1, 2) CartesianIndex(2, 3)] |> permutedims)] + (rval′, rind′) = findmax(B, dims=tup) + @test all(rval′ .=== rval) + @test all(rind′ .== rind) + @test all(maximum(B, dims=tup) .=== rval) + end + + for (tup, rval, rind) in [(1, [1.0 missing missing], [CartesianIndex(1, 1) CartesianIndex(1, 2) CartesianIndex(2, 3)]), + (2, [missing; missing], [CartesianIndex(1, 2) CartesianIndex(2, 3)] |> permutedims)] + (rval′, rind′) = findmin(B, dims=tup) + @test all(rval′ .=== rval) + @test all(rind′ .== rind) + @test all(minimum(B, dims=tup) .=== rval) + end +end + #issue #23209 A = [1.0 3.0 6.0; From fae5eac52f5a77426fa969d76fa7abd3cc786681 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Sun, 6 Dec 2020 19:07:00 +0000 Subject: [PATCH 20/29] Update docstrings --- base/reduce.jl | 4 ++-- base/reducedim.jl | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/base/reduce.jl b/base/reduce.jl index 77ba2ebe7dd11..37c5588546862 100644 --- a/base/reduce.jl +++ b/base/reduce.jl @@ -828,7 +828,7 @@ When `domain` is omitted, `f` must have an implicit domain. In particular, if (domain) to values (codomain), i.e. `findmin(itr)` returns the minimal element of the collection `itr` and its index. -Values are compared with `isgreater`. +`NaN` is treated as less than all other values except `missing`. # Examples @@ -913,7 +913,7 @@ When `domain` is omitted, `f` must have an implicit domain. In particular, if (domain) to values (codomain), i.e. `argmin(itr)` returns the index of the minimal element in `itr`. -Values are compared with `isgreater`. +`NaN` is treated as less than all other values except `missing`. # Examples ```jldoctest diff --git a/base/reducedim.jl b/base/reducedim.jl index 1a05208ea0ce9..116a667ac400a 100644 --- a/base/reducedim.jl +++ b/base/reducedim.jl @@ -967,7 +967,7 @@ end Find the minimum of `A` and the corresponding linear index along singleton dimensions of `rval` and `rind`, and store the results in `rval` and `rind`. -`NaN` is treated as less than all other values. +`NaN` is treated as less than all other values except `missing`. """ function findmin!(rval::AbstractArray, rind::AbstractArray, A::AbstractArray; init::Bool=true) @@ -978,7 +978,7 @@ end findmin(A; dims) -> (minval, index) For an array input, returns the value and index of the minimum over the given dimensions. -`NaN` is treated as less than all other values. +`NaN` is treated as less than all other values except `missing`. # Examples ```jldoctest @@ -1014,7 +1014,7 @@ end Find the maximum of `A` and the corresponding linear index along singleton dimensions of `rval` and `rind`, and store the results in `rval` and `rind`. -`NaN` is treated as greater than all other values. +`NaN` is treated as greater than all other values except `missing`. """ function findmax!(rval::AbstractArray, rind::AbstractArray, A::AbstractArray; init::Bool=true) @@ -1025,7 +1025,7 @@ end findmax(A; dims) -> (maxval, index) For an array input, returns the value and index of the maximum over the given dimensions. -`NaN` is treated as greater than all other values. +`NaN` is treated as greater than all other values except `missing`. # Examples ```jldoctest @@ -1062,7 +1062,7 @@ reducedim1(R, A) = length(axes1(R)) == 1 argmin(A; dims) -> indices For an array input, return the indices of the minimum elements over the given dimensions. -`NaN` is treated as less than all other values. +`NaN` is treated as less than all other values except `missing`. # Examples ```jldoctest @@ -1087,7 +1087,7 @@ argmin(A::AbstractArray; dims=:) = findmin(A; dims=dims)[2] argmax(A; dims) -> indices For an array input, return the indices of the maximum elements over the given dimensions. -`NaN` is treated as greater than all other values. +`NaN` is treated as greater than all other values except `missing`. # Examples ```jldoctest From 3671795f16e10a4fe92e8a605b63c4f2beb3d824 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Wed, 16 Dec 2020 17:21:01 +0000 Subject: [PATCH 21/29] Use convert in reducedim_init Thanks @nalimilan for the suggestion. This is simpler, more extensible and should handle larger unions. --- base/reducedim.jl | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/base/reducedim.jl b/base/reducedim.jl index 116a667ac400a..fb087832320a3 100644 --- a/base/reducedim.jl +++ b/base/reducedim.jl @@ -147,13 +147,9 @@ for (f1, f2, initval) in ((:min, :max, :Inf), (:max, :min, :(-Inf))) T = _realtype(f, promote_union(eltype(A))) # but NaNs and missing need to be avoided as initial values - if (v0 != v0) === true - v0 = typeof(v0)($initval) - elseif ismissing(v0) - # If it's a union type, pick the initval from the other type. - if typeof(T) == Union - v0 = (T.a == Missing ? T.b : T.a)($initval) - end + if is_poisoning(v0) + # Convert handles unions containing Missing nicely. + v0 = convert(T, $initval) end Tr = v0 isa T ? T : typeof(v0) From 202edf79f856a3049ff5adb6dd1dbb6b9a6c850a Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Thu, 17 Dec 2020 21:48:42 +0000 Subject: [PATCH 22/29] Support missing better in minimum(A; dims) convert(T, Inf) fails for lots of T (e.g. Int). --- base/reducedim.jl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/base/reducedim.jl b/base/reducedim.jl index fb087832320a3..47aa1eac8a1a1 100644 --- a/base/reducedim.jl +++ b/base/reducedim.jl @@ -125,7 +125,7 @@ function _reducedim_init(f, op, fv, fop, A, region) end # initialization when computing minima and maxima requires a little care -for (f1, f2, initval) in ((:min, :max, :Inf), (:max, :min, :(-Inf))) +for (f1, f2, typeextreme) in ((:min, :max, :typemax), (:max, :min, :typemin)) @eval function reducedim_init(f, op::typeof($f1), A::AbstractArray, region) # First compute the reduce indices. This will throw an ArgumentError # if any region is invalid @@ -145,14 +145,18 @@ for (f1, f2, initval) in ((:min, :max, :Inf), (:max, :min, :(-Inf))) v0 = mapreduce(f, $f2, A1) T = _realtype(f, promote_union(eltype(A))) + Tr = v0 isa T ? T : typeof(v0) # but NaNs and missing need to be avoided as initial values if is_poisoning(v0) - # Convert handles unions containing Missing nicely. - v0 = convert(T, $initval) + Tnm = nonmissingtype(Tr) + # TODO: Some types, like BigInt, don't support typemin/typemax. + # So a Matrix{Union{BigInt, Missing}} can still error here. + v0 = $typeextreme(Tnm) end - + # v0 may have changed type. Tr = v0 isa T ? T : typeof(v0) + return reducedim_initarray(A, region, v0, Tr) end end From 7aa7fdbbde2da338ec61c9fb2e1212d5291eb502 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Thu, 17 Dec 2020 21:55:02 +0000 Subject: [PATCH 23/29] rename is_poisoning -> isunordered and export --- base/exports.jl | 1 + base/operators.jl | 8 ++++---- base/reducedim.jl | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/base/exports.jl b/base/exports.jl index 0c157c45d2052..702c1bf485c3b 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -657,6 +657,7 @@ export isequal, ismutable, isless, + isunordered, ifelse, objectid, sizeof, diff --git a/base/operators.jl b/base/operators.jl index dcb90d534d4e9..992a5e4c9b52e 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -212,10 +212,10 @@ largest values and `isgreater` defines a descending total order with `NaN` and # Implementation This is unexported. Types should not usually implement this function. Instead, implement `isless`. """ -isgreater(x, y) = is_poisoning(x) || is_poisoning(y) ? isless(x, y) : isless(y, x) -is_poisoning(x) = false -is_poisoning(x::AbstractFloat) = isnan(x) -is_poisoning(x::Missing) = true +isgreater(x, y) = isunordered(x) || isunordered(y) ? isless(x, y) : isless(y, x) +isunordered(x) = false +isunordered(x::AbstractFloat) = isnan(x) +isunordered(x::Missing) = true function ==(T::Type, S::Type) @_pure_meta diff --git a/base/reducedim.jl b/base/reducedim.jl index 47aa1eac8a1a1..ce294ec473a54 100644 --- a/base/reducedim.jl +++ b/base/reducedim.jl @@ -148,7 +148,7 @@ for (f1, f2, typeextreme) in ((:min, :max, :typemax), (:max, :min, :typemin)) Tr = v0 isa T ? T : typeof(v0) # but NaNs and missing need to be avoided as initial values - if is_poisoning(v0) + if isunordered(v0) Tnm = nonmissingtype(Tr) # TODO: Some types, like BigInt, don't support typemin/typemax. # So a Matrix{Union{BigInt, Missing}} can still error here. From c8f05bb8bba5854512d4910f52e103c5887b97e5 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Thu, 17 Dec 2020 21:58:59 +0000 Subject: [PATCH 24/29] Add tests for isunordered --- test/operators.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/operators.jl b/test/operators.jl index acf1636947806..c14858657ce3b 100644 --- a/test/operators.jl +++ b/test/operators.jl @@ -96,6 +96,15 @@ import Base.< end end +@testset "isunordered" begin + @test isunordered(NaN) + @test isunordered(NaN32) + @test isunordered(missing) + @test !isunordered(1) + @test !isunordered([NaN, 1]) + @test !isunordered([1.0, missing]) +end + @testset "vectorized comparisons between numbers" begin @test 1 .!= 2 @test 1 .== 1 From abf1be4ca9f12e5fc16d32c3044d6710df9ab19c Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Thu, 17 Dec 2020 22:23:06 +0000 Subject: [PATCH 25/29] Simplify findmin/findmax/argmin/argmax docstrings --- base/reduce.jl | 110 ++++++++++++++++++++++++++++++------------------- 1 file changed, 68 insertions(+), 42 deletions(-) diff --git a/base/reduce.jl b/base/reduce.jl index 37c5588546862..1dbb1f2d54e9d 100644 --- a/base/reduce.jl +++ b/base/reduce.jl @@ -766,18 +766,12 @@ minimum(a; kw...) = mapreduce(identity, min, a; kw...) """ findmax(f, domain) -> (f(x), x) - findmax(f) Returns a pair of a value in the codomain (outputs of `f`) and the corresponding value in the `domain` (inputs to `f`) such that `f(x)` is maximised. If there are multiple maximal points, then the first one will be returned. -When `domain` is provided it may be any iterable and must not be empty. - -When `domain` is omitted, `f` must have an implicit domain. In particular, if -`f` is an indexable collection, it is interpreted as a function mapping keys -(domain) to values (codomain), i.e. `findmax(itr)` returns the maximal element -of the collection `itr` and its index. +`domain` must be a non-empty iterable. Values are compared with `isless`. @@ -795,7 +789,21 @@ julia> findmax(first, [(1, :a), (2, :b), (2, :c)]) julia> findmax(cos, 0:π/2:2π) (1.0, 0.0) +``` +""" +findmax(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmax, domain) +_rf_findmax((fm, m), (fx, x)) = isless(fm, fx) ? (fx, x) : (fm, m) + +""" + findmax(itr) -> (x, index) +Return the maximal element of the collection `itr` and its index or key. +If there are multiple maximal elements, then the first one will be returned. +Values are compared with `isless`. + +# Examples + +```jldoctest julia> findmax([8, 0.1, -9, pi]) (8.0, 1) @@ -805,28 +813,18 @@ julia> findmax([1, 7, 7, 6]) julia> findmax([1, 7, 7, NaN]) (NaN, 4) ``` - """ -findmax(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmax, domain) -_rf_findmax((fm, m), (fx, x)) = isless(fm, fx) ? (fx, x) : (fm, m) - -findmax(a) = _findmax(a, :) +findmax(itr) = _findmax(itr, :) _findmax(a, ::Colon) = mapfoldl( ((k, v),) -> (v, k), _rf_findmax, pairs(a) ) """ findmin(f, domain) -> (f(x), x) - findmin(f) Returns a pair of a value in the codomain (outputs of `f`) and the corresponding value in the `domain` (inputs to `f`) such that `f(x)` is minimised. If there are multiple minimal points, then the first one will be returned. -When `domain` is provided it may be any iterable and must not be empty. - -When `domain` is omitted, `f` must have an implicit domain. In particular, if -`f` is an indexable collection, it is interpreted as a function mapping keys -(domain) to values (codomain), i.e. `findmin(itr)` returns the minimal element -of the collection `itr` and its index. +`domain` must be a non-empty iterable. `NaN` is treated as less than all other values except `missing`. @@ -844,7 +842,22 @@ julia> findmin(first, [(1, :a), (1, :b), (2, :c)]) julia> findmin(cos, 0:π/2:2π) (-1.0, 3.141592653589793) +``` + +""" +findmin(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmin, domain) +_rf_findmin((fm, m), (fx, x)) = isgreater(fm, fx) ? (fx, x) : (fm, m) +""" + findmin(itr) -> (x, index) + +Return the minimal element of the collection `itr` and its index or key. +If there are multiple minimal elements, then the first one will be returned. +`NaN` is treated as less than all other values except `missing`. + +# Examples + +```jldoctest julia> findmin([8, 0.1, -9, pi]) (-9.0, 3) @@ -854,27 +867,17 @@ julia> findmin([1, 7, 7, 6]) julia> findmin([1, 7, 7, NaN]) (NaN, 4) ``` - """ -findmin(f, domain) = mapfoldl(x -> (f(x), x), _rf_findmin, domain) -_rf_findmin((fm, m), (fx, x)) = isgreater(fm, fx) ? (fx, x) : (fm, m) - -findmin(a) = _findmin(a, :) +findmin(itr) = _findmin(itr, :) _findmin(a, ::Colon) = mapfoldl( ((k, v),) -> (v, k), _rf_findmin, pairs(a) ) """ argmax(f, domain) - argmax(f) Return a value `x` in the domain of `f` for which `f(x)` is maximised. If there are multiple maximal values for `f(x)` then the first one will be found. -When `domain` is provided it may be any iterable and must not be empty. - -When `domain` is omitted, `f` must have an implicit domain. In particular, if -`f` is an indexable collection, it is interpreted as a function mapping keys -(domain) to values (codomain), i.e. `argmax(itr)` returns the index of the -maximal element in `itr`. +`domain` must be a non-empty iterable. Values are compared with `isless`. @@ -885,7 +888,22 @@ julia> argmax(abs, -10:5) julia> argmax(cos, 0:π/2:2π) 0.0 +``` +""" +argmax(f, domain) = findmax(f, domain)[2] +""" + argmax(itr) + +Return the index or key of the maximal element in a collection. +If there are multiple maximal elements, then the first one will be returned. + +The collection must not be empty. + +Values are compared with `isless`. + +# Examples +```jldoctest julia> argmax([8, 0.1, -9, pi]) 1 @@ -896,22 +914,15 @@ julia> argmax([1, 7, 7, NaN]) 4 ``` """ -argmax(f, domain) = findmax(f, domain)[2] -argmax(f) = findmax(f)[2] +argmax(itr) = findmax(itr)[2] """ argmin(f, domain) - argmin(f) Return a value `x` in the domain of `f` for which `f(x)` is minimised. If there are multiple minimal values for `f(x)` then the first one will be found. -When `domain` is provided it may be any iterable and must not be empty. - -When `domain` is omitted, `f` must have an implicit domain. In particular, if -`f` is an indexable collection, it is interpreted as a function mapping keys -(domain) to values (codomain), i.e. `argmin(itr)` returns the index of the -minimal element in `itr`. +`domain` must be a non-empty iterable. `NaN` is treated as less than all other values except `missing`. @@ -926,6 +937,22 @@ julia> argmin(x -> -x^3 + x^2 - 10, -5:5) julia> argmin(acos, 0:0.1:1) 1.0 +``` +""" +argmin(f, domain) = findmin(f, domain)[2] + +""" + argmin(itr) + +Return the index or key of the minimal element in a collection. +If there are multiple minimal elements, then the first one will be returned. + +The collection must not be empty. + +`NaN` is treated as less than all other values except `missing`. + +# Examples +```jldoctest julia> argmin([8, 0.1, -9, pi]) 3 @@ -936,8 +963,7 @@ julia> argmin([7, 1, 1, NaN]) 4 ``` """ -argmin(f, domain) = findmin(f, domain)[2] -argmin(f) = findmin(f)[2] +argmin(itr) = findmin(itr)[2] ## all & any From 925a365f02518b1fccc64da9991ba548e6580959 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Thu, 17 Dec 2020 22:31:09 +0000 Subject: [PATCH 26/29] Add isunorded docstring and compat admonitions --- base/operators.jl | 9 +++++++++ base/reduce.jl | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/base/operators.jl b/base/operators.jl index 992a5e4c9b52e..be867e93fc65b 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -213,6 +213,15 @@ largest values and `isgreater` defines a descending total order with `NaN` and This is unexported. Types should not usually implement this function. Instead, implement `isless`. """ isgreater(x, y) = isunordered(x) || isunordered(y) ? isless(x, y) : isless(y, x) + +""" + isunordered(x) + +Return true if `x` is a value that is not normally orderable, such as `NaN` or `missing`. + +!!! compat "Julia 1.7" + This method requires Julia 1.7 or later. +""" isunordered(x) = false isunordered(x::AbstractFloat) = isnan(x) isunordered(x::Missing) = true diff --git a/base/reduce.jl b/base/reduce.jl index 1dbb1f2d54e9d..a3c8099b979a5 100644 --- a/base/reduce.jl +++ b/base/reduce.jl @@ -775,6 +775,9 @@ are multiple maximal points, then the first one will be returned. Values are compared with `isless`. +!!! compat "Julia 1.7" + This method requires Julia 1.7 or later. + # Examples ```jldoctest @@ -828,6 +831,9 @@ are multiple minimal points, then the first one will be returned. `NaN` is treated as less than all other values except `missing`. +!!! compat "Julia 1.7" + This method requires Julia 1.7 or later. + # Examples ```jldoctest @@ -881,6 +887,9 @@ If there are multiple maximal values for `f(x)` then the first one will be found Values are compared with `isless`. +!!! compat "Julia 1.7" + This method requires Julia 1.7 or later. + # Examples ```jldoctest julia> argmax(abs, -10:5) @@ -926,6 +935,9 @@ If there are multiple minimal values for `f(x)` then the first one will be found `NaN` is treated as less than all other values except `missing`. +!!! compat "Julia 1.7" + This method requires Julia 1.7 or later. + # Examples ```jldoctest julia> argmin(sign, -10:5) From 5e93247b2043ce62bc2fc1edb6cca190648b1a1e Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Thu, 17 Dec 2020 22:32:50 +0000 Subject: [PATCH 27/29] Add NEWS entry for isunordered --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 862dcb8bdddfc..77bb5eacf00ed 100644 --- a/NEWS.md +++ b/NEWS.md @@ -29,6 +29,7 @@ New library functions --------------------- * Two argument methods `findmax(f, domain)`, `argmax(f, domain)` and the corresponding `min` versions ([#27613]). +* `isunordered(x)` returns true if `x` is value that is normally unordered, such as `NaN` or `missing`. New library features -------------------- From b2fe843f13de05a065e6645b05995306c362dc9f Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Thu, 17 Dec 2020 22:34:38 +0000 Subject: [PATCH 28/29] Correct compat admonition for isunordered --- base/operators.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/operators.jl b/base/operators.jl index be867e93fc65b..a35c44276a2e4 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -220,7 +220,7 @@ isgreater(x, y) = isunordered(x) || isunordered(y) ? isless(x, y) : isless(y, x) Return true if `x` is a value that is not normally orderable, such as `NaN` or `missing`. !!! compat "Julia 1.7" - This method requires Julia 1.7 or later. + This function requires Julia 1.7 or later. """ isunordered(x) = false isunordered(x::AbstractFloat) = isnan(x) From 623281000f1ce7a454d2c589442ba1b881350a59 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Thu, 17 Dec 2020 23:39:45 +0000 Subject: [PATCH 29/29] Fix minimum(A; dims) when eltype(A) doesn't define typemin, but does accept Inf This used to work, so it would be a regression to break it. --- base/reducedim.jl | 8 ++++++-- test/reducedim.jl | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/base/reducedim.jl b/base/reducedim.jl index ce294ec473a54..85807851cd23d 100644 --- a/base/reducedim.jl +++ b/base/reducedim.jl @@ -125,7 +125,7 @@ function _reducedim_init(f, op, fv, fop, A, region) end # initialization when computing minima and maxima requires a little care -for (f1, f2, typeextreme) in ((:min, :max, :typemax), (:max, :min, :typemin)) +for (f1, f2, initval, typeextreme) in ((:min, :max, :Inf, :typemax), (:max, :min, :(-Inf), :typemin)) @eval function reducedim_init(f, op::typeof($f1), A::AbstractArray, region) # First compute the reduce indices. This will throw an ArgumentError # if any region is invalid @@ -148,7 +148,11 @@ for (f1, f2, typeextreme) in ((:min, :max, :typemax), (:max, :min, :typemin)) Tr = v0 isa T ? T : typeof(v0) # but NaNs and missing need to be avoided as initial values - if isunordered(v0) + if (v0 == v0) === false + # v0 is NaN + v0 = $initval + elseif isunordered(v0) + # v0 is missing or a third-party unordered value Tnm = nonmissingtype(Tr) # TODO: Some types, like BigInt, don't support typemin/typemax. # So a Matrix{Union{BigInt, Missing}} can still error here. diff --git a/test/reducedim.jl b/test/reducedim.jl index 8fd48ac236a4d..cc07cfff1dad3 100644 --- a/test/reducedim.jl +++ b/test/reducedim.jl @@ -195,6 +195,7 @@ end end end + ## findmin/findmax/minimum/maximum A = [1.0 5.0 6.0; @@ -239,6 +240,19 @@ end end end +@testset "reducedim_init min/max unorderable handling" begin + x = Any[1.0, NaN] + y = [1, missing] + for (v, rval1, rval2) in [(x, [NaN], x), + (y, [missing], y), + (Any[1. NaN; 1. 1.], Any[1. NaN], Any[NaN, 1.])] + for f in (minimum, maximum) + @test all(f(v, dims=1) .=== rval1) + @test all(f(v, dims=2) .=== rval2) + end + end +end + #issue #23209 A = [1.0 3.0 6.0;