From cf0f574c77c40fe310de1f257959e98a0150567a Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Sun, 7 Dec 2025 23:48:52 +0530 Subject: [PATCH 1/6] Split OffsetArraysCore as a separate package --- OffsetArraysCore/Project.toml | 41 + OffsetArraysCore/ext/OffsetArraysAdaptExt.jl | 17 + OffsetArraysCore/src/OffsetArraysCore.jl | 939 +++++++++++ OffsetArraysCore/src/axes.jl | 295 ++++ OffsetArraysCore/src/origin.jl | 91 ++ OffsetArraysCore/src/utils.jl | 144 ++ OffsetArraysCore/test/adapt.jl | 27 + OffsetArraysCore/test/aqua.jl | 11 + OffsetArraysCore/test/axes.jl | 373 +++++ OffsetArraysCore/test/centered.jl | 70 + OffsetArraysCore/test/customranges.jl | 87 + OffsetArraysCore/test/doctests.jl | 15 + OffsetArraysCore/test/indexing.jl | 514 ++++++ OffsetArraysCore/test/misc.jl | 18 + OffsetArraysCore/test/offsetarray.jl | 1513 ++++++++++++++++++ OffsetArraysCore/test/origin.jl | 87 + OffsetArraysCore/test/runtests.jl | 9 + 17 files changed, 4251 insertions(+) create mode 100644 OffsetArraysCore/Project.toml create mode 100644 OffsetArraysCore/ext/OffsetArraysAdaptExt.jl create mode 100644 OffsetArraysCore/src/OffsetArraysCore.jl create mode 100644 OffsetArraysCore/src/axes.jl create mode 100644 OffsetArraysCore/src/origin.jl create mode 100644 OffsetArraysCore/src/utils.jl create mode 100644 OffsetArraysCore/test/adapt.jl create mode 100644 OffsetArraysCore/test/aqua.jl create mode 100644 OffsetArraysCore/test/axes.jl create mode 100644 OffsetArraysCore/test/centered.jl create mode 100644 OffsetArraysCore/test/customranges.jl create mode 100644 OffsetArraysCore/test/doctests.jl create mode 100644 OffsetArraysCore/test/indexing.jl create mode 100644 OffsetArraysCore/test/misc.jl create mode 100644 OffsetArraysCore/test/offsetarray.jl create mode 100644 OffsetArraysCore/test/origin.jl create mode 100644 OffsetArraysCore/test/runtests.jl diff --git a/OffsetArraysCore/Project.toml b/OffsetArraysCore/Project.toml new file mode 100644 index 0000000..f2dd300 --- /dev/null +++ b/OffsetArraysCore/Project.toml @@ -0,0 +1,41 @@ +name = "OffsetArraysCore" +uuid = "8bef33ee-3d68-4d38-b478-812fb774fb26" +version = "1.0.0" +authors = ["Jishnu Bhattacharya "] + +[deps] +Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" + +[weakdeps] +Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" + +[extensions] +OffsetArraysAdaptExt = "Adapt" + +[compat] +Adapt = "2, 3, 4" +Aqua = "0.8" +CatIndices = "0.2" +DelimitedFiles = "<0.0.1, 1" +Documenter = "0.27, 1" +EllipsisNotation = "1" +FillArrays = "0.11, 0.13, 1" +LinearAlgebra = "<0.0.1, 1" +StaticArrays = "1" +Test = "<0.0.1, 1" +julia = "0.7, 1" + +[extras] +Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" +Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" +CatIndices = "aafaddc9-749c-510e-ac4f-586e18779b91" +DelimitedFiles = "8bb1440f-4735-579b-a4ab-409b98df4dab" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +EllipsisNotation = "da5c29d0-fa7d-589e-88eb-ea29b0a81949" +FillArrays = "1a297f60-69ca-5386-bcde-b61e274b549b" +LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" + +[targets] +test = ["Adapt", "Aqua", "CatIndices", "DelimitedFiles", "Documenter", "EllipsisNotation", "FillArrays", "LinearAlgebra", "StaticArrays", "Test"] diff --git a/OffsetArraysCore/ext/OffsetArraysAdaptExt.jl b/OffsetArraysCore/ext/OffsetArraysAdaptExt.jl new file mode 100644 index 0000000..a4ce18d --- /dev/null +++ b/OffsetArraysCore/ext/OffsetArraysAdaptExt.jl @@ -0,0 +1,17 @@ +module OffsetArraysAdaptExt + +using OffsetArraysCore, Adapt + +## +# Adapt allows for automatic conversion of CPU OffsetArrays to GPU OffsetArrays +## +import Adapt +Adapt.adapt_structure(to, O::OffsetArray) = OffsetArraysCore.parent_call(x -> Adapt.adapt(to, x), O) + +@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.unwrap_type(W::Type{<:OffsetArray}) = unwrap_type(parent_type(W)) +end + +end diff --git a/OffsetArraysCore/src/OffsetArraysCore.jl b/OffsetArraysCore/src/OffsetArraysCore.jl new file mode 100644 index 0000000..5b4235d --- /dev/null +++ b/OffsetArraysCore/src/OffsetArraysCore.jl @@ -0,0 +1,939 @@ +module OffsetArraysCore + +export OffsetArray, OffsetMatrix, OffsetVector + +using Base: tail, @propagate_inbounds +@static if !isdefined(Base, :IdentityUnitRange) + const IdentityUnitRange = Base.Slice +else + using Base: IdentityUnitRange +end + +const IIUR = IdentityUnitRange{<:AbstractUnitRange{<:Integer}} + +const ArrayInitializer = Union{UndefInitializer, Missing, Nothing} + +include("axes.jl") +include("utils.jl") +include("origin.jl") + + +## OffsetArray +""" + OffsetArray(A, indices...) + +Return an `AbstractArray` that shares element type and size with the first argument but +uses the supplied `indices` to infer its axes. If all the indices are `AbstractUnitRange`s then +these are directly used as the axis span along each dimension. Refer to the examples below for other +permissible types. + +Alternatively it's possible to specify the coordinates of one corner of the array +and have the axes be computed automatically from the size of `A`. +This constructor makes it convenient to shift to +an arbitrary starting index along each axis, for example to a zero-based indexing scheme followed by +arrays in languages such as C and Python. +See [`Origin`](@ref) and the examples below for this usage. + +# Example: offsets + +There are two types of `indices`: integers and ranges-like types. + +Integers are recognized as offsets, where `0` means no offsets are applied: + +```jldoctest; setup=:(using OffsetArraysCore) +julia> A = OffsetArray(reshape(1:6, 2, 3), -1, -2) +2×3 OffsetArray(reshape(::UnitRange{$Int}, 2, 3), 0:1, -1:1) with eltype $Int with indices 0:1×-1:1: + 1 3 5 + 2 4 6 + +julia> A[0, 1] +5 +``` + +Examples of range-like types are: `UnitRange` (e.g, `-1:2`), `CartesianIndices`, +and `Colon()` (or concisely `:`). A `UnitRange` specifies the axis span along one particular dimension, +`CartesianIndices` specify the axis spans along multiple dimensions, and a `Colon` is a placeholder +that specifies that the `OffsetArray` shares its axis with its parent along that dimension. + +```jldoctest; setup=:(using OffsetArraysCore) +julia> OffsetArray(reshape(1:6, 2, 3), 0:1, -1:1) +2×3 OffsetArray(reshape(::UnitRange{$Int}, 2, 3), 0:1, -1:1) with eltype $Int with indices 0:1×-1:1: + 1 3 5 + 2 4 6 + +julia> OffsetArray(reshape(1:6, 2, 3), :, -1:1) # : as a placeholder to indicate that no offset is to be applied to the first dimension +2×3 OffsetArray(reshape(::UnitRange{$Int}, 2, 3), 1:2, -1:1) with eltype $Int with indices 1:2×-1:1: + 1 3 5 + 2 4 6 +``` + +Use `CartesianIndices` to specify the coordinates of two diagonally opposite corners: + +```jldoctest; setup=:(using OffsetArraysCore) +julia> OffsetArray(reshape(1:6, 2, 3), CartesianIndex(0, -1):CartesianIndex(1, 1)) +2×3 OffsetArray(reshape(::UnitRange{$Int}, 2, 3), 0:1, -1:1) with eltype $Int with indices 0:1×-1:1: + 1 3 5 + 2 4 6 +``` + +Integers and range-like types may not be combined in the same call: + +```julia +julia> OffsetArray(reshape(1:6, 2, 3), 0, -1:1) +ERROR: [...] +``` + +# Example: origin + +[`OffsetArraysCore.Origin`](@ref) may be used to specify the origin of the OffsetArray. The term origin here +refers to the corner with the lowest values of coordinates, such as the left edge for an `AbstractVector`, +the bottom left corner for an `AbstractMatrix` and so on. The coordinates of the origin sets the starting +index of the array along each dimension. + +```jldoctest; setup=:(using OffsetArraysCore) +julia> a = [1 2; 3 4]; + +julia> OffsetArray(a, OffsetArraysCore.Origin(0, 1)) +2×2 OffsetArray(::$(Array{Int,2}), 0:1, 1:2) with eltype $Int with indices 0:1×1:2: + 1 2 + 3 4 + +julia> OffsetArray(a, OffsetArraysCore.Origin(0)) # set the origin to zero along each dimension +2×2 OffsetArray(::$(Array{Int, 2}), 0:1, 0:1) with eltype $Int with indices 0:1×0:1: + 1 2 + 3 4 +``` + + +""" +struct OffsetArray{T,N,AA<:AbstractArray{T,N}} <: 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}} + # allocation of `map` on tuple is optimized away + checkoverflow && map(overflow_check, axes(parent), offsets) + new{T, N, AA}(parent, offsets) + end +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} + +""" + 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} + +# checks if the offset may be added to the range without overflowing +function overflow_check(r::AbstractUnitRange, offset::Integer) + Base.hastypemax(eltype(r)) || return nothing + # This gives some performance boost https://github.com/JuliaLang/julia/issues/33273 + throw_upper_overflow_error(val) = throw(OverflowError("offset should be <= $(typemax(Int) - val) corresponding to the axis $r, received an offset $offset")) + throw_lower_overflow_error(val) = throw(OverflowError("offset should be >= $(typemin(Int) - val) corresponding to the axis $r, received an offset $offset")) + + # With ranges in the picture, first(r) might not necessarily be < last(r) + # we therefore use the min and max of first(r) and last(r) to check for overflow + firstlast_min, firstlast_max = minmax(first(r), last(r)) + + if offset > 0 && firstlast_max > typemax(Int) - offset + throw_upper_overflow_error(firstlast_max) + elseif offset < 0 && firstlast_min < typemin(Int) - offset + throw_lower_overflow_error(firstlast_min) + end + return nothing +end + +# Tuples of integers are treated as offsets +# Empty Tuples are handled here +@inline function OffsetArray(A::AbstractArray, offsets::Tuple{Vararg{Integer}}; kw...) + _checkindices(A, offsets, "offsets") + OffsetArray{eltype(A), ndims(A), typeof(A)}(A, offsets; kw...) +end + +# These methods are necessary to disallow incompatible dimensions for +# the OffsetVector and the OffsetMatrix constructors +for (FT, ND) in ((:OffsetVector, :1), (:OffsetMatrix, :2)) + @eval @inline function $FT(A::AbstractArray{<:Any,$ND}, offsets::Tuple{Vararg{Integer}}; kw...) + _checkindices(A, offsets, "offsets") + OffsetArray{eltype(A), $ND, typeof(A)}(A, offsets; kw...) + end + FTstr = string(FT) + @eval @inline function $FT(A::AbstractArray, offsets::Tuple{Vararg{Integer}}; kw...) + throw(ArgumentError($FTstr*" requires a "*string($ND)*"D array")) + end +end + +## OffsetArray constructors +for FT in (:OffsetArray, :OffsetVector, :OffsetMatrix) + # Nested OffsetArraysCore 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) + _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 + @eval @inline function $FT(A::AbstractArray, inds::Tuple{Any,Vararg{Any}}; kw...) + $FT(A, _toAbstractUnitRanges(to_indices(A, axes(A), inds)); kw...) + end + + # convert ranges to offsets + @eval @inline function $FT(A::AbstractArray, inds::Tuple{AbstractUnitRange,Vararg{AbstractUnitRange}}; kw...) + _checkindices(A, inds, "indices") + # Performance gain by wrapping the error in a function: see https://github.com/JuliaLang/julia/issues/37558 + 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")) + lA = size(A) + lI = map(length, inds) + lA == lI || throw_dimerr(lA, lI) + $FT(A, map(_offset, axes(A), inds); kw...) + end + + @eval @inline $FT(A::AbstractArray, inds...; kw...) = $FT(A, inds; kw...) + @eval @inline $FT(A::AbstractArray; checkoverflow = false) = $FT(A, ntuple(zero, Val(ndims(A))), checkoverflow = checkoverflow) + + @eval @inline $FT(A::AbstractArray, origin::Origin; checkoverflow = true) = $FT(A, origin.index .- first.(axes(A)); checkoverflow = checkoverflow) +end + +(o::Origin)(A::AbstractArray) = OffsetArray(no_offset_view(A), o) +Origin(A::AbstractArray) = Origin(first.(axes(A))) + +# conversion-related methods +@inline OffsetArray{T}(M::AbstractArray, I...; kw...) where {T} = OffsetArray{T,ndims(M)}(M, I...; kw...) + +@inline function OffsetArray{T,N}(M::AbstractArray{<:Any,N}, I...; kw...) where {T,N} + M2 = _of_eltype(T, M) + OffsetArray{T,N}(M2, I...; kw...) +end +@inline OffsetArray{T,N}(M::OffsetArray{T,N}, I...; kw...) where {T,N} = OffsetArray(M, I...; kw...) +@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}} + 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 + 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")) + lM = size(M) + lI = map(length, I) + lM == lI || throw_dimerr(lM, lI) + OffsetArray{T,N,A}(M, map(_offset, axes(M), I); kw...) +end +@inline function OffsetArray{T,N,A}(M::AbstractArray{<:Any,N}, I::Tuple; kw...) where {T,N,A<:AbstractArray{T,N}} + OffsetArray{T,N,A}(M, _toAbstractUnitRanges(to_indices(M, axes(M), I)); kw...) +end +@inline function OffsetArray{T,N,A}(M::AbstractArray{<:Any,N}; kw...) where {T,N,A<:AbstractArray{T,N}} + Mv = no_offset_view(M) + MvA = convert(A, Mv)::A + OffsetArray{T,N,A}(MvA, _offsets(M); kw...) +end +@inline OffsetArray{T,N,A}(M::A; checkoverflow = false) where {T,N,A<:AbstractArray{T,N}} = OffsetArray{T,N,A}(M, ntuple(zero, Val(N)); checkoverflow = checkoverflow) + +Base.convert(::Type{T}, M::AbstractArray) where {T<:OffsetArray} = M isa T ? M : T(M) + +@inline AbstractArray{T,N}(M::OffsetArray{S,N}) where {T,S,N} = OffsetArray{T}(M) + +# array initialization +@inline function OffsetArray{T,N}(init::ArrayInitializer, inds::Tuple{Vararg{IntegerOrAbsUnitRange}}; kw...) where {T,N} + _checkindices(N, inds, "indices") + AA = Array{T,N}(init, map(_indexlength, inds)) + OffsetArray{T, N, typeof(AA)}(AA, map(_indexoffset, inds); kw...) +end +@inline function OffsetArray{T, N}(init::ArrayInitializer, inds::Tuple; kw...) where {T, N} + OffsetArray{T, N}(init, _toAbstractUnitRanges(inds); kw...) +end +@inline OffsetArray{T,N}(init::ArrayInitializer, inds...; kw...) where {T,N} = OffsetArray{T,N}(init, inds; kw...) + +@inline OffsetArray{T}(init::ArrayInitializer, inds::NTuple{N, IntegerOrAbsUnitRange}; kw...) where {T,N} = OffsetArray{T,N}(init, inds; kw...) +@inline function OffsetArray{T}(init::ArrayInitializer, inds::Tuple; kw...) where {T} + OffsetArray{T}(init, _toAbstractUnitRanges(inds); kw...) +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}}) where {T,N,AA} = AA +parenttype(A::OffsetArray) = parenttype(typeof(A)) + +Base.parent(A::OffsetArray) = A.parent + +# TODO: Ideally we would delegate to the parent's broadcasting implementation, but that +# is currently broken in sufficiently many implementation, namely RecursiveArrayTools, DistributedArrays +# and StaticArrays, that it will take concentrated effort to get this working across the ecosystem. +# The goal would be to have `OffsetArray(CuArray) .+ 1 == OffsetArray{CuArray}`. +# Base.Broadcast.BroadcastStyle(::Type{<:OffsetArray{<:Any, <:Any, AA}}) where AA = Base.Broadcast.BroadcastStyle(AA) + +@inline Base.size(A::OffsetArray) = size(parent(A)) +# specializing length isn't necessary, as length(A) = prod(size(A)), +# but specializing length enables constant-propagation for statically sized arrays +# see https://github.com/JuliaArrays/OffsetArraysCore.jl/pull/304 +@inline Base.length(A::OffsetArray) = length(parent(A)) + +@inline Base.axes(A::OffsetArray) = map(IdOffsetRange, axes(parent(A)), A.offsets) +@inline Base.axes(A::OffsetArray, d) = d <= ndims(A) ? IdOffsetRange(axes(parent(A), d), A.offsets[d]) : IdOffsetRange(axes(parent(A), d)) +@inline Base.axes1(A::OffsetArray{T,0}) where {T} = IdOffsetRange(axes(parent(A), 1)) # we only need to specialize this one + +# Issue 128 +# See https://github.com/JuliaLang/julia/issues/37274 for the issue reported in Base +# The fix https://github.com/JuliaLang/julia/pull/39404 should be available on v1.6 +# The following method is added on older Julia versions to ensure correct behavior for OffsetVectors +if VERSION < v"1.6" + @inline function Base.compute_linindex(A::OffsetVector, I::NTuple{N,Any}) where N + IP = Base.fill_to_length(axes(A), Base.OneTo(1), Val(N)) + Base.compute_linindex(first(LinearIndices(A)), 1, IP, I) + end +end + +# Utils to translate a function to the parent while preserving offsets +unwrap(x) = x, identity +unwrap(x::OffsetArray) = parent(x), data -> OffsetArray(data, x.offsets, checkoverflow = false) +function parent_call(f, x) + parent, wrap_offset = unwrap(x) + wrap_offset(f(parent)) +end + +Base.similar(A::OffsetArray, ::Type{T}, dims::Dims) where T = + similar(parent(A), T, dims) + +Base.similar(::Type{A}, sz::Tuple{Vararg{Int}}) where {A<:OffsetArray} = similar(Array{eltype(A)}, sz) + +for AX in (:IntegerOrAbsUnitRange, :IntegerOrOneTo, :IntegerOrOneToOrOffsetAxis) + @eval begin + function Base.similar(A::OffsetArray, ::Type{T}, shape::Tuple{$AX,Vararg{$AX}}) where T + return _similar(parent(A), T, shape) + end + # ambiguity resolution with AbstractArray + function Base.similar(::Type{T}, shape::Tuple{$AX,Vararg{$AX}}) where {T<:OffsetArray} + return _similar(T, shape) + end + end +end + +function Base.similar(A::AbstractArray, ::Type{T}, shape::Tuple{IntegerOrOneToOrOffsetAxis,Vararg{IntegerOrOneToOrOffsetAxis}}) where T + return _similar(A, T, shape) +end + +function Base.similar(::Type{T}, shape::Tuple{IntegerOrOneToOrOffsetAxis,Vararg{IntegerOrOneToOrOffsetAxis}}) where {T<:AbstractArray} + return _similar(T, shape) +end + +function _similar(A::AbstractArray, ::Type{T}, shape) where {T} + # strip IdOffsetRanges to extract the parent range and use it to generate the array + new_shape = map(_strip_IdOffsetRange, shape) + P = _similar_axes_or_length(A, T, new_shape, shape) + return _indexedby(P, shape) +end + +function _similar(::Type{T}, shape) where {T<:AbstractArray} + # strip IdOffsetRanges to extract the parent range and use it to generate the array + new_shape = map(_strip_IdOffsetRange, shape) + P = _similar_axes_or_length(T, new_shape, shape) + return _indexedby(P, shape) +end + +# Try to use the axes to generate the parent array type +# This is useful if the axes have special meanings, such as with static arrays +# This method is hit if at least one axis provided to similar(A, T, axes) is an IdOffsetRange +# For example this is hit when similar(A::OffsetArray) is called, +# which expands to similar(A, eltype(A), axes(A)) +_similar_axes_or_length(A, T, ax, ::Any) = similar(A, T, to_shape(ax)) +_similar_axes_or_length(AT, ax, ::Any) = similar(AT, to_shape(ax)) +# Handle the general case by resorting to lengths along each axis +# This is hit if none of the axes provided to similar(A, T, axes) are IdOffsetRanges, +# and if similar(A, T, axes::AX) is not defined for the type AX. +# In this case the best that we can do is to create a mutable array of the correct size +_similar_axes_or_length(A, T, ax::I, ::I) where {I} = similar(A, T, map(_indexlength, ax)) +_similar_axes_or_length(AT, ax::I, ::I) where {I} = similar(AT, map(_indexlength, ax)) + +# reshape accepts a single colon +Base.reshape(A::AbstractArray, inds::IntegerOrOneToOrOffsetAxisOrColon...) = reshape(A, inds) +function Base.reshape(A::AbstractArray, inds::Tuple{Vararg{IntegerOrOneToOrOffsetAxisOrColon}}) + AR = reshape(no_offset_view(A), to_shape(inds)) + O = _indexedby(AR, inds) + return _popreshape(O, axes(AR), _filterreshapeinds(inds)) +end + +# Reshaping OffsetArraysCore can "pop" the original OffsetArray wrapper and return +# an OffsetArray(reshape(...)) instead of an OffsetArray(reshape(OffsetArray(...))) +# Short-circuit for AbstractVectors if the axes are compatible to get around the Base restriction +# to 1-based vectors`` +function _reshape(A::AbstractVector, inds::Tuple{IntegerOrOneToOrOffsetAxisOrColon}) + @noinline throw_dimerr(ind::Integer) = throw( + DimensionMismatch("parent has $(size(A,1)) elements, which is incompatible with length $ind")) + @noinline throw_dimerr(ind) = throw( + DimensionMismatch("parent has $(size(A,1)) elements, which is incompatible with indices $ind")) + _checksize(first(inds), size(A,1)) || throw_dimerr(first(inds)) + A +end +_reshape(A, inds) = _reshape2(A, inds) +_reshape2(A, inds) = reshape(A, inds) +# avoid a stackoverflow by relegating to the parent if no_offset_view returns an offsetarray +_reshape2(A::OffsetArray, inds) = reshape(parent(A), inds) +_reshape_nov(A, inds) = _reshape(no_offset_view(A), inds) + +# And for non-offset axes, we can just return a reshape of the parent directly +Base.reshape(A::OffsetArray, inds::Tuple{Integer,Vararg{Integer}}) = _reshape_nov(A, inds) +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 + Base.reshape(A::OffsetArray, inds::Tuple{Vararg{Union{Int,Colon}}}) = _reshape_nov(A, inds) +end + +# permutedims in Base does not preserve axes, and can not be fixed in a non-breaking way +# This is a stopgap solution +Base.permutedims(v::OffsetVector) = reshape(v, (1, axes(v, 1))) + +Base.zero(A::OffsetArray) = parent_call(zero, A) +Base.fill!(A::OffsetArray, x) = parent_call(Ap -> fill!(Ap, x), A) + + +## Indexing + +# Note this gets the index of the parent *array*, not the index of the parent *range* +# Here's how one can think about this: +# Δi = i - first(r) +# i′ = first(r.parent) + Δi +# and one obtains the result below. +parentindex(r::IdOffsetRange, i) = i - r.offset + +@propagate_inbounds Base.getindex(A::OffsetArray{<:Any,0}) = A.parent[] + +@inline function Base.getindex(A::OffsetArray{<:Any,N}, I::Vararg{Int,N}) where N + @boundscheck checkbounds(A, I...) + J = map(parentindex, axes(A), I) + @inbounds parent(A)[J...] +end + +@propagate_inbounds Base.getindex(A::OffsetArray{<:Any,N}, c::Vararg{Colon,N}) where N = + parent_call(x -> getindex(x, c...), A) + +# With one Colon we use linear indexing. +# In this case we may forward the index to the parent, as the information about the axes is lost +# The exception to this is with OffsetVectors where the axis information is preserved, +# 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) + @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} + @boundscheck checkbounds(A, I...) + J = map(parentindex, axes(A), I) + @inbounds parent(A)[J...] = val + A +end + +@inline function Base.setindex!(A::OffsetVector, val, i::Int) + @boundscheck checkbounds(A, i) + @inbounds parent(A)[parentindex(Base.axes1(A), i)] = val + A +end +@propagate_inbounds function Base.setindex!(A::OffsetArray, val, i::Int) + parent(A)[i] = val + A +end + +@inline Base.iterate(a::OffsetArray, i...) = iterate(parent(a), i...) + +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.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)) +end + +# For fast broadcasting: ref https://discourse.julialang.org/t/why-is-there-a-performance-hit-on-broadcasting-with-OffsetArraysCore/32194 +Base.dataids(A::OffsetArray) = Base.dataids(parent(A)) +Broadcast.broadcast_unalias(dest::OffsetArray, src::OffsetArray) = parent(dest) === parent(src) ? src : Broadcast.unalias(dest, src) + + + +### Special handling for AbstractRange +const OffsetRange{T} = OffsetVector{T,<:AbstractRange{T}} +const OffsetUnitRange{T} = OffsetVector{T,<:AbstractUnitRange{T}} + +Base.step(a::OffsetRange) = step(parent(a)) + +Base.checkindex(::Type{Bool}, inds::AbstractUnitRange, or::OffsetRange) = Base.checkindex(Bool, inds, parent(or)) + +# Certain special methods for linear indexing with integer ranges (or OffsetRanges) +# These may bypass the default getindex(A, I...) pathway if the parent types permit this +# For example AbstractUnitRanges and Arrays have special linear indexing behavior defined + +@propagate_inbounds function _getindex_onebasedrange(A, r::AbstractRange{<:Integer}) + B = if iszero(step(r)) + A[StepRange(r)] + else + A[StepRangeLen(r)] + end + return B +end + +@propagate_inbounds function _getindex_onebasedrange(A, r::AbstractUnitRange{<:Integer}) + B = A[UnitRange(r)] + return B +end + +@static if isdefined(Base, :AbstractOneTo) + @propagate_inbounds function _getindex_onebasedrange(A, r::Base.AbstractOneTo{<:Integer}) + return A[r] + end +end + +# If both the arguments are offset, we may unwrap the indices to call (::OffsetArray)[::AbstractRange{Int}] +@propagate_inbounds function Base.getindex(A::OffsetArray, r::OffsetRange{Int}) + B = _getindex_onebasedrange(A, parent(r)) + _indexedby(B, axes(r)) +end +# If the indices are offset, we may unwrap them and pass the parent to getindex +@propagate_inbounds function Base.getindex(A::AbstractRange, r::OffsetRange{Int}) + B = _getindex_onebasedrange(A, parent(r)) + _indexedby(B, axes(r)) +end + +# An OffsetUnitRange might use the rapid getindex(::Array, ::AbstractUnitRange{Int}) for contiguous indexing +@propagate_inbounds function Base.getindex(A::Array, r::OffsetUnitRange{Int}) + B = _getindex_onebasedrange(A, parent(r)) + OffsetArray(B, axes(r), checkoverflow = false) +end + +# avoid hitting the slow method getindex(::Array, ::AbstractRange{Int}) +# instead use the faster getindex(::Array, ::UnitRange{Int}) +if VERSION <= v"1.7.0-DEV.1039" + @propagate_inbounds function Base.getindex(A::Array, r::IdOffsetRange) + B = _getindex_onebasedrange(A, parent(r)) + _indexedby(B, axes(r)) + end +end + +# Linear Indexing of OffsetArraysCore with AbstractUnitRanges may use the faster contiguous indexing methods +@inline function Base.getindex(A::OffsetArray, r::AbstractUnitRange{Int}) + @boundscheck checkbounds(A, r) + # nD OffsetArraysCore do not have their linear indices shifted, so we may forward the indices provided to the parent + @inbounds B = _getindex_onebasedrange(parent(A), r) + _indexedby(B, axes(r)) +end +@inline function Base.getindex(A::OffsetVector, r::AbstractUnitRange{Int}) + @boundscheck checkbounds(A, r) + # OffsetVectors may have their linear indices shifted, so we subtract the offset from the indices provided + @inbounds B = parent(A)[_subtractoffset(r, A.offsets[1])] + _indexedby(B, axes(r)) +end + +# This method added mainly to index an OffsetRange with another range +@inline function Base.getindex(A::OffsetVector, r::AbstractRange{Int}) + @boundscheck checkbounds(A, r) + @inbounds B = parent(A)[_subtractoffset(r, A.offsets[1])] + _indexedby(B, axes(r)) +end + +# In general we would pass through getindex(A, I...) which calls to_indices(A, I) and finally to_index(I) +# 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}}) + of = first(axes(r,1)) - 1 + IdOffsetRange(_subtractoffset(parent(r), of), of) +end + +@inline function _boundscheck_index_retaining_axes(r, s) + @boundscheck checkbounds(r, s) + @inbounds pr = r[UnitRange(s)] + _indexedby(pr, axes(s)) +end +@inline _boundscheck_return(r, s) = (@boundscheck checkbounds(r, s); s) + +for R in [:StepRange, :StepRangeLen, :LinRange, :UnitRange] + @eval @inline Base.getindex(r::$R, s::IdOffsetRange) = _boundscheck_index_retaining_axes(r, s) +end + +# this method is needed for ambiguity resolution +@eval @inline function Base.getindex(r::StepRangeLen{T,<:Base.TwicePrecision,<:Base.TwicePrecision}, s::IdOffsetRange) where T + _boundscheck_index_retaining_axes(r, s) +end + +Base.getindex(r::Base.OneTo, s::IdOffsetRange) = _boundscheck_index_retaining_axes(r, s) + +# These methods are added to avoid ambiguities with Base. +# The ones involving Base types should be ported to Base and version-limited here +@inline Base.getindex(r::IdentityUnitRange, s::IdOffsetRange) = _boundscheck_return(r, s) +if IdentityUnitRange !== Base.Slice + @inline Base.getindex(r::Base.Slice, s::IdOffsetRange) = _boundscheck_return(r, s) +end + +# eltype conversion +# This may use specialized map methods for the parent +Base.map(::Type{T}, O::OffsetArray) where {T} = parent_call(x -> map(T, x), O) +Base.map(::Type{T}, r::IdOffsetRange) where {T<:Real} = _indexedby(map(T, UnitRange(r)), axes(r)) + + +if VERSION < v"1.7.2" + # mapreduce is faster with an IdOffsetRange than with an OffsetUnitRange on Julia 1.6 + # We therefore convert OffsetUnitRanges to IdOffsetRanges with the same values and axes + function Base.mapreduce(f, op, A1::OffsetUnitRange{<:Integer}, As::OffsetUnitRange{<:Integer}...; kw...) + As = (A1, As...) + ofs = map(A -> first(axes(A,1)) - 1, As) + AIds = map((A, of) -> IdOffsetRange(_subtractoffset(parent(A), of), of), As, ofs) + mapreduce(f, op, AIds...; kw...) + end +end + + +# Optimize certain reductions that treat an OffsetVector as a list +for f in [:minimum, :maximum, :extrema, :sum] + @eval Base.$f(r::OffsetRange) = $f(parent(r)) +end + +function Base.show(io::IO, r::OffsetRange) + show(io, r.parent) + print(io, " with indices ", UnitRange(axes(r, 1))) +end +Base.show(io::IO, ::MIME"text/plain", r::OffsetRange) = show(io, r) + + +### Some mutating functions defined only for OffsetVector ### + +Base.resize!(A::OffsetVector, nl::Integer) = (resize!(A.parent, nl); A) +Base.push!(A::OffsetVector, x...) = (push!(A.parent, x...); A) +Base.pop!(A::OffsetVector) = pop!(A.parent) +Base.append!(A::OffsetVector, items) = (append!(A.parent, items); A) +Base.empty!(A::OffsetVector) = (empty!(A.parent); A) + + +# These functions keep the summary compact +const OffsetIndices = Union{IdOffsetRange, IdentityUnitRange{<:IdOffsetRange}} +function Base.inds2string(inds::Tuple{OffsetIndices, Vararg{OffsetIndices}}) + Base.inds2string(map(UnitRange, inds)) +end +Base.showindices(io::IO, ind1::IdOffsetRange, inds::IdOffsetRange...) = Base.showindices(io, map(UnitRange, (ind1, inds...))...) + +function Base.showarg(io::IO, @nospecialize(a::OffsetArray), toplevel) + print(io, "OffsetArray(") + Base.showarg(io, parent(a), false) + Base.showindices(io, axes(a)...) + print(io, ')') + if toplevel + print(io, " with eltype ", eltype(a)) + end +end + +function Base.replace_in_print_matrix(A::OffsetArray{<:Any,2}, i::Integer, j::Integer, s::AbstractString) + J = map(parentindex, axes(A), (i,j)) + Base.replace_in_print_matrix(parent(A), J..., s) +end +function Base.replace_in_print_matrix(A::OffsetArray{<:Any,1}, i::Integer, j::Integer, s::AbstractString) + ip = parentindex(axes(A,1), i) + Base.replace_in_print_matrix(parent(A), ip, j, s) +end + + +# Actual unsafe_wrap implementation +@inline function _unsafe_wrap(pointer::Ptr{T}, inds::NTuple{N, IntegerOrAbsUnitRange}; own = false, kw...) where {T,N} + _checkindices(N, inds, "indices") + AA = Base.unsafe_wrap(Array, pointer, map(_indexlength, inds); own=own) + OffsetArray{T, N, typeof(AA)}(AA, map(_indexoffset, inds); kw...) +end +const OffsetArrayUnion{T,N} = Union{Type{OffsetArray}, Type{OffsetArray{T}}, Type{OffsetArray{T,N}}, Type{OffsetArray{T1, N} where T1}} where {T,N} + +""" + Base.unsafe_wrap(OffsetArray, pointer::Ptr{T}, inds...; own=false, kw...) + Base.unsafe_wrap(OffsetArray, pointer::Ptr{T}, inds::Tuple; own=false, kw...) + +Construct an `OffsetArray` around a pointer with axes defined by `inds` which may be Integers or AbstractUnitRanges. +If the keyword `own` is true, then Julia will free the pointer when the array is garbage collected. +Other keywords are forwarded to the `OffsetArray` constructor. +""" +@inline function Base.unsafe_wrap(::OffsetArrayUnion{T,N}, pointer::Ptr{T}, inds::NTuple{N, IntegerOrAbsUnitRange}; kw...) where {T,N} + _unsafe_wrap(pointer, inds; kw...) +end +# Avoid ambiguity +@inline function Base.unsafe_wrap(::OffsetArrayUnion{T,N}, pointer::Ptr{T}, inds::NTuple{N, <:Integer}; kw...) where {T,N} + _unsafe_wrap(pointer, inds; kw...) +end +@inline function Base.unsafe_wrap(::OffsetArrayUnion{T,N}, pointer::Ptr{T}, inds::Vararg{IntegerOrAbsUnitRange,N}; kw...) where {T,N} + _unsafe_wrap(pointer, inds; kw...) +end +# Avoid ambiguity +@inline function Base.unsafe_wrap(::OffsetArrayUnion{T,N}, pointer::Ptr{T}, inds::Vararg{Integer,N}; kw...) where {T,N} + _unsafe_wrap(pointer, inds; kw...) +end + + +""" + no_offset_view(A) + +Return an `AbstractArray` that shares structure and underlying data with the argument, +but uses 1-based indexing. May just return the argument when applicable. +Not exported. + +The default implementation uses `OffsetArraysCore`, but other types should use something more +specific to remove a level of indirection when applicable. + +```jldoctest; setup=:(using OffsetArraysCore) +julia> A = [1 3 5; 2 4 6]; + +julia> O = OffsetArray(A, 0:1, -1:1) +2×3 OffsetArray(::$(Matrix{Int}), 0:1, -1:1) with eltype $Int with indices 0:1×-1:1: + 1 3 5 + 2 4 6 + +julia> OffsetArraysCore.no_offset_view(O)[1,1] = -9 +-9 + +julia> A +2×3 $(Matrix{Int}): + -9 3 5 + 2 4 6 +``` +""" +no_offset_view(A::OffsetArray) = no_offset_view(parent(A)) +if isdefined(Base, :IdentityUnitRange) + # valid only if Slice is distinguished from IdentityUnitRange + _onebasedslice(S::Base.Slice) = Base.Slice(Base.OneTo(length(S))) + _onebasedslice(S::Base.Slice{<:Base.OneTo}) = S + _onebasedslice(S) = S + _isoffsetslice(::Any) = false + _isoffsetslice(::Base.Slice) = true + _isoffsetslice(::Base.Slice{<:Base.OneTo}) = false + function no_offset_view(S::SubArray) + #= If a view contains an offset Slice axis, + i.e. it is a view of an offset array along the offset axis, + we shift the axis to a 1-based one. + E.g. Slice(2:3) -> Slice(Base.OneTo(2)) + We transform the `parent` as well as the `parentindices`, + so that the view still points to the same elements, even though the indices have changed. + This way, we retain the axis of the view as a `Slice` + =# + P = parent(S) + pinds = parentindices(S) + #= + Check if all the axes are `Slice`s and the parent has `OneTo` axes, + in which case we may unwrap the `OffsetArray` and forward the view to the parent. + =# + may_pop_parent = all(_isoffsetslice, pinds) && P isa OffsetArray && all(x -> x isa Base.OneTo, axes(parent(P))) + if may_pop_parent + return no_offset_view(P) + end + #= + we convert offset `Slice`s to 1-based ones using `_onebasedslice`. + The next call, `no_offset_view`, is a no-op on a `Slice{<:OneTo}`, + while it converts the offset axes to 1-based ones. + Eventually, we end up with a `Tuple` comprising `Slice{<:OneTo}`s and other 1-based axes. + + The difference between `_onebasedslice` and `no_offset_view` is that + the latter does not change the value of the range, while the former does. + =# + newviewinds = map(no_offset_view ∘ _onebasedslice, pinds) + needs_shifting = any(_isoffsetslice, pinds) + P_maybeshiftedinds = if needs_shifting + t = Origin(parent(S)).index + neworigin = ntuple(i -> _isoffsetslice(pinds[i]) ? 1 : t[i], length(t)) + Origin(neworigin)(P) + else + P + end + view(P_maybeshiftedinds, newviewinds...) + end +end +no_offset_view(a::Array) = a +no_offset_view(i::Number) = i +no_offset_view(A::AbstractArray) = _no_offset_view(axes(A), A) +_no_offset_view(::Tuple{}, A::AbstractArray{T,0}) where T = A +_no_offset_view(::Tuple{Base.OneTo, Vararg{Base.OneTo}}, A::AbstractArray) = A +_no_offset_view(::Any, A::AbstractArray) = _no_offset_view(A) +_no_offset_view(A::AbstractArray) = OffsetArray(A, Origin(1)) +_no_offset_view(A::AbstractUnitRange) = UnitRange(A) + + +##### +# center/centered +# These two helpers are deliberately not exported; their meaning can be very different in +# other scenarios and will be very likely to cause name conflicts if exported. +##### + +if VERSION < v"1.4" + _halfroundInt(v, r::RoundingMode) = round(Int, v/2, r) +else + _halfroundInt(v, r::RoundingMode) = div(v, 2, r) +end + +""" + center(A, [r::RoundingMode=RoundDown])::Dims + +Return the center coordinate of given array `A`. If `size(A, k)` is even, +a rounding procedure will be applied with mode `r`. + +!!! compat "OffsetArraysCore 1.9" + This method requires at least OffsetArraysCore 1.9. + +# Examples + +```jldoctest; setup=:(using OffsetArraysCore) +julia> A = reshape(collect(1:9), 3, 3) +3×3 $(Matrix{Int}): + 1 4 7 + 2 5 8 + 3 6 9 + +julia> c = OffsetArraysCore.center(A) +(2, 2) + +julia> A[c...] +5 + +julia> Ao = OffsetArray(A, -2, -2); # axes (-1:1, -1:1) + +julia> c = OffsetArraysCore.center(Ao) +(0, 0) + +julia> Ao[c...] +5 +``` + +To shift the center coordinate of the given array to `(0, 0, ...)`, you +can use [`centered`](@ref OffsetArraysCore.centered). +""" +function center(A::AbstractArray, r::RoundingMode=RoundDown) + map(axes(A)) do inds + _halfroundInt(length(inds)-1, r) + first(inds) + end +end + +""" + centered(A, cp=center(A)) -> Ao + +Shift the center coordinate/point `cp` of array `A` to `(0, 0, ..., 0)`. Internally, this is +equivalent to `OffsetArray(A, .-cp)`. + +!!! compat "OffsetArraysCore 1.9" + This method requires at least OffsetArraysCore 1.9. + +# Examples + +```jldoctest; setup=:(using OffsetArraysCore) +julia> A = reshape(collect(1:9), 3, 3) +3×3 $(Matrix{Int}): + 1 4 7 + 2 5 8 + 3 6 9 + +julia> Ao = OffsetArraysCore.centered(A); # axes (-1:1, -1:1) + +julia> Ao[0, 0] +5 + +julia> Ao = OffsetArray(A, OffsetArraysCore.Origin(0)); # axes (0:2, 0:2) + +julia> Aoo = OffsetArraysCore.centered(Ao); # axes (-1:1, -1:1) + +julia> Aoo[0, 0] +5 +``` + +Users are allowed to pass `cp` to change how "center point" is interpreted, but the meaning of the +output array should be reinterpreted as well. For instance, if `cp = map(last, axes(A))` then this +function no longer shifts the center point but instead the bottom-right point to `(0, 0, ..., 0)`. +A commonly usage of `cp` is to change the rounding behavior when the array is of even size at some +dimension: + +```jldoctest; setup=:(using OffsetArraysCore) +julia> A = reshape(collect(1:4), 2, 2) # Ideally the center should be (1.5, 1.5) but OffsetArraysCore only support integer offsets +2×2 $(Matrix{Int}): + 1 3 + 2 4 + +julia> OffsetArraysCore.centered(A, OffsetArraysCore.center(A, RoundUp)) # set (2, 2) as the center point +2×2 OffsetArray(::$(Matrix{Int}), -1:0, -1:0) with eltype $(Int) with indices -1:0×-1:0: + 1 3 + 2 4 + +julia> OffsetArraysCore.centered(A, OffsetArraysCore.center(A, RoundDown)) # set (1, 1) as the center point +2×2 OffsetArray(::$(Matrix{Int}), 0:1, 0:1) with eltype $(Int) with indices 0:1×0:1: + 1 3 + 2 4 +``` + +See also [`center`](@ref OffsetArraysCore.center). +""" +centered(A::AbstractArray, cp::Dims=center(A)) = OffsetArray(A, .-cp) + +centered(A::AbstractArray, i::CartesianIndex) = centered(A, Tuple(i)) + + + +if VERSION < v"1.12.0-DEV.1713" + # The Base implementations are fixed in https://github.com/JuliaLang/julia/pull/56464 and https://github.com/JuliaLang/julia/pull/56474 + # we therefore limit these specializations to older versions of julia + + # we may pass the searchsorted* functions to the parent, and wrap the offset + for f in [:searchsortedfirst, :searchsortedlast, :searchsorted] + _safe_f = Symbol("_safe_" * String(f)) + @eval function $_safe_f(v::OffsetVector, x, ilo, ihi, o::Base.Ordering) + offset = v.offsets[1] + $f(parent(v), x, ilo - offset, ihi - offset, o) .+ offset + end + @eval Base.$f(v::OffsetVector, x, ilo::T, ihi::T, o::Base.Ordering) where T<:Integer = + $_safe_f(v, x, ilo, ihi, o) + end + + if VERSION <= v"1.2" + # ambiguity warnings in earlier versions + for f in [:searchsortedfirst, :searchsortedlast, :searchsorted] + _safe_f = Symbol("_safe_" * String(f)) + @eval Base.$f(v::OffsetVector, x, ilo::Int, ihi::Int, o::Base.Ordering) = + $_safe_f(v, x, ilo, ihi, o) + end + end +end + + +if VERSION < v"1.1.0-DEV.783" + Base.copyfirst!(dest::OffsetArray, src::OffsetArray) = (maximum!(parent(dest), parent(src)); return dest) +end + +if VERSION <= v"1.7.0-DEV.400" + # https://github.com/JuliaLang/julia/pull/39393 + # index for zero-argument getindex should be first linear index instead of 1 (#194) + Base._to_linear_index(A::OffsetArray) = first(LinearIndices(A)) +end + +if !isdefined(Base, :get_extension) + include("../ext/OffsetArraysAdaptExt.jl") +end + + +## +# Deprecations +## + +# This is a bad API design as it introduces counter intuitive results (#250) +@deprecate centered(A::AbstractArray, r::RoundingMode) OffsetArray(A, .-center(A, r)) false + +end # module OffsetArraysCore diff --git a/OffsetArraysCore/src/axes.jl b/OffsetArraysCore/src/axes.jl new file mode 100644 index 0000000..7762e5c --- /dev/null +++ b/OffsetArraysCore/src/axes.jl @@ -0,0 +1,295 @@ +""" + ro = IdOffsetRange(r::AbstractUnitRange, offset=0) + +Construct an "identity offset range". Numerically, `collect(ro) == collect(r) .+ offset`, +with the additional property that `axes(ro, 1) = axes(r, 1) .+ offset`. +When `r` starts at 1, then `ro[i] == i` and even `ro[ro] == ro`, +i.e., it's the "identity," which is the origin of the "Id" in `IdOffsetRange`. + +# Examples + +The most common case is shifting a range that starts at 1 (either `1:n` or `Base.OneTo(n)`): +```jldoctest ior +julia> using OffsetArraysCore: IdOffsetRange + +julia> ro = IdOffsetRange(1:3, -2) +IdOffsetRange(values=-1:1, indices=-1:1) + +julia> axes(ro, 1) +IdOffsetRange(values=-1:1, indices=-1:1) + +julia> ro[-1] +-1 + +julia> ro[3] +ERROR: BoundsError: attempt to access 3-element IdOffsetRange{$Int, UnitRange{$Int}} with indices -1:1 at index [3] +``` + +If the range doesn't start at 1, the values may be different from the indices: +```jldoctest ior +julia> ro = IdOffsetRange(11:13, -2) +IdOffsetRange(values=9:11, indices=-1:1) + +julia> axes(ro, 1) # 11:13 is indexed by 1:3, and the offset is also applied to the axes +IdOffsetRange(values=-1:1, indices=-1:1) + +julia> ro[-1] +9 + +julia> ro[3] +ERROR: BoundsError: attempt to access 3-element IdOffsetRange{$Int, UnitRange{$Int}} with indices -1:1 at index [3] +``` + +# Extended help + +Construction/coercion preserves the (shifted) values of the input range, but may modify +the indices if required by the specified types. For example, + + r = OffsetArraysCore.IdOffsetRange{Int,UnitRange{Int}}(3:4) + +has `r[1] == 3` and `r[2] == 4`, whereas + + r = OffsetArraysCore.IdOffsetRange{Int,Base.OneTo{Int}}(3:4) + +has `r[3] == 3` and `r[4] == 4`, and `r[1]` would throw a `BoundsError`. +In this latter case, a shift in the axes was needed because `Base.OneTo` ranges +must start with value 1. + +!!! warning + + In the future, *conversion* will preserve both the values and + the indices, throwing an error when this is not achievable. For instance, + + r = convert(OffsetArraysCore.IdOffsetRange{Int,UnitRange{Int}}, 3:4) + + has `r[1] == 3` and `r[2] == 4` and would satisfy `r == 3:4`, whereas + + ```julia + julia> convert(OffsetArraysCore.IdOffsetRange{Int,Base.OneTo{Int}}, 3:4) # future behavior, not present behavior + ERROR: ArgumentError: first element must be 1, got 3 + ``` + + where the error will arise because the result could not have the same axes as the input. + + An important corollary is that `typeof(r1)(r2)` and `oftype(r1, r2)` will behave differently: + the first coerces `r2` to be of the type of `r1`, whereas the second converts. + Developers are urged to future-proof their code by choosing the behavior appropriate for each usage. +""" +struct IdOffsetRange{T<:Integer,I<:AbstractUnitRange{T}} <: AbstractUnitRange{T} + parent::I + offset::T + + function IdOffsetRange{T,I}(r::I, offset::T) where {T<:Integer,I<:AbstractUnitRange{T}} + _bool_check(T, r, offset) + new{T,I}(r, offset) + end + + #= This method is necessary to avoid a StackOverflowError in IdOffsetRange{T,I}(r::IdOffsetRange, offset::Integer). + The type signature in that method is more specific than IdOffsetRange{T,I}(r::I, offset::T), + so it ends up calling itself if I <: IdOffsetRange. + =# + function IdOffsetRange{T,IdOffsetRange{T,I}}(r::IdOffsetRange{T,I}, offset::T) where {T<:Integer,I<:AbstractUnitRange{T}} + _bool_check(T, r, offset) + new{T,IdOffsetRange{T,I}}(r, offset) + end +end + +function _bool_check(::Type{Bool}, r, offset) + # disallow the construction of IdOffsetRange{Bool, UnitRange{Bool}}(true:true, true) + if offset && (first(r) || last(r)) + throw(ArgumentError("values = $r and offset = $offset can not produce a boolean range")) + end + return nothing +end +_bool_check(::Type, r, offset) = nothing + +# Construction/coercion from arbitrary AbstractUnitRanges +function IdOffsetRange{T,I}(r::AbstractUnitRange, offset::Integer = 0) where {T<:Integer,I<:AbstractUnitRange{T}} + rc, o = offset_coerce(I, r) + return IdOffsetRange{T,I}(rc, convert(T, o+offset)::T) +end +function IdOffsetRange{T}(r::AbstractUnitRange, offset::Integer = 0) where T<:Integer + rc = convert(AbstractUnitRange{T}, r)::AbstractUnitRange{T} + return IdOffsetRange{T,typeof(rc)}(rc, convert(T, offset)::T) +end +IdOffsetRange(r::AbstractUnitRange{T}, offset::Integer = 0) where T<:Integer = + IdOffsetRange{T,typeof(r)}(r, convert(T, offset)::T) + +# Coercion from other IdOffsetRanges +IdOffsetRange{T,I}(r::IdOffsetRange{T,I}) where {T<:Integer,I<:AbstractUnitRange{T}} = r +function IdOffsetRange{T,I}(r::IdOffsetRange, offset::Integer = 0) where {T<:Integer,I<:AbstractUnitRange{T}} + rc, offset_rc = offset_coerce(I, r.parent) + return IdOffsetRange{T,I}(rc, convert(T, r.offset + offset + offset_rc)::T) +end +IdOffsetRange{T}(r::IdOffsetRange{T}) where {T<:Integer} = r +function IdOffsetRange{T}(r::IdOffsetRange, offset::Integer = 0) where T<:Integer + return IdOffsetRange{T}(r.parent, r.offset + offset) +end +IdOffsetRange(r::IdOffsetRange) = r + +# Constructor to make `show` round-trippable +# try to preserve typeof(values) if the indices are known to be 1-based +_subtractindexoffset(values, indices::Union{Base.OneTo, IdentityUnitRange{<:Base.OneTo}}, offset) = values +_subtractindexoffset(values, indices, offset) = _subtractoffset(values, offset) +function IdOffsetRange(; values::AbstractUnitRange{<:Integer}, indices::AbstractUnitRange{<:Integer}) + length(values) == length(indices) || throw(ArgumentError("values and indices must have the same length")) + values_nooffset = no_offset_view(values) + offset = first(indices) - 1 + values_minus_offset = _subtractindexoffset(values_nooffset, indices, offset) + return IdOffsetRange(values_minus_offset, offset) +end + +# Conversions to an AbstractUnitRange{Int} (and to an OrdinalRange{Int,Int} on Julia v"1.6") are necessary +# to evaluate CartesianIndices for BigInt ranges, as their axes are also BigInt ranges +Base.AbstractUnitRange{T}(r::IdOffsetRange) where {T<:Integer} = IdOffsetRange{T}(r) + +# https://github.com/JuliaLang/julia/pull/40038 +if v"1.6" <= VERSION < v"1.9.0-DEV.642" + Base.OrdinalRange{T,T}(r::IdOffsetRange) where {T<:Integer} = IdOffsetRange{T}(r) +end + +# TODO: uncomment these when Julia is ready +# # Conversion preserves both the values and the indices, throwing an InexactError if this +# # is not possible. +# Base.convert(::Type{IdOffsetRange{T,I}}, r::IdOffsetRange{T,I}) where {T<:Integer,I<:AbstractUnitRange{T}} = r +# Base.convert(::Type{IdOffsetRange{T,I}}, r::IdOffsetRange) where {T<:Integer,I<:AbstractUnitRange{T}} = +# IdOffsetRange{T,I}(convert(I, r.parent), r.offset) +# Base.convert(::Type{IdOffsetRange{T,I}}, r::AbstractUnitRange) where {T<:Integer,I<:AbstractUnitRange{T}} = +# IdOffsetRange{T,I}(convert(I, r), 0) + +offset_coerce(::Type{Base.OneTo{T}}, r::Base.OneTo) where T<:Integer = convert(Base.OneTo{T}, r), 0 +function offset_coerce(::Type{Base.OneTo{T}}, r::AbstractUnitRange) where T<:Integer + o = first(r) - 1 + return Base.OneTo{T}(last(r) - o), o +end +# function offset_coerce(::Type{Base.OneTo{T}}, r::IdOffsetRange) where T<:Integer +# rc, o = offset_coerce(Base.OneTo{T}, r.parent) + +# Fallback, specialze this method if `convert(I, r)` doesn't do what you need +offset_coerce(::Type{I}, r::AbstractUnitRange) where I<:AbstractUnitRange = + convert(I, r)::I, 0 + +@inline Base.parent(r::IdOffsetRange) = r.parent +@inline Base.axes(r::IdOffsetRange) = (axes1(r),) +@inline axes1(r::IdOffsetRange) = IdOffsetRange(Base.axes1(r.parent), r.offset) +if VERSION < v"1.8.2" + Base.axes1(r::IdOffsetRange) = axes1(r) +end +@inline Base.unsafe_indices(r::IdOffsetRange) = (axes1(r),) +@inline Base.length(r::IdOffsetRange) = length(r.parent) +@inline Base.isempty(r::IdOffsetRange) = isempty(r.parent) +#= We specialize on reduced_indices to work around cases where the parent axis type doesn't +support reduced_index, but the axes do support reduced_indices +The difference is that reduced_index expects the axis type to remain unchanged, +which may not always be possible, eg. for statically sized axes +See https://github.com/JuliaArrays/OffsetArraysCore.jl/issues/204 +=# +function Base.reduced_indices(inds::Tuple{IdOffsetRange, Vararg{IdOffsetRange}}, d::Int) + parents_reduced = Base.reduced_indices(map(parent, inds), d) + ntuple(i -> IdOffsetRange(parents_reduced[i], inds[i].offset), Val(length(inds))) +end +Base.reduced_index(i::IdOffsetRange) = typeof(i)(first(i):first(i)) +# Workaround for #92 on Julia < 1.4 +Base.reduced_index(i::IdentityUnitRange{<:IdOffsetRange}) = typeof(i)(first(i):first(i)) +if VERSION < v"1.8.2" + for f in [:firstindex, :lastindex] + @eval @inline Base.$f(r::IdOffsetRange) = $f(r.parent) + r.offset + end +end +for f in [:first, :last] + # coerce the type to deal with values that get promoted on addition (eg. Bool) + @eval @inline Base.$f(r::IdOffsetRange) = eltype(r)($f(r.parent) + r.offset) +end + +# Iteration for an IdOffsetRange +@inline Base.iterate(r::IdOffsetRange, i...) = _iterate(r, i...) +# In general we iterate over the parent term by term and add the offset. +# This might have some performance degradation when coupled with bounds-checking +# See https://github.com/JuliaArrays/OffsetArraysCore.jl/issues/214 +@inline function _iterate(r::IdOffsetRange, i...) + ret = iterate(r.parent, i...) + ret === nothing && return nothing + return (eltype(r)(ret[1] + r.offset), ret[2]) +end +# Base.OneTo(n) is known to be exactly equivalent to the range 1:n, +# and has no specialized iteration defined for it, +# so we may add the offset to the range directly and iterate over the result +# This gets around the performance issue described in issue #214 +# We use the helper function _addoffset to evaluate the range instead of broadcasting +# just in case this makes it easy for the compiler. +@inline _iterate(r::IdOffsetRange{<:Integer, <:Base.OneTo}, i...) = iterate(_addoffset(r.parent, r.offset), i...) + +@inline function Base.getindex(r::IdOffsetRange, i::Integer) + i isa Bool && throw(ArgumentError("invalid index: $i of type Bool")) + @boundscheck checkbounds(r, i) + @inbounds eltype(r)(r.parent[i - r.offset] + r.offset) +end + +# Logical indexing following https://github.com/JuliaLang/julia/pull/31829 +#= Helper function to perform logical indxeing for boolean ranges +The code implemented is a branch-free version of the following: + + range(first(s) ? first(r) : last(r), length=Int(last(s))) + +See https://github.com/JuliaArrays/OffsetArraysCore.jl/pull/224#discussion_r595635143 + +Logical indexing does not preserve indices, unlike other forms of vector indexing +=# +@inline function _getindex(r, s::AbstractUnitRange{Bool}) + range(first(r) * first(s) + last(r) * !first(s), length=Int(last(s))) +end +@inline function _getindex(r, s::StepRange{Bool}) + range(first(r) * first(s) + last(r) * !first(s), step = oneunit(step(s)), length=Int(last(s))) +end +@inline function _getindex(r, s::AbstractUnitRange) + @inbounds rs = r.parent[_subtractoffset(s, r.offset)] .+ r.offset + _indexedby(rs, axes(s)) +end +@inline function _getindex(r, s::StepRange) + rs = @inbounds r.parent[s .- r.offset] .+ r.offset + _indexedby(rs, axes(s)) +end + +for T in [:AbstractUnitRange, :StepRange] + @eval @inline function Base.getindex(r::IdOffsetRange, s::$T{<:Integer}) + @boundscheck checkbounds(r, s) + return _getindex(r, s) + end +end + +# These methods are necessary to avoid ambiguity +for R in [:IIUR, :IdOffsetRange] + @eval @inline function Base.getindex(r::IdOffsetRange, s::$R) + @boundscheck checkbounds(r, s) + return _getindex(r, s) + end +end + +# offset-preserve broadcasting +Broadcast.broadcasted(::Base.Broadcast.DefaultArrayStyle{1}, ::typeof(-), r::IdOffsetRange, x::Integer) = + IdOffsetRange(r.parent .- x, r.offset) +Broadcast.broadcasted(::Base.Broadcast.DefaultArrayStyle{1}, ::typeof(+), r::IdOffsetRange, x::Integer) = + IdOffsetRange(r.parent .+ x, r.offset) +Broadcast.broadcasted(::Base.Broadcast.DefaultArrayStyle{1}, ::typeof(+), x::Integer, r::IdOffsetRange) = + IdOffsetRange(x .+ r.parent, r.offset) +Broadcast.broadcasted(::Base.Broadcast.DefaultArrayStyle{1}, ::typeof(big), r::IdOffsetRange) = + IdOffsetRange(big.(r.parent), r.offset) + +Base.show(io::IO, r::IdOffsetRange) = print(io, IdOffsetRange, "(values=",first(r), ':', last(r),", indices=",first(eachindex(r)),':',last(eachindex(r)), ")") + +# Optimizations +@inline Base.checkindex(::Type{Bool}, inds::IdOffsetRange, i::Real) = Base.checkindex(Bool, inds.parent, i - inds.offset) + +if VERSION < v"1.5.2" + # issue 100, 133: IdOffsetRange as another index-preserving case shouldn't comtribute offsets + # fixed by https://github.com/JuliaLang/julia/pull/37204 + @inline Base.compute_offset1(parent, stride1::Integer, dims::Tuple{Int}, inds::Tuple{IdOffsetRange}, I::Tuple) = + Base.compute_linindex(parent, I) - stride1*first(Base.axes1(inds[1])) +end + +# This was deemed "too private" to extend: see issue #184 +# # Fixes an inference failure in Base.mapfirst! +# # Test: A = OffsetArray(rand(4,4), (-3,5)); R = similar(A, (1:1, 6:9)); maximum!(R, A) +# if isdefined(Base, :_firstslice) +# Base._firstslice(i::IdOffsetRange) = IdOffsetRange(Base._firstslice(i.parent), i.offset) +# end diff --git a/OffsetArraysCore/src/origin.jl b/OffsetArraysCore/src/origin.jl new file mode 100644 index 0000000..0454817 --- /dev/null +++ b/OffsetArraysCore/src/origin.jl @@ -0,0 +1,91 @@ +""" + Origin(indices...) + Origin(origin::Tuple) + Origin(origin::CartesianIndex) + +A helper type to construct OffsetArray with a given origin. This is not exported. + +The `origin` of an array is defined as the tuple of the first index along each axis, i.e., `first.(axes(A))`. + +# Example + +```jldoctest origin; setup=:(using OffsetArraysCore) +julia> a = [1 2; 3 4]; + +julia> using OffsetArraysCore: Origin + +julia> OffsetArray(a, Origin(0, 1)) +2×2 OffsetArray(::$(Array{Int,2}), 0:1, 1:2) with eltype $Int with indices 0:1×1:2: + 1 2 + 3 4 + +julia> OffsetArray(a, Origin(0)) # short notation for `Origin(0, 0)` +2×2 OffsetArray(::$(Array{Int, 2}), 0:1, 0:1) with eltype $Int with indices 0:1×0:1: + 1 2 + 3 4 +``` + +An `Origin` object is callable, and it may shift the origin of an array to the specified point. + +```jldoctest origin +julia> b = Origin(0)(a) # shift the origin of the array to (0,0) +2×2 OffsetArray(::$(Array{Int, 2}), 0:1, 0:1) with eltype $Int with indices 0:1×0:1: + 1 2 + 3 4 +``` + +The type `Origin`, when called with an `AbstractArray` as the argument, will return an instance +corresponding ot the origin of the array. + +```jldoctest origin +julia> origin_b = Origin(b) # retrieve the origin of the array as an Origin instance +Origin(0, 0) + +julia> origin_b(ones(2,2)) # shift the origin of another array to that of b, in this case to (0,0) +2×2 OffsetArray(::$(Array{Float64, 2}), 0:1, 0:1) with eltype Float64 with indices 0:1×0:1: + 1.0 1.0 + 1.0 1.0 +``` + +!!! tip + One may broadcast an `Origin` instance over multiple arrays to shift them all to the same origin. + ```jldoctest + julia> using OffsetArraysCore: Origin + + julia> a = [1 2; 3 4]; # origin at (1,1) + + julia> b = Origin(2,3)(a); # origin at (2,3) + + julia> c = Origin(4)(a); # origin at (4,4) + + julia> ao, bo, co = Origin(0).((a, b, c)); # shift all origins to (0,0) + + julia> first.(axes(ao)) == first.(axes(bo)) == first.(axes(co)) == (0,0) + true + + julia> ao, bo, co = Origin(b).((a, b, c)); # shift all origins to that of b + + julia> first.(axes(ao)) == first.(axes(bo)) == first.(axes(co)) == (2,3) + true + + julia> ao, bo, co = OffsetArray.((a, b, c), Origin(b)); # another way to do the same + + julia> first.(axes(ao)) == first.(axes(bo)) == first.(axes(co)) == (2,3) + true + ``` +""" +struct Origin{T<:Union{Tuple{Vararg{Int}}, Int}} + index::T +end +Origin(I::Tuple{Vararg{Int}}) = Origin{typeof(I)}(I) +Origin(I::Tuple{Vararg{Number}}) = Origin(map(Int, I)) +Origin(I::CartesianIndex) = Origin(Tuple(I)) +Origin(I::Number...) = Origin(I) +# Origin(0) != Origin((0, )) but they work the same with broadcasting +Origin(n::Number) = Origin{Int}(Int(n)) + +Base.Broadcast.broadcastable(o::Origin) = Ref(o) + +_showidx(index::Integer) = "(" * string(index) * ")" +_showidx(index::Tuple) = string(index) +Base.show(io::IO, o::Origin) = print(io, "Origin", _showidx(o.index)) diff --git a/OffsetArraysCore/src/utils.jl b/OffsetArraysCore/src/utils.jl new file mode 100644 index 0000000..0682d98 --- /dev/null +++ b/OffsetArraysCore/src/utils.jl @@ -0,0 +1,144 @@ +const OffsetAxis{T<:Integer} = Union{IdOffsetRange{T}, IdentityUnitRange{<:IdOffsetRange{T}}} +@static if isdefined(Base, :AbstractOneTo) + const IntegerOrOneTo = Union{Integer, Base.AbstractOneTo{<:Integer}} +else + const IntegerOrOneTo = Union{Integer, Base.OneTo} +end +const IntegerOrAbsUnitRange = Union{Integer, AbstractUnitRange{<:Integer}} +const IntegerOrOneToOrOffsetAxis{T<:Integer} = Union{IntegerOrOneTo, OffsetAxis{T}} +const IntegerOrOneToOrOffsetAxisOrColon{T<:Integer} = Union{IntegerOrOneToOrOffsetAxis{T}, Colon} + + +### Low-level utilities ### + +_indexoffset(r::AbstractRange) = first(r) - 1 +_indexoffset(i::Integer) = 0 +_indexlength(r::AbstractRange) = length(r) +_indexlength(i::Integer) = Int(i) +_indexlength(i::Colon) = Colon() + +to_shape(shape) = map(_indexlength, shape) +to_shape(shape::Tuple{Vararg{IntegerOrOneTo}}) = shape + +# utility methods used in reshape +# we don't use _indexlength in this to avoid converting the arguments to Int +_checksize(ind::Integer, s) = ind == s +_checksize(ind::AbstractUnitRange, s) = length(ind) == s + +_toaxis(i::Integer) = Base.OneTo(i) +_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, ::Union{Integer, Colon}) = 1 - first(axparent) + +_offsets(A::AbstractArray) = map(ax -> first(ax) - 1, axes(A)) +_offsets(A::AbstractArray, B::AbstractArray) = map(_offset, axes(B), axes(A)) + +""" + OffsetArraysCore.AxisConversionStyle(typeof(indices)) + +`AxisConversionStyle` declares if `indices` should be converted to a single `AbstractUnitRange{Int}` +or to a `Tuple{Vararg{AbstractUnitRange{Int}}}` while flattening custom types into indices. +This method is called after `to_indices(A::Array, axes(A), indices)` to provide +further information in case `to_indices` does not return a `Tuple` of `AbstractUnitRange{Int}`. + +Custom index types should extend `AxisConversionStyle` and return either `OffsetArray.SingleRange()`, +which is the default, or `OffsetArray.TupleOfRanges()`. In the former case, the type `T` should +define `Base.convert(::Type{AbstractUnitRange{Int}}, ::T)`, whereas in the latter it should define +`Base.convert(::Type{Tuple{Vararg{AbstractUnitRange{Int}}}}, ::T)`. + +An example of the latter is `CartesianIndices`, which is converted to a `Tuple` of +`AbstractUnitRange{Int}` while flattening the indices. + +# Example +```jldoctest; setup=:(using OffsetArraysCore) +julia> struct NTupleOfUnitRanges{N} + x ::NTuple{N, UnitRange{Int}} + end + +julia> Base.to_indices(A, inds, t::Tuple{NTupleOfUnitRanges{N}}) where {N} = t; + +julia> OffsetArraysCore.AxisConversionStyle(::Type{NTupleOfUnitRanges{N}}) where {N} = OffsetArraysCore.TupleOfRanges(); + +julia> Base.convert(::Type{Tuple{Vararg{AbstractUnitRange{Int}}}}, t::NTupleOfUnitRanges) = t.x; + +julia> a = zeros(3, 3); + +julia> inds = NTupleOfUnitRanges((3:5, 2:4)); + +julia> oa = OffsetArray(a, inds); + +julia> axes(oa, 1) == 3:5 +true + +julia> axes(oa, 2) == 2:4 +true +``` +""" +abstract type AxisConversionStyle end +struct SingleRange <: AxisConversionStyle end +struct TupleOfRanges <: AxisConversionStyle end + +AxisConversionStyle(::Type) = SingleRange() +AxisConversionStyle(::Type{<:CartesianIndices}) = TupleOfRanges() + +_convertTupleAbstractUnitRange(x) = _convertTupleAbstractUnitRange(AxisConversionStyle(typeof(x)), x) +_convertTupleAbstractUnitRange(::SingleRange, x) = (convert(AbstractUnitRange{Int}, x),) +_convertTupleAbstractUnitRange(::TupleOfRanges, x) = convert(Tuple{Vararg{AbstractUnitRange{Int}}}, x) + +_toAbstractUnitRanges(t::Tuple) = (_convertTupleAbstractUnitRange(first(t))..., _toAbstractUnitRanges(tail(t))...) +_toAbstractUnitRanges(::Tuple{}) = () + +# ensure that the indices are consistent in the constructor +_checkindices(A::AbstractArray, indices, label) = _checkindices(ndims(A), indices, label) +function _checkindices(N::Integer, indices, label) + throw_argumenterror(N, indices, label) = throw(ArgumentError(label*" $indices are not compatible with a $(N)D array")) + N == length(indices) || throw_argumenterror(N, indices, label) +end + +@inline _indexedby(r::AbstractVector, ax::Tuple{Any}) = _indexedby(r, ax[1]) +@inline _indexedby(r::AbstractUnitRange{<:Integer}, ::Base.OneTo) = no_offset_view(r) +@inline _indexedby(r::AbstractUnitRange{Bool}, ::Base.OneTo) = no_offset_view(r) +@inline _indexedby(r::AbstractVector, ::Base.OneTo) = no_offset_view(r) +@inline function _indexedby(r::AbstractUnitRange{<:Integer}, ax::AbstractUnitRange) + of = convert(eltype(r), first(ax) - 1) + IdOffsetRange(_subtractoffset(r, of), of) +end +@inline _indexedby(r::AbstractUnitRange{Bool}, ax::AbstractUnitRange) = OffsetArray(r, ax) +@inline _indexedby(r::AbstractVector, ax::AbstractUnitRange) = OffsetArray(r, ax) + +_indexedby(A::AbstractArray{<:Any,0}, ::Tuple{}) = no_offset_view(A) +_indexedby(A::AbstractVector, shape::Tuple{IntegerOrOneTo}) = _indexedby(A, shape[1]) +_indexedby(A::AbstractArray{<:Any,N}, ::NTuple{N,IntegerOrOneTo}) where {N} = no_offset_view(A) +_indexedby(A::AbstractVector, shape::Tuple{IntegerOrAbsUnitRange}) = _indexedby(A, shape[1]) +_indexedby(A::AbstractArray{<:Any,N}, shape::NTuple{N,IntegerOrAbsUnitRange}) where {N} = OffsetArray(A, map(_offset, axes(A), shape)) + +# These functions are equivalent to the broadcasted operation r .- of +# However these ensure that the result is an AbstractRange even if a specific +# broadcasting behavior is not defined for a custom type +@inline _subtractoffset(r::AbstractUnitRange, of) = UnitRange(first(r) - of, last(r) - of) +@inline _subtractoffset(r::AbstractRange, of) = range(first(r) - of, stop = last(r) - of, step = step(r)) + +# similar to _subtractoffset, except these evaluate r .+ of +@inline _addoffset(r::AbstractUnitRange, of) = UnitRange(first(r) + of, last(r) + of) +@inline _addoffset(r::AbstractRange, of) = range(first(r) + of, stop = last(r) + of, step = step(r)) + +if VERSION <= v"1.7.0-DEV.1039" + _contiguousindexingtype(r::AbstractUnitRange{<:Integer}) = UnitRange{Int}(r) +else + _contiguousindexingtype(r::AbstractUnitRange{<:Integer}) = r +end + +_of_eltype(::Type{T}, M::AbstractArray{T}) where {T} = M +_of_eltype(T, M::AbstractArray) = map(T, M) + +# filter the arguments to reshape to check if there are any ranges +# If not, we may pop the parent array +_filterreshapeinds(t::Tuple{AbstractUnitRange, Vararg{Any}}) = t +_filterreshapeinds(t::Tuple) = _filterreshapeinds(tail(t)) +_filterreshapeinds(t::Tuple{}) = t +_popreshape(A::AbstractArray, ax::Tuple{Vararg{Base.OneTo}}, inds::Tuple{}) = no_offset_view(A) +_popreshape(A::AbstractArray, ax, inds) = A diff --git a/OffsetArraysCore/test/adapt.jl b/OffsetArraysCore/test/adapt.jl new file mode 100644 index 0000000..8a387e9 --- /dev/null +++ b/OffsetArraysCore/test/adapt.jl @@ -0,0 +1,27 @@ +module AdaptTests + +using OffsetArraysCore +using Adapt +using StaticArrays +using Test + +@testset "Adapt" begin + # We need another storage type, CUDA.jl defines one but we can't use that for CI + # let's define an appropriate method for SArrays + Adapt.adapt_storage(::Type{SA}, xs::Array) where SA<:SArray = convert(SA, xs) # ambiguity + Adapt.adapt_storage(::Type{SA}, xs::AbstractArray) where SA<:SArray = convert(SA, xs) + arr = OffsetArray(rand(3, 3), -1:1, -1:1) + s_arr = adapt(SMatrix{3,3}, arr) + @test parent(s_arr) isa SArray + @test arr == adapt(Array, s_arr) + + arr2 = OffsetArray(view(rand(5, 5), 2:4, 2:4), -1:1, -1:1) + + if isdefined(Adapt, :parent_type) + @test Adapt.parent_type(typeof(arr)) == typeof(arr.parent) + @test Adapt.unwrap_type(typeof(arr)) == typeof(arr.parent) + @test Adapt.unwrap_type(typeof(arr2)) == typeof(arr.parent) + end +end + +end \ No newline at end of file diff --git a/OffsetArraysCore/test/aqua.jl b/OffsetArraysCore/test/aqua.jl new file mode 100644 index 0000000..fd5ab36 --- /dev/null +++ b/OffsetArraysCore/test/aqua.jl @@ -0,0 +1,11 @@ +module AquaTests + +import Aqua +import OffsetArraysCore +using Test + +@testset "Project meta quality checks" begin + Aqua.test_all(OffsetArraysCore, piracies=false) +end + +end \ No newline at end of file diff --git a/OffsetArraysCore/test/axes.jl b/OffsetArraysCore/test/axes.jl new file mode 100644 index 0000000..bc48f70 --- /dev/null +++ b/OffsetArraysCore/test/axes.jl @@ -0,0 +1,373 @@ +module AxesTests + +using OffsetArraysCore +using OffsetArraysCore: IdOffsetRange, IdentityUnitRange, no_offset_view +using Test + +function same_value(r1, r2) + length(r1) == length(r2) || return false + for (v1, v2) in zip(r1, r2) + v1 == v2 || return false + end + return true +end + +no_offset_axes(x, d) = no_offset_view(axes(x, d)) +no_offset_axes(x) = map(no_offset_view, axes(x)) + +@testset "IdOffsetRange" begin + + function check_indexed_by(r, rindx) + for i in rindx + r[i] + end + @test_throws BoundsError r[minimum(rindx)-1] + @test_throws BoundsError r[maximum(rindx)+1] + return nothing + end + + ro = IdOffsetRange(Base.OneTo(3)) + rs = IdOffsetRange(3:5, -2) + @test typeof(ro) !== typeof(rs) + @test same_value(ro, 1:3) + check_indexed_by(ro, 1:3) + @test same_value(rs, 1:3) + check_indexed_by(rs, -1:1) + @test firstindex(ro) == 1 + @test lastindex(ro) == 3 + @test firstindex(rs) == -1 + @test lastindex(rs) == 1 + @test @inferred(typeof(ro)(ro)) === ro + @test @inferred(IdOffsetRange{Int}(ro)) === ro + @test @inferred(IdOffsetRange{Int16}(ro)) === IdOffsetRange(Base.OneTo(Int16(3))) + @test @inferred(IdOffsetRange(ro)) === ro + @test parent(ro) === ro.parent + @test parent(rs) === rs.parent + # construction/coercion preserves the values, altering the axes if needed + r2 = @inferred(typeof(rs)(ro)) + @test typeof(r2) === typeof(rs) + @test same_value(ro, 1:3) + check_indexed_by(ro, 1:3) + r2 = @inferred(typeof(ro)(rs)) + @test typeof(r2) === typeof(ro) + @test same_value(r2, 1:3) + check_indexed_by(r2, 1:3) + # check the example in the comments + r = IdOffsetRange{Int,UnitRange{Int}}(3:4) + @test same_value(r, 3:4) + check_indexed_by(r, 1:2) + r = IdOffsetRange{Int,Base.OneTo{Int}}(3:4) + @test same_value(r, 3:4) + check_indexed_by(r, 3:4) + r = IdOffsetRange{Int,Base.OneTo{Int}}(3:4, -2) + @test same_value(r, 1:2) + check_indexed_by(r, 1:2) + + r = IdOffsetRange{Int32, Base.OneTo{Int32}}(Base.OneTo(Int64(2)), 3) + @test same_value(r, 4:5) + check_indexed_by(r, 4:5) + + r = IdOffsetRange{Int, UnitRange{Int}}(IdOffsetRange(3:5, 2), 2) + @test typeof(r) == IdOffsetRange{Int, UnitRange{Int}} + @test same_value(r, 7:9) + check_indexed_by(r, 5:7) + + r = IdOffsetRange{Int, Base.OneTo{Int}}(IdOffsetRange(Base.OneTo(3), 1), 1) + @test typeof(r) == IdOffsetRange{Int,Base.OneTo{Int}} + @test same_value(r, 3:5) + check_indexed_by(r, 3:5) + + rp = Base.OneTo(3) + r = IdOffsetRange(rp) + r2 = IdOffsetRange{Int,typeof(r)}(r, 1) + @test same_value(r2, 2:4) + check_indexed_by(r2, 2:4) + + r2 = IdOffsetRange{Int32,IdOffsetRange{Int32,Base.OneTo{Int32}}}(r, 1) + @test typeof(r2) == IdOffsetRange{Int32,IdOffsetRange{Int32,Base.OneTo{Int32}}} + @test same_value(r2, 2:4) + check_indexed_by(r2, 2:4) + + # eltype coercion through the AbstractUnitRange constructor + ro = IdOffsetRange(Base.OneTo(3)) + @test @inferred(AbstractUnitRange{Int}(ro)) === ro + rb = IdOffsetRange(Base.OneTo(big(3))) + @test @inferred(AbstractUnitRange{Int}(rb)) === IdOffsetRange(Base.OneTo(3)) + + # Constructor that's round-trippable with `show` + rrt = IdOffsetRange(values=7:9, indices=-1:1) + @test same_value(rrt, 7:9) + check_indexed_by(rrt, -1:1) + @test_throws ArgumentError IdOffsetRange(values=7:9, indices=-1:2) + @test_throws ArgumentError IdOffsetRange(values=7:9, indices=-1:0) + @test_throws TypeError IdOffsetRange(values=7:9, indices=-1) + @test_throws UndefKeywordError IdOffsetRange(values=7:9) + @test_throws UndefKeywordError IdOffsetRange(indices=-1:1) + @test_throws MethodError IdOffsetRange(7:9, indices=-1:1) + @test_throws MethodError IdOffsetRange(-1:1, values=7:9) + + p = IdOffsetRange(1:3, 2) + q = IdOffsetRange(values = p .- 2, indices = p) + @test same_value(q, 1:3) + check_indexed_by(q, p) + + @testset for indices in Any[Base.OneTo(3), IdentityUnitRange(Base.OneTo(3))] + p = IdOffsetRange(values = IdOffsetRange(1:3, 2), indices = indices) + @test same_value(p, 3:5) + check_indexed_by(p, 1:3) + q = IdOffsetRange(values = Base.OneTo(3), indices = indices) + @test same_value(q, 1:3) + @test q isa IdOffsetRange{Int, Base.OneTo{Int}} + end + + # conversion preserves both the values and the axes, throwing an error if this is not possible + @test @inferred(oftype(ro, ro)) === ro + @test @inferred(convert(IdOffsetRange{Int}, ro)) === ro + @test @inferred(convert(IdOffsetRange{Int}, rs)) === rs + @test @inferred(convert(IdOffsetRange{Int16}, ro)) === IdOffsetRange(Base.OneTo(Int16(3))) + r2 = @inferred(oftype(rs, ro)) + @test typeof(r2) === typeof(rs) + @test same_value(r2, 1:3) + check_indexed_by(r2, 1:3) + # These two broken tests can be fixed by uncommenting the `convert` definitions + # in axes.jl, but unfortunately Julia may not quite be ready for this. (E.g. `reinterpretarray.jl`) + @test_broken try oftype(ro, rs); false catch err true end # replace with line below + # @test_throws ArgumentError oftype(ro, rs) + @test @inferred(oftype(ro, Base.OneTo(2))) === IdOffsetRange(Base.OneTo(2)) + @test @inferred(oftype(ro, 1:2)) === IdOffsetRange(Base.OneTo(2)) + @test_broken try oftype(ro, 3:4); false catch err true end + # @test_throws ArgumentError oftype(ro, 3:4) + + # broadcasting behavior with scalars (issue #104) + r3 = (1 .+ IdOffsetRange(3:5, -1) .+ 1) .- 1 + @test r3 isa IdOffsetRange + @test same_value(r3, 3:5) + check_indexed_by(r3, axes(r3,1)) + + r = IdOffsetRange(3:5, -1) + rc = copyto!(similar(r), r) + n = big(typemax(Int)) + @test @inferred(broadcast(+, r, n)) == @inferred(broadcast(+, n, r)) == rc .+ n + @test @inferred(broadcast(-, r, n)) == rc .- n + @test @inferred(broadcast(big, r)) == big.(rc) + for n in Any[2, big(typemax(Int))] + @test @inferred(broadcast(+, r, n)) == @inferred(broadcast(+, n, r)) == rc .+ n + end + + @testset "Idempotent indexing" begin + @testset "Indexing into an IdOffsetRange" begin + r = IdOffsetRange(3:5, -1) + # Indexing with IdentityUnitRange + s = IdentityUnitRange(0:2) + @test axes(r[s]) == axes(s) + for i in eachindex(s) + @test r[s[i]] == r[s][i] + end + + # Indexing with IdOffsetRange + s = IdOffsetRange(-4:-2, 4) + @test axes(r[s]) == axes(s) + for i in eachindex(s) + @test r[s[i]] == r[s][i] + end + + # Indexing with UnitRange + s = 0:2 + @test axes(r[s]) == axes(s) + for i in eachindex(s) + @test r[s[i]] == r[s][i] + end + end + @testset "Indexing using an IdOffsetRange" begin + r = IdOffsetRange(3:5, -1) + # Indexing into an IdentityUnitRange + s = IdentityUnitRange(-1:5) + @test axes(s[r]) == axes(r) + for i in eachindex(r) + @test s[r[i]] == s[r][i] + end + + # Indexing into an UnitRange + s = -3:6 + @test axes(s[r]) == axes(r) + for i in eachindex(r) + @test s[r[i]] == s[r][i] + end + end + end + + # Test reduced index + rred = Base.reduced_index(r) + @test typeof(rred) == typeof(r) + @test length(rred) == 1 + @test first(rred) == first(r) + + @testset "reduced_indices" begin + a = reshape(1:24, 2, 3, 4) + sa = OffsetArray(a, (2, 3, 4)); + @testset for dim in 1:ndims(sa) + sasum = sum(sa, dims = dim) + @test parent(sasum) == sum(a, dims = dim) + find = firstindex(sa, dim) + @test no_offset_axes(sasum, dim) == find:find + end + end + + @testset "conversion to AbstractUnitRange" begin + r = IdOffsetRange(1:2) + @test AbstractUnitRange{Int}(r) === r + r2 = IdOffsetRange(big(1):big(2)) + @test AbstractUnitRange{Int}(r2) === r + @test AbstractUnitRange{BigInt}(r2) === r2 + + if v"1.5" < VERSION + @test OrdinalRange{Int,Int}(r2) === r + @test OrdinalRange{BigInt,BigInt}(r2) === r2 + end + end + + @testset "Bool IdOffsetRange (issue #223)" begin + for b1 in [false, true], b2 in [false, true] + r = IdOffsetRange(b1:b2) + @test first(r) === b1 + @test last(r) === b2 + end + @test_throws ArgumentError IdOffsetRange(true:true, true) + @test_throws ArgumentError IdOffsetRange{Bool,UnitRange{Bool}}(true:true, true) + @test_throws ArgumentError IdOffsetRange{Bool,IdOffsetRange{Bool,UnitRange{Bool}}}(IdOffsetRange(true:true), true) + end + + @testset "Logical indexing" begin + @testset "indexing with a single bool" begin + r = IdOffsetRange(1:2) + @test_throws ArgumentError r[true] + @test_throws ArgumentError r[false] + end + @testset "indexing with a Bool UnitRange" begin + r = IdOffsetRange(1:0) + + @test r[true:false] == 1:0 + @test r[true:false] == collect(r)[true:false] + @test_throws BoundsError r[true:true] + @test_throws BoundsError r[false:false] + @test_throws BoundsError r[false:true] + + r = IdOffsetRange(1:1) + + @test r[true:true] == 1:1 + @test r[true:true] == collect(r)[true:true] + + @test r[false:false] == 1:0 + @test r[false:false] == collect(r)[false:false] + + @test_throws BoundsError r[true:false] + @test_throws BoundsError r[false:true] + + r = IdOffsetRange(1:2) + + @test r[false:true] == 2:2 + @test r[false:true] == collect(r)[false:true] + + @test_throws BoundsError r[true:true] + @test_throws BoundsError r[true:false] + @test_throws BoundsError r[false:false] + end + @testset "indexing with a Bool IdOffsetRange" begin + # bounds-checking requires the axes of the indices to match that of the array + function testlogicalindexing(r, r2) + r3 = r[r2]; + @test no_offset_view(r3) == collect(r)[collect(r2)] + end + + r = IdOffsetRange(10:9) + r2 = IdOffsetRange(true:false) + testlogicalindexing(r, r2) + + r = IdOffsetRange(10:10) + r2 = IdOffsetRange(false:false) + testlogicalindexing(r, r2) + r2 = IdOffsetRange(true:true) + testlogicalindexing(r, r2) + + r = IdOffsetRange(10:10, 1) + r2 = IdOffsetRange(false:false, 1) # effectively true:true with indices 2:2 + testlogicalindexing(r, r2) + + r = IdOffsetRange(10:11) + r2 = IdOffsetRange(false:true) + testlogicalindexing(r, r2) + end + @testset "indexing with a Bool StepRange" begin + r = IdOffsetRange(1:0) + + @test r[true:true:false] == 1:1:0 + @test_throws BoundsError r[true:true:true] + @test_throws BoundsError r[false:true:false] + @test_throws BoundsError r[false:true:true] + + r = IdOffsetRange(1:1) + + @test r[true:true:true] == 1:1:1 + @test r[true:true:true] == collect(r)[true:true:true] + @test axes(r[true:true:true], 1) == 1:1 + + @test r[false:true:false] == 1:1:0 + @test r[false:true:false] == collect(r)[false:true:false] + + # StepRange{Bool,Int} + s = StepRange(true, 1, true) + @test r[s] == 1:1:1 + @test r[s] == collect(r)[s] + + s = StepRange(true, 2, true) + @test r[s] == 1:1:1 + @test r[s] == collect(r)[s] + + s = StepRange(false, 1, false) + @test r[s] == 1:1:0 + @test r[s] == collect(r)[s] + + s = StepRange(false, 2, false) + @test r[s] == 1:1:0 + @test r[s] == collect(r)[s] + + @test_throws BoundsError r[true:true:false] + @test_throws BoundsError r[false:true:true] + + r = IdOffsetRange(1:2) + + @test r[false:true:true] == 2:1:2 + @test r[false:true:true] == collect(r)[false:true:true] + + # StepRange{Bool,Int} + s = StepRange(false, 1, true) + @test r[s] == 2:1:2 + @test r[s] == collect(r)[s] + + @test_throws BoundsError r[true:true:true] + @test_throws BoundsError r[true:true:false] + @test_throws BoundsError r[false:true:false] + end + end + + @testset "iteration" begin + # parent has Base.OneTo axes + A = OffsetArray(ones(7), 4:10) + ax = axes(A, 1) + ind, st = iterate(ax) + @test A[ind] == A[4] + ind, st = iterate(ax, st) + @test A[ind] == A[5] + + # parent doesn't have Base.OneTo axes + B = @view A[:] + C = OffsetArray(B, 0) + ax = axes(C, 1) + ind, st = iterate(ax) + @test C[ind] == C[4] + ind, st = iterate(ax, st) + @test C[ind] == C[5] + end +end + +end \ No newline at end of file diff --git a/OffsetArraysCore/test/centered.jl b/OffsetArraysCore/test/centered.jl new file mode 100644 index 0000000..f099b7f --- /dev/null +++ b/OffsetArraysCore/test/centered.jl @@ -0,0 +1,70 @@ +module CenteredTests + +using OffsetArraysCore +using OffsetArraysCore: center, centered +using Test + +@testset "center/centered" begin + @testset "center" begin + A = reshape(collect(1:9), 3, 3) + c = center(A) + @test c == (2, 2) + @test A[c...] == 5 + @test center(A, RoundDown) == center(A, RoundUp) + + A = reshape(collect(1:6), 2, 3) + c = center(A) + @test center(A, RoundDown) == c + @test c == (1, 2) + @test A[c...] == 3 + c = center(A, RoundUp) + @test c == (2, 2) + @test A[c...] == 4 + end + + @testset "centered" begin + A = reshape(collect(1:9), 3, 3) + Ao = centered(A) + @test centered(Ao) === Ao + @test centered(Ao, center(Ao)) === Ao + @test typeof(Ao) <: OffsetArray + @test parent(Ao) === A + @test Ao.offsets == (-2, -2) + @test Ao[0, 0] == 5 + + A = reshape(collect(1:6), 2, 3) + Ao = centered(A) + @test centered(A, center(A, RoundDown)) == Ao + @test typeof(Ao) <: OffsetArray + @test parent(Ao) === A + @test Ao.offsets == (-1, -2) + @test Ao[0, 0] == 3 + Ao = centered(A, center(A, RoundUp)) + @test typeof(Ao) <: OffsetArray + @test parent(Ao) === A + @test Ao.offsets == (-2, -2) + @test Ao[0, 0] == 4 + + A = reshape(collect(1:9), 3, 3) + Ao = OffsetArray(A, -1, -1) + Aoo = centered(Ao) + @test parent(Aoo) === A # there will be only one OffsetArray wrapper + @test Aoo.offsets == (-2, -2) + @test Aoo[0, 0] == 5 + + A = reshape(collect(1:9), 3, 3) + Aoo = centered(A, CartesianIndex(2,2)) + c = (0,0) + i = CartesianIndex(c...) + @test Aoo[i] == Aoo[c...] + + end +end + +@info "Following deprecations are expected" +@testset "deprecations" begin + A = reshape(collect(1:9), 3, 3) + @test centered(A, RoundDown) == centered(A, RoundUp) +end + +end \ No newline at end of file diff --git a/OffsetArraysCore/test/customranges.jl b/OffsetArraysCore/test/customranges.jl new file mode 100644 index 0000000..8b337cb --- /dev/null +++ b/OffsetArraysCore/test/customranges.jl @@ -0,0 +1,87 @@ +# Useful for testing indexing +struct ZeroBasedRange{T,A<:AbstractRange{T}} <: AbstractRange{T} + a :: A + function ZeroBasedRange(a::AbstractRange{T}) where {T} + @assert !Base.has_offset_axes(a) + new{T, typeof(a)}(a) + end +end + +struct ZeroBasedUnitRange{T,A<:AbstractUnitRange{T}} <: AbstractUnitRange{T} + a :: A + function ZeroBasedUnitRange(a::AbstractUnitRange{T}) where {T} + @assert !Base.has_offset_axes(a) + new{T, typeof(a)}(a) + end +end + +for Z in [:ZeroBasedRange, :ZeroBasedUnitRange] + @eval Base.parent(A::$Z) = A.a + @eval Base.first(A::$Z) = first(A.a) + @eval Base.length(A::$Z) = length(A.a) + @eval Base.last(A::$Z) = last(A.a) + @eval Base.size(A::$Z) = size(A.a) + @eval Base.axes(A::$Z) = map(x -> IdentityUnitRange(0:x-1), size(A.a)) + @eval Base.getindex(A::$Z, i::Int) = A.a[i + 1] + @eval Base.getindex(A::$Z, i::Integer) = A.a[i + 1] + @eval Base.firstindex(A::$Z) = 0 + @eval Base.step(A::$Z) = step(A.a) + @eval OffsetArraysCore.no_offset_view(A::$Z) = A.a + @eval function Base.show(io::IO, A::$Z) + show(io, A.a) + print(io, " with indices $(axes(A,1))") + end +end + +for Z in [:ZeroBasedRange, :ZeroBasedUnitRange] + for R in [:AbstractRange, :AbstractUnitRange, :StepRange] + @eval @inline function Base.getindex(A::$Z, r::$R{<:Integer}) + @boundscheck checkbounds(A, r) + OffsetArraysCore._indexedby(A.a[r .+ 1], axes(r)) + end + end + + for R in [:ZeroBasedUnitRange, :ZeroBasedRange] + @eval @inline function Base.getindex(A::$Z, r::$R{<:Integer}) + @boundscheck checkbounds(A, r) + OffsetArraysCore._indexedby(A.a[r.a .+ 1], axes(r)) + end + end + + for R in [:IdOffsetRange] + @eval @inline function Base.getindex(A::$Z, r::$R) + @boundscheck checkbounds(A, r) + OffsetArraysCore._indexedby(A.a[r .+ 1], axes(r)) + end + end + + for R in [:AbstractUnitRange, :IdOffsetRange, :StepRange, :StepRangeLen, :LinRange] + @eval @inline function Base.getindex(A::$R, r::$Z) + @boundscheck checkbounds(A, r) + OffsetArraysCore._indexedby(A[r.a], axes(r)) + end + end + @eval @inline function Base.getindex(A::StepRangeLen{<:Any,<:Base.TwicePrecision,<:Base.TwicePrecision}, r::$Z) + @boundscheck checkbounds(A, r) + OffsetArraysCore._indexedby(A[r.a], axes(r)) + end + + @eval Base.reshape(z::$Z, inds::Tuple{}) = reshape(parent(z), inds) + @eval Base.reshape(z::$Z, inds::Tuple{Int, Vararg{Int}}) = reshape(parent(z), inds) + @eval Base.reshape(z::$Z, inds::Tuple{Union{Int, AbstractUnitRange{<:Integer}}, Vararg{Union{Int, AbstractUnitRange{<:Integer}}}}) = reshape(parent(z), inds) +end + +# A basic range that does not have specialized vector indexing methods defined +# In this case the best that we may do is to return an OffsetArray +# Despite this, an indexing operation involving this type should preserve the axes of the indices +struct CustomRange{T,A<:AbstractRange{T}} <: AbstractRange{T} + a :: A +end +Base.parent(r::CustomRange) = r.a +Base.size(r::CustomRange) = size(parent(r)) +Base.length(r::CustomRange) = length(parent(r)) +Base.axes(r::CustomRange) = axes(parent(r)) +Base.first(r::CustomRange) = first(parent(r)) +Base.last(r::CustomRange) = last(parent(r)) +Base.step(r::CustomRange) = step(parent(r)) +Base.getindex(r::CustomRange, i::Int) = getindex(parent(r), i) diff --git a/OffsetArraysCore/test/doctests.jl b/OffsetArraysCore/test/doctests.jl new file mode 100644 index 0000000..ccad849 --- /dev/null +++ b/OffsetArraysCore/test/doctests.jl @@ -0,0 +1,15 @@ +module DocStringTests + +using OffsetArraysCore +using Test +using Documenter + +DocMeta.setdocmeta!(OffsetArraysCore, :DocTestSetup, :(using OffsetArraysCore); recursive=true) + +@testset "Project meta quality checks" begin + if VERSION >= v"1.2" + doctest(OffsetArraysCore, manual = false) + end +end + +end \ No newline at end of file diff --git a/OffsetArraysCore/test/indexing.jl b/OffsetArraysCore/test/indexing.jl new file mode 100644 index 0000000..6cbfe63 --- /dev/null +++ b/OffsetArraysCore/test/indexing.jl @@ -0,0 +1,514 @@ +module IndexingTests + +using OffsetArraysCore +using OffsetArraysCore: OffsetArray, IdOffsetRange, no_offset_view, IdentityUnitRange +using StaticArrays +using FillArrays +using Test + +no_offset_axes(x, d) = no_offset_view(axes(x, d)) +no_offset_axes(x) = map(no_offset_view, axes(x)) + +function same_value(r1, r2) + length(r1) == length(r2) || return false + for (v1, v2) in zip(r1, r2) + v1 == v2 || return false + end + return true +end + +include("customranges.jl") + +@testset "Traits" begin + A0 = [1 3; 2 4] + A = OffsetArray(A0, (-1,2)) # IndexLinear + S = OffsetArray(view(A0, 1:2, 1:2), (-1,2)) # IndexCartesian + @test axes(A) === axes(S) + @test no_offset_axes(A) == no_offset_axes(S) == (0:1, 3:4) + @test axes(A, 1) === IdOffsetRange(Base.OneTo(2), -1) + @test size(A) == size(A0) + @test size(A, 1) == size(A0, 1) + @test length(A) == length(A0) + @test A == OffsetArray(A0, 0:1, 3:4) + @test_throws DimensionMismatch OffsetArray(A0, 0:2, 3:4) + @test_throws DimensionMismatch OffsetArray(A0, 0:1, 2:4) + @test eachindex(IndexLinear(), A) == eachindex(IndexLinear(), parent(A)) + @test eachindex(IndexCartesian(), A) == CartesianIndices(A) == CartesianIndices(axes(A)) + @test eachindex(S) == eachindex(IndexCartesian(), S) == CartesianIndices(S) + @test eachindex(IndexLinear(), S) == eachindex(IndexLinear(), A0) + A = OffsetArray(ones(2), 5:6) + @test eachindex(IndexLinear(), A) === axes(A, 1) + + A = OffsetArray(big(1):big(2), 1) + B = OffsetArray(1:2, 1) + @test CartesianIndices(A) == CartesianIndices(B) + @test LinearIndices(A) == LinearIndices(B) + @test eachindex(A) == eachindex(B) +end + +@testset "Scalar indexing" begin + A0 = [1 3; 2 4] + A = OffsetArray(A0, (-1,2)) + S = OffsetArray(view(A0, 1:2, 1:2), (-1,2)) + + @test @inferred(A[0,3]) == @inferred(A[0,3,1]) == @inferred(A[1]) == @inferred(S[0,3]) == @inferred(S[0,3,1]) == @inferred(S[1]) == 1 + @test A[1,3] == A[1,3,1] == A[2] == S[1,3] == S[1,3,1] == S[2] == 2 + @test A[0,4] == A[0,4,1] == A[3] == S[0,4] == S[0,4,1] == S[3] == 3 + @test A[1,4] == A[1,4,1] == A[4] == S[1,4] == S[1,4,1] == S[4] == 4 + @test @inbounds(A[0,3]) == @inbounds(A[0,3,1]) == @inbounds(A[1]) == @inbounds(S[0,3]) == @inbounds(S[0,3,1]) == @inbounds(S[1]) == 1 + @test @inbounds(A[1,3]) == @inbounds(A[1,3,1]) == @inbounds(A[2]) == @inbounds(S[1,3]) == @inbounds(S[1,3,1]) == @inbounds(S[2]) == 2 + @test @inbounds(A[0,4]) == @inbounds(A[0,4,1]) == @inbounds(A[3]) == @inbounds(S[0,4]) == @inbounds(S[0,4,1]) == @inbounds(S[3]) == 3 + @test @inbounds(A[1,4]) == @inbounds(A[1,4,1]) == @inbounds(A[4]) == @inbounds(S[1,4]) == @inbounds(S[1,4,1]) == @inbounds(S[4]) == 4 + @test_throws BoundsError(A, (1,1)) A[1,1] + @test_throws BoundsError(A, (1,1)) A[1,1] = 4 + @test_throws BoundsError(S, (1,1)) S[1,1] + @test_throws BoundsError(S, (1,1)) S[1,1] = 4 + @test_throws BoundsError(A, (0,3,2)) A[0,3,2] + @test_throws BoundsError(A, (0,3,2)) A[0,3,2] = 4 + @test_throws BoundsError(A, (0,3,0)) A[0,3,0] + @test_throws BoundsError(A, (0,3,0)) A[0,3,0] = 4 + Ac = copy(A) + Ac[0,3] = 10 + @test Ac[0,3] == 10 + Ac[0,3,1] = 11 + @test Ac[0,3] == 11 + @inbounds Ac[0,3,1] = 12 + @test Ac[0,3] == 12 + + y = OffsetArray{Float64}(undef, -1:1, -7:7, -3:-1, -5:5, -1:1, -3:3, -2:2, -1:1) + y[-1,-7,-3,-5,-1,-3,-2,-1] = 14 + y[-1,-7,-3,-5,-1,-3,-2,-1] += 5 + @test y[-1,-7,-3,-5,-1,-3,-2,-1] == 19 + + @testset "setindex!" begin + A = OffsetArray(ones(2,2), 1:2, 1:2) + @test setindex!(A, 2, 1, 1) === A + @test A[1,1] == 2 + @test setindex!(A, 2, 1) === A + @test A[1] == 2 + + v = OffsetArray(ones(3), 4:6) + @test setindex!(A, 2, 4) === A + @test A[4] == 2 + end + + @testset "Zero-index indexing (#194)" begin + @test OffsetArray([6], 2:2)[] == 6 + @test OffsetArray(fill(6, 1, 1), 2:2, 3:3)[] == 6 + @test OffsetArray(fill(6))[] == 6 + @test_throws BoundsError OffsetArray([6,7], 2:3)[] + @test_throws BoundsError OffsetArray([6 7], 2:2, 2:3)[] + @test_throws BoundsError OffsetArray([], 2:1)[] + end +end + + +_comp(x::Integer, y::Integer) = x == y +_comp(x::Any, y::Any) = isapprox(Real(x), Real(y), atol = 1e-14, rtol = 1e-8) +function test_indexing_axes_and_vals(r1, r2) + r12 = r1[r2] + if axes(r12, 1) != axes(r2, 1) + @show r1 r2 r12 axes(r12, 1) axes(r2, 1) + end + @test axes(r12, 1) == axes(r2, 1) + + if axes(r12, 1) == axes(r2, 1) + res1 = try + _comp(first(r12), r1[first(r2)]) + catch + @show r1 r2 + rethrow() + end + res2 = try + _comp(last(r12), r1[last(r2)]) + catch + @show r1 r2 + rethrow() + end + if !(res1 & res2) + @show r1 r2 + end + @test res1 + @test res2 + for i in eachindex(r2) + @test _comp(r12[i], r1[r2[i]]) + end + end +end + +@testset "Vector indexing" begin + A0 = [1 3; 2 4] + A = OffsetArray(A0, (-1,2)) + S = OffsetArray(view(A0, 1:2, 1:2), (-1,2)) + + @test A[:, 3] == S[:, 3] == OffsetArray([1,2], (A.offsets[1],)) + @test A[:, 4] == S[:, 4] == OffsetArray([3,4], (A.offsets[1],)) + @test_throws BoundsError A[:, 1] + @test_throws BoundsError S[:, 1] + @test A[0, :] == S[0, :] == OffsetArray([1,3], (A.offsets[2],)) + @test A[1, :] == S[1, :] == OffsetArray([2,4], (A.offsets[2],)) + @test_throws BoundsError A[2, :] + @test_throws BoundsError S[2, :] + @test A[0:1, 3] == S[0:1, 3] == [1,2] + @test A[[1,0], 3] == S[[1,0], 3] == [2,1] + @test A[0, 3:4] == S[0, 3:4] == [1,3] + @test A[1, [4,3]] == S[1, [4,3]] == [4,2] + @test A[:, :] == S[:, :] == A + + # Indexing a nD OffsetArray with n colons preserves the type + r1 = OffsetArray(IdentityUnitRange(100:1000), 3) + @test r1[:] === r1 + + # In general with more colons than dimensions, + # the type might not be preserved but the values and the leading axes should be + r2 = r1[:,:] + @test axes(r2, 1) == axes(r1, 1) + @test same_value(r1, r2) + + s = @SVector[i for i in 1:3] + so = OffsetArray(s, 3) + @test so[:] === so + + a = Ones(3, 2, 1) + ao = OffsetArray(a, axes(a)) + @test ao[:,:,:] === ao + @test same_value(ao[:], ao) + @test same_value(ao[:,:], ao) + + # Indexing an nD OffsetArray with one Colon preserves only the values. + # This uses linear indexing + a = OffsetArray(ones(2,2), (2:3, 2:3)) + b = a[:] + @test same_value(a, b) + + vals = (1,2,3,4,5,6,7,8) + s = SArray{Tuple{2,2,2},Int,3,8}(vals) + so = OffsetArray(s, axes(s)); + so2 = so[:] + @test same_value(so2, s) + + # Test r1[inds] for various combinations of types + + # AbstractArrays with 1-based indices + indslist1 = Any[ + OffsetArray(5:8, 0), + # This currently errors for IdentityUnitRange + # see https://github.com/JuliaLang/julia/issues/39997 + # OffsetArray(big(5):big(80), 0), + OffsetArray(5:2:9, 0), + OffsetArray(9:-2:5, 0), + OffsetArray(IdentityUnitRange(5:8), -4), + OffsetArray(IdOffsetRange(5:8), 0), + ] + + # AbstractRanges with 1-based indices + indslist2 = Any[ + 5:8, + # This currently errors for IdentityUnitRange + # see https://github.com/JuliaLang/julia/issues/39997 + # big(5):big(80), + 5:2:9, + 9:-2:5, + IdOffsetRange(5:8), + IdOffsetRange(ZeroBasedUnitRange(4:7), 1), + ] + + for r1 in Any[ + # AbstractArrays + collect(1:100), + OffsetArray(collect(-1:100), -1), + OffsetArray(collect(reshape(1:400, 20, 20)), -10, -10), + OffsetArray(collect(reshape(1:21^2, 21, 21)), -10, -10), + + # OffsetRanges + OffsetArray(10:1000, 0), # 1-based index + OffsetArray(UnitRange(10.0, 1000.0), 0), # 1-based index + OffsetArray(10:3:1000, 3), # offset index + OffsetArray(10.0:3:1000.0, 0), # 1-based index + OffsetArray(10.0:3:1000.0, 3), # offset index + OffsetArray(IdOffsetRange(10:1000, 1), -1), # 1-based index + OffsetArray(IdOffsetRange(10:1000, 1), 3), # offset index + OffsetArray(IdOffsetRange(IdOffsetRange(10:1000, -4), 1), 3), # 1-based index + OffsetArray(IdOffsetRange(IdOffsetRange(10:1000, -1), 1), 3), # offset index + + # AbstractRanges + Base.OneTo(1000), + CustomRange(Base.OneTo(1000)), + Base.Slice(Base.OneTo(1000)), + 1:1000, + UnitRange(1.0, 1000.0), + 1:3:1000, + 1000:-3:1, + 1.0:3.0:1000.0, + StepRangeLen(Float64(1), Float64(1000), 1000), + LinRange(1, 1000, 1000), + Base.Slice(Base.OneTo(1000)), # 1-based index + IdentityUnitRange(Base.OneTo(1000)), # 1-based index + IdOffsetRange(Base.OneTo(1000)), # 1-based index + IdentityUnitRange(2:1000), # offset index + IdOffsetRange(ZeroBasedUnitRange(1:1000), 1), # 1-based index + IdOffsetRange(ZeroBasedUnitRange(1:1000), 2), # offset index + ZeroBasedUnitRange(1:1000), # offset range + ZeroBasedRange(1:1000), # offset range + ZeroBasedRange(1:1:1000), # offset range + CustomRange(ZeroBasedRange(1:1:1000)), # offset range + ] + + # AbstractArrays with 1-based indices + for r2 in indslist1 + test_indexing_axes_and_vals(r1, r2) + test_indexing_axes_and_vals(r1, collect(r2)) + end + + # AbstractRanges with 1-based indices + for r2 in indslist2 + if !(!(r1 isa AbstractUnitRange) && r1 isa AbstractRange && r2 isa OffsetVector) + test_indexing_axes_and_vals(r1, r2) + test_indexing_axes_and_vals(r1, collect(r2)) + end + + if r1 isa AbstractRange && !(r1 isa CustomRange) && axes(r2, 1) isa Base.OneTo + @test r1[r2] isa AbstractRange + end + end + end + + # Indexing with IdentityUnitRange(::Base.OneTo) or Base.Slice(::OneTo) is special. + # This is because axes(::IdentityUnitRange{<:Base.OneTo}, 1) isa Base.OneTo, and not an IdentityUnitRange. + # These therefore may pass through no_offset_view unchanged. + # This had led to a stack-overflow in indexing, as getindex was using no_offset_view. + # Issue 209 + for r1 in Any[ + # This set of tests is for ranges r1 that have 1-based indices + UnitRange(1.0, 99.0), + 1:99, + Base.OneTo(99), + 1:1:99, + 99:-1:1, + 1.0:1.0:99.0, + StepRangeLen(Float64(1), Float64(99), 99), + LinRange(1, 99, 99), + Base.Slice(Base.OneTo(99)), + IdentityUnitRange(Base.OneTo(99)), + IdOffsetRange(Base.OneTo(99)), + ] + + for r2 in Any[ + IdentityUnitRange(Base.OneTo(3)), + Base.Slice(Base.OneTo(3)), + IdOffsetRange(Base.OneTo(3)), + ] + + test_indexing_axes_and_vals(r1, r2) + test_indexing_axes_and_vals(r1, collect(r2)) + if axes(r2, 1) isa Base.OneTo + @test r1[r2] isa AbstractRange + end + end + end +end + +@testset "Vector indexing with offset ranges" begin + r = OffsetArray(8:10, -1:1) + r1 = r[0:1] + @test r1 === 9:10 + r1 = (8:10)[OffsetArray(1:2, -5:-4)] + @test no_offset_axes(r1) == (-5:-4,) + @test no_offset_view(r1) == 8:9 + r1 = OffsetArray(8:10, -1:1)[OffsetArray(0:1, -5:-4)] + @test no_offset_axes(r1) == (-5:-4,) + @test no_offset_view(r1) == 9:10 + + a = OffsetVector(3:4, 10:11) + ax = IdOffsetRange(5:6, 5) + @test axes(a[ax]) == axes(ax) + for i in axes(ax,1) + @test a[ax[i]] == a[ax][i] + end + + ax = IdentityUnitRange(10:11) + @test axes(a[ax]) == axes(ax) + for i in axes(ax,1) + @test a[ax[i]] == a[ax][i] + end + + # AbstractArrays with offset axes + indslist1 = Any[OffsetArray(5:9, 40), + OffsetArray(5:2:9, 40), + OffsetArray(9:-2:5, 40), + OffsetArray(IdentityUnitRange(5:8), 2), + OffsetArray(IdOffsetRange(5:8, 1), 3), + OffsetArray(IdOffsetRange(IdOffsetRange(5:8, 4), 1), 3), + OffsetArray(IdOffsetRange(IdentityUnitRange(5:8), 1), 3), + OffsetArray(IdentityUnitRange(IdOffsetRange(5:8, 1)), 3), + ] + + # AbstractRanges with offset axes + indslist2 = Any[IdOffsetRange(5:8, 1), + IdOffsetRange(Base.OneTo(3), 4), + IdOffsetRange(IdOffsetRange(5:8, 2), 1), + IdOffsetRange(IdOffsetRange(IdOffsetRange(5:8, -1), 2), 1), + IdOffsetRange(IdentityUnitRange(15:20), -2), + ] + + indslist3 = Any[IdentityUnitRange(5:8), + IdentityUnitRange(IdOffsetRange(1:4, 5)), + ZeroBasedUnitRange(5:8), + ZeroBasedRange(5:8), + ZeroBasedRange(5:2:9), + ZeroBasedRange(9:-2:5), + ] + + for r1 in Any[ + # AbstractArrays + collect(1:100), + OffsetArray(collect(-1:100), -1), + OffsetArray(collect(reshape(1:400, 20, 20)), -10, -10), + OffsetArray(collect(reshape(1:21^2, 21, 21)), -10, -10), + + # OffsetRanges + OffsetArray(10:1000, 0), # 1-based index + OffsetArray(UnitRange(10.0, 1000.0), 0), # 1-based index + OffsetArray(10:1000, 3), # offset index + OffsetArray(10:3:1000, 0), # 1-based index + OffsetArray(10:3:1000, 3), # offset index + OffsetArray(10.0:3:1000.0, 0), # 1-based index + OffsetArray(10.0:3:1000.0, 3), # offset index + OffsetArray(IdOffsetRange(10:1000, -3), 3), # 1-based index + OffsetArray(IdOffsetRange(10:1000, 1), 3), # offset index + OffsetArray(IdOffsetRange(IdOffsetRange(10:1000, -4), 1), 3), # 1-based index + OffsetArray(IdOffsetRange(IdOffsetRange(10:1000, -1), 1), 3), # offset index + + # AbstractRanges + Base.OneTo(1000), + Base.Slice(Base.OneTo(1000)), + CustomRange(Base.OneTo(1000)), + 1:1000, + UnitRange(1.0, 1000.0), + 1:2:2000, + 2000:-1:1, + 1.0:2.0:2000.0, + StepRangeLen(Float64(1), Float64(1000), 1000), + LinRange(1.0, 2000.0, 2000), + Base.Slice(Base.OneTo(1000)), # 1-based index + IdOffsetRange(Base.OneTo(1000)), # 1-based index + IdOffsetRange(1:1000, 0), # 1-based index + IdOffsetRange(Base.OneTo(1000), 4), # offset index + IdOffsetRange(1:1000, 4), # offset index + IdOffsetRange(ZeroBasedUnitRange(1:1000), 1), # 1-based index + IdOffsetRange(ZeroBasedUnitRange(1:1000), 2), # offset index + IdentityUnitRange(ZeroBasedUnitRange(1:1000)), # 1-based index + IdentityUnitRange(5:1000), # offset index + ZeroBasedUnitRange(1:1000), # offset index + ZeroBasedRange(1:1000), # offset index + ZeroBasedRange(1:1:1000), # offset index + ZeroBasedUnitRange(IdentityUnitRange(1:1000)), # offset index + CustomRange(ZeroBasedUnitRange(IdentityUnitRange(1:1000))), # offset index + ] + + # AbstractArrays with offset axes + for r2 in indslist1 + test_indexing_axes_and_vals(r1, r2) + r2_dense = OffsetArray(collect(r2), axes(r2)) + test_indexing_axes_and_vals(r1, r2_dense) + end + + # AbstractRanges with offset axes + for r2 in indslist2 + + test_indexing_axes_and_vals(r1, r2) + r2_dense = OffsetArray(collect(r2), axes(r2)) + test_indexing_axes_and_vals(r1, r2_dense) + + # This might not hold for all ranges, but holds for the known ones being tested here + if r1 isa AbstractUnitRange{<:Integer} && r2 isa AbstractUnitRange{<:Integer} + @test r1[r2] isa AbstractUnitRange{<:Integer} + end + end + end +end + +@testset "LinearIndexing" begin + r = OffsetArray(ZeroBasedRange(3:4), 1) + @test LinearIndices(r) == axes(r,1) + r = OffsetArray(ZeroBasedRange(3:4), 2) + @test LinearIndices(r) == axes(r,1) +end + +@testset "CartesianIndexing" begin + A0 = [1 3; 2 4] + A = OffsetArray(A0, (-1,2)) + S = OffsetArray(view(A0, 1:2, 1:2), (-1,2)) + + @test A[CartesianIndex((0,3))] == S[CartesianIndex((0,3))] == 1 + @test A[CartesianIndex((0,3)),1] == S[CartesianIndex((0,3)),1] == 1 + @test @inbounds(A[CartesianIndex((0,3))]) == @inbounds(S[CartesianIndex((0,3))]) == 1 + @test @inbounds(A[CartesianIndex((0,3)),1]) == @inbounds(S[CartesianIndex((0,3)),1]) == 1 + @test_throws BoundsError A[CartesianIndex(1,1)] + @test_throws BoundsError A[CartesianIndex(1,1),0] + @test_throws BoundsError A[CartesianIndex(1,1),2] + @test_throws BoundsError S[CartesianIndex(1,1)] + @test_throws BoundsError S[CartesianIndex(1,1),0] + @test_throws BoundsError S[CartesianIndex(1,1),2] + @test eachindex(A) == 1:4 + @test eachindex(S) == CartesianIndices(IdentityUnitRange.((0:1,3:4))) +end + +@testset "IdentityUnitRange indexing" begin + # 155 + a = OffsetVector(3:4, 2:3) + ax = IdentityUnitRange(2:3) + @test a[ax[2]] == a[ax][2] + + s = -2:2:4 + r = 5:8 + y = OffsetArray(s, r) + @test no_offset_axes(y) == (r,) + @test step(y) == step(s) + + a = OffsetVector(3:4, 10:11) + ax = IdOffsetRange(5:6, 5) + @test axes(a[ax]) == axes(ax) + for i in axes(ax,1) + @test a[ax[i]] == a[ax][i] + end + + ax = IdentityUnitRange(10:11) + @test axes(a[ax]) == axes(ax) + for i in axes(ax,1) + @test a[ax[i]] == a[ax][i] + end +end + +@testset "Indexing with OffsetArray axes" begin + A0 = [1 3; 2 4] + + i1 = OffsetArray([2,1], (-5,)) + i1 = OffsetArray([2,1], -5) + b = A0[i1, 1] + @test axes(b) == (IdentityUnitRange(-4:-3),) + @test b[-4] == 2 + @test b[-3] == 1 + b = A0[1,i1] + @test axes(b) == (IdentityUnitRange(-4:-3),) + @test b[-4] == 3 + @test b[-3] == 1 + v = view(A0, i1, 1) + @test axes(v) == (IdentityUnitRange(-4:-3),) + v = view(A0, 1:1, i1) + @test axes(v) == (Base.OneTo(1), IdentityUnitRange(-4:-3)) + + for r in (1:10, 1:1:10, StepRangeLen(1, 1, 10), LinRange(1, 10, 10), 0.1:0.2:0.9) + s = OffsetArray(2:3, 2:3) + @test axes(r[s]) == axes(s) + end +end + +@testset "logical indexing" begin + A0 = [1 3; 2 4] + A = OffsetArray(A0, (-1,2)) + + @test A[A .> 2] == [3,4] +end + +end \ No newline at end of file diff --git a/OffsetArraysCore/test/misc.jl b/OffsetArraysCore/test/misc.jl new file mode 100644 index 0000000..0153633 --- /dev/null +++ b/OffsetArraysCore/test/misc.jl @@ -0,0 +1,18 @@ +module MiscTests + +using OffsetArraysCore +using Test + +@testset "misc" begin + @test OffsetArraysCore._subtractoffset(Base.OneTo(2), 1) isa AbstractUnitRange{Int} + @test OffsetArraysCore._subtractoffset(Base.OneTo(2), 1) == 0:1 + @test OffsetArraysCore._subtractoffset(3:2:9, 1) isa AbstractRange{Int} + @test OffsetArraysCore._subtractoffset(3:2:9, 1) == 2:2:8 + + @test OffsetArraysCore._addoffset(Base.OneTo(2), 1) isa AbstractUnitRange{Int} + @test OffsetArraysCore._addoffset(Base.OneTo(2), 1) == 2:3 + @test OffsetArraysCore._addoffset(3:2:9, 1) isa AbstractRange{Int} + @test OffsetArraysCore._addoffset(3:2:9, 1) == 4:2:10 +end + +end \ No newline at end of file diff --git a/OffsetArraysCore/test/offsetarray.jl b/OffsetArraysCore/test/offsetarray.jl new file mode 100644 index 0000000..fda75f5 --- /dev/null +++ b/OffsetArraysCore/test/offsetarray.jl @@ -0,0 +1,1513 @@ +module OffsetArraysTest + +using CatIndices: BidirectionalVector +using DelimitedFiles +using EllipsisNotation +using LinearAlgebra +using OffsetArraysCore +using OffsetArraysCore: no_offset_view, Origin, IdOffsetRange, IdentityUnitRange +using Test +using StaticArrays + +include("customranges.jl") + +no_offset_axes(x, d) = no_offset_view(axes(x, d)) +no_offset_axes(x) = map(no_offset_view, axes(x)) + +function same_value(r1, r2) + length(r1) == length(r2) || return false + for (v1, v2) in zip(r1, r2) + v1 == v2 || return false + end + return true +end + +# Custom index types +struct ZeroBasedIndexing end +struct NewColon end +struct TupleOfRanges{N} + x ::NTuple{N, UnitRange{Int}} +end + +# used in testing the constructor +struct WeirdInteger{T} <: Integer + x :: T +end +# assume that it doesn't behave as expected +Base.Int(a::WeirdInteger) = a + +@testset "Constructors" begin + @testset "Single-entry arrays in dims 0:5" begin + for n = 0:5 + for z in (OffsetArray(ones(Int,ntuple(d->1,n)), ntuple(x->x-1,n)), + fill!(OffsetArray{Float64}(undef, ntuple(x->x:x, n)), 1), + fill!(OffsetArray{Float64}(undef, ntuple(x->x:x, n)...), 1), + fill!(OffsetArray{Float64,n}(undef, ntuple(x->x:x, n)), 1), + fill!(OffsetArray{Float64,n}(undef, ntuple(x->x:x, n)...), 1)) + @test length(LinearIndices(z)) == 1 + @test no_offset_axes(z) == ntuple(x->x:x, n) + @test z[1] == 1 + end + end + a0 = reshape([3]) + a = OffsetArray(a0) + @test axes(a) == () + @test ndims(a) == 0 + @test a[] == 3 + @test a === OffsetArray(a, ()) + @test_throws ArgumentError OffsetArray(a, 0) + @test_throws ArgumentError OffsetArray(a0, 0) + end + + @testset "OffsetVector" begin + # initialization + one_based_axes = Any[ + (Base.OneTo(4), ), + (1:4, ), + (big(1):big(4), ), + (CartesianIndex(1):CartesianIndex(4), ), + (IdentityUnitRange(1:4), ), + (IdOffsetRange(1:4),), + (IdOffsetRange(3:6, -2),) + ] + + offset_axes = Any[ + (-1:2, ), + (big(-1):big(2), ), + (CartesianIndex(-1):CartesianIndex(2), ), + (IdentityUnitRange(-1:2), ), + (IdOffsetRange(-1:2),), + (IdOffsetRange(3:6, -4),) + ] + + offsets = size.(one_based_axes[1], 1) + offsets_big = map(big, offsets) + + for inds in Any[offsets, offsets_big, 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, ) + @test axes(a.parent) == (Base.OneTo(4), ) + + a = OffsetVector{Nothing}(nothing, inds) + @test eltype(a) === Nothing + @test axes(a) === axes(OffsetVector{Nothing}(nothing, inds...)) === axes(OffsetArray{Nothing, 1}(nothing, inds)) + @test axes(a) === (IdOffsetRange(Base.OneTo(4), 0), ) + + a = OffsetVector{Missing}(missing, inds) + @test eltype(a) === Missing + @test axes(a) === axes(OffsetVector{Missing}(missing, inds...)) === axes(OffsetArray{Missing, 1}(missing, inds)) + @test axes(a) === (IdOffsetRange(Base.OneTo(4), 0), ) + end + + # nested OffsetVectors + for inds in Any[offsets, offsets_big] + a = OffsetVector{Float64}(undef, inds) + b = OffsetVector(a, inds); b2 = OffsetVector(a, inds...); + @test eltype(b) === eltype(b2) === Float64 + @test axes(b, 1) === axes(b2, 1) === IdOffsetRange(Base.OneTo(4), 4) + end + + # offset indexing + for inds in offset_axes + # test offsets + a = OffsetVector{Float64}(undef, inds) + ax = (IdOffsetRange(Base.OneTo(4), -2), ) + @test a.offsets === (-2, ) + @test axes(a.parent) == (Base.OneTo(4), ) + @test axes(a) === ax + a = OffsetVector{Nothing}(nothing, inds) + @test axes(a) === ax + a = OffsetVector{Missing}(missing, inds) + @test axes(a) === ax + + for (T, t) in Any[(Nothing, nothing), (Missing, missing)] + a = OffsetVector{Union{T, Vector{Int}}}(undef, inds) + @test !isassigned(a, -1) + @test eltype(a) === Union{T, Vector{Int}} + @test axes(a) === ax + + a = OffsetVector{Union{T, Vector{Int}}}(t, inds) + @test a[-1] === t + end + end + @test_throws Union{ArgumentError, ErrorException} OffsetVector{Float64}(undef, -2) # only positive number works + + # convenient constructors + a = rand(4) + for inds in offset_axes + oa1 = OffsetVector(a, inds...) + oa2 = OffsetVector(a, inds) + oa3 = OffsetArray(a, inds...) + oa4 = OffsetArray(a, inds) + @test oa1 === oa2 === oa3 === oa4 + @test axes(oa1) === (IdOffsetRange(Base.OneTo(4), -2), ) + @test parent(oa1) === a + @test oa1.offsets === (-2, ) + end + + oa = OffsetArray(a, :) + @test oa === OffsetArray(a, (:, )) === OffsetArray(a, axes(a)) === OffsetVector(a, :) === OffsetVector(a, axes(a)) + @test oa == a + @test axes(oa) == axes(a) + @test axes(oa) !== axes(a) + + # nested offset array + a = rand(4) + oa = OffsetArray(a, -1) + 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...) + @test ooa == a + @test axes(ooa) == axes(a) + @test axes(ooa) !== axes(a) + end + + # overflow bounds check + v = rand(5) + @test axes(OffsetVector(v, typemax(Int)-length(v))) == (IdOffsetRange(axes(v)[1], typemax(Int)-length(v)), ) + @test_throws OverflowError OffsetVector(v, typemax(Int)-length(v)+1) + ao = OffsetArray(v, typemin(Int)) + ao2 = OffsetArray{Float64, 1, typeof(ao)}(ao, (-1, )) + @test no_offset_axes(ao2, 1) == typemin(Int) .+ (0:length(v)-1) + ao2 = OffsetArray(ao, (-1,)) + @test no_offset_axes(ao2, 1) == typemin(Int) .+ (0:length(v)-1) + @test_throws OverflowError OffsetArray{Float64, 1, typeof(ao)}(ao, (-2, )) # inner Constructor + @test_throws OverflowError OffsetArray(ao, (-2, )) # convenient constructor accumulate offsets + @test_throws OverflowError OffsetVector(1:0, typemax(Int)) + @test_throws OverflowError OffsetVector(OffsetVector(1:0, 0), typemax(Int)) + @test_throws OverflowError OffsetArray(zeros(Int, typemax(Int):typemax(Int)), 2) + @test_throws OverflowError OffsetArray(v, Origin(typemax(Int))) + + b = OffsetArray(OffsetArray(big(1):2, 1), typemax(Int)-1) + @test no_offset_axes(b, 1) == big(typemax(Int)) .+ (1:2) + + @testset "OffsetRange" begin + for r in Any[1:100, big(1):big(2)] + a = OffsetVector(r, 4) + @test first(r) in a + @test !(last(r) + 1 in a) + end + + @testset "BigInt axes" begin + r = OffsetArray(1:big(2)^65, 4000) + @test eltype(r) === BigInt + @test no_offset_axes(r, 1) == (big(1):big(2)^65) .+ 4000 + end + end + + # disallow OffsetVector(::Array{<:Any, N}, offsets) where N != 1 + @test_throws ArgumentError OffsetVector(zeros(2,2), (2, 2)) + @test_throws ArgumentError OffsetVector(zeros(2,2), 2, 2) + @test_throws ArgumentError OffsetVector(zeros(2,2), (1:2, 1:2)) + @test_throws ArgumentError OffsetVector(zeros(2,2), 1:2, 1:2) + @test_throws ArgumentError OffsetVector(zeros(), ()) + @test_throws ArgumentError OffsetVector(zeros()) + @test_throws ArgumentError OffsetVector(zeros(2,2), ()) + @test_throws ArgumentError OffsetVector(zeros(2,2)) + @test_throws ArgumentError OffsetVector(zeros(2,2), 2) + @test_throws ArgumentError OffsetVector(zeros(2,2), (2,)) + @test_throws ArgumentError OffsetVector(zeros(2:3,2:3), 2, 3) + @test_throws ArgumentError OffsetVector(zeros(2:3,2:3), (2, 4)) + @test_throws ArgumentError OffsetVector(zeros(2:3,2:3), ()) + @test_throws ArgumentError OffsetVector(zeros(2:3,2:3)) + + # eltype of an OffsetArray should match that of the parent (issue #162) + @test_throws TypeError OffsetVector{Float64,Vector{ComplexF64}} + # ndim of an OffsetArray should match that of the parent + @test_throws TypeError OffsetVector{Float64,Matrix{Float64}} + end + + @testset "OffsetMatrix" begin + # initialization + + one_based_axes = Any[ + (Base.OneTo(4), Base.OneTo(3)), + (1:4, 1:3), + (big(1):big(4), big(1):big(3)), + (CartesianIndex(1, 1):CartesianIndex(4, 3), ), + (CartesianIndex(1):CartesianIndex(4), CartesianIndex(1):CartesianIndex(3)), + (CartesianIndex(1):CartesianIndex(4), 1:3), + (IdentityUnitRange(1:4), IdentityUnitRange(1:3)), + (IdOffsetRange(1:4), IdOffsetRange(1:3)), + (IdOffsetRange(3:6, -2), IdOffsetRange(3:5, -2)), + (IdOffsetRange(3:6, -2), IdentityUnitRange(1:3)), + (IdOffsetRange(3:6, -2), 1:3), + ] + + offset_axes = Any[ + (-1:2, 0:2), + (big(-1):big(2), big(0):big(2)), + (CartesianIndex(-1, 0):CartesianIndex(2, 2), ), + (-1:2, CartesianIndex(0):CartesianIndex(2)), + (CartesianIndex(-1):CartesianIndex(2), CartesianIndex(0):CartesianIndex(2)), + (CartesianIndex(-1):CartesianIndex(2), 0:2), + (IdentityUnitRange(-1:2), 0:2), + (IdOffsetRange(-1:2), IdOffsetRange(0:2)), + (IdOffsetRange(3:6, -4), IdOffsetRange(2:4, -2)), + (IdOffsetRange(3:6, -4), IdentityUnitRange(0:2)), + (IdOffsetRange(-1:2), 0:2), + ] + + offsets = size.(one_based_axes[1], 1) + offsets_big = map(big, offsets) + + for inds in Any[offsets, offsets_big, 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) + @test axes(a.parent) == (Base.OneTo(4), Base.OneTo(3)) + + a = OffsetMatrix{Nothing}(nothing, inds) + @test eltype(a) === Nothing + @test axes(a) === axes(OffsetMatrix{Nothing}(nothing, inds...)) === axes(OffsetArray{Nothing, 2}(nothing, inds)) === axes(OffsetArray{Nothing, 2}(nothing, inds...)) + @test axes(a) === ax + + a = OffsetMatrix{Missing}(missing, inds) + @test eltype(a) === Missing + @test axes(a) === axes(OffsetMatrix{Missing}(missing, inds...)) === axes(OffsetArray{Missing, 2}(missing, inds)) === axes(OffsetArray{Missing, 2}(missing, inds...)) + @test axes(a) === ax + end + @test_throws Union{ArgumentError, ErrorException} OffsetMatrix{Float64}(undef, 2, -2) # only positive numbers works + + # nested OffsetMatrices + for inds in Any[offsets, offsets_big] + a = OffsetMatrix{Float64}(undef, inds) + b = OffsetMatrix(a, inds); b2 = OffsetMatrix(a, inds...); + @test eltype(b) === eltype(b2) === Float64 + @test axes(b, 1) === axes(b2, 1) === IdOffsetRange(Base.OneTo(4), 4) + @test axes(b, 2) === axes(b2, 2) === IdOffsetRange(Base.OneTo(3), 3) + end + + for inds in offset_axes + # test offsets + a = OffsetMatrix{Float64}(undef, inds) + ax = (IdOffsetRange(Base.OneTo(4), -2), IdOffsetRange(Base.OneTo(3), -1)) + @test a.offsets === (-2, -1) + @test axes(a.parent) == (Base.OneTo(4), Base.OneTo(3)) + @test axes(a) === ax + a = OffsetMatrix{Nothing}(nothing, inds) + @test axes(a) === ax + a = OffsetMatrix{Missing}(missing, inds) + @test axes(a) === ax + + for (T, t) in Any[(Nothing, nothing), (Missing, missing)] + a = OffsetMatrix{Union{T, Vector{Int}}}(undef, inds) + @test !isassigned(a, -1, 0) + @test eltype(a) === Union{T, Vector{Int}} + @test axes(a) === ax + + a = OffsetMatrix{Union{T, Vector{Int}}}(t, inds) + @test a[-1, 0] === t + end + end + + # convenient constructors + a = rand(4, 3) + for inds in offset_axes + ax = (IdOffsetRange(Base.OneTo(4), -2), IdOffsetRange(Base.OneTo(3), -1)) + oa1 = OffsetMatrix(a, inds...) + oa2 = OffsetMatrix(a, inds) + oa3 = OffsetArray(a, inds...) + oa4 = OffsetArray(a, inds) + @test oa1 === oa2 === oa3 === oa4 + @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)) + @test oa == a + @test axes(oa) == axes(a) + @test axes(oa) !== axes(a) + + oa = OffsetMatrix(a, :, 2:4) + @test oa === OffsetMatrix(a, axes(a, 1), 2:4) === OffsetMatrix(a, (axes(oa, 1), 2:4)) + + # nested offset array + a = rand(4, 3) + 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...) + @test typeof(parent(ooa)) <: Matrix + @test ooa == a + @test axes(ooa) == axes(a) + @test axes(ooa) !== axes(a) + end + + # overflow bounds check + a = rand(4, 3) + @test axes(OffsetMatrix(a, typemax(Int)-size(a, 1), 0)) == (IdOffsetRange(axes(a)[1], typemax(Int)-size(a, 1)), axes(a, 2)) + @test_throws OverflowError OffsetMatrix(a, typemax(Int)-size(a,1)+1, 0) + @test_throws OverflowError OffsetMatrix(a, 0, typemax(Int)-size(a, 2)+1) + + # disallow OffsetMatrix(::Array{<:Any, N}, offsets) where N != 2 + @test_throws ArgumentError OffsetMatrix(zeros(2), (2,)) + @test_throws ArgumentError OffsetMatrix(zeros(2), 2) + @test_throws ArgumentError OffsetMatrix(zeros(2), (1:2,)) + @test_throws ArgumentError OffsetMatrix(zeros(2), 1:2) + @test_throws ArgumentError OffsetMatrix(zeros(), ()) + @test_throws ArgumentError OffsetMatrix(zeros()) + @test_throws ArgumentError OffsetMatrix(zeros(2), ()) + @test_throws ArgumentError OffsetMatrix(zeros(2)) + @test_throws ArgumentError OffsetMatrix(zeros(2), (1, 2)) + @test_throws ArgumentError OffsetMatrix(zeros(2), 1, 2) + @test_throws ArgumentError OffsetMatrix(zeros(2:3), (2,)) + @test_throws ArgumentError OffsetMatrix(zeros(2:3), 2) + @test_throws ArgumentError OffsetMatrix(zeros(2:3, 1:2, 1:2), (2,0,0)) + @test_throws ArgumentError OffsetMatrix(zeros(2:3, 1:2, 1:2), 2,0,0) + @test_throws ArgumentError OffsetMatrix(zeros(2:3, 1:2, 1:2), ()) + @test_throws ArgumentError OffsetMatrix(zeros(2:3, 1:2, 1:2)) + + # eltype of an OffsetArray should match that of the parent (issue #162) + @test_throws TypeError OffsetMatrix{Float64,Matrix{ComplexF64}} + # ndim of an OffsetArray should match that of the parent + @test_throws TypeError OffsetMatrix{Float64,Vector{Float64}} + end + + # no need to duplicate the 2D case here, + # only add some special test cases + @testset "OffsetArray" begin + a = rand(2, 2, 2) + oa = OffsetArray(a, 0:1, 3:4, 2:3) + @test OffsetArray(a, CartesianIndices(axes(oa))) == oa + @test no_offset_axes(OffsetArray(a, :, CartesianIndices((3:4, 2:3)))) == (1:2, 3:4, 2:3) + @test no_offset_axes(OffsetArray(a, 10:11, CartesianIndices((3:4, 2:3)) )) == (10:11, 3:4, 2:3) + @test no_offset_axes(OffsetArray(a, CartesianIndices((3:4, 2:3)), :)) == (3:4, 2:3, 1:2) + @test no_offset_axes(OffsetArray(a, CartesianIndices((3:4, 2:3)), 10:11)) == (3:4, 2:3, 10:11) + @test no_offset_axes(OffsetArray(a, :, :, CartesianIndices((3:4,)) )) == (1:2, 1:2, 3:4) + @test no_offset_axes(OffsetArray(a, 10:11, :, CartesianIndices((3:4,)) )) == (10:11, 1:2, 3:4) + @test no_offset_axes(OffsetArray(a, 10:11, 2:3, CartesianIndices((3:4,)) )) == (10:11, 2:3, 3:4) + + # ignore empty CartesianIndices + @test OffsetArray(a, CartesianIndices(()), 0:1, :, 2:3) == OffsetArray(a, 0:1, :, 2:3) + @test OffsetArray(a, 0:1, CartesianIndices(()), :, 2:3) == OffsetArray(a, 0:1, :, 2:3) + @test OffsetArray(a, 0:1, :, CartesianIndices(()), 2:3) == OffsetArray(a, 0:1, :, 2:3) + @test OffsetArray(a, 0:1, :, 2:3, CartesianIndices(())) == OffsetArray(a, 0:1, :, 2:3) + + # nested OffsetArrays + for offsets in [(1,1,1), big.((1,1,1))] + ob = OffsetArray(oa, offsets); ob2 = OffsetArray(oa, offsets...); + @test eltype(ob) === eltype(ob2) === Float64 + @test axes(ob, 1) === axes(ob2, 1) === IdOffsetRange(Base.OneTo(2), 0) + @test axes(ob, 2) === axes(ob2, 2) === IdOffsetRange(Base.OneTo(2), 3) + @test axes(ob, 3) === axes(ob2, 3) === IdOffsetRange(Base.OneTo(2), 2) + end + + indices = (-1:1, -7:7, -1:2, -5:5, -1:1, -3:3, -2:2, -1:1) + y = OffsetArray{Float64}(undef, indices...); + @test axes(y) === axes(OffsetArray{Float64}(undef, indices)) + @test axes(y) === axes(OffsetArray{Float64, length(indices)}(undef, indices...)) + @test no_offset_axes(y) == (-1:1, -7:7, -1:2, -5:5, -1:1, -3:3, -2:2, -1:1) + @test eltype(y) === Float64 + + @test_throws ArgumentError OffsetArray{Float64, 2}(undef, indices) + @test_throws ArgumentError OffsetArray(y, indices[1:2]) + + @test ndims(OffsetArray(zeros(), ())) == 0 + @test Base.axes1(OffsetArray(zeros(), ())) === IdOffsetRange(Base.OneTo(1)) + + # eltype of an OffsetArray should match that of the parent (issue #162) + @test_throws TypeError OffsetArray{Float64,2,Matrix{ComplexF64}} + # 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),)) + end + + @testset "custom range types" begin + @testset "EllipsisNotation" begin + @testset "Vector" begin + v = rand(5) + @test axes(OffsetArray(v, ..)) == axes(v) + @test OffsetArray(v, ..) == OffsetArray(v, :) + @test axes(OffsetVector(v, ..)) == axes(v) + @test OffsetVector(v, ..) == OffsetVector(v, :) + + @test no_offset_axes(OffsetArray(v, .., 2:6)) == (2:6, ) + @test OffsetArray(v, .., 2:6) == OffsetArray(v, 2:6) + @test no_offset_axes(OffsetVector(v, .., 2:6)) == (2:6, ) + @test OffsetVector(v, .., 2:6) == OffsetVector(v, 2:6) + end + @testset "Matrix" begin + m = rand(2, 2) + @test axes(OffsetArray(m, ..)) == axes(m) + @test OffsetArray(m, ..) == OffsetArray(m, :, :) + @test axes(OffsetMatrix(m, ..)) == axes(m) + @test OffsetMatrix(m, ..) == OffsetMatrix(m, :, :) + + @test no_offset_axes(OffsetArray(m, .., 2:3)) == (axes(m, 1), 2:3) + @test OffsetArray(m, .., 2:3) == OffsetArray(m, :, 2:3) + @test no_offset_axes(OffsetMatrix(m, .., 2:3)) == (axes(m, 1), 2:3) + @test OffsetMatrix(m, .., 2:3) == OffsetMatrix(m, :, 2:3) + + @test no_offset_axes(OffsetArray(m, .., 2:3, 3:4)) == (2:3, 3:4) + @test OffsetArray(m, .., 2:3, 3:4) == OffsetArray(m, 2:3, 3:4) + @test no_offset_axes(OffsetMatrix(m, .., 2:3, 3:4)) == (2:3, 3:4) + @test OffsetMatrix(m, .., 2:3, 3:4) == OffsetMatrix(m, 2:3, 3:4) + end + @testset "3D Array" begin + a = rand(2, 2, 2) + @test axes(OffsetArray(a, ..)) == axes(a) + @test OffsetArray(a, ..) == OffsetArray(a, :, :, :) + + @test no_offset_axes(OffsetArray(a, .., 2:3)) == (axes(a)[1:2]..., 2:3) + @test OffsetArray(a, .., 2:3) == OffsetArray(a, :, :, 2:3) + + @test no_offset_axes(OffsetArray(a, .., 2:3, 3:4)) == (axes(a, 1), 2:3, 3:4) + @test OffsetArray(a, .., 2:3, 3:4) == OffsetArray(a, :, 2:3, 3:4) + + @test no_offset_axes(OffsetArray(a, 2:3, .., 3:4)) == (2:3, axes(a, 2), 3:4) + @test OffsetArray(a, 2:3, .., 3:4) == OffsetArray(a, 2:3, :, 3:4) + + @test no_offset_axes(OffsetArray(a, .., 4:5, 2:3, 3:4)) == (4:5, 2:3, 3:4) + @test OffsetArray(a, .., 4:5, 2:3, 3:4) == OffsetArray(a, 4:5, 2:3, 3:4) + end + end + @testset "ZeroBasedIndexing" begin + Base.to_indices(A, inds, ::Tuple{ZeroBasedIndexing}) = map(x -> 0:length(x) - 1, inds) + + a = zeros(3,3) + oa = OffsetArray(a, ZeroBasedIndexing()) + @test no_offset_axes(oa) == (0:2, 0:2) + end + @testset "TupleOfRanges" begin + Base.to_indices(A, inds, t::Tuple{TupleOfRanges{N}}) where {N} = t + OffsetArraysCore.AxisConversionStyle(::Type{TupleOfRanges{N}}) where {N} = + OffsetArraysCore.TupleOfRanges() + + Base.convert(::Type{Tuple{Vararg{AbstractUnitRange{Int}}}}, t::TupleOfRanges) = t.x + + a = zeros(3,3) + inds = TupleOfRanges((3:5, 2:4)) + oa = OffsetArray(a, inds) + @test no_offset_axes(oa) == inds.x + end + @testset "NewColon" begin + Base.to_indices(A, inds, t::Tuple{NewColon,Vararg{Any}}) = + (_uncolon(inds, t), to_indices(A, Base.tail(inds), Base.tail(t))...) + + _uncolon(inds::Tuple{}, I::Tuple{NewColon, Vararg{Any}}) = OneTo(1) + _uncolon(inds::Tuple, I::Tuple{NewColon, Vararg{Any}}) = inds[1] + + a = zeros(3, 3) + oa = OffsetArray(a, (NewColon(), 2:4)) + @test no_offset_axes(oa) == (axes(a,1), 2:4) + end + end + + @testset "Offset range construction" begin + r = -2:5 + for AT in Any[OffsetArray, OffsetVector] + y = AT(r, r) + @test no_offset_axes(y) == (r,) + @test step(y) == step(r) + y = AT(r, (r,)) + @test no_offset_axes(y) == (r,) + y = AT(r, CartesianIndices((r, ))) + @test no_offset_axes(y) == (r, ) + end + end + + @testset "size/length" begin + for p in Any[SA[1,2,3,4], 1:4, [1:4;]] + for A in Any[OffsetArray(p, 4), + OffsetArray(reshape(p, 2, 2), 3, 4), + OffsetArray(reshape(p, 2, 1, 2), 3, 0, 4), + OffsetArray(reshape(p, Val(1)), 2)] + @test size(A) == size(parent(A)) + @test length(A) == length(parent(A)) + end + end + end +end + +@testset "Axes supplied to constructor correspond to final result" begin + # Ref https://github.com/JuliaArrays/OffsetArrays.jl/pull/65#issuecomment-457181268 + B = BidirectionalVector([1, 2, 3], -2) + A = OffsetArray(B, -1:1) + @test no_offset_axes(A) == (-1:1,) +end + +@testset "view" begin + A0 = [1 3; 2 4] + A = OffsetArray(A0, (-1,2)) + + S = view(A, :, 3) + @test S == OffsetArray([1,2], (A.offsets[1],)) + @test S[0] == 1 + @test S[1] == 2 + @test_throws BoundsError S[2] + @test axes(S) == (IdentityUnitRange(0:1),) + S = view(A, 0, :) + @test S == OffsetArray([1,3], (A.offsets[2],)) + @test S[3] == 1 + @test S[4] == 3 + @test_throws BoundsError S[1] + @test axes(S) == (IdentityUnitRange(3:4),) + S = view(A, 0:0, 4) + @test S == [3] + @test S[1] == 3 + @test_throws BoundsError S[0] + @test axes(S) === (Base.OneTo(1),) + S = view(A, 1, 3:4) + @test S == [2,4] + @test S[1] == 2 + @test S[2] == 4 + @test_throws BoundsError S[3] + @test axes(S) === (Base.OneTo(2),) + S = view(A, :, :) + @test S == A + @test S[0,3] == S[1] == 1 + @test S[1,3] == S[2] == 2 + @test S[0,4] == S[3] == 3 + @test S[1,4] == S[4] == 4 + @test_throws BoundsError S[1,1] + @test axes(S) == IdentityUnitRange.((0:1, 3:4)) + S = view(A, axes(A)...) + @test S == A + @test S[0,3] == S[1] == 1 + @test S[1,3] == S[2] == 2 + @test S[0,4] == S[3] == 3 + @test S[1,4] == S[4] == 4 + @test_throws BoundsError S[1,1] + @test no_offset_axes(S) == (0:1, 3:4) + # issue 100 + S = view(A, axes(A, 1), 3) + @test S == A[:, 3] + @test S[0] == 1 + @test S[1] == 2 + @test_throws BoundsError S[length(S)] + @test no_offset_axes(S) == (0:1, ) + # issue 100 + S = view(A, 1, axes(A, 2)) + @test S == A[1, :] + @test S[3] == 2 + @test S[4] == 4 + @test_throws BoundsError S[1] + @test no_offset_axes(S) == (3:4, ) + + # issue 133 + r = IdOffsetRange(1:2, -1) + v1 = view(A, r, 3) + @test v1[0] == 1 + @test v1[1] == 2 + @test axes(v1, 1) == axes(r, 1) + v2 = view(A, UnitRange(r), 3) + for (indflat, indoffset) in enumerate(r) + @test v1[indoffset] == v2[indflat] + end + + # issue 133 + r = IdOffsetRange(1:2, 2) + v1 = view(A, 1, r) + @test v1[3] == 2 + @test v1[4] == 4 + @test axes(v1, 1) == axes(r, 1) + v2 = view(A, 1, UnitRange(r)) + for (indflat, indoffset) in enumerate(r) + @test v1[indoffset] == v2[indflat] + end + + # issue 133 + a12 = zeros(3:8, 3:4) + r = IdOffsetRange(Base.OneTo(3), 5) + a12[r, 4] .= 3 + @test all(a12[r, 4] .== 3) + @test all(a12[UnitRange(r), 4] .== 3) + + A0 = collect(reshape(1:24, 2, 3, 4)) + A = OffsetArray(A0, (-1,2,1)) + S = view(A, axes(A, 1), 3:4, axes(A, 3)) + @test S == A[:, 3:4, :] + @test S[0, 1, 2] == A[0, 3, 2] + @test S[0, 2, 2] == A[0, 4, 2] + @test S[1, 1, 2] == A[1, 3, 2] + @test no_offset_axes(S) == (0:1, Base.OneTo(2), 2:5) + + # issue #186 + a = reshape(1:12, 3, 4) + r = IdOffsetRange(3:4) + av = view(a, :, r) + @test av == a[:, 3:4] + @test axes(av) == (axes(a,1), axes(r,1)) + r = IdOffsetRange(1:2,2) + av = view(a, :, r) + @test no_offset_view(av) == a[:, 3:4] + @test axes(av) == (axes(a,1), axes(r,1)) + r = IdOffsetRange(2:3) + av1d = view(a, r, 3) + @test av1d == a[2:3, 3] + @test axes(av1d) == (axes(r,1),) + r = IdOffsetRange(Base.OneTo(2), 1) + av1d = view(a, r, 3) + @test no_offset_view(av1d) == a[2:3, 3] + @test axes(av1d) == (axes(r,1),) + + # fix IdOffsetRange(::IdOffsetRange, offset) nesting from #178 + b = 1:20 + bov = OffsetArray(view(b, 3:4), 3:4) + c = @view b[bov] + @test same_value(c, 3:4) + @test no_offset_axes(c,1) == 3:4 + d = OffsetArray(c, 1:2) + @test same_value(d, c) + @test axes(d,1) == 1:2 + + # Issue 128 + a = OffsetArray(1:3, 0:2); + b = @view a[0] + @test b[] == b[1] == 1 + + a = reshape(1:16, 4, 4); + for ax1 in (:, axes(a,1), UnitRange(axes(a,1))), + ax2 in (:, axes(a,2), UnitRange(axes(a,2))) + av = @view a[ax1, ax2] + av_nooffset = no_offset_view(av) + @test axes(av_nooffset) === axes(av) + end +end + + +@testset "iteration" begin + A0 = [1 3; 2 4] + A = OffsetArray(A0, (-1,2)) + + let a + for (a,d) in zip(A, A0) + @test a == d + end + end + + v = ones(10) + for r in Any[1:1:10, 1:10], s in Any[r, collect(r)] + so = OffsetArray(s) + @test Float64[v[i] for i in s] == Float64[v[i] for i in so] + end +end + +@testset "show/summary" begin + A0 = [1 3; 2 4] + A = OffsetArray(A0, (-1,2)) + S = OffsetArray(view(A0, 1:2, 1:2), (-1,2)) + + @test sprint(show, A) == "[1 3; 2 4]" + @test sprint(show, S) == "[1 3; 2 4]" + strs = split(strip(sprint(show, MIME("text/plain"), A)), '\n') + @test strs[2] == " 1 3" + @test strs[3] == " 2 4" + v = OffsetArray(rand(3), (-2,)) + @test sprint(show, v) == sprint(show, parent(v)) + function cmp_showf(printfunc, A) + str1 = sprint(printfunc, A, context=(:limit=>true, :compact=>true)) + str2 = sprint(printfunc, parent(A), context=(:limit=>true, :compact=>true)) + @test str1 == str2 + end + cmp_showf(Base.print_matrix, OffsetArray(rand(5,5), (10,-9))) # rows&cols fit + cmp_showf(Base.print_matrix, OffsetArray(rand(10^3,5), (10,-9))) # columns fit + cmp_showf(Base.print_matrix, OffsetArray(rand(5,10^3), (10,-9))) # rows fit + cmp_showf(Base.print_matrix, OffsetArray(rand(10^3,10^3), (10,-9))) # neither fits + + a = OffsetArray([1 2; 3 4], -1:0, 5:6) + shownsz = VERSION >= v"1.2.0-DEV.229" ? Base.dims2string(size(a))*' ' : "" + @test summary(a) == "$(shownsz)OffsetArray(::$(typeof(parent(a))), -1:0, 5:6) with eltype $(Int) with indices -1:0×5:6" + shownsz = VERSION >= v"1.2.0-DEV.229" ? Base.dims2string(size(view(a, :, 5)))*' ' : "" + @test summary(view(a, :, 5)) == "$(shownsz)view(OffsetArray(::$(typeof(parent(a))), -1:0, 5:6), :, 5) with eltype $(Int) with indices -1:0" + a = OffsetArray(reshape([1])) + @test summary(a) == "0-dimensional OffsetArray(::$(typeof(parent(a)))) with eltype $(Int)" + + a = OffsetArray([1 2; 3 4], -1:0, 5:6) + @test occursin("IdOffsetRange(values=-1:0, indices=-1:0)", sprint(show, axes(a, 1))) + @test occursin("IdOffsetRange(values=5:6, indices=5:6)", sprint(show, axes(a, 2))) + rrtable = IdOffsetRange(values=7:9, indices=-1:1) + rrted = eval(Meta.parse(string(rrtable))) + @test pairs(rrtable) == pairs(rrted) + + @test Base.inds2string(axes(a)) == Base.inds2string(map(UnitRange, axes(a))) + @test Base.inds2string((IdOffsetRange(3:4),)) == "3:4" + @test Base.inds2string((IdentityUnitRange(IdOffsetRange(3:4)),)) == "3:4" + # check that the following doesn't throw + @test Base.inds2string(()) isa Any + + @test sprint(show, OffsetArray(3:5, 0:2)) == "3:5 with indices 0:2" + + @test sprint(show, MIME"text/plain"(), OffsetArray(3:5, 0:2)) == "3:5 with indices 0:2" + + # issue #198 + for r in Any[axes(OffsetVector(1:10, -5), 1), 1:1:2, 1.0:1.0:2.0, 1:-1:-5] + a = OffsetVector(r, 5) + @test sprint(show, a) == "$r with indices $(UnitRange(axes(a,1)))" + end + + d = Diagonal([1,2,3]) + s1 = sprint(Base.print_array, d) + od = OffsetArray(d, -1:1, 3:5) + s2 = sprint(Base.print_array, od) + @test s1 == s2 + + @test Base.replace_in_print_matrix(od, -1, 3, " ") == Base.replace_in_print_matrix(d, 1, 1, " ") + @test Base.replace_in_print_matrix(od, -1, 4, " ") == Base.replace_in_print_matrix(d, 1, 2, " ") + + v = rand(3) + ov = OffsetArray(v, (-2,)) + @test Base.replace_in_print_matrix(ov, -1, 1, " ") == Base.replace_in_print_matrix(v, 1, 1, " ") + + # Avoid returning the value of toplevel if it is false + # showarg should only print values, it shouldn't return anything + io = IOBuffer() + @test Base.showarg(io, a, false) === nothing + # check the other case too for good measure + @test Base.showarg(io, a, true) === nothing +end + +@testset "readdlm/writedlm" begin + A0 = [1 3; 2 4] + A = OffsetArray(A0, (-1,2)) + + io = IOBuffer() + writedlm(io, A) + seek(io, 0) + @test readdlm(io, eltype(A)) == parent(A) +end + +@testset "similar" begin + A0 = [1 3; 2 4] + A = OffsetArray(A0, (-1,2)) + + B = similar(A, Float32) + @test isa(B, OffsetArray{Float32,2}) + @test axes(B) === axes(A) + B = similar(A, (3,4)) + @test isa(B, Array{Int,2}) + @test size(B) == (3,4) + @test axes(B) === (Base.OneTo(3), Base.OneTo(4)) + B = similar(A, (-3:3,1:4)) + @test isa(B, OffsetArray{Int,2}) + @test axes(B) == IdentityUnitRange.((-3:3, 1:4)) + + idunitrange(x::AbstractUnitRange) = IdOffsetRange(values=x, indices=x) + idunitrange(x::Integer) = x + + B = similar(parent(A), idunitrange.((-3:3,1:4))) + @test isa(B, OffsetArray{Int,2}) + @test axes(B) == IdentityUnitRange.((-3:3, 1:4)) + @test isa([x for x in [1,2,3]], Vector{Int}) + @test similar(Array{Int}, idunitrange.((0:0, 0:0))) isa OffsetArray{Int, 2} + @test similar(Array{Int}, (1, 1)) isa Matrix{Int} + @test similar(Array{Int}, (Base.OneTo(1), Base.OneTo(1))) isa Matrix{Int} + + # check with an unseen axis type + A = similar(ones(1), Int, ZeroBasedUnitRange(3:4), 4:5) + @test eltype(A) === Int + @test no_offset_axes(A) == (3:4, 4:5) + A = similar(ones(1), Int, IdOffsetRange(ZeroBasedUnitRange(3:4), 2), 4:5) + @test eltype(A) === Int + @test no_offset_axes(A) == (5:6, 4:5) + A = similar(ones(1), Int, IdOffsetRange(ZeroBasedUnitRange(3:4), 2), 4) + @test eltype(A) === Int + @test no_offset_axes(A) == (5:6, 1:4) + + @testset "similar with OffsetArray type (issue #263)" begin + for i in Any[[1,2,3], 1:3, reshape(1:4, 2, 2)] + k = OffsetArray(i, map(x -> -2, size(i))) + j = similar(typeof(k), axes(k)) + @test axes(j) == axes(k) + @test eltype(j) == eltype(k) + j = similar(typeof(k), size(k)) + @test eltype(j) == eltype(k) + @test size(j) == size(k) + @test all(==(1), first.(axes(j))) + end + end +end + +@testset "copyto!" begin + a = OffsetArray{Int}(undef, (-3:-1,)) + fill!(a, -1) + copyto!(a, (1,2)) # non-array iterables + @test a[-3] == 1 + @test a[-2] == 2 + @test a[-1] == -1 + fill!(a, -1) + copyto!(a, -2, (1,2)) + @test a[-3] == -1 + @test a[-2] == 1 + @test a[-1] == 2 + @test_throws BoundsError copyto!(a, 1, (1,2)) + fill!(a, -1) + copyto!(a, -2, (1,2,3), 2) + @test a[-3] == -1 + @test a[-2] == 2 + @test a[-1] == 3 + @test_throws BoundsError copyto!(a, -2, (1,2,3), 1) + fill!(a, -1) + copyto!(a, -2, (1,2,3), 1, 2) + @test a[-3] == -1 + @test a[-2] == 1 + @test a[-1] == 2 + + b = 1:2 # copy between AbstractArrays + bo = OffsetArray(1:2, (-3,)) + if VERSION < v"1.5-" + @test_throws BoundsError copyto!(a, b) + fill!(a, -1) + copyto!(a, bo) + @test a[-3] == -1 + @test a[-2] == 1 + @test a[-1] == 2 + else + # the behavior of copyto! is corrected as the documentation says "first n element" + # https://github.com/JuliaLang/julia/pull/34049 + fill!(a, -1) + copyto!(a, bo) + @test a[-3] == 1 + @test a[-2] == 2 + @test a[-1] == -1 + end + fill!(a, -1) + copyto!(a, -2, bo) + @test a[-3] == -1 + @test a[-2] == 1 + @test a[-1] == 2 + @test_throws BoundsError copyto!(a, -4, bo) + @test_throws BoundsError copyto!(a, -1, bo) + fill!(a, -1) + copyto!(a, -3, b, 2) + @test a[-3] == 2 + @test a[-2] == a[-1] == -1 + @test_throws BoundsError copyto!(a, -3, b, 1, 4) + am = OffsetArray{Int}(undef, (1:1, 7:9)) # for testing linear indexing + fill!(am, -1) + copyto!(am, b) + @test am[1] == 1 + @test am[2] == 2 + @test am[3] == -1 + @test am[1,7] == 1 + @test am[1,8] == 2 + @test am[1,9] == -1 +end + +@testset "map" begin + am = OffsetArray{Int}(undef, (1:1, 7:9)) # for testing linear indexing + fill!(am, -1) + copyto!(am, 1:2) + + dest = similar(am) + map!(+, dest, am, am) + @test dest[1,7] == 2 + @test dest[1,8] == 4 + @test dest[1,9] == -2 + + @testset "eltype conversion" begin + a = OffsetArray(1:2, 1) + b = map(BigInt, a) + @test eltype(b) == BigInt + @test b == a + @test parent(b) isa AbstractRange + + for ri in Any[2:3, Base.OneTo(2)] + for r in Any[IdOffsetRange(ri), IdOffsetRange(ri, 1), OffsetArray(ri), OffsetArray(ri, 2)] + for T in [Int8, Int16, Int32, Int64, Int128, BigInt, Float32, Float64, BigFloat] + r2 = map(T, r) + @test eltype(r2) == T + @test axes(r2) == axes(r) + @test all(((x,y),) -> isequal(x,y), zip(r, r2)) + end + end + end + + @testset "Bool" begin + for ri in Any[0:0, 0:1, 1:0, 1:1, Base.OneTo(0), Base.OneTo(1)] + for r = Any[IdOffsetRange(ri), IdOffsetRange(ri .- 1, 1), OffsetVector(ri)] + r2 = map(Bool, r) + @test eltype(r2) == Bool + @test axes(r2) == axes(r) + @test all(((x,y),) -> isequal(x,y), zip(r, r2)) + end + end + end + end +end + +@testset "reductions" begin + A = OffsetArray(rand(Int,4,4), (-3,5)) + @test maximum(A) == maximum(parent(A)) + @test minimum(A) == minimum(parent(A)) + @test extrema(A) == extrema(parent(A)) + @test sum(A) == sum(parent(A)) + @test sum(A, dims=1) == OffsetArray(sum(parent(A), dims=1), A.offsets) + @test sum(A, dims=2) == OffsetArray(sum(parent(A), dims=2), A.offsets) + @test sum(A, dims=(1,2)) == OffsetArray(sum(parent(A), dims=(1,2)), A.offsets) + @test sum(view(OffsetArray(reshape(1:27, 3, 3, 3), 0, 0, 0), :, :, 1:2), dims=(2,3)) == reshape([51,57,63], 3, 1, 1) + C = similar(A) + cumsum!(C, A, dims = 1) + @test parent(C) == cumsum(parent(A), dims = 1) + @test parent(cumsum(A, dims = 1)) == cumsum(parent(A), dims = 1) + cumsum!(C, A, dims = 2) + @test parent(C) == cumsum(parent(A), dims = 2) + R = similar(A, (1:1, 6:9)) + maximum!(R, A) + @test parent(R) == maximum(parent(A), dims = 1) + R = similar(A, (-2:1, 1:1)) + maximum!(R, A) + @test parent(R) == maximum(parent(A), dims = 2) + amin, iamin = findmin(A) + pmin, ipmin = findmin(parent(A)) + @test amin == pmin + @test A[iamin] == amin + @test amin == parent(A)[ipmin] + amax, iamax = findmax(A) + pmax, ipmax = findmax(parent(A)) + @test amax == pmax + @test A[iamax] == amax + @test amax == parent(A)[ipmax] + + amin, amax = extrema(parent(A)) + @test clamp.(A, (amax+amin)/2, amax) == OffsetArray(clamp.(parent(A), (amax+amin)/2, amax), axes(A)) + + @testset "mapreduce for OffsetRange" begin + rangelist = Any[ + # AbstractUnitRanges + 5:100, UnitRange(5.0, 20.0), false:true, + IdOffsetRange(4:5), + IdOffsetRange(1:10, 4), + # AbstractRanges + 2:4:14, 1.5:1.0:10.5, + ] + + for r in rangelist + + a = OffsetVector(r, 2); + @test mapreduce(identity, +, a) == mapreduce(identity, +, r) + @test mapreduce(x -> x^2, (x,y) -> x, a) == mapreduce(x -> x^2, (x,y) -> x, r) + + b = mapreduce(identity, +, a, dims = 1) + br = mapreduce(identity, +, r, dims = 1) + @test no_offset_view(b) == no_offset_view(br) + @test no_offset_axes(b, 1) == first(axes(a,1)):first(axes(a,1)) + + @test mapreduce(identity, +, a, init = 3) == mapreduce(identity, +, r, init = 3) + if VERSION >= v"1.2" + @test mapreduce((x,y) -> x*y, +, a, a) == mapreduce((x,y) -> x*y, +, r, r) + @test mapreduce((x,y) -> x*y, +, a, a, init = 10) == mapreduce((x,y) -> x*y, +, r, r, init = 10) + end + + for f in [sum, minimum, maximum] + @test f(a) == f(r) + + b = f(a, dims = 1); + br = f(r, dims = 1) + @test no_offset_view(b) == no_offset_view(br) + @test no_offset_axes(b, 1) == first(axes(a,1)):first(axes(a,1)) + + b = f(a, dims = 2); + br = f(r, dims = 2) + @test no_offset_view(b) == no_offset_view(br) + @test axes(b, 1) == axes(a,1) + end + + @test extrema(a) == extrema(r) + end + end +end + +@testset "Collections" begin + A = OffsetArray(rand(4,4), (-3,5)) + + @test unique(A, dims=1) == OffsetArray(parent(A), 0, first(axes(A, 2)) - 1) + @test unique(A, dims=2) == OffsetArray(parent(A), first(axes(A, 1)) - 1, 0) + v = OffsetArray(rand(8), (-2,)) + @test sort(v) == OffsetArray(sort(parent(v)), v.offsets) + @test sortslices(A; dims=1) == OffsetArray(sortslices(parent(A); dims=1), A.offsets) + @test sortslices(A; dims=2) == OffsetArray(sortslices(parent(A); dims=2), A.offsets) + @test sort(A, dims = 1) == OffsetArray(sort(parent(A), dims = 1), A.offsets) + @test sort(A, dims = 2) == OffsetArray(sort(parent(A), dims = 2), A.offsets) + + @test mapslices(sort, A, dims = 1) == OffsetArray(mapslices(sort, parent(A), dims = 1), A.offsets) + @test mapslices(sort, A, dims = 2) == OffsetArray(mapslices(sort, parent(A), dims = 2), A.offsets) +end + +@testset "rot/reverse" begin + A = OffsetArray(rand(4,4), (-3,5)) + + @test rotl90(A) == OffsetArray(rotl90(parent(A)), A.offsets[[2,1]]) + @test rotr90(A) == OffsetArray(rotr90(parent(A)), A.offsets[[2,1]]) + @test reverse(A, dims = 1) == OffsetArray(reverse(parent(A), dims = 1), A.offsets) + @test reverse(A, dims = 2) == OffsetArray(reverse(parent(A), dims = 2), A.offsets) +end + +@testset "broadcasting" begin + A = OffsetArray(rand(4,4), (-3,5)) + + @test A.+1 == OffsetArray(parent(A).+1, A.offsets) + @test 2*A == OffsetArray(2*parent(A), A.offsets) + @test A+A == OffsetArray(parent(A)+parent(A), A.offsets) + @test A.*A == OffsetArray(parent(A).*parent(A), A.offsets) +end + +@testset "Resizing OffsetVectors" begin + local a = OffsetVector(rand(5),-3) + axes(a,1) == -2:2 + length(a) == 5 + resize!(a,3) + length(a) == 3 + axes(a,1) == -2:0 + @test_throws ArgumentError resize!(a,-3) +end + +#### +#### type defined for testing no_offset_view +#### + +struct NegativeArray{T,N,S <: AbstractArray{T,N}} <: AbstractArray{T,N} + parent::S +end + +# Note: this defines the axes-of-the-axes to be OneTo. +# In general this isn't recommended, because +# positionof(A, i, j, ...) == map(getindex, axes(A), (i, j, ...)) +# is quite desirable, and this requires that the axes be "identity" ranges, i.e., +# `r[i] == i`. +# Nevertheless it's useful to test this on a "broken" implementation +# to make sure we still get the right answer. +Base.axes(A::NegativeArray) = map(n -> (-n):(-1), size(A.parent)) + +Base.size(A::NegativeArray) = size(A.parent) + +function Base.getindex(A::NegativeArray{T,N}, I::Vararg{Int,N}) where {T,N} + getindex(A.parent, (I .+ size(A.parent) .+ 1)...) +end + +struct PointlessWrapper{T,N, A <: AbstractArray{T,N}} <: AbstractArray{T,N} + parent :: A +end +Base.parent(x::PointlessWrapper) = x.parent +Base.size(x::PointlessWrapper) = size(parent(x)) +Base.axes(x::PointlessWrapper) = axes(parent(x)) +Base.getindex(x::PointlessWrapper, i...) = x.parent[i...] + +@testset "no offset view" begin + # OffsetArray fallback + A = randn(3, 3) + @inferred no_offset_view(A) + O1 = OffsetArray(A, -1:1, 0:2) + O2 = OffsetArray(O1, -2:0, -3:(-1)) + @test no_offset_view(O2) ≡ A + @inferred no_offset_view(O1) + @inferred no_offset_view(O2) + + P = PointlessWrapper(A) + @test @inferred(no_offset_view(P)) === P + @test @inferred(no_offset_view(A)) === A + a0 = reshape([1]) + @test @inferred(no_offset_view(a0)) === a0 + a0v = view(a0) + @test @inferred(no_offset_view(a0v)) === a0v + + # generic fallback + A = collect(reshape(1:12, 3, 4)) + N = NegativeArray(A) + @test N[-3, -4] == 1 + V = no_offset_view(N) + @test collect(V) == A + A = reshape(view([5], 1, 1)) + @test no_offset_view(A) == A + + # bidirectional + B = BidirectionalVector([1, 2, 3]) + pushfirst!(B, 0) + OB = no_offset_view(B) + @test no_offset_axes(OB, 1) == 1:4 + @test collect(OB) == 0:3 + + # issue #198 + offax = axes(OffsetVector(1:10, -5), 1) + noffax = no_offset_view(offax) + @test noffax == -4:5 + @test axes(noffax, 1) == 1:10 # ideally covered by the above, but current it isn't + @test isa(noffax, AbstractUnitRange) + + r = Base.OneTo(4) + @test no_offset_view(r) isa typeof(r) + + # SubArrays + A = reshape(1:12, 3, 4) + V = view(A, IdentityUnitRange(2:3), IdentityUnitRange(2:3)) + if collect(V) == [5 8; 6 9] # julia 1.0 has a bug here + @test no_offset_view(V) == [5 8; 6 9] + end + V = view(A, IdentityUnitRange(2:3), 2) + @test V != [5;6] + if collect(V) == [5;6] + @test no_offset_view(V) == [5;6] + end + O = OffsetArray(A, -1:1, 0:3) + V = view(O, 0:1, 1:2) + @test V == no_offset_view(V) == [5 8; 6 9] + r1, r2 = IdOffsetRange(1:3, -2), IdentityUnitRange(2:3) + V = view(O, r1, r2) + @test V != collect(V) + @test no_offset_view(V) == collect(V) + V = @view O[:,:] + @test IndexStyle(A) == IndexStyle(O) == IndexStyle(V) == IndexStyle(no_offset_view(V)) == IndexLinear() + + @testset "issue #375" begin + arr = OffsetArray(reshape(1:15, 3, 5), 2, 3) + arr_no_offset = no_offset_view(@view arr[:, 4]) + @test all(!Base.has_offset_axes, axes(arr_no_offset)) + end +end + +@testset "no nesting" begin + A = randn(2, 3) + x = A[2, 2] + O1 = OffsetArray(A, -1:0, -1:1) + O2 = OffsetArray(O1, 0:1, 0:2) + @test parent(O1) ≡ parent(O2) + @test eltype(O1) ≡ eltype(O2) + O2[1, 1] = x + 1 # just a sanity check + @test A[2, 2] == x + 1 +end + +@testset "mutating functions for OffsetVector" begin + # push! + o = OffsetVector(Int[], -1) + @test push!(o) === o + @test no_offset_axes(o, 1) == 0:-1 + @test push!(o, 1) === o + @test no_offset_axes(o, 1) == 0:0 + @test o[end] == 1 + @test push!(o, 2, 3) === o + @test no_offset_axes(o, 1) == 0:2 + @test o[end-1:end] == [2, 3] + # pop! + o = OffsetVector([1, 2, 3], -1) + @test pop!(o) == 3 + @test no_offset_axes(o, 1) == 0:1 + # append! + o = OffsetVector([1, 2, 3], -1) + append!(o, [4, 5]) + @test no_offset_axes(o, 1) == 0:4 + # empty! + o = OffsetVector([1, 2, 3], -1) + @test empty!(o) === o + @test no_offset_axes(o, 1) == 0:-1 +end + +@testset "searchsorted (#85)" begin + o = OffsetVector([1,3,4,5],-2) + @test searchsortedfirst(o,-2) == -1 + @test searchsortedfirst(o, 1) == -1 + @test searchsortedfirst(o, 2) == 0 + @test searchsortedfirst(o, 5) == 2 + @test searchsortedfirst(o, 6) == 3 + @test searchsortedlast(o, -2) == -2 + @test searchsortedlast(o, 1) == -1 + @test searchsortedlast(o, 2) == -1 + @test searchsortedlast(o, 5) == 2 + @test searchsortedlast(o, 6) == 2 + @test searchsorted(o, -2) == -1:-2 + @test searchsorted(o, 1) == -1:-1 + @test searchsorted(o, 2) == 0:-1 + @test searchsorted(o, 5) == 2:2 + @test searchsorted(o, 6) == 3:2 + + if VERSION > v"1.2" + # OffsetVector of another offset vector + v = OffsetVector(Base.IdentityUnitRange(4:10),-2) + @test searchsortedfirst(v, first(v)-1) == firstindex(v) + for i in axes(v,1) + @test searchsortedfirst(v, v[i]) == i + end + @test searchsortedfirst(v, last(v)+1) == lastindex(v)+1 + @test searchsortedlast(v, first(v)-1) == firstindex(v)-1 + for i in axes(v,1) + @test searchsortedlast(v, v[i]) == i + end + @test searchsortedlast(v, last(v)+1) == lastindex(v) + @test searchsorted(v, first(v)-1) === firstindex(v) .+ (0:-1) + for i in axes(v,1) + @test searchsorted(v, v[i]) == i:i + end + @test searchsorted(v, last(v)+1) === lastindex(v) .+ (1:0) + end + + v = OffsetVector{Float64, OffsetVector{Float64, Vector{Float64}}}(OffsetVector([2,2,3,3,3,4], 3), 4) + @test searchsortedfirst(v, minimum(v)-1) == firstindex(v) + for el in unique(v) + @test searchsortedfirst(v, el) == findfirst(isequal(el), v) + end + @test searchsortedfirst(v, maximum(v)+1) == lastindex(v)+1 + + @test searchsortedlast(v, minimum(v)-1) == firstindex(v)-1 + for el in unique(v) + @test searchsortedlast(v, el) == findlast(isequal(el), v) + end + @test searchsortedlast(v, maximum(v)+1) == lastindex(v) + + @test searchsorted(v, minimum(v)-1) === firstindex(v) .+ (0:-1) + for el in unique(v) + @test searchsorted(v, el) == findfirst(isequal(el), v):findlast(isequal(el), v) + end + @test searchsorted(v, maximum(v)+1) === lastindex(v) .+ (1:0) + + soa = OffsetArray([2,2,3], typemax(Int)-3) + @test searchsortedfirst(soa, 1) == firstindex(soa) == typemax(Int)-2 + @test searchsortedfirst(soa, 2) == firstindex(soa) == typemax(Int)-2 + @test searchsortedfirst(soa, 3) == lastindex(soa) == typemax(Int) + + soa = OffsetArray([2,2,3], typemin(Int)) + @test searchsortedlast(soa, 2) == firstindex(soa) + 1 == typemin(Int) + 2 + @test searchsortedlast(soa, 3) == lastindex(soa) == typemin(Int) + 3 + @test searchsortedlast(soa, 1) == typemin(Int) + + soa = OffsetArray([2,2,3], typemax(Int)-4) + @test searchsorted(soa, 1) === firstindex(soa) .+ (0:-1) + @test searchsorted(soa, 2) == firstindex(soa) .+ (0:1) == typemax(Int) .+ (-3:-2) + @test searchsorted(soa, 3) == lastindex(soa) .+ (0:0) == typemax(Int) .+ (-1:-1) + @test searchsorted(soa, 4) === lastindex(soa) .+ (1:0) + + soa = OffsetArray([2,2,3], typemax(Int)-3) + @test searchsorted(soa, 1) === firstindex(soa) .+ (0:-1) + @test searchsorted(soa, 2) == firstindex(soa) .+ (0:1) == typemax(Int) .+ (-2:-1) + @test searchsorted(soa, 3) == lastindex(soa) .+ (0:0) == typemax(Int) .+ (0:0) + @test searchsorted(soa, 4) === lastindex(soa) .+ (1:0) + + soa = OffsetArray([2,2,3], typemin(Int)) + @test searchsorted(soa, 1) === firstindex(soa) .+ (0:-1) + @test searchsorted(soa, 2) == firstindex(soa) .+ (0:1) == typemin(Int) .+ (1:2) + @test searchsorted(soa, 3) == lastindex(soa) .+ (0:0) == typemin(Int) .+ (3:3) + @test searchsorted(soa, 4) === lastindex(soa) .+ (1:0) +end + +@testset "Pointer" begin + a = OffsetVector(collect(10:20), 9); + @test 12 == a[12] == unsafe_load(pointer(a), 12 + (1 - firstindex(a))) == unsafe_load(pointer(a, 12)) + + A = OffsetArray(reshape(collect(10:130), (11,11)), 9, 9); + @test 21 == A[12] == unsafe_load(pointer(A), 12) == unsafe_load(pointer(A, 12)) + @test 61 == A[52] == unsafe_load(pointer(A), 52) == unsafe_load(pointer(A, 52)) + + @test pointer(a) === pointer(parent(a)) + @test pointer(A) === pointer(parent(A)) + @test pointer(a, 12) === pointer(parent(a), 12 + (1 - firstindex(a))) + @test pointer(A, 12) === pointer(parent(A), 12) + @test pointer(a) === pointer(a, firstindex(a)) + @test pointer(A) === pointer(A, firstindex(A)) + if VERSION ≥ v"1.5" + @test pointer(a') === pointer(parent(a)) + @test pointer(A') === pointer(parent(A)) + @test pointer(a', 5) === pointer(parent(a), 5) + @test pointer(A', 15) === pointer(parent(A)', 15) + end + + @test Base.cconvert(Ptr{eltype(A)}, A) == Base.cconvert(Ptr{eltype(A)}, parent(A)) +end + +struct Foo2 + o::OffsetArray{Float64,1,Array{Float64,1}} +end + +@testset "convert" begin + d = Diagonal([1,1,1]) + M = convert(Matrix{Float64}, d) + od = OffsetArray(d, 1, 1) + oM = convert(OffsetMatrix{Float64, Matrix{Float64}}, od) + @test eltype(oM) == Float64 + @test typeof(parent(oM)) == Matrix{Float64} + @test oM == od + oM2 = convert(OffsetMatrix{Float64, Matrix{Float64}}, d) + @test eltype(oM2) == Float64 + @test typeof(parent(oM2)) == Matrix{Float64} + @test oM2 == d + + # issue 171 + O = OffsetArray(zeros(Int, 3), -1) + F = Foo2(O) + @test F.o == O + + a = [MMatrix{2,2}(1:4) for i = 1:2] + oa = [OffsetArray(ai, 0, 0) for ai in a] + b = ones(2,2) + @test b * a == b * oa + + for a = [1:4, OffsetArray(ones(5))] + for T in [OffsetArray, OffsetVector, + OffsetArray{eltype(a)}, OffsetArray{Float32}, + OffsetVector{eltype(a)}, OffsetVector{Float32}, + OffsetVector{Float32, Vector{Float32}}, + OffsetVector{Float32, OffsetVector{Float32, Vector{Float32}}}, + OffsetVector{eltype(a), typeof(a)}, + ] + + @test convert(T, a) isa T + @test convert(T, a) == a + + b = T(a) + @test b isa T + @test b == a + + b = T(a, 0) + @test b isa T + @test b == a + + b = T(a, axes(a)) + @test b isa T + @test b == a + end + + a2 = reshape(a, :, 1) + for T in [OffsetArray{Float32}, OffsetMatrix{Float32}, OffsetArray{Float32, 2, Matrix{Float32}}] + b = T(a2, 0, 0) + @test b isa T + @test b == a2 + + b = T(a2, axes(a2)) + @test b isa T + @test b == a2 + + b = T(a2, 1, 1) + @test no_offset_axes(b) == map((x,y) -> x .+ y, axes(a2), (1,1)) + + b = T(a2) + @test b isa T + @test b == a2 + end + a3 = reshape(a, :, 1, 1) + for T in [OffsetArray{Float32}, OffsetArray{Float32, 3}, OffsetArray{Float32, 3, Array{Float32,3}}] + b = T(a3, 0, 0, 0) + @test b isa T + @test b == a3 + + b = T(a3, axes(a3)) + @test b isa T + @test b == a3 + + b = T(a3, 1, 1, 1) + @test no_offset_axes(b) == map((x,y) -> x .+ y, axes(a3), (1,1,1)) + + b = T(a3) + @test b isa T + @test b == a3 + end + end + + a = OffsetArray(ones(2), 1) + b = convert(OffsetArray, a) + @test a === b + b = convert(OffsetVector, a) + @test a === b + + # test that non-Int offsets work correctly + a = 1:4 + b1 = OffsetVector{Float64,Vector{Float64}}(a, 2) + b2 = OffsetVector{Float64,Vector{Float64}}(a, big(2)) + @test b1 == b2 + + a = OffsetArray(ones(2), 1) + b1 = OffsetArray{Float64, 1, typeof(a)}(a, (-1,)) + b2 = OffsetArray{Float64, 1, typeof(a)}(a, (-big(1),)) + @test b1 == b2 + + # test for custom offset arrays + a = ZeroBasedRange(1:3) + for T in [OffsetVector{Float64, UnitRange{Float64}}, OffsetVector{Int, Vector{Int}}, + OffsetVector{Float64,OffsetVector{Float64,UnitRange{Float64}}}, + OffsetArray{Int,1,OffsetArray{Int,1,UnitRange{Int}}}, + ] + + b = T(a) + @test b isa T + @test b == a + + b = T(a, 2:4) + @test b isa T + @test no_offset_axes(b, 1) == 2:4 + @test no_offset_view(b) == no_offset_view(a) + + b = T(a, 1) + @test b isa T + @test axes(b, 1) == 1:3 + @test no_offset_view(b) == no_offset_view(a) + + c = convert(T, a) + @test c isa T + @test c == a + end + + # test using custom indices + a = ones(2,2) + for T in [OffsetMatrix{Int}, OffsetMatrix{Float64}, OffsetMatrix{Float64, Matrix{Float64}}, + OffsetMatrix{Int, Matrix{Int}}] + + b = T(a, ZeroBasedIndexing()) + @test b isa T + @test no_offset_axes(b) == (0:1, 0:1) + end + + # changing the number of dimensions is not permitted + A = rand(2,2) + @test_throws MethodError convert(OffsetArray{Float64, 3}, A) + @test_throws MethodError convert(OffsetArray{Float64, 3, Array{Float64,3}}, A) +end + +@testset "Conversion to AbstractArray{T}" begin + r = 1:4 + T = Float64 + V = typeof(map(T, r)) + v = OffsetVector(r) + @test OffsetArray{T}(v) isa OffsetVector{T,V} + @test AbstractArray{T}(v) isa OffsetVector{T,V} + @test AbstractVector{T}(v) isa OffsetVector{T,V} + @test convert(AbstractVector{T}, v) isa OffsetVector{T,V} + @test convert(AbstractArray{T}, v) isa OffsetVector{T,V} + @test axes(OffsetArray{T}(v)) === axes(v) + @test axes(AbstractArray{T}(v)) === axes(v) + @test axes(AbstractVector{T}(v)) === axes(v) + @test axes(convert(AbstractVector{T}, v)) === axes(v) + @test axes(convert(AbstractArray{T}, v)) == axes(v) + + A = SMatrix{2,2}(1, 0, 0, 1) + TA = typeof(map(T, A)) + OA = OffsetMatrix(A, 3:4, 5:6) + @test OffsetArray{T}(OA) isa OffsetMatrix{T,TA} + @test AbstractArray{T}(OA) isa OffsetMatrix{T,TA} + @test AbstractMatrix{T}(OA) isa OffsetMatrix{T,TA} + @test convert(AbstractMatrix{T}, OA) isa OffsetMatrix{T,TA} + @test convert(AbstractArray{T}, OA) isa OffsetMatrix{T,TA} + @test axes(OffsetArray{T}(OA)) === axes(OA) + @test axes(AbstractArray{T}(OA)) === axes(OA) + @test axes(AbstractMatrix{T}(OA)) === axes(OA) + @test axes(convert(AbstractMatrix{T}, OA)) === axes(OA) + @test axes(convert(AbstractArray{T}, OA)) === axes(OA) +end + +@testset "unsafe_wrap" begin + p = Ptr{UInt16}(Libc.malloc(2*3*4*2)) + @test unsafe_wrap(OffsetArray, p, 2, 3, 4) isa OffsetArray{UInt16, 3} + @test unsafe_wrap(OffsetArray, p, (2, 3, 4)) isa OffsetArray{UInt16, 3} + @test unsafe_wrap(OffsetVector, p, 2*3*4) isa OffsetVector{UInt16} + @test unsafe_wrap(OffsetMatrix, p, 2*3, 4) isa OffsetMatrix{UInt16} + @test unsafe_wrap(OffsetArray{UInt16}, p, 2, 3, 4) isa OffsetArray{UInt16, 3} + @test unsafe_wrap(OffsetArray{UInt16}, p, (2, 3, 4)) isa OffsetArray{UInt16, 3} + @test unsafe_wrap(OffsetVector{UInt16}, p, 2*3*4) isa OffsetVector{UInt16} + @test unsafe_wrap(OffsetMatrix{UInt16}, p, 2*3, 4) isa OffsetMatrix{UInt16} + p = Ptr{UInt8}(p) + @test unsafe_wrap(OffsetArray, p, 2:3, 3:5, 4:7) isa OffsetArray{UInt8, 3} + @test unsafe_wrap(OffsetArray, p, (2:3, 3:5, 4:7)) isa OffsetArray{UInt8, 3} + @test unsafe_wrap(OffsetVector, p, 1:(2*3*4) .- 1) isa OffsetVector{UInt8} + @test unsafe_wrap(OffsetMatrix, p, 1:(2*3) .+ 6, 4:7) isa OffsetMatrix{UInt8} + @test unsafe_wrap(OffsetMatrix, p, -5:5, Base.OneTo(3); own = true) isa OffsetMatrix{UInt8} +end + +end \ No newline at end of file diff --git a/OffsetArraysCore/test/origin.jl b/OffsetArraysCore/test/origin.jl new file mode 100644 index 0000000..e1fcac3 --- /dev/null +++ b/OffsetArraysCore/test/origin.jl @@ -0,0 +1,87 @@ +module OriginTests + +using OffsetArraysCore +using OffsetArraysCore: Origin +using StaticArrays +using FillArrays +using Test + +@testset "Origin" begin + get_origin(A::AbstractArray) = first.(axes(A)) + + @test Origin(0) != Origin((0, )) + @test Origin(CartesianIndex(1, 2)) === Origin((1, 2)) === Origin(1, 2) + + @test Origin(Int32.((1,2))) == Origin(Int64.((1,2))) + @test Origin(Int32.((1,2))...) == Origin(Int64.((1,2))...) == Origin((1.0, 2.0)) + @test Origin(Int32(1)) == Origin(Int64(1)) == Origin(1.0) + @test_throws Exception Origin(1.5) + + # 0d + A = OffsetArray(zeros()) + B = OffsetArray(zeros(), Origin()) + @test axes(A) == axes(B) + + # 1d + v = [1, 2] + @test get_origin(OffsetArray(v, Origin(2))) == (2, ) + ov = OffsetArray(v, -3) + @test get_origin(OffsetArray(ov, Origin(2))) == (2, ) + @test get_origin(OffsetVector(ov, Origin(2))) == (2, ) + @test get_origin(OffsetArray(ov, Origin((2, )))) == (2, ) + + # 2d + a = [1 2;3 4] + @test get_origin(OffsetArray(a, Origin(0))) == (0, 0) + oa = OffsetArray(a, -3, -3) + @test get_origin(OffsetArray(oa, Origin(0))) == (0, 0) + @test get_origin(OffsetMatrix(oa, Origin(0))) == (0, 0) + @test get_origin(OffsetArray(oa, Origin(1, 2))) == (1, 2) + + # 3d + a = ones(3, 3, 3) + @test get_origin(OffsetArray(a, Origin(0))) == (0, 0, 0) + oa = OffsetArray(a, -3, -3, -3) + @test get_origin(OffsetArray(oa, Origin(0))) == (0, 0, 0) + @test get_origin(OffsetArray(oa, Origin(1, 2, 3))) == (1, 2, 3) + + # Scalar broadcasting + let + a = [ [1,2,3], [4,5,6] ] + oa = OffsetVector.(a, Origin(0)) + @test get_origin.(oa) == [ (0,), (0,) ] + + a = [ [1 2; 3 4], [5 6 7; 8 9 10] ] + oa = OffsetArray.(a, Origin(0, -1)) + @test get_origin.(oa) == [ (0,-1), (0,-1) ] + end + + @testset "as a callable" begin + a = [1 2; 3 4]; + @test OffsetArray(a, Origin(2)) == Origin(2)(a) + for (index, firstinds) in Any[(1, (1,1)), ((2,3), (2,3))] + b = Origin(index)(a) + @test first.(axes(b)) == firstinds + @test Origin(b) == Origin(firstinds) + @test Origin(OffsetArraysCore.no_offset_view(b)) == Origin(ntuple(_ -> 1, Val(ndims(b)))) + end + # compatibility with other array types + @test Origin(Ones(2,2)) == Origin(1,1) + @test Origin(SMatrix{2,2,Int,4}(1,2,3,4)) == Origin(1,1) + end + @testset "display" begin + io = IOBuffer() + show(io, Origin(1)) + @test String(take!(io)) == "Origin(1)" + show(io, Origin(1, 1)) + @test String(take!(io)) == "Origin(1, 1)" + end + + @testset "avoid overflow (issue #279)" begin + A = Origin(typemin(Int)+1)(rand(3,3)) + B = Origin(typemax(Int)-4)(A) + @test first.(axes(B)) == ntuple(_ -> typemax(Int)-4, Val(ndims(B))) + end +end + +end \ No newline at end of file diff --git a/OffsetArraysCore/test/runtests.jl b/OffsetArraysCore/test/runtests.jl new file mode 100644 index 0000000..0051aa6 --- /dev/null +++ b/OffsetArraysCore/test/runtests.jl @@ -0,0 +1,9 @@ +include("aqua.jl") +include("origin.jl") +include("axes.jl") +include("offsetarray.jl") +include("indexing.jl") +include("adapt.jl") +include("centered.jl") +include("misc.jl") +include("doctests.jl") \ No newline at end of file From 8f2d0190376f42bb0edcd662b3bb7d14049de2be Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Mon, 8 Dec 2025 13:38:37 +0530 Subject: [PATCH 2/6] Add CI for OffsetArraysCore --- .github/workflows/UnitTest core.yml | 45 +++++++++++++++++++++++++++++ .github/workflows/UnitTest.yml | 2 ++ 2 files changed, 47 insertions(+) create mode 100644 .github/workflows/UnitTest core.yml diff --git a/.github/workflows/UnitTest core.yml b/.github/workflows/UnitTest core.yml new file mode 100644 index 0000000..18d58b0 --- /dev/null +++ b/.github/workflows/UnitTest core.yml @@ -0,0 +1,45 @@ +name: Unit test + +on: + push: + tags: + - 'v*' + branches: + - master + paths: + - 'OffsetArraysCore/**' + pull_request: + paths: + - 'OffsetArraysCore/**' + +concurrency: + group: build-${{ github.event.pull_request.number || github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + # allow-failure is not supported yet + # https://github.com/actions/toolkit/issues/399 + fail-fast: false + matrix: + julia-version: ['min', 'lts', '1', 'pre'] + os: [ubuntu-latest, windows-latest, macOS-latest] + # only test one 32-bit job + include: + - os: ubuntu-latest + julia-version: '1' + julia-arch: x86 + + steps: + - uses: actions/checkout@v6 + - uses: julia-actions/setup-julia@v2 + - uses: julia-actions/cache@v2 + - name: Test OffsetArraysCore subpackage + run: julia --project=OffsetArraysCore -e 'using Pkg; Pkg.test()' + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: lcov.info diff --git a/.github/workflows/UnitTest.yml b/.github/workflows/UnitTest.yml index e02a962..293afb0 100644 --- a/.github/workflows/UnitTest.yml +++ b/.github/workflows/UnitTest.yml @@ -10,11 +10,13 @@ on: - 'LICENSE.md' - 'README.md' - '.github/workflows/TagBot.yml' + - 'OffsetArraysCore/**' pull_request: paths-ignore: - 'LICENSE.md' - 'README.md' - '.github/workflows/TagBot.yml' + - 'OffsetArraysCore/**' schedule: - cron: '20 00 1 * *' From 041eb9608bd3bed2cc17b870c7d90c26330a0cf7 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Mon, 8 Dec 2025 13:42:23 +0530 Subject: [PATCH 3/6] Rename test files --- .github/workflows/UnitTest core.yml | 2 +- .github/workflows/UnitTest.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/UnitTest core.yml b/.github/workflows/UnitTest core.yml index 18d58b0..04924c7 100644 --- a/.github/workflows/UnitTest core.yml +++ b/.github/workflows/UnitTest core.yml @@ -1,4 +1,4 @@ -name: Unit test +name: OffsetArraysCore Unit Tests on: push: diff --git a/.github/workflows/UnitTest.yml b/.github/workflows/UnitTest.yml index 293afb0..156cbd3 100644 --- a/.github/workflows/UnitTest.yml +++ b/.github/workflows/UnitTest.yml @@ -1,4 +1,4 @@ -name: Unit test +name: OffsetArrays Unit Tests on: push: From 766234befea303f3d9ad6bc191cbf8da308ec6b5 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Mon, 8 Dec 2025 15:23:04 +0530 Subject: [PATCH 4/6] Compute coverage in OffsetArraysCore tests --- .github/workflows/UnitTest core.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/UnitTest core.yml b/.github/workflows/UnitTest core.yml index 04924c7..e9c5205 100644 --- a/.github/workflows/UnitTest core.yml +++ b/.github/workflows/UnitTest core.yml @@ -37,7 +37,7 @@ jobs: - uses: julia-actions/setup-julia@v2 - uses: julia-actions/cache@v2 - name: Test OffsetArraysCore subpackage - run: julia --project=OffsetArraysCore -e 'using Pkg; Pkg.test()' + run: julia --project=OffsetArraysCore -e 'using Pkg; Pkg.test(coverage=true)' - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v5 with: From 5d5b0e3d999946c55c89379ff94e2814eb255281 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Mon, 8 Dec 2025 15:32:35 +0530 Subject: [PATCH 5/6] Add a custom invalidations check --- .github/workflows/UnitTest.yml | 2 + .../{UnitTest core.yml => UnitTest_core.yml} | 2 + .github/workflows/invalidations.yml | 5 ++- .github/workflows/invalidations_core.yml | 40 +++++++++++++++++++ 4 files changed, 48 insertions(+), 1 deletion(-) rename .github/workflows/{UnitTest core.yml => UnitTest_core.yml} (92%) create mode 100644 .github/workflows/invalidations_core.yml diff --git a/.github/workflows/UnitTest.yml b/.github/workflows/UnitTest.yml index d7aace9..990720e 100644 --- a/.github/workflows/UnitTest.yml +++ b/.github/workflows/UnitTest.yml @@ -10,12 +10,14 @@ on: - 'LICENSE.md' - 'README.md' - '.github/workflows/TagBot.yml' + - '.github/workflows/UnitTest_core.yml' - 'OffsetArraysCore/**' pull_request: paths-ignore: - 'LICENSE.md' - 'README.md' - '.github/workflows/TagBot.yml' + - '.github/workflows/UnitTest_core.yml' - 'OffsetArraysCore/**' schedule: - cron: '20 00 1 * *' diff --git a/.github/workflows/UnitTest core.yml b/.github/workflows/UnitTest_core.yml similarity index 92% rename from .github/workflows/UnitTest core.yml rename to .github/workflows/UnitTest_core.yml index e9c5205..98b900a 100644 --- a/.github/workflows/UnitTest core.yml +++ b/.github/workflows/UnitTest_core.yml @@ -8,9 +8,11 @@ on: - master paths: - 'OffsetArraysCore/**' + - '.github/workflows/UnitTest_core.yml' pull_request: paths: - 'OffsetArraysCore/**' + - '.github/workflows/UnitTest_core.yml' concurrency: group: build-${{ github.event.pull_request.number || github.ref }}-${{ github.workflow }} diff --git a/.github/workflows/invalidations.yml b/.github/workflows/invalidations.yml index 8f387e1..f13bc88 100644 --- a/.github/workflows/invalidations.yml +++ b/.github/workflows/invalidations.yml @@ -1,5 +1,8 @@ name: Invalidations -on: pull_request +on: + pull_request: + paths: + - 'src/**' jobs: evaluate: diff --git a/.github/workflows/invalidations_core.yml b/.github/workflows/invalidations_core.yml new file mode 100644 index 0000000..3ab8944 --- /dev/null +++ b/.github/workflows/invalidations_core.yml @@ -0,0 +1,40 @@ +name: Invalidations +on: + pull_request: + paths: + - 'OffsetArraysCore/src/**' + +jobs: + evaluate: + runs-on: ubuntu-latest + steps: + - uses: julia-actions/setup-julia@v2 + with: + version: '1.10' + - uses: actions/checkout@v6 + - uses: julia-actions/julia-buildpkg@latest + - uses: julia-actions/julia-invalidations@v1 + id: invs_pr + with: + test_script: 'import Pkg; Pkg.activate("OffsetArraysCore"); using OffsetArraysCore' + + - uses: actions/checkout@v6 + with: + ref: 'master' + - uses: julia-actions/julia-buildpkg@latest + - uses: julia-actions/julia-invalidations@v1 + id: invs_master + with: + test_script: 'import Pkg; Pkg.activate("OffsetArraysCore"); using OffsetArraysCore' + + - name: Report invalidation counts + run: | + echo "Invalidations on master: ${{ steps.invs_master.outputs.total }} (${{ steps.invs_master.outputs.deps }} via deps)" + echo "This branch: ${{ steps.invs_pr.outputs.total }} (${{ steps.invs_pr.outputs.deps }} via deps)" + shell: bash + - name: PR doesn't increase number of invalidations + run: | + if (( ${{ steps.invs_pr.outputs.total }} > ${{ steps.invs_master.outputs.total }} )); then + exit 1 + fi + shell: bash From f6b691d201bb1a2e6ec13e21af58665055ee1a04 Mon Sep 17 00:00:00 2001 From: Jishnu Bhattacharya Date: Tue, 9 Dec 2025 15:50:57 +0530 Subject: [PATCH 6/6] Remove invalidation script for subdir package --- .github/workflows/invalidations_core.yml | 40 ------------------------ 1 file changed, 40 deletions(-) delete mode 100644 .github/workflows/invalidations_core.yml diff --git a/.github/workflows/invalidations_core.yml b/.github/workflows/invalidations_core.yml deleted file mode 100644 index 3ab8944..0000000 --- a/.github/workflows/invalidations_core.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Invalidations -on: - pull_request: - paths: - - 'OffsetArraysCore/src/**' - -jobs: - evaluate: - runs-on: ubuntu-latest - steps: - - uses: julia-actions/setup-julia@v2 - with: - version: '1.10' - - uses: actions/checkout@v6 - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-invalidations@v1 - id: invs_pr - with: - test_script: 'import Pkg; Pkg.activate("OffsetArraysCore"); using OffsetArraysCore' - - - uses: actions/checkout@v6 - with: - ref: 'master' - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-invalidations@v1 - id: invs_master - with: - test_script: 'import Pkg; Pkg.activate("OffsetArraysCore"); using OffsetArraysCore' - - - name: Report invalidation counts - run: | - echo "Invalidations on master: ${{ steps.invs_master.outputs.total }} (${{ steps.invs_master.outputs.deps }} via deps)" - echo "This branch: ${{ steps.invs_pr.outputs.total }} (${{ steps.invs_pr.outputs.deps }} via deps)" - shell: bash - - name: PR doesn't increase number of invalidations - run: | - if (( ${{ steps.invs_pr.outputs.total }} > ${{ steps.invs_master.outputs.total }} )); then - exit 1 - fi - shell: bash