Skip to content

Commit 1e7b1e8

Browse files
committed
Add a bunch of coercion & conversion methods
1 parent 11558a9 commit 1e7b1e8

File tree

3 files changed

+203
-41
lines changed

3 files changed

+203
-41
lines changed

src/OffsetArrays.jl

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,47 +9,7 @@ end
99

1010
export OffsetArray, OffsetVector
1111

12-
"""
13-
ro = IdOffsetRange(r::AbstractUnitRange, offset)
14-
15-
Construct an "identity offset range". Numerically, `collect(ro) == collect(r) .+ offset`,
16-
with the additional property that `axes(ro) = (ro,)`, which is where the "identity" comes from.
17-
"""
18-
struct IdOffsetRange{T<:Integer,I<:AbstractUnitRange{T}} <: AbstractUnitRange{T}
19-
parent::I
20-
offset::T
21-
end
22-
IdOffsetRange(r::AbstractUnitRange{T}, offset::Integer) where T =
23-
IdOffsetRange{T,typeof(r)}(r, convert(T, offset))
24-
25-
@inline Base.axes(r::IdOffsetRange) = (Base.axes1(r),)
26-
@inline Base.axes1(r::IdOffsetRange) = IdOffsetRange(Base.axes1(r.parent), r.offset)
27-
@inline Base.unsafe_indices(r::IdOffsetRange) = (r,)
28-
@inline Base.length(r::IdOffsetRange) = length(r.parent)
29-
30-
function Base.iterate(r::IdOffsetRange)
31-
ret = iterate(r.parent)
32-
ret === nothing && return nothing
33-
return (ret[1] + r.offset, ret[2])
34-
end
35-
function Base.iterate(r::IdOffsetRange, i) where T
36-
ret = iterate(r.parent, i)
37-
ret === nothing && return nothing
38-
return (ret[1] + r.offset, ret[2])
39-
end
40-
41-
@inline Base.first(r::IdOffsetRange) = first(r.parent) + r.offset
42-
@inline Base.last(r::IdOffsetRange) = last(r.parent) + r.offset
43-
44-
@propagate_inbounds Base.getindex(r::IdOffsetRange, i::Integer) = r.parent[i - r.offset] + r.offset
45-
@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::AbstractUnitRange{<:Integer})
46-
return r.parent[s .- r.offset] .+ r.offset
47-
end
48-
49-
Base.show(io::IO, r::IdOffsetRange) = print(io, first(r), ':', last(r))
50-
51-
# Optimizations
52-
@inline Base.checkindex(::Type{Bool}, inds::IdOffsetRange, i::Real) = Base.checkindex(Bool, inds.parent, i - inds.offset)
12+
include("axes.jl")
5313

5414
## OffsetArray
5515
struct OffsetArray{T,N,AA<:AbstractArray} <: AbstractArray{T,N}

src/axes.jl

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
"""
2+
ro = IdOffsetRange(r::AbstractUnitRange, offset=0)
3+
4+
Construct an "identity offset range". Numerically, `collect(ro) == collect(r) .+ offset`,
5+
with the additional property that `axes(ro) = (ro,)`, which is where the "identity" comes from.
6+
7+
# Examples
8+
9+
The most common case is shifting a range that starts at 1 (either `1:n` or `Base.OneTo(n)`):
10+
```jldoctest
11+
julia> ro = OffsetArrays.IdOffsetRange(1:3, -2)
12+
-1:1
13+
14+
julia> ro[-1]
15+
-1
16+
17+
julia> ro[3]
18+
ERROR: BoundsError: attempt to access 3-element UnitRange{Int64} at index [5]
19+
```
20+
21+
If the range doesn't start at 1, the values may be different from the indices:
22+
```jldoctest
23+
julia> ro = OffsetArrays.IdOffsetRange(11:13, -2)
24+
9:11
25+
26+
julia> ro[-1]
27+
9
28+
29+
julia> ro[3]
30+
ERROR: BoundsError: attempt to access 3-element UnitRange{Int64} at index [5]
31+
```
32+
33+
# Extended help
34+
35+
Construction/coercion preserves the (shifted) values of the input range, but may modify
36+
the indexes if required by the specified types. For example,
37+
38+
r = OffsetArrays.IdOffsetRange{Int,UnitRange{Int}}(3:4)
39+
40+
has `r[1] == 3` and `r[2] == 4`, whereas
41+
42+
r = OffsetArrays.IdOffsetRange{Int,Base.OneTo{Int}}(3:4)
43+
44+
has `r[3] == 3` and `r[4] == 4`, and `r[1]` would throw a `BoundsError`.
45+
In this latter case, a shift in the axes was needed because `Base.OneTo` ranges
46+
must start with value 1.
47+
48+
In contrast, *conversion* preserves both the values and the indices, throwing an error
49+
when this is not achievable. For instance,
50+
51+
r = convert(OffsetArrays.IdOffsetRange{Int,UnitRange{Int}}, 3:4)
52+
53+
has `r[1] == 3` and `r[2] == 4` and would satisfy `r == 3:4`, whereas
54+
55+
```jldoctest
56+
julia> convert(OffsetArrays.IdOffsetRange{Int,Base.OneTo{Int}}, 3:4)
57+
ERROR: ArgumentError: first element must be 1, got 3
58+
```
59+
60+
where the error arises because the result could not have the same axes as the input.
61+
"""
62+
struct IdOffsetRange{T<:Integer,I<:AbstractUnitRange{T}} <: AbstractUnitRange{T}
63+
parent::I
64+
offset::T
65+
66+
IdOffsetRange{T,I}(r::I, offset::T) where {T<:Integer,I<:AbstractUnitRange{T}} = new{T,I}(r, offset)
67+
end
68+
69+
# Construction/coercion from arbitrary AbstractUnitRanges
70+
function IdOffsetRange{T,I}(r::AbstractUnitRange, offset::Integer = 0) where {T<:Integer,I<:AbstractUnitRange{T}}
71+
rc, o = offset_coerce(I, r)
72+
return IdOffsetRange{T,I}(rc, convert(T, o+offset))
73+
end
74+
function IdOffsetRange{T}(r::AbstractUnitRange, offset::Integer = 0) where T<:Integer
75+
rc = convert(AbstractUnitRange{T}, r)::AbstractUnitRange{T}
76+
return IdOffsetRange{T,typeof(rc)}(rc, convert(T, offset))
77+
end
78+
IdOffsetRange(r::AbstractUnitRange{T}, offset::Integer = 0) where T<:Integer =
79+
IdOffsetRange{T,typeof(r)}(r, convert(T, offset))
80+
81+
# Coercion from other IdOffsetRanges
82+
IdOffsetRange{T,I}(r::IdOffsetRange{T,I}) where {T<:Integer,I<:AbstractUnitRange{T}} = r
83+
function IdOffsetRange{T,I}(r::IdOffsetRange) where {T<:Integer,I<:AbstractUnitRange{T}}
84+
rc, offset = offset_coerce(I, r.parent)
85+
return IdOffsetRange{T,I}(rc, r.offset+offset)
86+
end
87+
function IdOffsetRange{T}(r::IdOffsetRange) where T<:Integer
88+
return IdOffsetRange(convert(AbstractUnitRange{T}, r.parent), r.offset)
89+
end
90+
IdOffsetRange(r::IdOffsetRange) = r
91+
92+
# Conversion preserves both the values and the indexes, throwing an InexactError if this
93+
# is not possible.
94+
Base.convert(::Type{IdOffsetRange{T,I}}, r::IdOffsetRange{T,I}) where {T<:Integer,I<:AbstractUnitRange{T}} = r
95+
Base.convert(::Type{IdOffsetRange{T,I}}, r::IdOffsetRange) where {T<:Integer,I<:AbstractUnitRange{T}} =
96+
IdOffsetRange{T,I}(convert(I, r.parent), r.offset)
97+
Base.convert(::Type{IdOffsetRange{T,I}}, r::AbstractUnitRange) where {T<:Integer,I<:AbstractUnitRange{T}} =
98+
IdOffsetRange{T,I}(convert(I, r), 0)
99+
100+
offset_coerce(::Type{Base.OneTo{T}}, r::Base.OneTo) where T<:Integer = convert(Base.OneTo{T}, r), 0
101+
function offset_coerce(::Type{Base.OneTo{T}}, r::AbstractUnitRange) where T<:Integer
102+
o = first(r) - 1
103+
return Base.OneTo{T}(last(r) - o), o
104+
end
105+
# function offset_coerce(::Type{Base.OneTo{T}}, r::IdOffsetRange) where T<:Integer
106+
# rc, o = offset_coerce(Base.OneTo{T}, r.parent)
107+
108+
# Fallback, specialze this method if `convert(I, r)` doesn't do what you need
109+
offset_coerce(::Type{I}, r::AbstractUnitRange) where I<:AbstractUnitRange{T} where T =
110+
convert(I, r), 0
111+
112+
@inline Base.axes(r::IdOffsetRange) = (Base.axes1(r),)
113+
@inline Base.axes1(r::IdOffsetRange) = IdOffsetRange(Base.axes1(r.parent), r.offset)
114+
@inline Base.unsafe_indices(r::IdOffsetRange) = (r,)
115+
@inline Base.length(r::IdOffsetRange) = length(r.parent)
116+
117+
function Base.iterate(r::IdOffsetRange)
118+
ret = iterate(r.parent)
119+
ret === nothing && return nothing
120+
return (ret[1] + r.offset, ret[2])
121+
end
122+
function Base.iterate(r::IdOffsetRange, i) where T
123+
ret = iterate(r.parent, i)
124+
ret === nothing && return nothing
125+
return (ret[1] + r.offset, ret[2])
126+
end
127+
128+
@inline Base.first(r::IdOffsetRange) = first(r.parent) + r.offset
129+
@inline Base.last(r::IdOffsetRange) = last(r.parent) + r.offset
130+
131+
@propagate_inbounds Base.getindex(r::IdOffsetRange, i::Integer) = r.parent[i - r.offset] + r.offset
132+
@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::AbstractUnitRange{<:Integer})
133+
return r.parent[s .- r.offset] .+ r.offset
134+
end
135+
136+
Base.show(io::IO, r::IdOffsetRange) = print(io, first(r), ':', last(r))
137+
138+
# Optimizations
139+
@inline Base.checkindex(::Type{Bool}, inds::IdOffsetRange, i::Real) = Base.checkindex(Bool, inds.parent, i - inds.offset)

test/runtests.jl

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,69 @@ using CatIndices: BidirectionalVector
66

77
@test isempty(detect_ambiguities(OffsetArrays, Base, Core))
88

9+
@testset "IdOffsetRange" begin
10+
function same_value(r1, r2)
11+
length(r1) == length(r2) || return false
12+
for (v1, v2) in zip(r1, r2)
13+
v1 == v2 || return false
14+
end
15+
return true
16+
end
17+
function check_indexed_by(r, rindx)
18+
for i in rindx
19+
r[i]
20+
end
21+
@test_throws BoundsError r[minimum(rindx)-1]
22+
@test_throws BoundsError r[maximum(rindx)+1]
23+
return nothing
24+
end
25+
26+
ro = OffsetArrays.IdOffsetRange(Base.OneTo(3))
27+
rs = OffsetArrays.IdOffsetRange(3:5, -2)
28+
@test typeof(ro) !== typeof(rs)
29+
@test same_value(ro, 1:3)
30+
check_indexed_by(ro, 1:3)
31+
@test same_value(rs, 1:3)
32+
check_indexed_by(rs, -1:1)
33+
@test @inferred(typeof(ro)(ro)) === ro
34+
@test @inferred(OffsetArrays.IdOffsetRange{Int}(ro)) === ro
35+
@test @inferred(OffsetArrays.IdOffsetRange{Int16}(ro)) === OffsetArrays.IdOffsetRange(Base.OneTo(Int16(3)))
36+
@test @inferred(OffsetArrays.IdOffsetRange(ro)) === ro
37+
# construction/coercion preserves the values, altering the axes if needed
38+
r2 = @inferred(typeof(rs)(ro))
39+
@test typeof(r2) === typeof(rs)
40+
@test same_value(ro, 1:3)
41+
check_indexed_by(ro, 1:3)
42+
r2 = @inferred(typeof(ro)(rs))
43+
@test typeof(r2) === typeof(ro)
44+
@test same_value(r2, 1:3)
45+
check_indexed_by(r2, 1:3)
46+
# check the example in the comments
47+
r = OffsetArrays.IdOffsetRange{Int,UnitRange{Int}}(3:4)
48+
@test same_value(r, 3:4)
49+
check_indexed_by(r, 1:2)
50+
r = OffsetArrays.IdOffsetRange{Int,Base.OneTo{Int}}(3:4)
51+
@test same_value(r, 3:4)
52+
check_indexed_by(r, 3:4)
53+
r = OffsetArrays.IdOffsetRange{Int,Base.OneTo{Int}}(3:4, -2)
54+
@test same_value(r, 1:2)
55+
check_indexed_by(r, 1:2)
56+
57+
# conversion preserves both the values and the axes, throwing an error if this is not possible
58+
@test @inferred(oftype(ro, ro)) === ro
59+
@test @inferred(convert(OffsetArrays.IdOffsetRange{Int}, ro)) === ro
60+
@test @inferred(convert(OffsetArrays.IdOffsetRange{Int}, rs)) === rs
61+
@test @inferred(convert(OffsetArrays.IdOffsetRange{Int16}, ro)) === OffsetArrays.IdOffsetRange(Base.OneTo(Int16(3)))
62+
r2 = @inferred(oftype(rs, ro))
63+
@test typeof(r2) === typeof(rs)
64+
@test same_value(r2, 1:3)
65+
check_indexed_by(r2, 1:3)
66+
@test_throws ArgumentError oftype(ro, rs)
67+
@test @inferred(oftype(ro, Base.OneTo(2))) === OffsetArrays.IdOffsetRange(Base.OneTo(2))
68+
@test @inferred(oftype(ro, 1:2)) === OffsetArrays.IdOffsetRange(Base.OneTo(2))
69+
@test_throws ArgumentError oftype(ro, 3:4)
70+
end
71+
972
@testset "Single-entry arrays in dims 0:5" begin
1073
for n = 0:5
1174
for z in (OffsetArray(ones(Int,ntuple(d->1,n)), ntuple(x->x-1,n)),

0 commit comments

Comments
 (0)