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 14 commits
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)
60 changes: 60 additions & 0 deletions src/interval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,65 @@ 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

function Base.minimum(interval::AbstractInterval{T,L,R}; increment=nothing) where {T,L,R}
return L === Unbounded ? typemin(T) : first(interval)
end

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) && 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
throw(BoundsError(interval, min_val + increment))
end

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

function Base.minimum(interval::AbstractInterval{T,Open,R}; increment=nothing) where {T<:AbstractFloat,R}
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
nextfloat(min_val)
else
min_val + increment
end
next_val ∈ interval && return next_val
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) && throw(BoundsError(interval, 0))
max_val = last(interval)
next_val = if !isfinite(max_val) || increment === nothing
prevfloat(max_val)
else
max_val - increment
end
next_val ∈ interval && return next_val
throw(BoundsError(interval, next_val))
end

##### CONVERSION #####

# Allows an interval to be converted to a scalar when the set contained by the interval only
Expand All @@ -201,6 +260,7 @@ end

##### DISPLAY #####


function Base.show(io::IO, interval::Interval{T,L,R}) where {T,L,R}
if get(io, :compact, false)
print(io, interval)
Expand Down
15 changes: 15 additions & 0 deletions test/anchoredinterval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded

@test first(interval) == DateTime(2016, 8, 11, 1, 45)
@test last(interval) == dt
@test minimum(interval) == first(interval)
@test maximum(interval) == last(interval)
@test bounds_types(interval) == (Closed, Closed)
@test span(interval) == -P

Expand All @@ -175,6 +177,9 @@ 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 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 @@ -187,6 +192,8 @@ using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded
interval = AnchoredInterval{Day(1)}(startpoint)
@test first(interval) == startpoint
@test last(interval) == ZonedDateTime(2018, 3, 12, tz"America/Winnipeg")
@test minimum(interval) == startpoint
@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 @@ -197,24 +204,32 @@ using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded
interval = AnchoredInterval{Day(1)}(startpoint)
@test first(interval) == startpoint
@test last(interval) == ZonedDateTime(2018, 11, 5, tz"America/Winnipeg")
@test minimum(interval) == startpoint
@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 last(interval) == endpoint
@test_throws NonExistentTimeError minimum(interval, increment=Hour(1))
@test maximum(interval) == endpoint
@test span(interval) == Day(1)

# Non-period AnchoredIntervals
interval = AnchoredInterval{-10}(10)
@test first(interval) == 0
@test last(interval) == 10
@test minimum(interval) == 1
@test maximum(interval) == 10
@test bounds_types(interval) == (Open, Closed)
@test span(interval) == 10

interval = AnchoredInterval{25}('a')
@test first(interval) == 'a'
@test last(interval) == 'z'
@test minimum(interval) == 'a'
@test maximum(interval, increment=1) == 'y'
@test bounds_types(interval) == (Closed, Open)
@test span(interval) == 25
end
Expand Down
151 changes: 150 additions & 1 deletion test/interval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,6 @@
for (a, b, _) in test_values
for (L, R) in BOUND_PERMUTATIONS
interval = Interval{L, R}(a, b)

@test first(interval) == a
@test last(interval) == b
@test span(interval) == b - a
Expand All @@ -144,6 +143,156 @@
@test span(interval) == Hour(3)
end

@testset "maximum/minimum" begin
# Helper functions that manage the value we should be expecting from min and max.
function _min_val_helper(interval, a, unit)
t = eltype(interval)
# If the interal is empty, min is nothing
isempty(interval) && return nothing

# If a is in the interval, it is closed/unbounded and min is the first value.
# If a is nothing then it is unbounded and min is typemin(T)
a === nothing && return typemin(t)
a ∈ interval && return first(interval)

# From this point on, b ∉ interval so the bound is Open
# Also, if a is infinite we return typemin
# If it's an abstractfloat, we can't return just typemin since typemin IS Inf and
# since the bound is open at this point, Inf ∉ interval So we return the one after INF
!isfinite(a) && t <: AbstractFloat && return nextfloat(a)
!isfinite(a) && return typemin(t)

f = first(interval)
nv = if t <: AbstractFloat && unit === nothing
nextfloat(f)
else
f + unit
end

nv ∈ interval && return nv

# If we get to this point, the min/max functions throw a DomainError
# Since we want our tests to be predictable, we will not throw an error in this helper.
end

function _max_val_helper(interval, b, unit)
t = eltype(interval)
# If the interal is empty, min is nothing
isempty(interval) && return nothing

# If a is in the interval, it is closed/unbounded and min is the first value.
# If a is nothing then it is unbounded and min is typemin(T)
b === nothing && return typemax(t)
b ∈ interval && return last(interval)

# From this point on, b ∉ interval so the bound is Open
# Also, if a is infinite we return typemin
# If it's an abstractfloat, we can't return just typemin since typemin IS Inf and
# since the bound is open at this point, Inf ∉ interval So we return the one after INF
!isfinite(b) && t <: AbstractFloat && return prevfloat(b)
!isfinite(b) && return typemax(t)

l = last(interval)
nv = if t <: AbstractFloat && unit === nothing
prevfloat(l)
else
l - unit
end

nv ∈ interval && return nv

# If we get to this point, the min/max functions throw a DomainError
# Since we want our tests to be predictable, we will not throw an error in this helper.
end
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As theses helpers pretty much just re-implement minimum/maximum there aren't that helpful with testing. I think we can proceed with this but something to keep in mind for future work.

Copy link
Contributor Author

@Arvind-Maan Arvind-Maan Sep 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, in hindsight it would've been best to separate the tests per edge case and avoid having to do this. I constricted myself by trying to stay consistent with the first/last testing scheme and the test_values that were already in place.

@testset "bounded intervals" begin
bounded_test_vals = [
Arvind-Maan marked this conversation as resolved.
Show resolved Hide resolved
(-10, 1000, 1),
(0.0, 1, 0.01), # Use different types to test promotion
('a', 'z', 1),
(Date(2013, 2, 13), Date(2013, 3, 13), Day(1)),
(DateTime(2016, 8, 11, 0, 30), DateTime(2016, 8, 11, 1), Millisecond(1)),

# Infinite endpoints
(-Inf, 10, 1),
(10, Inf, 1),
(-Inf, Inf, 1),
(-Inf, 1.0, 0.01),
(0.0, Inf, 0.01),

# test adding eps()
(-10.0, 10.0, nothing),

#test inf with nextfloat and prevfloat
(-Inf, Inf, nothing),

('c', 'x', 2),
(Date(2004, 2, 13), Date(2020, 3, 13), Day(1)),
]
for (a, b, unit) in bounded_test_vals
for (L, R) in BOUND_PERMUTATIONS
interval = Interval{L, R}(a, b)

mi = _min_val_helper(interval, a, unit)
ma = _max_val_helper(interval, b, unit)

@test minimum(interval; increment=unit) == mi
@test maximum(interval; increment=unit) == ma
end
end
end

@testset "unbounded intervals" begin
unbounded_test_values = [
# one side unbounded with different types
(Interval{Open,Unbounded}(-10, nothing), 1),
(Interval{Unbounded,Closed}(nothing, 1.0), 0.01),
(Interval{Unbounded,Open}(nothing, 'z'), 1),
(Interval{Closed,Unbounded}(Date(2013, 2, 13), nothing), Day(1)),
(Interval{Open,Unbounded}(DateTime(2016, 8, 11, 0, 30), nothing), Millisecond(1)),
# both sides unbounded different types
(Interval{Int}(nothing, nothing), 1),
(Interval{Float64}(nothing, nothing), 0.01),
(Interval{Char}(nothing , nothing), 1),
(Interval{Day}(nothing, nothing), Day(1)),
(Interval{DateTime}(nothing, nothing), Millisecond(1)),
# test adding eps() with unbounded
(Interval{Open,Unbounded}(-10.0, nothing), nothing),
(Interval{Unbounded,Open}(nothing, 10.0), nothing),
# test infinity
(Interval{Open,Unbounded}(-Inf, nothing), nothing),
(Interval{Unbounded,Open}(nothing, Inf), nothing),
]
for (interval, unit) in unbounded_test_values
a, b = first(interval), last(interval)

mi = _min_val_helper(interval, a, unit)
ma = _max_val_helper(interval, b, unit)

@test minimum(interval; increment=unit) == mi
@test maximum(interval; increment=unit) == ma
@test_throws DomainError span(interval)

end
end
@testset "bounds errors in min/max" begin
error_test_vals = [
# 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 BoundsError minimum(interval; increment=unit)
@test_throws BoundsError maximum(interval; increment=unit)
end
end
end
@testset "display" begin
interval = Interval{Open, Open}(1, 2)
@test string(interval) == "(1 .. 2)"
Expand Down