Skip to content

Commit de948d1

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 de948d1

File tree

8 files changed

+318
-39
lines changed

8 files changed

+318
-39
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, indices)`.
150+
151+
1. In the first step, the constructor calls `to_indices(A, axes(A), indices)` to lower `indices` 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), indices)` with the desired conversion of `indices` 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._toAbsUnitRanges`. 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: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ 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}
18+
const OffsetAxisKnownLength = Union{Integer, AbstractUnitRange}
1919
const OffsetAxis = Union{OffsetAxisKnownLength, CartesianIndices, Colon}
2020
const ArrayInitializer = Union{UndefInitializer, Missing, Nothing}
2121

@@ -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,86 @@ function overflow_check(r, offset::T) where T
123123
throw_lower_overflow_error()
124124
end
125125
end
126-
## OffsetArray constructors
127126

127+
# The only route out to inner constructor
128+
function OffsetArray(A::AbstractArray{T,N}, offsets::NTuple{N,Integer}) where {T,N}
129+
OffsetArray{T, ndims(A), typeof(A)}(A, offsets)
130+
end
131+
# Nested OffsetArrays
132+
function OffsetArray(A::OffsetArray{<:Any,N}, offsets::NTuple{N, Integer}) where {N}
133+
OffsetArray(parent(A), A.offsets .+ offsets)
134+
end
135+
136+
for (FT, ND) in ((:OffsetVector, :1), (:OffsetMatrix, :2))
137+
@eval function $FT(A::AbstractArray{<:Any,$ND}, offsets::NTuple{$ND,Integer})
138+
OffsetArray{eltype(A), $ND, typeof(A)}(A, offsets)
139+
end
140+
@eval function $FT(A::OffsetArray{<:Any,$ND}, offsets::NTuple{$ND, Integer})
141+
$FT(parent(A), A.offsets .+ offsets)
142+
end
143+
FTstr = string(FT)
144+
@eval function $FT(A::AbstractArray{<:Any,N}, offsets::NTuple{N,Integer}) where {N}
145+
throw(ArgumentError($FTstr*" requires a $(string($ND))D array"))
146+
end
147+
# Ambiguity resolution
148+
@eval function $FT(A::AbstractArray{<:Any,0}, offsets::NTuple{0,Integer})
149+
throw(ArgumentError($FTstr*" requires a $(string($ND))D array"))
150+
end
151+
end
152+
153+
## OffsetArray constructors
128154
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)
155+
@eval function $FT(A::AbstractArray, offsets::Tuple{Vararg{Integer}})
156+
throw(DimensionMismatch("offsets $offsets are not compatible with a $(ndims(A))D array"))
157+
end
158+
@eval function $FT(A::AbstractArray, inds::Tuple{})
159+
throw(DimensionMismatch("indices $inds are not compatible with a $(ndims(A))D array"))
160+
end
161+
@eval function $FT(A::AbstractArray, inds::Indices)
162+
throw(DimensionMismatch("indices $inds are not compatible with a $(ndims(A))D array"))
163+
end
164+
165+
# In general, indices get converted to AbstractUnitRanges.
166+
# CartesianIndices{N} get converted to N ranges
167+
@eval function $FT(A::AbstractArray, inds::Tuple)
168+
inds2 = to_indices(A, axes(A), inds)
169+
OffsetArray(A, _toAbstractUnitRanges(inds2))
133170
end
134-
# nested OffsetArrays
135-
@eval $FT(A::OffsetArray{T, N}, offsets::NTuple{N, Integer}) where {T,N} = $FT(parent(A), A.offsets .+ offsets)
171+
172+
@eval $FT(A::AbstractArray, inds::Vararg) = OffsetArray(A, inds)
173+
136174
# convert ranges to offsets
137-
@eval function $FT(A::AbstractArray{T}, inds::NTuple{N,OffsetAxisKnownLength}) where {T,N}
175+
@eval function $FT(A::AbstractArray{<:Any,N}, inds::Indices{N}) where {N}
138176
axparent = axes(A)
139177
lA = map(length, axparent)
140178
lI = map(length, inds)
141179
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"))
142180
$FT(A, map(_offset, axparent, inds))
143181
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)
148-
end
149-
@eval $FT(A::AbstractArray{T}, inds::Vararg{OffsetAxis,N}) where {T, N} = $FT(A, inds)
150182

151183
@eval $FT(A::AbstractArray, origin::Origin) = OffsetArray(A, origin(A))
152184
end
153185

186+
# Ambiguity resolution
187+
OffsetArray(A::AbstractArray{<:Any,0}, ::Tuple{}) = OffsetArray{eltype(A),0,typeof(A)}(A, ())
188+
OffsetArray(A::OffsetArray{<:Any,0}, ::Tuple{}) = typeof(A)(A.parent, ())
189+
154190
# array initialization
155-
function OffsetArray{T,N}(init::ArrayInitializer, inds::NTuple{N, OffsetAxisKnownLength}) where {T,N}
191+
function OffsetArray{T,N}(init::ArrayInitializer, inds::Tuple{Vararg{OffsetAxisKnownLength}}) where {T,N}
192+
length(inds) == N || throw(DimensionMismatch("indices $inds are not compatible with a $(N)D array"))
156193
AA = Array{T,N}(init, map(_indexlength, inds))
157194
OffsetArray{T, N, typeof(AA)}(AA, map(_indexoffset, inds))
158195
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)
196+
function OffsetArray{T, N}(init::ArrayInitializer, inds::Tuple) where {T, N}
197+
OffsetArray{T, N}(init, _toAbstractUnitRanges(inds))
164198
end
165-
OffsetArray{T,N}(init::ArrayInitializer, inds::Union{OffsetAxisKnownLength, CartesianIndices}...) where {T,N} = OffsetArray{T,N}(init, inds)
199+
OffsetArray{T,N}(init::ArrayInitializer, inds::Vararg) where {T,N} = OffsetArray{T,N}(init, inds)
166200

167201
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)
202+
function OffsetArray{T}(init::ArrayInitializer, inds::Tuple) where {T}
203+
OffsetArray{T}(init, _toAbstractUnitRanges(inds))
172204
end
173-
OffsetArray{T}(init::ArrayInitializer, inds::Union{OffsetAxisKnownLength, CartesianIndices}...) where {T} = OffsetArray{T}(init, inds)
205+
OffsetArray{T}(init::ArrayInitializer, inds::Vararg) where {T} = OffsetArray{T}(init, inds)
174206

175207
Base.IndexStyle(::Type{OA}) where {OA<:OffsetArray} = IndexStyle(parenttype(OA))
176208
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: 53 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,59 @@ _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+
_convertfirst(t::Tuple) = _convertfirst(AxisConversionStyle(typeof(first(t))), first(t))
62+
_convertfirst(::SingleRange, x) = (convert(AbstractUnitRange{Int}, x),)
63+
_convertfirst(::TupleOfRanges, x) = convert(Tuple{Vararg{AbstractUnitRange{Int}}}, x)
64+
65+
_toAbstractUnitRanges(t::Tuple) = (_convertfirst(t)..., _toAbstractUnitRanges(tail(t))...)
66+
_toAbstractUnitRanges(::Tuple{}) = ()

0 commit comments

Comments
 (0)