Intervals

This package defines:

  • AbstractInterval, along with its subtypes:
    • Interval{T,L,R}, which represents a non-iterable range between two endpoints of type T with left/right bounds types respectively being L and R
    • AnchoredInterval{P,T,L,R}, which represents a non-iterable range defined by a single value anchor::T and the value type P which represents the span of the range. Left/right bounds types are specifed by L and R respectively
      • HourEnding, a type alias for AnchoredInterval{Hour(-1)}
      • HourBeginning, a type alias for AnchoredInterval{Hour(1)}
      • HE and HB, pseudoconstructors for HourEnding and HourBeginning that round the anchor up (HE) or down (HB) to the nearest hour
  • Bound, abstract type for all possible bounds type classifications:
    • Closed, indicating the endpoint value of the interval is included
    • Open, indicating the endpoint value of the interval is not included
    • Unbounded, indicating the endpoint value is effectively infinite

Sets

A single interval can be used to represent a contiguous set within a domain but cannot be used to represent a disjoint set. For general purpose set operations you need to use the IntervalSet type.

Intervals.IntervalSetType
IntervalSet{T<:AbstractInterval}

A set of points represented by a sequence of intervals. Set operations over interval sets return a new IntervalSet, with the fewest number of intervals possible. Unbounded intervals are not supported. The individual intervals in the set can be accessed by calling convert(Array, interval_set).

see also: https://en.wikipedia.org/wiki/Intervalarithmetic#Intervaloperators

Examples

julia> union(IntervalSet(1..5), IntervalSet(3..8))
1-interval IntervalSet{Interval{Int64, Closed, Closed}}:
[1 .. 8]

julia> intersect(IntervalSet(1..5), IntervalSet(3..8))
1-interval IntervalSet{Interval{Int64, Closed, Closed}}:
[3 .. 5]

julia> symdiff(IntervalSet(1..5), IntervalSet(3..8))
2-interval IntervalSet{Interval{Int64, L, R} where {L<:Bound, R<:Bound}}:
[1 .. 3)
(5 .. 8]

julia> union(IntervalSet([1..2, 2..5]), IntervalSet(6..7))
2-interval IntervalSet{Interval{Int64, Closed, Closed}}:
[1 .. 5]
[6 .. 7]

julia> union(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))
2-interval IntervalSet{Interval{Int64, Closed, Closed}}:
[1 .. 10]
[12 .. 14]

julia> intersect(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))
2-interval IntervalSet{Interval{Int64, Closed, Closed}}:
[4 .. 5]
[8 .. 9]

julia> setdiff(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))
2-interval IntervalSet{Interval{Int64, L, R} where {L<:Bound, R<:Bound}}:
[1 .. 4)
(9 .. 10]
source

If you wish to instead treat each interval as an element of a set, you can operate over vectors or Sets of intervals.

For example:

julia> intersect([1..2, 2..3, 3..4, 4..5], [2..3, 3..4])
2-element Vector{Interval{Int64, Closed, Closed}}:
 Interval{Int64, Closed, Closed}(2, 3)
 Interval{Int64, Closed, Closed}(3, 4)

Example Usage

Bounds

julia> a = Interval{Closed, Closed}(1, 10)
Interval{Int64, Closed, Closed}(1, 10)

julia> b = Interval{Open, Open}(5, 15)
Interval{Int64, Open, Open}(5, 15)

julia> 5 in a
true

julia> 5 in b
false

julia> intersect(a, b)
Interval{Int64, Open, Closed}(5, 10)

julia> c = Interval(15, 20)
Interval{Int64, Closed, Closed}(15, 20)

julia> isempty(intersect(b, c))
true

Display

julia> a = Interval('a', 'z')
Interval{Char, Closed, Closed}('a', 'z')

julia> string(a)
"[a .. z]"

julia> using Dates

julia> b = Interval{Closed, Open}(Date(2013), Date(2016))
Interval{Date, Closed, Open}(Date("2013-01-01"), Date("2016-01-01"))

julia> string(b)
"[2013-01-01 .. 2016-01-01)"

julia> c = HourEnding(DateTime(2016, 8, 11))
HourEnding{DateTime, Open, Closed}(DateTime("2016-08-11T00:00:00"))

julia> string(c)
"(2016-08-10 HE24]"

HourEnding and HE

julia> using TimeZones, Dates

julia> unrounded = HourEnding(ZonedDateTime(2013, 2, 13, 0, 30, tz"America/Winnipeg"))
HourEnding{ZonedDateTime, Open, Closed}(ZonedDateTime(2013, 2, 13, 0, 30, tz"America/Winnipeg"))

julia> he = HE(ZonedDateTime(2013, 2, 13, 0, 30, tz"America/Winnipeg"))
HourEnding{ZonedDateTime, Open, Closed}(ZonedDateTime(2013, 2, 13, 1, tz"America/Winnipeg"))

julia> he + Hour(1)
HourEnding{ZonedDateTime, Open, Closed}(ZonedDateTime(2013, 2, 13, 2, tz"America/Winnipeg"))

julia> foreach(println, he:he + Day(1))
(2013-02-13 HE01-06:00]
(2013-02-13 HE02-06:00]
(2013-02-13 HE03-06:00]
(2013-02-13 HE04-06:00]
(2013-02-13 HE05-06:00]
(2013-02-13 HE06-06:00]
(2013-02-13 HE07-06:00]
(2013-02-13 HE08-06:00]
(2013-02-13 HE09-06:00]
(2013-02-13 HE10-06:00]
(2013-02-13 HE11-06:00]
(2013-02-13 HE12-06:00]
(2013-02-13 HE13-06:00]
(2013-02-13 HE14-06:00]
(2013-02-13 HE15-06:00]
(2013-02-13 HE16-06:00]
(2013-02-13 HE17-06:00]
(2013-02-13 HE18-06:00]
(2013-02-13 HE19-06:00]
(2013-02-13 HE20-06:00]
(2013-02-13 HE21-06:00]
(2013-02-13 HE22-06:00]
(2013-02-13 HE23-06:00]
(2013-02-13 HE24-06:00]
(2013-02-14 HE01-06:00]

julia> anchor(he)
2013-02-13T01:00:00-06:00

Comparisons

Equality

Two AbstractIntervals are considered equal if they have identical left and right endpoints (taking bounds into account):

julia> a = Interval{Closed, Open}(DateTime(2013, 2, 13), DateTime(2013, 2, 13, 1))
Interval{DateTime, Closed, Open}(DateTime("2013-02-13T00:00:00"), DateTime("2013-02-13T01:00:00"))

julia> b = Interval{Open, Closed}(DateTime(2013, 2, 13), DateTime(2013, 2, 13, 1))
Interval{DateTime, Open, Closed}(DateTime("2013-02-13T00:00:00"), DateTime("2013-02-13T01:00:00"))

julia> c = HourEnding(DateTime(2013, 2, 13, 1))
HourEnding{DateTime, Open, Closed}(DateTime("2013-02-13T01:00:00"))

julia> a == b
false

julia> b == c
true

Less Than

When determining whether one AbstractInterval is less than (or greater than) another, two sets of comparison operators are available: </> and /.

The standard < and > operators (which are not explicitly defined, but are derived from isless) simply compare the leftmost endpoint of the intervals, and are used for things like sort, min, max, etc.

The and operators (the Unicode symbols for "much less than" and "much greater than", accessible from the REPL with \ll and \gg, respectively) are used in this context to mean "less/greater than and disjoint"; they will verify that there is no overlap between the intervals.

julia> 0..10 < 10..20
true

julia> 0..10 ≪ 10..20
false

julia> 0..10 ≪ 11..20
true

Rounding

Interval rounding maintains the original span of the interval, shifting it according to whichever endpoint is specified as the one to use for rounding. The operations floor, ceil, and round are supported, as long as the on keyword is supplied to specify which endpoint should be used for rounding. Valid options are :left, :right, or :anchor if dealing with anchored intervals.

julia> floor(Interval(0.0, 1.0), on=:left)
Interval{Float64, Closed, Closed}(0.0, 1.0)

julia> floor(Interval(0.5, 1.0), on=:left)
Interval{Float64, Closed, Closed}(0.0, 0.5)

julia> floor(Interval(0.5, 1.5), on=:right)
Interval{Float64, Closed, Closed}(0.0, 1.0)

Anchored intervals default to rounding using the anchor point.

julia> round(AnchoredInterval{-0.5}(1.0))
AnchoredInterval{-0.5, Float64, Open, Closed}(1.0)

julia> round(AnchoredInterval{+0.5}(0.5))
AnchoredInterval{0.5, Float64, Closed, Open}(0.0)

julia> round(AnchoredInterval{+0.5}(0.5), on=:anchor)
AnchoredInterval{0.5, Float64, Closed, Open}(0.0)

julia> round(AnchoredInterval{+0.5}(0.5), on=:left)
AnchoredInterval{0.5, Float64, Closed, Open}(0.0)

julia> round(AnchoredInterval{+0.5}(0.5), on=:right)
AnchoredInterval{0.5, Float64, Closed, Open}(0.5)

Plotting

AbstractInterval subtypes can be plotted with Plots.jl.

julia> using Plots

julia> start_dt = DateTime(2017,1,1,0,0,0);

julia> end_dt = DateTime(2017,1,1,10,30,0);

julia> datetimes = start_dt:Hour(1):end_dt
DateTime("2017-01-01T00:00:00"):Hour(1):DateTime("2017-01-01T10:00:00")

julia> intervals = HE.(datetimes);

julia> plot(intervals, 1:11)

Example Plot

In the plot, inclusive boundaries are marked with a vertical bar, whereas exclusive boundaries just end.

API

Intervals.IntervalType
Interval{T, L <: Bound, R <: Bound}

An Interval represents a non-iterable range or span of values (non-iterable because, unlike a StepRange, no step is defined).

An Interval can be closed (both first and last are included in the interval), open (neither first nor last are included), or half-open. This openness is defined by the bounds information which is stored as the type parameters L and R.

Example

julia> interval = Interval{Closed,Open}(0, 100)
Interval{Int64,Closed,Open}}(0, 100)

julia> 0 in interval
true

julia> 50 in interval
true

julia> 100 in interval
false

julia> intersect(Interval{Open,Open}(0, 25), Interval{Closed,Closed}(20, 50)
Interval{Int64,Closed,Open}(20, 25)

Infix Constructor: ..

A closed Interval can be constructed with the .. infix constructor:

julia> Dates.today() - Dates.Week(1) .. Dates.today()
Interval{Date,Closed,Closed}(2018-01-24, 2018-01-31)

See also: AnchoredInterval

source
Intervals.AnchoredIntervalType
AnchoredInterval{P,T,L,R}

AnchoredInterval is a subtype of AbstractInterval that represents a non-iterable range or span of values defined not by two endpoints but instead by a single anchor point and the value type P which represents the size of the range. When P is positive, the anchor represents the lesser endpoint (the beginning of the range); when P is negative, the anchor represents the greater endpoint (the end of the range).

The interval represented by an AnchoredInterval value may be closed (both endpoints are included in the interval), open (neither endpoint is included), or half-open. This openness is defined by the bounds types L and R, which defaults to half-open (with the lesser endpoint included for positive values of P and the greater endpoint included for negative values).

Why?

AnchoredIntervals are most useful in cases where a single value is used to stand in for a range of values. This happens most often with dates and times, where "HE15" is often used as shorthand for (14:00..15:00].

To this end, HourEnding is a type alias for AnchoredInterval{Hour(-1)}. Similarly, HourBeginning is a type alias for AnchoredInterval{Hour(1)}.

Rounding

While the user may expect an HourEnding or HourBeginning value to be anchored to a specific hour, the constructor makes no guarantees that the anchor provided is rounded:

julia> HourEnding(DateTime(2016, 8, 11, 2, 30))
HourEnding{DateTime, Open, Closed}(DateTime("2016-08-11T02:30:00"))

The HE and HB pseudoconstructors round the input up or down to the nearest hour, as appropriate:

julia> HE(DateTime(2016, 8, 11, 2, 30))
HourEnding{DateTime, Open, Closed}(DateTime("2016-08-11T03:00:00"))

julia> HB(DateTime(2016, 8, 11, 2, 30))
HourBeginning{DateTime, Closed, Open}(DateTime("2016-08-11T02:00:00"))

Example

julia> AnchoredInterval{Hour(-1)}(DateTime(2016, 8, 11, 12))
HourEnding{DateTime, Open, Closed}(DateTime("2016-08-11T12:00:00"))

julia> AnchoredInterval{Day(1)}(DateTime(2016, 8, 11))
AnchoredInterval{Day(1), DateTime, Closed, Open}(DateTime("2016-08-11T00:00:00"))

julia> AnchoredInterval{Minute(5),Closed,Closed}(DateTime(2016, 8, 11, 12, 30))
AnchoredInterval{Minute(5), DateTime, Closed, Closed}(DateTime("2016-08-11T12:30:00"))

See also: Interval, HE, HB

source
Intervals.HourEndingType
HourEnding{T<:TimeType, L, R} <: AbstractInterval{T}

A type alias for AnchoredInterval{Hour(-1), T} which is used to denote a 1-hour period of time which ends at a time instant (of type T).

When constructing an instance of HourEnding{T} the resulting interval will right-closed (of type HourEnding{T,Open,Closed}).

source
Intervals.HourBeginningType
HourBeginning{T<:TimeType, L, R} <: AbstractInterval{T}

A type alias for AnchoredInterval{Hour(1), T} which is used to denote a 1-hour period of time which begins at a time instant (of type T).

When constructing an instance of HourBeginning{T} the resulting interval will left-closed (of type HourBeginning{T,Closed,Open}).

source
Intervals.HEFunction
HE(anchor) -> HourEnding

HE is a pseudoconstructor for HourEnding that rounds the anchor provided up to the nearest hour.

source
Intervals.HBFunction
HB(anchor) -> HourBeginning

HB is a pseudoconstructor for HourBeginning that rounds the anchor provided down to the nearest hour.

source
Intervals.BoundType
Bound <: Any

Abstract type representing all possible endpoint classifications (e.g. open, closed, unbounded).

source
Intervals.BoundedType
Bounded <: Bound

Abstract type indicating that the endpoint of an interval is not unbounded (e.g. open or closed).

source
Intervals.ClosedType
Closed <: Bounded <: Bound

Type indicating that the endpoint of an interval is closed (the endpoint value is included in the interval).

source
Intervals.OpenType
Open <: Bounded <: Bound

Type indicating that the endpoint of an interval is open (the endpoint value is not included in the interval).

source
Intervals.UnboundedType
Unbounded <: Bound

Type indicating that the endpoint of an interval is unbounded (the endpoint value is effectively infinite).

source
Base.firstFunction
first(interval::AbstractInterval{T}) -> Union{T,Nothing}

The value of the lower endpoint. When the lower endpoint is unbounded nothing will be returned.

source
Base.lastFunction
last(interval::AbstractInterval{T}) -> Union{T,Nothing}

The value of the upper endpoint. When the upper endpoint is unbounded nothing will be returned.

source
Intervals.spanFunction
span(interval::AbstractInterval) -> Any

The delta between the upper and lower endpoints. For bounded intervals returns a non-negative value while intervals with any unbounded endpoints will throw an ArgumentError.

To avoid having to capture the exception use the pattern:

Intervals.isbounded(interval) ? span(interval) : infinity

Where infinity is a variable representing the value you wish to use to represent an unbounded, or infinite, span.

source
Base.isopenFunction
isopen(interval) -> Bool

Is an open-interval: excludes both of its endpoints.

source
Intervals.isboundedFunction
isbounded(interval) -> Bool

Is a bounded-interval: either open, closed, left-closed/right-open, or left-open/right-closed.

Note using !isbounded is commonly used to determine if any end of the interval is unbounded.

source
Base.parseMethod
parse(::Type{Interval{T}}, str; element_parser=parse) -> Interval{T}

Parse a string of the form <left-type><left-value><delim><right-value><right-type> (e.g. [1 .. 2)) as an Interval{T}. The format above is interpreted as:

  • left-type: Must be either "[" or "(" which indicates if the left-endpoint of the interval is either Closed or Open.

  • left-value: Specifies the value of the left-endpoint which will be parsed as the type T. If the value string has a length of zero then the left-endpoint will be specified as Unbounded. If the value string contains the delimiter (see below) then you may double-quote the value string to avoid any ambiguity.

  • delim: Must be either ".." or "," which indicates the delimiter separating the left/right endpoint values.

  • right-value: Specifies the value of the right-endpoint. See left-value for more details.

  • right-type: Must be either "]" or ")" which indicates if the right-endpoint of the interval is either Closed or Open.

The element_parser keyword allows a custom parser to be used when parsing the left/right values. The function is expected to take two arguments: Type{T} and AbstractString. This is useful for supplying additional arguments/keywords, alternative parser functions, or for types that do not define parse (e.g. String).

source
Intervals.:≪Function
≪(a::AbstractInterval, b::AbstractInterval) -> Bool
less_than_disjoint(a::AbstractInterval, b::AbstractInterval) -> Bool

Less-than-and-disjoint comparison operator. Returns true if a is less than b and they are disjoint (they do not overlap).

julia> 0..10 ≪ 10..20
false

julia> 0..10 ≪ 11..20
true
source
Intervals.:≫Function
≫(a::AbstractInterval, b::AbstractInterval) -> Bool
greater_than_disjoint(a::AbstractInterval, b::AbstractInterval) -> Bool

Greater-than-and-disjoint comparison operator. Returns true if a is greater than b and they are disjoint (they do not overlap).

julia> 10..20 ≫ 0..10
false

julia> 11..20 ≫ 0..10
true
source
Base.:==Function
==(a::Endpoint, b::Endpoint) -> Bool

Determine if two endpoints are equal. When both endpoints are left or right then the points and inclusiveness must be the same.

Checking the equality of left-endpoint and a right-endpoint is slightly more difficult. A left-endpoint and a right-endpoint are only equal when they use the same point and are both included. Note that left/right endpoints which are both not included are not equal as the left-endpoint contains values below that point while the right-endpoint only contains values that are above that point.

Visualizing two contiguous intervals can assist in understanding this logic:

[x..y][y..z] -> RightEndpoint == LeftEndpoint
[x..y)[y..z] -> RightEndpoint != LeftEndpoint
[x..y](y..z] -> RightEndpoint != LeftEndpoint
[x..y)(y..z] -> RightEndpoint != LeftEndpoint
source
Base.unionFunction
union(intervals::IntervalSets)

Flattens any overlapping intervals within the IntervalSet into a new, smaller set containing only non-overlapping intervals.

source
Base.union!Function
union!(intervals::IntervalSet)

Flattens a vector of overlapping intervals in-place to be a smaller vector containing only non-overlapping intervals.

source
Intervals.supersetFunction
superset(intervals::IntervalSet) -> Interval

Create the smallest single interval which encompasses all of the provided intervals.

source
Intervals.find_intersectionsFunction
find_intersections(
    x::AbstractVector{<:AbstractInterval},
    y::AbstractVector{<:AbstractInterval}
)

Returns a Vector{Vector{Int}} where the value at index i gives the indices to all intervals in y that intersect with x[i].

source