Skip to content

Commit

Permalink
Merge pull request #141 from invenia/am/bounded-first/last
Browse files Browse the repository at this point in the history
Create min/max functions that take bounds into account
  • Loading branch information
Arvind Maan committed Sep 23, 2020
2 parents a9d99c9 + 3b31ff4 commit 6e3e3a2
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 1 deletion.
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]) -> T
The minimum value in the interval contained within the `interval`.
If left-closed, returns `first(interval)`.
If left-open, returns `first(interval) + eps(first(interval))`
If left-unbounded, returns minimum value possible for type `T`.
A `BoundsError` is thrown for empty intervals or when the increment results in a minimum value
not-contained by the interval.
"""
minimum(::AbstractInterval; increment)

"""
maximum(interval::AbstractInterval{T}; increment::Any) -> T
The maximum value in the interval contained within the `interval`.
If right-closed, returns `last(interval)`.
If right-open, returns `first(interval) + eps(first(interval))`
If right-unbounded, returns maximum value possible for type `T`.
A `BoundsError` is thrown for empty intervals or when the increment results in a maximum value
not-contained by the interval.
"""
maximum(::AbstractInterval; increment)
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}
isempty(interval) && throw(BoundsError(interval, 0))
min_val = first(interval) + increment
# Since intervals can't have NaN, we can just use !isfinite to check if infinite
!isfinite(min_val) && return typemin(T)
min_val interval && return min_val
throw(BoundsError(interval, min_val))
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) - increment
# Since intervals can't have NaN, we can just use !isfinite to check if infinite
!isfinite(max_val) && return typemax(T)
max_val interval && return max_val
throw(BoundsError(interval, max_val))
end

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

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
1 change: 1 addition & 0 deletions src/isfinite.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
# `Char` and `Period` as well as other types.
isfinite(x) = iszero(x - x)
isfinite(x::Real) = Base.isfinite(x)
isfinite(x::Union{Type{T}, T}) where T<:TimeType = Base.isfinite(x)
14 changes: 14 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,8 @@ using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded

@test first(interval) == Date(2016, 8, 11)
@test last(interval) == Date(2016, 8, 12)
@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 +191,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 +203,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
136 changes: 135 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,141 @@
@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
!Intervals.isfinite(a) && t <: AbstractFloat && return nextfloat(a)
!Intervals.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
@testset "bounded intervals" begin
bounded_test_vals = [
#test nextfloat and prevfloat
(-10.0, 10.0, nothing),
(-Inf, Inf, nothing),

('c', 'x', 2),
(Date(2004, 2, 13), Date(2020, 3, 13), Day(1)),
]
for (a, b, unit) in append!(bounded_test_vals, test_values)
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

0 comments on commit 6e3e3a2

Please sign in to comment.