From b055421cbe0fe7b155ec5230849748e98cb188ae Mon Sep 17 00:00:00 2001 From: Curtis Vogt Date: Fri, 12 Jun 2020 14:33:19 -0500 Subject: [PATCH] Disallow support for non-bounded AnchoredInterval https://github.com/invenia/Intervals.jl/pull/104#issuecomment-648245619 --- src/Intervals.jl | 5 -- src/anchoredinterval.jl | 112 +++++++++------------------------------ src/interval.jl | 5 +- test/anchoredinterval.jl | 89 ++++++------------------------- test/comparisons.jl | 10 +--- 5 files changed, 49 insertions(+), 172 deletions(-) diff --git a/src/Intervals.jl b/src/Intervals.jl index bf2c3d5a..b0f39244 100644 --- a/src/Intervals.jl +++ b/src/Intervals.jl @@ -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") diff --git a/src/anchoredinterval.jl b/src/anchoredinterval.jl index c2294867..2af9b192 100644 --- a/src/anchoredinterval.jl +++ b/src/anchoredinterval.jl @@ -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 @@ -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) @@ -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 @@ -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) """ @@ -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 ##### @@ -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 ##### @@ -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 ##### diff --git a/src/interval.jl b/src/interval.jl index ab854fe9..35ae4dff 100644 --- a/src/interval.jl +++ b/src/interval.jl @@ -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 diff --git a/test/anchoredinterval.jl b/test/anchoredinterval.jl index bc39e505..9a32fb2a 100644 --- a/test/anchoredinterval.jl +++ b/test/anchoredinterval.jl @@ -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) @@ -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 @@ -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 @@ -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 @@ -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)}) == @@ -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 diff --git a/test/comparisons.jl b/test/comparisons.jl index 28caef8f..c2231c3f 100644 --- a/test/comparisons.jl +++ b/test/comparisons.jl @@ -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)