From b6272ec3faf34280bb382b3e2ed56b93d3ae9cd1 Mon Sep 17 00:00:00 2001 From: Rory Finnegan Date: Wed, 1 Jun 2022 09:55:10 -0700 Subject: [PATCH 01/30] Revert "Revert "Multi-interval set operators"" --- .JuliaFormatter.toml | 2 + .gitignore | 1 + Project.toml | 15 +- docs/src/index.md | 84 ++++--- src/Intervals.jl | 1 + src/endpoint.jl | 3 +- src/interval.jl | 54 ----- src/interval_sets.jl | 405 ++++++++++++++++++++++++++++++++++ test/Project.toml | 18 ++ test/comparisons.jl | 514 ++++++++++++++++++++++++++++++++++++++----- test/interval.jl | 2 + test/runtests.jl | 1 + test/sets.jl | 128 +++++++++++ 13 files changed, 1068 insertions(+), 160 deletions(-) create mode 100644 .JuliaFormatter.toml create mode 100644 src/interval_sets.jl create mode 100644 test/Project.toml create mode 100644 test/sets.jl 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/.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..310f0476 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.7.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..4fc13736 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,6 +239,30 @@ 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 diff --git a/src/Intervals.jl b/src/Intervals.jl index 0c606436..fe00041a 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") 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..e0760a7b --- /dev/null +++ b/src/interval_sets.jl @@ -0,0 +1,405 @@ + +###### Set-related Helpers ##### + +const IntervalSet = AbstractVector{<:AbstractInterval} +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::AbstractVector, b::AbstractVector) = endpoint_tracking(eltype(a), eltype(b)) +endpoint_tracking(a::AbstractInterval, b::AbstractInterval) = endpoint_tracking(typeof(a), typeof(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 +unbunch_by_fn(_) = identity +function unbunch(intervals::Union{AbstractIntervals, Base.Iterators.Enumerate{<:AbstractIntervals}}, + 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::AbstractIntervals, b::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 interval_type(tracking)[] + return map(Iterators.partition(endpoints, 2)) do pair + return Interval(pair..., tracking) + end +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::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 + +# allow a concretely typed array for `Interval` objects (as opposed to e.g. anchored intervals +# which may change type during the union process) +function Base.union(intervals::AbstractVector{T}) where T <: Interval + return union!(copy(intervals)) +end + +""" + union!(intervals::AbstractVector{<:AbstractInterval}) + +Flattens a vector of overlapping intervals in-place to be a smaller vector containing only +non-overlapping intervals. +""" +function Base.union!(intervals::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 + + +# 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.issubset(x::AbstractIntervals, y::AbstractIntervals) = isempty(setdiff(x, y)) +Base.isdisjoint(x::AbstractIntervals, y::AbstractIntervals) = isempty(intersect(x, y)) + +function Base.issetequal(x::AbstractIntervals, y::AbstractIntervals) + x, y, tracking = unbunch(union(vcat(x)), union(vcat(y))) + return x == y || all(isempty, bunch(x, tracking)) && all(isempty, bunch(y, tracking)) +end + +# 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::Union{AbstractInterval, AbstractVector{<:AbstractInterval}}, + y::Union{AbstractInterval, 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]`. +""" +function find_intersections(x_::AbstractIntervals, y_::AbstractIntervals) + xa, ya = vcat(x_), vcat(y_) + tracking = endpoint_tracking(xa, ya) + lt = intersection_isless_fn(tracking) + x = unbunch(enumerate(xa), tracking; lt) + y = unbunch(enumerate(ya), tracking; lt) + result = [Vector{Int}() for _ in 1:length(xa)] + + return find_intersections_helper!(result, x, y, 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..1ddcd7ca 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,31 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(earlier) && isbounded(later) + @test union([earlier], [later]) == [earlier, later] + @test intersect([earlier], [later]) == [] + + @test setdiff([earlier], [later]) == expected_xor[1:1] + @test setdiff([later], [earlier]) == expected_xor[2:2] + + @test symdiff([earlier], [later]) == expected_xor + end end end @@ -111,9 +132,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 +154,31 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(earlier) && isbounded(later) + @test union([earlier], [later]) == [earlier, later] + @test intersect([earlier], [later]) == [] + + @test setdiff([earlier], [later]) == expected_xor[1:1] + @test setdiff([later], [earlier]) == expected_xor[2:2] + + @test symdiff([earlier], [later]) == expected_xor + end end end @@ -169,9 +211,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,12 +233,31 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(earlier) && isbounded(later) + @test union([earlier], [later]) == [expected_superset] + @test intersect([earlier], [later]) == [] + + @test setdiff([earlier], [later]) == expected_xor[1:1] + @test setdiff([later], [earlier]) == expected_xor[2:2] + + @test symdiff([earlier], [later]) == union(expected_xor) + end end end @@ -227,9 +290,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 +312,31 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(earlier) && isbounded(later) + @test union([earlier], [later]) == [expected_superset] + @test intersect([earlier], [later]) == [] + + @test setdiff([earlier], [later]) == expected_xor[1:1] + @test setdiff([later], [earlier]) == expected_xor[2:2] + + @test symdiff([earlier], [later]) == union(expected_xor) + end end end @@ -286,8 +370,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 +396,31 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(earlier) && isbounded(later) + @test union([earlier], [later]) == [expected_superset] + @test intersect([earlier], [later]) == [expected_overlap] + + @test setdiff([earlier], [later]) == expected_xor[1:1] + @test setdiff([later], [earlier]) == expected_xor[2:2] + + @test symdiff([earlier], [later]) == expected_xor + end end end @@ -344,8 +454,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 +480,31 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(earlier) && isbounded(later) + @test union([earlier], [later]) == [expected_superset] + @test intersect([earlier], [later]) == [expected_overlap] + + @test setdiff([earlier], [later]) == expected_xor[1:1] + @test setdiff([later], [earlier]) == expected_xor[2:2] + + @test symdiff([earlier], [later]) == expected_xor + end end end @@ -392,6 +528,7 @@ end @test a == b @test isequal(a, b) + @test issetequal(b, a) @test hash(a) == hash(b) @test !isless(a, b) @@ -409,12 +546,31 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + @test intersect([a], [b]) == [expected_overlap] + + @test setdiff([a], [b]) == [] + @test setdiff([b], [a]) == [] + + @test symdiff([a], [b]) == [] + end end end @@ -435,9 +591,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 +613,33 @@ 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] + + # 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) + @test union([a], [b]) == [expected_superset] + @test intersect([a], [b]) == [expected_overlap] + + @test setdiff([a], [b]) == expected_xor[1:1] + @test setdiff([b], [a]) == [] + + @test symdiff([a], [b]) == expected_xor + end end end @@ -481,9 +660,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 +682,33 @@ 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] + + # 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) + @test union([a], [b]) == [expected_superset] + @test intersect([a], [b]) == [expected_overlap] + + @test setdiff([a], [b]) == expected_xor[1:1] + @test setdiff([b], [a]) == [] + + @test symdiff([a], [b]) == expected_xor + end end end @@ -516,7 +718,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 +730,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 +755,33 @@ 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] + + # 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) + @test union([a], [b]) == [expected_superset] + @test intersect([a], [b]) == [expected_overlap] + + @test setdiff([a], [b]) == expected_xor + @test setdiff([b], [a]) == [] + + @test symdiff([a], [b]) == expected_xor + end end end @@ -573,9 +802,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 +826,31 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + @test intersect([a], [b]) == [expected_overlap] + + @test setdiff([a], [b]) == [] + @test setdiff([b], [a]) == expected_xor[1:1] + + @test symdiff([a], [b]) == expected_xor + end end end @@ -619,9 +871,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 +895,31 @@ 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 intersect([a], [b]) == [expected_overlap] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + + @test setdiff([a], [b]) == [] + @test setdiff([b], [a]) == expected_xor[1:1] + + @test symdiff([a], [b]) == expected_xor + end end end @@ -668,6 +943,7 @@ end @test a == b @test isequal(a, b) + @test issetequal(a, b) @test hash(a) == hash(b) @test !isless(a, b) @@ -685,12 +961,31 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + @test intersect([a], [b]) == [expected_overlap] + + @test setdiff([a], [b]) == [] + @test setdiff([b], [a]) == [] + + @test symdiff([a], [b]) == [] + end end end @@ -711,9 +1006,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 +1028,31 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + @test intersect([a], [b]) == [expected_overlap] + + @test setdiff([a], [b]) == expected_xor[1:1] + @test setdiff([b], [a]) == [] + + @test symdiff([a], [b]) == [] + end end end @@ -757,9 +1073,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 +1095,31 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + @test intersect([a], [b]) == [expected_overlap] + + @test setdiff([a], [b]) == expected_xor[1:1] + @test setdiff([b], [a]) == [] + + @test symdiff([a], [b]) == [] + end end end @@ -805,6 +1142,7 @@ end @test a == b @test isequal(a, b) + @test issetequal(a, b) @test hash(a) == hash(b) @test !isless(a, b) @@ -822,12 +1160,31 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + @test intersect([a], [b]) == [expected_overlap] + + @test setdiff([a], [b]) == [] + @test setdiff([b], [a]) == [] + + @test symdiff([a], [b]) == [] + end end end @@ -841,6 +1198,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 +1217,28 @@ 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]) == [expected_superset] + + @test intersect([a], [b]) == [expected_overlap] + + @test setdiff([a], [b]) == [] + @test setdiff([b], [a]) == [] + + @test symdiff([a], [b]) == [] end end @@ -905,8 +1279,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 +1302,34 @@ 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] + + # TODO: These functions should be compatible with unbounded intervals + if isbounded(a) && isbounded(b) + @test union([a], [b]) == [expected_superset] + @test intersect([a], [b]) == [expected_overlap] + + @test setdiff([a], [b]) == [] + @test setdiff([b], [a]) == expected_xor[1:2] + + @test symdiff([a], [b]) == expected_xor + end end end end diff --git a/test/interval.jl b/test/interval.jl index 0025e14e..197a06a9 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 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..839a38c4 --- /dev/null +++ b/test/sets.jl @@ -0,0 +1,128 @@ +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) = isempty(x) ? 0 : error("Undefined area for object of type $(typeof(x))") + myunion(x::Interval) = x + myunion(x::AbstractVector{<:Interval}) = union(x) + + rand_bound_type(rng) = rand(rng, (Closed, Open)) + + 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(a, b) + a, b = vcat(a), vcat(b) # Always make `a` / `b` vectors + + # 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(a[i:i], b[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(a[i:i], b[Not(x)])) + end + end + + # verify empty interval set + @test isempty(union(Interval[])) + + # a few taylored interval sets + a = [Interval(i, i + 3) for i in 1:5:15] + b = a .+ (1:2:5) + @test all(first.(a) .∈ a) + testsets(a, b) + testsets(a[1:1], b) + testsets(a, b[1:1]) + + # verify that `last` need not be ordered + intervals = [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 = Interval.(starts, ends) + b = Interval.(starts .+ offsets, ends .+ offsets) + @test all(first.(a) .∈ a) + testsets(a, b) + testsets(a[1:1], b) + testsets(a, b[1:1]) + + a = Interval{rand_bound_type(rng), rand_bound_type(rng)}.(starts, ends) + b = Interval{rand_bound_type(rng), rand_bound_type(rng)}.(starts .+ offsets, ends .+ offsets) + testsets(a, b) + testsets(a[1:1], b) + testsets(a, b[1:1]) + + a = Interval{Closed, Open}.(starts, ends) + b = Interval{Closed, Open}.(starts .+ offsets, ends .+ offsets) + @test Intervals.endpoint_tracking(a, b) isa Intervals.TrackStatically + @test Intervals.endpoint_tracking(a[1:1], b) isa Intervals.TrackStatically + @test Intervals.endpoint_tracking(a, b[1:1]) isa Intervals.TrackStatically + testsets(a, b) + testsets(a[1:1], b) + testsets(a, b[1:1]) + + a = Interval{Open, Closed}.(starts, ends) + b = Interval{Open, Closed}.(starts .+ offsets, ends .+ offsets) + @test Intervals.endpoint_tracking(a, b) isa Intervals.TrackStatically + @test Intervals.endpoint_tracking(a[1:1], b) isa Intervals.TrackStatically + @test Intervals.endpoint_tracking(a, b[1:1]) isa Intervals.TrackStatically + testsets(a, b) + testsets(a[1:1], b) + testsets(a, b[1:1]) + + 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 = Interval{Closed, Open}.(starts, ends) + b = Interval{Closed, Open}.(starts .+ offsets, ends .+ offsets) + testsets(a, randint.(b)) + testsets(a[1:1], randint.(b)) + testsets(a, leftint.(b[1:1])) + testsets(a, rightint.(b)) + testsets(a[1:1], rightint.(b)) + testsets(a, rightint.(b[1:1])) +end From 573ebe9e6cdb65d7dd9b28e315ab9be83cbc01c2 Mon Sep 17 00:00:00 2001 From: rofinn Date: Wed, 1 Jun 2022 20:19:22 -0700 Subject: [PATCH 02/30] Introduce an IntervalSet type. --- src/Intervals.jl | 1 + src/deprecated.jl | 4 + src/interval_sets.jl | 100 +++++++++++------- test/comparisons.jl | 247 ++++++++++++++++++++++++++++++++++--------- test/interval.jl | 2 + test/sets.jl | 77 +++++++------- 6 files changed, 304 insertions(+), 127 deletions(-) diff --git a/src/Intervals.jl b/src/Intervals.jl index fe00041a..7d3a939a 100644 --- a/src/Intervals.jl +++ b/src/Intervals.jl @@ -42,6 +42,7 @@ export Bound, Unbounded, AbstractInterval, Interval, + IntervalSet, AnchoredInterval, HourEnding, HourBeginning, diff --git a/src/deprecated.jl b/src/deprecated.jl index e91ae7c4..a549e981 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}) collect(union(IntervalSet(intervals))) +@deprecate union!(intervals::AbstractVector{<:AbstractInterval}) collect(union!(IntervalSet(intervals))) +@deprecate superset(intervals::AbstractVector{<:AbstractInterval}) superset(IntervalSet(intervals)) + # END Intervals 1.X.Y deprecations diff --git a/src/interval_sets.jl b/src/interval_sets.jl index e0760a7b..f24aedb4 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -1,7 +1,22 @@ ###### Set-related Helpers ##### -const IntervalSet = AbstractVector{<:AbstractInterval} +struct IntervalSet{T<:AbstractInterval} + items::Vector{<:AbstractInterval} +end + +IntervalSet(v::AbstractVector) = IntervalSet{eltype(v)}(v) +IntervalSet(interval::T) where T <: AbstractInterval = IntervalSet{T}([interval]) +IntervalSet(interval::IntervalSet) = interval +IntervalSet(itr) = IntervalSet{eltype(itr)}(collect(itr)) + +Base.copy(intervals::IntervalSet{T}) where T = IntervalSet{T}(copy(intervals.items)) +Base.length(intervals::IntervalSet) = length(intervals.items) +Base.iterate(intervals::IntervalSet, args...) = iterate(intervals.items, args...) +Base.eltype(::IntervalSet{T}) where T = T +Base.:(==)(a::IntervalSet, b::IntervalSet) = a.items == b.items +Base.isequal(a::IntervalSet, b::IntervalSet) = isequal(a, b) + const AbstractIntervals = Union{AbstractInterval, IntervalSet} # During merge operations used to compute unions, intersections etc..., @@ -39,9 +54,13 @@ function endpoint_tracking( ) return TrackEachEndpoint() end -endpoint_tracking(a::AbstractVector, b::AbstractVector) = endpoint_tracking(eltype(a), eltype(b)) + +endpoint_tracking(a::IntervalSet, b::IntervalSet) = endpoint_tracking(eltype(a), eltype(b)) endpoint_tracking(a::AbstractInterval, b::AbstractInterval) = endpoint_tracking(typeof(a), typeof(b)) +# TODO: Delete once union deprecation is gone. +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 @@ -58,11 +77,11 @@ interval_type(::TrackRightOpen{T}) where T = Interval{T, Closed, Open} # 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) +function unbunch(interval::AbstractInterval, tracking::EndpointTracking; lt=isless) return endpoint_type(tracking)[LeftEndpoint(interval), RightEndpoint(interval)] end unbunch_by_fn(_) = identity -function unbunch(intervals::Union{AbstractIntervals, Base.Iterators.Enumerate{<:AbstractIntervals}}, +function unbunch(intervals::Union{AbstractIntervals, Base.Iterators.Enumerate{<:AbstractIntervals}}, tracking::EndpointTracking; lt=isless) by = unbunch_by_fn(intervals) filtered = Iterators.filter(!isempty ∘ by, intervals) @@ -72,7 +91,7 @@ function unbunch(intervals::Union{AbstractIntervals, Base.Iterators.Enumerate{<: 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) +function unbunch((i, interval)::Tuple, tracking; lt=isless) eltype = Tuple{Int, endpoint_type(tracking)} return eltype[(i, LeftEndpoint(interval)), (i, RightEndpoint(interval))] end @@ -84,19 +103,25 @@ function unbunch(a::AbstractIntervals, b::AbstractIntervals; kwargs...) return a_, b_, tracking end +# TODO: Delete fallback once union deprecation is removed +function unbunch(a::Vector{<:AbstractInterval}, b::Vector{<:AbstractInterval}; kwargs...) + return unbunch(IntervalSet(a), IntervalSet(b); kwargs...) +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 interval_type(tracking)[] - return map(Iterators.partition(endpoints, 2)) do pair + 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 +# 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 @@ -108,7 +133,7 @@ function first_endpoint(x) return eltype(x) <: Tuple ? last(first(x)) : first(x) end function last_endpoint(x) - isempty(x) && return SentinelEndpoint() + isempty(x) && return SentinelEndpoint() # if the endpoints are enumerated, eltype will be a tuple return eltype(x) <: Tuple ? last(last(x)) : last(x) end @@ -139,7 +164,7 @@ isleft(::RightEndpoint) = false # 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: +# 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 @@ -264,35 +289,35 @@ right_endpoint(t, ::TrackRightOpen{T}) where T = RightEndpoint{T,Open}(endpoint( # There is power in a union. """ - union(intervals::AbstractVector{<:AbstractInterval}) + union(intervals::IntervalSets) -Flattens a vector of overlapping intervals into a new, smaller vector containing only -non-overlapping intervals. +Flattens any overlapping intervals within the `IntervalSet` into a new, smaller set +containing only non-overlapping intervals. """ -function Base.union(intervals::AbstractVector{<:AbstractInterval}) - return union!(convert(Vector{AbstractInterval}, intervals)) -end +Base.union(intervals::IntervalSet{<:Interval}) = union!(copy(intervals)) -# allow a concretely typed array for `Interval` objects (as opposed to e.g. anchored intervals -# which may change type during the union process) -function Base.union(intervals::AbstractVector{T}) where T <: Interval - return 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 + return union!(IntervalSet{T}(convert(Vector{T}, intervals.items))) end """ - union!(intervals::AbstractVector{<:AbstractInterval}) + 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::AbstractVector{<:AbstractInterval}) - sort!(intervals) +function Base.union!(intervals::IntervalSet) + items = intervals.items + sort!(items) i = 2 - n = length(intervals) + n = length(items) while i <= n - prev = intervals[i - 1] - curr = intervals[i] + 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) @@ -301,8 +326,8 @@ function Base.union!(intervals::AbstractVector{<:AbstractInterval}) # 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) + items[i - 1] = merge(prev, curr) + deleteat!(items, i) n -= 1 end end @@ -311,13 +336,13 @@ function Base.union!(intervals::AbstractVector{<:AbstractInterval}) end """ - superset(intervals::AbstractArray{<:AbstractInterval}) -> Interval + superset(intervals::IntervalSet) -> 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)) +function superset(intervals::IntervalSet) + left = minimum(LeftEndpoint.(intervals.items)) + right = maximum(RightEndpoint.(intervals.items)) return Interval(left, right) end @@ -332,14 +357,14 @@ Base.issubset(x::AbstractIntervals, y::AbstractIntervals) = isempty(setdiff(x, y Base.isdisjoint(x::AbstractIntervals, y::AbstractIntervals) = isempty(intersect(x, y)) function Base.issetequal(x::AbstractIntervals, y::AbstractIntervals) - x, y, tracking = unbunch(union(vcat(x)), union(vcat(y))) + x, y, tracking = unbunch(union(IntervalSet(x)), union(IntervalSet(y))) return x == y || all(isempty, bunch(x, tracking)) && all(isempty, bunch(y, tracking)) end # 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 intersection_isless_fn(::TrackEachEndpoint) function (x,y) if isequal(x, y) return isless(intersection_order(x), intersection_order(y)) @@ -351,15 +376,15 @@ end """ find_intersections( - x::Union{AbstractInterval, AbstractVector{<:AbstractInterval}}, - y::Union{AbstractInterval, AbstractVector{<:AbstractInterval}}, + x::Union{AbstractInterval, IntervalSet}, + y::Union{AbstractInterval, IntervalSet}, ) Returns a `Vector{Vector{Int}}` where the value at index `i` gives the indices to all intervals in `y` that intersect with `x[i]`. """ function find_intersections(x_::AbstractIntervals, y_::AbstractIntervals) - xa, ya = vcat(x_), vcat(y_) + xa, ya = IntervalSet(x_), IntervalSet(y_) tracking = endpoint_tracking(xa, ya) lt = intersection_isless_fn(tracking) x = unbunch(enumerate(xa), tracking; lt) @@ -402,4 +427,3 @@ function find_intersections_helper!(result, x, y, lt) return unique!.(result) end - diff --git a/test/comparisons.jl b/test/comparisons.jl index 1ddcd7ca..decc0ed9 100644 --- a/test/comparisons.jl +++ b/test/comparisons.jl @@ -89,16 +89,24 @@ end # 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(Interval[]) @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 @@ -168,16 +176,24 @@ end # 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(Interval[]) @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 @@ -247,20 +263,29 @@ end # 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_superset] + @test union([earlier], [later]) != [expected_superset] + @test union(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_superset) + @test intersect([earlier], [later]) == [] + @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(Interval[]) @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]) == union(expected_xor) + @test symdiff([earlier], [later]) != 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: # @@ -326,16 +351,24 @@ end # 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_superset] + @test union([earlier], [later]) != [expected_superset] + @test union(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_superset) + @test intersect([earlier], [later]) == [] + @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(Interval[]) @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]) == union(expected_xor) + @test symdiff([earlier], [later]) != union(expected_xor) + @test symdiff(IntervalSet(earlier), IntervalSet(later)) == union(IntervalSet(expected_xor)) end end end @@ -410,16 +443,24 @@ end # 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_superset] - @test intersect([earlier], [later]) == [expected_overlap] + @test union([earlier], [later]) != [expected_superset] + @test union(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_superset) - @test setdiff([earlier], [later]) == expected_xor[1:1] - @test setdiff([later], [earlier]) == expected_xor[2:2] + @test intersect([earlier], [later]) != [expected_overlap] + @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_overlap) - @test symdiff([earlier], [later]) == expected_xor + @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 @@ -494,16 +535,24 @@ end # 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_superset] - @test intersect([earlier], [later]) == [expected_overlap] + @test union([earlier], [later]) != [expected_superset] + @test union(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_superset) - @test setdiff([earlier], [later]) == expected_xor[1:1] - @test setdiff([later], [earlier]) == expected_xor[2:2] + @test intersect([earlier], [later]) != [expected_overlap] + @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_overlap) - @test symdiff([earlier], [later]) == expected_xor + @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 @@ -560,16 +609,24 @@ end # 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(Interval[]) + @test setdiff([b], [a]) == [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) @test symdiff([a], [b]) == [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) end end end @@ -627,18 +684,26 @@ end # 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) - @test union([a], [b]) == [expected_superset] - @test intersect([a], [b]) == [expected_overlap] + @test union([a], [b]) != [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) - @test setdiff([a], [b]) == expected_xor[1:1] - @test setdiff([b], [a]) == [] + @test intersect([a], [b]) != [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) != expected_xor[1:1] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor[1:1]) - @test symdiff([a], [b]) == expected_xor + @test setdiff([b], [a]) != [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) + + @test symdiff([a], [b]) != expected_xor + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) end end end @@ -696,18 +761,26 @@ end # 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) - @test union([a], [b]) == [expected_superset] - @test intersect([a], [b]) == [expected_overlap] + @test union([a], [b]) != [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) - @test setdiff([a], [b]) == expected_xor[1:1] - @test setdiff([b], [a]) == [] + @test intersect([a], [b]) != [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) + + @test setdiff([a], [b]) != expected_xor[1:1] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor[1:1]) - @test symdiff([a], [b]) == expected_xor + @test setdiff([b], [a]) != [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) + + @test symdiff([a], [b]) != expected_xor + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) end end end @@ -769,18 +842,26 @@ end # 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) - @test union([a], [b]) == [expected_superset] - @test intersect([a], [b]) == [expected_overlap] + @test union([a], [b]) != [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) - @test setdiff([a], [b]) == expected_xor - @test setdiff([b], [a]) == [] + @test intersect([a], [b]) != [expected_overlap] + @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) - @test symdiff([a], [b]) == expected_xor + @test setdiff([a], [b]) != expected_xor + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) + + @test setdiff([b], [a]) != [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) + + @test symdiff([a], [b]) != expected_xor + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) end end end @@ -840,16 +921,24 @@ end # 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 intersect([a], [b]) == [expected_overlap] + @test union([a], [b]) != [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) - @test setdiff([a], [b]) == [] - @test setdiff([b], [a]) == expected_xor[1:1] + @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(Interval[]) - @test symdiff([a], [b]) == expected_xor + @test setdiff([b], [a]) != expected_xor[1:1] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(expected_xor[1:1]) + + @test symdiff([a], [b]) != expected_xor + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) end end end @@ -909,16 +998,26 @@ end # Using a vector of intervals as sets @test union([a, b]) == [expected_superset] - @test intersect([a], [b]) == [expected_overlap] + @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) - @test union([a], [b]) == [expected_superset] + @test union([a], [b]) != [expected_superset] + @test union(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_superset) - @test setdiff([a], [b]) == [] - @test setdiff([b], [a]) == expected_xor[1:1] + @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(Interval[]) + + @test setdiff([b], [a]) != expected_xor[1:1] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(expected_xor[1:1]) - @test symdiff([a], [b]) == expected_xor + @test symdiff([a], [b]) != expected_xor + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) end end end @@ -975,16 +1074,24 @@ end # 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(Interval[]) + @test setdiff([b], [a]) == [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) @test symdiff([a], [b]) == [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) end end end @@ -1042,16 +1149,24 @@ end # 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]) != expected_xor[1:1] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor[1:1]) - @test setdiff([a], [b]) == expected_xor[1:1] @test setdiff([b], [a]) == [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) @test symdiff([a], [b]) == [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) end end end @@ -1109,16 +1224,24 @@ end # 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]) != expected_xor[1:1] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor[1:1]) - @test setdiff([a], [b]) == expected_xor[1:1] @test setdiff([b], [a]) == [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) @test symdiff([a], [b]) == [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) end end end @@ -1174,16 +1297,24 @@ end # 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(Interval[]) + @test setdiff([b], [a]) == [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) @test symdiff([a], [b]) == [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) end end end @@ -1231,14 +1362,20 @@ end # Using a vector of intervals as sets @test union([a, b]) == [expected_superset] - @test union([a], [b]) == [expected_superset] + @test union([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 intersect([a], [b]) == [expected_overlap] + @test setdiff([a], [b]) != [] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) - @test setdiff([a], [b]) == [] - @test setdiff([b], [a]) == [] + @test setdiff([b], [a]) != [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) - @test symdiff([a], [b]) == [] + @test symdiff([a], [b]) != [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) end end @@ -1319,16 +1456,24 @@ end # 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 intersect([a], [b]) == [expected_overlap] + @test union([a], [b]) != [expected_superset] + @test union(IntervalSet([a, b])) == IntervalSet(expected_superset) - @test setdiff([a], [b]) == [] - @test setdiff([b], [a]) == expected_xor[1:2] + @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(Interval[]) + + @test setdiff([b], [a]) != expected_xor[1:2] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(expected_xor[1:2]) - @test symdiff([a], [b]) == expected_xor + @test symdiff([a], [b]) != expected_xor + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) end end end diff --git a/test/interval.jl b/test/interval.jl index 197a06a9..1ee25d04 100644 --- a/test/interval.jl +++ b/test/interval.jl @@ -765,6 +765,8 @@ Interval{Open, Open}(-10, -1), Interval{Open, Open}(13, 20), ] + @show typeof(intervals) + @test union!(intervals) == expected @test intervals == expected diff --git a/test/sets.jl b/test/sets.jl index 839a38c4..38f31513 100644 --- a/test/sets.jl +++ b/test/sets.jl @@ -28,9 +28,11 @@ end 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)) @@ -43,36 +45,35 @@ end @test issetequal(a, a) @test isdisjoint(setdiff(a, b), b) @test !isdisjoint(a, a) - + intersections = find_intersections(a, b) - a, b = vcat(a), vcat(b) # Always make `a` / `b` vectors # 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(a[i:i], b[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(a[i:i], b[Not(x)])) + isempty(intersect(IntervalSet(a.items[i]), IntervalSet(b.items[Not(x)]))) end end # verify empty interval set @test isempty(union(Interval[])) - + # a few taylored interval sets - a = [Interval(i, i + 3) for i in 1:5:15] - b = a .+ (1:2:5) + a = IntervalSet([Interval(i, i + 3) for i in 1:5:15]) + b = IntervalSet(a.items .+ (1:2:5)) @test all(first.(a) .∈ a) testsets(a, b) - testsets(a[1:1], b) - testsets(a, b[1:1]) + testsets(IntervalSet(first(a)), b) + testsets(a, IntervalSet(first(b))) # verify that `last` need not be ordered - intervals = [Interval(0, 5), Interval(0, 3)] + intervals = IntervalSet([Interval(0, 5), Interval(0, 3)]) @test superset(union(intervals)) == Interval(0, 5) # try out some more involved (random) intervals @@ -82,47 +83,47 @@ end ends = starts .+ rand(rng, 1:10_000, n) offsets = round.(Int, (ends .- starts) .* (2 .* rand(rng, n) .- 1)) - a = Interval.(starts, ends) - b = Interval.(starts .+ offsets, ends .+ offsets) + a = IntervalSet(Interval.(starts, ends)) + b = IntervalSet(Interval.(starts .+ offsets, ends .+ offsets)) @test all(first.(a) .∈ a) testsets(a, b) - testsets(a[1:1], b) - testsets(a, b[1:1]) + testsets(IntervalSet(first(a)), b) + testsets(a, IntervalSet(first(b))) - a = Interval{rand_bound_type(rng), rand_bound_type(rng)}.(starts, ends) - b = Interval{rand_bound_type(rng), rand_bound_type(rng)}.(starts .+ offsets, ends .+ offsets) + 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(a[1:1], b) - testsets(a, b[1:1]) + testsets(IntervalSet(first(a)), b) + testsets(a, IntervalSet(first(b))) - a = Interval{Closed, Open}.(starts, ends) - b = Interval{Closed, Open}.(starts .+ offsets, ends .+ offsets) + 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(a[1:1], b) isa Intervals.TrackStatically - @test Intervals.endpoint_tracking(a, b[1:1]) isa Intervals.TrackStatically + @test Intervals.endpoint_tracking(IntervalSet(first(a)), b) isa Intervals.TrackStatically + @test Intervals.endpoint_tracking(a, IntervalSet(first(b))) isa Intervals.TrackStatically testsets(a, b) - testsets(a[1:1], b) - testsets(a, b[1:1]) + testsets(IntervalSet(first(a)), b) + testsets(a, IntervalSet(first(b))) - a = Interval{Open, Closed}.(starts, ends) - b = Interval{Open, Closed}.(starts .+ offsets, ends .+ offsets) + 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(a[1:1], b) isa Intervals.TrackStatically - @test Intervals.endpoint_tracking(a, b[1:1]) isa Intervals.TrackStatically + @test Intervals.endpoint_tracking(IntervalSet(first(a)), b) isa Intervals.TrackStatically + @test Intervals.endpoint_tracking(a, IntervalSet(first(b))) isa Intervals.TrackStatically testsets(a, b) - testsets(a[1:1], b) - testsets(a, b[1:1]) + testsets(IntervalSet(first(a)), b) + testsets(a, IntervalSet(first(b))) 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 = Interval{Closed, Open}.(starts, ends) - b = Interval{Closed, Open}.(starts .+ offsets, ends .+ offsets) - testsets(a, randint.(b)) - testsets(a[1:1], randint.(b)) - testsets(a, leftint.(b[1:1])) - testsets(a, rightint.(b)) - testsets(a[1:1], rightint.(b)) - testsets(a, rightint.(b[1:1])) + a = IntervalSet(Interval{Closed, Open}.(starts, ends)) + b = IntervalSet(Interval{Closed, Open}.(starts .+ offsets, ends .+ offsets)) + testsets(a, IntervalSet(randint.(b))) + testsets(IntervalSet(first(a)), IntervalSet(randint.(b))) + testsets(a, IntervalSet(leftint.(IntervalSet(first(b))))) + testsets(a, IntervalSet(rightint.(b))) + testsets(IntervalSet(first(a)), IntervalSet(rightint.(b))) + testsets(a, IntervalSet(rightint.(IntervalSet(first(b))))) end From e122bfeaf4e72dafee778bbcf88f09e4f3bd4abc Mon Sep 17 00:00:00 2001 From: rofinn Date: Mon, 6 Jun 2022 15:00:09 -0700 Subject: [PATCH 03/30] Addressing review comments. 1. Made IntervalSet a subtype of AbstractSet - Note about changing internally representation once `union!(Vector{AbstractInterval})` is deprecated 2. Add an extra empty constructor 3. `==` falls back to issetequal 4. Fixed a bug where `union` didn't correctly copy data in the non-concrete case. 5. Updated a bunch of tests to include both the previous (reverted) expectation and the current base fallback behaviour. --- Project.toml | 2 +- src/interval_sets.jl | 18 +++-- test/comparisons.jl | 163 +++++++++++++++++++++++-------------------- test/interval.jl | 1 - 4 files changed, 100 insertions(+), 84 deletions(-) diff --git a/Project.toml b/Project.toml index 310f0476..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.0" +version = "1.8.0" [deps] Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" diff --git a/src/interval_sets.jl b/src/interval_sets.jl index f24aedb4..93dad4a8 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -1,21 +1,27 @@ ###### Set-related Helpers ##### +""" + IntervalSet{T<:AbstractInterval} <: AbstractSet{T} -struct IntervalSet{T<:AbstractInterval} - items::Vector{<:AbstractInterval} +A set type for performing multi-interval specific operations. +https://en.wikipedia.org/wiki/Interval_arithmetic#Interval_operators +""" +struct IntervalSet{T<:AbstractInterval} <: AbstractSet{T} + # TODO: Use Dict internally once union!(Vector{AbstractInterval}) is fully deprecated + items::Vector{T} end IntervalSet(v::AbstractVector) = IntervalSet{eltype(v)}(v) IntervalSet(interval::T) where T <: AbstractInterval = IntervalSet{T}([interval]) IntervalSet(interval::IntervalSet) = interval IntervalSet(itr) = IntervalSet{eltype(itr)}(collect(itr)) +IntervalSet() = IntervalSet{AbstractInterval}(AbstractInterval[]) Base.copy(intervals::IntervalSet{T}) where T = IntervalSet{T}(copy(intervals.items)) Base.length(intervals::IntervalSet) = length(intervals.items) Base.iterate(intervals::IntervalSet, args...) = iterate(intervals.items, args...) Base.eltype(::IntervalSet{T}) where T = T -Base.:(==)(a::IntervalSet, b::IntervalSet) = a.items == b.items -Base.isequal(a::IntervalSet, b::IntervalSet) = isequal(a, b) +Base.:(==)(a::IntervalSet, b::IntervalSet) = issetequal(a, b) const AbstractIntervals = Union{AbstractInterval, IntervalSet} @@ -300,7 +306,9 @@ Base.union(intervals::IntervalSet{<:Interval}) = union!(copy(intervals)) # allocate a AbstractInterval vector function Base.union(intervals::IntervalSet{<:AbstractInterval}) T = AbstractInterval - return union!(IntervalSet{T}(convert(Vector{T}, intervals.items))) + dest = Vector{T}(undef, length(intervals.items)) + copyto!(dest, intervals.items) + return union!(IntervalSet{T}(dest)) end """ diff --git a/test/comparisons.jl b/test/comparisons.jl index decc0ed9..2c8b131d 100644 --- a/test/comparisons.jl +++ b/test/comparisons.jl @@ -97,7 +97,7 @@ end @test union(IntervalSet(earlier), IntervalSet(later)) == IntervalSet([earlier, later]) @test intersect([earlier], [later]) == [] - @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(Interval[]) + @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]) @@ -184,7 +184,7 @@ end @test union(IntervalSet(earlier), IntervalSet(later)) == IntervalSet([earlier, later]) @test intersect([earlier], [later]) == [] - @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(Interval[]) + @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]) @@ -267,11 +267,11 @@ end # TODO: These functions should be compatible with unbounded intervals if isbounded(earlier) && isbounded(later) - @test union([earlier], [later]) != [expected_superset] + @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(Interval[]) + @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]) @@ -279,7 +279,8 @@ end @test setdiff([later], [earlier]) == expected_xor[2:2] @test setdiff(IntervalSet(later), IntervalSet(earlier)) == IntervalSet(expected_xor[2:2]) - @test symdiff([earlier], [later]) != union(expected_xor) + # 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 @@ -355,11 +356,11 @@ end # TODO: These functions should be compatible with unbounded intervals if isbounded(earlier) && isbounded(later) - @test union([earlier], [later]) != [expected_superset] + @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(Interval[]) + @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]) @@ -367,7 +368,7 @@ end @test setdiff([later], [earlier]) == expected_xor[2:2] @test setdiff(IntervalSet(later), IntervalSet(earlier)) == IntervalSet(expected_xor[2:2]) - @test symdiff([earlier], [later]) != union(expected_xor) + @test symdiff([earlier], [later]) == expected_xor != union(expected_xor) @test symdiff(IntervalSet(earlier), IntervalSet(later)) == union(IntervalSet(expected_xor)) end end @@ -447,19 +448,20 @@ end # TODO: These functions should be compatible with unbounded intervals if isbounded(earlier) && isbounded(later) - @test union([earlier], [later]) != [expected_superset] + # 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([earlier], [later]) == [] != [expected_overlap] @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_overlap) - @test setdiff([earlier], [later]) != expected_xor[1:1] + @test setdiff([earlier], [later]) == [earlier] != 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([later], [earlier]) == [later] != expected_xor[2:2] @test setdiff(IntervalSet(later), IntervalSet(earlier)) == IntervalSet(expected_xor[2:2]) - @test symdiff([earlier], [later]) != expected_xor + @test symdiff([earlier], [later]) == [earlier, later] @test symdiff(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_xor) end end @@ -539,19 +541,20 @@ end # TODO: These functions should be compatible with unbounded intervals if isbounded(earlier) && isbounded(later) - @test union([earlier], [later]) != [expected_superset] + # 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([earlier], [later]) == [] != [expected_overlap] @test intersect(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_overlap) - @test setdiff([earlier], [later]) != expected_xor[1:1] + @test setdiff([earlier], [later]) == [earlier] != 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([later], [earlier]) == [later] != expected_xor[2:2] @test setdiff(IntervalSet(later), IntervalSet(earlier)) == IntervalSet(expected_xor[2:2]) - @test symdiff([earlier], [later]) != expected_xor + @test symdiff([earlier], [later]) == [earlier, later] != expected_xor @test symdiff(IntervalSet(earlier), IntervalSet(later)) == IntervalSet(expected_xor) end end @@ -620,13 +623,13 @@ end @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) @test setdiff([a], [b]) == [] - @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() @test setdiff([b], [a]) == [] - @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() @test symdiff([a], [b]) == [] - @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() end end end @@ -690,19 +693,20 @@ end # TODO: will have to think carefully about the `expected_` variables # when we allow for unbounded values if isbounded(a) && isbounded(b) - @test union([a], [b]) != [expected_superset] + # 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([a], [b]) == [] != [expected_overlap] @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) - @test setdiff([a], [b]) != expected_xor[1:1] + @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(Interval[]) + @test setdiff([b], [a]) == [b] != [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() - @test symdiff([a], [b]) != expected_xor + @test symdiff([a], [b]) == [a, b] != expected_xor @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) end end @@ -767,19 +771,20 @@ end # TODO: will have to think carefully about the `expected_` variables # when we allow for unbounded values if isbounded(a) && isbounded(b) - @test union([a], [b]) != [expected_superset] + # 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([a], [b]) == [] != [expected_overlap] @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) - @test setdiff([a], [b]) != expected_xor[1:1] + @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(Interval[]) + @test setdiff([b], [a]) == [b] != [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() - @test symdiff([a], [b]) != expected_xor + @test symdiff([a], [b]) == [a, b] != expected_xor @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) end end @@ -848,19 +853,20 @@ end # TODO: will have to think carefully about the `expected_` variables # when we allow for unbounded values if isbounded(a) && isbounded(b) - @test union([a], [b]) != [expected_superset] + # 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([a], [b]) == [] != [expected_overlap] @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) - @test setdiff([a], [b]) != expected_xor + @test setdiff([a], [b]) == [a] != expected_xor @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) - @test setdiff([b], [a]) != [] - @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) + @test setdiff([b], [a]) == [b] != [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() - @test symdiff([a], [b]) != expected_xor + @test symdiff([a], [b]) == [a, b] != expected_xor @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) end end @@ -925,19 +931,20 @@ end # TODO: These functions should be compatible with unbounded intervals if isbounded(a) && isbounded(b) - @test union([a], [b]) != [expected_superset] + # 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([a], [b]) == [] != [expected_overlap] @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) - @test setdiff([a], [b]) != [] - @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test setdiff([a], [b]) == [a] != [] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() - @test setdiff([b], [a]) != expected_xor[1:1] + @test setdiff([b], [a]) == [b] != expected_xor[1:1] @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(expected_xor[1:1]) - @test symdiff([a], [b]) != expected_xor + @test symdiff([a], [b]) == [a, b] != expected_xor @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) end end @@ -1004,19 +1011,20 @@ end # TODO: These functions should be compatible with unbounded intervals if isbounded(a) && isbounded(b) - @test union([a], [b]) != [expected_superset] + # 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([a], [b]) == [] != [expected_overlap] @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) - @test setdiff([a], [b]) != [] - @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test setdiff([a], [b]) == [a] != [] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() - @test setdiff([b], [a]) != expected_xor[1:1] + @test setdiff([b], [a]) == [b] != expected_xor[1:1] @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(expected_xor[1:1]) - @test symdiff([a], [b]) != expected_xor + @test symdiff([a], [b]) == [a, b] != expected_xor @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) end end @@ -1085,13 +1093,13 @@ end @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) @test setdiff([a], [b]) == [] - @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() @test setdiff([b], [a]) == [] - @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() @test symdiff([a], [b]) == [] - @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() end end end @@ -1159,14 +1167,14 @@ end @test intersect([a], [b]) == [expected_overlap] @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) - @test setdiff([a], [b]) != expected_xor[1:1] + @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(Interval[]) + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() @test symdiff([a], [b]) == [] - @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() end end end @@ -1234,14 +1242,14 @@ end @test intersect([a], [b]) == [expected_overlap] @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) - @test setdiff([a], [b]) != expected_xor[1:1] + @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(Interval[]) + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() @test symdiff([a], [b]) == [] - @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() end end end @@ -1308,13 +1316,13 @@ end @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) @test setdiff([a], [b]) == [] - @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() @test setdiff([b], [a]) == [] - @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() @test symdiff([a], [b]) == [] - @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() end end end @@ -1362,20 +1370,20 @@ end # Using a vector of intervals as sets @test union([a, b]) == [expected_superset] - @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([a], [b]) == [] != [expected_overlap] @test intersect(IntervalSet([a, b])) == IntervalSet(expected_overlap) - @test setdiff([a], [b]) != [] - @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test setdiff([a], [b]) == [a] != [] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() - @test setdiff([b], [a]) != [] - @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(Interval[]) + @test setdiff([b], [a]) == [b] != [] + @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet() - @test symdiff([a], [b]) != [] - @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test symdiff([a], [b]) == [a, b] != [] + @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() end end @@ -1460,19 +1468,20 @@ end # TODO: These functions should be compatible with unbounded intervals if isbounded(a) && isbounded(b) - @test union([a], [b]) != [expected_superset] + # 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([a], [b]) == [] != [expected_overlap] @test intersect(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_overlap) - @test setdiff([a], [b]) != [] - @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(Interval[]) + @test setdiff([a], [b]) == [a] != [] + @test setdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet() - @test setdiff([b], [a]) != expected_xor[1:2] + @test setdiff([b], [a]) == [b] != expected_xor[1:2] @test setdiff(IntervalSet(b), IntervalSet(a)) == IntervalSet(expected_xor[1:2]) - @test symdiff([a], [b]) != expected_xor + @test symdiff([a], [b]) == [a, b] != expected_xor @test symdiff(IntervalSet(a), IntervalSet(b)) == IntervalSet(expected_xor) end end diff --git a/test/interval.jl b/test/interval.jl index 1ee25d04..b85814d6 100644 --- a/test/interval.jl +++ b/test/interval.jl @@ -765,7 +765,6 @@ Interval{Open, Open}(-10, -1), Interval{Open, Open}(13, 20), ] - @show typeof(intervals) @test union!(intervals) == expected @test intervals == expected From a68029963e6ea063f4c5708bc84e7d3b1b4f1eaf Mon Sep 17 00:00:00 2001 From: David F Little Date: Fri, 17 Jun 2022 14:21:30 +0000 Subject: [PATCH 04/30] some small adgjuetments --- src/interval_sets.jl | 6 ++++++ test/sets.jl | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index f24aedb4..a585bdd0 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -1,6 +1,12 @@ ###### Set-related Helpers ##### +""" + IntervalSet(x::AbstractVector{<:AbstractInterval}) + +Interpret an array of intervals as a set of points: the union of all points in the +intervals. +""" struct IntervalSet{T<:AbstractInterval} items::Vector{<:AbstractInterval} end diff --git a/test/sets.jl b/test/sets.jl index 38f31513..1889175c 100644 --- a/test/sets.jl +++ b/test/sets.jl @@ -36,6 +36,10 @@ end rand_bound_type(rng) = rand(rng, (Closed, Open)) + # verify case where we interpret array as a set of intervals (rather than a set of + # points) + @test intersect([1..2, 2..3, 3..4, 4..5], [2..3, 3..4]) == [2..3, 3..4] + function testsets(a, b) @test area(a ∪ b) ≤ area(myunion(a)) + area(myunion(b)) @test area(setdiff(a, b)) ≤ area(myunion(a)) From 707c7bf6bb058b5a2ae72436cd8c339a5b5f0e5e Mon Sep 17 00:00:00 2001 From: David F Little Date: Fri, 17 Jun 2022 16:11:38 +0000 Subject: [PATCH 05/30] update docs --- docs/src/index.md | 2 ++ src/interval_sets.jl | 41 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/src/index.md b/docs/src/index.md index 4fc13736..9c86ba9d 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -268,6 +268,7 @@ In the plot, inclusive boundaries are marked with a vertical bar, whereas exclus ```@docs Interval AnchoredInterval +IntervalSet HourEnding HourBeginning HE @@ -291,4 +292,5 @@ Base.parse(::Type{Interval{T}}, ::AbstractString) where T union union! superset +Intervals.find_intersections ``` diff --git a/src/interval_sets.jl b/src/interval_sets.jl index a585bdd0..9bdee144 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -5,7 +5,45 @@ IntervalSet(x::AbstractVector{<:AbstractInterval}) Interpret an array of intervals as a set of points: the union of all points in the -intervals. +intervals. Set operations over intervals sets will return an IntervalSet containing the +fewest number of intervals that can be used to represent the resulting point set. + +## Examples + +```jldoctest +julia> Array(union(IntervalSet([1..5]), IntervalSet[3..8])) +1-element Vector{Interval{Int64, Closed, Closed}}: + Interval{Int64, Closed, Closed}(1, 8) + +julia> Array(intersect(IntervalSet([1..5]), IntervalSet([3..8]))) +1-element Vector{Interval{Int64, Closed, Closed}}: + Interval{Int64, Closed, Closed}(3, 5) + +julia> Array(symdiff(IntervalSet([1..5]), IntervalSet([3..8]))) +2-element Vector{Interval{Int64}}: + Interval{Int64, Closed, Open}(1, 3) + Interval{Int64, Open, Closed}(5, 8) + +julia> Array(union(IntervalSet([1..2, 2..5]), IntervalSet([6..7]))) +2-element Vector{Interval{Int64, Closed, Closed}}: + Interval{Int64, Closed, Closed}(1, 5) + Interval{Int64, Closed, Closed}(6, 7) + +julia> Array(union(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) +2-element Vector{Interval{Int64, Closed, Closed}}: + Interval{Int64, Closed, Closed}(1, 10) + Interval{Int64, Closed, Closed}(12, 14) + +julia> Array(intersect(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) +2-element Vector{Interval{Int64, Closed, Closed}}: + Interval{Int64, Closed, Closed}(4, 5) + Interval{Int64, Closed, Closed}(8, 9) + +julia> Array(setdiff(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) +2-element Vector{Interval{Int64}}: + Interval{Int64, Closed, Open}(1, 4) + Interval{Int64, Open, Closed}(9, 10) +``` """ struct IntervalSet{T<:AbstractInterval} items::Vector{<:AbstractInterval} @@ -22,6 +60,7 @@ Base.iterate(intervals::IntervalSet, args...) = iterate(intervals.items, args... Base.eltype(::IntervalSet{T}) where T = T Base.:(==)(a::IntervalSet, b::IntervalSet) = a.items == b.items Base.isequal(a::IntervalSet, b::IntervalSet) = isequal(a, b) +Base.Array(intervals::IntervalSet) = intervals.items const AbstractIntervals = Union{AbstractInterval, IntervalSet} From abaddf2689eb03ed3e11b7ab27f220f133b4cac2 Mon Sep 17 00:00:00 2001 From: David F Little Date: Fri, 17 Jun 2022 16:16:31 +0000 Subject: [PATCH 06/30] proofreading --- src/interval_sets.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 9bdee144..f8c1e22a 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -5,8 +5,9 @@ IntervalSet(x::AbstractVector{<:AbstractInterval}) Interpret an array of intervals as a set of points: the union of all points in the -intervals. Set operations over intervals sets will return an IntervalSet containing the +intervals. Set operations over them will return an IntervalSet containing the fewest number of intervals that can be used to represent the resulting point set. +Unbounded intervals are not supported. ## Examples From 8231eeb44b7d68628a02da83cbcbff1369eca3fc Mon Sep 17 00:00:00 2001 From: David F Little Date: Fri, 17 Jun 2022 16:18:08 +0000 Subject: [PATCH 07/30] add set link --- src/interval_sets.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index f8c1e22a..029ac1aa 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -9,6 +9,8 @@ intervals. Set operations over them will return an IntervalSet containing the fewest number of intervals that can be used to represent the resulting point set. Unbounded intervals are not supported. +see also: https://en.wikipedia.org/wiki/Interval_arithmetic#Interval_operators + ## Examples ```jldoctest From 20b0c20dadab4cad227644f20d5cc09d84250445 Mon Sep 17 00:00:00 2001 From: David F Little Date: Fri, 17 Jun 2022 16:57:20 +0000 Subject: [PATCH 08/30] revise doc tests --- src/interval_sets.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 16e161ea..4f659a49 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -13,20 +13,21 @@ see also: https://en.wikipedia.org/wiki/Interval_arithmetic#Interval_operators ## Examples ```jldoctest -julia> Array(union(IntervalSet([1..5]), IntervalSet[3..8])) +julia> using Intervals +julia> Array(union(IntervalSet(1..5), IntervalSet(3..8))) 1-element Vector{Interval{Int64, Closed, Closed}}: Interval{Int64, Closed, Closed}(1, 8) -julia> Array(intersect(IntervalSet([1..5]), IntervalSet([3..8]))) +julia> Array(intersect(IntervalSet(1..5), IntervalSet(3..8))) 1-element Vector{Interval{Int64, Closed, Closed}}: Interval{Int64, Closed, Closed}(3, 5) -julia> Array(symdiff(IntervalSet([1..5]), IntervalSet([3..8]))) +julia> Array(symdiff(IntervalSet(1..5), IntervalSet(3..8))) 2-element Vector{Interval{Int64}}: Interval{Int64, Closed, Open}(1, 3) Interval{Int64, Open, Closed}(5, 8) -julia> Array(union(IntervalSet([1..2, 2..5]), IntervalSet([6..7]))) +julia> Array(union(IntervalSet([1..2, 2..5]), IntervalSet(6..7))) 2-element Vector{Interval{Int64, Closed, Closed}}: Interval{Int64, Closed, Closed}(1, 5) Interval{Int64, Closed, Closed}(6, 7) From 9af3a28b5609cc837284dd4b1fd69ba1f98dec1a Mon Sep 17 00:00:00 2001 From: David F Little Date: Fri, 17 Jun 2022 18:30:50 +0000 Subject: [PATCH 09/30] support other array types for an interval set --- src/interval_sets.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 4f659a49..34abf81c 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -48,8 +48,8 @@ julia> Array(setdiff(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) Interval{Int64, Open, Closed}(9, 10) ``` """ -struct IntervalSet{T<:AbstractInterval} - items::Vector{<:AbstractInterval} +struct IntervalSet{A <: AbstractVector{<:AbstractInterval}} + items::A end IntervalSet(v::AbstractVector) = IntervalSet{eltype(v)}(v) From f4386edc77157042a62dbbc59ec05960e4509046 Mon Sep 17 00:00:00 2001 From: David F Little Date: Fri, 17 Jun 2022 18:34:33 +0000 Subject: [PATCH 10/30] remove superfluous intervalset conversion --- src/interval_sets.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 34abf81c..64d0195d 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -435,7 +435,7 @@ Returns a `Vector{Vector{Int}}` where the value at index `i` gives the indices t intervals in `y` that intersect with `x[i]`. """ function find_intersections(x_::AbstractIntervals, y_::AbstractIntervals) - xa, ya = IntervalSet(x_), IntervalSet(y_) + xa, ya = vcat(x_), vcat(y_) tracking = endpoint_tracking(xa, ya) lt = intersection_isless_fn(tracking) x = unbunch(enumerate(xa), tracking; lt) From df2d9db522f04f141de42d305dbe37674b03cb9e Mon Sep 17 00:00:00 2001 From: David F Little Date: Fri, 17 Jun 2022 21:18:10 +0000 Subject: [PATCH 11/30] fix type signature --- src/interval_sets.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 64d0195d..59e635f0 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -434,7 +434,7 @@ end Returns a `Vector{Vector{Int}}` where the value at index `i` gives the indices to all intervals in `y` that intersect with `x[i]`. """ -function find_intersections(x_::AbstractIntervals, y_::AbstractIntervals) +function find_intersections(x_::Union{AbstractInterval, AbstractVector{<:AbstractInterval}}, y_::Union{AbstractInterval, AbstractVector{<:AbstractInterval}}) xa, ya = vcat(x_), vcat(y_) tracking = endpoint_tracking(xa, ya) lt = intersection_isless_fn(tracking) From 3503f7404eadf5e5338b276c0494904e99891192 Mon Sep 17 00:00:00 2001 From: David F Little Date: Fri, 17 Jun 2022 21:18:42 +0000 Subject: [PATCH 12/30] fix test --- test/sets.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/sets.jl b/test/sets.jl index 1889175c..a10a5afd 100644 --- a/test/sets.jl +++ b/test/sets.jl @@ -50,7 +50,7 @@ end @test isdisjoint(setdiff(a, b), b) @test !isdisjoint(a, a) - intersections = find_intersections(a, b) + intersections = find_intersections(Array(a), Array(b)) # verify that all indices returned in `find_intersections` correspond to sets # in b that overlap with the given set in a From fc5dea1b95a27525ae79b61e0594a71e0f2a1f24 Mon Sep 17 00:00:00 2001 From: David F Little Date: Fri, 17 Jun 2022 21:31:55 +0000 Subject: [PATCH 13/30] fix some type issues --- src/interval_sets.jl | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 59e635f0..cdaf8ff6 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -66,7 +66,11 @@ Base.:(==)(a::IntervalSet, b::IntervalSet) = a.items == b.items Base.isequal(a::IntervalSet, b::IntervalSet) = isequal(a, b) Base.Array(intervals::IntervalSet) = intervals.items -const AbstractIntervals = Union{AbstractInterval, IntervalSet} +# currently (to avoid breaking changes) new methods for `Base` +# accept `IntervalSet` objects and Interval singletons. +const AbstractIntervalSets = Union{AbstractInterval, IntervalSet} +# internal methods need to operate on both sets and arrays of intervals +const AbstractIntervals = Union{AbstractIntervalSets, AbstractArray{<:AbstractInterval}} # During merge operations used to compute unions, intersections etc..., # endpoint types can change (from left to right, and from open to closed, @@ -152,11 +156,6 @@ function unbunch(a::AbstractIntervals, b::AbstractIntervals; kwargs...) return a_, b_, tracking end -# TODO: Delete fallback once union deprecation is removed -function unbunch(a::Vector{<:AbstractInterval}, b::Vector{<:AbstractInterval}; kwargs...) - return unbunch(IntervalSet(a), IntervalSet(b); kwargs...) -end - # represent a sequence of endpoints as a sequence of one or more intervals function bunch(endpoints, tracking) @assert iseven(length(endpoints)) @@ -404,10 +403,10 @@ Base.intersect(x::IntervalSet, y::IntervalSet) = mergesets((inx, iny) -> inx && 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.issubset(x::AbstractIntervals, y::AbstractIntervals) = isempty(setdiff(x, y)) -Base.isdisjoint(x::AbstractIntervals, y::AbstractIntervals) = isempty(intersect(x, y)) +Base.issubset(x::AbstractIntervalSets, y::AbstractIntervalSets) = isempty(setdiff(x, y)) +Base.isdisjoint(x::AbstractIntervalSets, y::AbstractIntervalSets) = isempty(intersect(x, y)) -function Base.issetequal(x::AbstractIntervals, y::AbstractIntervals) +function Base.issetequal(x::AbstractIntervalSets, y::AbstractIntervalSets) x, y, tracking = unbunch(union(IntervalSet(x)), union(IntervalSet(y))) return x == y || all(isempty, bunch(x, tracking)) && all(isempty, bunch(y, tracking)) end From bcffe822c6dea8b525f805d39d1fcc93c730a181 Mon Sep 17 00:00:00 2001 From: David F Little Date: Fri, 17 Jun 2022 22:08:16 +0000 Subject: [PATCH 14/30] fix bug in interval set construction --- src/interval_sets.jl | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index cdaf8ff6..9cac769f 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -1,7 +1,7 @@ ###### Set-related Helpers ##### """ - IntervalSet{T<:AbstractInterval} <: AbstractSet{T} + IntervalSet{T<:AbstractInterval} An 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 @@ -48,7 +48,7 @@ julia> Array(setdiff(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) Interval{Int64, Open, Closed}(9, 10) ``` """ -struct IntervalSet{A <: AbstractVector{<:AbstractInterval}} +struct IntervalSet{I <: AbstractInterval, A <: AbstractVector{I}} items::A end @@ -58,7 +58,7 @@ IntervalSet(interval::IntervalSet) = interval IntervalSet(itr) = IntervalSet{eltype(itr)}(collect(itr)) IntervalSet() = IntervalSet{AbstractInterval}(AbstractInterval[]) -Base.copy(intervals::IntervalSet{T}) where T = IntervalSet{T}(copy(intervals.items)) +Base.copy(intervals::IntervalSet{T, A}) where {T, A} = IntervalSet{T, A}(copy(intervals.items)) Base.length(intervals::IntervalSet) = length(intervals.items) Base.iterate(intervals::IntervalSet, args...) = iterate(intervals.items, args...) Base.eltype(::IntervalSet{T}) where T = T @@ -110,8 +110,6 @@ 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)) - -# TODO: Delete once union deprecation is gone. endpoint_tracking(a::AbstractVector, b::AbstractVector) = endpoint_tracking(eltype(a), eltype(b)) # track: run a thunk, but only if we are tracking endpoints dynamically From 7f8017d025ada26ace1ed8fdb15a000b0e9d69b9 Mon Sep 17 00:00:00 2001 From: David F Little Date: Fri, 17 Jun 2022 22:31:08 +0000 Subject: [PATCH 15/30] type cleanup --- src/interval_sets.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 9cac769f..3e844c05 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -52,11 +52,11 @@ struct IntervalSet{I <: AbstractInterval, A <: AbstractVector{I}} items::A end -IntervalSet(v::AbstractVector) = IntervalSet{eltype(v)}(v) -IntervalSet(interval::T) where T <: AbstractInterval = IntervalSet{T}([interval]) +IntervalSet{T}(intervals) where T <: AbstractInterval = IntervalSet{T, Vector{T}}(intervals) +IntervalSet(interval::T) where T <: AbstractInterval = IntervalSet{T, Vector{T}}([interval]) IntervalSet(interval::IntervalSet) = interval IntervalSet(itr) = IntervalSet{eltype(itr)}(collect(itr)) -IntervalSet() = IntervalSet{AbstractInterval}(AbstractInterval[]) +IntervalSet() = IntervalSet(AbstractInterval[]) Base.copy(intervals::IntervalSet{T, A}) where {T, A} = IntervalSet{T, A}(copy(intervals.items)) Base.length(intervals::IntervalSet) = length(intervals.items) From e077615398bb9bec4e880b3b726337a3d65c49e1 Mon Sep 17 00:00:00 2001 From: David F Little Date: Tue, 21 Jun 2022 13:26:47 +0000 Subject: [PATCH 16/30] removed array-based type alias --- src/interval_sets.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 3e844c05..8de76950 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -68,9 +68,7 @@ Base.Array(intervals::IntervalSet) = intervals.items # currently (to avoid breaking changes) new methods for `Base` # accept `IntervalSet` objects and Interval singletons. -const AbstractIntervalSets = Union{AbstractInterval, IntervalSet} -# internal methods need to operate on both sets and arrays of intervals -const AbstractIntervals = Union{AbstractIntervalSets, AbstractArray{<:AbstractInterval}} +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, @@ -132,7 +130,8 @@ function unbunch(interval::AbstractInterval, tracking::EndpointTracking; lt=isle return endpoint_type(tracking)[LeftEndpoint(interval), RightEndpoint(interval)] end unbunch_by_fn(_) = identity -function unbunch(intervals::Union{AbstractIntervals, Base.Iterators.Enumerate{<:AbstractIntervals}}, +function unbunch(intervals::Union{AbstractVector{<:AbstractInterval}, AbstractIntervals, + Base.Iterators.Enumerate{<:AbstractIntervals}}, tracking::EndpointTracking; lt=isless) by = unbunch_by_fn(intervals) filtered = Iterators.filter(!isempty ∘ by, intervals) @@ -147,7 +146,8 @@ function unbunch((i, interval)::Tuple, tracking; lt=isless) return eltype[(i, LeftEndpoint(interval)), (i, RightEndpoint(interval))] end -function unbunch(a::AbstractIntervals, b::AbstractIntervals; kwargs...) +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...) @@ -424,8 +424,8 @@ end """ find_intersections( - x::Union{AbstractInterval, IntervalSet}, - y::Union{AbstractInterval, IntervalSet}, + x::AbstractVector{<:AbstractInterval}, + y::AbstractVector{<:AbstractInterval} ) Returns a `Vector{Vector{Int}}` where the value at index `i` gives the indices to all From 3ec49101b563ae3b148d76ec95339e84177ecb69 Mon Sep 17 00:00:00 2001 From: David F Little Date: Tue, 21 Jun 2022 13:27:05 +0000 Subject: [PATCH 17/30] fixed typo --- src/interval_sets.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 8de76950..d0c42d06 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -146,7 +146,7 @@ function unbunch((i, interval)::Tuple, tracking; lt=isless) return eltype[(i, LeftEndpoint(interval)), (i, RightEndpoint(interval))] end -function unbunch(a::Union{AbstractVector{<:AbstractInterval}, AbstractIntervals, +function unbunch(a::Union{AbstractVector{<:AbstractInterval}, AbstractIntervals}, b::Union{AbstractVector{<:AbstractInterval}, AbstractIntervals}; kwargs...) tracking = endpoint_tracking(a, b) a_ = unbunch(a, tracking; kwargs...) From fe6bb9163c0d75fb2be2891a01235069b0e89284 Mon Sep 17 00:00:00 2001 From: David F Little Date: Tue, 21 Jun 2022 13:41:34 +0000 Subject: [PATCH 18/30] fixed missing set alias --- src/interval_sets.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index d0c42d06..35e03dd6 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -401,10 +401,10 @@ Base.intersect(x::IntervalSet, y::IntervalSet) = mergesets((inx, iny) -> inx && 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.issubset(x::AbstractIntervalSets, y::AbstractIntervalSets) = isempty(setdiff(x, y)) -Base.isdisjoint(x::AbstractIntervalSets, y::AbstractIntervalSets) = isempty(intersect(x, y)) +Base.issubset(x::AbstractIntervals, y::AbstractIntervals) = isempty(setdiff(x, y)) +Base.isdisjoint(x::AbstractIntervals, y::AbstractIntervals) = isempty(intersect(x, y)) -function Base.issetequal(x::AbstractIntervalSets, y::AbstractIntervalSets) +function Base.issetequal(x::AbstractIntervals, y::AbstractIntervals) x, y, tracking = unbunch(union(IntervalSet(x)), union(IntervalSet(y))) return x == y || all(isempty, bunch(x, tracking)) && all(isempty, bunch(y, tracking)) end From f8a9a908d5657485f9454bd0f5e15fcd7ab98838 Mon Sep 17 00:00:00 2001 From: David F Little Date: Tue, 21 Jun 2022 17:05:09 +0000 Subject: [PATCH 19/30] fix method signature --- src/interval_sets.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 35e03dd6..0d08c4c1 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -131,7 +131,8 @@ function unbunch(interval::AbstractInterval, tracking::EndpointTracking; lt=isle end unbunch_by_fn(_) = identity function unbunch(intervals::Union{AbstractVector{<:AbstractInterval}, AbstractIntervals, - Base.Iterators.Enumerate{<:AbstractIntervals}}, + Base.Iterators.Enumerate{<:Union{AbstractIntervals, + AbstractVector{<:AbstractInterval}}}}, tracking::EndpointTracking; lt=isless) by = unbunch_by_fn(intervals) filtered = Iterators.filter(!isempty ∘ by, intervals) From 525e59fc4a98fb4e6d6438f314288f09983dc9c6 Mon Sep 17 00:00:00 2001 From: David F Little Date: Tue, 21 Jun 2022 17:53:56 +0000 Subject: [PATCH 20/30] changed `Array` to `convert(Array` --- src/interval_sets.jl | 16 ++++++++-------- test/sets.jl | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 0d08c4c1..8ea4ade8 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -14,35 +14,35 @@ see also: https://en.wikipedia.org/wiki/Interval_arithmetic#Interval_operators ```jldoctest julia> using Intervals -julia> Array(union(IntervalSet(1..5), IntervalSet(3..8))) +julia> convert(Array, union(IntervalSet(1..5), IntervalSet(3..8))) 1-element Vector{Interval{Int64, Closed, Closed}}: Interval{Int64, Closed, Closed}(1, 8) -julia> Array(intersect(IntervalSet(1..5), IntervalSet(3..8))) +julia> convert(Array, intersect(IntervalSet(1..5), IntervalSet(3..8))) 1-element Vector{Interval{Int64, Closed, Closed}}: Interval{Int64, Closed, Closed}(3, 5) -julia> Array(symdiff(IntervalSet(1..5), IntervalSet(3..8))) +julia> convert(Array, symdiff(IntervalSet(1..5), IntervalSet(3..8))) 2-element Vector{Interval{Int64}}: Interval{Int64, Closed, Open}(1, 3) Interval{Int64, Open, Closed}(5, 8) -julia> Array(union(IntervalSet([1..2, 2..5]), IntervalSet(6..7))) +julia> convert(Array, union(IntervalSet([1..2, 2..5]), IntervalSet(6..7))) 2-element Vector{Interval{Int64, Closed, Closed}}: Interval{Int64, Closed, Closed}(1, 5) Interval{Int64, Closed, Closed}(6, 7) -julia> Array(union(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) +julia> convert(Array, union(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) 2-element Vector{Interval{Int64, Closed, Closed}}: Interval{Int64, Closed, Closed}(1, 10) Interval{Int64, Closed, Closed}(12, 14) -julia> Array(intersect(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) +julia> convert(Array, intersect(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) 2-element Vector{Interval{Int64, Closed, Closed}}: Interval{Int64, Closed, Closed}(4, 5) Interval{Int64, Closed, Closed}(8, 9) -julia> Array(setdiff(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) +julia> convert(Array, setdiff(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) 2-element Vector{Interval{Int64}}: Interval{Int64, Closed, Open}(1, 4) Interval{Int64, Open, Closed}(9, 10) @@ -64,7 +64,7 @@ Base.iterate(intervals::IntervalSet, args...) = iterate(intervals.items, args... Base.eltype(::IntervalSet{T}) where T = T Base.:(==)(a::IntervalSet, b::IntervalSet) = a.items == b.items Base.isequal(a::IntervalSet, b::IntervalSet) = isequal(a, b) -Base.Array(intervals::IntervalSet) = intervals.items +Base.convert(::Type{T}, intervals::IntervalSet) where T <: AbstractArray = convert(T, intervals.items) # currently (to avoid breaking changes) new methods for `Base` # accept `IntervalSet` objects and Interval singletons. diff --git a/test/sets.jl b/test/sets.jl index a10a5afd..51c503a3 100644 --- a/test/sets.jl +++ b/test/sets.jl @@ -50,7 +50,7 @@ end @test isdisjoint(setdiff(a, b), b) @test !isdisjoint(a, a) - intersections = find_intersections(Array(a), Array(b)) + 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 From 5453749b3b7d381d0a544c16611f8a3f27ab77c6 Mon Sep 17 00:00:00 2001 From: David F Little Date: Wed, 22 Jun 2022 14:25:05 +0000 Subject: [PATCH 21/30] revisions based on review --- src/interval_sets.jl | 94 ++++++++++++++++++++++---------------------- test/sets.jl | 14 ++++++- 2 files changed, 58 insertions(+), 50 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 8ea4ade8..48401662 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -12,59 +12,54 @@ see also: https://en.wikipedia.org/wiki/Interval_arithmetic#Interval_operators ## Examples -```jldoctest -julia> using Intervals -julia> convert(Array, union(IntervalSet(1..5), IntervalSet(3..8))) -1-element Vector{Interval{Int64, Closed, Closed}}: - Interval{Int64, Closed, Closed}(1, 8) - -julia> convert(Array, intersect(IntervalSet(1..5), IntervalSet(3..8))) -1-element Vector{Interval{Int64, Closed, Closed}}: - Interval{Int64, Closed, Closed}(3, 5) - -julia> convert(Array, symdiff(IntervalSet(1..5), IntervalSet(3..8))) -2-element Vector{Interval{Int64}}: - Interval{Int64, Closed, Open}(1, 3) - Interval{Int64, Open, Closed}(5, 8) - -julia> convert(Array, union(IntervalSet([1..2, 2..5]), IntervalSet(6..7))) -2-element Vector{Interval{Int64, Closed, Closed}}: - Interval{Int64, Closed, Closed}(1, 5) - Interval{Int64, Closed, Closed}(6, 7) - -julia> convert(Array, union(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) -2-element Vector{Interval{Int64, Closed, Closed}}: - Interval{Int64, Closed, Closed}(1, 10) - Interval{Int64, Closed, Closed}(12, 14) - -julia> convert(Array, intersect(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) -2-element Vector{Interval{Int64, Closed, Closed}}: - Interval{Int64, Closed, Closed}(4, 5) - Interval{Int64, Closed, Closed}(8, 9) - -julia> convert(Array, setdiff(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14]))) -2-element Vector{Interval{Int64}}: - Interval{Int64, Closed, Open}(1, 4) - Interval{Int64, Open, Closed}(9, 10) +```jldoctest; setup = :(using Intervals) +julia> union(IntervalSet(1..5), IntervalSet(3..8)) +[1 .. 8] + +julia> intersect(IntervalSet(1..5), IntervalSet(3..8)) +[3 .. 5] + +julia> symdiff(IntervalSet(1..5), IntervalSet(3..8)) +[1 .. 3) ∪ (5 .. 8] + +julia> union(IntervalSet([1..2, 2..5]), IntervalSet(6..7)) +[1 .. 5] ∪ [6 .. 7] + +julia> union(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14])) +[1 .. 10] ∪ [12 .. 14] + +julia> intersect(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14])) +[4 .. 5] ∪ [8 .. 9] + +julia> setdiff(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14])) +[1 .. 4) ∪ (9 .. 10] ``` """ -struct IntervalSet{I <: AbstractInterval, A <: AbstractVector{I}} - items::A +struct IntervalSet{T <: AbstractInterval} + items::Vector{T} end -IntervalSet{T}(intervals) where T <: AbstractInterval = IntervalSet{T, Vector{T}}(intervals) -IntervalSet(interval::T) where T <: AbstractInterval = IntervalSet{T, Vector{T}}([interval]) +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, A}) where {T, A} = IntervalSet{T, A}(copy(intervals.items)) +Base.copy(intervals::IntervalSet{T}) where {T} = IntervalSet{T}(copy(intervals.items)) Base.length(intervals::IntervalSet) = length(intervals.items) Base.iterate(intervals::IntervalSet, args...) = iterate(intervals.items, args...) Base.eltype(::IntervalSet{T}) where T = T -Base.:(==)(a::IntervalSet, b::IntervalSet) = a.items == b.items -Base.isequal(a::IntervalSet, b::IntervalSet) = isequal(a, b) +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::IO, ::MIME"text/plain", x::IntervalSet) + intervals = union(x) + iocompact = IOContext(io, :compact => true) + for interval in intervals.items[1:(end-1)] + show(iocompact, MIME"text/plain"(), interval) + print(io, " ∪ ") + end + show(iocompact, MIME"text/plain"(), intervals.items[end]) +end # currently (to avoid breaking changes) new methods for `Base` # accept `IntervalSet` objects and Interval singletons. @@ -410,6 +405,9 @@ function Base.issetequal(x::AbstractIntervals, y::AbstractIntervals) return x == y || all(isempty, bunch(x, tracking)) && all(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) + # 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 @@ -432,15 +430,15 @@ end Returns a `Vector{Vector{Int}}` where the value at index `i` gives the indices to all intervals in `y` that intersect with `x[i]`. """ -function find_intersections(x_::Union{AbstractInterval, AbstractVector{<:AbstractInterval}}, y_::Union{AbstractInterval, AbstractVector{<:AbstractInterval}}) - xa, ya = vcat(x_), vcat(y_) - tracking = endpoint_tracking(xa, ya) +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 = unbunch(enumerate(xa), tracking; lt) - y = unbunch(enumerate(ya), tracking; lt) - result = [Vector{Int}() for _ in 1:length(xa)] + 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, y, lt) + return find_intersections_helper!(result, x_endpoints, y_endpoints, lt) end function find_intersections_helper!(result, x, y, lt) diff --git a/test/sets.jl b/test/sets.jl index 51c503a3..600de81a 100644 --- a/test/sets.jl +++ b/test/sets.jl @@ -36,10 +36,20 @@ end rand_bound_type(rng) = rand(rng, (Closed, Open)) - # verify case where we interpret array as a set of intervals (rather than a set of - # points) + # 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])) + function testsets(a, b) @test area(a ∪ b) ≤ area(myunion(a)) + area(myunion(b)) @test area(setdiff(a, b)) ≤ area(myunion(a)) From c170ffcf956f4060a5b86a25db317f471b31ee5a Mon Sep 17 00:00:00 2001 From: David F Little Date: Thu, 23 Jun 2022 16:09:15 +0000 Subject: [PATCH 22/30] revised show method --- src/interval_sets.jl | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 48401662..a2ebb04c 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -14,25 +14,37 @@ see also: https://en.wikipedia.org/wiki/Interval_arithmetic#Interval_operators ```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)) -[1 .. 3) ∪ (5 .. 8] +2-interval IntervalSet{Interval{Int64}}: +[1 .. 3) +(5 .. 8] julia> union(IntervalSet([1..2, 2..5]), IntervalSet(6..7)) -[1 .. 5] ∪ [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])) -[1 .. 10] ∪ [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])) -[4 .. 5] ∪ [8 .. 9] +2-interval IntervalSet{Interval{Int64, Closed, Closed}}: +[4 .. 5] +[8 .. 9] julia> setdiff(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14])) -[1 .. 4) ∪ (9 .. 10] +2-interval IntervalSet{Interval{Int64}}: +[1 .. 4) +(9 .. 10] ``` """ struct IntervalSet{T <: AbstractInterval} @@ -54,11 +66,14 @@ Base.convert(::Type{T}, intervals::IntervalSet) where T <: AbstractArray = conve function Base.show(io::IO, ::MIME"text/plain", x::IntervalSet) intervals = union(x) iocompact = IOContext(io, :compact => true) + print(io, "$(length(intervals))-interval ") + show(io, MIME"text/plain"(), typeof(x)) + println(io, ":") for interval in intervals.items[1:(end-1)] show(iocompact, MIME"text/plain"(), interval) - print(io, " ∪ ") + println(io, "") end - show(iocompact, MIME"text/plain"(), intervals.items[end]) + isempty(intervals) || show(iocompact, MIME"text/plain"(), intervals.items[end]) end # currently (to avoid breaking changes) new methods for `Base` From 99b5c6e58abe399ac3a16bf9e26d9a747d42e2f1 Mon Sep 17 00:00:00 2001 From: David F Little Date: Thu, 23 Jun 2022 16:35:33 +0000 Subject: [PATCH 23/30] handle display size --- src/interval_sets.jl | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index a2ebb04c..7aef1153 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -63,17 +63,32 @@ Base.eltype(::IntervalSet{T}) where T = T 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::IO, ::MIME"text/plain", x::IntervalSet) +function Base.show(io::Base.AbstractPipe, ::MIME"text/plain", x::IntervalSet) intervals = union(x) iocompact = IOContext(io, :compact => true) print(io, "$(length(intervals))-interval ") show(io, MIME"text/plain"(), typeof(x)) println(io, ":") - for interval in intervals.items[1:(end-1)] - show(iocompact, MIME"text/plain"(), interval) - println(io, "") + nrows = displaysize(io)[1] + half = fld(nrows, 2) - 2 + if nrows ≥ length(intervals.items) && 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 - isempty(intervals) || show(iocompact, MIME"text/plain"(), intervals.items[end]) end # currently (to avoid breaking changes) new methods for `Base` From a011e00b270b69906a3a8ec02f7cbb4d0dc3d2e4 Mon Sep 17 00:00:00 2001 From: rofinn Date: Thu, 23 Jun 2022 15:32:13 -0700 Subject: [PATCH 24/30] Fixed doctests for Julia 1.6 and made the IntervalSet non-iterable. --- src/deprecated.jl | 4 ++-- src/interval_sets.jl | 36 ++++++++++++++++++++++-------------- test/sets.jl | 44 ++++++++++++++++++++++---------------------- 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/src/deprecated.jl b/src/deprecated.jl index a549e981..e6c370b5 100644 --- a/src/deprecated.jl +++ b/src/deprecated.jl @@ -195,8 +195,8 @@ function HB(anchor, inc::Inclusivity) return HourBeginning{L,R}(floor(anchor, Hour)) end -@deprecate union(intervals::AbstractVector{<:AbstractInterval}) collect(union(IntervalSet(intervals))) -@deprecate union!(intervals::AbstractVector{<:AbstractInterval}) collect(union!(IntervalSet(intervals))) +@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/interval_sets.jl b/src/interval_sets.jl index 7aef1153..0ca7f389 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -22,7 +22,7 @@ julia> intersect(IntervalSet(1..5), IntervalSet(3..8)) [3 .. 5] julia> symdiff(IntervalSet(1..5), IntervalSet(3..8)) -2-interval IntervalSet{Interval{Int64}}: +2-interval IntervalSet{Interval{Int64, L, R} where {L<:Bound, R<:Bound}}: [1 .. 3) (5 .. 8] @@ -42,7 +42,7 @@ julia> intersect(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14])) [8 .. 9] julia> setdiff(IntervalSet([1..5, 8..10]), IntervalSet([4..9, 12..14])) -2-interval IntervalSet{Interval{Int64}}: +2-interval IntervalSet{Interval{Int64, L, R} where {L<:Bound, R<:Bound}}: [1 .. 4) (9 .. 10] ``` @@ -57,21 +57,21 @@ IntervalSet(itr) = IntervalSet{eltype(itr)}(collect(itr)) IntervalSet() = IntervalSet(AbstractInterval[]) Base.copy(intervals::IntervalSet{T}) where {T} = IntervalSet{T}(copy(intervals.items)) -Base.length(intervals::IntervalSet) = length(intervals.items) -Base.iterate(intervals::IntervalSet, args...) = iterate(intervals.items, args...) 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) +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, "$(length(intervals))-interval ") + print(io, "$n-interval ") show(io, MIME"text/plain"(), typeof(x)) println(io, ":") nrows = displaysize(io)[1] half = fld(nrows, 2) - 2 - if nrows ≥ length(intervals.items) && half > 1 + if nrows ≥ n && half > 1 for interval in intervals.items[1:(end-1)] show(iocompact, MIME"text/plain"(), interval) println(io, "") @@ -154,11 +154,18 @@ interval_type(::TrackRightOpen{T}) where T = Interval{T, Closed, Open} 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}, AbstractIntervals, - Base.Iterators.Enumerate{<:Union{AbstractIntervals, - AbstractVector{<:AbstractInterval}}}}, - tracking::EndpointTracking; lt=isless) +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)[] @@ -172,7 +179,7 @@ function unbunch((i, interval)::Tuple, tracking; lt=isless) return eltype[(i, LeftEndpoint(interval)), (i, RightEndpoint(interval))] end -function unbunch(a::Union{AbstractVector{<:AbstractInterval}, AbstractIntervals}, +function unbunch(a::Union{AbstractVector{<:AbstractInterval}, AbstractIntervals}, b::Union{AbstractVector{<:AbstractInterval}, AbstractIntervals}; kwargs...) tracking = endpoint_tracking(a, b) a_ = unbunch(a, tracking; kwargs...) @@ -427,16 +434,17 @@ Base.intersect(x::IntervalSet, y::IntervalSet) = mergesets((inx, iny) -> inx && 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.issubset(x::T, y::IntervalSet{<:AbstractInterval{T}}) where {T} = any(i -> x in i, y.items) Base.issubset(x::AbstractIntervals, y::AbstractIntervals) = isempty(setdiff(x, y)) Base.isdisjoint(x::AbstractIntervals, y::AbstractIntervals) = isempty(intersect(x, y)) function Base.issetequal(x::AbstractIntervals, y::AbstractIntervals) x, y, tracking = unbunch(union(IntervalSet(x)), union(IntervalSet(y))) - return x == y || all(isempty, bunch(x, tracking)) && all(isempty, bunch(y, tracking)) + 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) +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) diff --git a/test/sets.jl b/test/sets.jl index 600de81a..38ac987d 100644 --- a/test/sets.jl +++ b/test/sets.jl @@ -81,10 +81,10 @@ end # 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(first.(a) .∈ a) + @test all(x -> first(x) ∈ a, a.items) testsets(a, b) - testsets(IntervalSet(first(a)), b) - testsets(a, IntervalSet(first(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)]) @@ -99,34 +99,34 @@ end a = IntervalSet(Interval.(starts, ends)) b = IntervalSet(Interval.(starts .+ offsets, ends .+ offsets)) - @test all(first.(a) .∈ a) + @test all(x -> first(x) ∈ a, a.items) testsets(a, b) - testsets(IntervalSet(first(a)), b) - testsets(a, IntervalSet(first(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)), b) - testsets(a, IntervalSet(first(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)), b) isa Intervals.TrackStatically - @test Intervals.endpoint_tracking(a, IntervalSet(first(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)), b) - testsets(a, IntervalSet(first(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)), b) isa Intervals.TrackStatically - @test Intervals.endpoint_tracking(a, IntervalSet(first(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)), b) - testsets(a, IntervalSet(first(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)) @@ -134,10 +134,10 @@ end a = IntervalSet(Interval{Closed, Open}.(starts, ends)) b = IntervalSet(Interval{Closed, Open}.(starts .+ offsets, ends .+ offsets)) - testsets(a, IntervalSet(randint.(b))) - testsets(IntervalSet(first(a)), IntervalSet(randint.(b))) - testsets(a, IntervalSet(leftint.(IntervalSet(first(b))))) - testsets(a, IntervalSet(rightint.(b))) - testsets(IntervalSet(first(a)), IntervalSet(rightint.(b))) - testsets(a, IntervalSet(rightint.(IntervalSet(first(b))))) + 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 From 1a5713286c080f0a35c5b9db11bc55bbb4cb2bb4 Mon Sep 17 00:00:00 2001 From: rofinn Date: Thu, 23 Jun 2022 15:36:53 -0700 Subject: [PATCH 25/30] Build documentation on 1.6 for now. --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From ca36a293cf54538a881ac391c52f5ea45ab2ba98 Mon Sep 17 00:00:00 2001 From: rofinn Date: Fri, 24 Jun 2022 11:23:05 -0700 Subject: [PATCH 26/30] Added a few more issubset signatures that don't work because we're falling back to the base `setdiff`. --- test/sets.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/sets.jl b/test/sets.jl index 38ac987d..8dd0f077 100644 --- a/test/sets.jl +++ b/test/sets.jl @@ -49,6 +49,11 @@ end @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])) + @test issubset(2 .. 4, IntervalSet([1 .. 5, 7 .. 9])) + @test !issubset(2 .. 4, IntervalSet([1 ..3, 5 .. 10])) + @test issubset(IntervalSet([1 .. 3, 5 .. 10]), 1 .. 20) + @test !issubset(IntervalSet([1 .. 3, 5 .. 10]), 1 .. 9) function testsets(a, b) @test area(a ∪ b) ≤ area(myunion(a)) + area(myunion(b)) From 40aa82ba72d1e0f4012f3822937332402441abbc Mon Sep 17 00:00:00 2001 From: rofinn Date: Fri, 24 Jun 2022 11:47:32 -0700 Subject: [PATCH 27/30] Fix isssubset signatures. --- src/interval_sets.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 0ca7f389..06b23952 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -434,10 +434,13 @@ Base.intersect(x::IntervalSet, y::IntervalSet) = mergesets((inx, iny) -> inx && 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.issubset(x::T, y::IntervalSet{<:AbstractInterval{T}}) where {T} = any(i -> x in i, y.items) -Base.issubset(x::AbstractIntervals, y::AbstractIntervals) = isempty(setdiff(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)) + 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)) From 212469e5343051e799ec9e18fc78f1219c7f7c5b Mon Sep 17 00:00:00 2001 From: rofinn Date: Fri, 24 Jun 2022 14:34:06 -0700 Subject: [PATCH 28/30] More set operation tests for mismatched Interval and IntervalSet arguments. --- test/sets.jl | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/test/sets.jl b/test/sets.jl index 8dd0f077..e28e24fc 100644 --- a/test/sets.jl +++ b/test/sets.jl @@ -50,10 +50,6 @@ end @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])) - @test issubset(2 .. 4, IntervalSet([1 .. 5, 7 .. 9])) - @test !issubset(2 .. 4, IntervalSet([1 ..3, 5 .. 10])) - @test issubset(IntervalSet([1 .. 3, 5 .. 10]), 1 .. 20) - @test !issubset(IntervalSet([1 .. 3, 5 .. 10]), 1 .. 9) function testsets(a, b) @test area(a ∪ b) ≤ area(myunion(a)) + area(myunion(b)) @@ -78,6 +74,24 @@ end @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 From 376484a20a19f0719052dba03132dcfe6b9df16b Mon Sep 17 00:00:00 2001 From: rofinn Date: Fri, 24 Jun 2022 14:47:53 -0700 Subject: [PATCH 29/30] Fix interval set operation which were incorrectly falling back to base. --- src/interval_sets.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 06b23952..3bfb4e85 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -434,6 +434,7 @@ Base.intersect(x::IntervalSet, y::IntervalSet) = mergesets((inx, iny) -> inx && 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 @@ -441,6 +442,12 @@ Base.issubset(x::AbstractInterval, y::IntervalSet) = any(Base.Fix1(issubset, x), 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)) From e891f305483365dd01da1153eb6fe2ca954fe209 Mon Sep 17 00:00:00 2001 From: Rory Finnegan Date: Thu, 30 Jun 2022 13:15:01 -0500 Subject: [PATCH 30/30] Apply suggestions from code review Co-authored-by: mattBrzezinski --- src/interval_sets.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/interval_sets.jl b/src/interval_sets.jl index 3bfb4e85..16211696 100644 --- a/src/interval_sets.jl +++ b/src/interval_sets.jl @@ -3,7 +3,7 @@ """ IntervalSet{T<:AbstractInterval} -An set of points represented by a sequence of intervals. Set operations over interval sets +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`. @@ -434,7 +434,6 @@ Base.intersect(x::IntervalSet, y::IntervalSet) = mergesets((inx, iny) -> inx && 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