Skip to content

Commit

Permalink
Merge pull request #3 from rafaqz/more_methods
Browse files Browse the repository at this point in the history
add more Base and utility methods
  • Loading branch information
rafaqz authored Jun 20, 2022
2 parents 381fff6 + 978a46e commit 1235598
Show file tree
Hide file tree
Showing 2 changed files with 196 additions and 36 deletions.
154 changes: 142 additions & 12 deletions src/Extents.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,54 @@ A wrapper for a `NamedTuple` of tuples holding
the lower and upper bounds for each dimension of the object.
`keys(extent)` will return the dimension name Symbols,
in the order the dimensions are used in the object.
in the order the dimensions are used in the object.
`values` will return a tuple of tuples: `(lowerbound, upperbound)` for each dimension.
"""
struct Extent{T<:NamedTuple}
bounds::T
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)
new{K,typeof(values(bounds))}(bounds)
end
end
Extent(; kw...) = Extent(values(kw))
Extent{K}(vals::V) where {K,V} = Extent{K,V}(NamedTuple{K,V}(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)

bounds(ext::Extent) = getfield(ext, :bounds)

function Base.getproperty(ext::Extent, key::Symbol)
function Base.getproperty(ext::Extent, key::Symbol)
haskey(bounds(ext), key) || throw(ErrorException("Extent has no field $key"))
getproperty(bounds(ext), key)
end
function Base.getindex(ext::Extent, key::Symbol)

Base.getindex(ext::Extent, keys::NTuple{<:Any,Symbol}) = Extent{keys}(bounds(ext))
Base.getindex(ext::Extent, keys::AbstractVector{Symbol}) = ext[Tuple(keys)]
@inline function Base.getindex(ext::Extent, key::Symbol)
haskey(bounds(ext), key) || throw(ErrorException("Extent has no field $key"))
getindex(bounds(ext), key)
end
function Base.getindex(ext::Extent, i::Int)
@inline function Base.getindex(ext::Extent, i::Int)
haskey(bounds(ext), i) || throw(ErrorException("Extent has no field $i"))
getindex(bounds(ext), i)
end
Base.keys(ext::Extent{<:NamedTuple}) = keys(bounds(ext))
Base.haskey(ext::Extent, x) = haskey(bounds(ext), x)
Base.keys(ext::Extent) = keys(bounds(ext))
Base.values(ext::Extent) = values(bounds(ext))
Base.length(ext::Extent) = length(bounds(ext))
Base.iterate(ext::Extent, args...) = iterate(bounds(ext), args...)

function Base.:(==)(a::Extent{<:NamedTuple{K1}}, b::Extent{<:NamedTuple{K2}}) where {K1, K2}
length(K1) == length(K2) || return false
all(map(k -> k in K1, K2)) || return false
return all(map((k -> a[k] == b[k]), K1))
function Base.:(==)(a::Extent{K1}, b::Extent{K2}) where {K1,K2}
_keys_match(a, b) || return false
values_match = map(K1) do k
va = a[k]
vb = b[k]
isnothing(va) && isnothing(vb) || va == vb
end
return all(values_match)
end
Base.:(==)(a::Extent, b::Extent) = false

function Base.show(io::IO, ::MIME"text/plain", extent::Extent)
print(io, "Extent")
Expand All @@ -60,4 +76,118 @@ function extent end
extent(extent) = nothing
extent(extent::Extent) = extent

"""
intersects(ext1::Extent, ext2::Extent; strict=false)
Check if two `Extent` objects intersect.
Returns `true` if the extents of all common dimensions share some values
including just the edge values of their range.
Dimensions that are not shared are ignored by default, with `strict=false`.
When `strict=true`, any unshared dimensions cause the function to return `false`.
The order of dimensions is ignored in both cases.
If there are no common dimensions, `false` is returned.
"""
function intersects(ext1::Extent, ext2::Extent; strict=false)
_maybe_keys_match(ext1, ext2, strict) || return false
keys = _shared_keys(ext1, ext2)
if length(keys) == 0
return false # Otherwise `all` returns `true` for empty tuples
else
dimintersections = map(keys) do k
_bounds_intersect(ext1[_unwrap(k)], ext2[_unwrap(k)])
end
return all(dimintersections)
end
end
intersects(obj1, obj2) = intersects(extent(obj1), extent(obj2))
intersects(obj1::Extent, obj2::Nothing) = false
intersects(obj1::Nothing, obj2::Extent) = false
intersects(obj1::Nothing, obj2::Nothing) = false

"""
union(ext1::Extent, ext2::Extent; strict=false)
Get the union of two extents, e.g. the combined extent of both objects
for all dimensions.
Dimensions that are not shared are ignored by default, with `strict=false`.
When `strict=true`, any unshared dimensions cause the function to return `nothing`.
"""
function union(ext1::Extent, ext2::Extent; strict=false)
_maybe_keys_match(ext1, ext2, strict) || return nothing
keys = _shared_keys(ext1, ext2)
if length(keys) == 0
return nothing
else
values = map(keys) do k
k = _unwrap(k)
k_exts = (ext1[k], ext2[k])
a = min(map(first, k_exts)...)
b = max(map(last, k_exts)...)
(a, b)
end
return Extent{map(_unwrap, keys)}(values)
end
end
union(obj1, obj2) = union(extent(obj1), extent(obj2))
union(obj1, obj2, obj3, objs...) = union(union(obj1, obj2), obj3, objs...)

"""
intersect(ext1::Extent, ext2::Extent; strict=false)
Get the intersection of two extents as another `Extent`, e.g.
the area covered by the shared dimensions for both extents.
If there is no intersection for any shared dimension, `nothing` will be returned.
When `strict=true`, any unshared dimensions cause the function to return `nothing`.
"""
function intersect(ext1::Extent, ext2::Extent; strict=false)
_maybe_keys_match(ext1, ext2, strict) || return nothing
intersects(ext1, ext2) || return nothing
keys = _shared_keys(ext1, ext2)
values = map(keys) do k
k = _unwrap(k)
k_exts = (ext1[k], ext2[k])
a = max(map(first, k_exts)...)
b = min(map(last, k_exts)...)
(a, b)
end
return Extent{map(_unwrap, keys)}(values)
end
intersect(obj1, obj2) = intersect(extent(obj1), extent(obj2))
intersect(obj1, obj2, obj3, objs...) = intersect(intersect(obj1, obj2), obj3, objs...)

# Internal utils

_maybe_keys_match(ext1, ext2, strict) = !strict || _keys_match(ext1, ext2)

function _keys_match(::Extent{K1}, ::Extent{K2}) where {K1,K2}
length(K1) == length(K2) || return false
keys_match = map(K2) do k
k in K1
end |> all
end

function _bounds_intersect(b1::Tuple, b2::Tuple)
(b1[1] <= b2[2] && b1[2] >= b2[1])
end

# _shared_keys uses a static `Val{k}` instead of a `Symbol` to
# represent keys, because constant propagation fails through `reduce`
# meaning most of the time of `union` or `intersect` is doing the `Symbol` lookup.
# So we help the compiler out a little by doing manual constant propagation.
# We know K1 and K2 at compile time, and wrapping them in `Val{k}() maintains
# that through reduce. This makes union/intersect 15x faster, at ~10ns.
function _shared_keys(ext1::Extent{K1}, ext2::Extent{K2}) where {K1,K2}
reduce(K1; init=()) do acc, k
k in K2 ? (acc..., Val{k}()) : acc
end
end

_unwrap(::Val{X}) where {X} = X

end
78 changes: 54 additions & 24 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -1,28 +1,58 @@
using Extents
using Test

@testset "Extents.jl" begin
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))

@testset "bounds" begin
@test bounds(ex1) === (X=(1, 2), Y=(3, 4))
@test bounds(ex2) === (Y=(3, 4), X=(1, 2))
@test bounds(ex3) === (X=(1, 2), Y=(3, 4), Z=(5.0, 6.0))
end

@testset "extent" begin
@test extent(ex1) === ex1
end

@testset "equality" begin
@test ex1 == ex2
@test ex1 != ex3
end

@testset "properties" begin
@test keys(ex1) == (:X, :Y)
@test values(ex1) == ((1, 2), (3, 4))
end
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))

@testset "getindex" begin
@test ex3[1] == ex3[:X] == (1, 2)
@test ex3[[:X, :Z]] == ex3[(:X, :Z)] == Extent{(:X, :Z)}(((1, 2), (5.0, 6.0)))
end

@testset "getproperty" begin
@test ex3.X == (1, 2)
end

@testset "bounds" begin
@test bounds(ex1) === (X=(1, 2), Y=(3, 4))
@test bounds(ex2) === (Y=(3, 4), X=(1, 2))
@test bounds(ex3) === (X=(1, 2), Y=(3, 4), Z=(5.0, 6.0))
end

@testset "extent" begin
@test extent(ex1) === ex1
end

@testset "equality" begin
@test ex1 == ex2
@test ex1 != ex3
end

@testset "properties" begin
@test keys(ex1) == (:X, :Y)
@test values(ex1) == ((1, 2), (3, 4))
end

@testset "union" begin
a = Extent(X=(0.1, 0.5), Y=(1.0, 2.0))
b = Extent(X=(2.1, 2.5), Y=(3.0, 4.0), Z=(0.0, 1.0))
c = Extent(Z=(0.2, 2.0))
@test Extents.union(a, b) == Extent(X=(0.1, 2.5), Y=(1.0, 4.0))
@test Extents.union(a, b; strict=true) === nothing
@test Extents.union(a, c) === nothing
end

@testset "intersect/intersects" begin
a = Extent(X=(0.1, 0.5), Y=(1.0, 2.0))
b = Extent(X=(2.1, 2.5), Y=(3.0, 4.0), Z=(0.0, 1.0))
c = Extent(X=(0.4, 2.5), Y=(1.5, 4.0), Z=(0.0, 1.0))
d = Extent(A=(0.0, 1.0))
@test Extents.intersects(a, b) == false
@test Extents.intersect(a, b) === nothing
@test Extents.intersects(a, c) == true
@test Extents.intersects(a, c; strict=true) == false
@test Extents.intersect(a, c) == Extent(X=(0.4, 0.5), Y=(1.5, 2.0))
@test Extents.intersect(a, d) === nothing
@test Extents.intersect(a, c; strict=true) === nothing
end

0 comments on commit 1235598

Please sign in to comment.