From d59a1e2b818ea8804bbc7dd7ba0f0b3663c0036d Mon Sep 17 00:00:00 2001 From: jishnub Date: Mon, 21 Sep 2020 11:26:56 +0400 Subject: [PATCH 1/5] use to_indices to simplify constructor dispatch to_indices converts colons to ranges. This is followed by converting CartesianIndices to ranges. Fix indexing IdOffsetRange using offset ranges, ensuring that indexing is idempotent --- Project.toml | 3 +- docs/src/index.md | 2 +- docs/src/internals.md | 25 ++++ docs/src/reference.md | 1 + src/OffsetArrays.jl | 87 ++++++++------ src/axes.jl | 8 +- src/utils.jl | 68 +++++++++-- test/runtests.jl | 272 ++++++++++++++++++++++++++++++++++++------ 8 files changed, 386 insertions(+), 80 deletions(-) diff --git a/Project.toml b/Project.toml index f087d1f8..f36ccc86 100644 --- a/Project.toml +++ b/Project.toml @@ -10,8 +10,9 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" CatIndices = "aafaddc9-749c-510e-ac4f-586e18779b91" DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +EllipsisNotation = "da5c29d0-fa7d-589e-88eb-ea29b0a81949" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Aqua", "CatIndices", "DelimitedFiles", "Documenter", "Test", "LinearAlgebra"] +test = ["Aqua", "CatIndices", "DelimitedFiles", "Documenter", "Test", "LinearAlgebra", "EllipsisNotation"] \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md index 455bdbe8..79004fb4 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -56,7 +56,7 @@ Base.require_one_based_indexing(OA) ``` [`OffsetArrays.Origin`](@ref) can be convenient if you want to directly specify the origin of the output -OffsetArray, it will automatically compute the needed offsets. For example: +OffsetArray, it will automatically compute the corresponding offsets. For example: ```@repl index OffsetArray(A, OffsetArrays.Origin(-1, -1)) diff --git a/docs/src/internals.md b/docs/src/internals.md index 3d6c8117..a5f53dd9 100644 --- a/docs/src/internals.md +++ b/docs/src/internals.md @@ -143,3 +143,28 @@ OffsetArrays.IdOffsetRange(-3:3) julia> Ao[ax, 0][1] == Ao[ax[1], 0] true ``` + +## Using custom axis types + +While a wide variety of `AbstractUnitRange`s provided by `Base` may be used as indices to construct an `OffsetArray`, at times it might be convenient to define custom types. The `OffsetArray` constructor accepts any type that may be converted to an `AbstractUnitRange`. This proceeds through a two-step process. Let's assume that the constructor called is `OffsetArray(A, indstup)`, where `indstup` is a `Tuple` of indices. + +1. In the first step, the constructor calls `to_indices(A, axes(A), indstup)` to lower `indstup` to a `Tuple` of `AbstractUnitRange`s. This step converts --- among other things --- `Colon`s to axis ranges. Custom types may extend `Base.to_indices(A, axes(A), indstup)` with the desired conversion of `indstup` to `Tuple{Vararg{AbstractUnitRange{Int}}}` if this is feasible. + +2. In the second step, the result of the previous step is passed to `OffsetArrays._toAbstractUnitRanges`. This step is only necessary if the previous step didn't return a `Tuple` of `AbstractUnitRange`s. This step allows an additional customization option: a type may be converted either to a single `AbstractUnitRange{Int}`, or to a `Tuple` of them. A custom type might specify which of these two behaviours is desired by extending [`OffsetArrays.AxisConversionStyle`](@ref). An example of a type that is acted upon at this stage is `CartesianIndices`, which is converted to a `Tuple` of `AbstractUnitRange`s. + +For example, here is a custom type that leads to zero-based indexing: + +```jldoctest; setup = :(using OffsetArrays) +julia> struct ZeroBasedIndexing end + +julia> Base.to_indices(A, inds, ::Tuple{ZeroBasedIndexing}) = map(x -> 0:length(x)-1, inds) + +julia> a = zeros(3, 3); + +julia> oa = OffsetArray(a, ZeroBasedIndexing()); + +julia> axes(oa) +(OffsetArrays.IdOffsetRange(0:2), OffsetArrays.IdOffsetRange(0:2)) +``` + +Note that zero-based indexing may also be achieved using [`OffsetArrays.Origin`](@ref). \ No newline at end of file diff --git a/docs/src/reference.md b/docs/src/reference.md index 2d17f596..30d3b6a3 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -7,4 +7,5 @@ OffsetMatrix OffsetArrays.Origin OffsetArrays.IdOffsetRange OffsetArrays.no_offset_view +OffsetArrays.AxisConversionStyle ``` diff --git a/src/OffsetArrays.jl b/src/OffsetArrays.jl index d9fec2b9..b63f2297 100644 --- a/src/OffsetArrays.jl +++ b/src/OffsetArrays.jl @@ -1,6 +1,6 @@ module OffsetArrays -using Base: Indices, tail, @propagate_inbounds +using Base: tail, @propagate_inbounds @static if !isdefined(Base, :IdentityUnitRange) const IdentityUnitRange = Base.Slice else @@ -15,8 +15,8 @@ include("origin.jl") # Technically we know the length of CartesianIndices but we need to convert it first, so here we # don't put it in OffsetAxisKnownLength. -const OffsetAxisKnownLength = Union{Integer, AbstractUnitRange, IdOffsetRange} -const OffsetAxis = Union{OffsetAxisKnownLength, CartesianIndices, Colon} +const OffsetAxisKnownLength = Union{Integer, AbstractUnitRange} +const OffsetAxis = Union{OffsetAxisKnownLength, Colon} const ArrayInitializer = Union{UndefInitializer, Missing, Nothing} ## OffsetArray @@ -51,7 +51,7 @@ julia> OffsetArray(reshape(1:6, 2, 3), 0:1, -1:1) 1 3 5 2 4 6 -julia> OffsetArray(reshape(1:6, 2, 3), :, -1:1) # : as a placeholder means no offset is applied at this dimension +julia> OffsetArray(reshape(1:6, 2, 3), :, -1:1) # : as a placeholder to indicate that no offset is to be applied to this dimension 2×3 OffsetArray(reshape(::UnitRange{$Int}, 2, 3), 1:2, -1:1) with eltype $Int with indices 1:2×-1:1: 1 3 5 2 4 6 @@ -123,54 +123,71 @@ function overflow_check(r, offset::T) where T throw_lower_overflow_error() end end -## OffsetArray constructors +function OffsetArray(A::AbstractArray, offsets::Tuple{Vararg{Integer}}) + _checkindices(A, offsets, "offsets") + OffsetArray{eltype(A), ndims(A), typeof(A)}(A, offsets) +end +# Nested OffsetArrays may strip off the layer and collate the offsets +function OffsetArray(A::OffsetArray, offsets::Tuple{Vararg{Integer}}) + _checkindices(A, offsets, "offsets") + OffsetArray(parent(A), A.offsets .+ offsets) +end + +for (FT, ND) in ((:OffsetVector, :1), (:OffsetMatrix, :2)) + @eval function $FT(A::AbstractArray{<:Any,$ND}, offsets::Tuple{Vararg{Integer}}) + _checkindices(A, offsets, "offsets") + OffsetArray{eltype(A), $ND, typeof(A)}(A, offsets) + end + @eval function $FT(A::OffsetArray{<:Any,$ND}, offsets::Tuple{Vararg{Integer}}) + _checkindices(A, offsets, "offsets") + $FT(parent(A), A.offsets .+ offsets) + end + FTstr = string(FT) + @eval function $FT(A::AbstractArray, offsets::Tuple{Vararg{Integer}}) + throw(ArgumentError($FTstr*" requires a "*string($ND)*"D array")) + end +end + +## OffsetArray constructors for FT in (:OffsetArray, :OffsetVector, :OffsetMatrix) - # The only route out to inner constructor - @eval function $FT(A::AbstractArray{T, N}, offsets::NTuple{N, Integer}) where {T, N} - ndims(A) == N || throw(DimensionMismatch("The number of offsets $(N) should equal ndims(A) = $(ndims(A))")) - OffsetArray{T, ndims(A), typeof(A)}(A, offsets) + # In general, indices get converted to AbstractUnitRanges. + # CartesianIndices{N} get converted to N ranges + @eval function $FT(A::AbstractArray, inds::Tuple) + $FT(A, _toAbstractUnitRanges(to_indices(A, axes(A), inds))) end - # nested OffsetArrays - @eval $FT(A::OffsetArray{T, N}, offsets::NTuple{N, Integer}) where {T,N} = $FT(parent(A), A.offsets .+ offsets) + + @eval $FT(A::AbstractArray, inds::Vararg) = $FT(A, inds) + # convert ranges to offsets - @eval function $FT(A::AbstractArray{T}, inds::NTuple{N,OffsetAxisKnownLength}) where {T,N} - axparent = axes(A) - lA = map(length, axparent) + @eval function $FT(A::AbstractArray, inds::Tuple{AbstractUnitRange,Vararg{AbstractUnitRange}}) + _checkindices(A, inds, "indices") + throw_dimerr(lA, lI) = throw(DimensionMismatch("supplied axes do not agree with the size of the array (got size $lA for the array and $lI for the indices")) + lA = size(A) lI = map(length, inds) - lA == lI || throw(DimensionMismatch("supplied axes do not agree with the size of the array (got size $lA for the array and $lI for the indices")) - $FT(A, map(_offset, axparent, inds)) - end - # lower CartesianIndices and Colon - @eval function $FT(A::AbstractArray{T}, inds::NTuple{N, OffsetAxis}) where {T, N} - indsN = _uncolonindices(A, _expandCartesianIndices(inds)) - $FT(A, indsN) + lA == lI || throw_dimerr(lA, lI) + $FT(A, map(_offset, axes(A), inds)) end - @eval $FT(A::AbstractArray{T}, inds::Vararg{OffsetAxis,N}) where {T, N} = $FT(A, inds) - @eval $FT(A::AbstractArray, origin::Origin) = OffsetArray(A, origin(A)) + @eval $FT(A::AbstractArray, origin::Origin) = $FT(A, origin(A)) end # array initialization -function OffsetArray{T,N}(init::ArrayInitializer, inds::NTuple{N, OffsetAxisKnownLength}) where {T,N} +function OffsetArray{T,N}(init::ArrayInitializer, inds::Tuple{Vararg{OffsetAxisKnownLength}}) where {T,N} + _checkindices(N, inds, "indices") AA = Array{T,N}(init, map(_indexlength, inds)) OffsetArray{T, N, typeof(AA)}(AA, map(_indexoffset, inds)) end -function OffsetArray{T, N}(init::ArrayInitializer, inds::NTuple{NT, Union{OffsetAxisKnownLength, CartesianIndices}}) where {T, N, NT} - # NT is probably not the actual dimension of the array; CartesianIndices might contain multiple dimensions - indsN = _expandCartesianIndices(inds) - length(indsN) == N || throw(DimensionMismatch("The number of offsets $(length(indsN)) should equal ndims(A) = $N")) - OffsetArray{T, N}(init, indsN) +function OffsetArray{T, N}(init::ArrayInitializer, inds::Tuple) where {T, N} + OffsetArray{T, N}(init, _toAbstractUnitRanges(inds)) end -OffsetArray{T,N}(init::ArrayInitializer, inds::Union{OffsetAxisKnownLength, CartesianIndices}...) where {T,N} = OffsetArray{T,N}(init, inds) +OffsetArray{T,N}(init::ArrayInitializer, inds::Vararg) where {T,N} = OffsetArray{T,N}(init, inds) OffsetArray{T}(init::ArrayInitializer, inds::NTuple{N, OffsetAxisKnownLength}) where {T,N} = OffsetArray{T,N}(init, inds) -function OffsetArray{T}(init::ArrayInitializer, inds::NTuple{N, Union{OffsetAxisKnownLength, CartesianIndices}}) where {T, N} - # N is probably not the actual dimension of the array; CartesianIndices might contain multiple dimensions - indsN = _expandCartesianIndices(inds) - OffsetArray{T, length(indsN)}(init, indsN) +function OffsetArray{T}(init::ArrayInitializer, inds::Tuple) where {T} + OffsetArray{T}(init, _toAbstractUnitRanges(inds)) end -OffsetArray{T}(init::ArrayInitializer, inds::Union{OffsetAxisKnownLength, CartesianIndices}...) where {T} = OffsetArray{T}(init, inds) +OffsetArray{T}(init::ArrayInitializer, inds::Vararg) where {T} = OffsetArray{T}(init, inds) Base.IndexStyle(::Type{OA}) where {OA<:OffsetArray} = IndexStyle(parenttype(OA)) parenttype(::Type{OffsetArray{T,N,AA}}) where {T,N,AA} = AA diff --git a/src/axes.jl b/src/axes.jl index 0de4739b..1300d821 100644 --- a/src/axes.jl +++ b/src/axes.jl @@ -132,6 +132,9 @@ offset_coerce(::Type{I}, r::AbstractUnitRange) where I<:AbstractUnitRange{T} whe Base.reduced_index(i::IdOffsetRange) = typeof(i)(first(i):first(i)) # Workaround for #92 on Julia < 1.4 Base.reduced_index(i::IdentityUnitRange{<:IdOffsetRange}) = typeof(i)(first(i):first(i)) +for f in [:firstindex, :lastindex] + @eval Base.$f(r::IdOffsetRange) = $f(r.parent) .+ r.offset +end @inline function Base.iterate(r::IdOffsetRange) ret = iterate(r.parent) @@ -151,9 +154,12 @@ end @propagate_inbounds function Base.getindex(r::IdOffsetRange, s::AbstractUnitRange{<:Integer}) return r.parent[s .- r.offset] .+ r.offset end -@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::IdOffsetRange) +@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::IdentityUnitRange) return IdOffsetRange(r.parent[s .- r.offset], r.offset) end +@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::IdOffsetRange) + return IdOffsetRange(r.parent[s.parent .+ (s.offset - r.offset)] .+ (r.offset - s.offset), s.offset) +end # offset-preserve broadcasting Broadcast.broadcasted(::Base.Broadcast.DefaultArrayStyle{1}, ::typeof(-), r::IdOffsetRange{T}, x::Integer) where T = diff --git a/src/utils.jl b/src/utils.jl index 85efe9ad..8f8b3d90 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -8,14 +8,66 @@ _indexlength(i::Integer) = i _indexlength(i::Colon) = Colon() _offset(axparent::AbstractUnitRange, ax::AbstractUnitRange) = first(ax) - first(axparent) -_offset(axparent::AbstractUnitRange, ax::CartesianIndices) = _offset(axparent, first(ax.indices)) _offset(axparent::AbstractUnitRange, ax::Integer) = 1 - first(axparent) -_uncolonindices(A::AbstractArray{<:Any,N}, inds::NTuple{N,Any}) where {N} = _uncolonindices(axes(A), inds) -_uncolonindices(ax::Tuple, inds::Tuple) = (first(inds), _uncolonindices(tail(ax), tail(inds))...) -_uncolonindices(ax::Tuple, inds::Tuple{Colon, Vararg{Any}}) = (first(ax), _uncolonindices(tail(ax), tail(inds))...) -_uncolonindices(::Tuple{}, ::Tuple{}) = () +""" + OffsetArrays.AxisConversionStyle(typeof(indices)) -_expandCartesianIndices(inds::Tuple{<:CartesianIndices, Vararg{Any}}) = (convert(Tuple{Vararg{AbstractUnitRange{Int}}}, inds[1])..., _expandCartesianIndices(Base.tail(inds))...) -_expandCartesianIndices(inds::Tuple{Any,Vararg{Any}}) = (inds[1], _expandCartesianIndices(Base.tail(inds))...) -_expandCartesianIndices(::Tuple{}) = () +`AxisConversionStyle` declares if `indices` should be converted to a single `AbstractUnitRange{Int}` +or to a `Tuple{Vararg{AbstractUnitRange{Int}}}` while flattening custom types into indices. +This method is called after `to_indices(A::Array, axes(A), indices)` to provide +further information in case `to_indices` does not return a `Tuple` of `AbstractUnitRange{Int}`. + +Custom index types should extend `AxisConversionStyle` and return either `OffsetArray.SingleRange()`, +which is the default, or `OffsetArray.TupleOfRanges()`. In the former case, the type `T` should +define `Base.convert(::Type{AbstractUnitRange{Int}}, ::T)`, whereas in the latter it should define +`Base.convert(::Type{Tuple{Vararg{AbstractUnitRange{Int}}}}, ::T)`. + +An example of the latter is `CartesianIndices`, which is converted to a `Tuple` of +`AbstractUnitRange{Int}` while flattening the indices. + +# Example +```jldoctest; setup=:(using OffsetArrays) +julia> struct NTupleOfUnitRanges{N} + x ::NTuple{N, UnitRange{Int}} + end + +julia> Base.to_indices(A, inds, t::Tuple{NTupleOfUnitRanges{N}}) where {N} = t; + +julia> OffsetArrays.AxisConversionStyle(::Type{NTupleOfUnitRanges{N}}) where {N} = OffsetArrays.TupleOfRanges(); + +julia> Base.convert(::Type{Tuple{Vararg{AbstractUnitRange{Int}}}}, t::NTupleOfUnitRanges) = t.x; + +julia> a = zeros(3, 3); + +julia> inds = NTupleOfUnitRanges((3:5, 2:4)); + +julia> oa = OffsetArray(a, inds); + +julia> axes(oa, 1) == 3:5 +true + +julia> axes(oa, 2) == 2:4 +true +``` +""" +abstract type AxisConversionStyle end +struct SingleRange <: AxisConversionStyle end +struct TupleOfRanges <: AxisConversionStyle end + +AxisConversionStyle(::Type) = SingleRange() +AxisConversionStyle(::Type{<:CartesianIndices}) = TupleOfRanges() + +_convertTupleAbstractUnitRange(x) = _convertTupleAbstractUnitRange(AxisConversionStyle(typeof(x)), x) +_convertTupleAbstractUnitRange(::SingleRange, x) = (convert(AbstractUnitRange{Int}, x),) +_convertTupleAbstractUnitRange(::TupleOfRanges, x) = convert(Tuple{Vararg{AbstractUnitRange{Int}}}, x) + +_toAbstractUnitRanges(t::Tuple) = (_convertTupleAbstractUnitRange(first(t))..., _toAbstractUnitRanges(tail(t))...) +_toAbstractUnitRanges(::Tuple{}) = () + +# ensure that the indices are consistent in the constructor +_checkindices(A::AbstractArray, indices, label) = _checkindices(ndims(A), indices, label) +function _checkindices(N::Integer, indices, label) + throw_argumenterror(N, indices, label) = throw(ArgumentError(label*" $indices are not compatible with a $(N)D array")) + N == length(indices) || throw_argumenterror(N, indices, label) +end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 7a1888a5..df010122 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,6 +5,7 @@ using Test, Aqua, Documenter using LinearAlgebra using DelimitedFiles using CatIndices: BidirectionalVector +using EllipsisNotation # https://github.com/JuliaLang/julia/pull/29440 if VERSION < v"1.1.0-DEV.389" @@ -12,6 +13,13 @@ if VERSION < v"1.1.0-DEV.389" CartesianIndices(map((i,j) -> i:j, Tuple(I), Tuple(J))) end +# Custom index types +struct ZeroBasedIndexing end +struct NewColon end +struct TupleOfRanges{N} + x ::NTuple{N, UnitRange{Int}} +end + @testset "Project meta quality checks" begin # Not checking compat section for test-only dependencies Aqua.test_all(OffsetArrays; project_extras=true, deps_compat=true, stale_deps=true, project_toml_formatting=true) @@ -44,6 +52,10 @@ end check_indexed_by(ro, 1:3) @test same_value(rs, 1:3) check_indexed_by(rs, -1:1) + @test firstindex(ro) == 1 + @test lastindex(ro) == 3 + @test firstindex(rs) == -1 + @test lastindex(rs) == 1 @test @inferred(typeof(ro)(ro)) === ro @test @inferred(OffsetArrays.IdOffsetRange{Int}(ro)) === ro @test @inferred(OffsetArrays.IdOffsetRange{Int16}(ro)) === OffsetArrays.IdOffsetRange(Base.OneTo(Int16(3))) @@ -70,6 +82,10 @@ end @test same_value(r, 1:2) check_indexed_by(r, 1:2) + r = OffsetArrays.IdOffsetRange{Int32, Base.OneTo{Int32}}(Base.OneTo(Int64(2)), 3) + @test same_value(r, 4:5) + check_indexed_by(r, 4:5) + # 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 @@ -92,6 +108,37 @@ end r3 = (1 .+ OffsetArrays.IdOffsetRange(3:5, -1) .+ 1) .- 1 @test same_value(r3, 3:5) check_indexed_by(r3, 0:2) + + @testset "Idempotent indexing" begin + r = OffsetArrays.IdOffsetRange(3:5, -1) + + # Indexing with IdentityUnitRange + s = IdentityUnitRange(0:2) + @test axes(r[s]) == axes(s) + for i in eachindex(s) + @test r[s[i]] == r[s][i] + end + + # Indexing with IdOffsetRange + s = OffsetArrays.IdOffsetRange(-4:-2, 4) + @test axes(r[s]) == axes(s) + for i in eachindex(s) + @test r[s[i]] == r[s][i] + end + + # Indexing with UnitRange + s = 0:2 + @test axes(r[s]) == axes(s) + for i in eachindex(s) + @test r[s[i]] == r[s][i] + end + end + + # Test reduced index + rred = Base.reduced_index(r) + @test typeof(rred) == typeof(r) + @test length(rred) == 1 + @test first(rred) == first(r) end @testset "Constructors" begin @@ -113,13 +160,30 @@ end @test ndims(a) == 0 @test a[] == 3 @test a === OffsetArray(a, ()) - @test_throws DimensionMismatch OffsetArray(a, 0) - @test_throws DimensionMismatch OffsetArray(a0, 0) + @test_throws ArgumentError OffsetArray(a, 0) + @test_throws ArgumentError OffsetArray(a0, 0) end @testset "OffsetVector" begin # initialization - for inds in [(4, ), (Base.OneTo(4), ), (1:4, ), (CartesianIndex(1):CartesianIndex(4), ), (IdentityUnitRange(1:4), )] + one_based_axes = [ + (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),), + (IdOffsetRange(3:6, -4),) + ] + + for inds in [size.(one_based_axes[1], 1), one_based_axes...] # test indices API a = OffsetVector{Float64}(undef, inds) @test eltype(a) === Float64 @@ -139,7 +203,8 @@ end @test axes(a) === (IdOffsetRange(Base.OneTo(4), 0), ) end - for inds in [(-1:2, ), (CartesianIndex(-1):CartesianIndex(2), ), (IdentityUnitRange(-1:2), )] + # offset indexing + for inds in offset_axes # test offsets a = OffsetVector{Float64}(undef, inds) ax = (IdOffsetRange(Base.OneTo(4), -2), ) @@ -165,7 +230,7 @@ end # convenient constructors a = rand(4) - for inds in [(-2, ), (-1:2, ), (CartesianIndex(-1):CartesianIndex(2), ), (IdentityUnitRange(-1:2), )] + for inds in offset_axes oa1 = OffsetVector(a, inds...) oa2 = OffsetVector(a, inds) oa3 = OffsetArray(a, inds...) @@ -185,7 +250,7 @@ end # nested offset array a = rand(4) oa = OffsetArray(a, -1) - for inds in [(1, ), (1:4, ), (CartesianIndex(1):CartesianIndex(4), ), (IdentityUnitRange(1:4), )] + for inds in [.-oa.offsets, one_based_axes...] ooa = OffsetArray(oa, inds) @test typeof(parent(ooa)) <: Vector @test ooa === OffsetArray(oa, inds...) === OffsetVector(oa, inds) === OffsetVector(oa, inds...) @@ -202,19 +267,50 @@ end @test_nowarn OffsetArray{Float64, 1, typeof(ao)}(ao, (-1, )) @test_throws ArgumentError OffsetArray{Float64, 1, typeof(ao)}(ao, (-2, )) # inner Constructor @test_throws ArgumentError OffsetArray(ao, (-2, )) # convinient constructor accumulate offsets + + # disallow OffsetVector(::Array{<:Any, N}, offsets) where N != 1 + @test_throws ArgumentError OffsetVector(zeros(2,2), (2, 2)) + @test_throws ArgumentError OffsetVector(zeros(2,2), 2, 2) + @test_throws ArgumentError OffsetVector(zeros(2,2), (1:2, 1:2)) + @test_throws ArgumentError OffsetVector(zeros(2,2), 1:2, 1:2) + @test_throws ArgumentError OffsetVector(zeros(), ()) + @test_throws ArgumentError OffsetVector(zeros()) + @test_throws ArgumentError OffsetVector(zeros(2,2), ()) + @test_throws ArgumentError OffsetVector(zeros(2,2)) + @test_throws ArgumentError OffsetVector(zeros(2,2), 2) + @test_throws ArgumentError OffsetVector(zeros(2,2), (2,)) end @testset "OffsetMatrix" begin # initialization - for inds in [ - (4, 3), + + one_based_axes = [ (Base.OneTo(4), Base.OneTo(3)), (1:4, 1:3), (CartesianIndex(1, 1):CartesianIndex(4, 3), ), (CartesianIndex(1):CartesianIndex(4), CartesianIndex(1):CartesianIndex(3)), (CartesianIndex(1):CartesianIndex(4), 1:3), - (IdentityUnitRange(1:4), IdentityUnitRange(1:3)) + (IdentityUnitRange(1:4), IdentityUnitRange(1:3)), + (IdOffsetRange(1:4), IdOffsetRange(1:3)), + (IdOffsetRange(3:6, -2), IdOffsetRange(3:5, -2)), + (IdOffsetRange(3:6, -2), IdentityUnitRange(1:3)), + (IdOffsetRange(3:6, -2), 1:3), ] + + offset_axes = [ + (-1:2, 0:2), + (CartesianIndex(-1, 0):CartesianIndex(2, 2), ), + (-1:2, CartesianIndex(0):CartesianIndex(2)), + (CartesianIndex(-1):CartesianIndex(2), CartesianIndex(0):CartesianIndex(2)), + (CartesianIndex(-1):CartesianIndex(2), 0:2), + (IdentityUnitRange(-1:2), 0:2), + (IdOffsetRange(-1:2), IdOffsetRange(0:2)), + (IdOffsetRange(3:6, -4), IdOffsetRange(2:4, -2)), + (IdOffsetRange(3:6, -4), IdentityUnitRange(0:2)), + (IdOffsetRange(-1:2), 0:2), + ] + + for inds in [size.(one_based_axes[1], 1), one_based_axes...] # test API a = OffsetMatrix{Float64}(undef, inds) ax = (IdOffsetRange(Base.OneTo(4), 0), IdOffsetRange(Base.OneTo(3), 0)) @@ -236,14 +332,7 @@ end end @test_throws Union{ArgumentError, ErrorException} OffsetMatrix{Float64}(undef, 2, -2) # only positive numbers works - for inds in [ - (-1:2, 0:2), - (CartesianIndex(-1, 0):CartesianIndex(2, 2), ), - (-1:2, CartesianIndex(0):CartesianIndex(2)), - (CartesianIndex(-1):CartesianIndex(2), CartesianIndex(0):CartesianIndex(2)), - (CartesianIndex(-1):CartesianIndex(2), 0:2), - (IdentityUnitRange(-1:2), 0:2) - ] + for inds in offset_axes # test offsets a = OffsetMatrix{Float64}(undef, inds) ax = (IdOffsetRange(Base.OneTo(4), -2), IdOffsetRange(Base.OneTo(3), -1)) @@ -268,13 +357,7 @@ end # convenient constructors a = rand(4, 3) - for inds in [ - (-1:2, 0:2), - (CartesianIndex(-1, 0):CartesianIndex(2, 2), ), - (-1:2, CartesianIndex(0):CartesianIndex(2)), - (CartesianIndex(-1):CartesianIndex(2), CartesianIndex(0):CartesianIndex(2)), - (IdentityUnitRange(-1:2), 0:2) - ] + for inds in offset_axes ax = (IdOffsetRange(Base.OneTo(4), -2), IdOffsetRange(Base.OneTo(3), -1)) oa1 = OffsetMatrix(a, inds...) oa2 = OffsetMatrix(a, inds) @@ -297,14 +380,7 @@ end # nested offset array a = rand(4, 3) oa = OffsetArray(a, -1, -2) - for inds in [ - (1, 2), - (1:4, 1:3), - (CartesianIndex(1, 1):CartesianIndex(4, 3), ), - (1:4, CartesianIndex(1):CartesianIndex(3)), - (CartesianIndex(1):CartesianIndex(4), CartesianIndex(1):CartesianIndex(3)), - (IdentityUnitRange(1:4), 1:3) - ] + for inds in [.-oa.offsets, one_based_axes...] ooa = OffsetArray(oa, inds) @test ooa === OffsetArray(oa, inds...) === OffsetMatrix(oa, inds) === OffsetMatrix(oa, inds...) @test typeof(parent(ooa)) <: Matrix @@ -318,6 +394,18 @@ end @test axes(OffsetMatrix(a, typemax(Int)-size(a, 1), 0)) == (IdOffsetRange(axes(a)[1], typemax(Int)-size(a, 1)), axes(a, 2)) @test_throws ArgumentError OffsetMatrix(a, typemax(Int)-size(a,1)+1, 0) @test_throws ArgumentError OffsetMatrix(a, 0, typemax(Int)-size(a, 2)+1) + + # disallow OffsetMatrix(::Array{<:Any, N}, offsets) where N != 2 + @test_throws ArgumentError OffsetMatrix(zeros(2), (2,)) + @test_throws ArgumentError OffsetMatrix(zeros(2), 2) + @test_throws ArgumentError OffsetMatrix(zeros(2), (1:2,)) + @test_throws ArgumentError OffsetMatrix(zeros(2), 1:2) + @test_throws ArgumentError OffsetMatrix(zeros(), ()) + @test_throws ArgumentError OffsetMatrix(zeros()) + @test_throws ArgumentError OffsetMatrix(zeros(2), ()) + @test_throws ArgumentError OffsetMatrix(zeros(2)) + @test_throws ArgumentError OffsetMatrix(zeros(2), (1, 2)) + @test_throws ArgumentError OffsetMatrix(zeros(2), 1, 2) end # no need to duplicate the 2D case here, @@ -347,8 +435,108 @@ end @test axes(y) == (-1:1, -7:7, -1:2, -5:5, -1:1, -3:3, -2:2, -1:1) @test eltype(y) === Float64 - @test_throws DimensionMismatch OffsetArray{Float64, 2}(undef, indices) - @test_throws DimensionMismatch OffsetArray(y, indices[1:2]) + @test_throws ArgumentError OffsetArray{Float64, 2}(undef, indices) + @test_throws ArgumentError OffsetArray(y, indices[1:2]) + + @test ndims(OffsetArray(zeros(), ())) == 0 + @test Base.axes1(OffsetArray(zeros(), ())) === OffsetArrays.IdOffsetRange(Base.OneTo(1)) + + @testset "convenience constructors" begin + ax = (2:3, 4:5) + + for f in [zeros, ones] + a = f(Float64, ax) + @test axes(a) == ax + @test eltype(a) == Float64 + end + + for f in [trues, falses] + a = f(ax) + @test axes(a) == ax + @test eltype(a) == Bool + end + end + end + + @testset "custom range types" begin + @testset "EllipsisNotation" begin + @testset "Vector" begin + v = rand(5) + @test axes(OffsetArray(v, ..)) == axes(v) + @test OffsetArray(v, ..) == OffsetArray(v, :) + @test axes(OffsetVector(v, ..)) == axes(v) + @test OffsetVector(v, ..) == OffsetVector(v, :) + + @test axes(OffsetArray(v, .., 2:6)) == (2:6, ) + @test OffsetArray(v, .., 2:6) == OffsetArray(v, 2:6) + @test axes(OffsetVector(v, .., 2:6)) == (2:6, ) + @test OffsetVector(v, .., 2:6) == OffsetVector(v, 2:6) + end + @testset "Matrix" begin + m = rand(2, 2) + @test axes(OffsetArray(m, ..)) == axes(m) + @test OffsetArray(m, ..) == OffsetArray(m, :, :) + @test axes(OffsetMatrix(m, ..)) == axes(m) + @test OffsetMatrix(m, ..) == OffsetMatrix(m, :, :) + + @test axes(OffsetArray(m, .., 2:3)) == (axes(m, 1), 2:3) + @test OffsetArray(m, .., 2:3) == OffsetArray(m, :, 2:3) + @test axes(OffsetMatrix(m, .., 2:3)) == (axes(m, 1), 2:3) + @test OffsetMatrix(m, .., 2:3) == OffsetMatrix(m, :, 2:3) + + @test axes(OffsetArray(m, .., 2:3, 3:4)) == (2:3, 3:4) + @test OffsetArray(m, .., 2:3, 3:4) == OffsetArray(m, 2:3, 3:4) + @test axes(OffsetMatrix(m, .., 2:3, 3:4)) == (2:3, 3:4) + @test OffsetMatrix(m, .., 2:3, 3:4) == OffsetMatrix(m, 2:3, 3:4) + end + @testset "3D Array" begin + a = rand(2, 2, 2) + @test axes(OffsetArray(a, ..)) == axes(a) + @test OffsetArray(a, ..) == OffsetArray(a, :, :, :) + + @test axes(OffsetArray(a, .., 2:3)) == (axes(a)[1:2]..., 2:3) + @test OffsetArray(a, .., 2:3) == OffsetArray(a, :, :, 2:3) + + @test axes(OffsetArray(a, .., 2:3, 3:4)) == (axes(a, 1), 2:3, 3:4) + @test OffsetArray(a, .., 2:3, 3:4) == OffsetArray(a, :, 2:3, 3:4) + + @test axes(OffsetArray(a, 2:3, .., 3:4)) == (2:3, axes(a, 2), 3:4) + @test OffsetArray(a, 2:3, .., 3:4) == OffsetArray(a, 2:3, :, 3:4) + + @test axes(OffsetArray(a, .., 4:5, 2:3, 3:4)) == (4:5, 2:3, 3:4) + @test OffsetArray(a, .., 4:5, 2:3, 3:4) == OffsetArray(a, 4:5, 2:3, 3:4) + end + end + @testset "ZeroBasedIndexing" begin + Base.to_indices(A, inds, ::Tuple{ZeroBasedIndexing}) = map(x -> 0:length(x) - 1, inds) + + a = zeros(3,3) + oa = OffsetArray(a, ZeroBasedIndexing()) + @test axes(oa) == (0:2, 0:2) + end + @testset "TupleOfRanges" begin + Base.to_indices(A, inds, t::Tuple{TupleOfRanges{N}}) where {N} = t + OffsetArrays.AxisConversionStyle(::Type{TupleOfRanges{N}}) where {N} = + OffsetArrays.TupleOfRanges() + + Base.convert(::Type{Tuple{Vararg{AbstractUnitRange{Int}}}}, t::TupleOfRanges) = t.x + + a = zeros(3,3) + inds = TupleOfRanges((3:5, 2:4)) + oa = OffsetArray(a, inds) + @test axes(oa) == inds.x + end + @testset "NewColon" begin + 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) + _uncolon(inds::Tuple, I::Tuple{NewColon, Vararg{Any}}) = inds[1] + + a = zeros(3, 3) + oa = OffsetArray(a, (NewColon(), 2:4)) + @test axes(oa) == (axes(a,1), 2:4) + end end @testset "Offset range construction" begin @@ -385,6 +573,8 @@ end @test A == OffsetArray(A0, 0:1, 3:4) @test_throws DimensionMismatch OffsetArray(A0, 0:2, 3:4) @test_throws DimensionMismatch OffsetArray(A0, 0:1, 2:4) + @test eachindex(IndexLinear(), A) == eachindex(IndexLinear(), parent(A)) + @test eachindex(IndexCartesian(), A) == CartesianIndices(A) end @testset "Scalar indexing" begin @@ -678,6 +868,9 @@ end show(io, OffsetArray(3:5, 0:2)) @test String(take!(io)) == "3:5 with indices 0:2" + show(io, MIME"text/plain"(), OffsetArray(3:5, 0:2)) + @test String(take!(io)) == "3:5 with indices 0:2" + d = Diagonal([1,2,3]) Base.print_array(io, d) s1 = String(take!(io)) @@ -770,6 +963,17 @@ end @test reshape(OffsetArray(-1:0, -1:0), :) == OffsetArray(-1:0, -1:0) @test reshape(A, :) == reshape(A0, :) + # pop the parent + B = reshape(A, size(A)) + @test B == A0 + @test parent(B) === A0 + B = reshape(A, (Base.OneTo(2), 2)) + @test B == A0 + @test parent(B) === A0 + B = reshape(A, (2,:)) + @test B == A0 + @test parent(B) === A0 + # julialang/julia #33614 A = OffsetArray(-1:0, (-2,)) @test reshape(A, :) === A From 29efcd06d8aa4b9ad397e5530bc3f4c49db9944f Mon Sep 17 00:00:00 2001 From: jishnub Date: Sat, 26 Sep 2020 18:45:54 +0400 Subject: [PATCH 2/5] comment on error message optimization --- src/OffsetArrays.jl | 1 + src/utils.jl | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/OffsetArrays.jl b/src/OffsetArrays.jl index b63f2297..54f4b800 100644 --- a/src/OffsetArrays.jl +++ b/src/OffsetArrays.jl @@ -162,6 +162,7 @@ for FT in (:OffsetArray, :OffsetVector, :OffsetMatrix) # convert ranges to offsets @eval function $FT(A::AbstractArray, inds::Tuple{AbstractUnitRange,Vararg{AbstractUnitRange}}) _checkindices(A, inds, "indices") + # Performance gain by wrapping the error in a function: see https://github.com/JuliaLang/julia/issues/37558 throw_dimerr(lA, lI) = throw(DimensionMismatch("supplied axes do not agree with the size of the array (got size $lA for the array and $lI for the indices")) lA = size(A) lI = map(length, inds) diff --git a/src/utils.jl b/src/utils.jl index 8f8b3d90..d12d8160 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -11,7 +11,7 @@ _offset(axparent::AbstractUnitRange, ax::AbstractUnitRange) = first(ax) - first( _offset(axparent::AbstractUnitRange, ax::Integer) = 1 - first(axparent) """ - OffsetArrays.AxisConversionStyle(typeof(indices)) + OffsetArrays.AxisConversionStyle(typeof(indices)) `AxisConversionStyle` declares if `indices` should be converted to a single `AbstractUnitRange{Int}` or to a `Tuple{Vararg{AbstractUnitRange{Int}}}` while flattening custom types into indices. @@ -68,6 +68,6 @@ _toAbstractUnitRanges(::Tuple{}) = () # ensure that the indices are consistent in the constructor _checkindices(A::AbstractArray, indices, label) = _checkindices(ndims(A), indices, label) function _checkindices(N::Integer, indices, label) - throw_argumenterror(N, indices, label) = throw(ArgumentError(label*" $indices are not compatible with a $(N)D array")) + throw_argumenterror(N, indices, label) = throw(ArgumentError(label*" $indices are not compatible with a $(N)D array")) N == length(indices) || throw_argumenterror(N, indices, label) end \ No newline at end of file From 53ba85743c4e2db1fc1c39b904ef3e3c113e92b5 Mon Sep 17 00:00:00 2001 From: jishnub Date: Sun, 27 Sep 2020 09:23:27 +0400 Subject: [PATCH 3/5] simplify constructors --- src/OffsetArrays.jl | 25 +++++++++++++------------ test/runtests.jl | 10 ++++++++++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/OffsetArrays.jl b/src/OffsetArrays.jl index 54f4b800..7536c711 100644 --- a/src/OffsetArrays.jl +++ b/src/OffsetArrays.jl @@ -124,25 +124,20 @@ function overflow_check(r, offset::T) where T end end +# Tuples of integers are treated as offsets +# Empty Tuples are handled here function OffsetArray(A::AbstractArray, offsets::Tuple{Vararg{Integer}}) _checkindices(A, offsets, "offsets") OffsetArray{eltype(A), ndims(A), typeof(A)}(A, offsets) end -# Nested OffsetArrays may strip off the layer and collate the offsets -function OffsetArray(A::OffsetArray, offsets::Tuple{Vararg{Integer}}) - _checkindices(A, offsets, "offsets") - OffsetArray(parent(A), A.offsets .+ offsets) -end +# These methods are necessary to disallow incompatible dimensions for +# the OffsetVector and the OffsetMatrix constructors for (FT, ND) in ((:OffsetVector, :1), (:OffsetMatrix, :2)) @eval function $FT(A::AbstractArray{<:Any,$ND}, offsets::Tuple{Vararg{Integer}}) _checkindices(A, offsets, "offsets") OffsetArray{eltype(A), $ND, typeof(A)}(A, offsets) end - @eval function $FT(A::OffsetArray{<:Any,$ND}, offsets::Tuple{Vararg{Integer}}) - _checkindices(A, offsets, "offsets") - $FT(parent(A), A.offsets .+ offsets) - end FTstr = string(FT) @eval function $FT(A::AbstractArray, offsets::Tuple{Vararg{Integer}}) throw(ArgumentError($FTstr*" requires a "*string($ND)*"D array")) @@ -151,14 +146,18 @@ end ## OffsetArray constructors for FT in (:OffsetArray, :OffsetVector, :OffsetMatrix) + # Nested OffsetArrays may strip off the wrapper and collate the offsets + @eval function $FT(A::OffsetArray, offsets::Tuple{Vararg{Integer}}) + _checkindices(A, offsets, "offsets") + $FT(parent(A), map(+, A.offsets, offsets)) + end + # In general, indices get converted to AbstractUnitRanges. # CartesianIndices{N} get converted to N ranges - @eval function $FT(A::AbstractArray, inds::Tuple) + @eval function $FT(A::AbstractArray, inds::Tuple{Any,Vararg{Any}}) $FT(A, _toAbstractUnitRanges(to_indices(A, axes(A), inds))) end - @eval $FT(A::AbstractArray, inds::Vararg) = $FT(A, inds) - # convert ranges to offsets @eval function $FT(A::AbstractArray, inds::Tuple{AbstractUnitRange,Vararg{AbstractUnitRange}}) _checkindices(A, inds, "indices") @@ -170,6 +169,8 @@ for FT in (:OffsetArray, :OffsetVector, :OffsetMatrix) $FT(A, map(_offset, axes(A), inds)) end + @eval $FT(A::AbstractArray, inds::Vararg) = $FT(A, inds) + @eval $FT(A::AbstractArray, origin::Origin) = $FT(A, origin(A)) end diff --git a/test/runtests.jl b/test/runtests.jl index df010122..ded3f220 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -279,6 +279,10 @@ end @test_throws ArgumentError OffsetVector(zeros(2,2)) @test_throws ArgumentError OffsetVector(zeros(2,2), 2) @test_throws ArgumentError OffsetVector(zeros(2,2), (2,)) + @test_throws ArgumentError OffsetVector(zeros(2:3,2:3), 2, 3) + @test_throws ArgumentError OffsetVector(zeros(2:3,2:3), (2, 4)) + @test_throws ArgumentError OffsetVector(zeros(2:3,2:3), ()) + @test_throws ArgumentError OffsetVector(zeros(2:3,2:3)) end @testset "OffsetMatrix" begin @@ -406,6 +410,12 @@ end @test_throws ArgumentError OffsetMatrix(zeros(2)) @test_throws ArgumentError OffsetMatrix(zeros(2), (1, 2)) @test_throws ArgumentError OffsetMatrix(zeros(2), 1, 2) + @test_throws ArgumentError OffsetMatrix(zeros(2:3), (2,)) + @test_throws ArgumentError OffsetMatrix(zeros(2:3), 2) + @test_throws ArgumentError OffsetMatrix(zeros(2:3, 1:2, 1:2), (2,0,0)) + @test_throws ArgumentError OffsetMatrix(zeros(2:3, 1:2, 1:2), 2,0,0) + @test_throws ArgumentError OffsetMatrix(zeros(2:3, 1:2, 1:2), ()) + @test_throws ArgumentError OffsetMatrix(zeros(2:3, 1:2, 1:2)) end # no need to duplicate the 2D case here, From 2249dc3268eed27991cbdfa1f4db8c0adc8e557c Mon Sep 17 00:00:00 2001 From: jishnub Date: Sun, 27 Sep 2020 09:49:28 +0400 Subject: [PATCH 4/5] remove references to internal functions in docs --- docs/src/internals.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/internals.md b/docs/src/internals.md index a5f53dd9..b41293c4 100644 --- a/docs/src/internals.md +++ b/docs/src/internals.md @@ -148,9 +148,9 @@ true While a wide variety of `AbstractUnitRange`s provided by `Base` may be used as indices to construct an `OffsetArray`, at times it might be convenient to define custom types. The `OffsetArray` constructor accepts any type that may be converted to an `AbstractUnitRange`. This proceeds through a two-step process. Let's assume that the constructor called is `OffsetArray(A, indstup)`, where `indstup` is a `Tuple` of indices. -1. In the first step, the constructor calls `to_indices(A, axes(A), indstup)` to lower `indstup` to a `Tuple` of `AbstractUnitRange`s. This step converts --- among other things --- `Colon`s to axis ranges. Custom types may extend `Base.to_indices(A, axes(A), indstup)` with the desired conversion of `indstup` to `Tuple{Vararg{AbstractUnitRange{Int}}}` if this is feasible. +1. At the first step, the constructor calls `to_indices(A, axes(A), indstup)` to lower `indstup` to a `Tuple` of `AbstractUnitRange`s. This step converts --- among other things --- `Colon`s to axis ranges. Custom types may extend `Base.to_indices(A, axes(A), indstup)` with the desired conversion of `indstup` to `Tuple{Vararg{AbstractUnitRange{Int}}}` if this is feasible. -2. In the second step, the result of the previous step is passed to `OffsetArrays._toAbstractUnitRanges`. This step is only necessary if the previous step didn't return a `Tuple` of `AbstractUnitRange`s. This step allows an additional customization option: a type may be converted either to a single `AbstractUnitRange{Int}`, or to a `Tuple` of them. A custom type might specify which of these two behaviours is desired by extending [`OffsetArrays.AxisConversionStyle`](@ref). An example of a type that is acted upon at this stage is `CartesianIndices`, which is converted to a `Tuple` of `AbstractUnitRange`s. +2. At the second step, the result obtained from the previous step treated again to convert it to a `Tuple` of `AbstractUnitRange`s to handle cases where the first step doesn't achieve this. An additional customization option may be specified at this stage: a type may be converted either to a single `AbstractUnitRange{Int}`, or to a `Tuple` of them. A type might specify which of these two behaviours is desired by extending [`OffsetArrays.AxisConversionStyle`](@ref). An example of a type that is acted upon at this stage is `CartesianIndices`, which is converted to a `Tuple` of `AbstractUnitRange`s. For example, here is a custom type that leads to zero-based indexing: From e3f8fd9984ccb076beb8b59b69cb93cf4eeeb60e Mon Sep 17 00:00:00 2001 From: jishnub Date: Sun, 27 Sep 2020 17:30:17 +0400 Subject: [PATCH 5/5] Add example with a custom AbstractUnitRange --- docs/src/internals.md | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/docs/src/internals.md b/docs/src/internals.md index b41293c4..c4c1173a 100644 --- a/docs/src/internals.md +++ b/docs/src/internals.md @@ -152,7 +152,7 @@ While a wide variety of `AbstractUnitRange`s provided by `Base` may be used as i 2. At the second step, the result obtained from the previous step treated again to convert it to a `Tuple` of `AbstractUnitRange`s to handle cases where the first step doesn't achieve this. An additional customization option may be specified at this stage: a type may be converted either to a single `AbstractUnitRange{Int}`, or to a `Tuple` of them. A type might specify which of these two behaviours is desired by extending [`OffsetArrays.AxisConversionStyle`](@ref). An example of a type that is acted upon at this stage is `CartesianIndices`, which is converted to a `Tuple` of `AbstractUnitRange`s. -For example, here is a custom type that leads to zero-based indexing: +For example, here are a couple of custom type that facilitate zero-based indexing: ```jldoctest; setup = :(using OffsetArrays) julia> struct ZeroBasedIndexing end @@ -167,4 +167,22 @@ julia> axes(oa) (OffsetArrays.IdOffsetRange(0:2), OffsetArrays.IdOffsetRange(0:2)) ``` -Note that zero-based indexing may also be achieved using [`OffsetArrays.Origin`](@ref). \ No newline at end of file +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) + end + +julia> Base.first(::ZeroTo) = 0 + +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)) +``` + +Note that zero-based indexing may also be achieved using the pre-defined type [`OffsetArrays.Origin`](@ref). \ No newline at end of file