Skip to content

Commit d59a1e2

Browse files
committed
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
1 parent 2ed1598 commit d59a1e2

File tree

8 files changed

+386
-80
lines changed

8 files changed

+386
-80
lines changed

Project.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
1010
CatIndices = "aafaddc9-749c-510e-ac4f-586e18779b91"
1111
DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab"
1212
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
13+
EllipsisNotation = "da5c29d0-fa7d-589e-88eb-ea29b0a81949"
1314
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
1415
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
1516

1617
[targets]
17-
test = ["Aqua", "CatIndices", "DelimitedFiles", "Documenter", "Test", "LinearAlgebra"]
18+
test = ["Aqua", "CatIndices", "DelimitedFiles", "Documenter", "Test", "LinearAlgebra", "EllipsisNotation"]

docs/src/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ Base.require_one_based_indexing(OA)
5656
```
5757

5858
[`OffsetArrays.Origin`](@ref) can be convenient if you want to directly specify the origin of the output
59-
OffsetArray, it will automatically compute the needed offsets. For example:
59+
OffsetArray, it will automatically compute the corresponding offsets. For example:
6060

6161
```@repl index
6262
OffsetArray(A, OffsetArrays.Origin(-1, -1))

docs/src/internals.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,28 @@ OffsetArrays.IdOffsetRange(-3:3)
143143
julia> Ao[ax, 0][1] == Ao[ax[1], 0]
144144
true
145145
```
146+
147+
## Using custom axis types
148+
149+
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.
150+
151+
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.
152+
153+
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.
154+
155+
For example, here is a custom type that leads to zero-based indexing:
156+
157+
```jldoctest; setup = :(using OffsetArrays)
158+
julia> struct ZeroBasedIndexing end
159+
160+
julia> Base.to_indices(A, inds, ::Tuple{ZeroBasedIndexing}) = map(x -> 0:length(x)-1, inds)
161+
162+
julia> a = zeros(3, 3);
163+
164+
julia> oa = OffsetArray(a, ZeroBasedIndexing());
165+
166+
julia> axes(oa)
167+
(OffsetArrays.IdOffsetRange(0:2), OffsetArrays.IdOffsetRange(0:2))
168+
```
169+
170+
Note that zero-based indexing may also be achieved using [`OffsetArrays.Origin`](@ref).

docs/src/reference.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ OffsetMatrix
77
OffsetArrays.Origin
88
OffsetArrays.IdOffsetRange
99
OffsetArrays.no_offset_view
10+
OffsetArrays.AxisConversionStyle
1011
```

src/OffsetArrays.jl

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module OffsetArrays
22

3-
using Base: Indices, tail, @propagate_inbounds
3+
using Base: tail, @propagate_inbounds
44
@static if !isdefined(Base, :IdentityUnitRange)
55
const IdentityUnitRange = Base.Slice
66
else
@@ -15,8 +15,8 @@ include("origin.jl")
1515

1616
# Technically we know the length of CartesianIndices but we need to convert it first, so here we
1717
# don't put it in OffsetAxisKnownLength.
18-
const OffsetAxisKnownLength = Union{Integer, AbstractUnitRange, IdOffsetRange}
19-
const OffsetAxis = Union{OffsetAxisKnownLength, CartesianIndices, Colon}
18+
const OffsetAxisKnownLength = Union{Integer, AbstractUnitRange}
19+
const OffsetAxis = Union{OffsetAxisKnownLength, Colon}
2020
const ArrayInitializer = Union{UndefInitializer, Missing, Nothing}
2121

2222
## OffsetArray
@@ -51,7 +51,7 @@ julia> OffsetArray(reshape(1:6, 2, 3), 0:1, -1:1)
5151
1 3 5
5252
2 4 6
5353
54-
julia> OffsetArray(reshape(1:6, 2, 3), :, -1:1) # : as a placeholder means no offset is applied at this dimension
54+
julia> OffsetArray(reshape(1:6, 2, 3), :, -1:1) # : as a placeholder to indicate that no offset is to be applied to this dimension
5555
2×3 OffsetArray(reshape(::UnitRange{$Int}, 2, 3), 1:2, -1:1) with eltype $Int with indices 1:2×-1:1:
5656
1 3 5
5757
2 4 6
@@ -123,54 +123,71 @@ function overflow_check(r, offset::T) where T
123123
throw_lower_overflow_error()
124124
end
125125
end
126-
## OffsetArray constructors
127126

127+
function OffsetArray(A::AbstractArray, offsets::Tuple{Vararg{Integer}})
128+
_checkindices(A, offsets, "offsets")
129+
OffsetArray{eltype(A), ndims(A), typeof(A)}(A, offsets)
130+
end
131+
# Nested OffsetArrays may strip off the layer and collate the offsets
132+
function OffsetArray(A::OffsetArray, offsets::Tuple{Vararg{Integer}})
133+
_checkindices(A, offsets, "offsets")
134+
OffsetArray(parent(A), A.offsets .+ offsets)
135+
end
136+
137+
for (FT, ND) in ((:OffsetVector, :1), (:OffsetMatrix, :2))
138+
@eval function $FT(A::AbstractArray{<:Any,$ND}, offsets::Tuple{Vararg{Integer}})
139+
_checkindices(A, offsets, "offsets")
140+
OffsetArray{eltype(A), $ND, typeof(A)}(A, offsets)
141+
end
142+
@eval function $FT(A::OffsetArray{<:Any,$ND}, offsets::Tuple{Vararg{Integer}})
143+
_checkindices(A, offsets, "offsets")
144+
$FT(parent(A), A.offsets .+ offsets)
145+
end
146+
FTstr = string(FT)
147+
@eval function $FT(A::AbstractArray, offsets::Tuple{Vararg{Integer}})
148+
throw(ArgumentError($FTstr*" requires a "*string($ND)*"D array"))
149+
end
150+
end
151+
152+
## OffsetArray constructors
128153
for FT in (:OffsetArray, :OffsetVector, :OffsetMatrix)
129-
# The only route out to inner constructor
130-
@eval function $FT(A::AbstractArray{T, N}, offsets::NTuple{N, Integer}) where {T, N}
131-
ndims(A) == N || throw(DimensionMismatch("The number of offsets $(N) should equal ndims(A) = $(ndims(A))"))
132-
OffsetArray{T, ndims(A), typeof(A)}(A, offsets)
154+
# In general, indices get converted to AbstractUnitRanges.
155+
# CartesianIndices{N} get converted to N ranges
156+
@eval function $FT(A::AbstractArray, inds::Tuple)
157+
$FT(A, _toAbstractUnitRanges(to_indices(A, axes(A), inds)))
133158
end
134-
# nested OffsetArrays
135-
@eval $FT(A::OffsetArray{T, N}, offsets::NTuple{N, Integer}) where {T,N} = $FT(parent(A), A.offsets .+ offsets)
159+
160+
@eval $FT(A::AbstractArray, inds::Vararg) = $FT(A, inds)
161+
136162
# convert ranges to offsets
137-
@eval function $FT(A::AbstractArray{T}, inds::NTuple{N,OffsetAxisKnownLength}) where {T,N}
138-
axparent = axes(A)
139-
lA = map(length, axparent)
163+
@eval function $FT(A::AbstractArray, inds::Tuple{AbstractUnitRange,Vararg{AbstractUnitRange}})
164+
_checkindices(A, inds, "indices")
165+
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"))
166+
lA = size(A)
140167
lI = map(length, inds)
141-
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"))
142-
$FT(A, map(_offset, axparent, inds))
143-
end
144-
# lower CartesianIndices and Colon
145-
@eval function $FT(A::AbstractArray{T}, inds::NTuple{N, OffsetAxis}) where {T, N}
146-
indsN = _uncolonindices(A, _expandCartesianIndices(inds))
147-
$FT(A, indsN)
168+
lA == lI || throw_dimerr(lA, lI)
169+
$FT(A, map(_offset, axes(A), inds))
148170
end
149-
@eval $FT(A::AbstractArray{T}, inds::Vararg{OffsetAxis,N}) where {T, N} = $FT(A, inds)
150171

151-
@eval $FT(A::AbstractArray, origin::Origin) = OffsetArray(A, origin(A))
172+
@eval $FT(A::AbstractArray, origin::Origin) = $FT(A, origin(A))
152173
end
153174

154175
# array initialization
155-
function OffsetArray{T,N}(init::ArrayInitializer, inds::NTuple{N, OffsetAxisKnownLength}) where {T,N}
176+
function OffsetArray{T,N}(init::ArrayInitializer, inds::Tuple{Vararg{OffsetAxisKnownLength}}) where {T,N}
177+
_checkindices(N, inds, "indices")
156178
AA = Array{T,N}(init, map(_indexlength, inds))
157179
OffsetArray{T, N, typeof(AA)}(AA, map(_indexoffset, inds))
158180
end
159-
function OffsetArray{T, N}(init::ArrayInitializer, inds::NTuple{NT, Union{OffsetAxisKnownLength, CartesianIndices}}) where {T, N, NT}
160-
# NT is probably not the actual dimension of the array; CartesianIndices might contain multiple dimensions
161-
indsN = _expandCartesianIndices(inds)
162-
length(indsN) == N || throw(DimensionMismatch("The number of offsets $(length(indsN)) should equal ndims(A) = $N"))
163-
OffsetArray{T, N}(init, indsN)
181+
function OffsetArray{T, N}(init::ArrayInitializer, inds::Tuple) where {T, N}
182+
OffsetArray{T, N}(init, _toAbstractUnitRanges(inds))
164183
end
165-
OffsetArray{T,N}(init::ArrayInitializer, inds::Union{OffsetAxisKnownLength, CartesianIndices}...) where {T,N} = OffsetArray{T,N}(init, inds)
184+
OffsetArray{T,N}(init::ArrayInitializer, inds::Vararg) where {T,N} = OffsetArray{T,N}(init, inds)
166185

167186
OffsetArray{T}(init::ArrayInitializer, inds::NTuple{N, OffsetAxisKnownLength}) where {T,N} = OffsetArray{T,N}(init, inds)
168-
function OffsetArray{T}(init::ArrayInitializer, inds::NTuple{N, Union{OffsetAxisKnownLength, CartesianIndices}}) where {T, N}
169-
# N is probably not the actual dimension of the array; CartesianIndices might contain multiple dimensions
170-
indsN = _expandCartesianIndices(inds)
171-
OffsetArray{T, length(indsN)}(init, indsN)
187+
function OffsetArray{T}(init::ArrayInitializer, inds::Tuple) where {T}
188+
OffsetArray{T}(init, _toAbstractUnitRanges(inds))
172189
end
173-
OffsetArray{T}(init::ArrayInitializer, inds::Union{OffsetAxisKnownLength, CartesianIndices}...) where {T} = OffsetArray{T}(init, inds)
190+
OffsetArray{T}(init::ArrayInitializer, inds::Vararg) where {T} = OffsetArray{T}(init, inds)
174191

175192
Base.IndexStyle(::Type{OA}) where {OA<:OffsetArray} = IndexStyle(parenttype(OA))
176193
parenttype(::Type{OffsetArray{T,N,AA}}) where {T,N,AA} = AA

src/axes.jl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ offset_coerce(::Type{I}, r::AbstractUnitRange) where I<:AbstractUnitRange{T} whe
132132
Base.reduced_index(i::IdOffsetRange) = typeof(i)(first(i):first(i))
133133
# Workaround for #92 on Julia < 1.4
134134
Base.reduced_index(i::IdentityUnitRange{<:IdOffsetRange}) = typeof(i)(first(i):first(i))
135+
for f in [:firstindex, :lastindex]
136+
@eval Base.$f(r::IdOffsetRange) = $f(r.parent) .+ r.offset
137+
end
135138

136139
@inline function Base.iterate(r::IdOffsetRange)
137140
ret = iterate(r.parent)
@@ -151,9 +154,12 @@ end
151154
@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::AbstractUnitRange{<:Integer})
152155
return r.parent[s .- r.offset] .+ r.offset
153156
end
154-
@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::IdOffsetRange)
157+
@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::IdentityUnitRange)
155158
return IdOffsetRange(r.parent[s .- r.offset], r.offset)
156159
end
160+
@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::IdOffsetRange)
161+
return IdOffsetRange(r.parent[s.parent .+ (s.offset - r.offset)] .+ (r.offset - s.offset), s.offset)
162+
end
157163

158164
# offset-preserve broadcasting
159165
Broadcast.broadcasted(::Base.Broadcast.DefaultArrayStyle{1}, ::typeof(-), r::IdOffsetRange{T}, x::Integer) where T =

src/utils.jl

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,66 @@ _indexlength(i::Integer) = i
88
_indexlength(i::Colon) = Colon()
99

1010
_offset(axparent::AbstractUnitRange, ax::AbstractUnitRange) = first(ax) - first(axparent)
11-
_offset(axparent::AbstractUnitRange, ax::CartesianIndices) = _offset(axparent, first(ax.indices))
1211
_offset(axparent::AbstractUnitRange, ax::Integer) = 1 - first(axparent)
1312

14-
_uncolonindices(A::AbstractArray{<:Any,N}, inds::NTuple{N,Any}) where {N} = _uncolonindices(axes(A), inds)
15-
_uncolonindices(ax::Tuple, inds::Tuple) = (first(inds), _uncolonindices(tail(ax), tail(inds))...)
16-
_uncolonindices(ax::Tuple, inds::Tuple{Colon, Vararg{Any}}) = (first(ax), _uncolonindices(tail(ax), tail(inds))...)
17-
_uncolonindices(::Tuple{}, ::Tuple{}) = ()
13+
"""
14+
OffsetArrays.AxisConversionStyle(typeof(indices))
1815
19-
_expandCartesianIndices(inds::Tuple{<:CartesianIndices, Vararg{Any}}) = (convert(Tuple{Vararg{AbstractUnitRange{Int}}}, inds[1])..., _expandCartesianIndices(Base.tail(inds))...)
20-
_expandCartesianIndices(inds::Tuple{Any,Vararg{Any}}) = (inds[1], _expandCartesianIndices(Base.tail(inds))...)
21-
_expandCartesianIndices(::Tuple{}) = ()
16+
`AxisConversionStyle` declares if `indices` should be converted to a single `AbstractUnitRange{Int}`
17+
or to a `Tuple{Vararg{AbstractUnitRange{Int}}}` while flattening custom types into indices.
18+
This method is called after `to_indices(A::Array, axes(A), indices)` to provide
19+
further information in case `to_indices` does not return a `Tuple` of `AbstractUnitRange{Int}`.
20+
21+
Custom index types should extend `AxisConversionStyle` and return either `OffsetArray.SingleRange()`,
22+
which is the default, or `OffsetArray.TupleOfRanges()`. In the former case, the type `T` should
23+
define `Base.convert(::Type{AbstractUnitRange{Int}}, ::T)`, whereas in the latter it should define
24+
`Base.convert(::Type{Tuple{Vararg{AbstractUnitRange{Int}}}}, ::T)`.
25+
26+
An example of the latter is `CartesianIndices`, which is converted to a `Tuple` of
27+
`AbstractUnitRange{Int}` while flattening the indices.
28+
29+
# Example
30+
```jldoctest; setup=:(using OffsetArrays)
31+
julia> struct NTupleOfUnitRanges{N}
32+
x ::NTuple{N, UnitRange{Int}}
33+
end
34+
35+
julia> Base.to_indices(A, inds, t::Tuple{NTupleOfUnitRanges{N}}) where {N} = t;
36+
37+
julia> OffsetArrays.AxisConversionStyle(::Type{NTupleOfUnitRanges{N}}) where {N} = OffsetArrays.TupleOfRanges();
38+
39+
julia> Base.convert(::Type{Tuple{Vararg{AbstractUnitRange{Int}}}}, t::NTupleOfUnitRanges) = t.x;
40+
41+
julia> a = zeros(3, 3);
42+
43+
julia> inds = NTupleOfUnitRanges((3:5, 2:4));
44+
45+
julia> oa = OffsetArray(a, inds);
46+
47+
julia> axes(oa, 1) == 3:5
48+
true
49+
50+
julia> axes(oa, 2) == 2:4
51+
true
52+
```
53+
"""
54+
abstract type AxisConversionStyle end
55+
struct SingleRange <: AxisConversionStyle end
56+
struct TupleOfRanges <: AxisConversionStyle end
57+
58+
AxisConversionStyle(::Type) = SingleRange()
59+
AxisConversionStyle(::Type{<:CartesianIndices}) = TupleOfRanges()
60+
61+
_convertTupleAbstractUnitRange(x) = _convertTupleAbstractUnitRange(AxisConversionStyle(typeof(x)), x)
62+
_convertTupleAbstractUnitRange(::SingleRange, x) = (convert(AbstractUnitRange{Int}, x),)
63+
_convertTupleAbstractUnitRange(::TupleOfRanges, x) = convert(Tuple{Vararg{AbstractUnitRange{Int}}}, x)
64+
65+
_toAbstractUnitRanges(t::Tuple) = (_convertTupleAbstractUnitRange(first(t))..., _toAbstractUnitRanges(tail(t))...)
66+
_toAbstractUnitRanges(::Tuple{}) = ()
67+
68+
# ensure that the indices are consistent in the constructor
69+
_checkindices(A::AbstractArray, indices, label) = _checkindices(ndims(A), indices, label)
70+
function _checkindices(N::Integer, indices, label)
71+
throw_argumenterror(N, indices, label) = throw(ArgumentError(label*" $indices are not compatible with a $(N)D array"))
72+
N == length(indices) || throw_argumenterror(N, indices, label)
73+
end

0 commit comments

Comments
 (0)