Skip to content

Commit

Permalink
Disallow support for non-bounded AnchoredInterval
Browse files Browse the repository at this point in the history
  • Loading branch information
omus committed Jun 23, 2020
1 parent c0f946a commit b055421
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 172 deletions.
5 changes: 0 additions & 5 deletions src/Intervals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,6 @@ Base.eltype(::AbstractInterval{T}) where {T} = T
Base.broadcastable(x::AbstractInterval) = Ref(x)
bounds_types(x::AbstractInterval{T,L,R}) where {T,L,R} = (L, R)

const SPAN_NON_BOUNDED_EXCEPTION = DomainError(
"unbounded endpoint(s)",
"Unable to determine the span of an non-bounded interval",
)

include("endpoint.jl")
include("interval.jl")
include("anchoredinterval.jl")
Expand Down
112 changes: 26 additions & 86 deletions src/anchoredinterval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -58,27 +58,10 @@ AnchoredInterval{5 minutes,DateTime,Closed,Closed}(2016-08-11T12:30:00)
See also: [`Interval`](@ref), [`HE`](@ref), [`HB`](@ref)
"""
struct AnchoredInterval{P, T, L <: Bound, R <: Bound} <: AbstractInterval{T,L,R}
struct AnchoredInterval{P, T, L <: Bounded, R <: Bounded} <: AbstractInterval{T,L,R}
anchor::T

function AnchoredInterval{P,T,L,R}(anchor::T) where {P, T, L <: Bound, R <: Bound}
if sign(P) < 0 && R === Unbounded
throw(ArgumentError(
"Unable to represent a right-unbounded interval as `AnchoredInterval` " *
"when anchor defines the right bound"
))
elseif sign(P) > 0 && L === Unbounded
throw(ArgumentError(
"Unable to represent a left-unbounded interval as `AnchoredInterval` " *
"when anchor defines the left bound"
))
elseif sign(P) == 0 && L === Unbounded && R === Unbounded
throw(ArgumentError(
"Unable to represent a non-bounded interval as `AnchoredInterval` " *
"when anchor defines both the left and right bound"
))
end

function AnchoredInterval{P,T,L,R}(anchor::T) where {P, T, L <: Bounded, R <: Bounded}
# A valid interval requires that neither endpoints or the span are nan. Typically,
# we use `left <= right` to ensure a valid interval but for `AnchoredInterval`s
# computing the other endpoint requires `anchor + P` which may fail with certain
Expand All @@ -105,16 +88,6 @@ struct AnchoredInterval{P, T, L <: Bound, R <: Bound} <: AbstractInterval{T,L,R}
end
end

# Using `nothing` as the anchor makes it impossible to compute the other endpoint if it is
# anything other than `nothing`. Since an `AnchoredInterval` cannot represent the interval
# `[-Inf,Inf]` (there is no way to compute the other endpoint using an `Inf` endpoint
# with an `Inf` span) we'll also disallow support for a unbounded anchored interval.
function AnchoredInterval{P,T,L,R}(anchor::Nothing) where {P, T <: Nothing, L <: Bound, R <: Bound}
throw(ArgumentError(
"Unable to represent `AnchoredInterval` with a unbounded anchor endpoint"
))
end

_isfinite(x) = iszero(x - x)
_isfinite(x::Real) = Base.isfinite(x)

Expand Down Expand Up @@ -142,7 +115,7 @@ AnchoredInterval{P}(anchor::T) where {P,T} = AnchoredInterval{P,T}(anchor)
A type alias for `AnchoredInterval{Hour(-1), T}` which is used to denote a 1-hour period of
time which ends at a time instant (of type `T`).
"""
const HourEnding{T,L,R} = AnchoredInterval{Hour(-1), T, L, R} where {T, L <: Bound, R <: Bound}
const HourEnding{T,L,R} = AnchoredInterval{Hour(-1), T, L, R} where {T, L <: Bounded, R <: Bounded}
HourEnding(anchor::T) where T = HourEnding{T}(anchor)

# Note: Ideally we would define the restriction `T <: TimeType` but doing so interferes with
Expand All @@ -153,7 +126,7 @@ HourEnding(anchor::T) where T = HourEnding{T}(anchor)
A type alias for `AnchoredInterval{Hour(1), T}` which is used to denote a 1-hour period of
time which begins at a time instant (of type `T`).
"""
const HourBeginning{T,L,R} = AnchoredInterval{Hour(1), T, L, R} where {T, L <: Bound, R <: Bound}
const HourBeginning{T,L,R} = AnchoredInterval{Hour(1), T, L, R} where {T, L <: Bounded, R <: Bounded}
HourBeginning(anchor::T) where T = HourBeginning{T}(anchor)

"""
Expand Down Expand Up @@ -182,31 +155,16 @@ end
# can get unexpected behaviour if adding the span to the anchor endpoint produces a value
# that is no longer comparable (e.g., `NaN`).

function Base.first(interval::AnchoredInterval{P,T,L,R}) where {P,T,L,R}
return if L !== Unbounded
P < zero(P) ? (interval.anchor + P) : (interval.anchor)
else
nothing
end
function Base.first(interval::AnchoredInterval{P}) where P
P < zero(P) ? (interval.anchor + P) : (interval.anchor)
end

function Base.last(interval::AnchoredInterval{P,T,L,R}) where {P,T,L,R}
return if R !== Unbounded
P < zero(P) ? (interval.anchor) : (interval.anchor + P)
else
nothing
end
function Base.last(interval::AnchoredInterval{P}) where P
P < zero(P) ? (interval.anchor) : (interval.anchor + P)
end

anchor(interval::AnchoredInterval) = interval.anchor

function span(interval::AnchoredInterval{P}) where P
if !isunbounded(interval)
abs(P)
else
throw(SPAN_NON_BOUNDED_EXCEPTION)
end
end
span(interval::AnchoredInterval{P}) where P = abs(P)

##### CONVERSION #####

Expand Down Expand Up @@ -235,25 +193,18 @@ function Base.convert(::Type{AnchoredInterval{P}}, interval::Interval{T}) where
end
=#

_span_fallback(::Type{T}) where T <: TimeType = eps(T)
_span_fallback(::Type{T}) where T = one(T)

function Base.convert(::Type{AnchoredInterval{Ending}}, interval::Interval{T}) where {T}
left, right = LeftEndpoint(interval), RightEndpoint(interval)
if isunbounded(right)
throw(ArgumentError("Unable to represent a right-unbounded interval using a `AnchoredInterval{Ending}`"))
function Base.convert(::Type{AnchoredInterval{Ending}}, interval::Interval{T,L,R}) where {T,L,R}
if !isbounded(interval)
throw(ArgumentError("Unable to represent a non-bounded interval using a `AnchoredInterval`"))
end
sp = isbounded(left) ? span(interval) : _span_fallback(T)
return AnchoredInterval{-sp, T, bound_type(left), bound_type(right)}(last(interval))
AnchoredInterval{-span(interval), T, L, R}(last(interval))
end

function Base.convert(::Type{AnchoredInterval{Beginning}}, interval::Interval{T}) where {T}
left, right = LeftEndpoint(interval), RightEndpoint(interval)
if isunbounded(left)
throw(ArgumentError("Unable to represent a left-unbounded interval using a `AnchoredInterval{Beginning}`"))
function Base.convert(::Type{AnchoredInterval{Beginning}}, interval::Interval{T,L,R}) where {T,L,R}
if !isbounded(interval)
throw(ArgumentError("Unable to represent a non-bounded interval using a `AnchoredInterval`"))
end
sp = isbounded(right) ? span(interval) : _span_fallback(T)
return AnchoredInterval{sp, T, bound_type(left), bound_type(right)}(first(interval))
AnchoredInterval{span(interval), T, L, R}(first(interval))
end

##### DISPLAY #####
Expand Down Expand Up @@ -335,29 +286,18 @@ end
# When intersecting two `AnchoredInterval`s attempt to return an `AnchoredInterval`
function Base.intersect(a::AnchoredInterval{P,T}, b::AnchoredInterval{Q,T}) where {P,Q,T}
interval = invoke(intersect, Tuple{AbstractInterval{T}, AbstractInterval{T}}, a, b)
anchor_side = P zero(P) ? :right : :left

# The endpoint which will be represented by the anchor must be bounded
if (
anchor_side === :left && isbounded(LeftEndpoint(interval)) ||
anchor_side === :right && isbounded(RightEndpoint(interval))
)
sp = isbounded(interval) ? span(interval) : _span_fallback(T)
sp = isa(P, Period) ? canonicalize(typeof(P), sp) : sp

if anchor_side === :right
anchor = last(interval)
new_P = -sp
else
anchor = first(interval)
new_P = sp
end

L, R = bounds_types(interval)
return AnchoredInterval{new_P, T, L, R}(anchor)
sp = isa(P, Period) ? canonicalize(typeof(P), span(interval)) : span(interval)
if P zero(P)
anchor = last(interval)
new_P = -sp
else
return interval
anchor = first(interval)
new_P = sp
end

L, R = bounds_types(interval)
return AnchoredInterval{new_P, T, L, R}(anchor)
end

##### UTILITIES #####
Expand Down
5 changes: 4 additions & 1 deletion src/interval.jl
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,10 @@ function span(interval::Interval)
if isbounded(interval)
interval.last - interval.first
else
throw(SPAN_NON_BOUNDED_EXCEPTION)
throw(DomainError(
"unbounded endpoint(s)",
"Unable to determine the span of an non-bounded interval",
))
end
end

Expand Down
89 changes: 17 additions & 72 deletions test/anchoredinterval.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Intervals: Beginning, Ending, canonicalize, isunbounded
using Intervals: Bounded, Ending, Beginning, canonicalize, isunbounded

@testset "AnchoredInterval" begin
dt = DateTime(2016, 8, 11, 2)
Expand Down Expand Up @@ -36,18 +36,6 @@ using Intervals: Beginning, Ending, canonicalize, isunbounded

@testset "zero-span" begin
@test AnchoredInterval{0}(10) == 10 .. 10
@test AnchoredInterval{0,Unbounded,Closed}(10) == nothing .. 10
@test AnchoredInterval{0,Closed,Unbounded}(10) == 10 .. nothing

# The anchor represents either endpoint. If the constructor below was allowed this
# would be the same as allowing `Interval{Unbounded,Unbounded}(10, 10)`.
@test_throws ArgumentError AnchoredInterval{0,Unbounded,Unbounded}(10)

# Ignore positive/negative zero for span
@test AnchoredInterval{-0.0,Unbounded,Closed}(10) == nothing .. 10
@test AnchoredInterval{+0.0,Closed,Unbounded}(10) == 10 .. nothing
@test AnchoredInterval{+0.0,Unbounded,Closed}(10) == nothing .. 10
@test AnchoredInterval{-0.0,Closed,Unbounded}(10) == 10 .. nothing

@test AnchoredInterval{+0.0}(0.0) == 0 .. 0
@test AnchoredInterval{-0.0}(0.0) == 0 .. 0
Expand Down Expand Up @@ -90,43 +78,19 @@ using Intervals: Beginning, Ending, canonicalize, isunbounded
@testset "non-bounded" begin
x = 1 # Non-zero value representing any positive value

interval = 0 .. nothing
@test AnchoredInterval{+x,Closed,Unbounded}(0.0) == interval
@test_throws ArgumentError AnchoredInterval{-x,Closed,Unbounded}(nothing)
@test AnchoredInterval{0,Closed,Unbounded}(0.0) == interval

interval = nothing .. 0
@test_throws ArgumentError convert(AnchoredInterval{Beginning}, interval)
@test AnchoredInterval{-x,Unbounded,Closed}(0.0) == interval
@test AnchoredInterval{0,Unbounded,Closed}(0.0) == interval

interval = -Inf .. nothing
@test AnchoredInterval{+x,Closed,Unbounded}(-Inf) == interval
@test_throws ArgumentError AnchoredInterval{-x,Closed,Unbounded}(nothing)
@test AnchoredInterval{0,Closed,Unbounded}(-Inf) == interval

interval = nothing .. Inf
@test_throws ArgumentError AnchoredInterval{+x,Unbounded,Closed}(nothing)
@test AnchoredInterval{-x,Unbounded,Closed}(Inf) == interval
@test AnchoredInterval{0,Unbounded,Closed}(Inf) == interval

interval = nothing .. nothing
@test_throws ArgumentError AnchoredInterval{+x,Unbounded,Unbounded}(nothing)
@test_throws ArgumentError AnchoredInterval{-x,Unbounded,Unbounded}(nothing)
@test_throws ArgumentError AnchoredInterval{0,Unbounded,Unbounded}(nothing)

# Other invalid non-bounded intervals
@test_throws ArgumentError AnchoredInterval{+1,Unbounded,Unbounded}(0)
@test_throws ArgumentError AnchoredInterval{-1,Unbounded,Unbounded}(0)
@test_throws ArgumentError AnchoredInterval{0,Unbounded,Unbounded}(0)

@test_throws MethodError AnchoredInterval{+x,Int,Closed,Unbounded}(nothing)
@test_throws MethodError AnchoredInterval{-x,Int,Unbounded,Closed}(nothing)
@test_throws MethodError AnchoredInterval{0,Int,Unbounded,Unbounded}(nothing)

@test_throws ArgumentError AnchoredInterval{+x,Nothing,Closed,Unbounded}(nothing)
@test_throws ArgumentError AnchoredInterval{-x,Nothing,Unbounded,Closed}(nothing)
@test_throws ArgumentError AnchoredInterval{0,Nothing,Unbounded,Unbounded}(nothing)
# Unbounded AnchoredIntervals are disallowed as most types have no span value that
# actually represents the span of the interval
@test_throws TypeError AnchoredInterval{+x,Int,Closed,Unbounded}(0)
@test_throws TypeError AnchoredInterval{-x,Int,Unbounded,Closed}(0)
@test_throws TypeError AnchoredInterval{0,Int,Unbounded,Unbounded}(0)

@test_throws MethodError AnchoredInterval{+x,Int}(nothing)
@test_throws MethodError AnchoredInterval{-x,Int}(nothing)
@test_throws MethodError AnchoredInterval{0,Int}(nothing)

@test_throws MethodError AnchoredInterval{+x,Nothing}(nothing)
@test_throws MethodError AnchoredInterval{-x,Nothing}(nothing)
@test_throws MethodError AnchoredInterval{0,Nothing}(nothing)
end

@testset "hash" begin
Expand Down Expand Up @@ -165,11 +129,11 @@ using Intervals: Beginning, Ending, canonicalize, isunbounded
@test_throws ArgumentError convert(AnchoredInterval{Ending}, Interval(0, Inf))
@test convert(AnchoredInterval{Beginning}, Interval(0, Inf)) == AnchoredInterval{Inf,Float64,Closed,Closed}(0)

@test convert(AnchoredInterval{Ending}, Interval(nothing, 0)) == AnchoredInterval{-1,Int,Unbounded,Closed}(0)
@test_throws ArgumentError convert(AnchoredInterval{Ending}, Interval(nothing, 0))
@test_throws ArgumentError convert(AnchoredInterval{Beginning}, Interval(nothing, 0))

@test_throws ArgumentError convert(AnchoredInterval{Ending}, Interval(0, nothing))
@test convert(AnchoredInterval{Beginning}, Interval(0, nothing)) == AnchoredInterval{1,Int,Closed,Unbounded}(0)
@test_throws ArgumentError convert(AnchoredInterval{Beginning}, Interval(0, nothing))
end

@testset "eltype" begin
Expand Down Expand Up @@ -251,7 +215,7 @@ using Intervals: Beginning, Ending, canonicalize, isunbounded
# When dropping VERSION < v"1.2.0-DEV.223" (https://github.com/JuliaLang/julia/pull/30817)
# - `repr(Period(...))`can be converted to hardcode strings

where_lr = "where R<:$Bound where L<:$Bound"
where_lr = "where R<:$Bounded where L<:$Bounded"
where_tlr = "$where_lr where T"

@test sprint(show, AnchoredInterval{Hour(-1)}) ==
Expand Down Expand Up @@ -713,25 +677,6 @@ using Intervals: Beginning, Ending, canonicalize, isunbounded
# Non-period AnchoredIntervals
@test intersect(AnchoredInterval{-2}(3), AnchoredInterval{-2}(4)) ==
AnchoredInterval{-1}(3)

# Unbounded AnchoredIntervals
intersection = intersect(
AnchoredInterval{-2,Unbounded,Closed}(3),
AnchoredInterval{-2,Unbounded,Closed}(4),
)
@test intersection == AnchoredInterval{-1,Unbounded,Closed}(3)

intersection = intersect(
AnchoredInterval{2,Open,Unbounded}(3),
AnchoredInterval{2,Open,Unbounded}(4),
)
@test intersection == AnchoredInterval{1,Open,Unbounded}(4)

intersection = intersect(
AnchoredInterval{-2,Unbounded,Closed}(3),
AnchoredInterval{2,Closed,Unbounded}(4),
)
@test intersection == AnchoredInterval{0,Int,Open,Open}(0)
end

@testset "canonicalize" begin
Expand Down
10 changes: 2 additions & 8 deletions test/comparisons.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,11 @@ const INTERVAL_TYPES = [Interval, AnchoredInterval{Ending}, AnchoredInterval{Beg
viable_convert(::Type{Interval}, interval::AbstractInterval) = true

function viable_convert(::Type{AnchoredInterval{Beginning}}, interval::AbstractInterval)
return (
!isunbounded(LeftEndpoint(interval)) &&
(isfinite(first(interval)) || first(interval) == last(interval))
)
return isbounded(interval) && isfinite(first(interval))
end

function viable_convert(::Type{AnchoredInterval{Ending}}, interval::AbstractInterval)
return (
!isunbounded(RightEndpoint(interval)) &&
(isfinite(last(interval)) || first(interval) == last(interval))
)
return isbounded(interval) && isfinite(last(interval))
end

@testset "comparisons: $A vs. $B" for (A, B) in unique_paired_permutation(INTERVAL_TYPES)
Expand Down

0 comments on commit b055421

Please sign in to comment.