Skip to content

Commit c4757ea

Browse files
authored
Add tests for LinearIndices (#218)
* Add tests for LinearIndices * update docs to clarify that OffsetArrays require the parent's axes to support idempotent indexing
1 parent b33bc00 commit c4757ea

File tree

2 files changed

+133
-11
lines changed

2 files changed

+133
-11
lines changed

docs/src/internals.md

Lines changed: 125 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ type:
4444

4545
```jldoctest oa
4646
julia> ax = axes(oa, 2)
47-
OffsetArrays.IdOffsetRange(5:6)
47+
OffsetArrays.IdOffsetRange(values=5:6, indices=5:6)
4848
```
4949

5050
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:
6161

6262
```jldoctest oa
6363
julia> axes(ax)
64-
(OffsetArrays.IdOffsetRange(5:6),)
64+
(OffsetArrays.IdOffsetRange(values=5:6, indices=5:6),)
6565
6666
julia> axes(ax[ax])
67-
(OffsetArrays.IdOffsetRange(5:6),)
67+
(OffsetArrays.IdOffsetRange(values=5:6, indices=5:6),)
6868
```
6969

7070
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)
8080
20
8181
8282
julia> ax2 = axes(oa2, 1)
83-
OffsetArrays.IdOffsetRange(0:3)
83+
OffsetArrays.IdOffsetRange(values=0:3, indices=0:3)
8484
8585
julia> oa2[2]
8686
15
@@ -104,6 +104,121 @@ julia> oa2[ax2[2]]
104104

105105
While these behave equivalently now (conversion currently performs coercion), developers are encouraged to "future-proof" their code by choosing the behavior appropriate for each usage.
106106

107+
108+
## Wrapping other offset array types
109+
110+
An `OffsetArray` may wrap any subtype of `AbstractArray`, including ones that do not use `1`-based indexing. Such arrays however need to satisfy the fundamental axiom of idempotent indexing for things to work correctly. In other words, an axis of an offset array needs to have the same values as its own axis. This property is built into `OffsetArray`s if the parent uses 1-based indexing, but it's up to the user to ensure the correctness in case a type is to be wrapped that uses offset indices.
111+
112+
We demonstrate this through an example by creating a custom 0-based range type that we wrap in an `OffsetArray`:
113+
114+
```jldoctest zerobasedrange; setup=:(using OffsetArrays)
115+
julia> struct ZeroBasedRange{T,A<:AbstractRange{T}} <: AbstractRange{T}
116+
a :: A
117+
function ZeroBasedRange(a::AbstractRange{T}) where {T}
118+
@assert !Base.has_offset_axes(a)
119+
new{T, typeof(a)}(a)
120+
end
121+
end;
122+
123+
julia> Base.parent(A::ZeroBasedRange) = A.a;
124+
125+
julia> Base.first(A::ZeroBasedRange) = first(A.a);
126+
127+
julia> Base.length(A::ZeroBasedRange) = length(A.a);
128+
129+
julia> Base.last(A::ZeroBasedRange) = last(A.a);
130+
131+
julia> Base.size(A::ZeroBasedRange) = size(A.a);
132+
133+
julia> Base.axes(A::ZeroBasedRange) = map(x -> 0:x-1, size(A.a));
134+
135+
julia> Base.getindex(A::ZeroBasedRange, i::Int) = A.a[i + 1];
136+
137+
julia> Base.step(A::ZeroBasedRange) = step(A.a);
138+
139+
julia> function Base.show(io::IO, A::ZeroBasedRange)
140+
show(io, A.a)
141+
print(io, " with indices $(axes(A,1))")
142+
end;
143+
```
144+
145+
This definition of a `ZeroBasedRange` appears to have the correct indices, for example:
146+
147+
```jldoctest zerobasedrange
148+
julia> z = ZeroBasedRange(1:4)
149+
1:4 with indices 0:3
150+
151+
julia> z[0]
152+
1
153+
154+
julia> z[3]
155+
4
156+
```
157+
158+
However this does not use idempotent indexing, as the axis of a `ZeroBasedRange` is not its own axis.
159+
160+
```jldoctest zerobasedrange
161+
julia> axes(z, 1)
162+
0:3
163+
164+
julia> axes(axes(z, 1), 1)
165+
Base.OneTo(4)
166+
```
167+
168+
This will lead to complications in certain functions --- for example `LinearIndices` --- that tend to implictly assume idempotent indexing. In this case the `LinearIndices` of `z` will not match its axis.
169+
170+
```jldoctest zerobasedrange
171+
julia> LinearIndices(z)
172+
4-element LinearIndices{1, Tuple{UnitRange{Int64}}}:
173+
1
174+
2
175+
3
176+
4
177+
```
178+
179+
Wrapping such a type in an `OffsetArray` might lead to unexpected bugs.
180+
181+
```jldoctest zerobasedrange
182+
julia> zo = OffsetArray(z, 1);
183+
184+
julia> axes(zo, 1)
185+
OffsetArrays.IdOffsetRange(values=1:4, indices=2:5)
186+
187+
julia> Array(zo)
188+
ERROR: BoundsError: attempt to access 4-element UnitRange{Int64} at index [5]
189+
[...]
190+
```
191+
192+
The `Array` conversion errors despite `zo` having 1-based indices. The function `axes(zo, 1)` hints at the underlying problem --- the values and the indices of the axis are different. We may check that the axis of `zo` is not its own axis:
193+
194+
```jldoctest zerobasedrange
195+
julia> axes(zo, 1)
196+
OffsetArrays.IdOffsetRange(values=1:4, indices=2:5)
197+
198+
julia> axes(axes(zo, 1), 1)
199+
OffsetArrays.IdOffsetRange(values=2:5, indices=2:5)
200+
```
201+
202+
In this case the bug may be fixed by defining the `axes` of a `ZeroBasedRange` to be idempotent, for example using the `OffsetArrays.IdentityUnitRange` wrapper:
203+
204+
```jldoctest zerobasedrange
205+
julia> Base.axes(A::ZeroBasedRange) = map(x -> OffsetArrays.IdentityUnitRange(0:x-1), size(A.a))
206+
207+
julia> axes(zo, 1)
208+
OffsetArrays.IdOffsetRange(values=1:4, indices=1:4)
209+
```
210+
211+
With this new definition, the values and indices of the axis are identical, which makes indexing idempotent. The conversion to an `Array` works as expected now:
212+
213+
```jldoctest zerobasedrange
214+
julia> Array(zo)
215+
4-element Vector{Int64}:
216+
1
217+
2
218+
3
219+
4
220+
```
221+
107222
## Caveats
108223

109224
Because `IdOffsetRange` behaves quite differently to the normal `UnitRange` type, there are some
@@ -115,13 +230,13 @@ One such cases is `getindex`:
115230
julia> Ao = zeros(-3:3, -3:3); Ao[:] .= 1:49;
116231
117232
julia> Ao[-3:0, :] |> axes # the first dimension does not preserve offsets
118-
(OffsetArrays.IdOffsetRange(1:4), OffsetArrays.IdOffsetRange(-3:3))
233+
(OffsetArrays.IdOffsetRange(values=1:4, indices=1:4), OffsetArrays.IdOffsetRange(values=-3:3, indices=-3:3))
119234
120235
julia> Ao[-3:0, -3:3] |> axes # neither dimensions preserve offsets
121236
(Base.OneTo(4), Base.OneTo(7))
122237
123238
julia> Ao[axes(Ao)...] |> axes # offsets are preserved
124-
(OffsetArrays.IdOffsetRange(-3:3), OffsetArrays.IdOffsetRange(-3:3))
239+
(OffsetArrays.IdOffsetRange(values=-3:3, indices=-3:3), OffsetArrays.IdOffsetRange(values=-3:3, indices=-3:3))
125240
126241
julia> Ao[:] |> axes # This is linear indexing
127242
(Base.OneTo(49),)
@@ -138,7 +253,7 @@ julia> Ao[I, 0][1] == Ao[I[1], 0]
138253
true
139254
140255
julia> ax = axes(Ao, 1) # ax starts at index -3
141-
OffsetArrays.IdOffsetRange(-3:3)
256+
OffsetArrays.IdOffsetRange(values=-3:3, indices=-3:3)
142257
143258
julia> Ao[ax, 0][1] == Ao[ax[1], 0]
144259
true
@@ -164,7 +279,7 @@ julia> a = zeros(3, 3);
164279
julia> oa = OffsetArray(a, ZeroBasedIndexing());
165280
166281
julia> axes(oa)
167-
(OffsetArrays.IdOffsetRange(0:2), OffsetArrays.IdOffsetRange(0:2))
282+
(OffsetArrays.IdOffsetRange(values=0:2, indices=0:2), OffsetArrays.IdOffsetRange(values=0:2, indices=0:2))
168283
```
169284

170285
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:
@@ -182,7 +297,7 @@ julia> Base.length(r::ZeroTo) = r.n + 1
182297
julia> oa = OffsetArray(zeros(2,2), ZeroTo(1), ZeroTo(1));
183298
184299
julia> axes(oa)
185-
(OffsetArrays.IdOffsetRange(0:1), OffsetArrays.IdOffsetRange(0:1))
300+
(OffsetArrays.IdOffsetRange(values=0:1, indices=0:1), OffsetArrays.IdOffsetRange(values=0:1, indices=0:1))
186301
```
187302

188-
Note that zero-based indexing may also be achieved using the pre-defined type [`OffsetArrays.Origin`](@ref).
303+
Note that zero-based indexing may also be achieved using the pre-defined type [`OffsetArrays.Origin`](@ref).

test/runtests.jl

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ for Z in [:ZeroBasedRange, :ZeroBasedUnitRange]
4747
@eval Base.length(A::$Z) = length(A.a)
4848
@eval Base.last(A::$Z) = last(A.a)
4949
@eval Base.size(A::$Z) = size(A.a)
50-
@eval Base.axes(A::$Z) = map(x -> 0:x-1, size(A.a))
50+
@eval Base.axes(A::$Z) = map(x -> IdentityUnitRange(0:x-1), size(A.a))
5151
@eval Base.getindex(A::$Z, i::Int) = A.a[i + 1]
5252
@eval Base.step(A::$Z) = step(A.a)
5353
@eval OffsetArrays.no_offset_view(A::$Z) = A.a
@@ -1023,6 +1023,13 @@ end
10231023
end
10241024
end
10251025

1026+
@testset "LinearIndexing" begin
1027+
r = OffsetArray(ZeroBasedRange(3:4), 1);
1028+
@test LinearIndices(r) == axes(r,1)
1029+
r = OffsetArray(ZeroBasedRange(3:4), 2);
1030+
@test LinearIndices(r) == axes(r,1)
1031+
end
1032+
10261033
@testset "CartesianIndexing" begin
10271034
A0 = [1 3; 2 4]
10281035
A = OffsetArray(A0, (-1,2))

0 commit comments

Comments
 (0)