diff --git a/src/Extents.jl b/src/Extents.jl index b630378..cd659bb 100644 --- a/src/Extents.jl +++ b/src/Extents.jl @@ -8,6 +8,44 @@ const ORDER_DOC = """ The order of dimensions is ignored in all cases. """ +abstract type AbstractBounds{L,U,T} end + +const CC = AbstractBounds{:closed,:closed} +const CO = AbstractBounds{:closed,:open} +const OC = AbstractBounds{:open,:closed} +const OO = AbstractBounds{:open,:open} + +(::Type{T})(bounds::Tuple{<:Any,<:Any}) where {T<:AbstractBounds{L,U}} where {L,U} = + T(bounds; kw...) + +Base.getindex(b::AbstractBounds, i) = b.bounds[i] +Base.first(b::AbstractBounds) = b[1] +Base.last(b::AbstractBounds) = b[2] +Base.iterate(b::AbstractBounds, args...) = iterate(b.bounds, args...) + +struct Bounds{L,U,T} <: AbstractBounds{L,U,T} + bounds::Tuple{T,T} + Bounds{L,U}(bounds::Tuple{T,T}) where {L,U,T} = new{L,U,T}(bounds) +end +Bounds{L,U}(a, b) where {L,U} = Bounds{L,U}((a, b)) +Bounds{L,U}((a, b)::Tuple) where {L,U} = Bounds{L,U}(promote(a, b)) + +Base.show(io::IO, bs::Bounds) = + print(io, "$(typeof(bs).name.wrapper)($(bs[1]), $(bs[2]))") + +struct CyclicBounds{L,U,T,C} <: AbstractBounds{L,U,T} + bounds::Tuple{T,T} + cycle::Tuple{T,T} + CyclicBounds{L,U}(bounds::Tuple{T,T}, cycle::Tuple{T,T}) where {L,U,T,C} = + new{L,U,T,C}(bounds, cycle) +end +CyclicBounds{L,U}(a, b; kw...) where {L,U} = CyclicBounds{L,U}((a, b); kw...) +CyclicBounds{L,U}((a, b)::Tuple{<:Any,<:Any}; cycle) where {L,U} = + CyclicBounds{L,U}(promote(a, b), cycle) + +Base.show(io::IO, bs::CyclicBounds) = + print(io, "$(typeof(bs).name.wrapper)($(bs[1]), $(bs[2]); cycle=$(bs.cycle))") + """ Extent @@ -38,7 +76,7 @@ julia> values(ext) struct Extent{K,V} bounds::NamedTuple{K,V} function Extent{K,V}(bounds::NamedTuple{K,V}) where {K,V} - bounds = map(b -> promote(b...), bounds) + bounds = map(_promote, bounds) new{K,typeof(values(bounds))}(bounds) end end @@ -49,6 +87,9 @@ Extent{K}(vals) where {K} = Extent{K}(NamedTuple{K}(vals)) Extent{K1}(vals::NamedTuple{K2,V}) where {K1,K2,V} = Extent(NamedTuple{K1}(vals)) Extent(vals::NamedTuple{K,V}) where {K,V} = Extent{K,V}(vals) +_promote(b::Tuple) = promote(b...) +_promote(b::AbstractBounds) = b + bounds(ext::Extent) = getfield(ext, :bounds) @inline function Base.getproperty(ext::Extent, key::Symbol) @@ -283,7 +324,7 @@ $DE_9IM_DOC contains(a::Extent, b::Extent; strict=false) = _do_bounds(all, _contain, a, b, strict) # Must contain interior points, not just boundary -_contain(a::Tuple, b::Tuple) = _cover(a, b) && _hasinterior(b) +_contain(a, b) = _cover(a, b) && _hasinterior(b) """ within(a::Extent, b::Extent; strict=false) @@ -323,9 +364,10 @@ $DE_9IM_DOC """ intersects(a::Extent, b::Extent; strict=false) = _do_bounds(all, _intersect, a, b, strict) -_intersect((min_a, max_a)::Tuple, (min_b, max_b)::Tuple) = +_intersect((min_a, max_a), (min_b, max_b)) = (min_a <= min_b && max_a >= min_b) || (min_b <= min_a && max_b >= min_a) + """ disjoint(a::Extent, b::Extent; strict=false) @@ -369,8 +411,17 @@ function touches(a::Extent, b::Extent; strict=false) end end -_touch((min_a, max_a)::Tuple, (min_b, max_b)::Tuple) = (min_a == max_b || max_a == min_b) - +_touch((min_a, max_a)::T, (min_b, max_b)::T ) where T = (min_a == max_b || max_a == min_b) +_touch((min_a, max_a)::CC, (min_b, max_b)::CO) = max_a == min_b +_touch((min_a, max_a)::OO, (min_b, max_b)::OC) = min_a == max_b +_touch((min_a, max_a)::CC, (min_b, max_b)::OC) = min_a == max_b +_touch((min_a, max_a)::OO, (min_b, max_b)::CO) = max_a == min_b +_touch((min_a, max_a)::CO, (min_b, max_b)::CC) = min_a == max_b +_touch((min_a, max_a)::OC, (min_b, max_b)::OO) = min_a == max_b +_touch((min_a, max_a)::OC, (min_b, max_b)::CC) = max_a == min_b +_touch((min_a, max_a)::CO, (min_b, max_b)::OO) = max_a == min_b +_touch((min_a, max_a)::CO, (min_b, max_b)::OC) = false +_touch((min_a, max_a)::OC, (min_b, max_b)::CO) = false """ covers(a::Extent, b::Extent; strict=false) @@ -390,7 +441,9 @@ $DE_9IM_DOC """ covers(a::Extent, b::Extent; strict=false) = _do_bounds(all, _cover, a, b, strict) -_cover((min_a, max_a)::Tuple, (min_b, max_b)::Tuple) = (min_a <= min_b && max_a >= max_b) +_cover((min_a, max_a)::AbstractBounds{T,T}, (min_b, max_b)::AbstractBounds{T,T}) where {T} = + (min_a <= min_b && max_a >= max_b) +_cover((min_a, max_a)::CO, (min_b, max_b)) = (min_a <= min_b && max_a >= max_b) """ coveredby(a::Extent, b::Extent; strict=false) @@ -452,14 +505,16 @@ $ORDER_DOC $DE_9IM_DOC """ equals(a::Extent, b::Extent; strict=false) = _do_bounds(all, _equal, a, b, strict) +equals(a::Extent{L,U}, b::Extent{L,U}; strict=false) where {L,U} = _do_bounds(all, _equal, a, b, strict) -_equal(a::Tuple, b::Tuple) = a == b +_equal(a::AbstractBounds{L,U}, b::AbstractBounds{L,U}) where {L,U} = a == b +_equal(a, b) = false # Handle `nothing` bounds for all methods for f in (:_intersect, :_cover, :_contain, :_touch, :_equal) @eval begin - $f(::Nothing, ::Tuple) = nothing - $f(::Tuple, ::Nothing) = nothing + $f(::Nothing, ::Union{Tuple,AbstractBounds}) = nothing + $f(::Union{Tuple,AbstractBounds}, ::Nothing) = nothing $f(::Nothing, ::Nothing) = nothing end end @@ -536,6 +591,6 @@ _skipnothing(::Nothing, vals...) = _skipnothing(vals...) _skipnothing() = () _hasinterior(ex::Extent) = all(map(_hasinterior, bounds(ex))) -_hasinterior((min, max)::Tuple) = min != max +_hasinterior((min, max)) = min != max end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index fed423a..513f3e6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,6 +6,7 @@ const E = Extent ex1 = Extent(X=(1, 2), Y=(3, 4)) ex2 = Extent(Y=(3, 4), X=(1, 2)) ex3 = Extent(X=(1, 2), Y=(3, 4), Z=(5.0, 6.0)) +ex1 = Extent(X=Extents.Bounds(1, 2), Y=(3, 4)) struct HasExtent end Extents.extent(::HasExtent) = Extent(X=(0, 1), Y=(0, 1)) @@ -73,7 +74,7 @@ end @testset "covers" begin # An extent contains itself - @test Extents.covers(E(X=(2, 3), Y=(3, 4)), E(X=(2, 3), Y=(3, 4))) == true + @test Extents.covers(E(X=Extents.Bounds(2, 3), Y=(3, 4)), E(X=(2, 3), Y=(3, 4))) == true # A larger extent covers a smaller one inside it @test Extents.covers(E(X=(0, 4), Y=(1, 5)), E(X=(2, 3), Y=(3, 4))) == true # Intersecting but not covers in one dimension @@ -103,7 +104,7 @@ end @testset "coveredby" begin # An extent contains itself - @test Extents.coveredby(E(X=(2, 3), Y=(3, 4)), E(X=(2, 3), Y=(3, 4))) == true + @test Extents.coveredby(E(X=Extents.Bounds(2, 3), Y=(3, 4)), E(X=(2, 3), Y=(3, 4))) == true # A larger extent coveredby a smaller one inside it @test Extents.contains(E(X=(0, 4), Y=(1, 5)), E(X=(2, 3), Y=(3, 4))) == true @test Extents.coveredby(E(X=(2, 3), Y=(3, 4)), E(X=(0, 4), Y=(1, 5))) == true @@ -134,7 +135,7 @@ end @testset "contains" begin # An extent contains itself - @test Extents.contains(E(X=(2, 3), Y=(3, 4)), E(X=(2, 3), Y=(3, 4))) == true + @test Extents.contains(E(X=(2, 3), Y=(3, 4)), E(X=(2, 3), Y=Extents.Bounds(3, 4))) == true # A larger extent contains a smaller one inside it @test Extents.contains(E(X=(0, 4), Y=(1, 5)), E(X=(2, 3), Y=(3, 4))) == true # Intersecting but not contains in one dimension @@ -164,7 +165,7 @@ end @testset "within" begin # An extent contains itself - @test Extents.within(E(X=(2, 3), Y=(3, 4)), E(X=(2, 3), Y=(3, 4))) == true + @test Extents.within(E(X=(2, 3), Y=(3, 4)), E(X=Extents.Bounds(2, 3), Y=(3, 4))) == true # A larger extent within a smaller one inside it @test Extents.contains(E(X=(0, 4), Y=(1, 5)), E(X=(2, 3), Y=(3, 4))) == true @test Extents.within(E(X=(2, 3), Y=(3, 4)), E(X=(0, 4), Y=(1, 5))) == true