diff --git a/docs/src/internals.md b/docs/src/internals.md index c4c1173a..6430cf30 100644 --- a/docs/src/internals.md +++ b/docs/src/internals.md @@ -44,7 +44,7 @@ type: ```jldoctest oa julia> ax = axes(oa, 2) -OffsetArrays.IdOffsetRange(5:6) +(5 => 5):(6 => 6) ``` This has a similar design to `Base.IdentityUnitRange` that `ax[x] == x` always holds. @@ -61,10 +61,10 @@ This property makes sure that they tend to be their own axes: ```jldoctest oa julia> axes(ax) -(OffsetArrays.IdOffsetRange(5:6),) +((5 => 5):(6 => 6),) julia> axes(ax[ax]) -(OffsetArrays.IdOffsetRange(5:6),) +((5 => 5):(6 => 6),) ``` This example of indexing is [idempotent](https://en.wikipedia.org/wiki/Idempotence). @@ -80,7 +80,7 @@ julia> oa2 = OffsetArray([5, 10, 15, 20], 0:3) 20 julia> ax2 = axes(oa2, 1) -OffsetArrays.IdOffsetRange(0:3) +(0 => 0):(3 => 3) julia> oa2[2] 15 @@ -112,22 +112,23 @@ cases that you should be aware of, especially when you are working with multi-di One such cases is `getindex`: ```jldoctest getindex; setup = :(using OffsetArrays) -julia> Ao = zeros(-3:3, -3:3); Ao[:] .= 1:49; +julia> Ao = zeros(-3:3, -3:3); Ao[:] .= 1:49; axes(Ao) +((-3 => -3):(3 => 3), (-3 => -3):(3 => 3)) julia> Ao[-3:0, :] |> axes # the first dimension does not preserve offsets -(OffsetArrays.IdOffsetRange(1:4), OffsetArrays.IdOffsetRange(-3:3)) +((1 => 1):(4 => 4), (-3 => -3):(3 => 3)) -julia> Ao[-3:0, -3:3] |> axes # neither dimensions preserve offsets +julia> Ao[-3:0, -3:3] |> axes # neither dimension preserves offsets (Base.OneTo(4), Base.OneTo(7)) julia> Ao[axes(Ao)...] |> axes # offsets are preserved -(OffsetArrays.IdOffsetRange(-3:3), OffsetArrays.IdOffsetRange(-3:3)) +((-3 => -3):(3 => 3), (-3 => -3):(3 => 3)) julia> Ao[:] |> axes # This is linear indexing (Base.OneTo(49),) ``` -Note that if you pass a `UnitRange`, the offsets in corresponding dimension will not be preserved. +Note that if you pass a `UnitRange`, the offsets in the corresponding dimension will not be preserved. This might look weird at first, but since it follows the `a[ax][i] == a[ax[i]]` rule, it is not a bug. @@ -138,7 +139,7 @@ julia> Ao[I, 0][1] == Ao[I[1], 0] true julia> ax = axes(Ao, 1) # ax starts at index -3 -OffsetArrays.IdOffsetRange(-3:3) +(-3 => -3):(3 => 3) julia> Ao[ax, 0][1] == Ao[ax[1], 0] true @@ -164,15 +165,15 @@ julia> a = zeros(3, 3); julia> oa = OffsetArray(a, ZeroBasedIndexing()); julia> axes(oa) -(OffsetArrays.IdOffsetRange(0:2), OffsetArrays.IdOffsetRange(0:2)) +((0 => 0):(2 => 2), (0 => 0):(2 => 2)) ``` In this example we had to define the action of `to_indices` as the type `ZeroBasedIndexing` did not have a familiar hierarchy. Things are even simpler if we subtype `AbstractUnitRange`, in which case we need to define `first` and `length` for the custom range to be able to use it as an axis: ```jldoctest; setup = :(using OffsetArrays) julia> struct ZeroTo <: AbstractUnitRange{Int} - n :: Int - ZeroTo(n) = new(n < 0 ? -1 : n) + n :: Int + ZeroTo(n) = new(n < 0 ? -1 : n) end julia> Base.first(::ZeroTo) = 0 @@ -182,7 +183,7 @@ julia> Base.length(r::ZeroTo) = r.n + 1 julia> oa = OffsetArray(zeros(2,2), ZeroTo(1), ZeroTo(1)); julia> axes(oa) -(OffsetArrays.IdOffsetRange(0:1), OffsetArrays.IdOffsetRange(0:1)) +((0 => 0):(1 => 1), (0 => 0):(1 => 1)) ``` -Note that zero-based indexing may also be achieved using the pre-defined type [`OffsetArrays.Origin`](@ref). \ No newline at end of file +Note that zero-based indexing may also be achieved using the pre-defined type [`OffsetArrays.Origin`](@ref). diff --git a/src/axes.jl b/src/axes.jl index 29833c39..e86f5bf1 100644 --- a/src/axes.jl +++ b/src/axes.jl @@ -9,33 +9,55 @@ i.e., it's the "identity," which is the origin of the "Id" in `IdOffsetRange`. # Examples The most common case is shifting a range that starts at 1 (either `1:n` or `Base.OneTo(n)`): -```jldoctest; setup=:(import OffsetArrays) +```jldoctest idorange; setup=:(import OffsetArrays) julia> ro = OffsetArrays.IdOffsetRange(1:3, -2) -OffsetArrays.IdOffsetRange(-1:1) +(-1 => -1):(1 => 1) +``` -julia> axes(ro, 1) -OffsetArrays.IdOffsetRange(-1:1) +You can think of this display as indicating that an index of -1 maps to -1, and an index of 1 maps to 1. +```jldoctest idorange julia> ro[-1] -1 julia> ro[3] ERROR: BoundsError: attempt to access 3-element UnitRange{$Int} at index [5] + +julia> axes(ro, 1) # `axes` is Idempotent +(-1 => -1):(1 => 1) ``` If the range doesn't start at 1, the values may be different from the indices: ```jldoctest; setup=:(import OffsetArrays) julia> ro = OffsetArrays.IdOffsetRange(11:13, -2) -OffsetArrays.IdOffsetRange(9:11) - -julia> axes(ro, 1) # 11:13 is indexed by 1:3, and the offset is also applied to the axes -OffsetArrays.IdOffsetRange(-1:1) +(-1 => 9):(1 => 11) julia> ro[-1] 9 julia> ro[3] ERROR: BoundsError: attempt to access 3-element UnitRange{$Int} at index [5] + +julia> axes(ro, 1) # 11:13 is indexed by 1:3, and the offset is also applied to the axes +(-1 => -1):(1 => 1) +``` + +You can construct these ranges as they are displayed: + +```jldoctest; setup=(import OffsetArrays), filter=r", ?U" +julia> r = (0=>8):(3=>11) +(0 => 8):(3 => 11) + +julia> typeof(r) +OffsetArrays.IdOffsetRange{$Int, UnitRange{$Int}} + +julia> for p in pairs(r) + println(p) + end +0 => 8 +1 => 9 +2 => 10 +3 => 11 ``` # Extended help @@ -104,6 +126,14 @@ end IdOffsetRange(r::IdOffsetRange) = r IdOffsetRange(r::IdOffsetRange, offset::Integer) = typeof(r)(r.parent, offset + r.offset) +function Base.:(:)((istart,rstart)::Pair{Int,Int}, (istop,rstop)::Pair{Int,Int}) + throw_argerr(istart, istop, rstart, rstop) = throw(ArgumentError("indices and values must have the same length, got $istart:$istop (length $(istop-istart+1)) and $rstart:$rstop (length $(rstop-rstart+1)), respectively")) + + istop - istart == rstop - rstart || throw_argerr(istart, istop, rstart, rstop) + offset = istart - 1 + return IdOffsetRange(rstart-offset : rstop-offset, offset) +end + # TODO: uncomment these when Julia is ready # # Conversion preserves both the values and the indexes, throwing an InexactError if this # # is not possible. @@ -167,7 +197,10 @@ Broadcast.broadcasted(::Base.Broadcast.DefaultArrayStyle{1}, ::typeof(+), r::IdO Broadcast.broadcasted(::Base.Broadcast.DefaultArrayStyle{1}, ::typeof(+), x::Integer, r::IdOffsetRange{T}) where T = IdOffsetRange{T}(x .+ r.parent, r.offset) -Base.show(io::IO, r::IdOffsetRange) = print(io, "OffsetArrays.IdOffsetRange(",first(r), ':', last(r),")") +function Base.show(io::IO, r::IdOffsetRange) + axr = axes(r, 1) + print(io, "(",first(axr)=>first(r), "):(", last(axr)=>last(r),")") +end # Optimizations @inline Base.checkindex(::Type{Bool}, inds::IdOffsetRange, i::Real) = Base.checkindex(Bool, inds.parent, i - inds.offset) diff --git a/test/runtests.jl b/test/runtests.jl index 2095b9cb..7e476b1d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -101,6 +101,14 @@ end r = IdOffsetRange(IdOffsetRange(3:5, 2), 1) @test parent(r) isa UnitRange + # Pair construction + rp = (2=>3):(5=>6) + @test first(rp) === 3 + @test last(rp) === 6 + @test firstindex(rp) === 2 + @test lastindex(rp) === 5 + @test_throws ArgumentError("indices and values must have the same length, got 0:1 (length 2) and 5:7 (length 3), respectively") (0=>5):(1=>7) + # conversion preserves both the values and the axes, throwing an error if this is not possible @test @inferred(oftype(ro, ro)) === ro @test @inferred(convert(OffsetArrays.IdOffsetRange{Int}, ro)) === ro @@ -199,19 +207,19 @@ end @testset "OffsetVector" begin # initialization one_based_axes = [ - (Base.OneTo(4), ), - (1:4, ), - (CartesianIndex(1):CartesianIndex(4), ), - (IdentityUnitRange(1:4), ), - (IdOffsetRange(1:4),), + (Base.OneTo(4), ), + (1:4, ), + (CartesianIndex(1):CartesianIndex(4), ), + (IdentityUnitRange(1:4), ), + (IdOffsetRange(1:4),), (IdOffsetRange(3:6, -2),) ] offset_axes = [ - (-1:2, ), - (CartesianIndex(-1):CartesianIndex(2), ), - (IdentityUnitRange(-1:2), ), - (IdOffsetRange(-1:2),), + (-1:2, ), + (CartesianIndex(-1):CartesianIndex(2), ), + (IdentityUnitRange(-1:2), ), + (IdOffsetRange(-1:2),), (IdOffsetRange(3:6, -4),) ] @@ -324,7 +332,7 @@ end @testset "OffsetMatrix" begin # initialization - + one_based_axes = [ (Base.OneTo(4), Base.OneTo(3)), (1:4, 1:3), @@ -573,7 +581,7 @@ end end @testset "TupleOfRanges" begin Base.to_indices(A, inds, t::Tuple{TupleOfRanges{N}}) where {N} = t - OffsetArrays.AxisConversionStyle(::Type{TupleOfRanges{N}}) where {N} = + OffsetArrays.AxisConversionStyle(::Type{TupleOfRanges{N}}) where {N} = OffsetArrays.TupleOfRanges() Base.convert(::Type{Tuple{Vararg{AbstractUnitRange{Int}}}}, t::TupleOfRanges) = t.x @@ -584,7 +592,7 @@ end @test axes(oa) == inds.x end @testset "NewColon" begin - Base.to_indices(A, inds, t::Tuple{NewColon,Vararg{Any}}) = + Base.to_indices(A, inds, t::Tuple{NewColon,Vararg{Any}}) = (_uncolon(inds, t), to_indices(A, Base.tail(inds), Base.tail(t))...) _uncolon(inds::Tuple{}, I::Tuple{NewColon, Vararg{Any}}) = OneTo(1) @@ -916,9 +924,9 @@ end a = OffsetArray([1 2; 3 4], -1:0, 5:6) io = IOBuffer() show(io, axes(a, 1)) - @test String(take!(io)) == "OffsetArrays.IdOffsetRange(-1:0)" + @test String(take!(io)) == "(-1 => -1):(0 => 0)" show(io, axes(a, 2)) - @test String(take!(io)) == "OffsetArrays.IdOffsetRange(5:6)" + @test String(take!(io)) == "(5 => 5):(6 => 6)" @test Base.inds2string(axes(a)) == Base.inds2string(map(UnitRange, axes(a)))