Skip to content

Interval with independent endpoint types #122

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "IntervalSets"
uuid = "8197267c-284f-5f27-9208-e0e47529a953"
version = "0.7.3"
version = "0.8.0"

[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -32,7 +32,7 @@ You can construct `ClosedInterval`s in a variety of ways:
julia> using IntervalSets

julia> ClosedInterval{Float64}(1,3)
1.0..3.0
1..3

julia> 0.5..2.5
0.5..2.5
@@ -44,7 +44,7 @@ julia> 1.5±1
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)
1..3 (open)

julia> OpenInterval(0.5..2.5)
0.5..2.5 (open)
23 changes: 15 additions & 8 deletions src/IntervalSets.jl
Original file line number Diff line number Diff line change
@@ -18,10 +18,20 @@ export AbstractInterval, Interval, OpenInterval, ClosedInterval,
searchsorted_interval

"""
A subtype of `Domain{T}` represents a subset of type `T`, that provides `in`.
A subtype of `Domain{T}` represents a set that provides `in`. `T` is a type suitable for representing elements in the domain.
"""
abstract type Domain{T} end

"""
eltype(::Domain{T})
eltype(::Type{<:Domain{T}})

Return `T`. The `eltype`, `T`, of a `Domain` is the type that best represents elements of the domain according to the criteria chosen by the programmer who created the domain.

Note: Objects of other types may be in the domain (as determined by the `in` function) and there may not be a unique object of type `T` for each mathematical element in the domain (e.g. a real interval may be represented by a `Domain{Float64}`, but there there are not unique `Float64`s for each real number in the interval).
"""
Base.eltype(::Type{<:Domain{T}}) where T = T

Base.IteratorSize(::Type{<:Domain}) = Base.SizeUnknown()
Base.isdisjoint(a::Domain, b::Domain) = isempty(a ∩ b)

@@ -64,20 +74,17 @@ isclosedset(d::AbstractInterval) = isleftclosed(d) && isrightclosed(d)
"Is the interval open?"
isopenset(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{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)...)

checked_conversion(::Type{T}, a, b) where {T} = _checked_conversion(T, convert(T, a), convert(T, b))
_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"))
default_interval_eltype(left, right) = default_interval_eltype(typeof(left), typeof(right))
default_interval_eltype(TL::Type, TR::Type) = default_interval_eltype(promote_type(TL, TR))
default_interval_eltype(T::Type) = T
default_interval_eltype(T::Type{<:Number}) = float(T)

function infimum(d::AbstractInterval{T}) where T
a = leftendpoint(d)
43 changes: 26 additions & 17 deletions src/interval.jl
Original file line number Diff line number Diff line change
@@ -6,29 +6,33 @@ is an interval set containg `x` such that
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))
struct Interval{L,R,T,TL,TR} <: TypedEndpointsInterval{L,R,T}
left::TL
right::TR
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}
const ClosedInterval{T,TL,TR} = Interval{:closed,:closed,T,TL,TR}

"""
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}
const OpenInterval{T,TL,TR} = Interval{:open,:open,T,TL,TR}

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(left,right)...)
Interval{L,R}(left::T, right::T) where {L,R,T} = Interval{L,R,T}(left, right)
Interval{L,R,T,TL,TR}(i::AbstractInterval) where {L,R,T,TL,TR} = Interval{L,R,T,TL,TR}(endpoints(i)...)
Interval{L,R,T}(l, r) where {L,R,T} = Interval{L,R,T,typeof(l),typeof(r)}(l, r)
function Interval{L,R}(left, right) where {L,R}
T = default_interval_eltype(left, right)
if T == Any
error("Endpoints ($left, $right) of Interval were incompatible (inferred eltype was Any).")
end
Interval{L,R,T}(left, right)
end
Interval(left, right) = ClosedInterval(left, right)


@@ -94,10 +98,15 @@ Construct a ClosedInterval `iv` spanning the region from
±(x, y) = ClosedInterval(x - y, x + y)
±(x::CartesianIndex, y::CartesianIndex) = ClosedInterval(x-y, x+y)

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)")
function show(io::IO, I::ClosedInterval)
print(io, leftendpoint(I), "..", rightendpoint(I))
if eltype(I) != default_interval_eltype(leftendpoint(I), rightendpoint(I))
print(io, " (", eltype(I), ")")
end
end
show(io::IO, I::OpenInterval) = print(io, leftendpoint(I), "..", rightendpoint(I), " (", eltype(I), ", open)")
show(io::IO, I::Interval{:open,:closed}) = print(io, leftendpoint(I), "..", rightendpoint(I), " (", eltype(I), ", open–closed)")
show(io::IO, I::Interval{:closed,:open}) = print(io, leftendpoint(I), "..", rightendpoint(I), " (", eltype(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)
@@ -159,18 +168,18 @@ function _union(A::TypedEndpointsInterval{L1,R1}, B::TypedEndpointsInterval{L2,R
end

# random sampling from interval
Random.gentype(::Type{Interval{L,R,T}}) where {L,R,T} = float(T)
Random.gentype(::Type{Interval{L,R,T}}) where {L,R,T} = T
function Random.rand(rng::AbstractRNG, i::Random.SamplerTrivial{<:TypedEndpointsInterval{:closed, :closed, T}}) where T<:Real
_i = i[]
isempty(_i) && throw(ArgumentError("The interval should be non-empty."))
a,b = endpoints(_i)
t = rand(rng, float(T)) # technically this samples from [0, 1), but we still allow it with TypedEndpointsInterval{:closed, :closed} for convenience
return clamp(t*a+(1-t)*b, _i)
return convert(T, clamp(t*a+(1-t)*b, _i))
end

ClosedInterval{T}(i::AbstractUnitRange{I}) where {T,I<:Integer} = ClosedInterval{T}(minimum(i), maximum(i))
ClosedInterval(i::AbstractUnitRange{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.promote_rule(::Type{Interval{L,R,T1,TL1,TR1}}, ::Type{Interval{L,R,T2,TL2,TR2}}) where {L,R,T1,T2,TL1,TR1,TL2,TR2} = Interval{L,R,promote_type(T1, T2),promote_type(TL1,TL2),promote_type(TR1,TR2)}

float(i::Interval{L, R, T}) where {L,R,T} = Interval{L, R, float(T)}(endpoints(i)...)
103 changes: 53 additions & 50 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -31,30 +31,33 @@ struct IncompleteInterval <: AbstractInterval{Int} end

@testset "Basic Closed Sets" begin
@test_throws ErrorException :a .. "b"
@test_throws ErrorException 1 .. missing
@test_throws ErrorException 1u"m" .. 2u"s"
@test ismissing(2 in 1 .. missing)
@test !(0 in 1 .. missing)
@test_throws Exception 1u"m" in 1u"m" .. 2u"s"
I = 0..3
@test I === ClosedInterval(0,3) === ClosedInterval{Int}(0,3) ===
@test I === ClosedInterval(0,3) === ClosedInterval{Float64}(0,3) ===
Interval(0,3)
@test string(I) == "0..3"
@test @inferred(UnitRange(I)) === 0:3
@test @inferred(range(I)) === 0: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
@test @inferred(ClosedInterval(Base.OneTo(3))) === 1..3

II = ClosedInterval{Int}(0,3)
@test @inferred(UnitRange(II)) === 0:3
@test @inferred(range(II)) === 0:3
@test @inferred(UnitRange{Int16}(II)) === Int16(0):Int16(3)
@test @inferred(ClosedInterval(0:3)) === II
@test @inferred(ClosedInterval{Float64}(0:3)) === 0..3
@test @inferred(ClosedInterval(Base.OneTo(3))) === ClosedInterval{Int}(1,3)
J = 3..2
K = 5..4
L = 3 ± 2
M = @inferred(ClosedInterval(2, 5.0))
@test string(M) == "2.0..5.0"
@test string(M) == "2..5.0"
N = @inferred(ClosedInterval(UInt8(255), 300))

x, y = CartesianIndex(1, 2, 3, 4), CartesianIndex(1, 2, 3, 4)
O = @inferred x±y
@test O == ClosedInterval(x-y, x+y)

@test eltype(I) == Int
@test eltype(I) == Float64
@test eltype(M) == Float64

@test !isempty(I)
@@ -67,10 +70,10 @@ struct IncompleteInterval <: AbstractInterval{Int} end
@test isequal(I, I)
@test isequal(J, K)

@test typeof(leftendpoint(M)) == typeof(rightendpoint(M)) && typeof(leftendpoint(M)) == Float64
@test typeof(leftendpoint(N)) == typeof(rightendpoint(N)) && typeof(leftendpoint(N)) == Int
@test @inferred(endpoints(M)) === (2.0,5.0)
@test @inferred(endpoints(N)) === (255,300)
@test typeof(leftendpoint(M)) == Int && typeof(rightendpoint(M)) == Float64
@test typeof(leftendpoint(N)) == UInt8 && typeof(rightendpoint(N)) == Int
@test @inferred(endpoints(M)) === (2,5.0)
@test @inferred(endpoints(N)) === (UInt8(255),300)

@test maximum(I) === 3
@test minimum(I) === 0
@@ -157,42 +160,42 @@ struct IncompleteInterval <: AbstractInterval{Int} end
@inferred(convert(Domain{Float64}, I)) ===
@inferred(ClosedInterval{Float64}(I)) ===
@inferred(convert(TypedEndpointsInterval{:closed,:closed,Float64},I)) ===
0.0..3.0
0..3
@test @inferred(convert(ClosedInterval, I)) ===
@inferred(convert(Interval, I)) ===
@inferred(ClosedInterval(I)) ===
@inferred(Interval(I)) ===
@inferred(convert(AbstractInterval, I)) ===
@inferred(convert(Domain, I)) ===
@inferred(convert(TypedEndpointsInterval{:closed,:closed}, I)) ===
@inferred(convert(TypedEndpointsInterval{:closed,:closed,Int}, I)) ===
@inferred(convert(ClosedInterval{Int}, I)) === I
@inferred(convert(TypedEndpointsInterval{:closed,:closed,Float64}, I)) ===
@inferred(convert(ClosedInterval{Float64}, 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 !(convert(ClosedInterval{Int}, I) === 0..3)
@test ClosedInterval{Float64, Float64, 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 ClosedInterval{Float64,Int,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)
@inferred(OpenInterval{Float64}(J)) === OpenInterval(0..3)
@test @inferred(convert(OpenInterval, J)) ===
@inferred(convert(Interval, J)) ===
@inferred(convert(AbstractInterval, J)) ===
@inferred(convert(Domain, J)) ===
@inferred(OpenInterval(J)) ===
@inferred(OpenInterval{Int}(J)) ===
@inferred(convert(OpenInterval{Int},J)) === OpenInterval(J)
@inferred(OpenInterval{Float64}(J)) ===
@inferred(convert(OpenInterval{Float64},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)
@inferred(Interval{:open,:closed,Float64}(J)) === Interval{:open,:closed}(0..3)
@test @inferred(convert(Interval{:open,:closed}, J)) ===
@inferred(convert(Interval, J)) ===
@inferred(convert(AbstractInterval, J)) ===
@@ -203,18 +206,16 @@ struct IncompleteInterval <: AbstractInterval{Int} end
@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)
@inferred(Interval{:closed,:open,Float64}(J)) === Interval{:closed,:open}(0..3)
@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 1.0..2.0 === ClosedInterval{Float64}(1.0..2.0) === Interval(1.0,2.0)

@test promote_type(Interval{:closed,:open,Float64}, Interval{:closed,:open,Int}) ===
Interval{:closed,:open,Float64}
@test promote_type(Interval{:closed,:open,Float64,Float64,Int}, Interval{:closed,:open,Int,Int,Int}) === Interval{:closed,:open,Float64,Float64,Int}
end


@@ -612,8 +613,8 @@ struct IncompleteInterval <: AbstractInterval{Int} end
Interval{:closed,:open}(0..1)

# - different interval types
@test (1..2) OpenInterval(0.5, 1.5) Interval{:closed, :open}(1, 1.5)
@test (1..2) OpenInterval(0.5, 1.5) Interval{:open, :closed}(0.5, 2)
@test (1..2) OpenInterval(0.5, 1.5) == Interval{:closed, :open}(1, 1.5)
@test (1..2) OpenInterval(0.5, 1.5) == Interval{:open, :closed}(0.5, 2)
end
end

@@ -638,7 +639,7 @@ struct IncompleteInterval <: AbstractInterval{Int} end
@test isrightclosed(I)
@test !isrightopen(I)
@test ClosedInterval(I) === convert(ClosedInterval, I) ===
ClosedInterval{Int}(I) === convert(ClosedInterval{Int}, I) ===
ClosedInterval{Float64}(I) === convert(ClosedInterval{Float64}, I) ===
convert(Interval, I) === Interval(I) === 0..1
@test_throws InexactError convert(OpenInterval, I)
I = MyUnitInterval(false,false)
@@ -647,23 +648,23 @@ struct IncompleteInterval <: AbstractInterval{Int} end
@test !isleftclosed(I)
@test !isrightclosed(I)
@test OpenInterval(I) === convert(OpenInterval, I) ===
OpenInterval{Int}(I) === convert(OpenInterval{Int}, I) ===
OpenInterval{Float64}(I) === convert(OpenInterval{Float64}, 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) ===
Interval{:open,:closed,Float64}(I) === convert(Interval{:open,:closed,Float64}, 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) ===
Interval{:closed,:open,Float64}(I) === convert(Interval{:closed,:open,Float64}, I) ===
convert(Interval, I) === Interval(I) === Interval{:closed,:open}(0..1)
@test convert(AbstractInterval, I) === convert(AbstractInterval{Int}, I) === I
end
@@ -675,7 +676,7 @@ struct IncompleteInterval <: AbstractInterval{Int} end
@test isleftclosed(I) == true
@test isrightclosed(I) == true
@test ClosedInterval(I) === convert(ClosedInterval, I) ===
ClosedInterval{Int}(I) === convert(ClosedInterval{Int}, I) ===
ClosedInterval{Float64}(I) === convert(ClosedInterval{Float64}, I) ===
convert(Interval, I) === Interval(I) === 0..1
@test_throws InexactError convert(OpenInterval, I)
@test I I === 0..1
@@ -735,10 +736,12 @@ struct IncompleteInterval <: AbstractInterval{Int} end
end

@testset "OneTo" begin
@test_throws ArgumentError Base.OneTo{Int}(0..5)
@test_throws ArgumentError Base.OneTo(0..5)
@test Base.OneTo(1..5) == Base.OneTo{Int}(1..5) == Base.OneTo(5)
@test Base.Slice(1..5) == Base.Slice{UnitRange{Int}}(1..5) == Base.Slice(1:5)
@test_throws MethodError Base.OneTo{Int}(0..5)
@test_throws MethodError Base.OneTo(0..5)
@test_throws ArgumentError Base.OneTo{Int}(ClosedInterval{Int}(0..5))
@test_throws ArgumentError Base.OneTo(ClosedInterval{Int}(0..5))
@test Base.OneTo(ClosedInterval{Int}(1..5)) == Base.OneTo{Int}(ClosedInterval{Int}(1..5)) == Base.OneTo(5)
@test Base.Slice(ClosedInterval{Int}(1..5)) == Base.Slice{UnitRange{Int}}(ClosedInterval{Int}(1..5)) == Base.Slice(1:5)
end

@testset "range" begin
@@ -772,12 +775,12 @@ struct IncompleteInterval <: AbstractInterval{Int} end
end

@testset "rand" begin
@test rand(1..2) isa Float64
@test rand(1..2.) isa Float64
@test rand(1..big(2)) isa BigFloat
@test rand(1..(3//2)) isa Float64
@test rand(Int32(1)..Int32(2)) isa Float64
@test rand(Float32(1)..Float32(2)) isa Float32
@test rand(1..2)::Float64 in 1..2
@test rand(1..2.)::Float64 in 1..2
@test rand(1..big(2))::BigFloat in 1..2
@test rand(1..(3//2))::Float64 in 1..3/2
@test rand(Int32(1)..Int32(2))::Float64 in 1..2
@test rand(Float32(1)..Float32(2))::Float32 in 1..2
@test_throws ArgumentError rand(2..1)

i1 = 1..2
@@ -816,15 +819,15 @@ struct IncompleteInterval <: AbstractInterval{Int} end
end

@testset "float" begin
i1 = 1..2
i1 = ClosedInterval{Int}(1..2)
@test i1 isa ClosedInterval{Int}
@test float(i1) isa ClosedInterval{Float64}
@test float(i1) == i1
i2 = big(1)..2
i2 = ClosedInterval{BigInt}(big(1)..2)
@test i2 isa ClosedInterval{BigInt}
@test float(i2) isa ClosedInterval{BigFloat}
@test float(i2) == i2
i3 = OpenInterval(1,2)
i3 = OpenInterval{Int}(1,2)
@test i3 isa OpenInterval{Int}
@test float(i3) isa OpenInterval{Float64}
@test float(i3) == i3