Skip to content

Commit

Permalink
Add 2-arg versions of findmax/min, argmax/min (#35316)
Browse files Browse the repository at this point in the history
Defines a descending total order, `isgreater` (not exported), where unordered values like NaNs and missing are last. This makes defining min, argmin, etc, simpler and more consistent. Also adds 2-arg versions of findmax/min, argmax/min. Defines and exports the `isunordered` predicate for testing whether a value is unordered like NaN and missing. Fixes #27613. Related: #27639, #27612, #34674.

Thanks to @tkf, @StefanKarpinski and @drewrobson for their assistance with this PR.

Co-authored-by: Jameson Nash <[email protected]>
Co-authored-by: Takafumi Arakaki <[email protected]>
  • Loading branch information
3 people committed Dec 18, 2020
1 parent a88c435 commit 76952a8
Show file tree
Hide file tree
Showing 9 changed files with 392 additions and 153 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ Build system changes
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
--------------------
Expand Down
134 changes: 0 additions & 134 deletions base/array.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2205,140 +2205,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)
Expand Down
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,7 @@ export
isequal,
ismutable,
isless,
isunordered,
ifelse,
objectid,
sizeof,
Expand Down
59 changes: 58 additions & 1 deletion base/operators.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -168,6 +168,63 @@ 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)
Not the inverse of `isless`! Test whether `x` is greater than `y`, according to
a fixed total order compatible with `min`.
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> Base.isgreater(1, NaN) # 1 is greater than NaN
true
julia> Base.isgreater((1,), (NaN,)) # But (1,) is not greater than (NaN,)
false
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=Base.isgreater)
4-element Vector{Tuple{Float64}}:
(NaN,)
(3.0,)
(2.0,)
(1.0,)
```
# Implementation
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 function requires Julia 1.7 or later.
"""
isunordered(x) = false
isunordered(x::AbstractFloat) = isnan(x)
isunordered(x::Missing) = true

function ==(T::Type, S::Type)
@_pure_meta
Expand Down
Loading

0 comments on commit 76952a8

Please sign in to comment.