Skip to content

Commit a0983f1

Browse files
authored
Add open and mixed open/closed intervals (#26)
* 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
1 parent 3128af5 commit a0983f1

File tree

6 files changed

+951
-166
lines changed

6 files changed

+951
-166
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,18 @@ julia> 1.5±1
3838
0.5..2.5
3939
```
4040

41+
Similarly, you can construct `OpenInterval`s and `Interval{:open,:closed}`s, and `Interval{:closed,:open}`:
42+
```julia
43+
julia> OpenInterval{Float64}(1,3)
44+
1.0..3.0 (open)
45+
46+
julia> OpenInterval(0.5..2.5)
47+
0.5..2.5 (open)
48+
49+
julia> Interval{:open,:closed}(1,3)
50+
1..3 (open–closed)
51+
```
52+
4153
The `±` operator may be typed as `\pm<TAB>` (using Julia's LaTeX
4254
syntax tab-completion).
4355

@@ -50,6 +62,9 @@ true
5062
julia> 0 1.5±1
5163
false
5264

65+
julia> 1 OpenInterval(0..1)
66+
false
67+
5368
julia> intersect(1..5, 3..7) # can also use `a ∩ b`, where the symbol is \cap<TAB>
5469
3..5
5570

REQUIRE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
julia 0.6
2-
Compat 0.49
2+
Compat 1.0

src/IntervalSets.jl

Lines changed: 179 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,27 +6,65 @@ using Base: @pure
66
import Base: eltype, convert, show, in, length, isempty, isequal, issubset, ==, hash,
77
union, intersect, minimum, maximum, extrema, range,
88

9+
using Compat.Statistics
10+
import Compat.Statistics: mean
11+
12+
913
using Compat
1014
using Compat.Dates
1115

12-
export AbstractInterval, ClosedInterval, , .., ±, ordered, width
16+
export AbstractInterval, Interval, OpenInterval, ClosedInterval,
17+
, .., ±, ordered, width, duration, leftendpoint, rightendpoint, endpoints,
18+
isclosed, isleftclosed, isrightclosed, isleftopen, isrightopen, closedendpoints,
19+
infimum, supremum
20+
21+
"""
22+
A subtype of `Domain{T}` represents a subset of type `T`, that provides `in`.
23+
"""
24+
abstract type Domain{T} end
25+
"""
26+
A subtype of `AbstractInterval{T}` represents an interval subset of type `T`, that provides
27+
`endpoints`, `closedendpoints`.
28+
"""
29+
abstract type AbstractInterval{T} <: Domain{T} end
30+
31+
32+
"A tuple containing the left and right endpoints of the interval."
33+
endpoints(d::AI) where AI<:AbstractInterval = error("Override endpoints(::$(AI))")
34+
35+
"The left endpoint of the interval."
36+
leftendpoint(d::AbstractInterval) = endpoints(d)[1]
37+
38+
"The right endpoint of the interval."
39+
rightendpoint(d::AbstractInterval) = endpoints(d)[2]
40+
41+
"A tuple of `Bool`'s encoding whether the left/right endpoints are closed."
42+
closedendpoints(d::AI) where AI<:AbstractInterval = error("Override closedendpoints(::$(AI))")
43+
44+
"Is the interval closed at the left endpoint?"
45+
isleftclosed(d::AbstractInterval) = closedendpoints(d)[1]
46+
47+
"Is the interval closed at the right endpoint?"
48+
isrightclosed(d::AbstractInterval) = closedendpoints(d)[2]
49+
50+
# open_left and open_right are implemented in terms of closed_* above, so those
51+
# are the only ones that should be implemented for specific intervals
52+
"Is the interval open at the left endpoint?"
53+
isleftopen(d::AbstractInterval) = !isleftclosed(d)
1354

14-
abstract type AbstractInterval{T} end
55+
"Is the interval open at the right endpoint?"
56+
isrightopen(d::AbstractInterval) = !isrightclosed(d)
1557

16-
include("closed.jl")
58+
# Only closed if closed at both endpoints, and similar for open
59+
isclosed(d::AbstractInterval) = isleftclosed(d) && isrightclosed(d)
60+
isopen(d::AbstractInterval) = isleftopen(d) && isrightopen(d)
1761

1862
eltype(::Type{AbstractInterval{T}}) where {T} = T
1963
@pure eltype(::Type{I}) where {I<:AbstractInterval} = eltype(supertype(I))
2064

21-
convert(::Type{I}, i::I) where {I<:AbstractInterval} = i
22-
function convert(::Type{I}, i::AbstractInterval) where I<:AbstractInterval
23-
T = eltype(I)
24-
I(convert(T, i.left), convert(T, i.right))
25-
end
26-
function convert(::Type{I}, r::AbstractRange) where I<:AbstractInterval
27-
T = eltype(I)
28-
I(convert(T, minimum(r)), convert(T, maximum(r)))
29-
end
65+
convert(::Type{AbstractInterval}, i::AbstractInterval) = i
66+
convert(::Type{AbstractInterval{T}}, i::AbstractInterval{T}) where T = i
67+
3068

3169
ordered(a::T, b::T) where {T} = ifelse(a < b, (a, b), (b, a))
3270
ordered(a, b) = ordered(promote(a, b)...)
@@ -36,4 +74,133 @@ _checked_conversion(::Type{T}, a::T, b::T) where {T} = a, b
3674
_checked_conversion(::Type{Any}, a, b) = throw(ArgumentError("$a and $b promoted to type Any"))
3775
_checked_conversion(::Type{T}, a, b) where {T} = throw(ArgumentError("$a and $b are not both of type $T"))
3876

77+
function infimum(d::AbstractInterval{T}) where T
78+
a = leftendpoint(d)
79+
b = rightendpoint(d)
80+
a > b && throw(ArgumentError("Infimum not defined for empty intervals"))
81+
a
82+
end
83+
84+
function supremum(d::AbstractInterval{T}) where T
85+
a = leftendpoint(d)
86+
b = rightendpoint(d)
87+
a > b && throw(ArgumentError("Supremum not defined for empty intervals"))
88+
b
89+
end
90+
91+
mean(d::AbstractInterval) = (leftendpoint(d) + rightendpoint(d))/2
92+
93+
issubset(A::AbstractInterval, B::AbstractInterval) = ((leftendpoint(A) in B) && (rightendpoint(A) in B)) || isempty(A)
94+
(A::AbstractInterval, B::AbstractInterval) = issubset(B, A)
95+
if VERSION < v"1.1.0-DEV.123"
96+
issubset(x, B::AbstractInterval) = issubset(convert(AbstractInterval, x), B)
97+
end
98+
99+
"""
100+
w = width(iv)
101+
102+
Calculate the width (max-min) of interval `iv`. Note that for integers
103+
`l` and `r`, `width(l..r) = length(l:r) - 1`.
104+
"""
105+
function width(A::AbstractInterval)
106+
_width = rightendpoint(A) - leftendpoint(A)
107+
max(zero(_width), _width) # this works when T is a Date
108+
end
109+
110+
"""
111+
A subtype of `TypedEndpointsInterval{L,R,T}` where `L` and `R` are `:open` or `:closed`,
112+
that represents an interval subset of type `T`, and provides `endpoints`.
113+
"""
114+
abstract type TypedEndpointsInterval{L,R,T} <: AbstractInterval{T} end
115+
116+
closedendpoints(d::TypedEndpointsInterval{:closed,:closed}) = (true,true)
117+
closedendpoints(d::TypedEndpointsInterval{:closed,:open}) = (true,false)
118+
closedendpoints(d::TypedEndpointsInterval{:open,:closed}) = (false,true)
119+
closedendpoints(d::TypedEndpointsInterval{:open,:open}) = (false,false)
120+
121+
122+
in(v, I::TypedEndpointsInterval{:closed,:closed}) = leftendpoint(I) v rightendpoint(I)
123+
in(v, I::TypedEndpointsInterval{:open,:open}) = leftendpoint(I) < v < rightendpoint(I)
124+
in(v, I::TypedEndpointsInterval{:closed,:open}) = leftendpoint(I) v < rightendpoint(I)
125+
in(v, I::TypedEndpointsInterval{:open,:closed}) = leftendpoint(I) < v rightendpoint(I)
126+
127+
in(a::AbstractInterval, b::TypedEndpointsInterval{:closed,:closed}) =
128+
(leftendpoint(a) leftendpoint(b)) & (rightendpoint(a) rightendpoint(b))
129+
in(a::TypedEndpointsInterval{:open,:open}, b::TypedEndpointsInterval{:open,:open}) =
130+
(leftendpoint(a) leftendpoint(b)) & (rightendpoint(a) rightendpoint(b))
131+
in(a::TypedEndpointsInterval{:closed,:open}, b::TypedEndpointsInterval{:open,:open}) =
132+
(leftendpoint(a) > leftendpoint(b)) & (rightendpoint(a) rightendpoint(b))
133+
in(a::TypedEndpointsInterval{:open,:closed}, b::TypedEndpointsInterval{:open,:open}) =
134+
(leftendpoint(a) leftendpoint(b)) & (rightendpoint(a) < rightendpoint(b))
135+
in(a::TypedEndpointsInterval{:closed,:closed}, b::TypedEndpointsInterval{:open,:open}) =
136+
(leftendpoint(a) > leftendpoint(b)) & (rightendpoint(a) < rightendpoint(b))
137+
in(a::TypedEndpointsInterval{:closed}, b::TypedEndpointsInterval{:open,:closed}) =
138+
(leftendpoint(a) > leftendpoint(b)) & (rightendpoint(a) rightendpoint(b))
139+
in(a::TypedEndpointsInterval{:open}, b::TypedEndpointsInterval{:open,:closed}) =
140+
(leftendpoint(a) leftendpoint(b)) & (rightendpoint(a) rightendpoint(b))
141+
in(a::TypedEndpointsInterval{L,:closed}, b::TypedEndpointsInterval{:closed,:open}) where L = (leftendpoint(a) leftendpoint(b)) & (rightendpoint(a) < rightendpoint(b))
142+
in(a::TypedEndpointsInterval{L,:open}, b::TypedEndpointsInterval{:closed,:open}) where L = (leftendpoint(a) leftendpoint(b)) & (rightendpoint(a) rightendpoint(b))
143+
144+
isempty(A::TypedEndpointsInterval{:closed,:closed}) = leftendpoint(A) > rightendpoint(A)
145+
isempty(A::TypedEndpointsInterval) = leftendpoint(A) rightendpoint(A)
146+
147+
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))
148+
isequal(A::TypedEndpointsInterval, B::TypedEndpointsInterval) = isempty(A) & isempty(B)
149+
150+
==(A::TypedEndpointsInterval{L,R}, B::TypedEndpointsInterval{L,R}) where {L,R} = (leftendpoint(A) == leftendpoint(B) && rightendpoint(A) == rightendpoint(B)) || (isempty(A) && isempty(B))
151+
==(A::TypedEndpointsInterval, B::TypedEndpointsInterval) = isempty(A) && isempty(B)
152+
153+
const _interval_hash = UInt == UInt64 ? 0x1588c274e0a33ad4 : 0x1e3f7252
154+
155+
hash(I::TypedEndpointsInterval, h::UInt) = hash(leftendpoint(I), hash(rightendpoint(I), hash(_interval_hash, h)))
156+
157+
minimum(d::TypedEndpointsInterval{:closed}) = infimum(d)
158+
minimum(d::TypedEndpointsInterval{:open}) = throw(ArgumentError("$d is open on the left. Use infimum."))
159+
maximum(d::TypedEndpointsInterval{L,:closed}) where L = supremum(d)
160+
maximum(d::TypedEndpointsInterval{L,:open}) where L = throw(ArgumentError("$d is open on the right. Use supremum."))
161+
162+
extrema(I::TypedEndpointsInterval) = (infimum(I), supremum(I))
163+
164+
# Open and closed at endpoints
165+
isleftclosed(d::TypedEndpointsInterval{:closed}) = true
166+
isleftclosed(d::TypedEndpointsInterval{:open}) = false
167+
isrightclosed(d::TypedEndpointsInterval{L,:closed}) where {L} = true
168+
isrightclosed(d::TypedEndpointsInterval{L,:open}) where {L} = false
169+
170+
# UnitRange construction
171+
# The third is the one we want, but the first two are needed to resolve ambiguities
172+
Base.Slice{T}(i::TypedEndpointsInterval{:closed,:closed,I}) where {T<:AbstractUnitRange,I<:Integer} =
173+
Base.Slice{T}(minimum(i):maximum(i))
174+
function Base.OneTo{T}(i::TypedEndpointsInterval{:closed,:closed,I}) where {T<:Integer,I<:Integer}
175+
@noinline throwstart(i) = throw(ArgumentError("smallest element must be 1, got $(minimum(i))"))
176+
minimum(i) == 1 || throwstart(i)
177+
Base.OneTo{T}(maximum(i))
178+
end
179+
UnitRange{T}(i::TypedEndpointsInterval{:closed,:closed,I}) where {T<:Integer,I<:Integer} = UnitRange{T}(minimum(i), maximum(i))
180+
UnitRange(i::TypedEndpointsInterval{:closed,:closed,I}) where {I<:Integer} = UnitRange{I}(i)
181+
range(i::TypedEndpointsInterval{:closed,:closed,I}) where {I<:Integer} = UnitRange{I}(i)
182+
183+
"""
184+
duration(iv)
185+
186+
calculates the the total number of integers or dates of an integer or date
187+
valued interval. For example, `duration(0..1)` is 2, while `width(0..1)` is 1.
188+
"""
189+
duration(A::TypedEndpointsInterval{:closed,:closed,T}) where {T<:Integer} = max(0, Int(A.right - A.left) + 1)
190+
duration(A::TypedEndpointsInterval{:closed,:closed,Date}) = max(0, Dates.days(A.right - A.left) + 1)
191+
192+
include("interval.jl")
193+
194+
# convert numbers to intervals
195+
convert(::Type{AbstractInterval}, x::Number) = x..x
196+
convert(::Type{AbstractInterval{T}}, x::Number) where T =
197+
convert(AbstractInterval{T}, convert(AbstractInterval, x))
198+
convert(::Type{TypedEndpointsInterval{:closed,:closed}}, x::Number) = x..x
199+
convert(::Type{TypedEndpointsInterval{:closed,:closed,T}}, x::Number) where T =
200+
convert(AbstractInterval{T}, convert(AbstractInterval, x))
201+
convert(::Type{ClosedInterval}, x::Number) = x..x
202+
convert(::Type{ClosedInterval{T}}, x::Number) where T =
203+
convert(AbstractInterval{T}, convert(AbstractInterval, x))
204+
205+
39206
end # module

src/closed.jl

Lines changed: 0 additions & 119 deletions
This file was deleted.

0 commit comments

Comments
 (0)