Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ext/OffsetArraysAdaptExt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Adapt.adapt_structure(to, O::OffsetArray) = OffsetArrays.parent_call(x -> Adapt.

@static if isdefined(Adapt, :parent_type)
# To support Adapt 3.0 which doesn't have parent_type defined
Adapt.parent_type(::Type{OffsetArray{T,N,AA}}) where {T,N,AA} = AA
Adapt.parent_type(::Type{OffsetArray{T,N,AA,I}}) where {T,N,AA,I} = AA
Adapt.unwrap_type(W::Type{<:OffsetArray}) = unwrap_type(parent_type(W))
end

Expand Down
53 changes: 34 additions & 19 deletions src/OffsetArrays.jl
Original file line number Diff line number Diff line change
Expand Up @@ -109,29 +109,48 @@ julia> OffsetArray(a, OffsetArrays.Origin(0)) # set the origin to zero along eac


"""
struct OffsetArray{T,N,AA<:AbstractArray{T,N}} <: AbstractArray{T,N}
struct OffsetArray{T, N, AA<:AbstractArray{T,N}, I<:Integer} <: AbstractArray{T, N}
parent::AA
offsets::NTuple{N,Int}
@inline function OffsetArray{T, N, AA}(parent::AA, offsets::NTuple{N, Int}; checkoverflow = true) where {T, N, AA<:AbstractArray{T,N}}
offsets::NTuple{N,I}
# Inner constructor with all 4 type parameters bound
@inline function OffsetArray{T, N, AA, I}(parent::AA, offsets::NTuple{N, I}; checkoverflow=true) where {T, N, AA<:AbstractArray{T,N}, I<:Integer}
# allocation of `map` on tuple is optimized away
checkoverflow && map(overflow_check, axes(parent), offsets)
new{T, N, AA}(parent, offsets)
new{T, N, AA, I}(parent, offsets)
end
# Special case of a 0-dimensional array, offsets do not make sense here
@inline function OffsetArray{T, N, AA, I}(parent::AA, offsets::Tuple{}; checkoverflow=true) where {T, N, AA<:AbstractArray{T,0}, I<:Integer}
new{T, N, AA, I}(parent, offsets)
end
end

# Helper to get the element type of an offset tuple
_offset_eltype(::Tuple{I, Vararg{I}}) where {I<:Integer} = I
_offset_eltype(::Tuple{}) = Int

# Outer constructor that infers I from the offsets - for backwards compatibility with 3-param calls
@inline function OffsetArray{T, N, AA}(parent::AA, offsets::NTuple{N, <:Integer}; checkoverflow=true) where {T, N, AA<:AbstractArray{T,N}}
I = _offset_eltype(offsets)
OffsetArray{T, N, AA, I}(parent, offsets; checkoverflow=checkoverflow)
end
# Special case for 0-dimensional arrays with 3-param call
@inline function OffsetArray{T, N, AA}(parent::AA, offsets::Tuple{}; checkoverflow=true) where {T, N, AA<:AbstractArray{T,0}}
OffsetArray{T, N, AA, _offset_eltype(offsets)}(parent, offsets; checkoverflow=checkoverflow)
end

"""
OffsetVector(v, index)

Type alias and convenience constructor for one-dimensional [`OffsetArray`](@ref)s.
"""
const OffsetVector{T,AA<:AbstractVector{T}} = OffsetArray{T,1,AA}
const OffsetVector{T,AA<:AbstractVector{T},I<:Integer} = OffsetArray{T,1,AA,I}

"""
OffsetMatrix(A, index1, index2)

Type alias and convenience constructor for two-dimensional [`OffsetArray`](@ref)s.
"""
const OffsetMatrix{T,AA<:AbstractMatrix{T}} = OffsetArray{T,2,AA}
const OffsetMatrix{T,AA<:AbstractMatrix{T},I<:Integer} = OffsetArray{T,2,AA,I}

# checks if the offset may be added to the range without overflowing
function overflow_check(r::AbstractUnitRange, offset::Integer)
Expand Down Expand Up @@ -176,16 +195,13 @@ end
for FT in (:OffsetArray, :OffsetVector, :OffsetMatrix)
# Nested OffsetArrays may strip off the wrapper and collate the offsets
# empty tuples are handled here
@eval @inline function $FT(A::OffsetArray, offsets::Tuple{Vararg{Int}}; checkoverflow = true)
@eval @inline function $FT(A::OffsetArray, offsets::Tuple{Vararg{Integer}}; checkoverflow = true)
_checkindices(A, offsets, "offsets")
# ensure that the offsets may be added together without an overflow
checkoverflow && map(overflow_check, axes(A), offsets)
I = map(+, _offsets(A, parent(A)), offsets)
$FT(parent(A), I, checkoverflow = false)
end
@eval @inline function $FT(A::OffsetArray, offsets::Tuple{Integer,Vararg{Integer}}; kw...)
$FT(A, map(Int, offsets); kw...)
end

# In general, indices get converted to AbstractUnitRanges.
# CartesianIndices{N} get converted to N ranges
Expand Down Expand Up @@ -224,16 +240,13 @@ end
@inline OffsetArray{T,N}(M::AbstractArray{T,N}, I...; kw...) where {T,N} = OffsetArray{T,N,typeof(M)}(M, I...; kw...)

@inline OffsetArray{T,N,A}(M::AbstractArray{<:Any,N}, I...; kw...) where {T,N,A<:AbstractArray{T,N}} = OffsetArray{T,N,A}(M, I; kw...)
@inline function OffsetArray{T,N,A}(M::AbstractArray{<:Any,N}, I::NTuple{N,Int}; checkoverflow = true) where {T,N,A<:AbstractArray{T,N}}
@inline function OffsetArray{T,N,A}(M::AbstractArray{<:Any,N}, I::NTuple{N,Integer}; checkoverflow = true) where {T,N,A<:AbstractArray{T,N}}
checkoverflow && map(overflow_check, axes(M), I)
Mv = no_offset_view(M)
MvA = convert(A, Mv)::A
Iof = map(+, _offsets(M), I)
OffsetArray{T,N,A}(MvA, Iof, checkoverflow = false)
end
@inline function OffsetArray{T, N, AA}(parent::AbstractArray{<:Any,N}, offsets::NTuple{N, Integer}; kw...) where {T, N, AA<:AbstractArray{T,N}}
OffsetArray{T, N, AA}(parent, map(Int, offsets)::NTuple{N,Int}; kw...)
end
@inline function OffsetArray{T,N,A}(M::AbstractArray{<:Any,N}, I::Tuple{AbstractUnitRange,Vararg{AbstractUnitRange}}; kw...) where {T,N,A<:AbstractArray{T,N}}
_checkindices(M, I, "indices")
# Performance gain by wrapping the error in a function: see https://github.com/JuliaLang/julia/issues/37558
Expand Down Expand Up @@ -275,6 +288,7 @@ end
@inline OffsetArray{T}(init::ArrayInitializer, inds...; kw...) where {T} = OffsetArray{T}(init, inds; kw...)

Base.IndexStyle(::Type{OA}) where {OA<:OffsetArray} = IndexStyle(parenttype(OA))
parenttype(::Type{OffsetArray{T,N,AA,I}}) where {T,N,AA,I} = AA
parenttype(::Type{OffsetArray{T,N,AA}}) where {T,N,AA} = AA
parenttype(A::OffsetArray) = parenttype(typeof(A))

Expand Down Expand Up @@ -381,6 +395,7 @@ Base.reshape(A::OffsetArray, inds::Dims) = _reshape_nov(A, inds)
if VERSION < v"1.10.7"
# the specialized reshape(parent::AbstractVector, ::Tuple{Colon}) is available in Base at least on this version
Base.reshape(A::OffsetVector, ::Tuple{Colon}) = A
# Keep Int here (not Integer) to avoid method ambiguity with Base on older Julia versions
Base.reshape(A::OffsetArray, inds::Tuple{Vararg{Union{Int,Colon}}}) = _reshape_nov(A, inds)
end

Expand Down Expand Up @@ -430,13 +445,13 @@ end
# but that case is handled by getindex(::OffsetArray{<:Any,N}, ::Vararg{Colon,N})
@propagate_inbounds Base.getindex(A::OffsetArray, c::Colon) = A.parent[:]

@inline function Base.getindex(A::OffsetVector, i::Int)
@inline function Base.getindex(A::OffsetVector{T,AA,<:Integer}, i::Int) where {T,AA<:AbstractVector{T}}
@boundscheck checkbounds(A, i)
@inbounds parent(A)[parentindex(Base.axes1(A), i)]
end
@propagate_inbounds Base.getindex(A::OffsetArray, i::Int) = parent(A)[i]

@inline function Base.setindex!(A::OffsetArray{T,N}, val, I::Vararg{Int,N}) where {T,N}
@inline function Base.setindex!(A::OffsetArray{T,N,AA,<:Any}, val, I::Vararg{Int,N}) where {T,N,AA}
@boundscheck checkbounds(A, I...)
J = map(parentindex, axes(A), I)
@inbounds parent(A)[J...] = val
Expand All @@ -459,7 +474,7 @@ Base.in(x, A::OffsetArray) = in(x, parent(A))
Base.copy(A::OffsetArray) = parent_call(copy, A)

Base.strides(A::OffsetArray) = strides(parent(A))
Base.elsize(::Type{OffsetArray{T,N,A}}) where {T,N,A} = Base.elsize(A)
Base.elsize(::Type{OffsetArray{T,N,A,I}}) where {T,N,A,I} = Base.elsize(A)
Base.cconvert(P::Type{Ptr{T}}, A::OffsetArray{T}) where {T} = Base.cconvert(P, parent(A))
if VERSION < v"1.11-"
@inline Base.unsafe_convert(::Type{Ptr{T}}, A::OffsetArray{T}) where {T} = Base.unsafe_convert(Ptr{T}, parent(A))
Expand Down Expand Up @@ -491,7 +506,7 @@ end
end

# An OffsetUnitRange might use the rapid getindex(::Array, ::AbstractUnitRange{Int}) for contiguous indexing
@propagate_inbounds function Base.getindex(A::Array, r::OffsetUnitRange{Int})
@propagate_inbounds function Base.getindex(A::Array, r::OffsetUnitRange{<:Integer})
B = A[_contiguousindexingtype(parent(r))]
OffsetArray(B, axes(r), checkoverflow = false)
end
Expand Down Expand Up @@ -530,7 +545,7 @@ end
# An OffsetUnitRange{Int} has an equivalent IdOffsetRange with the same values and axes,
# something similar also holds for OffsetUnitRange{BigInt}
# We may replace the former with the latter in an indexing operation to obtain a performance boost
@inline function Base.to_index(r::OffsetUnitRange{<:Union{Int,BigInt}})
@inline function Base.to_index(r::OffsetUnitRange{<:Integer})
of = first(axes(r,1)) - 1
IdOffsetRange(_subtractoffset(parent(r), of), of)
end
Expand Down
7 changes: 5 additions & 2 deletions src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
_indexoffset(r::AbstractRange) = first(r) - 1
_indexoffset(i::Integer) = 0
_indexlength(r::AbstractRange) = length(r)
_indexlength(i::Integer) = Int(i)
_indexlength(i::Integer) = Int(i) # Convert to Int for Base compatibility (reshape, etc.)
_indexlength(i::Colon) = Colon()

# utility methods used in reshape
Expand All @@ -17,7 +17,10 @@ _toaxis(i) = i
_strip_IdOffsetRange(r::IdOffsetRange) = parent(r)
_strip_IdOffsetRange(r) = r

_offset(axparent::AbstractUnitRange, ax::AbstractUnitRange) = first(ax) - first(axparent)
_offset(axparent::AbstractUnitRange, ax::AbstractUnitRange{Int}) = first(ax) - first(axparent)

# Keep the provided integer if possible
_offset(axparent::AbstractUnitRange, ax::AbstractUnitRange{I}) where {I<:Integer} = first(ax) - convert(I, first(axparent))
_offset(axparent::AbstractUnitRange, ::Union{Integer, Colon}) = 1 - first(axparent)

_offsets(A::AbstractArray) = map(ax -> first(ax) - 1, axes(A))
Expand Down
93 changes: 72 additions & 21 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ using OffsetArrays: IdentityUnitRange, no_offset_view, IIUR, Origin, IdOffsetRan
using StaticArrays
using Test

const SliceIntUR = Slice{<:AbstractUnitRange{<:Integer}}
if !isdefined(Main, :SliceIntUR)
const SliceIntUR = Slice{<:AbstractUnitRange{<:Integer}}
end

DocMeta.setdocmeta!(OffsetArrays, :DocTestSetup, :(using OffsetArrays); recursive=true)

Expand Down Expand Up @@ -46,7 +48,7 @@ end
@testset "Project meta quality checks" begin
Aqua.test_all(OffsetArrays, piracies=false)
if VERSION >= v"1.2"
doctest(OffsetArrays, manual = false)
doctest(OffsetArrays, manual=false)
end
end

Expand Down Expand Up @@ -433,6 +435,10 @@ Base.Int(a::WeirdInteger) = a
@test a === OffsetArray(a, ())
@test_throws ArgumentError OffsetArray(a, 0)
@test_throws ArgumentError OffsetArray(a0, 0)
# Test 0-dimensional array with explicit type parameters (coverage for 0-dim constructors)
b = OffsetArray{Int, 0, typeof(a0)}(a0, ())
@test b[] == 3
@test axes(b) == ()
end

@testset "OffsetVector" begin
Expand All @@ -457,15 +463,23 @@ Base.Int(a::WeirdInteger) = a
]

offsets = size.(one_based_axes[1], 1)
offsets_big = map(big, offsets)
# Test with different integer offset types (Int8, Int16, Int32, Int64, Int128, BigInt)
offsets_all = [map(T, offsets) for T in (Int8, Int16, Int32, Int64, Int128, BigInt)]

for inds in Any[offsets, offsets_big, one_based_axes...]
for inds in Any[offsets_all..., one_based_axes...]
# test indices API
a = OffsetVector{Float64}(undef, inds)
@test eltype(a) === Float64
@test axes(a) === axes(OffsetVector{Float64}(undef, inds...)) === axes(OffsetArray{Float64, 1}(undef, inds)) === axes(OffsetArray{Float64}(undef, inds))
@test axes(a) === (IdOffsetRange(Base.OneTo(4), 0), )
@test a.offsets === (0, )

OffsetType = eltype(a.offsets)

if OffsetType == BigInt # Note BigInt.((0, )) === BigInt.((0, )) is false
@test a.offsets == (0, )
else
@test a.offsets === map(OffsetType, (0, ))
end
@test axes(a.parent) == (Base.OneTo(4), )

a = OffsetVector{Nothing}(nothing, inds)
Expand All @@ -480,7 +494,7 @@ Base.Int(a::WeirdInteger) = a
end

# nested OffsetVectors
for inds in Any[offsets, offsets_big]
for inds in offsets_all
a = OffsetVector{Float64}(undef, inds)
b = OffsetVector(a, inds); b2 = OffsetVector(a, inds...);
@test eltype(b) === eltype(b2) === Float64
Expand All @@ -492,7 +506,12 @@ Base.Int(a::WeirdInteger) = a
# test offsets
a = OffsetVector{Float64}(undef, inds)
ax = (IdOffsetRange(Base.OneTo(4), -2), )
@test a.offsets === (-2, )
OffsetType = eltype(a.offsets)
if OffsetType == BigInt # Note BigInt.((-2, )) === BigInt.((-2, )) is false
@test a.offsets == (-2, )
else
@test a.offsets === map(OffsetType, (-2, ))
end
@test axes(a.parent) == (Base.OneTo(4), )
@test axes(a) === ax
a = OffsetVector{Nothing}(nothing, inds)
Expand All @@ -519,10 +538,15 @@ Base.Int(a::WeirdInteger) = a
oa2 = OffsetVector(a, inds)
oa3 = OffsetArray(a, inds...)
oa4 = OffsetArray(a, inds)
@test oa1 === oa2 === oa3 === oa4
if eltype(oa1.offsets) == BigInt # Note: BigInt(1) === BigInt(1) is false
@test oa1 == oa2 == oa3 == oa4
@test oa1.offsets == (-2, )
else
@test oa1 === oa2 === oa3 === oa4
@test oa1.offsets === (-2, )
end
@test axes(oa1) === (IdOffsetRange(Base.OneTo(4), -2), )
@test parent(oa1) === a
@test oa1.offsets === (-2, )
end

oa = OffsetArray(a, :)
Expand All @@ -537,7 +561,11 @@ Base.Int(a::WeirdInteger) = a
for inds in Any[.-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...)
if eltype(ooa.offsets) == BigInt # Note: BigInt(1) === BigInt(1) is false
@test ooa == OffsetArray(oa, inds...) == OffsetVector(oa, inds) == OffsetVector(oa, inds...)
else
@test ooa === OffsetArray(oa, inds...) === OffsetVector(oa, inds) === OffsetVector(oa, inds...)
end
@test ooa == a
@test axes(ooa) == axes(a)
@test axes(ooa) !== axes(a)
Expand Down Expand Up @@ -630,16 +658,21 @@ Base.Int(a::WeirdInteger) = a
]

offsets = size.(one_based_axes[1], 1)
offsets_big = map(big, offsets)
# Test with different integer offset types (Int8, Int16, Int32, Int64, Int128, BigInt)
offsets_all = [map(T, offsets) for T in (Int8, Int16, Int32, Int64, Int128, BigInt)]

for inds in Any[offsets, offsets_big, one_based_axes...]
for inds in Any[offsets_all..., one_based_axes...]
# test API
a = OffsetMatrix{Float64}(undef, inds)
ax = (IdOffsetRange(Base.OneTo(4), 0), IdOffsetRange(Base.OneTo(3), 0))
@test eltype(a) === Float64
@test axes(a) === axes(OffsetMatrix{Float64}(undef, inds...)) === axes(OffsetArray{Float64, 2}(undef, inds)) === axes(OffsetArray{Float64, 2}(undef, inds...)) === axes(OffsetArray{Float64}(undef, inds))
@test axes(a) === ax
@test a.offsets === (0, 0)
if eltype(a.offsets) == BigInt # Note BigInt.((0, 0)) === BigInt.((0, 0)) is false
@test a.offsets == (0, 0)
else
@test a.offsets === map(eltype(a.offsets), (0, 0))
end
@test axes(a.parent) == (Base.OneTo(4), Base.OneTo(3))

a = OffsetMatrix{Nothing}(nothing, inds)
Expand All @@ -655,7 +688,7 @@ Base.Int(a::WeirdInteger) = a
@test_throws Union{ArgumentError, ErrorException} OffsetMatrix{Float64}(undef, 2, -2) # only positive numbers works

# nested OffsetMatrices
for inds in Any[offsets, offsets_big]
for inds in offsets_all
a = OffsetMatrix{Float64}(undef, inds)
b = OffsetMatrix(a, inds); b2 = OffsetMatrix(a, inds...);
@test eltype(b) === eltype(b2) === Float64
Expand All @@ -667,7 +700,11 @@ Base.Int(a::WeirdInteger) = a
# test offsets
a = OffsetMatrix{Float64}(undef, inds)
ax = (IdOffsetRange(Base.OneTo(4), -2), IdOffsetRange(Base.OneTo(3), -1))
@test a.offsets === (-2, -1)
if eltype(a.offsets) == BigInt # Note BigInt.((0, 0)) === BigInt.((0, 0)) is false
@test a.offsets == (-2, -1)
else
@test a.offsets === (-2, -1)
end
@test axes(a.parent) == (Base.OneTo(4), Base.OneTo(3))
@test axes(a) === ax
a = OffsetMatrix{Nothing}(nothing, inds)
Expand All @@ -694,13 +731,22 @@ Base.Int(a::WeirdInteger) = a
oa2 = OffsetMatrix(a, inds)
oa3 = OffsetArray(a, inds...)
oa4 = OffsetArray(a, inds)
@test oa1 === oa2 === oa3 === oa4
if eltype(oa1.offsets) == BigInt # Note BigInt.((0, 0)) === BigInt.((0, 0)) is false
@test oa1 == oa2 == oa3 == oa4
@test oa1.offsets == (-2, -1)
else
@test oa1 === oa2 === oa3 === oa4
@test oa1.offsets === (-2, -1)
end
@test axes(oa1) === ax
@test parent(oa1) === a
@test oa1.offsets === (-2, -1)
end
oa = OffsetArray(a, :, axes(a, 2))
@test oa === OffsetArray(a, (axes(oa, 1), :)) === OffsetArray(a, axes(a)) === OffsetMatrix(a, (axes(oa, 1), :)) === OffsetMatrix(a, axes(a))
if eltype(oa.offsets) == BigInt #
@test oa == OffsetArray(a, (:, axes(a, 2))) == OffsetArray(a, axes(a)) == OffsetMatrix(a, (IdOffsetRange(axes(a)[1], 0), axes(a, 2)))
else
@test oa === OffsetArray(a, (:, axes(a, 2))) === OffsetArray(a, axes(a)) === OffsetMatrix(a, (IdOffsetRange(axes(a)[1], 0), axes(a, 2)))
end
@test oa == a
@test axes(oa) == axes(a)
@test axes(oa) !== axes(a)
Expand All @@ -713,7 +759,11 @@ Base.Int(a::WeirdInteger) = a
oa = OffsetArray(a, -1, -2)
for inds in Any[.-oa.offsets, one_based_axes...]
ooa = OffsetArray(oa, inds)
@test ooa === OffsetArray(oa, inds...) === OffsetMatrix(oa, inds) === OffsetMatrix(oa, inds...)
if eltype(ooa.offsets) == BigInt # Note BigInt.((-1, -2)) === BigInt.((-1, -2)) is false
@test ooa == OffsetArray(oa, inds...) == OffsetMatrix(oa, inds) == OffsetMatrix(oa, inds...)
else
@test ooa === OffsetArray(oa, inds...) === OffsetMatrix(oa, inds) === OffsetMatrix(oa, inds...)
end
@test typeof(parent(ooa)) <: Matrix
@test ooa == a
@test axes(ooa) == axes(a)
Expand Down Expand Up @@ -813,8 +863,9 @@ Base.Int(a::WeirdInteger) = a
# ndim of an OffsetArray should match that of the parent
@test_throws TypeError OffsetArray{Float64,3,Matrix{Float64}}

# should throw a TypeError if the offsets can not be converted to Ints
@test_throws TypeError OffsetVector{Int,Vector{Int}}(zeros(Int,2), (WeirdInteger(1),))
# should throw an error if the offset type doesn't support proper arithmetic operations
# (previously threw TypeError when converting to Int, now throws during overflow_check)
@test_throws Exception OffsetVector{Int,Vector{Int}}(zeros(Int,2), (WeirdInteger(1),))
end

@testset "custom range types" begin
Expand Down
Loading