Skip to content

Commit

Permalink
Add open and mixed open/closed intervals (#26)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
dlfivefifty authored Sep 1, 2018
1 parent 3128af5 commit a0983f1
Show file tree
Hide file tree
Showing 6 changed files with 951 additions and 166 deletions.
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<TAB>` (using Julia's LaTeX
syntax tab-completion).

Expand All @@ -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<TAB>
3..5

Expand Down
2 changes: 1 addition & 1 deletion REQUIRE
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
julia 0.6
Compat 0.49
Compat 1.0
191 changes: 179 additions & 12 deletions src/IntervalSets.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)...)
Expand All @@ -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
119 changes: 0 additions & 119 deletions src/closed.jl

This file was deleted.

Loading

0 comments on commit a0983f1

Please sign in to comment.