diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 00000000..e82e637e --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,2 @@ +style = "blue" +margin = 92 diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index dcec41d6..13a4973f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -79,7 +79,7 @@ jobs: - uses: actions/checkout@v2 - uses: julia-actions/setup-julia@v1 with: - version: '1' + version: '1.6' - run: | julia --project=docs -e ' using Pkg diff --git a/.gitignore b/.gitignore index 07c5a085..bb330b5b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /docs/build/ /docs/site/ /Manifest.toml +/test/Manifest.toml diff --git a/Project.toml b/Project.toml index 1a5a52b5..17b9f7a7 100644 --- a/Project.toml +++ b/Project.toml @@ -2,7 +2,7 @@ name = "Intervals" uuid = "d8418881-c3e1-53bb-8760-2df7ec849ed5" license = "MIT" authors = ["Invenia Technical Computing"] -version = "1.7.1" +version = "1.8.0" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" @@ -12,19 +12,6 @@ Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" [compat] -Documenter = "0.23, 0.24, 0.25, 0.26, 0.27" -Infinity = "0.2.3" RecipesBase = "0.7, 0.8, 1" TimeZones = "1.7" julia = "1.6" - -[extras] -Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" -Infinity = "a303e19e-6eb4-11e9-3b09-cd9505f79100" -Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" -Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" - -[targets] -test = ["Documenter", "ImageMagick", "Infinity", "Plots", "Test", "VisualRegressionTests"] diff --git a/docs/src/index.md b/docs/src/index.md index c7bb368e..9c86ba9d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -26,21 +26,38 @@ This package defines: * [`Open`](@ref), indicating the endpoint value of the interval is not included * [`Unbounded`](@ref), indicating the endpoint value is effectively infinite -## Example Usage +## Sets -### Intersection +A single interval can be used to represent a contiguous set within a domain but cannot be +used to represent a disjoint set. Due to this restriction all set-based operations that +return an interval will always return a vector of intervals. These operations will combine +any intervals which are overlapping or touching into a single continuous interval and never +return an interval instance which itself is empty. -```jldoctest -julia> a = 1..10 -Interval{Int64, Closed, Closed}(1, 10) +```julia +julia> union([1..10], [5..15]) +1-element Vector{Interval{Int64, Closed, Closed}}: + Interval{Int64, Closed, Closed}(1, 15) -julia> b = 5..15 -Interval{Int64, Closed, Closed}(5, 15) +julia> intersect([1..10], [5..15]) +1-element Vector{Interval{Int64, Closed, Closed}}: + Interval{Int64, Closed, Closed}(5, 10) -julia> intersect(a, b) -Interval{Int64, Closed, Closed}(5, 10) +julia> setdiff([1..10], [5..15]) +1-element Vector{Interval{Int64, Closed, Open}}: + Interval{Int64, Closed, Open}(1, 5) + +julia> symdiff([1..10], [5..15]) +2-element Vector{Interval{Int64}}: + Interval{Int64, Closed, Open}(1, 5) + Interval{Int64, Open, Closed}(10, 15) + +julia> intersect([1..5], [10..15]) +Interval[] ``` +## Example Usage + ### Bounds ```jldoctest @@ -135,31 +152,6 @@ julia> anchor(he) 2013-02-13T01:00:00-06:00 ``` -### Plotting -`AbstractInterval` subtypes can be plotted with [Plots.jl](https://github.com/JuliaPlots/Plots.jl). - - -```julia -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](assets/HE.png) - -In the plot, inclusive boundaries are marked with a vertical bar, whereas exclusive boundaries just end. - - - ### Comparisons #### Equality @@ -247,11 +239,36 @@ 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](https://github.com/JuliaPlots/Plots.jl). + +```julia +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](assets/HE.png) + +In the plot, inclusive boundaries are marked with a vertical bar, whereas exclusive boundaries just end. + + ## API ```@docs Interval AnchoredInterval +IntervalSet HourEnding HourBeginning HE @@ -275,4 +292,5 @@ Base.parse(::Type{Interval{T}}, ::AbstractString) where T union union! superset +Intervals.find_intersections ``` diff --git a/src/Intervals.jl b/src/Intervals.jl index 0c606436..7d3a939a 100644 --- a/src/Intervals.jl +++ b/src/Intervals.jl @@ -27,6 +27,7 @@ bounds_types(x::AbstractInterval{T,L,R}) where {T,L,R} = (L, R) include("isfinite.jl") include("endpoint.jl") include("interval.jl") +include("interval_sets.jl") include("anchoredinterval.jl") include("parse.jl") include("description.jl") @@ -41,6 +42,7 @@ export Bound, Unbounded, AbstractInterval, Interval, + IntervalSet, AnchoredInterval, HourEnding, HourBeginning, diff --git a/src/deprecated.jl b/src/deprecated.jl index e91ae7c4..e6c370b5 100644 --- a/src/deprecated.jl +++ b/src/deprecated.jl @@ -195,4 +195,8 @@ function HB(anchor, inc::Inclusivity) return HourBeginning{L,R}(floor(anchor, Hour)) end +@deprecate union(intervals::AbstractVector{<:AbstractInterval}) convert(Vector, union(IntervalSet(intervals))) +@deprecate union!(intervals::AbstractVector{<:AbstractInterval}) convert(Vector, union!(IntervalSet(intervals))) +@deprecate superset(intervals::AbstractVector{<:AbstractInterval}) superset(IntervalSet(intervals)) + # END Intervals 1.X.Y deprecations diff --git a/src/endpoint.jl b/src/endpoint.jl index 68482c2e..eb91b844 100644 --- a/src/endpoint.jl +++ b/src/endpoint.jl @@ -6,7 +6,8 @@ const Right = Direction{:Right}() const Beginning = Left const Ending = Right -struct Endpoint{T, D, B <: Bound} +abstract type AbstractEndpoint end +struct Endpoint{T, D, B <: Bound} <: AbstractEndpoint endpoint::T function Endpoint{T,D,B}(ep::T) where {T, D, B <: Bounded} diff --git a/src/interval.jl b/src/interval.jl index a674c2e6..cb18c8f9 100644 --- a/src/interval.jl +++ b/src/interval.jl @@ -405,60 +405,6 @@ function Base.intersect(a::AbstractInterval{S}, b::AbstractInterval{T}) where {S return Interval(left, right) end -# There is power in a union. -""" - union(intervals::AbstractVector{<:AbstractInterval}) - -Flattens a vector of overlapping intervals into a new, smaller vector containing only -non-overlapping intervals. -""" -function Base.union(intervals::AbstractVector{<:AbstractInterval}) - return union!(convert(Vector{AbstractInterval}, intervals)) -end - -""" - union!(intervals::AbstractVector{<:Union{Interval, AbstractInterval}}) - -Flattens a vector of overlapping intervals in-place to be a smaller vector containing only -non-overlapping intervals. -""" -function Base.union!(intervals::Union{AbstractVector{<:Interval}, AbstractVector{AbstractInterval}}) - sort!(intervals) - - i = 2 - n = length(intervals) - while i <= n - prev = intervals[i - 1] - curr = intervals[i] - - # If the current and previous intervals don't meet then move along - if !overlaps(prev, curr) && !contiguous(prev, curr) - i = i + 1 - - # If the two intervals meet then we absorb the current interval into - # the previous one. - else - intervals[i - 1] = merge(prev, curr) - deleteat!(intervals, i) - n -= 1 - end - end - - return intervals -end - -""" - superset(intervals::AbstractArray{<:AbstractInterval}) -> Interval - -Create the smallest single interval which encompasses all of the provided intervals. -""" -function superset(intervals::AbstractArray{<:AbstractInterval}) - left = minimum(LeftEndpoint.(intervals)) - right = maximum(RightEndpoint.(intervals)) - - return Interval(left, right) -end - function Base.merge(a::AbstractInterval, b::AbstractInterval) if !overlaps(a, b) && !contiguous(a, b) throw(ArgumentError("$a and $b are neither overlapping or contiguous.")) diff --git a/src/interval_sets.jl b/src/interval_sets.jl new file mode 100644 index 00000000..16211696 --- /dev/null +++ b/src/interval_sets.jl @@ -0,0 +1,523 @@ + +###### Set-related Helpers ##### +""" + 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 using the iteration +API or by passing the set to `Array`. + +see also: https://en.wikipedia.org/wiki/Interval_arithmetic#Interval_operators + +## Examples + +```jldoctest; setup = :(using Intervals) +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] +``` +""" +struct IntervalSet{T <: AbstractInterval} + items::Vector{T} +end + +IntervalSet(interval::T) where T <: AbstractInterval = IntervalSet{T}([interval]) +IntervalSet(interval::IntervalSet) = interval +IntervalSet(itr) = IntervalSet{eltype(itr)}(collect(itr)) +IntervalSet() = IntervalSet(AbstractInterval[]) + +Base.copy(intervals::IntervalSet{T}) where {T} = IntervalSet{T}(copy(intervals.items)) +Base.eltype(::IntervalSet{T}) where T = T +Base.isempty(intervals::IntervalSet) = isempty(intervals.items) || all(isempty, intervals.items) +Base.:(==)(a::IntervalSet, b::IntervalSet) = issetequal(a, b) +Base.isequal(a::IntervalSet, b::IntervalSet) = isequal(a.items, b.items) +Base.convert(::Type{T}, intervals::IntervalSet) where T <: AbstractArray = convert(T, intervals.items) +function Base.show(io::Base.AbstractPipe, ::MIME"text/plain", x::IntervalSet) + intervals = union(x) + n = length(intervals.items) + iocompact = IOContext(io, :compact => true) + print(io, "$n-interval ") + show(io, MIME"text/plain"(), typeof(x)) + println(io, ":") + nrows = displaysize(io)[1] + half = fld(nrows, 2) - 2 + if nrows ≥ n && half > 1 + for interval in intervals.items[1:(end-1)] + show(iocompact, MIME"text/plain"(), interval) + println(io, "") + end + isempty(intervals) || show(iocompact, MIME"text/plain"(), intervals.items[end]) + else + for interval in intervals.items[1:half] + show(iocompact, MIME"text/plain"(), interval) + println(io, "") + end + println(io, "⋮") + for interval in intervals.items[(end-half+1):end-1] + show(iocompact, MIME"text/plain"(), interval) + println(io, "") + end + show(iocompact, MIME"text/plain"(), intervals.items[end]) + end +end + +# currently (to avoid breaking changes) new methods for `Base` +# accept `IntervalSet` objects and Interval singletons. +const AbstractIntervals = Union{AbstractInterval, IntervalSet} + +# During merge operations used to compute unions, intersections etc..., +# endpoint types can change (from left to right, and from open to closed, +# etc...). The following structures indicate how endpoints should be tracked. + +# TrackEachEndpoint tracks endpoints on a case-by-case basis +# computing closed/open with boolean flags +abstract type EndpointTracking; end +struct TrackEachEndpoint <: EndpointTracking; end +# TrackLeftOpen and TrackRightOpen track the endpoints statically: if the +# intervals to be merged are all left open (or all right open), the resulting +# output will always be all left open (or all right open). +abstract type TrackStatically{T} <: EndpointTracking; end +struct TrackLeftOpen{T} <: TrackStatically{T}; end +struct TrackRightOpen{T} <: TrackStatically{T}; end + +function endpoint_tracking( + ::Type{<:AbstractInterval{T,Open,Closed}}, + ::Type{<:AbstractInterval{U,Open,Closed}}, +) where {T,U} + W = promote_type(T, U) + return TrackLeftOpen{W}() +end +function endpoint_tracking( + ::Type{<:AbstractInterval{T,Closed,Open}}, + ::Type{<:AbstractInterval{U,Closed,Open}}, +) where {T,U} + W = promote_type(T, U) + return TrackRightOpen{W}() +end +function endpoint_tracking( + ::Type{<:AbstractInterval}, + ::Type{<:AbstractInterval}, +) + return TrackEachEndpoint() +end + +endpoint_tracking(a::IntervalSet, b::IntervalSet) = endpoint_tracking(eltype(a), eltype(b)) +endpoint_tracking(a::AbstractInterval, b::AbstractInterval) = endpoint_tracking(typeof(a), typeof(b)) +endpoint_tracking(a::AbstractVector, b::AbstractVector) = endpoint_tracking(eltype(a), eltype(b)) + +# track: run a thunk, but only if we are tracking endpoints dynamically +track(fn::Function, ::TrackEachEndpoint, args...) = fn(args...) +track(_, tracking::TrackStatically, args...) = tracking + +endpoint_type(::TrackEachEndpoint) = Endpoint +endpoint_type(::TrackLeftOpen{T}) where T = Union{LeftEndpoint{T,Open}, RightEndpoint{T, Closed}} +endpoint_type(::TrackRightOpen{T}) where T = Union{LeftEndpoint{T,Closed}, RightEndpoint{T, Open}} +interval_type(::TrackEachEndpoint) = Interval +interval_type(::TrackLeftOpen{T}) where T = Interval{T, Open, Closed} +interval_type(::TrackRightOpen{T}) where T = Interval{T, Closed, Open} + +# `unbunch/bunch`: the generic operation used to implement all set operations operates on a +# series of sorted endpoints (see `mergesets` below); this first requires that +# all vectors of sets be represented by their endpoints. The functions unbunch +# and bunch convert between an interval and an endpoint representation + +function unbunch(interval::AbstractInterval, tracking::EndpointTracking; lt=isless) + return endpoint_type(tracking)[LeftEndpoint(interval), RightEndpoint(interval)] +end +function unbunch(intervals::IntervalSet, tracking::EndpointTracking; kwargs...) + return unbunch(convert(Vector, intervals), tracking; kwargs...) +end +unbunch_by_fn(_) = identity +function unbunch( + intervals::Union{ + AbstractVector{<:AbstractInterval}, + Base.Iterators.Enumerate{<:Union{AbstractIntervals, AbstractVector{<:AbstractInterval}}} + }, + tracking::EndpointTracking; + lt=isless, +) + by = unbunch_by_fn(intervals) + filtered = Iterators.filter(!isempty ∘ by, intervals) + isempty(filtered) && return endpoint_type(tracking)[] + result = mapreduce(x -> unbunch(x, tracking), vcat, filtered) + return sort!(result; lt, by) +end +# support for `unbunch(enumerate(vcat(x)))` (transforming [(i, interval)] -> [(i, endpoint), (i,endpoint)]) +unbunch_by_fn(::Base.Iterators.Enumerate) = last +function unbunch((i, interval)::Tuple, tracking; lt=isless) + eltype = Tuple{Int, endpoint_type(tracking)} + return eltype[(i, LeftEndpoint(interval)), (i, RightEndpoint(interval))] +end + +function unbunch(a::Union{AbstractVector{<:AbstractInterval}, AbstractIntervals}, + b::Union{AbstractVector{<:AbstractInterval}, AbstractIntervals}; kwargs...) + tracking = endpoint_tracking(a, b) + a_ = unbunch(a, tracking; kwargs...) + b_ = unbunch(b, tracking; kwargs...) + return a_, b_, tracking +end + +# represent a sequence of endpoints as a sequence of one or more intervals +function bunch(endpoints, tracking) + @assert iseven(length(endpoints)) + isempty(endpoints) && return IntervalSet(interval_type(tracking)[]) + res = map(Iterators.partition(endpoints, 2)) do pair + return Interval(pair..., tracking) + end + return IntervalSet(res) +end +Interval(a::Endpoint, b::Endpoint, ::TrackEachEndpoint) = Interval(a, b) +Interval(a::Endpoint, b::Endpoint, ::TrackLeftOpen{T}) where T = Interval{T,Open,Closed}(a.endpoint, b.endpoint) +Interval(a::Endpoint, b::Endpoint, ::TrackRightOpen{T}) where T = Interval{T,Closed,Open}(a.endpoint, b.endpoint) + +# the sentinel endpoint reduces the number of edgecases +# we have to deal with when comparing endpoints during a merge +# NOTE: it's tempting to replace this with an unbounded endpoint +# but if we ever want to support unbounded endpoints in mergesets +# then SentinalEndpoint needs to be greater than those endpointss +struct SentinelEndpoint <: AbstractEndpoint end +function first_endpoint(x) + isempty(x) && return SentinelEndpoint() + # if the endpoints are enumerated, eltype will be a tuple + return eltype(x) <: Tuple ? last(first(x)) : first(x) +end +function last_endpoint(x) + isempty(x) && return SentinelEndpoint() + # if the endpoints are enumerated, eltype will be a tuple + return eltype(x) <: Tuple ? last(last(x)) : last(x) +end + + +Base.isless(::LeftEndpoint, ::SentinelEndpoint) = true +Base.isless(::RightEndpoint, ::SentinelEndpoint) = true +Base.isless(::SentinelEndpoint, ::LeftEndpoint) = false +Base.isless(::SentinelEndpoint, ::RightEndpoint) = false +Base.isless(::SentinelEndpoint, ::SentinelEndpoint) = false + +Base.isequal(::LeftEndpoint, ::SentinelEndpoint) = false +Base.isequal(::RightEndpoint, ::SentinelEndpoint) = false +Base.isequal(::SentinelEndpoint, ::LeftEndpoint) = false +Base.isequal(::SentinelEndpoint, ::RightEndpoint) = false +Base.isequal(::SentinelEndpoint, ::SentinelEndpoint) = true +isclosed(::SentinelEndpoint) = true +isleft(::SentinelEndpoint) = false +isleft(::LeftEndpoint) = true +isleft(::RightEndpoint) = false + +# mergesets(op, x, y) +# +# `mergesets` is the primary internal function implementing set operations (see below for +# its usage). It iterates through the left and right endpoints in x and y, in order from +# lowest to highest. The implementation is based on the insight that we can make a decision +# to include or exclude all points after a given endpoint (based on `op`) and that decision +# will remain unchanged moving left to right along the real-number line until we encounter a +# new endpoint. +# +# For each endpoint, we determine two things: +# 1. whether subsequent points should be included in the merge operation or not (based on +# its membership in both `x` and `y`) by using `op` +# 2. whether the next step will define a left (start including) or right endpoint (stop +# includeing) +# +# Then, we decide to add a new endpoint if 1 and 2 match (i.e. "should include" points will +# create a time point when the next point will start including points). +# +# A final issue is handling the closed/open nature of each endpoint. In the general case, we +# have to track whether to keep the endpoint (closed) or not (open) separately. Keeping the +# endpoint may require we keep a singleton endpoint ([1,1]) such as when two closed +# endpoints intersect with one another (e.g. (0, 1] ∩ [1, 2)). In some cases we don't need +# track endpoints at all: e.g. when all endpoints are open right ([1, 0)) or they are all +# open left ((1, 1]) then all resulting endpoints will follow the same pattern. + +function mergesets(op, x, y) + x_, y_, tracking = unbunch(union(x), union(y)) + return mergesets_helper(op, x_, y_, tracking) +end +length_(x::AbstractInterval) = 1 +length_(x) = length(x) +function mergesets_helper(op, x, y, endpoint_tracking) + result = endpoint_type(endpoint_tracking)[] + sizehint!(result, length_(x) + length_(y)) + + # to start, points are not included (until we see the starting endpoint of a set) + inresult = false + inx = false + iny = false + + while !(isempty(x) && isempty(y)) + xᵢ, yᵢ = first_endpoint.((x,y)) + t = xᵢ < yᵢ ? xᵢ : yᵢ + + # whether to include (close) an endpoint + bound = track(endpoint_tracking) do + x_closed_end = xᵢ ≤ yᵢ ? isclosed(xᵢ) : inx + y_closed_end = yᵢ ≤ xᵢ ? isclosed(yᵢ) : iny + return op(x_closed_end, y_closed_end) ? Closed : Open + end + + # update endpoints + if xᵢ ≤ yᵢ + inx = isleft(xᵢ) + x = @view(x[2:end]) + end + if yᵢ ≤ xᵢ + iny = isleft(yᵢ) + y = @view(y[2:end]) + end + + # does (new point inclusion) match (current inclusion state)? + if op(inx, iny) != inresult + # start including points (left endpoint) + if !inresult + endpoint = left_endpoint(t, bound) + # If we get here, *normally* we want to add a new left (starting) + # endpoint. + # EXCEPTION: new endpoint directly abuts old endpoint e.g. [0, 1] ∪ (1, 2] + if !abuts(last_endpoint(result), endpoint, endpoint_tracking) + push!(result, endpoint) + else + pop!(result) + end + inresult = true + else + # If we get here, *normally* we want to add a right (stopping) end point + # EXCEPTION: the interval to be created would be empty e.g. [0, 1] ∩ (1, 2] + if !empty_interval(last_endpoint(result), t, endpoint_tracking) + push!(result, right_endpoint(t, bound)) + else + pop!(result) + end + inresult = false + end + else + track(endpoint_tracking) do + # edgecase: if we're supposed to close the endpoint but we're not including + # any points right now, we need to add a singleton endpoint (e.g. [0, 1] ∩ + # [1, 2]) + if bound === Closed && !inresult + push!(result, left_endpoint(t, Closed)) + push!(result, right_endpoint(t, Closed)) + end + + # edgecase: we have an open endpoint right at the edge of two intervals, but we + # are continuing to include points right now: e.g. symdiff of [0, 1] and [1, 2] + if bound === Open && inresult && xᵢ == yᵢ + push!(result, right_endpoint(t, Open)) + push!(result, left_endpoint(t, Open)) + end + end + end + + end + + return bunch(result, endpoint_tracking) +end +# abuts: true if unioning the two endpoints would lead to a single interval (e.g. (0 1] ∪ (1, 2))) +abuts(::SentinelEndpoint, _, _) = false +abuts(oldstop::Endpoint, newstart, ::TrackStatically) = oldstop.endpoint == newstart.endpoint +function abuts(oldstop::Endpoint, newstart, ::TrackEachEndpoint) + return oldstop.endpoint == newstart.endpoint && (isclosed(oldstop) || isclosed(newstart)) +end + +# empty_interval: true if the given left and right endpoints would create an empty interval +empty_interval(::SentinelEndpoint, _, _) = false # sentinal means there was no starting endpoint; there is thus no interval, and so no empty interval +empty_interval(start, stop, ::TrackStatically) = start.endpoint == stop.endpoint +empty_interval(start, stop, ::TrackEachEndpoint) = start > stop +# the below methods create a left or a right endpoint from the endpoint t: note +# that t might not be the same type of endpoint (e.g. +# `left_endpoint(RightEndpoint(...))` is perfectly valid). `mergesets` may +# change which side of an interval an endpoint is on. +left_endpoint(t::Endpoint{T}, ::Type{B}) where {T, B <: Bound} = LeftEndpoint{T, B}(endpoint(t)) +right_endpoint(t::Endpoint{T}, ::Type{B}) where {T, B <: Bound} = RightEndpoint{T, B}(endpoint(t)) +left_endpoint(t, ::TrackLeftOpen{T}) where T = LeftEndpoint{T,Open}(endpoint(t)) +left_endpoint(t, ::TrackRightOpen{T}) where T = LeftEndpoint{T,Closed}(endpoint(t)) +right_endpoint(t, ::TrackLeftOpen{T}) where T = RightEndpoint{T,Closed}(endpoint(t)) +right_endpoint(t, ::TrackRightOpen{T}) where T = RightEndpoint{T,Open}(endpoint(t)) + +##### Multi-interval Set Operations ##### + +# There is power in a union. +""" + union(intervals::IntervalSets) + +Flattens any overlapping intervals within the `IntervalSet` into a new, smaller set +containing only non-overlapping intervals. +""" +Base.union(intervals::IntervalSet{<:Interval}) = union!(copy(intervals)) + +# In the case where we're dealing with a non-concrete interval type like AnchoredIntervals then simply +# allocate a AbstractInterval vector +function Base.union(intervals::IntervalSet{<:AbstractInterval}) + T = AbstractInterval + dest = Vector{T}(undef, length(intervals.items)) + copyto!(dest, intervals.items) + return union!(IntervalSet{T}(dest)) +end + +""" + union!(intervals::IntervalSet) + +Flattens a vector of overlapping intervals in-place to be a smaller vector containing only +non-overlapping intervals. +""" +function Base.union!(intervals::IntervalSet) + items = intervals.items + sort!(items) + + i = 2 + n = length(items) + while i <= n + prev = items[i - 1] + curr = items[i] + + # If the current and previous intervals don't meet then move along + if !overlaps(prev, curr) && !contiguous(prev, curr) + i = i + 1 + + # If the two intervals meet then we absorb the current interval into + # the previous one. + else + items[i - 1] = merge(prev, curr) + deleteat!(items, i) + n -= 1 + end + end + + return intervals +end + +""" + superset(intervals::IntervalSet) -> Interval + +Create the smallest single interval which encompasses all of the provided intervals. +""" +function superset(intervals::IntervalSet) + left = minimum(LeftEndpoint.(intervals.items)) + right = maximum(RightEndpoint.(intervals.items)) + + return Interval(left, right) +end + + +# set operations over multi-interval sets +Base.intersect(x::IntervalSet, y::IntervalSet) = mergesets((inx, iny) -> inx && iny, x, y) +Base.union(x::IntervalSet, y::IntervalSet) = mergesets((inx, iny) -> inx || iny, x, y) +Base.setdiff(x::IntervalSet, y::IntervalSet) = mergesets((inx, iny) -> inx && !iny, x, y) +Base.symdiff(x::IntervalSet, y::IntervalSet) = mergesets((inx, iny) -> inx ⊻ iny, x, y) +Base.isdisjoint(x::AbstractIntervals, y::AbstractIntervals) = isempty(intersect(x, y)) + +Base.issubset(x, y::IntervalSet) = x in y +Base.issubset(x::AbstractInterval, y::IntervalSet) = any(Base.Fix1(issubset, x), y.items) +Base.issubset(x::IntervalSet, y::AbstractInterval) = all(Base.Fix2(issubset, y), x.items) +Base.issubset(x::IntervalSet, y::IntervalSet) = isempty(setdiff(x, y)) + +# Add methods where just 1 argument is an Interval +for f in (:intersect, :union, :setdiff, :symdiff) + @eval Base.$f(x::AbstractInterval, y::IntervalSet) = $f(IntervalSet([x]), y) + @eval Base.$f(x::IntervalSet, y::AbstractInterval) = $f(x, IntervalSet([y])) +end + +function Base.issetequal(x::AbstractIntervals, y::AbstractIntervals) + x, y, tracking = unbunch(union(IntervalSet(x)), union(IntervalSet(y))) + return x == y || isempty(bunch(x, tracking)) && isempty(bunch(y, tracking)) +end + +# when `x` is number-like object (Number, Date, Time, etc...): +Base.in(x, y::IntervalSet) = any(Base.Fix1(in, x), y.items) + +# order edges so that closed boundaries are on the outside: e.g. [( )] +intersection_order(x::Endpoint) = isleft(x) ? !isclosed(x) : isclosed(x) +intersection_isless_fn(::TrackStatically) = isless +function intersection_isless_fn(::TrackEachEndpoint) + function (x,y) + if isequal(x, y) + return isless(intersection_order(x), intersection_order(y)) + else + return isless(x, y) + end + end +end + +""" + 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]`. +""" +find_intersections(x, y) = find_intersections(vcat(x), vcat(y)) +function find_intersections(x::AbstractVector{<:AbstractInterval}, y::AbstractVector{<:AbstractInterval}) + tracking = endpoint_tracking(x, y) + lt = intersection_isless_fn(tracking) + x_endpoints = unbunch(enumerate(x), tracking; lt) + y_endpoints = unbunch(enumerate(y), tracking; lt) + result = [Vector{Int}() for _ in 1:length(x)] + + return find_intersections_helper!(result, x_endpoints, y_endpoints, lt) +end + +function find_intersections_helper!(result, x, y, lt) + active_xs = Set{Int}() + active_ys = Set{Int}() + while !isempty(x) + xᵢ, yᵢ = first_endpoint(x), first_endpoint(y) + x_less = lt(xᵢ, yᵢ) + y_less = lt(yᵢ, xᵢ) + + if !y_less + if isleft(xᵢ) + push!(active_xs, first(first(x))) + else + delete!(active_xs, first(first(x))) + end + x = @view x[2:end] + end + + if !x_less + if isleft(yᵢ) + push!(active_ys, first(first(y))) + else + delete!(active_ys, first(first(y))) + end + y = @view y[2:end] + end + + for i in active_xs + append!(result[i], active_ys) + end + end + + return unique!.(result) +end diff --git a/test/Project.toml b/test/Project.toml new file mode 100644 index 00000000..41f05e98 --- /dev/null +++ b/test/Project.toml @@ -0,0 +1,18 @@ +[deps] +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1" +Infinity = "a303e19e-6eb4-11e9-3b09-cd9505f79100" +Intervals = "d8418881-c3e1-53bb-8760-2df7ec849ed5" +InvertedIndices = "41ab1584-1d38-5bbf-9106-f11c6c58b48f" +Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" +StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53" +VisualRegressionTests = "34922c18-7c2a-561c-bac1-01e79b2c4c92" + +[compat] +Documenter = "0.23, 0.24, 0.25, 0.26, 0.27" +Infinity = "0.2.3" diff --git a/test/comparisons.jl b/test/comparisons.jl index 3b25de32..2c8b131d 100644 --- a/test/comparisons.jl +++ b/test/comparisons.jl @@ -53,9 +53,11 @@ end later = convert(B, b) expected_superset = Interval(LeftEndpoint(a), RightEndpoint(b)) expected_overlap = Interval{promote_type(eltype(a), eltype(b))}() + expected_xor = [earlier, later] @test earlier != later @test !isequal(earlier, later) + @test !issetequal(earlier, later) @test hash(earlier) != hash(later) @test isless(earlier, later) @@ -73,12 +75,39 @@ end @test isdisjoint(earlier, later) @test isdisjoint(later, earlier) - @test intersect(earlier, later) == expected_overlap - @test_throws ArgumentError merge(earlier, later) - @test union([earlier, later]) == [earlier, later] @test !overlaps(earlier, later) @test !contiguous(earlier, later) + @test_throws ArgumentError merge(earlier, later) @test superset([earlier, later]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(earlier, later) + @test intersect(earlier, later) == expected_overlap + @test_throws MethodError setdiff(earlier, later) + @test_throws MethodError setdiff(later, earlier) + @test_throws MethodError symdiff(earlier, later) + + # Using a vector of intervals as sets + @test union([earlier, later]) == [earlier, later] + @test union(IntervalSet([earlier, later])) == IntervalSet([earlier, later]) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(earlier) && isbounded(later) + @test union([earlier], [later]) == [earlier, later] + @test union(IntervalSet(earlier), IntervalSet(later)) == IntervalSet([earlier, later]) + + @test intersect([earlier], [later]) == [] + @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet() + + @test setdiff([earlier], [later]) == expected_xor[1:1] + @test setdiff(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_xor[1:1]) + + @test setdiff([later], [earlier]) == expected_xor[2:2] + @test setdiff(IntervalSet(later), IntervalSet(earlier)) == IntervalSet(expected_xor[2:2]) + + @test symdiff([earlier], [later]) == expected_xor + @test symdiff(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_xor) + end end end @@ -111,9 +140,11 @@ end later = convert(B, b) expected_superset = Interval(LeftEndpoint(a), RightEndpoint(b)) expected_overlap = Interval{promote_type(eltype(a), eltype(b))}() + expected_xor = [earlier, later] @test earlier != later @test !isequal(earlier, later) + @test !issetequal(earlier, later) @test hash(earlier) != hash(later) @test isless(earlier, later) @@ -131,12 +162,39 @@ end @test isdisjoint(earlier, later) @test isdisjoint(later, earlier) - @test intersect(earlier, later) == expected_overlap - @test_throws ArgumentError merge(earlier, later) - @test union([earlier, later]) == [earlier, later] @test !overlaps(earlier, later) @test !contiguous(earlier, later) + @test_throws ArgumentError merge(earlier, later) @test superset([earlier, later]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(earlier, later) + @test intersect(earlier, later) == expected_overlap + @test_throws MethodError setdiff(earlier, later) + @test_throws MethodError setdiff(later, earlier) + @test_throws MethodError symdiff(earlier, later) + + # Using a vector of intervals as sets + @test union([earlier, later]) == [earlier, later] + @test union(IntervalSet([earlier, later])) == IntervalSet([earlier, later]) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(earlier) && isbounded(later) + @test union([earlier], [later]) == [earlier, later] + @test union(IntervalSet(earlier), IntervalSet(later)) == IntervalSet([earlier, later]) + + @test intersect([earlier], [later]) == [] + @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet() + + @test setdiff([earlier], [later]) == expected_xor[1:1] + @test setdiff(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_xor[1:1]) + + @test setdiff([later], [earlier]) == expected_xor[2:2] + @test setdiff(IntervalSet(later), IntervalSet(earlier)) == IntervalSet(expected_xor[2:2]) + + @test symdiff([earlier], [later]) == expected_xor + @test symdiff(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_xor) + end end end @@ -169,9 +227,11 @@ end later = convert(B, b) expected_superset = Interval(LeftEndpoint(a), RightEndpoint(b)) expected_overlap = Interval{promote_type(eltype(a), eltype(b))}() + expected_xor = [earlier, later] @test earlier != later @test !isequal(earlier, later) + @test !issetequal(earlier, later) @test hash(earlier) != hash(later) @test isless(earlier, later) @@ -189,15 +249,44 @@ end @test isdisjoint(earlier, later) @test isdisjoint(later, earlier) - @test intersect(earlier, later) == expected_overlap - @test merge(earlier, later) == expected_superset - @test union([earlier, later]) == [expected_superset] @test !overlaps(earlier, later) @test contiguous(earlier, later) + @test merge(earlier, later) == expected_superset @test superset([earlier, later]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(earlier, later) + @test intersect(earlier, later) == expected_overlap + @test_throws MethodError setdiff(earlier, later) + @test_throws MethodError setdiff(later, earlier) + @test_throws MethodError symdiff(earlier, later) + + # Using a vector of intervals as sets + @test union([earlier, later]) == [expected_superset] + @test union(IntervalSet([earlier, later])) == IntervalSet([expected_superset]) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(earlier) && isbounded(later) + @test union([earlier], [later]) == expected_xor != [expected_superset] + @test union(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_superset) + + @test intersect([earlier], [later]) == [] + @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet() + + @test setdiff([earlier], [later]) == expected_xor[1:1] + @test setdiff(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_xor[1:1]) + + @test setdiff([later], [earlier]) == expected_xor[2:2] + @test setdiff(IntervalSet(later), IntervalSet(earlier)) == IntervalSet(expected_xor[2:2]) + + # TODO: Sometimes expected_xor would get mutated in this call + @test symdiff([earlier], [later]) == expected_xor != union(expected_xor) + @test symdiff(IntervalSet(earlier), IntervalSet(later)) == union(IntervalSet(expected_xor)) + end end end + # Compare two intervals which "touch" and the earlier interval includes that point: # Visualization of the finite case: # @@ -227,9 +316,11 @@ end later = convert(B, b) expected_superset = Interval(LeftEndpoint(a), RightEndpoint(b)) expected_overlap = Interval{promote_type(eltype(a), eltype(b))}() + expected_xor = [earlier, later] @test earlier != later @test !isequal(earlier, later) + @test !issetequal(earlier, later) @test hash(earlier) != hash(later) @test isless(earlier, later) @@ -247,12 +338,39 @@ end @test isdisjoint(earlier, later) @test isdisjoint(later, earlier) - @test intersect(earlier, later) == expected_overlap - @test merge(earlier, later) == expected_superset - @test union([earlier, later]) == [expected_superset] @test !overlaps(earlier, later) @test contiguous(earlier, later) + @test merge(earlier, later) == expected_superset @test superset([earlier, later]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(earlier, later) + @test intersect(earlier, later) == expected_overlap + @test_throws MethodError setdiff(earlier, later) + @test_throws MethodError setdiff(later, earlier) + @test_throws MethodError symdiff(earlier, later) + + # Using a vector of intervals as sets + @test union([earlier, later]) == [expected_superset] + @test union(IntervalSet([earlier, later])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(earlier) && isbounded(later) + @test union([earlier], [later]) == expected_xor != [expected_superset] + @test union(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_superset) + + @test intersect([earlier], [later]) == [] + @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet() + + @test setdiff([earlier], [later]) == expected_xor[1:1] + @test setdiff(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_xor[1:1]) + + @test setdiff([later], [earlier]) == expected_xor[2:2] + @test setdiff(IntervalSet(later), IntervalSet(earlier)) == IntervalSet(expected_xor[2:2]) + + @test symdiff([earlier], [later]) == expected_xor != union(expected_xor) + @test symdiff(IntervalSet(earlier), IntervalSet(later)) == union(IntervalSet(expected_xor)) + end end end @@ -286,8 +404,15 @@ end expected_superset = Interval(LeftEndpoint(a), RightEndpoint(b)) expected_overlap = Interval{Closed, Closed}(last(a), first(b)) + L, R = first(bounds_types(a)), last(bounds_types(b)) + expected_xor = [ + Interval{L, Open}(first(a), first(b)), + Interval{Open, R}(last(a), last(b)), + ] + @test earlier != later @test !isequal(earlier, later) + @test !issetequal(earlier, later) @test hash(earlier) != hash(later) @test isless(earlier, later) @@ -305,12 +430,40 @@ end @test !isdisjoint(earlier, later) @test !isdisjoint(later, earlier) - @test intersect(earlier, later) == expected_overlap - @test merge(earlier, later) == expected_superset - @test union([earlier, later]) == [expected_superset] @test overlaps(earlier, later) @test !contiguous(earlier, later) + @test merge(earlier, later) == expected_superset @test superset([earlier, later]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(earlier, later) + @test intersect(earlier, later) == expected_overlap + @test_throws MethodError setdiff(earlier, later) + @test_throws MethodError setdiff(later, earlier) + @test_throws MethodError symdiff(earlier, later) + + # Using a vector of intervals as sets + @test union([earlier, later]) == [expected_superset] + @test union(IntervalSet([earlier, later])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(earlier) && isbounded(later) + # NOTE: expected_xor may have different bounds than [earlier, later] + @test union([earlier], [later]) == [earlier, later] != [expected_superset] + @test union(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_superset) + + @test intersect([earlier], [later]) == [] != [expected_overlap] + @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_overlap) + + @test setdiff([earlier], [later]) == [earlier] != expected_xor[1:1] + @test setdiff(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_xor[1:1]) + + @test setdiff([later], [earlier]) == [later] != expected_xor[2:2] + @test setdiff(IntervalSet(later), IntervalSet(earlier)) == IntervalSet(expected_xor[2:2]) + + @test symdiff([earlier], [later]) == [earlier, later] + @test symdiff(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_xor) + end end end @@ -344,8 +497,15 @@ end expected_superset = Interval(LeftEndpoint(a), RightEndpoint(b)) expected_overlap = Interval(LeftEndpoint(b), RightEndpoint(a)) + L, R = first(bounds_types(a)), last(bounds_types(b)) + expected_xor = [ + Interval{L, Open}(first(a), first(b)), + Interval{Open, R}(last(a), last(b)), + ] + @test earlier != later @test !isequal(earlier, later) + @test !issetequal(earlier, later) @test hash(earlier) != hash(later) @test isless(earlier, later) @@ -363,12 +523,40 @@ end @test !isdisjoint(earlier, later) @test !isdisjoint(later, earlier) - @test intersect(earlier, later) == expected_overlap - @test merge(earlier, later) == expected_superset - @test union([earlier, later]) == [expected_superset] @test overlaps(earlier, later) @test !contiguous(earlier, later) + @test merge(earlier, later) == expected_superset @test superset([earlier, later]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(earlier, later) + @test intersect(earlier, later) == expected_overlap + @test_throws MethodError setdiff(earlier, later) + @test_throws MethodError setdiff(later, earlier) + @test_throws MethodError symdiff(earlier, later) + + # Using a vector of intervals as sets + @test union([earlier, later]) == [expected_superset] + @test union(IntervalSet([earlier, later])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(earlier) && isbounded(later) + # NOTE: expected_xor may have different bounds than [earlier, later] + @test union([earlier], [later]) == [earlier, later] != [expected_superset] + @test union(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_superset) + + @test intersect([earlier], [later]) == [] != [expected_overlap] + @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_overlap) + + @test setdiff([earlier], [later]) == [earlier] != expected_xor[1:1] + @test setdiff(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_xor[1:1]) + + @test setdiff([later], [earlier]) == [later] != expected_xor[2:2] + @test setdiff(IntervalSet(later), IntervalSet(earlier)) == IntervalSet(expected_xor[2:2]) + + @test symdiff([earlier], [later]) == [earlier, later] != expected_xor + @test symdiff(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_xor) + end end end @@ -392,6 +580,7 @@ end @test a == b @test isequal(a, b) + @test issetequal(b, a) @test hash(a) == hash(b) @test !isless(a, b) @@ -409,12 +598,39 @@ end @test !isdisjoint(a, b) @test !isdisjoint(b, a) - @test intersect(a, b) == expected_overlap - @test merge(a, b) == expected_superset - @test union([a, b]) == [expected_superset] @test overlaps(a, b) @test !contiguous(a, b) + @test merge(a, b) == expected_superset @test superset([a, b]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(a, b) + @test intersect(a, b) == expected_overlap + @test_throws MethodError setdiff(a, b) + @test_throws MethodError setdiff(b, a) + @test_throws MethodError symdiff(a, b) + + # Using a vector of intervals as sets + @test union([a, b]) == [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) + + @test intersect([a], [b]) == [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) == [] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() + + @test setdiff([b], [a]) == [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() + + @test symdiff([a], [b]) == [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() + end end end @@ -435,9 +651,11 @@ end b = convert(B, b) expected_superset = Interval(LeftEndpoint(a), RightEndpoint(a)) expected_overlap = Interval(LeftEndpoint(b), RightEndpoint(b)) + expected_xor = [Interval{Closed, Closed}(first(a), first(a))] @test a != b @test !isequal(a, b) + @test !issetequal(a, b) @test hash(a) != hash(b) @test isless(a, b) @@ -455,12 +673,42 @@ end @test !isdisjoint(a, b) @test !isdisjoint(b, a) - @test intersect(a, b) == expected_overlap - @test merge(a, b) == expected_superset - @test union([a, b]) == [expected_superset] @test overlaps(a, b) @test !contiguous(a, b) + @test merge(a, b) == expected_superset @test superset([a, b]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(a, b) + @test intersect(a, b) == expected_overlap + @test_throws MethodError setdiff(a, b) + @test_throws MethodError setdiff(b, a) + @test_throws MethodError symdiff(a, b) + + # Using a vector of intervals as sets + @test union([a, b]) == [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + # TODO: will have to think carefully about the `expected_` variables + # when we allow for unbounded values + if isbounded(a) && isbounded(b) + # NOTE: expected_xor may have different bounds than [a, b] + @test union([a], [b]) == [a, b] != [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) + + @test intersect([a], [b]) == [] != [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) == [a] != expected_xor[1:1] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor[1:1]) + + @test setdiff([b], [a]) == [b] != [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() + + @test symdiff([a], [b]) == [a, b] != expected_xor + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) + end end end @@ -481,9 +729,11 @@ end b = convert(B, b) expected_superset = Interval(LeftEndpoint(a), RightEndpoint(a)) expected_overlap = Interval(LeftEndpoint(b), RightEndpoint(b)) + expected_xor = [Interval{Closed, Closed}(last(a), last(a))] @test a != b @test !isequal(a, b) + @test !issetequal(a, b) @test hash(a) != hash(b) @test !isless(a, b) @@ -501,12 +751,42 @@ end @test !isdisjoint(a, b) @test !isdisjoint(b, a) - @test intersect(a, b) == expected_overlap - @test merge(a, b) == expected_superset - @test union([a, b]) == [expected_superset] @test overlaps(a, b) @test !contiguous(a, b) + @test merge(a, b) == expected_superset @test superset([a, b]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(a, b) + @test intersect(a, b) == expected_overlap + @test_throws MethodError setdiff(a, b) + @test_throws MethodError setdiff(b, a) + @test_throws MethodError symdiff(a, b) + + # Using a vector of intervals as sets + @test union([a, b]) == [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + # TODO: will have to think carefully about the `expected_` variables + # when we allow for unbounded values + if isbounded(a) && isbounded(b) + # NOTE: expected_xor may have different bounds than [a, b] + @test union([a], [b]) == [a, b] != [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) + + @test intersect([a], [b]) == [] != [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) == [a] != expected_xor[1:1] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor[1:1]) + + @test setdiff([b], [a]) == [b] != [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() + + @test symdiff([a], [b]) == [a, b] != expected_xor + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) + end end end @@ -516,7 +796,8 @@ end Interval{Closed, Closed}(l, u), Interval{Open, Open}(l, u), ] - for (l, u) in product((1, -Inf, -∞), (5, Inf, ∞)) + # for (l, u) in product((1, -Inf, -∞), (5, Inf, ∞)) + for (l, u) in product((1,), (5,)) ) @testset "$a vs. $b" for (a, b) in test_intervals @@ -527,9 +808,14 @@ end b = convert(B, b) expected_superset = Interval(LeftEndpoint(a), RightEndpoint(a)) expected_overlap = Interval(LeftEndpoint(b), RightEndpoint(b)) + expected_xor = [ + Interval{Closed, Closed}(first(a), first(a)), + Interval{Closed, Closed}(last(a), last(a)), + ] @test a != b @test !isequal(a, b) + @test !issetequal(a, b) @test hash(a) != hash(b) @test isless(a, b) @@ -547,12 +833,42 @@ end @test !isdisjoint(a, b) @test !isdisjoint(b, a) - @test intersect(a, b) == expected_overlap - @test merge(a, b) == expected_superset - @test union([a, b]) == [expected_superset] @test overlaps(a, b) @test !contiguous(a, b) + @test merge(a, b) == expected_superset @test superset([a, b]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(a, b) + @test intersect(a, b) == expected_overlap + @test_throws MethodError setdiff(a, b) + @test_throws MethodError setdiff(b, a) + @test_throws MethodError symdiff(a, b) + + # Using a vector of intervals as sets + @test union([a, b]) == [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + # TODO: will have to think carefully about the `expected_` variables + # when we allow for unbounded values + if isbounded(a) && isbounded(b) + # NOTE: expected_xor may have different bounds than [a, b] + @test union([a], [b]) == [a, b] != [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) + + @test intersect([a], [b]) == [] != [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) == [a] != expected_xor + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) + + @test setdiff([b], [a]) == [b] != [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() + + @test symdiff([a], [b]) == [a, b] != expected_xor + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) + end end end @@ -573,9 +889,13 @@ end b = convert(B, b) expected_superset = Interval(LeftEndpoint(b), RightEndpoint(b)) expected_overlap = Interval(LeftEndpoint(a), RightEndpoint(a)) + expected_xor = [ + Interval{Closed, Closed}(last(b), last(b)), + ] @test a != b @test !isequal(a, b) + @test !issetequal(a, b) @test hash(a) != hash(b) @test !isless(a, b) @@ -593,12 +913,40 @@ end @test !isdisjoint(a, b) @test !isdisjoint(b, a) - @test intersect(a, b) == expected_overlap - @test merge(a, b) == expected_superset - @test union([a, b]) == [expected_superset] @test overlaps(a, b) @test !contiguous(a, b) + @test merge(a, b) == expected_superset @test superset([a, b]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(a, b) + @test intersect(a, b) == expected_overlap + @test_throws MethodError setdiff(a, b) + @test_throws MethodError setdiff(b, a) + @test_throws MethodError symdiff(a, b) + + # Using a vector of intervals as sets + @test union([a, b]) == [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + # NOTE: expected_xor may have different bounds than [a, b] + @test union([a], [b]) == [a, b] != [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) + + @test intersect([a], [b]) == [] != [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) == [a] != [] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() + + @test setdiff([b], [a]) == [b] != expected_xor[1:1] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(expected_xor[1:1]) + + @test symdiff([a], [b]) == [a, b] != expected_xor + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) + end end end @@ -619,9 +967,13 @@ end b = convert(B, b) expected_superset = Interval(LeftEndpoint(b), RightEndpoint(b)) expected_overlap = Interval(LeftEndpoint(a), RightEndpoint(a)) + expected_xor = [ + Interval{Closed, Closed}(first(b), first(b)), + ] @test a != b @test !isequal(a, b) + @test !issetequal(a, b) @test hash(a) != hash(b) @test !isless(a, b) @@ -639,12 +991,42 @@ end @test !isdisjoint(a, b) @test !isdisjoint(b, a) - @test intersect(a, b) == expected_overlap - @test merge(a, b) == expected_superset - @test union([a, b]) == [expected_superset] @test overlaps(a, b) @test !contiguous(a, b) + @test merge(a, b) == expected_superset @test superset([a, b]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(a, b) + @test intersect(a, b) == expected_overlap + @test_throws MethodError setdiff(a, b) + @test_throws MethodError setdiff(b, a) + @test_throws MethodError symdiff(a, b) + + # Using a vector of intervals as sets + @test union([a, b]) == [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) + @test intersect([a], [b]) != [expected_overlap] + @test_broken intersect(IntervalSet([a, b])) == IntervalSet(expected_overlap) # Internal type issue + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + # NOTE: expected_xor may have different bounds than [a, b] + @test union([a], [b]) == [a, b] != [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) + + @test intersect([a], [b]) == [] != [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) == [a] != [] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() + + @test setdiff([b], [a]) == [b] != expected_xor[1:1] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(expected_xor[1:1]) + + @test symdiff([a], [b]) == [a, b] != expected_xor + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) + end end end @@ -668,6 +1050,7 @@ end @test a == b @test isequal(a, b) + @test issetequal(a, b) @test hash(a) == hash(b) @test !isless(a, b) @@ -685,12 +1068,39 @@ end @test !isdisjoint(a, b) @test !isdisjoint(b, a) - @test intersect(a, b) == expected_overlap - @test merge(a, b) == expected_superset - @test union([a, b]) == [expected_superset] @test overlaps(a, b) @test !contiguous(a, b) + @test merge(a, b) == expected_superset @test superset([a, b]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(a, b) + @test intersect(a, b) == expected_overlap + @test_throws MethodError setdiff(a, b) + @test_throws MethodError setdiff(b, a) + @test_throws MethodError symdiff(a, b) + + # Using a vector of intervals as sets + @test union([a, b]) == [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) + + @test intersect([a], [b]) == [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) == [] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() + + @test setdiff([b], [a]) == [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() + + @test symdiff([a], [b]) == [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() + end end end @@ -711,9 +1121,11 @@ end b = convert(B, b) expected_superset = Interval(LeftEndpoint(a), RightEndpoint(a)) expected_overlap = Interval(LeftEndpoint(b), RightEndpoint(b)) + expected_xor = [Interval{Closed, Open}(first(a), first(a))] @test a != b @test !isequal(a, b) + @test !issetequal(a, b) @test hash(a) != hash(b) @test isless(a, b) @@ -731,12 +1143,39 @@ end @test !isdisjoint(a, b) @test !isdisjoint(b, a) - @test intersect(a, b) == expected_overlap - @test merge(a, b) == expected_superset - @test union([a, b]) == [expected_superset] @test overlaps(a, b) @test !contiguous(a, b) + @test merge(a, b) == expected_superset @test superset([a, b]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(a, b) + @test intersect(a, b) == expected_overlap + @test_throws MethodError setdiff(a, b) + @test_throws MethodError setdiff(b, a) + @test_throws MethodError symdiff(a, b) + + # Using a vector of intervals as sets + @test union([a, b]) == [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) + + @test intersect([a], [b]) == [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) == [a] != expected_xor[1:1] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor[1:1]) + + @test setdiff([b], [a]) == [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() + + @test symdiff([a], [b]) == [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() + end end end @@ -757,9 +1196,11 @@ end b = convert(B, b) expected_superset = Interval(LeftEndpoint(a), RightEndpoint(a)) expected_overlap = Interval(LeftEndpoint(b), RightEndpoint(b)) + expected_xor = [Interval{Open, Closed}(last(a), last(a))] @test a != b @test !isequal(a, b) + @test !issetequal(a, b) @test hash(a) != hash(b) @test !isless(a, b) @@ -777,12 +1218,39 @@ end @test !isdisjoint(a, b) @test !isdisjoint(b, a) - @test intersect(a, b) == expected_overlap - @test merge(a, b) == expected_superset - @test union([a, b]) == [expected_superset] @test overlaps(a, b) @test !contiguous(a, b) + @test merge(a, b) == expected_superset @test superset([a, b]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(a, b) + @test intersect(a, b) == expected_overlap + @test_throws MethodError setdiff(a, b) + @test_throws MethodError setdiff(b, a) + @test_throws MethodError symdiff(a, b) + + # Using a vector of intervals as sets + @test union([a, b]) == [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) + + @test intersect([a], [b]) == [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) == [a] != expected_xor[1:1] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor[1:1]) + + @test setdiff([b], [a]) == [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() + + @test symdiff([a], [b]) == [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() + end end end @@ -805,6 +1273,7 @@ end @test a == b @test isequal(a, b) + @test issetequal(a, b) @test hash(a) == hash(b) @test !isless(a, b) @@ -822,12 +1291,39 @@ end @test !isdisjoint(a, b) @test !isdisjoint(b, a) - @test intersect(a, b) == expected_overlap - @test merge(a, b) == expected_superset - @test union([a, b]) == [expected_superset] @test overlaps(a, b) @test !contiguous(a, b) + @test merge(a, b) == expected_superset @test superset([a, b]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(a, b) + @test intersect(a, b) == expected_overlap + @test_throws MethodError setdiff(a, b) + @test_throws MethodError setdiff(b, a) + @test_throws MethodError symdiff(a, b) + + # Using a vector of intervals as sets + @test union([a, b]) == [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) + + @test intersect([a], [b]) == [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) == [] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() + + @test setdiff([b], [a]) == [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() + + @test symdiff([a], [b]) == [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() + end end end @@ -841,6 +1337,7 @@ end @test a == b @test !isequal(a, b) + @test issetequal(a, b) @test hash(a) != hash(b) # All other comparison should still work as expected @@ -859,12 +1356,34 @@ end @test !isdisjoint(a, b) @test !isdisjoint(b, a) - @test intersect(a, b) == expected_overlap - @test merge(a, b) == expected_superset - @test union([a, b]) == [expected_superset] @test overlaps(a, b) @test !contiguous(a, b) + @test merge(a, b) == expected_superset @test superset([a, b]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(a, b) + @test intersect(a, b) == expected_overlap + @test_throws MethodError setdiff(a, b) + @test_throws MethodError setdiff(b, a) + @test_throws MethodError symdiff(a, b) + + # Using a vector of intervals as sets + @test union([a, b]) == [expected_superset] + @test union([a], [b]) == [a, b] != [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) + + @test intersect([a], [b]) == [] != [expected_overlap] + @test intersect(IntervalSet([a, b])) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) == [a] != [] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() + + @test setdiff([b], [a]) == [b] != [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() + + @test symdiff([a], [b]) == [a, b] != [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() end end @@ -905,8 +1424,15 @@ end expected_superset = Interval(larger) expected_overlap = Interval(smaller) + L, R = bounds_types(larger) + expected_xor = [ + Interval{L, Open}(first(larger), first(smaller)), + Interval{Open, R}(last(smaller), last(larger)), + ] + @test smaller != larger @test !isequal(smaller, larger) + @test !issetequal(smaller, larger) @test hash(smaller) != hash(larger) @test !isless(smaller, larger) @@ -921,15 +1447,43 @@ end @test issubset(smaller, larger) @test !issubset(larger, smaller) - @test !isdisjoint(a, b) - @test !isdisjoint(b, a) + @test !isdisjoint(smaller, larger) + @test !isdisjoint(larger, smaller) - @test intersect(smaller, larger) == expected_overlap - @test merge(smaller, larger) == expected_superset - @test union([smaller, larger]) == [expected_superset] @test overlaps(smaller, larger) @test !contiguous(smaller, larger) + @test merge(a, b) == expected_superset @test superset([smaller, larger]) == expected_superset + + # Intervals acting as sets. Functions should return a single interval + @test_throws MethodError union(a, b) + @test intersect(a, b) == expected_overlap + @test_throws MethodError setdiff(a, b) + @test_throws MethodError setdiff(b, a) + @test_throws MethodError symdiff(a, b) + + # Using a vector of intervals as sets + @test union([a, b]) == [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + # NOTE: expected_xor may have different bounds than [a, b] + @test union([a], [b]) == [a, b] != [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) + + @test intersect([a], [b]) == [] != [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) == [a] != [] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() + + @test setdiff([b], [a]) == [b] != expected_xor[1:2] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(expected_xor[1:2]) + + @test symdiff([a], [b]) == [a, b] != expected_xor + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) + end end end end diff --git a/test/interval.jl b/test/interval.jl index 0025e14e..b85814d6 100644 --- a/test/interval.jl +++ b/test/interval.jl @@ -589,6 +589,8 @@ @test in(b - unit, interval) || isinf(b) @test !in(b + unit, interval) || isinf(b) + # As an Interval instance is itself a collection one could expect this to return + # `true`. The correct check in this case is `issubset`. @test_throws ArgumentError (in(Interval(a, b), Interval(a, b))) end end @@ -763,6 +765,7 @@ Interval{Open, Open}(-10, -1), Interval{Open, Open}(13, 20), ] + @test union!(intervals) == expected @test intervals == expected diff --git a/test/runtests.jl b/test/runtests.jl index c40d1f4e..14eda85a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -18,6 +18,7 @@ include("test_utils.jl") include("interval.jl") include("anchoredinterval.jl") include("comparisons.jl") + include("sets.jl") include("plotting.jl") # Note: The output of the doctests currently requires a newer version of Julia diff --git a/test/sets.jl b/test/sets.jl new file mode 100644 index 00000000..e28e24fc --- /dev/null +++ b/test/sets.jl @@ -0,0 +1,162 @@ +using Intervals +using InvertedIndices +using Random +using StableRNGs + +using Intervals: TrackEachEndpoint, TrackLeftOpen, TrackRightOpen, endpoint_tracking, + find_intersections + +@testset "Endpoint Tracking" begin + @test endpoint_tracking( + Interval{Int, Open, Closed}, + Interval{Float64, Open, Closed}, + ) == TrackLeftOpen{Float64}() + + @test endpoint_tracking( + Interval{Int, Closed, Open}, + Interval{Float64, Closed, Open}, + ) == TrackRightOpen{Float64}() + + # Fallback tracking for all other bound combinations + @test endpoint_tracking( + Interval{Int, Closed, Closed}, + Interval{Float64, Closed, Closed}, + ) == TrackEachEndpoint() +end + +@testset "Set operations" begin + area(x::Interval) = last(x) - first(x) + # note: `mapreduce` fails here for empty vectors + area(x::AbstractVector{<:AbstractInterval{T}}) where T = mapreduce(area, +, x, init=zero(T)) + area(x::IntervalSet) = area(x.items) + area(x) = isempty(x) ? 0 : error("Undefined area for object of type $(typeof(x))") + myunion(x::Interval) = x + myunion(x::AbstractVector{<:Interval}) = union(x) + myunion(x::IntervalSet) = union(x) + + rand_bound_type(rng) = rand(rng, (Closed, Open)) + + # verify case where we interpret array as a set of elements (rather than an + # interval-bound point set) + @test intersect([1..2, 2..3, 3..4, 4..5], [2..3, 3..4]) == [2..3, 3..4] + + # verify that elements are in / subsets of interval sets + @test 2 ∈ IntervalSet([1..3, 5..10]) + @test 0 ∉ IntervalSet([1..3, 5..10]) + @test 4 ∉ IntervalSet([1..3, 5..10]) + @test 11 ∉ IntervalSet([1..3, 5..10]) + @test issubset(2, IntervalSet([1..3, 5..10])) + @test !issubset(0, IntervalSet([1..3, 5..10])) + @test !issubset(4, IntervalSet([1..3, 5..10])) + @test !issubset(11, IntervalSet([1..3, 5..10])) + @test issubset(2, IntervalSet([1.0 .. 3.0, 5.0 .. 10.0])) + + function testsets(a, b) + @test area(a ∪ b) ≤ area(myunion(a)) + area(myunion(b)) + @test area(setdiff(a, b)) ≤ area(myunion(a)) + @test area(a ∩ b) + area(symdiff(a, b)) == area(union(a,b)) + @test a ⊆ (a ∪ b) + @test !issetequal(a, setdiff(a, b)) + @test issetequal(a, a) + @test isdisjoint(setdiff(a, b), b) + @test !isdisjoint(a, a) + + intersections = find_intersections(convert(Array, a), convert(Array, b)) + + # verify that all indices returned in `find_intersections` correspond to sets + # in b that overlap with the given set in a + @test all(enumerate(intersections)) do (i, x) + isempty(x) || !isempty(intersect(IntervalSet(a.items[i]), IntervalSet(b.items[x]))) + end + + # verify that all indices not returned in `find_intersections` correspond to + # sets in b that do not overlap with the given set in akk + @test all(enumerate(intersections)) do (i, x) + isempty(intersect(IntervalSet(a.items[i]), IntervalSet(b.items[Not(x)]))) + end + + # Test f(interval, intervalset) and f(intervalset, interval) methods work the same + # as the regular f(intervalset, intervalset) methods + if a isa IntervalSet && b isa IntervalSet + for f in (intersect, union, setdiff, symdiff, isdisjoint, issubset) + # We'll just use the first and last entries as sample intervals for these + # tests rather than an exhausting search + x = first(a.items) + @test f(x, b) == f(IntervalSet([x]), b) + x = last(a.items) + @test f(x, b) == f(IntervalSet([x]), b) + + y = first(b.items) + @test f(a, y) == f(a, IntervalSet([y])) + y = last(b.items) + @test f(a, y) == f(a, IntervalSet([y])) + end + end + end + + # verify empty interval set + @test isempty(union(Interval[])) + + # a few taylored interval sets + a = IntervalSet([Interval(i, i + 3) for i in 1:5:15]) + b = IntervalSet(a.items .+ (1:2:5)) + @test all(x -> first(x) ∈ a, a.items) + testsets(a, b) + testsets(IntervalSet(a.items[1]), b) + testsets(a, IntervalSet(b.items[1])) + + # verify that `last` need not be ordered + intervals = IntervalSet([Interval(0, 5), Interval(0, 3)]) + @test superset(union(intervals)) == Interval(0, 5) + + # try out some more involved (random) intervals + rng = StableRNG(2020_10_21) + n = 25 + starts = rand(rng, 1:100_000, n) + ends = starts .+ rand(rng, 1:10_000, n) + offsets = round.(Int, (ends .- starts) .* (2 .* rand(rng, n) .- 1)) + + a = IntervalSet(Interval.(starts, ends)) + b = IntervalSet(Interval.(starts .+ offsets, ends .+ offsets)) + @test all(x -> first(x) ∈ a, a.items) + testsets(a, b) + testsets(IntervalSet(first(a.items)), b) + testsets(a, IntervalSet(first(b.items))) + + a = IntervalSet(Interval{rand_bound_type(rng), rand_bound_type(rng)}.(starts, ends)) + b = IntervalSet(Interval{rand_bound_type(rng), rand_bound_type(rng)}.(starts .+ offsets, ends .+ offsets)) + testsets(a, b) + testsets(IntervalSet(first(a.items)), b) + testsets(a, IntervalSet(first(b.items))) + + a = IntervalSet(Interval{Closed, Open}.(starts, ends)) + b = IntervalSet(Interval{Closed, Open}.(starts .+ offsets, ends .+ offsets)) + @test Intervals.endpoint_tracking(a, b) isa Intervals.TrackStatically + @test Intervals.endpoint_tracking(IntervalSet(first(a.items)), b) isa Intervals.TrackStatically + @test Intervals.endpoint_tracking(a, IntervalSet(first(b.items))) isa Intervals.TrackStatically + testsets(a, b) + testsets(IntervalSet(first(a.items)), b) + testsets(a, IntervalSet(first(b.items))) + + a = IntervalSet(Interval{Open, Closed}.(starts, ends)) + b = IntervalSet(Interval{Open, Closed}.(starts .+ offsets, ends .+ offsets)) + @test Intervals.endpoint_tracking(a, b) isa Intervals.TrackStatically + @test Intervals.endpoint_tracking(IntervalSet(first(a.items)), b) isa Intervals.TrackStatically + @test Intervals.endpoint_tracking(a, IntervalSet(first(b.items))) isa Intervals.TrackStatically + testsets(a, b) + testsets(IntervalSet(first(a.items)), b) + testsets(a, IntervalSet(first(b.items))) + + randint(x::Interval) = Interval{rand_bound_type(rng), rand_bound_type(rng)}(first(x), last(x)) + leftint(x::Interval) = Interval{Closed, Open}(first(x), last(x)) + rightint(x::Interval) = Interval{Open, Closed}(first(x), last(x)) + + a = IntervalSet(Interval{Closed, Open}.(starts, ends)) + b = IntervalSet(Interval{Closed, Open}.(starts .+ offsets, ends .+ offsets)) + testsets(a, IntervalSet(randint.(b.items))) + testsets(IntervalSet(first(a.items)), IntervalSet(randint.(b.items))) + testsets(a, IntervalSet(leftint.(first(b.items)))) + testsets(a, IntervalSet(rightint.(b.items))) + testsets(IntervalSet(first(a.items)), IntervalSet(rightint.(b.items))) + testsets(a, IntervalSet(rightint.(first(b.items)))) +end