Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create min/max functions that take bounds into account #141

Merged
merged 17 commits into from
Sep 23, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions src/docstrings.jl
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,31 @@ Note using `!isbounded` is commonly used to determine if any end of the interval
unbounded.
"""
isbounded(::AbstractInterval)

"""
minimum(interval::AbstractInterval{T}; increment::Any) -> T
Arvind-Maan marked this conversation as resolved.
Show resolved Hide resolved

The minimum value in the interval.
Arvind-Maan marked this conversation as resolved.
Show resolved Hide resolved

If left bound is closed, returns `first(interval)`.
Arvind-Maan marked this conversation as resolved.
Show resolved Hide resolved
If left bound is unbounded, returns `typemin(T)`
Arvind-Maan marked this conversation as resolved.
Show resolved Hide resolved

On an empty interval or when the increment results in `first(interval) + increment ∉ interval`, returns BoundsError.
Arvind-Maan marked this conversation as resolved.
Show resolved Hide resolved

When T <: AbstractFloat and no increment is passed, defaults to returning `nextfloat(first(interval))`
Arvind-Maan marked this conversation as resolved.
Show resolved Hide resolved
"""
minimum(::AbstractInterval; increment::Any)

"""
maximum(interval::AbstractInterval{T}; increment::Any) -> T

The maximum value in the interval.

If right bound is closed, returns `last(interval)`.
If right bound is unbounded, returns `typemax(T)`

On an empty interval or when the increment results in `last(interval) - increment ∉ interval`, returns BoundsError.

When T <: AbstractFloat and no increment is passed, defaults to returning `prevfloat(last(interval))`
"""
maximum(::AbstractInterval; increment::Any)
61 changes: 27 additions & 34 deletions src/interval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -187,70 +187,63 @@ Base.isopen(interval::AbstractInterval{T,L,R}) where {T,L,R} = L === Open && R =
isunbounded(interval::AbstractInterval{T,L,R}) where {T,L,R} = L === Unbounded && R === Unbounded
isbounded(interval::AbstractInterval{T,L,R}) where {T,L,R} = L !== Unbounded && R !== Unbounded

# Minimum and maximum calls for Closed and Unbounded Intervals, just return first and last.
function Base.minimum(interval::AbstractInterval{T,L,R}; increment=nothing) where {T,L,R}
first(interval) === nothing && return typemin(T)
return first(interval)
return L === Unbounded ? typemin(T) : first(interval)
end

function Base.maximum(interval::AbstractInterval{T,L,R}; increment=nothing) where {T,L,R}
last(interval) === nothing && return typemax(T)
return last(interval)
end

# Minimum and maximum calls for open bounds and ambigous types, return open side +- increment
# Default increment val is eps(T)
function Base.minimum(interval::AbstractInterval{T,Open,R}; increment=eps(T)) where {T,R}
Arvind-Maan marked this conversation as resolved.
Show resolved Hide resolved
isempty(interval) && return nothing
isempty(interval) && throw(BoundsError(interval, 0))
min_val = first(interval)
# Since intervals can't have NaN, we can just use !isfinite to check if infinite
!isfinite(min_val) && return typemin(T)
min_val + increment ∈ interval && return min_val + increment
Arvind-Maan marked this conversation as resolved.
Show resolved Hide resolved
throw(DomainError(interval, "The increment is too large, $(min_val + increment) is not in the interval"))
end

function Base.maximum(interval::AbstractInterval{T,L,Open}; increment=eps(T)) where {T,L}
isempty(interval) && return nothing
max_val = last(interval)
!isfinite(max_val) && return typemax(T)
max_val - increment ∈ interval && return max_val - increment
throw(DomainError(interval, "The increment is too large, $(max_val - increment) is not in the interval"))
throw(BoundsError(interval, min_val + increment))
end

# Minimum and maximum calls for open bounds and Integer types, return open side +- increment
# Default increment val for Integers is one(T)
function Base.minimum(interval::AbstractInterval{T,Open,R}) where {T<:Integer,R}
return minimum(interval, increment=one(T))
end

function Base.maximum(interval::AbstractInterval{T,L,Open}) where {T<:Integer,L}
return maximum(interval, increment=one(T))
end

# Minimum and maximum calls for open bounds and Float types, return open side +- increment
# Default increment val for Floats is nothing, which returns nextfloat or prevfloat (i.e., +- eps())
function Base.minimum(interval::AbstractInterval{T,Open,R}; increment=nothing) where {T<:AbstractFloat,R}
isempty(interval) && return nothing
isempty(interval) && throw(BoundsError(interval, 0))
min_val = first(interval)
# Since intervals can't have NaN, we can just use !isfinite to check if infinite
next_val = if !isfinite(min_val) || increment === nothing || increment == eps(T)
next_val = if !isfinite(min_val) || increment === nothing
nextfloat(min_val)
else
min_val + increment
end
next_val ∈ interval && return next_val
throw(DomainError(interval, "The increment is too large, $next_val is not in the interval"))
throw(BoundsError(interval, next_val))
end

function Base.maximum(interval::AbstractInterval{T,L,R}; increment=nothing) where {T,L,R}
return R === Unbounded ? typemax(T) : last(interval)
end

function Base.maximum(interval::AbstractInterval{T,L,Open}; increment=eps(T)) where {T,L}
isempty(interval) && throw(BoundsError(interval, 0))
max_val = last(interval)
# Since intervals can't have NaN, we can just use !isfinite to check if infinite
!isfinite(max_val) && return typemax(T)
max_val - increment ∈ interval && return max_val - increment
throw(BoundsError(interval, max_val - increment))
end

function Base.maximum(interval::AbstractInterval{T,L,Open}) where {T<:Integer,L}
return maximum(interval, increment=one(T))
end

Arvind-Maan marked this conversation as resolved.
Show resolved Hide resolved
function Base.maximum(interval::AbstractInterval{T,L,Open}; increment=nothing) where {T<:AbstractFloat,L}
isempty(interval) && return nothing
isempty(interval) && throw(BoundsError(interval, 0))
max_val = last(interval)
next_val = if !isfinite(max_val) || increment === nothing || increment == eps(T)
next_val = if !isfinite(max_val) || increment === nothing
prevfloat(max_val)
else
max_val - increment
end
next_val ∈ interval && return next_val
throw(DomainError(interval, "The increment is too large, $next_val is not in the interval"))
throw(BoundsError(interval, next_val))
end

##### CONVERSION #####
Expand Down
10 changes: 5 additions & 5 deletions test/anchoredinterval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded
@test first(interval) == Date(2016, 8, 11)
@test last(interval) == Date(2016, 8, 12)
# throws domain error
Arvind-Maan marked this conversation as resolved.
Show resolved Hide resolved
@test_throws DomainError minimum(interval, increment=Day(1)) == first(interval) + Day(1)
@test_throws DomainError maximum(interval, increment=Day(1)) == last(interval) - Day(1)
@test_throws BoundsError minimum(interval, increment=Day(1))
@test_throws BoundsError maximum(interval, increment=Day(1))
@test bounds_types(interval) == (Open, Open)
@test span(interval) == P

Expand All @@ -193,7 +193,7 @@ using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded
@test first(interval) == startpoint
@test last(interval) == ZonedDateTime(2018, 3, 12, tz"America/Winnipeg")
@test minimum(interval) == startpoint
@test maximum(interval, increment=Hour(1)) == ZonedDateTime(2018, 3, 11, 23, tz"America/Winnipeg")
@test maximum(interval, increment=Hour(1)) == last(interval) - Hour(1)
@test span(interval) == Day(1)

endpoint = ZonedDateTime(2018, 11, 4, 2, tz"America/Winnipeg")
Expand All @@ -205,14 +205,14 @@ using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded
@test first(interval) == startpoint
@test last(interval) == ZonedDateTime(2018, 11, 5, tz"America/Winnipeg")
@test minimum(interval) == startpoint
@test maximum(interval, increment=Hour(1)) == ZonedDateTime(2018, 11, 4, 23, tz"America/Winnipeg")
@test maximum(interval, increment=Millisecond(1)) == last(interval) - Millisecond(1)
@test span(interval) == Day(1)

endpoint = ZonedDateTime(2020, 3, 9, 2, tz"America/Winnipeg")
interval = AnchoredInterval{Day(-1)}(endpoint)
@test_throws NonExistentTimeError first(interval)
@test_throws NonExistentTimeError minimum(interval, increment=Hour(1))
@test last(interval) == endpoint
@test_throws NonExistentTimeError minimum(interval, increment=Hour(1))
@test maximum(interval) == endpoint
@test span(interval) == Day(1)

Expand Down
30 changes: 11 additions & 19 deletions test/interval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
!isfinite(a) && return typemin(t)

f = first(interval)
nv = if t <: AbstractFloat && (unit === nothing || unit == eps(t))
nv = if t <: AbstractFloat && unit === nothing
nextfloat(f)
else
f + unit
Expand Down Expand Up @@ -193,7 +193,7 @@
!isfinite(b) && return typemax(t)

l = last(interval)
nv = if t <: AbstractFloat && (unit === nothing || unit == eps(t))
nv = if t <: AbstractFloat && unit === nothing
prevfloat(l)
else
l - unit
Expand Down Expand Up @@ -274,30 +274,22 @@

end
end
@testset "empty intervals in min/max" begin
error_test_vals = [
# Different types
(Interval{Open,Open}(-10, -10), 1),
(Interval{Open,Open}(0.0, 0.0), 60),
(Interval{Open,Open}(Date(2013, 2, 13), Date(2013, 2, 13)), Day(1)),
(Interval{Open,Open}(DateTime(2016, 8, 11, 0, 30), DateTime(2016, 8, 11, 0, 30)), Day(1)),
]
for (interval, unit) in error_test_vals
@test minimum(interval; increment=unit) === nothing
@test maximum(interval; increment=unit) === nothing
end
end
@testset "domain errors in min/max" begin
@testset "bounds errors in min/max" begin
error_test_vals = [
# Different types
# empty intervals
(Interval{Open,Open}(-10, -10), 1),
(Interval{Open,Open}(0.0, 0.0), 60),
(Interval{Open,Open}(Date(2013, 2, 13), Date(2013, 2, 13)), Day(1)),
(Interval{Open,Open}(DateTime(2016, 8, 11, 0, 30), DateTime(2016, 8, 11, 0, 30)), Day(1)),
# increment too large
(Interval{Open,Open}(-10, 15), 60),
(Interval{Open,Open}(0.0, 25), 60.0),
(Interval{Open,Open}(Date(2013, 2, 13), Date(2013, 2, 14)), Day(5)),
(Interval{Open,Open}(DateTime(2016, 8, 11, 0, 30), DateTime(2016, 8, 11, 5, 30)), Day(5)),
]
for (interval, unit) in error_test_vals
@test_throws DomainError minimum(interval; increment=unit)
@test_throws DomainError maximum(interval; increment=unit)
@test_throws BoundsError minimum(interval; increment=unit)
@test_throws BoundsError maximum(interval; increment=unit)
end
end
end
Expand Down