From a0983f154d1b60eaa53e7ca76405f61c985308f1 Mon Sep 17 00:00:00 2001 From: Sheehan Olver Date: Sat, 1 Sep 2018 20:34:10 +0100 Subject: [PATCH] Add open and mixed open/closed intervals (#26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Start open interval * using works * Fix type inferrence so old tests pass * Tests pass * Fix warning in 0.7 * Improve coverage * Add test for custom defined interval conversion * last few coverage lines * Last few lines, hopefully * broken test fixed * Remove dodgy convert routine * Add to README, add mixed type intersect (needs tests still) * • Add TypedEndpointsInterval, implement union, intersect, etc. specifically for this • AbstractInfiniteSet -> Domain • deprecate length in favour of duration * update Compat, remove median, update mean * export isclosed, simplify default convert to avoid ambiguities * address timholy's comments * update issubset, add conversion of numbers to intervals * issubset override dependent on VERSION --- README.md | 15 ++ REQUIRE | 2 +- src/IntervalSets.jl | 191 ++++++++++++++- src/closed.jl | 119 ---------- src/interval.jl | 236 +++++++++++++++++++ test/runtests.jl | 554 +++++++++++++++++++++++++++++++++++++++++--- 6 files changed, 951 insertions(+), 166 deletions(-) delete mode 100644 src/closed.jl create mode 100644 src/interval.jl diff --git a/README.md b/README.md index 4e58b23..72cfc0d 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,18 @@ julia> 1.5±1 0.5..2.5 ``` +Similarly, you can construct `OpenInterval`s and `Interval{:open,:closed}`s, and `Interval{:closed,:open}`: +```julia +julia> OpenInterval{Float64}(1,3) +1.0..3.0 (open) + +julia> OpenInterval(0.5..2.5) +0.5..2.5 (open) + +julia> Interval{:open,:closed}(1,3) +1..3 (open–closed) +``` + The `±` operator may be typed as `\pm` (using Julia's LaTeX syntax tab-completion). @@ -50,6 +62,9 @@ true julia> 0 ∈ 1.5±1 false +julia> 1 ∈ OpenInterval(0..1) +false + julia> intersect(1..5, 3..7) # can also use `a ∩ b`, where the symbol is \cap 3..5 diff --git a/REQUIRE b/REQUIRE index 5d7206f..ab2915c 100644 --- a/REQUIRE +++ b/REQUIRE @@ -1,2 +1,2 @@ julia 0.6 -Compat 0.49 +Compat 1.0 diff --git a/src/IntervalSets.jl b/src/IntervalSets.jl index 84a5124..9bf895f 100644 --- a/src/IntervalSets.jl +++ b/src/IntervalSets.jl @@ -6,27 +6,65 @@ using Base: @pure import Base: eltype, convert, show, in, length, isempty, isequal, issubset, ==, hash, union, intersect, minimum, maximum, extrema, range, ⊇ +using Compat.Statistics +import Compat.Statistics: mean + + using Compat using Compat.Dates -export AbstractInterval, ClosedInterval, ⊇, .., ±, ordered, width +export AbstractInterval, Interval, OpenInterval, ClosedInterval, + ⊇, .., ±, ordered, width, duration, leftendpoint, rightendpoint, endpoints, + isclosed, isleftclosed, isrightclosed, isleftopen, isrightopen, closedendpoints, + infimum, supremum + +""" +A subtype of `Domain{T}` represents a subset of type `T`, that provides `in`. +""" +abstract type Domain{T} end +""" +A subtype of `AbstractInterval{T}` represents an interval subset of type `T`, that provides +`endpoints`, `closedendpoints`. +""" +abstract type AbstractInterval{T} <: Domain{T} end + + +"A tuple containing the left and right endpoints of the interval." +endpoints(d::AI) where AI<:AbstractInterval = error("Override endpoints(::$(AI))") + +"The left endpoint of the interval." +leftendpoint(d::AbstractInterval) = endpoints(d)[1] + +"The right endpoint of the interval." +rightendpoint(d::AbstractInterval) = endpoints(d)[2] + +"A tuple of `Bool`'s encoding whether the left/right endpoints are closed." +closedendpoints(d::AI) where AI<:AbstractInterval = error("Override closedendpoints(::$(AI))") + +"Is the interval closed at the left endpoint?" +isleftclosed(d::AbstractInterval) = closedendpoints(d)[1] + +"Is the interval closed at the right endpoint?" +isrightclosed(d::AbstractInterval) = closedendpoints(d)[2] + +# open_left and open_right are implemented in terms of closed_* above, so those +# are the only ones that should be implemented for specific intervals +"Is the interval open at the left endpoint?" +isleftopen(d::AbstractInterval) = !isleftclosed(d) -abstract type AbstractInterval{T} end +"Is the interval open at the right endpoint?" +isrightopen(d::AbstractInterval) = !isrightclosed(d) -include("closed.jl") +# Only closed if closed at both endpoints, and similar for open +isclosed(d::AbstractInterval) = isleftclosed(d) && isrightclosed(d) +isopen(d::AbstractInterval) = isleftopen(d) && isrightopen(d) eltype(::Type{AbstractInterval{T}}) where {T} = T @pure eltype(::Type{I}) where {I<:AbstractInterval} = eltype(supertype(I)) -convert(::Type{I}, i::I) where {I<:AbstractInterval} = i -function convert(::Type{I}, i::AbstractInterval) where I<:AbstractInterval - T = eltype(I) - I(convert(T, i.left), convert(T, i.right)) -end -function convert(::Type{I}, r::AbstractRange) where I<:AbstractInterval - T = eltype(I) - I(convert(T, minimum(r)), convert(T, maximum(r))) -end +convert(::Type{AbstractInterval}, i::AbstractInterval) = i +convert(::Type{AbstractInterval{T}}, i::AbstractInterval{T}) where T = i + ordered(a::T, b::T) where {T} = ifelse(a < b, (a, b), (b, a)) ordered(a, b) = ordered(promote(a, b)...) @@ -36,4 +74,133 @@ _checked_conversion(::Type{T}, a::T, b::T) where {T} = a, b _checked_conversion(::Type{Any}, a, b) = throw(ArgumentError("$a and $b promoted to type Any")) _checked_conversion(::Type{T}, a, b) where {T} = throw(ArgumentError("$a and $b are not both of type $T")) +function infimum(d::AbstractInterval{T}) where T + a = leftendpoint(d) + b = rightendpoint(d) + a > b && throw(ArgumentError("Infimum not defined for empty intervals")) + a +end + +function supremum(d::AbstractInterval{T}) where T + a = leftendpoint(d) + b = rightendpoint(d) + a > b && throw(ArgumentError("Supremum not defined for empty intervals")) + b +end + +mean(d::AbstractInterval) = (leftendpoint(d) + rightendpoint(d))/2 + +issubset(A::AbstractInterval, B::AbstractInterval) = ((leftendpoint(A) in B) && (rightendpoint(A) in B)) || isempty(A) +⊇(A::AbstractInterval, B::AbstractInterval) = issubset(B, A) +if VERSION < v"1.1.0-DEV.123" + issubset(x, B::AbstractInterval) = issubset(convert(AbstractInterval, x), B) +end + +""" + w = width(iv) + +Calculate the width (max-min) of interval `iv`. Note that for integers +`l` and `r`, `width(l..r) = length(l:r) - 1`. +""" +function width(A::AbstractInterval) + _width = rightendpoint(A) - leftendpoint(A) + max(zero(_width), _width) # this works when T is a Date +end + +""" +A subtype of `TypedEndpointsInterval{L,R,T}` where `L` and `R` are `:open` or `:closed`, +that represents an interval subset of type `T`, and provides `endpoints`. +""" +abstract type TypedEndpointsInterval{L,R,T} <: AbstractInterval{T} end + +closedendpoints(d::TypedEndpointsInterval{:closed,:closed}) = (true,true) +closedendpoints(d::TypedEndpointsInterval{:closed,:open}) = (true,false) +closedendpoints(d::TypedEndpointsInterval{:open,:closed}) = (false,true) +closedendpoints(d::TypedEndpointsInterval{:open,:open}) = (false,false) + + +in(v, I::TypedEndpointsInterval{:closed,:closed}) = leftendpoint(I) ≤ v ≤ rightendpoint(I) +in(v, I::TypedEndpointsInterval{:open,:open}) = leftendpoint(I) < v < rightendpoint(I) +in(v, I::TypedEndpointsInterval{:closed,:open}) = leftendpoint(I) ≤ v < rightendpoint(I) +in(v, I::TypedEndpointsInterval{:open,:closed}) = leftendpoint(I) < v ≤ rightendpoint(I) + +in(a::AbstractInterval, b::TypedEndpointsInterval{:closed,:closed}) = + (leftendpoint(a) ≥ leftendpoint(b)) & (rightendpoint(a) ≤ rightendpoint(b)) +in(a::TypedEndpointsInterval{:open,:open}, b::TypedEndpointsInterval{:open,:open}) = + (leftendpoint(a) ≥ leftendpoint(b)) & (rightendpoint(a) ≤ rightendpoint(b)) +in(a::TypedEndpointsInterval{:closed,:open}, b::TypedEndpointsInterval{:open,:open}) = + (leftendpoint(a) > leftendpoint(b)) & (rightendpoint(a) ≤ rightendpoint(b)) +in(a::TypedEndpointsInterval{:open,:closed}, b::TypedEndpointsInterval{:open,:open}) = + (leftendpoint(a) ≥ leftendpoint(b)) & (rightendpoint(a) < rightendpoint(b)) +in(a::TypedEndpointsInterval{:closed,:closed}, b::TypedEndpointsInterval{:open,:open}) = + (leftendpoint(a) > leftendpoint(b)) & (rightendpoint(a) < rightendpoint(b)) +in(a::TypedEndpointsInterval{:closed}, b::TypedEndpointsInterval{:open,:closed}) = + (leftendpoint(a) > leftendpoint(b)) & (rightendpoint(a) ≤ rightendpoint(b)) +in(a::TypedEndpointsInterval{:open}, b::TypedEndpointsInterval{:open,:closed}) = + (leftendpoint(a) ≥ leftendpoint(b)) & (rightendpoint(a) ≤ rightendpoint(b)) +in(a::TypedEndpointsInterval{L,:closed}, b::TypedEndpointsInterval{:closed,:open}) where L = (leftendpoint(a) ≥ leftendpoint(b)) & (rightendpoint(a) < rightendpoint(b)) +in(a::TypedEndpointsInterval{L,:open}, b::TypedEndpointsInterval{:closed,:open}) where L = (leftendpoint(a) ≥ leftendpoint(b)) & (rightendpoint(a) ≤ rightendpoint(b)) + +isempty(A::TypedEndpointsInterval{:closed,:closed}) = leftendpoint(A) > rightendpoint(A) +isempty(A::TypedEndpointsInterval) = leftendpoint(A) ≥ rightendpoint(A) + +isequal(A::TypedEndpointsInterval{L,R}, B::TypedEndpointsInterval{L,R}) where {L,R} = (isequal(leftendpoint(A), leftendpoint(B)) & isequal(rightendpoint(A), rightendpoint(B))) | (isempty(A) & isempty(B)) +isequal(A::TypedEndpointsInterval, B::TypedEndpointsInterval) = isempty(A) & isempty(B) + +==(A::TypedEndpointsInterval{L,R}, B::TypedEndpointsInterval{L,R}) where {L,R} = (leftendpoint(A) == leftendpoint(B) && rightendpoint(A) == rightendpoint(B)) || (isempty(A) && isempty(B)) +==(A::TypedEndpointsInterval, B::TypedEndpointsInterval) = isempty(A) && isempty(B) + +const _interval_hash = UInt == UInt64 ? 0x1588c274e0a33ad4 : 0x1e3f7252 + +hash(I::TypedEndpointsInterval, h::UInt) = hash(leftendpoint(I), hash(rightendpoint(I), hash(_interval_hash, h))) + +minimum(d::TypedEndpointsInterval{:closed}) = infimum(d) +minimum(d::TypedEndpointsInterval{:open}) = throw(ArgumentError("$d is open on the left. Use infimum.")) +maximum(d::TypedEndpointsInterval{L,:closed}) where L = supremum(d) +maximum(d::TypedEndpointsInterval{L,:open}) where L = throw(ArgumentError("$d is open on the right. Use supremum.")) + +extrema(I::TypedEndpointsInterval) = (infimum(I), supremum(I)) + +# Open and closed at endpoints +isleftclosed(d::TypedEndpointsInterval{:closed}) = true +isleftclosed(d::TypedEndpointsInterval{:open}) = false +isrightclosed(d::TypedEndpointsInterval{L,:closed}) where {L} = true +isrightclosed(d::TypedEndpointsInterval{L,:open}) where {L} = false + +# UnitRange construction +# The third is the one we want, but the first two are needed to resolve ambiguities +Base.Slice{T}(i::TypedEndpointsInterval{:closed,:closed,I}) where {T<:AbstractUnitRange,I<:Integer} = + Base.Slice{T}(minimum(i):maximum(i)) +function Base.OneTo{T}(i::TypedEndpointsInterval{:closed,:closed,I}) where {T<:Integer,I<:Integer} + @noinline throwstart(i) = throw(ArgumentError("smallest element must be 1, got $(minimum(i))")) + minimum(i) == 1 || throwstart(i) + Base.OneTo{T}(maximum(i)) +end +UnitRange{T}(i::TypedEndpointsInterval{:closed,:closed,I}) where {T<:Integer,I<:Integer} = UnitRange{T}(minimum(i), maximum(i)) +UnitRange(i::TypedEndpointsInterval{:closed,:closed,I}) where {I<:Integer} = UnitRange{I}(i) +range(i::TypedEndpointsInterval{:closed,:closed,I}) where {I<:Integer} = UnitRange{I}(i) + +""" + duration(iv) + +calculates the the total number of integers or dates of an integer or date +valued interval. For example, `duration(0..1)` is 2, while `width(0..1)` is 1. +""" +duration(A::TypedEndpointsInterval{:closed,:closed,T}) where {T<:Integer} = max(0, Int(A.right - A.left) + 1) +duration(A::TypedEndpointsInterval{:closed,:closed,Date}) = max(0, Dates.days(A.right - A.left) + 1) + +include("interval.jl") + +# convert numbers to intervals +convert(::Type{AbstractInterval}, x::Number) = x..x +convert(::Type{AbstractInterval{T}}, x::Number) where T = + convert(AbstractInterval{T}, convert(AbstractInterval, x)) +convert(::Type{TypedEndpointsInterval{:closed,:closed}}, x::Number) = x..x +convert(::Type{TypedEndpointsInterval{:closed,:closed,T}}, x::Number) where T = + convert(AbstractInterval{T}, convert(AbstractInterval, x)) +convert(::Type{ClosedInterval}, x::Number) = x..x +convert(::Type{ClosedInterval{T}}, x::Number) where T = + convert(AbstractInterval{T}, convert(AbstractInterval, x)) + + end # module diff --git a/src/closed.jl b/src/closed.jl deleted file mode 100644 index c537cf7..0000000 --- a/src/closed.jl +++ /dev/null @@ -1,119 +0,0 @@ -""" -A `ClosedInterval(left, right)` is an interval set that includes both its upper and lower bounds. In -mathematical notation, the constructed range is `[left, right]`. -""" -struct ClosedInterval{T} <: AbstractInterval{T} - left::T - right::T - - ClosedInterval{T}(l::T, r::T) where {T} = new{T}(l, r) - ClosedInterval{T}(l, r) where {T} = ((a, b) = checked_conversion(T, l, r); new{T}(a, b)) - ClosedInterval{T}(i::AbstractInterval) where {T} = convert(ClosedInterval{T}, i) -end - -function ClosedInterval(left, right) - # Defining this as ClosedInterval(promote(left, right)...) has one problem: - # if left and right do not promote to a common type, it triggers a StackOverflow. - T = promote_type(typeof(left), typeof(right)) - ClosedInterval{T}(checked_conversion(T, left, right)...) -end - -ClosedInterval(i::AbstractInterval) = convert(ClosedInterval{eltype(i)}, i) - -""" - iv = l..r - -Construct a ClosedInterval `iv` spanning the region from `l` to `r`. -""" -..(x, y) = ClosedInterval(x, y) - -""" - iv = center±halfwidth - -Construct a ClosedInterval `iv` spanning the region from -`center - halfwidth` to `center + halfwidth`. -""" -±(x, y) = ClosedInterval(x - y, x + y) -±(x::CartesianIndex, y) = (xy = y * one(x); map(ClosedInterval, (x - xy).I, (x + xy).I)) - -show(io::IO, I::ClosedInterval) = print(io, I.left, "..", I.right) - -in(v, I::ClosedInterval) = I.left <= v <= I.right -in(a::ClosedInterval, b::ClosedInterval) = (b.left <= a.left) & (a.right <= b.right) - -isempty(A::ClosedInterval) = A.left > A.right - -isequal(A::ClosedInterval, B::ClosedInterval) = (isequal(A.left, B.left) & isequal(A.right, B.right)) | (isempty(A) & isempty(B)) - -==(A::ClosedInterval, B::ClosedInterval) = (A.left == B.left && A.right == B.right) || (isempty(A) && isempty(B)) - -const _closed_interval_hash = UInt == UInt64 ? 0x1588c274e0a33ad4 : 0x1e3f7252 - -hash(I::ClosedInterval, h::UInt) = hash(I.left, hash(I.right, hash(_closed_interval_hash, h))) - -minimum(I::ClosedInterval) = I.left -maximum(I::ClosedInterval) = I.right -extrema(I::ClosedInterval) = (minimum(I), maximum(I)) - -function intersect(A::ClosedInterval, B::ClosedInterval) - left = max(A.left, B.left) - right = min(A.right, B.right) - ClosedInterval(left, right) -end - -function union(A::ClosedInterval{T}, B::ClosedInterval{T}) where T<:AbstractFloat - max(A.left, B.left) <= nextfloat(min(A.right, B.right)) || throw(ArgumentError("Cannot construct union of disjoint sets.")) - _union(A, B) -end - -function union(A::ClosedInterval, B::ClosedInterval) - max(A.left, B.left) <= min(A.right, B.right) || throw(ArgumentError("Cannot construct union of disjoint sets.")) - _union(A, B) -end - -function _union(A::ClosedInterval, B::ClosedInterval) - left = min(A.left, B.left) - right = max(A.right, B.right) - ClosedInterval(left, right) -end - -issubset(A::ClosedInterval, B::ClosedInterval) = ((A.left in B) && (A.right in B)) || isempty(A) - -⊇(A::ClosedInterval, B::ClosedInterval) = issubset(B, A) - -""" - w = width(iv) - -Calculate the width (max-min) of interval `iv`. Note that for integers -`l` and `r`, `width(l..r) = length(l:r) - 1`. -""" -function width(A::ClosedInterval{T}) where T - _width = A.right - A.left - max(zero(_width), _width) # this works when T is a Date -end - -length(A::ClosedInterval{T}) where {T<:Integer} = max(0, Int(A.right - A.left) + 1) - -length(A::ClosedInterval{Date}) = max(0, Dates.days(A.right - A.left) + 1) - -function convert(::Type{R}, i::ClosedInterval{I}) where {R<:AbstractUnitRange,I<:Integer} - R(minimum(i), maximum(i)) -end - -# The third is the one we want, but the first two are needed to resolve ambiguities -Base.Slice{T}(i::ClosedInterval{I}) where {T<:AbstractUnitRange,I<:Integer} = - Base.Slice{T}(minimum(i):maximum(i)) -function Base.OneTo{T}(i::ClosedInterval{I}) where {T<:Integer,I<:Integer} - @noinline throwstart(i) = throw(ArgumentError("smallest element must be 1, got $(minimum(i))")) - minimum(i) == 1 || throwstart(i) - Base.OneTo{T}(maximum(i)) -end -function (::Type{R})(i::ClosedInterval{I}) where {R<:AbstractUnitRange,I<:Integer} - R(minimum(i), maximum(i)) -end - -range(i::ClosedInterval{I}) where {I<:Integer} = convert(UnitRange{I}, i) - -Base.promote_rule(::Type{ClosedInterval{T1}}, ::Type{ClosedInterval{T2}}) where {T1,T2} = ClosedInterval{promote_type(T1, T2)} - -Base.IteratorSize(::Type{<:ClosedInterval}) = Base.SizeUnknown() diff --git a/src/interval.jl b/src/interval.jl new file mode 100644 index 0000000..638f99e --- /dev/null +++ b/src/interval.jl @@ -0,0 +1,236 @@ +""" +An `Interval{L,R}(left, right)` where L,R are :open or :closed +is an interval set containg `x` such that +1. `left ≤ x ≤ right` if `L == R == :closed` +2. `left < x ≤ right` if `L == :open` and `R == :closed` +3. `left ≤ x < right` if `L == :closed` and `R == :open`, or +4. `left < x < right` if `L == R == :open` +""" +struct Interval{L,R,T} <: TypedEndpointsInterval{L,R,T} + left::T + right::T + + Interval{L,R,T}(l, r) where {L,R,T} = ((a, b) = checked_conversion(T, l, r); new{L,R,T}(a, b)) +end + + +""" +A `ClosedInterval(left, right)` is an interval set that includes both its upper and lower bounds. In +mathematical notation, the constructed range is `[left, right]`. +""" +const ClosedInterval{T} = Interval{:closed,:closed,T} + +""" +An `TypedEndpointsInterval{:open,:open}(left, right)` is an interval set that includes both its upper and lower bounds. In +mathematical notation, the constructed range is `(left, right)`. +""" +const OpenInterval{T} = Interval{:open,:open,T} + +Interval{L,R,T}(i::AbstractInterval) where {L,R,T} = Interval{L,R,T}(endpoints(i)...) +Interval{L,R}(left, right) where {L,R} = Interval{L,R,promote_type(typeof(left), typeof(right))}(left,right) +Interval{L,R}(left::T, right::T) where {L,R,T} = Interval{L,R,T}(left, right) +Interval(left, right) = ClosedInterval(left, right) + + +# Interval(::AbstractInterval) allows open/closed intervals to be changed +Interval{L,R}(i::AbstractInterval) where {L,R} = Interval{L,R}(endpoints(i)...) +Interval(i::AbstractInterval) = Interval{isleftclosed(i) ? (:closed) : (:open), + isrightclosed(i) ? (:closed) : (:open)}(i) +Interval(i::TypedEndpointsInterval{L,R}) where {L,R} = Interval{L,R}(i) + +endpoints(i::Interval) = (i.left, i.right) + +for L in (:(:open),:(:closed)), R in (:(:open),:(:closed)) + @eval begin + convert(::Type{Interval{$L,$R}}, i::Interval{$L,$R}) = i + convert(::Type{Interval{$L,$R,T}}, i::Interval{$L,$R,T}) where T = i + end +end +convert(::Type{Interval}, i::Interval) = i + +function convert(::Type{II}, i::AbstractInterval) where II<:ClosedInterval + isclosed(i) || throw(InexactError(:convert,II,i)) + II(i) +end +function convert(::Type{II}, i::AbstractInterval) where II<:OpenInterval + isopen(i) || throw(InexactError(:convert,II,i)) + II(i) +end +function convert(::Type{II}, i::AbstractInterval) where II<:Interval{:open,:closed} + (isleftopen(i) && isrightclosed(i)) || throw(InexactError(:convert,II,i)) + II(i) +end +function convert(::Type{II}, i::AbstractInterval) where II<:Interval{:closed,:open} + (isleftclosed(i) && isrightopen(i)) || throw(InexactError(:convert,II,i)) + II(i) +end + +convert(::Type{Interval}, i::AbstractInterval) = Interval(i) + +convert(::Type{Domain{T}}, d::Interval{L,R}) where {L,R,T} = convert(Interval{L,R,T}, d) +convert(::Type{AbstractInterval{T}}, d::Interval{L,R}) where {L,R,T} = convert(Interval{L,R,T}, d) +convert(::Type{AbstractInterval{T}}, d::Interval{L,R,T}) where {L,R,T} = convert(Interval{L,R,T}, d) +convert(::Type{TypedEndpointsInterval{L,R,T}}, d::Interval{L,R}) where {L,R,T} = convert(Interval{L,R,T}, d) +convert(::Type{TypedEndpointsInterval{L,R,T}}, d::Interval{L,R,T}) where {L,R,T} = convert(Interval{L,R,T}, d) +convert(::Type{Domain}, d::Interval{L,R}) where {L,R} = d +convert(::Type{AbstractInterval}, d::Interval{L,R}) where {L,R} = d +convert(::Type{TypedEndpointsInterval{L,R}}, d::Interval{L,R}) where {L,R} = d + + +""" + iv = l..r + +Construct a ClosedInterval `iv` spanning the region from `l` to `r`. +""" +..(x, y) = ClosedInterval(x, y) + +""" + iv = center±halfwidth + +Construct a ClosedInterval `iv` spanning the region from +`center - halfwidth` to `center + halfwidth`. +""" +±(x, y) = ClosedInterval(x - y, x + y) +±(x::CartesianIndex, y) = (xy = y * one(x); map(ClosedInterval, (x - xy).I, (x + xy).I)) + +show(io::IO, I::ClosedInterval) = print(io, leftendpoint(I), "..", rightendpoint(I)) +show(io::IO, I::OpenInterval) = print(io, leftendpoint(I), "..", rightendpoint(I), " (open)") +show(io::IO, I::Interval{:open,:closed}) = print(io, leftendpoint(I), "..", rightendpoint(I), " (open–closed)") +show(io::IO, I::Interval{:closed,:open}) = print(io, leftendpoint(I), "..", rightendpoint(I), " (closed–open)") + +# The following are not typestable for mixed endpoint types +_left_intersect_type(::Type{Val{:open}}, ::Type{Val{L2}}, a1, a2) where L2 = a1 < a2 ? (a2,L2) : (a1,:open) +_left_intersect_type(::Type{Val{:closed}}, ::Type{Val{L2}}, a1, a2) where L2 = a1 ≤ a2 ? (a2,L2) : (a1,:closed) +_right_intersect_type(::Type{Val{:open}}, ::Type{Val{R2}}, b1, b2) where R2 = b1 > b2 ? (b2,R2) : (b1,:open) +_right_intersect_type(::Type{Val{:closed}}, ::Type{Val{R2}}, b1, b2) where R2 = b1 ≥ b2 ? (b2,R2) : (b1,:closed) + +function intersect(d1::TypedEndpointsInterval{L1,R1,T}, d2::TypedEndpointsInterval{L2,R2,T}) where {L1,R1,L2,R2,T} + a1, b1 = endpoints(d1); a2, b2 = endpoints(d2) + a,L = _left_intersect_type(Val{L1}, Val{L2}, a1, a2) + b,R = _right_intersect_type(Val{R1}, Val{R2}, b1, b2) + Interval{L,R}(a,b) +end + +function intersect(d1::TypedEndpointsInterval{L,R,T}, d2::TypedEndpointsInterval{L,R,T}) where {L,R,T} + a1, b1 = endpoints(d1); a2, b2 = endpoints(d2) + Interval{L,R}(max(a1,a2),min(b1,b2)) +end + +intersect(d1::TypedEndpointsInterval{L1,R1,T}, d2::TypedEndpointsInterval{L2,R2,V}) where {L1,R1,L2,R2,T,V} = + intersect(promote(d1, d2)...) + +intersect(d1::AbstractInterval, d2::AbstractInterval) = intersect(Interval(d1), Interval(d2)) + + +union(d1::TypedEndpointsInterval{:closed,:closed}, d2::TypedEndpointsInterval{:closed,:closed}) = + _leq_union(d1,d2) +union(d1::Interval{:open,:closed}, d2::TypedEndpointsInterval{:closed,:closed}) = + _leq_union(d1,d2) +union(d1::Interval{:closed,:open}, d2::TypedEndpointsInterval{:closed,:closed}) = + _leq_union(d1,d2) +union(d1::TypedEndpointsInterval{:closed,:closed}, d2::TypedEndpointsInterval{:open,:closed}) = + _leq_union(d1,d2) +union(d1::TypedEndpointsInterval{:closed,:closed}, d2::TypedEndpointsInterval{:closed,:open}) = + _leq_union(d1,d2) +union(d1::TypedEndpointsInterval{:open,:closed}, d2::TypedEndpointsInterval{:open,:closed}) = + _leq_union(d1,d2) +union(d1::TypedEndpointsInterval{:closed,:open}, d2::TypedEndpointsInterval{:closed,:open}) = + _leq_union(d1,d2) +union(d1::TypedEndpointsInterval{:closed,:closed}, d2::TypedEndpointsInterval{:open,:open}) = + _leq_union(d1,d2) +union(d1::TypedEndpointsInterval{:open,:open}, d2::TypedEndpointsInterval{:closed,:closed}) = + _leq_union(d1,d2) + + +function _leq_union(d1, d2) + isempty(d1) && return d2 + isempty(d2) && return d1 + leftendpoint(d1) ≤ leftendpoint(d2) ≤ rightendpoint(d1) || leftendpoint(d1) ≤ rightendpoint(d2) ≤ rightendpoint(d1) || + leftendpoint(d2) ≤ leftendpoint(d1) ≤ rightendpoint(d2) || leftendpoint(d2) ≤ rightendpoint(d1) ≤ rightendpoint(d2) || + throw(ArgumentError("Cannot construct union of disjoint sets.")) + _union(d1, d2) +end + + +function union(d1::TypedEndpointsInterval{:open,:open}, d2::TypedEndpointsInterval{:open,:open}) + isempty(d1) && return d2 + isempty(d2) && return d1 + leftendpoint(d1) ≤ leftendpoint(d2) < rightendpoint(d1) || leftendpoint(d1) < rightendpoint(d2) ≤ rightendpoint(d1) || + leftendpoint(d2) ≤ leftendpoint(d1) < rightendpoint(d2) || leftendpoint(d2) < rightendpoint(d1) ≤ rightendpoint(d2) || + throw(ArgumentError("Cannot construct union of disjoint sets.")) + _union(d1, d2) +end + +function union(d1::TypedEndpointsInterval{:open,:open}, d2::TypedEndpointsInterval{:open,:closed}) + isempty(d1) && return d2 + isempty(d2) && return d1 + leftendpoint(d1) ≤ leftendpoint(d2) < rightendpoint(d1) || leftendpoint(d1) ≤ rightendpoint(d2) ≤ rightendpoint(d1) || + leftendpoint(d2) ≤ leftendpoint(d1) ≤ rightendpoint(d2) || leftendpoint(d2) < rightendpoint(d1) ≤ rightendpoint(d2) || + throw(ArgumentError("Cannot construct union of disjoint sets.")) + _union(d1, d2) +end + +function union(d1::TypedEndpointsInterval{:open,:open}, d2::TypedEndpointsInterval{:closed,:open}) + isempty(d1) && return d2 + isempty(d2) && return d1 + leftendpoint(d1) ≤ leftendpoint(d2) ≤ rightendpoint(d1) || leftendpoint(d1) < rightendpoint(d2) ≤ rightendpoint(d1) || + leftendpoint(d2) ≤ leftendpoint(d1) < rightendpoint(d2) || leftendpoint(d2) ≤ rightendpoint(d1) ≤ rightendpoint(d2) || + throw(ArgumentError("Cannot construct union of disjoint sets.")) + _union(d1, d2) +end + +union(d1::TypedEndpointsInterval, d2::TypedEndpointsInterval{:open,:open}) = union(d2, d1) + + +function union(d1::TypedEndpointsInterval{:closed,:open}, d2::TypedEndpointsInterval{:open,:closed}) + isempty(d1) && return d2 + isempty(d2) && return d1 + leftendpoint(d1) ≤ leftendpoint(d2) < rightendpoint(d1) || leftendpoint(d1) ≤ rightendpoint(d2) ≤ rightendpoint(d1) || + leftendpoint(d2) ≤ leftendpoint(d1) ≤ rightendpoint(d2) || leftendpoint(d2) < rightendpoint(d1) ≤ rightendpoint(d2) || + throw(ArgumentError("Cannot construct union of disjoint sets.")) + _union(d1, d2) +end + +union(d1::TypedEndpointsInterval{:open,:closed}, d2::TypedEndpointsInterval{:closed,:open}) = union(d2, d1) + +# these assume overlap +function _union(A::TypedEndpointsInterval{L,R}, B::TypedEndpointsInterval{L,R}) where {L,R} + left = min(leftendpoint(A), leftendpoint(B)) + right = max(rightendpoint(A), rightendpoint(B)) + Interval{L,R}(left, right) +end + +# this is not typestable +function _union(A::TypedEndpointsInterval{L1,R1}, B::TypedEndpointsInterval{L2,R2}) where {L1,R1,L2,R2} + if leftendpoint(A) == leftendpoint(B) + L = L1 == :closed ? :closed : L2 + elseif leftendpoint(A) < leftendpoint(B) + L = L1 + else + L = L2 + end + if rightendpoint(A) == rightendpoint(B) + R = R1 == :closed ? :closed : R2 + elseif rightendpoint(A) > rightendpoint(B) + R = R1 + else + R = R2 + end + left = min(leftendpoint(A), leftendpoint(B)) + right = max(rightendpoint(A), rightendpoint(B)) + + Interval{L,R}(left, right) +end + +ClosedInterval{T}(i::UnitRange{I}) where {T,I<:Integer} = ClosedInterval{T}(minimum(i), maximum(i)) +ClosedInterval(i::UnitRange{I}) where {I<:Integer} = ClosedInterval{I}(minimum(i), maximum(i)) + + + +Base.promote_rule(::Type{Interval{L,R,T1}}, ::Type{Interval{L,R,T2}}) where {L,R,T1,T2} = Interval{L,R,promote_type(T1, T2)} + +Base.IteratorSize(::Type{<:ClosedInterval}) = Base.SizeUnknown() + +# convert should only work if they represent the same thing. +@deprecate convert(::Type{R}, i::ClosedInterval{I}) where {R<:AbstractUnitRange,I<:Integer} R(i) +@deprecate length(i::ClosedInterval) IntervalSets.duration(i) diff --git a/test/runtests.jl b/test/runtests.jl index fc33380..3ef6138 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,22 +2,39 @@ using IntervalSets using Compat using Compat.Test using Compat.Dates +using Compat.Statistics +import Compat.Statistics: mean -@test isempty(detect_ambiguities(IntervalSets, Base, Core)) +import IntervalSets: Domain, endpoints, closedendpoints, TypedEndpointsInterval + +struct MyClosedUnitInterval <: TypedEndpointsInterval{:closed,:closed,Int} end +endpoints(::MyClosedUnitInterval) = (0,1) + +struct MyUnitInterval <: AbstractInterval{Int} + isleftclosed::Bool + isrightclosed::Bool +end +endpoints(::MyUnitInterval) = (0,1) +closedendpoints(I::MyUnitInterval) = (I.isleftclosed,I.isrightclosed) @testset "IntervalSets" begin + @test isempty(detect_ambiguities(IntervalSets, Base, Core)) + @test ordered(2, 1) == (1, 2) @test ordered(1, 2) == (1, 2) @test ordered(Float16(1), 2) == (1, 2) - @testset "Closed Sets" begin + @testset "Basic Closed Sets" begin @test_throws ArgumentError :a .. "b" I = 0..3 + @test I === ClosedInterval(0,3) === ClosedInterval{Int}(0,3) === + Interval(0,3) @test string(I) == "0..3" - @test @inferred(convert(UnitRange, I)) === 0:3 + @test @inferred(UnitRange(I)) === 0:3 @test @inferred(range(I)) === 0:3 - @test @inferred(convert(UnitRange{Int16}, I)) === Int16(0):Int16(3) @test @inferred(UnitRange{Int16}(I)) === Int16(0):Int16(3) + @test @inferred(ClosedInterval(0:3)) === I + @test @inferred(ClosedInterval{Float64}(0:3)) === 0.0..3.0 J = 3..2 K = 5..4 L = 3 ± 2 @@ -29,12 +46,6 @@ using Compat.Dates @test eltype(I) == Int @test eltype(M) == Float64 - @test @inferred(convert(ClosedInterval{Float64}, I)) === 0.0..3.0 - @test @inferred(convert(ClosedInterval{Float64}, 0:3)) === 0.0..3.0 - @test !(convert(ClosedInterval{Float64}, I) === 0..3) - @test ClosedInterval{Float64}(1,3) === 1.0..3.0 - @test ClosedInterval(0.5..2.5) === 0.5..2.5 - @test ClosedInterval{Int}(1.0..3.0) === 1..3 @test !isempty(I) @test isempty(J) @@ -46,8 +57,8 @@ using Compat.Dates @test isequal(I, I) @test isequal(J, K) - @test typeof(M.left) == typeof(M.right) && typeof(M.left) == Float64 - @test typeof(N.left) == typeof(N.right) && typeof(N.left) == Int + @test typeof(leftendpoint(M)) == typeof(rightendpoint(M)) && typeof(leftendpoint(M)) == Float64 + @test typeof(leftendpoint(N)) == typeof(rightendpoint(N)) && typeof(leftendpoint(N)) == Int @test maximum(I) === 3 @test minimum(I) === 0 @@ -65,11 +76,26 @@ using Compat.Dates B = Float16(1.235)..Float16(1.3) C = Float16(1.236)..Float16(1.3) D = Float16(1.1)..Float16(1.236) - @test A ∪ B == Float16(1.1)..Float16(1.3) @test D ∪ B == Float16(1.1)..Float16(1.3) @test D ∪ C == Float16(1.1)..Float16(1.3) @test_throws(ArgumentError, A ∪ C) + @test 1.5 ∉ 0..1 + @test 1.5 ∉ 2..3 + @test_broken (0..1) ∪ (2..3) ≠ 0..3 + # even though A and B contain all Float32s between their extrema, + # union should not return an interval as there exists a Float64 + # inbetween + x32 = nextfloat(rightendpoint(A)) + x64 = nextfloat(Float64(rightendpoint(A))) + @test x32 ∉ A + @test x32 ∈ B + @test x64 ∉ A + @test x64 ∉ B + # these tests + @test_broken x32 ∈ A ∪ B + @test_broken x64 ∉ A ∪ B + @test J ⊆ L @test (L ⊆ J) == false @test K ⊆ I @@ -81,33 +107,493 @@ using Compat.Dates @test hash(1..3) == hash(1.0..3.0) - let A = Date(1990, 1, 1), B = Date(1990, 3, 1) - @test width(ClosedInterval(A, B)) == Dates.Day(59) - @test width(ClosedInterval(B, A)) == Dates.Day(0) - @test isempty(ClosedInterval(B, A)) - @test length(ClosedInterval(A, B)) ≡ 60 - @test length(ClosedInterval(B, A)) ≡ 0 - end + @test width(I) == 3 + @test width(J) == 0 @test width(ClosedInterval(3,7)) ≡ 4 @test width(ClosedInterval(4.0,8.0)) ≡ 4.0 + @test mean(0..1) == 0.5 + @test promote(1..2, 1.0..2.0) === (1.0..2.0, 1.0..2.0) - @test length(I) == 4 - @test length(J) == 0 - # length deliberately not defined for non-integer intervals - @test_throws MethodError length(1.2..2.4) - - # see issue #35 about below test - if (v"0.6.99" <= VERSION < v"1.1.0-DEV.123") - @test_broken issubset(0.1, 0.0..1.0) == true - @test_broken issubset(1.1, 0.0..1.0) == false - else - @test issubset(0.1, 0.0..1.0) == true - @test issubset(1.1, 0.0..1.0) == false + # duration deliberately not defined for non-integer intervals + @test_throws MethodError duration(1.2..2.4) + end + + @testset "Day interval" begin + A = Date(1990, 1, 1); B = Date(1990, 3, 1) + @test width(ClosedInterval(A, B)) == Dates.Day(59) + @test width(ClosedInterval(B, A)) == Dates.Day(0) + @test isempty(ClosedInterval(B, A)) + @test duration(ClosedInterval(A, B)) ≡ 60 + @test duration(ClosedInterval(B, A)) ≡ 0 + end + + @testset "Convert" begin + I = 0..3 + @test @inferred(convert(ClosedInterval{Float64}, I)) === + @inferred(convert(AbstractInterval{Float64}, I)) === + @inferred(convert(Domain{Float64}, I)) === + @inferred(ClosedInterval{Float64}(I)) === 0.0..3.0 + @test @inferred(convert(ClosedInterval, I)) === + @inferred(convert(Interval, I)) === + @inferred(ClosedInterval(I)) === + @inferred(Interval(I)) === + @inferred(convert(AbstractInterval, I)) === + @inferred(convert(Domain, I)) === I + @test_throws InexactError convert(OpenInterval, I) + @test_throws InexactError convert(Interval{:open,:closed}, I) + @test_throws InexactError convert(Interval{:closed,:open}, I) + @test !(convert(ClosedInterval{Float64}, I) === 0..3) + @test ClosedInterval{Float64}(1,3) === 1.0..3.0 + @test ClosedInterval(0.5..2.5) === 0.5..2.5 + @test ClosedInterval{Int}(1.0..3.0) === 1..3 + J = OpenInterval(I) + @test_throws InexactError convert(ClosedInterval, J) + @test @inferred(convert(OpenInterval{Float64}, J)) === + @inferred(convert(AbstractInterval{Float64}, J)) === + @inferred(convert(Domain{Float64}, J)) === + @inferred(OpenInterval{Float64}(J)) === OpenInterval(0.0..3.0) + @test @inferred(convert(OpenInterval, J)) === + @inferred(convert(Interval, J)) === + @inferred(convert(AbstractInterval, J)) === + @inferred(convert(Domain, J)) === + @inferred(OpenInterval(J)) === OpenInterval(J) + J = Interval{:open,:closed}(I) + @test_throws InexactError convert(Interval{:closed,:open}, J) + @test @inferred(convert(Interval{:open,:closed,Float64}, J)) === + @inferred(convert(AbstractInterval{Float64}, J)) === + @inferred(convert(Domain{Float64}, J)) === + @inferred(Interval{:open,:closed,Float64}(J)) === Interval{:open,:closed}(0.0..3.0) + @test @inferred(convert(Interval{:open,:closed}, J)) === + @inferred(convert(Interval, J)) === + @inferred(convert(AbstractInterval, J)) === + @inferred(convert(Domain, J)) === + @inferred(Interval{:open,:closed}(J)) === Interval{:open,:closed}(J) + J = Interval{:closed,:open}(I) + @test_throws InexactError convert(Interval{:open,:closed}, J) + @test @inferred(convert(Interval{:closed,:open,Float64}, J)) === + @inferred(convert(AbstractInterval{Float64}, J)) === + @inferred(convert(Domain{Float64}, J)) === + @inferred(Interval{:closed,:open,Float64}(J)) === Interval{:closed,:open}(0.0..3.0) + @test @inferred(convert(Interval{:closed,:open}, J)) === + @inferred(convert(Interval, J)) === + @inferred(convert(AbstractInterval, J)) === + @inferred(convert(Domain, J)) === + @inferred(Interval{:closed,:open}(J)) === Interval{:closed,:open}(J) + + @test 1.0..2.0 === 1.0..2 === 1..2.0 === ClosedInterval{Float64}(1..2) === + Interval(1.0,2.0) + + @test convert(AbstractInterval, 1.0) == + convert(AbstractInterval{Float64}, 1) == + convert(TypedEndpointsInterval{:closed,:closed}, 1.0) == + convert(TypedEndpointsInterval{:closed,:closed,Float64}, 1) == + convert(ClosedInterval, 1.0) == + convert(ClosedInterval{Float64}, 1) == + 1.0..1.0 + + end + + + @testset "Interval tests" begin + for T in (Float32,Float64,BigFloat) + d = zero(T) .. one(T) + @test T(0.5) ∈ d + @test T(1.1) ∉ d + @test 0.5f0 ∈ d + @test 1.1f0 ∉ d + @test BigFloat(0.5) ∈ d + @test BigFloat(1.1) ∉ d + @test leftendpoint(d) ∈ d + @test BigFloat(leftendpoint(d)) ∈ d + @test nextfloat(leftendpoint(d)) ∈ d + @test nextfloat(BigFloat(leftendpoint(d))) ∈ d + @test prevfloat(leftendpoint(d)) ∉ d + @test prevfloat(leftendpoint(d)) ∉ d + @test rightendpoint(d) ∈ d + @test BigFloat(rightendpoint(d)) ∈ d + @test nextfloat(rightendpoint(d)) ∉ d + @test nextfloat(BigFloat(rightendpoint(d))) ∉ d + @test prevfloat(rightendpoint(d)) ∈ d + @test prevfloat(rightendpoint(d)) ∈ d + + @test leftendpoint(d) == zero(T) + @test rightendpoint(d) == one(T) + @test minimum(d) == infimum(d) == leftendpoint(d) + @test maximum(d) == supremum(d) == rightendpoint(d) + + @test IntervalSets.isclosed(d) + @test !IntervalSets.isopen(d) + + @test convert(AbstractInterval, d) ≡ d + @test convert(AbstractInterval{T}, d) ≡ d + @test convert(IntervalSets.Domain, d) ≡ d + @test convert(IntervalSets.Domain{T}, d) ≡ d + + d = OpenInterval(zero(T) .. one(T)) + @test IntervalSets.isopen(d) + @test !IntervalSets.isclosed(d) + @test leftendpoint(d) ∉ d + @test BigFloat(leftendpoint(d)) ∉ d + @test nextfloat(leftendpoint(d)) ∈ d + @test nextfloat(BigFloat(leftendpoint(d))) ∈ d + @test prevfloat(leftendpoint(d)) ∉ d + @test prevfloat(leftendpoint(d)) ∉ d + @test rightendpoint(d) ∉ d + @test BigFloat(rightendpoint(d)) ∉ d + @test nextfloat(rightendpoint(d)) ∉ d + @test nextfloat(BigFloat(rightendpoint(d))) ∉ d + @test prevfloat(rightendpoint(d)) ∈ d + @test prevfloat(rightendpoint(d)) ∈ d + @test infimum(d) == leftendpoint(d) + @test supremum(d) == rightendpoint(d) + @test_throws ArgumentError minimum(d) + @test_throws ArgumentError maximum(d) + + @test isempty(OpenInterval(1,1)) + + d = Interval{:open,:closed}(zero(T) .. one(T)) + @test !IntervalSets.isopen(d) + @test !IntervalSets.isclosed(d) + @test leftendpoint(d) ∉ d + @test BigFloat(leftendpoint(d)) ∉ d + @test nextfloat(leftendpoint(d)) ∈ d + @test nextfloat(BigFloat(leftendpoint(d))) ∈ d + @test prevfloat(leftendpoint(d)) ∉ d + @test prevfloat(leftendpoint(d)) ∉ d + @test rightendpoint(d) ∈ d + @test BigFloat(rightendpoint(d)) ∈ d + @test nextfloat(rightendpoint(d)) ∉ d + @test nextfloat(BigFloat(rightendpoint(d))) ∉ d + @test prevfloat(rightendpoint(d)) ∈ d + @test prevfloat(rightendpoint(d)) ∈ d + @test infimum(d) == leftendpoint(d) + @test supremum(d) == rightendpoint(d) + @test infimum(d) == leftendpoint(d) + @test maximum(d) == supremum(d) == rightendpoint(d) + @test_throws ArgumentError minimum(d) + + d = Interval{:closed,:open}(zero(T) .. one(T)) + @test !IntervalSets.isopen(d) + @test !IntervalSets.isclosed(d) + @test leftendpoint(d) ∈ d + @test BigFloat(leftendpoint(d)) ∈ d + @test nextfloat(leftendpoint(d)) ∈ d + @test nextfloat(BigFloat(leftendpoint(d))) ∈ d + @test prevfloat(leftendpoint(d)) ∉ d + @test prevfloat(leftendpoint(d)) ∉ d + @test rightendpoint(d) ∉ d + @test BigFloat(rightendpoint(d)) ∉ d + @test nextfloat(rightendpoint(d)) ∉ d + @test nextfloat(BigFloat(rightendpoint(d))) ∉ d + @test prevfloat(rightendpoint(d)) ∈ d + @test prevfloat(rightendpoint(d)) ∈ d + @test infimum(d) == leftendpoint(d) + @test supremum(d) == rightendpoint(d) + @test infimum(d) == minimum(d) == leftendpoint(d) + @test supremum(d) == rightendpoint(d) + @test_throws ArgumentError maximum(d) + + # - empty interval + @test isempty(one(T) .. zero(T)) + @test zero(T) ∉ one(T) .. zero(T) + + d = one(T) .. zero(T) + @test_throws ArgumentError minimum(d) + @test_throws ArgumentError maximum(d) + @test_throws ArgumentError infimum(d) + @test_throws ArgumentError supremum(d) end end -end -nothing + @testset "In" begin + I = 0..3 + J = 1..2 + @test J ∈ I + @test I ∉ J + @test OpenInterval(J) ∈ I + @test OpenInterval(I) ∉ J + @test J ∈ OpenInterval(I) + @test I ∉ OpenInterval(J) + @test OpenInterval(J) ∈ OpenInterval(I) + @test OpenInterval(I) ∉ OpenInterval(J) + @test Interval{:closed,:open}(J) ∈ OpenInterval(I) + @test Interval{:open,:closed}(J) ∈ OpenInterval(I) + @test Interval{:open,:closed}(J) ∈ Interval{:open,:closed}(I) + @test OpenInterval(I) ∉ OpenInterval(J) + + @test Interval{:closed,:open}(J) ∈ I + @test I ∉ Interval{:closed,:open}(J) + + + @test I ∈ I + @test OpenInterval(I) ∈ I + @test Interval{:open,:closed}(I) ∈ I + @test Interval{:closed,:open}(I) ∈ I + @test I ∉ OpenInterval(I) + @test I ∉ Interval{:open,:closed}(I) + @test I ∉ Interval{:closed,:open}(I) + + @test Interval{:closed,:open}(I) ∈ Interval{:closed,:open}(I) + @test Interval{:open,:closed}(I) ∉ Interval{:closed,:open}(I) + + @test !isequal(I, OpenInterval(I)) + @test !(I == OpenInterval(I)) + end + + + + @testset "Union and intersection" begin + for T in (Float32,Float64) + i1 = zero(T) .. one(T) + i2 = one(T)/3 .. one(T)/2 + i3 = one(T)/2 .. 2*one(T) + i4 = T(2) .. T(3) + i5 = nextfloat(one(T)) .. 2one(T) + i_empty = one(T) ..zero(T) + + # - union of completely overlapping intervals + @test i1 ∪ i2 ≡ i2 ∪ i1 ≡ i1 + @test Interval{:open,:closed}(i1) ∪ Interval{:open,:closed}(i2) ≡ + Interval{:open,:closed}(i2) ∪ Interval{:open,:closed}(i1) ≡ Interval{:open,:closed}(i1) + @test Interval{:closed,:open}(i1) ∪ Interval{:closed,:open}(i2) ≡ + Interval{:closed,:open}(i2) ∪ Interval{:closed,:open}(i1) ≡ Interval{:closed,:open}(i1) + @test OpenInterval(i1) ∪ OpenInterval(i2) ≡ + OpenInterval(i2) ∪ OpenInterval(i1) ≡ OpenInterval(i1) + @test i1 ∪ Interval{:open,:closed}(i2) ≡ Interval{:open,:closed}(i2) ∪ i1 ≡ i1 + @test i1 ∪ Interval{:closed,:open}(i2) ≡ Interval{:closed,:open}(i2) ∪ i1 ≡ i1 + @test i1 ∪ OpenInterval(i2) ≡ OpenInterval(i2) ∪ i1 ≡ i1 + @test OpenInterval(i1) ∪ i2 ≡ i2 ∪ OpenInterval(i1) ≡ OpenInterval(i1) + @test OpenInterval(i1) ∪ Interval{:open,:closed}(i2) ≡ Interval{:open,:closed}(i2) ∪ OpenInterval(i1) ≡ OpenInterval(i1) + @test OpenInterval(i1) ∪ Interval{:closed,:open}(i2) ≡ Interval{:closed,:open}(i2) ∪ OpenInterval(i1) ≡ OpenInterval(i1) + @test Interval{:open,:closed}(i1) ∪ OpenInterval(i2) ≡ OpenInterval(i2) ∪ Interval{:open,:closed}(i1) ≡ Interval{:open,:closed}(i1) + @test Interval{:open,:closed}(i1) ∪ Interval{:closed,:open}(i2) ≡ Interval{:closed,:open}(i2) ∪ Interval{:open,:closed}(i1) ≡ Interval{:open,:closed}(i1) + + # - intersection of completely overlapping intervals + @test i1 ∩ i2 ≡ i2 ∩ i1 ≡ i2 + @test Interval{:open,:closed}(i1) ∩ Interval{:open,:closed}(i2) ≡ + Interval{:open,:closed}(i2) ∩ Interval{:open,:closed}(i1) ≡ Interval{:open,:closed}(i2) + @test Interval{:closed,:open}(i1) ∩ Interval{:closed,:open}(i2) ≡ + Interval{:closed,:open}(i2) ∩ Interval{:closed,:open}(i1) ≡ Interval{:closed,:open}(i2) + @test OpenInterval(i1) ∩ OpenInterval(i2) ≡ + OpenInterval(i2) ∩ OpenInterval(i1) ≡ OpenInterval(i2) + @test i1 ∩ Interval{:open,:closed}(i2) ≡ Interval{:open,:closed}(i2) ∩ i1 ≡ Interval{:open,:closed}(i2) + @test i1 ∩ Interval{:closed,:open}(i2) ≡ Interval{:closed,:open}(i2) ∩ i1 ≡ Interval{:closed,:open}(i2) + @test i1 ∩ OpenInterval(i2) ≡ OpenInterval(i2) ∩ i1 ≡ OpenInterval(i2) + @test OpenInterval(i1) ∩ i2 ≡ i2 ∩ OpenInterval(i1) ≡ i2 + @test OpenInterval(i1) ∩ Interval{:open,:closed}(i2) ≡ Interval{:open,:closed}(i2) ∩ OpenInterval(i1) ≡ Interval{:open,:closed}(i2) + @test OpenInterval(i1) ∩ Interval{:closed,:open}(i2) ≡ Interval{:closed,:open}(i2) ∩ OpenInterval(i1) ≡ Interval{:closed,:open}(i2) + @test Interval{:open,:closed}(i1) ∩ OpenInterval(i2) ≡ OpenInterval(i2) ∩ Interval{:open,:closed}(i1) ≡ OpenInterval(i2) + @test Interval{:open,:closed}(i1) ∩ Interval{:closed,:open}(i2) ≡ Interval{:closed,:open}(i2) ∩ Interval{:open,:closed}(i1) ≡ Interval{:closed,:open}(i2) + + + # - union of partially overlapping intervals + d = zero(T) .. 2*one(T) + @test i1 ∪ i3 ≡ i3 ∪ i1 ≡ d + @test Interval{:open,:closed}(i1) ∪ Interval{:open,:closed}(i3) ≡ + Interval{:open,:closed}(i3) ∪ Interval{:open,:closed}(i1) ≡ Interval{:open,:closed}(d) + @test Interval{:closed,:open}(i1) ∪ Interval{:closed,:open}(i3) ≡ + Interval{:closed,:open}(i3) ∪ Interval{:closed,:open}(i1) ≡ Interval{:closed,:open}(d) + @test OpenInterval(i1) ∪ OpenInterval(i3) ≡ + OpenInterval(i3) ∪ OpenInterval(i1) ≡ OpenInterval(d) + @test i1 ∪ Interval{:open,:closed}(i3) ≡ Interval{:open,:closed}(i3) ∪ i1 ≡ d + @test i1 ∪ Interval{:closed,:open}(i3) ≡ Interval{:closed,:open}(i3) ∪ i1 ≡ Interval{:closed,:open}(d) + @test i1 ∪ OpenInterval(i3) ≡ OpenInterval(i3) ∪ i1 ≡ Interval{:closed,:open}(d) + @test OpenInterval(i1) ∪ i3 ≡ i3 ∪ OpenInterval(i1) ≡ Interval{:open,:closed}(d) + @test OpenInterval(i1) ∪ Interval{:open,:closed}(i3) ≡ Interval{:open,:closed}(i3) ∪ OpenInterval(i1) ≡ Interval{:open,:closed}(d) + @test OpenInterval(i1) ∪ Interval{:closed,:open}(i3) ≡ Interval{:closed,:open}(i3) ∪ OpenInterval(i1) ≡ OpenInterval(d) + @test Interval{:open,:closed}(i1) ∪ OpenInterval(i3) ≡ OpenInterval(i3) ∪ Interval{:open,:closed}(i1) ≡ OpenInterval(d) + @test Interval{:open,:closed}(i1) ∪ Interval{:closed,:open}(i3) ≡ Interval{:closed,:open}(i3) ∪ Interval{:open,:closed}(i1) ≡ OpenInterval(d) + + + + # - intersection of partially overlapping intervals + d = one(T)/2 .. one(T) + @test i1 ∩ i3 ≡ i3 ∩ i1 ≡ d + @test Interval{:open,:closed}(i1) ∩ Interval{:open,:closed}(i3) ≡ + Interval{:open,:closed}(i3) ∩ Interval{:open,:closed}(i1) ≡ Interval{:open,:closed}(d) + @test Interval{:closed,:open}(i1) ∩ Interval{:closed,:open}(i3) ≡ + Interval{:closed,:open}(i3) ∩ Interval{:closed,:open}(i1) ≡ Interval{:closed,:open}(d) + @test OpenInterval(i1) ∩ OpenInterval(i3) ≡ + OpenInterval(i3) ∩ OpenInterval(i1) ≡ OpenInterval(d) + @test i1 ∩ Interval{:open,:closed}(i3) ≡ Interval{:open,:closed}(i3) ∩ i1 ≡ Interval{:open,:closed}(d) + @test i1 ∩ Interval{:closed,:open}(i3) ≡ Interval{:closed,:open}(i3) ∩ i1 ≡ d + @test i1 ∩ OpenInterval(i3) ≡ OpenInterval(i3) ∩ i1 ≡ Interval{:open,:closed}(d) + @test OpenInterval(i1) ∩ i3 ≡ i3 ∩ OpenInterval(i1) ≡ Interval{:closed,:open}(d) + @test OpenInterval(i1) ∩ Interval{:open,:closed}(i3) ≡ Interval{:open,:closed}(i3) ∩ OpenInterval(i1) ≡ OpenInterval(d) + @test OpenInterval(i1) ∩ Interval{:closed,:open}(i3) ≡ Interval{:closed,:open}(i3) ∩ OpenInterval(i1) ≡ Interval{:closed,:open}(d) + @test Interval{:open,:closed}(i1) ∩ OpenInterval(i3) ≡ OpenInterval(i3) ∩ Interval{:open,:closed}(i1) ≡ Interval{:open,:closed}(d) + @test Interval{:open,:closed}(i1) ∩ Interval{:closed,:open}(i3) ≡ Interval{:closed,:open}(i3) ∩ Interval{:open,:closed}(i1) ≡ d + + + # - union of barely overlapping intervals + d = one(T)/3 .. 2*one(T) + @test i2 ∪ i3 ≡ i3 ∪ i2 ≡ d + @test Interval{:open,:closed}(i2) ∪ Interval{:open,:closed}(i3) ≡ + Interval{:open,:closed}(i3) ∪ Interval{:open,:closed}(i2) ≡ Interval{:open,:closed}(d) + @test Interval{:closed,:open}(i2) ∪ Interval{:closed,:open}(i3) ≡ + Interval{:closed,:open}(i3) ∪ Interval{:closed,:open}(i2) ≡ Interval{:closed,:open}(d) + @test_throws ArgumentError OpenInterval(i2) ∪ OpenInterval(i3) + @test i2 ∪ Interval{:open,:closed}(i3) ≡ Interval{:open,:closed}(i3) ∪ i2 ≡ d + @test i2 ∪ Interval{:closed,:open}(i3) ≡ Interval{:closed,:open}(i3) ∪ i2 ≡ Interval{:closed,:open}(d) + @test i2 ∪ OpenInterval(i3) ≡ OpenInterval(i3) ∪ i2 ≡ Interval{:closed,:open}(d) + @test OpenInterval(i2) ∪ i3 ≡ i3 ∪ OpenInterval(i2) ≡ Interval{:open,:closed}(d) + @test_throws ArgumentError OpenInterval(i2) ∪ Interval{:open,:closed}(i3) + @test Interval{:open,:closed}(i2) ∪ OpenInterval(i3) ≡ OpenInterval(i3) ∪ Interval{:open,:closed}(i2) ≡ OpenInterval(d) + @test Interval{:open,:closed}(i2) ∪ Interval{:closed,:open}(i3) ≡ Interval{:closed,:open}(i3) ∪ Interval{:open,:closed}(i2) ≡ OpenInterval(d) + + # - intersection of barely overlapping intervals + d = one(T)/2 .. one(T)/2 + @test i2 ∩ i3 ≡ i3 ∩ i2 ≡ d + @test Interval{:open,:closed}(i2) ∩ Interval{:open,:closed}(i3) ≡ + Interval{:open,:closed}(i3) ∩ Interval{:open,:closed}(i2) ≡ Interval{:open,:closed}(d) + @test Interval{:closed,:open}(i2) ∩ Interval{:closed,:open}(i3) ≡ + Interval{:closed,:open}(i3) ∩ Interval{:closed,:open}(i2) ≡ Interval{:closed,:open}(d) + @test OpenInterval(i2) ∩ OpenInterval(i3) ≡ + OpenInterval(i3) ∩ OpenInterval(i2) ≡ OpenInterval(d) + @test i2 ∩ Interval{:open,:closed}(i3) ≡ Interval{:open,:closed}(i3) ∩ i2 ≡ Interval{:open,:closed}(d) + @test i2 ∩ Interval{:closed,:open}(i3) ≡ Interval{:closed,:open}(i3) ∩ i2 ≡ d + @test i2 ∩ OpenInterval(i3) ≡ OpenInterval(i3) ∩ i2 ≡ Interval{:open,:closed}(d) + @test OpenInterval(i2) ∩ i3 ≡ i3 ∩ OpenInterval(i2) ≡ Interval{:closed,:open}(d) + @test OpenInterval(i2) ∩ Interval{:open,:closed}(i3) ≡ Interval{:open,:closed}(i3) ∩ OpenInterval(i2) ≡ OpenInterval(d) + @test OpenInterval(i2) ∩ Interval{:closed,:open}(i3) ≡ Interval{:closed,:open}(i3) ∩ OpenInterval(i2) ≡ Interval{:closed,:open}(d) + @test Interval{:open,:closed}(i2) ∩ OpenInterval(i3) ≡ OpenInterval(i3) ∩ Interval{:open,:closed}(i2) ≡ Interval{:open,:closed}(d) + @test Interval{:open,:closed}(i2) ∩ Interval{:closed,:open}(i3) ≡ Interval{:closed,:open}(i3) ∩ Interval{:open,:closed}(i2) ≡ d + + + # - union of non-overlapping intervals + @test_throws ArgumentError i1 ∪ i4 + @test_throws ArgumentError i4 ∪ i1 + @test_throws ArgumentError OpenInterval(i1) ∪ i4 + @test_throws ArgumentError i1 ∪ OpenInterval(i4) + @test_throws ArgumentError Interval{:closed,:open}(i1) ∪ i4 + @test_throws ArgumentError Interval{:closed,:open}(i1) ∪ OpenInterval(i4) + + + + # - union of almost-overlapping intervals + @test_throws ArgumentError i1 ∪ i5 + @test_throws ArgumentError i5 ∪ i1 + @test_throws ArgumentError OpenInterval(i1) ∪ i5 + @test_throws ArgumentError i1 ∪ OpenInterval(i5) + @test_throws ArgumentError Interval{:closed,:open}(i1) ∪ i5 + @test_throws ArgumentError Interval{:closed,:open}(i1) ∪ OpenInterval(i5) + + # - intersection of non-overlapping intervals + @test isempty(i1 ∩ i4) + @test isempty(i4 ∩ i1) + @test isempty(OpenInterval(i1) ∩ i4) + @test isempty(i1 ∩ OpenInterval(i4)) + @test isempty(Interval{:closed,:open}(i1) ∩ i4) + + + # - intersection of almost-overlapping intervals + @test isempty(i1 ∩ i5) + @test isempty(i5 ∩ i1) + @test isempty(OpenInterval(i1) ∩ i5) + @test isempty(i1 ∩ OpenInterval(i5)) + @test isempty(Interval{:closed,:open}(i1) ∩ i5) + + # - union of interval with empty + @test i1 ∪ i_empty ≡ i_empty ∪ i1 ≡ i1 + @test Interval{:open,:closed}(i1) ∪ Interval{:open,:closed}(i_empty) ≡ + Interval{:open,:closed}(i_empty) ∪ Interval{:open,:closed}(i1) ≡ Interval{:open,:closed}(i1) + @test Interval{:closed,:open}(i1) ∪ Interval{:closed,:open}(i_empty) ≡ + Interval{:closed,:open}(i_empty) ∪ Interval{:closed,:open}(i1) ≡ Interval{:closed,:open}(i1) + @test OpenInterval(i1) ∪ OpenInterval(i_empty) ≡ + OpenInterval(i_empty) ∪ OpenInterval(i1) ≡ OpenInterval(i1) + @test i1 ∪ Interval{:open,:closed}(i_empty) ≡ Interval{:open,:closed}(i_empty) ∪ i1 ≡ i1 + @test i1 ∪ Interval{:closed,:open}(i_empty) ≡ Interval{:closed,:open}(i_empty) ∪ i1 ≡ i1 + @test i1 ∪ OpenInterval(i_empty) ≡ OpenInterval(i_empty) ∪ i1 ≡ i1 + @test OpenInterval(i1) ∪ i_empty ≡ i_empty ∪ OpenInterval(i1) ≡ OpenInterval(i1) + @test OpenInterval(i1) ∪ Interval{:open,:closed}(i_empty) ≡ Interval{:open,:closed}(i_empty) ∪ OpenInterval(i1) ≡ OpenInterval(i1) + @test OpenInterval(i1) ∪ Interval{:closed,:open}(i_empty) ≡ Interval{:closed,:open}(i_empty) ∪ OpenInterval(i1) ≡ OpenInterval(i1) + @test Interval{:open,:closed}(i1) ∪ OpenInterval(i_empty) ≡ OpenInterval(i_empty) ∪ Interval{:open,:closed}(i1) ≡ Interval{:open,:closed}(i1) + @test Interval{:open,:closed}(i1) ∪ Interval{:closed,:open}(i_empty) ≡ Interval{:closed,:open}(i_empty) ∪ Interval{:open,:closed}(i1) ≡ Interval{:open,:closed}(i1) + + # - intersection of interval with empty + @test isempty(i1 ∩ i_empty) + @test isempty(i_empty ∩ i1) + @test isempty(OpenInterval(i1) ∩ i_empty) + @test isempty(i1 ∩ OpenInterval(i_empty)) + @test isempty(Interval{:closed,:open}(i1) ∩ i_empty) + + # - test matching endpoints + @test (0..1) ∪ OpenInterval(0..1) ≡ OpenInterval(0..1) ∪ (0..1) ≡ 0..1 + @test Interval{:open,:closed}(0..1) ∪ OpenInterval(0..1) ≡ + OpenInterval(0..1) ∪ Interval{:open,:closed}(0..1) ≡ + Interval{:open,:closed}(0..1) + @test Interval{:closed,:open}(0..1) ∪ OpenInterval(0..1) ≡ + OpenInterval(0..1) ∪ Interval{:closed,:open}(0..1) ≡ + Interval{:closed,:open}(0..1) + end + end + + @testset "Empty" begin + for T in (Float32,Float64) + @test isempty(Interval{:open,:open}(zero(T),zero(T))) + @test zero(T) ∉ Interval{:open,:open}(zero(T),zero(T)) + @test isempty(Interval{:open,:closed}(zero(T),zero(T))) + @test zero(T) ∉ Interval{:open,:closed}(zero(T),zero(T)) + @test isempty(Interval{:closed,:open}(zero(T),zero(T))) + @test zero(T) ∉ Interval{:closed,:open}(zero(T),zero(T)) + end + end + + @testset "Custom intervals" begin + I = MyUnitInterval(true,true) + @test leftendpoint(I) == 0 + @test rightendpoint(I) == 1 + @test isleftclosed(I) == true + @test isrightclosed(I) == true + @test ClosedInterval(I) === convert(ClosedInterval, I) === + ClosedInterval{Int}(I) === convert(ClosedInterval{Int}, I) === + convert(Interval, I) === Interval(I) === 0..1 + @test_throws InexactError convert(OpenInterval, I) + I = MyUnitInterval(false,false) + @test leftendpoint(I) == 0 + @test rightendpoint(I) == 1 + @test isleftclosed(I) == false + @test isrightclosed(I) == false + @test OpenInterval(I) === convert(OpenInterval, I) === + OpenInterval{Int}(I) === convert(OpenInterval{Int}, I) === + convert(Interval, I) === Interval(I) === OpenInterval(0..1) + I = MyUnitInterval(false,true) + @test leftendpoint(I) == 0 + @test rightendpoint(I) == 1 + @test isleftclosed(I) == false + @test isrightclosed(I) == true + @test Interval{:open,:closed}(I) === convert(Interval{:open,:closed}, I) === + Interval{:open,:closed,Int}(I) === convert(Interval{:open,:closed,Int}, I) === + convert(Interval, I) === Interval(I) === Interval{:open,:closed}(0..1) + I = MyUnitInterval(true,false) + @test leftendpoint(I) == 0 + @test rightendpoint(I) == 1 + @test isleftclosed(I) == true + @test isrightclosed(I) == false + @test Interval{:closed,:open}(I) === convert(Interval{:closed,:open}, I) === + Interval{:closed,:open,Int}(I) === convert(Interval{:closed,:open,Int}, I) === + convert(Interval, I) === Interval(I) === Interval{:closed,:open}(0..1) + end + + @testset "Custom typed endpoints interval" begin + I = MyClosedUnitInterval() + @test leftendpoint(I) == 0 + @test rightendpoint(I) == 1 + @test isleftclosed(I) == true + @test isrightclosed(I) == true + @test ClosedInterval(I) === convert(ClosedInterval, I) === + ClosedInterval{Int}(I) === convert(ClosedInterval{Int}, I) === + convert(Interval, I) === Interval(I) === 0..1 + @test_throws InexactError convert(OpenInterval, I) + end + + @testset "issubset" begin + @test issubset(0.1, 0.0..1.0) == true + @test issubset(0.0, 0.0..1.0) == true + @test issubset(1.1, 0.0..1.0) == false + @test issubset(0.0, nextfloat(0.0)..1.0) == false + end +end