diff --git a/stdlib/Distributed/test/splitrange.jl b/stdlib/Distributed/test/splitrange.jl index 7b15593d21eaf..12bfb96650f6b 100644 --- a/stdlib/Distributed/test/splitrange.jl +++ b/stdlib/Distributed/test/splitrange.jl @@ -22,7 +22,7 @@ using Distributed: splitrange @test splitrange(-1, 1, 4) == Array{UnitRange{Int64},1}([-1:-1,0:0,1:1]) const BASE_TEST_PATH = joinpath(Sys.BINDIR, "..", "share", "julia", "test") -isdefined(Main, :OffsetArrays) || @eval Main include(joinpath($(BASE_TEST_PATH), "testhelpers", "OffsetArrays.jl")) +isdefined(Main, :OffsetArrays) || @eval Main @everywhere include(joinpath($(BASE_TEST_PATH), "testhelpers", "OffsetArrays.jl")) using .Main.OffsetArrays oa = OffsetArray([123, -345], (-2,)) diff --git a/test/arrayops.jl b/test/arrayops.jl index f44a6999013ea..59eae3409f601 100644 --- a/test/arrayops.jl +++ b/test/arrayops.jl @@ -1428,8 +1428,8 @@ end # non-1-indexed array oa = OffsetArray(Vector(1:10), -5) - filter!(x -> x > 5, oa) - @test oa == OffsetArray(Vector(6:10), -5) + oa = oa[oa.>5] # deleteat! is not supported for OffsetArrays + @test oa == Vector(6:10) # empty non-1-indexed array eoa = OffsetArray([], -5) diff --git a/test/offsetarray.jl b/test/offsetarray.jl index cd5c5bc848ace..bb0b4b13b8da4 100644 --- a/test/offsetarray.jl +++ b/test/offsetarray.jl @@ -6,8 +6,7 @@ using DelimitedFiles using Random using LinearAlgebra using Statistics - -const OAs_name = join(fullname(OffsetArrays), ".") +using Base: IdentityUnitRange if !isdefined(@__MODULE__, :T24Linear) include("testhelpers/arrayindexingtypes.jl") @@ -18,7 +17,7 @@ let v0 = rand(4) v = OffsetArray(v0, (-3,)) h = OffsetArray([-1,1,-2,2,0], (-3,)) -@test axes(v) == (-2:1,) +@test axes(v) === (OffsetArrays.IdOffsetRange(Base.OneTo(4), -3),) @test size(v) == (4,) @test size(v, 1) == 4 @test_throws DimensionMismatch Array(v) @@ -26,7 +25,7 @@ h = OffsetArray([-1,1,-2,2,0], (-3,)) 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) == (0:1, 3:4) +@test axes(A) === axes(S) === (OffsetArrays.IdOffsetRange(Base.OneTo(2), -1), OffsetArrays.IdOffsetRange(Base.OneTo(2), 2)) @test size(A) == (2,2) @test size(A, 1) == 2 @@ -133,13 +132,13 @@ S = view(A, :, 3) @test S[0] == 1 @test S[1] == 2 @test_throws BoundsError S[2] -@test axes(S) === (Base.IdentityUnitRange(0:1),) +@test axes(S) === (Base.IdentityUnitRange(OffsetArrays.IdOffsetRange(Base.OneTo(2), -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) === (Base.IdentityUnitRange(3:4),) +@test axes(S) === (Base.IdentityUnitRange(OffsetArrays.IdOffsetRange(Base.OneTo(2), 2)),) S = view(A, 0:0, 4) @test S == [3] @test S[1] == 3 @@ -158,17 +157,17 @@ S = view(A, :, :) @test S[0,4] == S[3] == 3 @test S[1,4] == S[4] == 4 @test_throws BoundsError S[1,1] -@test axes(S) === Base.IdentityUnitRange.((0:1, 3:4)) +@test axes(S) === Base.IdentityUnitRange.((OffsetArrays.IdOffsetRange(Base.OneTo(2), -1), OffsetArrays.IdOffsetRange(Base.OneTo(2), 2))) # https://github.com/JuliaArrays/OffsetArrays.jl/issues/27 g = OffsetArray(Vector(-2:3), (-3,)) gv = view(g, -1:2) @test axes(gv, 1) === Base.OneTo(4) @test collect(gv) == -1:2 gv = view(g, OffsetArray(-1:2, (-2,))) -@test axes(gv, 1) === Base.IdentityUnitRange(-1:2) +@test axes(gv, 1) === OffsetArrays.IdOffsetRange(Base.OneTo(4), -2) @test collect(gv) == -1:2 gv = view(g, OffsetArray(-1:2, (-1,))) -@test axes(gv, 1) === Base.IdentityUnitRange(0:3) +@test axes(gv, 1) === OffsetArrays.IdOffsetRange(Base.OneTo(4), -1) @test collect(gv) == -1:2 # iteration @@ -199,7 +198,7 @@ str = String(take!(io)) show(io, parent(v)) @test str == String(take!(io)) smry = summary(v) -@test occursin("OffsetArray{Float64, 1", smry) +@test occursin("OffsetArray(::Vector{Float64", smry) @test occursin("with indices -1:1", smry) function cmp_showf(printfunc, io, A; options = ()) ioc = IOContext(io, :limit => true, :compact => true, options...) @@ -216,11 +215,11 @@ cmp_showf(Base.print_matrix, io, OffsetArray(rand(10^3,10^3), (10,-9))) # neithe cmp_showf(Base.print_matrix, io, OffsetArray(reshape(range(-0.212121212121, stop=2/11, length=3*29), 3, 29), (-2, -15)); options=(:displaysize=>(53,210),)) cmp_showf(show, io, OffsetArray(collect(1:100), (100,))) # issue #31641 -targets1 = ["0-dimensional $OAs_name.OffsetArray{Float64, 0, Array{Float64, 0}}:\n1.0", - "1-element $OAs_name.OffsetArray{Float64, 1, Vector{Float64}} with indices 2:2:\n 1.0", - "1×1 $OAs_name.OffsetArray{Float64, 2, Matrix{Float64}} with indices 2:2×3:3:\n 1.0", - "1×1×1 $OAs_name.OffsetArray{Float64, 3, Array{Float64, 3}} with indices 2:2×3:3×4:4:\n[:, :, 4] =\n 1.0", - "1×1×1×1 $OAs_name.OffsetArray{Float64, 4, Array{Float64, 4}} with indices 2:2×3:3×4:4×5:5:\n[:, :, 4, 5] =\n 1.0"] +targets1 = ["0-dimensional OffsetArray(::Array{Float64, 0}) with eltype Float64:\n1.0", + "1-element OffsetArray(::Vector{Float64}, 2:2) with eltype Float64 with indices 2:2:\n 1.0", + "1×1 OffsetArray(::Matrix{Float64}, 2:2, 3:3) with eltype Float64 with indices 2:2×3:3:\n 1.0", + "1×1×1 OffsetArray(::Array{Float64, 3}, 2:2, 3:3, 4:4) with eltype Float64 with indices 2:2×3:3×4:4:\n[:, :, 4] =\n 1.0", + "1×1×1×1 OffsetArray(::Array{Float64, 4}, 2:2, 3:3, 4:4, 5:5) with eltype Float64 with indices 2:2×3:3×4:4×5:5:\n[:, :, 4, 5] =\n 1.0"] targets2 = ["(fill(1.0), fill(1.0))", "([1.0], [1.0])", "([1.0], [1.0])", @@ -235,7 +234,7 @@ targets2 = ["(fill(1.0), fill(1.0))", end P = OffsetArray(rand(8,8), (1,1)) PV = view(P, 2:3, :) -@test endswith(summary(PV), "with indices Base.OneTo(2)×2:9") +@test endswith(summary(PV), "with indices Base.OneTo(2)×OffsetArrays.IdOffsetRange(2:9)") # Similar B = similar(A, Float32) @@ -247,26 +246,26 @@ B = similar(A, (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) === Base.IdentityUnitRange.((-3:3, 1:4)) +@test axes(B) === (OffsetArrays.IdOffsetRange(Base.OneTo(7), -4), OffsetArrays.IdOffsetRange(Base.OneTo(4))) B = similar(parent(A), (-3:3,1:4)) @test isa(B, OffsetArray{Int,2}) -@test axes(B) === Base.IdentityUnitRange.((-3:3, 1:4)) +@test axes(B) === (OffsetArrays.IdOffsetRange(Base.OneTo(7), -4), OffsetArrays.IdOffsetRange(Base.OneTo(4))) # Indexing with OffsetArray indices i1 = OffsetArray([2,1], (-5,)) i1 = OffsetArray([2,1], -5) b = A0[i1, 1] -@test axes(b) === (Base.IdentityUnitRange(-4:-3),) +@test axes(b) === (OffsetArrays.IdOffsetRange(Base.OneTo(2), -5),) @test b[-4] == 2 @test b[-3] == 1 b = A0[1,i1] -@test axes(b) === (Base.IdentityUnitRange(-4:-3),) +@test axes(b) === (OffsetArrays.IdOffsetRange(Base.OneTo(2), -5),) @test b[-4] == 3 @test b[-3] == 1 v = view(A0, i1, 1) -@test axes(v) === (Base.IdentityUnitRange(-4:-3),) +@test axes(v) === (OffsetArrays.IdOffsetRange(Base.OneTo(2), -5),) v = view(A0, 1:1, i1) -@test axes(v) === (Base.OneTo(1), Base.IdentityUnitRange(-4:-3)) +@test axes(v) === (Base.OneTo(1), OffsetArrays.IdOffsetRange(Base.OneTo(2), -5)) # copyto! and fill! a = OffsetArray{Int}(undef, (-3:-1,)) @@ -395,7 +394,7 @@ v2 = copy(v) v = OffsetArray(v0, (-3,)) @test lastindex(v) == 1 @test v ≈ v -@test axes(v') === (Base.OneTo(1),Base.IdentityUnitRange(-2:1)) +@test axes(v') === (Base.OneTo(1), OffsetArrays.IdOffsetRange(Base.OneTo(4), -3)) @test parent(v) == collect(v) rv = reverse(v) @test axes(rv) == axes(v) @@ -411,7 +410,7 @@ A = OffsetArray(rand(4,4), (-3,5)) @test lastindex(A, 1) == 1 @test lastindex(A, 2) == 9 @test A ≈ A -@test axes(A') === Base.IdentityUnitRange.((6:9, -2:1)) +@test axes(A') === (OffsetArrays.IdOffsetRange(Base.OneTo(4), 5), OffsetArrays.IdOffsetRange(Base.OneTo(4), -3)) @test parent(copy(A')) == copy(parent(A)') @test collect(A) == parent(A) @test maximum(A) == maximum(parent(A)) @@ -583,9 +582,21 @@ module SimilarUR end ur = MyURange(1,3) a = Vector{Int}(undef, 2) - @test_throws MethodError similar(a, ur) - @test_throws MethodError similar(a, Float64, ur) - @test_throws MethodError similar(a, Float64, (ur,)) + + function catch_exception(f, args...) + try + f(args...) + catch err + return err + end + end + # type-piracy https://github.com/JuliaArrays/OffsetArrays.jl/issues/87 + @test_broken (catch_exception(similar, a, ur) isa MethodError) + @test_broken (catch_exception(similar, a, Float64, ur) isa MethodError) + @test_broken (catch_exception(similar, a, Float64, (ur,)) isa MethodError) + # @test_throws MethodError similar(a, ur) + # @test_throws MethodError similar(a, Float64, ur) + # @test_throws MethodError similar(a, Float64, (ur,)) @test_throws MethodError similar(a, (2.0,3.0)) end @@ -630,3 +641,101 @@ end @test last(v, 100) !== v @test last(v, 1) == [v[end]] 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 + +@testset "issue #37199: offset range indices" begin + # https://github.com/JuliaArrays/OffsetArrays.jl/issues/133 + A0 = [1 3; 2 4] + A = OffsetArray(A0, (-1,2)) + + r = OffsetArrays.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 + + r = OffsetArrays.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 + + a12 = zeros(3:8, 3:4) + r = OffsetArrays.IdOffsetRange(Base.OneTo(3), 5) + a12[r, 4] .= 3 + @test all(a12[r, 4] .== 3) + @test all(a12[UnitRange(r), 4] .== 3) + + # https://github.com/JuliaArrays/OffsetArrays.jl/issues/100 + 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 axes(S) == OffsetArrays.IdOffsetRange.((0:1, 3:4)) + 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 axes(S) == (OffsetArrays.IdOffsetRange(0:1), ) + 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 axes(S) == (OffsetArrays.IdOffsetRange(3:4), ) + + 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 axes(S) == (OffsetArrays.IdOffsetRange(0:1), Base.OneTo(2), OffsetArrays.IdOffsetRange(2:5)) +end + +@testset "IdentityUnitRange indexing" begin + 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 axes(y) == (r,) + @test step(y) == step(s) + + a = OffsetVector(3:4, 10:11) + ax = OffsetArrays.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 diff --git a/test/reinterpretarray.jl b/test/reinterpretarray.jl index 3791ad78db8d7..ee9e525c64d62 100644 --- a/test/reinterpretarray.jl +++ b/test/reinterpretarray.jl @@ -262,10 +262,10 @@ let a = [0.1 0.2; 0.3 0.4], at = reshape([(i,i+1) for i = 1:2:8], 2, 2) @test r[1,2] === reinterpret(Int64, v[1,2]) @test r[0,3] === reinterpret(Int64, v[0,3]) @test r[1,3] === reinterpret(Int64, v[1,3]) - @test_throws ArgumentError("cannot reinterpret a `Float64` array to `UInt32` when the first axis is Base.IdentityUnitRange(0:1). Try reshaping first.") reinterpret(UInt32, v) - @test_throws ArgumentError("`reinterpret(reshape, Tuple{Float64, Float64}, a)` where `eltype(a)` is Float64 requires that `axes(a, 1)` (got Base.IdentityUnitRange(0:1)) be equal to 1:2 (from the ratio of element sizes)") reinterpret(reshape, Tuple{Float64,Float64}, v) + @test_throws ArgumentError("cannot reinterpret a `Float64` array to `UInt32` when the first axis is OffsetArrays.IdOffsetRange(0:1). Try reshaping first.") reinterpret(UInt32, v) + @test_throws ArgumentError("`reinterpret(reshape, Tuple{Float64, Float64}, a)` where `eltype(a)` is Float64 requires that `axes(a, 1)` (got OffsetArrays.IdOffsetRange(0:1)) be equal to 1:2 (from the ratio of element sizes)") reinterpret(reshape, Tuple{Float64,Float64}, v) v = OffsetArray(a, (0, 1)) - @test axes(reinterpret(reshape, Tuple{Float64,Float64}, v)) === (Base.IdentityUnitRange(2:3),) + @test axes(reinterpret(reshape, Tuple{Float64,Float64}, v)) === (OffsetArrays.IdOffsetRange(Base.OneTo(2), 1),) r = reinterpret(UInt32, v) axsv = axes(v) @test axes(r) === (oftype(axsv[1], 1:4), axsv[2]) @@ -283,7 +283,7 @@ let a = [0.1 0.2; 0.3 0.4], at = reshape([(i,i+1) for i = 1:2:8], 2, 2) offsetvt = (-2, 4) vt = OffsetArray(at, offsetvt) istr = string(Int) - @test_throws ArgumentError("cannot reinterpret a `Tuple{$istr, $istr}` array to `$istr` when the first axis is Base.IdentityUnitRange(-1:0). Try reshaping first.") reinterpret(Int, vt) + @test_throws ArgumentError("cannot reinterpret a `Tuple{$istr, $istr}` array to `$istr` when the first axis is OffsetArrays.IdOffsetRange(-1:0). Try reshaping first.") reinterpret(Int, vt) vt = reshape(vt, 1:1, axes(vt)...) r = reinterpret(Int, vt) @test r == OffsetArray(reshape(1:8, 2, 2, 2), (0, offsetvt...)) diff --git a/test/testhelpers/OffsetArrays.jl b/test/testhelpers/OffsetArrays.jl index efebb74ded2d8..67de3ef476652 100644 --- a/test/testhelpers/OffsetArrays.jl +++ b/test/testhelpers/OffsetArrays.jl @@ -5,141 +5,438 @@ # This test file is designed to exercise support for generic indexing, # even though offset arrays aren't implemented in Base. +# OffsetArrays v1.3.0 +# No compat patch and docstrings module OffsetArrays -using Base: Indices, IndexCartesian, IndexLinear, tail +using Base: tail, @propagate_inbounds +using Base: IdentityUnitRange -export OffsetArray +export OffsetArray, OffsetMatrix, OffsetVector +struct IdOffsetRange{T<:Integer,I<:AbstractUnitRange{T}} <: AbstractUnitRange{T} + parent::I + offset::T + + IdOffsetRange{T,I}(r::I, offset::T) where {T<:Integer,I<:AbstractUnitRange{T}} = new{T,I}(r, offset) +end + +# 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)) +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)) +end +IdOffsetRange(r::AbstractUnitRange{T}, offset::Integer = 0) where T<:Integer = + IdOffsetRange{T,typeof(r)}(r, convert(T, offset)) + +# Coercion from other IdOffsetRanges +IdOffsetRange{T,I}(r::IdOffsetRange{T,I}) where {T<:Integer,I<:AbstractUnitRange{T}} = r +function IdOffsetRange{T,I}(r::IdOffsetRange) where {T<:Integer,I<:AbstractUnitRange{T}} + rc, offset = offset_coerce(I, r.parent) + return IdOffsetRange{T,I}(rc, r.offset+offset) +end +function IdOffsetRange{T}(r::IdOffsetRange) where T<:Integer + return IdOffsetRange(convert(AbstractUnitRange{T}, r.parent), r.offset) +end +IdOffsetRange(r::IdOffsetRange) = r + +# TODO: uncomment these when Julia is ready +# # Conversion preserves both the values and the indexes, 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{T} where T = + convert(I, r), 0 + +@inline Base.parent(r::IdOffsetRange) = r.parent +@inline Base.axes(r::IdOffsetRange) = (Base.axes1(r),) +@inline Base.axes1(r::IdOffsetRange) = IdOffsetRange(Base.axes1(r.parent), r.offset) +@inline Base.unsafe_indices(r::IdOffsetRange) = (r,) +@inline Base.length(r::IdOffsetRange) = length(r.parent) +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)) +for f in [:firstindex, :lastindex] + @eval Base.$f(r::IdOffsetRange) = $f(r.parent) .+ r.offset +end + +@inline function Base.iterate(r::IdOffsetRange) + ret = iterate(r.parent) + ret === nothing && return nothing + return (ret[1] + r.offset, ret[2]) +end +@inline function Base.iterate(r::IdOffsetRange, i) + ret = iterate(r.parent, i) + ret === nothing && return nothing + return (ret[1] + r.offset, ret[2]) +end + +@inline Base.first(r::IdOffsetRange) = first(r.parent) + r.offset +@inline Base.last(r::IdOffsetRange) = last(r.parent) + r.offset + +@propagate_inbounds Base.getindex(r::IdOffsetRange, i::Integer) = r.parent[i - r.offset] + r.offset +@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::AbstractUnitRange{<:Integer}) + return r.parent[s .- r.offset] .+ r.offset +end +@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::IdentityUnitRange) + return IdOffsetRange(r.parent[s .- r.offset], r.offset) +end +@propagate_inbounds function Base.getindex(r::IdOffsetRange, s::IdOffsetRange) + return IdOffsetRange(r.parent[s.parent .+ (s.offset - r.offset)] .+ (r.offset - s.offset), s.offset) +end + +# offset-preserve broadcasting +Broadcast.broadcasted(::Base.Broadcast.DefaultArrayStyle{1}, ::typeof(-), r::IdOffsetRange{T}, x::Integer) where T = + IdOffsetRange{T}(r.parent .- x, r.offset) +Broadcast.broadcasted(::Base.Broadcast.DefaultArrayStyle{1}, ::typeof(+), r::IdOffsetRange{T}, x::Integer) where T = + IdOffsetRange{T}(r.parent .+ x, r.offset) +Broadcast.broadcasted(::Base.Broadcast.DefaultArrayStyle{1}, ::typeof(+), x::Integer, r::IdOffsetRange{T}) where T = + IdOffsetRange{T}(x .+ r.parent, r.offset) + +Base.show(io::IO, r::IdOffsetRange) = print(io, "OffsetArrays.IdOffsetRange(", first(r), ':', last(r), ")") + +# Optimizations +@inline Base.checkindex(::Type{Bool}, inds::IdOffsetRange, i::Real) = Base.checkindex(Bool, inds.parent, i - inds.offset) + +struct Origin{T <: Union{Tuple,Int}} + index::T +end +Origin(I::NTuple{N,Int}) where N = Origin{typeof(I)}(I) +Origin(I::CartesianIndex) = Origin(I.I) +Origin(I1::Int, In::Int...) = Origin((I1, In...)) +# Origin(0) != Origin((0, )) but they work the same with broadcasting +Origin(n::Int) = Origin{Int}(n) + +(o::Origin)(A::AbstractArray) = o.index .- first.(axes(A)) + +### Low-level utilities ### + +_indexoffset(r::AbstractRange) = first(r) - 1 +_indexoffset(i::Integer) = 0 +_indexoffset(i::Colon) = 0 +_indexlength(r::AbstractRange) = length(r) +_indexlength(i::Integer) = i +_indexlength(i::Colon) = Colon() + +_offset(axparent::AbstractUnitRange, ax::AbstractUnitRange) = first(ax) - first(axparent) +_offset(axparent::AbstractUnitRange, ax::Integer) = 1 - first(axparent) + +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 + + +# Technically we know the length of CartesianIndices but we need to convert it first, so here we +# don't put it in OffsetAxisKnownLength. +const OffsetAxisKnownLength = Union{Integer,AbstractUnitRange} +const OffsetAxis = Union{OffsetAxisKnownLength,Colon} +const ArrayInitializer = Union{UndefInitializer,Missing,Nothing} + +## OffsetArray struct OffsetArray{T,N,AA<:AbstractArray} <: AbstractArray{T,N} parent::AA offsets::NTuple{N,Int} + function OffsetArray{T,N,AA}(parent::AA, offsets::NTuple{N,Int}) where {T,N,AA <: AbstractArray} + @boundscheck overflow_check.(axes(parent), offsets) + new{T,N,AA}(parent, offsets) + end end -OffsetVector{T,AA<:AbstractArray} = OffsetArray{T,1,AA} -OffsetArray(A::AbstractArray{T,N}, offsets::NTuple{N,Int}) where {T,N} = OffsetArray{T,N,typeof(A)}(A, offsets) -OffsetArray(A::AbstractArray{T,N}, offsets::Vararg{Int,N}) where {T,N} = OffsetArray(A, offsets) +const OffsetVector{T,AA <: AbstractArray} = OffsetArray{T,1,AA} + +const OffsetMatrix{T,AA <: AbstractArray} = OffsetArray{T,2,AA} -OffsetArray{T,N}(::UndefInitializer, inds::Indices{N}) where {T,N} = - OffsetArray{T,N,Array{T,N}}(Array{T,N}(undef, map(length, inds)), map(indsoffset, inds)) -OffsetArray{T}(::UndefInitializer, inds::Indices{N}) where {T,N} = - OffsetArray{T,N}(undef, inds) +function overflow_check(r, offset::T) where T + # This gives some performance boost https://github.com/JuliaLang/julia/issues/33273 + throw_upper_overflow_error() = throw(ArgumentError("Boundary overflow detected: offset $offset should be equal or less than $(typemax(T) - last(r))")) + throw_lower_overflow_error() = throw(ArgumentError("Boundary overflow detected: offset $offset should be equal or greater than $(typemin(T) - first(r))")) + + if offset > 0 && last(r) > typemax(T) - offset + throw_upper_overflow_error() + elseif offset < 0 && first(r) < typemin(T) - offset + throw_lower_overflow_error() + end +end -Base.IndexStyle(::Type{T}) where {T<:OffsetArray} = Base.IndexStyle(parenttype(T)) +# Tuples of integers are treated as offsets +# Empty Tuples are handled here +function OffsetArray(A::AbstractArray, offsets::Tuple{Vararg{Integer}}) + _checkindices(A, offsets, "offsets") + OffsetArray{eltype(A),ndims(A),typeof(A)}(A, offsets) +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 function $FT(A::AbstractArray{<:Any,$ND}, offsets::Tuple{Vararg{Integer}}) + _checkindices(A, offsets, "offsets") + OffsetArray{eltype(A),$ND,typeof(A)}(A, offsets) + end + FTstr = string(FT) + @eval function $FT(A::AbstractArray, offsets::Tuple{Vararg{Integer}}) + throw(ArgumentError($FTstr * " requires a " * string($ND) * "D array")) + end +end + +## OffsetArray constructors +for FT in (:OffsetArray, :OffsetVector, :OffsetMatrix) + # Nested OffsetArrays may strip off the wrapper and collate the offsets + @eval function $FT(A::OffsetArray, offsets::Tuple{Vararg{Integer}}) + _checkindices(A, offsets, "offsets") + $FT(parent(A), map(+, A.offsets, offsets)) + end + + # In general, indices get converted to AbstractUnitRanges. + # CartesianIndices{N} get converted to N ranges + @eval function $FT(A::AbstractArray, inds::Tuple{Any,Vararg{Any}}) + $FT(A, _toAbstractUnitRanges(to_indices(A, axes(A), inds))) + end + + # convert ranges to offsets + @eval function $FT(A::AbstractArray, inds::Tuple{AbstractUnitRange,Vararg{AbstractUnitRange}}) + _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)) + end + + @eval $FT(A::AbstractArray, inds::Vararg) = $FT(A, inds) + + @eval $FT(A::AbstractArray, origin::Origin) = $FT(A, origin(A)) +end + +# array initialization +function OffsetArray{T,N}(init::ArrayInitializer, inds::Tuple{Vararg{OffsetAxisKnownLength}}) where {T,N} + _checkindices(N, inds, "indices") + AA = Array{T,N}(init, map(_indexlength, inds)) + OffsetArray{T,N,typeof(AA)}(AA, map(_indexoffset, inds)) +end +function OffsetArray{T,N}(init::ArrayInitializer, inds::Tuple) where {T,N} + OffsetArray{T,N}(init, _toAbstractUnitRanges(inds)) +end +OffsetArray{T,N}(init::ArrayInitializer, inds::Vararg) where {T,N} = OffsetArray{T,N}(init, inds) + +OffsetArray{T}(init::ArrayInitializer, inds::NTuple{N,OffsetAxisKnownLength}) where {T,N} = OffsetArray{T,N}(init, inds) +function OffsetArray{T}(init::ArrayInitializer, inds::Tuple) where {T} + OffsetArray{T}(init, _toAbstractUnitRanges(inds)) +end +OffsetArray{T}(init::ArrayInitializer, inds::Vararg) where {T} = OffsetArray{T}(init, inds) + +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 -Base.size(A::OffsetArray) = size(A.parent) -Base.size(A::OffsetArray, d) = size(A.parent, d) Base.eachindex(::IndexCartesian, A::OffsetArray) = CartesianIndices(axes(A)) -Base.eachindex(::IndexLinear, A::OffsetVector) = axes(A, 1) +Base.eachindex(::IndexLinear, A::OffsetVector) = axes(A, 1) + +@inline Base.size(A::OffsetArray) = size(parent(A)) +@inline Base.size(A::OffsetArray, d) = size(parent(A), d) -# Implementations of indices and axes1. Since bounds-checking is -# performance-critical and relies on indices, these are usually worth -# optimizing thoroughly. -@inline Base.axes(A::OffsetArray, d) = 1 <= d <= length(A.offsets) ? Base.IdentityUnitRange(axes(parent(A))[d] .+ A.offsets[d]) : Base.IdentityUnitRange(1:1) -@inline Base.axes(A::OffsetArray) = _indices(axes(parent(A)), A.offsets) # would rather use ntuple, but see #15276 -@inline _indices(inds, offsets) = (Base.IdentityUnitRange(inds[1] .+ offsets[1]), _indices(tail(inds), tail(offsets))...) -_indices(::Tuple{}, ::Tuple{}) = () -Base.axes1(A::OffsetArray{T,0}) where {T} = Base.IdentityUnitRange(1:1) # we only need to specialize this one +@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 -const OffsetAxis = Union{Integer, UnitRange, Base.IdentityUnitRange{<:UnitRange}, Base.OneTo} -function Base.similar(A::OffsetArray, T::Type, dims::Dims) - B = similar(parent(A), T, dims) +Base.similar(A::OffsetArray, ::Type{T}, dims::Dims) where T = + similar(parent(A), T, dims) +function Base.similar(A::AbstractArray, ::Type{T}, inds::Tuple{OffsetAxisKnownLength,Vararg{OffsetAxisKnownLength}}) where T + B = similar(A, T, map(_indexlength, inds)) + return OffsetArray(B, map(_offset, axes(B), inds)) end -function Base.similar(A::AbstractArray, T::Type, inds::Tuple{OffsetAxis,Vararg{OffsetAxis}}) - B = similar(A, T, map(indslength, inds)) - OffsetArray(B, map(indsoffset, inds)) + +# reshape accepts a single colon +Base.reshape(A::AbstractArray, inds::OffsetAxis...) = reshape(A, inds) +function Base.reshape(A::AbstractArray, inds::Tuple{OffsetAxis,Vararg{OffsetAxis}}) + AR = reshape(A, map(_indexlength, inds)) + return OffsetArray(AR, map(_offset, axes(AR), inds)) end -Base.similar(::Type{T}, shape::Tuple{OffsetAxis,Vararg{OffsetAxis}}) where {T<:AbstractArray} = - OffsetArray(T(undef, map(indslength, shape)), map(indsoffset, shape)) +# Reshaping OffsetArrays can "pop" the original OffsetArray wrapper and return +# an OffsetArray(reshape(...)) instead of an OffsetArray(reshape(OffsetArray(...))) +Base.reshape(A::OffsetArray, inds::Tuple{OffsetAxis,Vararg{OffsetAxis}}) = + OffsetArray(reshape(parent(A), map(_indexlength, inds)), map(_indexoffset, inds)) +# And for non-offset axes, we can just return a reshape of the parent directly +Base.reshape(A::OffsetArray, inds::Tuple{Union{Integer,Base.OneTo},Vararg{Union{Integer,Base.OneTo}}}) = reshape(parent(A), inds) +Base.reshape(A::OffsetArray, inds::Dims) = reshape(parent(A), inds) +Base.reshape(A::OffsetArray, ::Colon) = reshape(parent(A), Colon()) +Base.reshape(A::OffsetVector, ::Colon) = A +Base.reshape(A::OffsetArray, inds::Union{Int,Colon}...) = reshape(parent(A), inds) +Base.reshape(A::OffsetArray, inds::Tuple{Vararg{Union{Int,Colon}}}) = reshape(parent(A), inds) -Base.reshape(A::AbstractArray, inds::Tuple{OffsetAxis,Vararg{OffsetAxis}}) = OffsetArray(reshape(A, map(indslength, inds)), map(indsoffset, inds)) +function Base.similar(::Type{T}, shape::Tuple{OffsetAxis,Vararg{OffsetAxis}}) where {T <: AbstractArray} + P = T(undef, map(_indexlength, shape)) + OffsetArray(P, map(_offset, axes(P), shape)) +end Base.fill(v, inds::NTuple{N, Union{Integer, AbstractUnitRange}}) where {N} = - fill!(OffsetArray(Array{typeof(v), N}(undef, map(indslength, inds)), map(indsoffset, inds)), v) + fill!(similar(Array{typeof(v)}, inds), v) Base.zeros(::Type{T}, inds::NTuple{N, Union{Integer, AbstractUnitRange}}) where {T, N} = - fill!(OffsetArray(Array{T, N}(undef, map(indslength, inds)), map(indsoffset, inds)), zero(T)) + fill!(similar(Array{T}, inds), zero(T)) Base.ones(::Type{T}, inds::NTuple{N, Union{Integer, AbstractUnitRange}}) where {T, N} = - fill!(OffsetArray(Array{T, N}(undef, map(indslength, inds)), map(indsoffset, inds)), one(T)) + fill!(similar(Array{T}, inds), one(T)) Base.trues(inds::NTuple{N, Union{Integer, AbstractUnitRange}}) where {N} = - fill!(OffsetArray(BitArray{N}(undef, map(indslength, inds)), map(indsoffset, inds)), true) + fill!(similar(BitArray, inds), true) Base.falses(inds::NTuple{N, Union{Integer, AbstractUnitRange}}) where {N} = - fill!(OffsetArray(BitArray{N}(undef, map(indslength, inds)), map(indsoffset, inds)), false) + fill!(similar(BitArray, inds), false) + +## 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 @inline function Base.getindex(A::OffsetArray{T,N}, I::Vararg{Int,N}) where {T,N} - checkbounds(A, I...) - @inbounds ret = parent(A)[offset(A.offsets, I)...] - ret + @boundscheck checkbounds(A, I...) + J = map(parentindex, axes(A), I) + @inbounds parent(A)[J...] end -# Vectors don't support one-based linear indexing; they always use the offsets + @inline function Base.getindex(A::OffsetVector, i::Int) - checkbounds(A, i) - @inbounds ret = parent(A)[offset(A.offsets, (i,))[1]] - ret -end -# But multidimensional arrays allow one-based linear indexing -@inline function Base.getindex(A::OffsetArray, i::Int) - checkbounds(A, i) - @inbounds ret = parent(A)[i] - ret + @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} - checkbounds(A, I...) - @inbounds parent(A)[offset(A.offsets, I)...] = val - val + @boundscheck checkbounds(A, I...) + J = @inbounds map(parentindex, axes(A), I) + @inbounds parent(A)[J...] = val + A end + @inline function Base.setindex!(A::OffsetVector, val, i::Int) - checkbounds(A, i) - @inbounds parent(A)[offset(A.offsets, (i,))[1]] = val - val + @boundscheck checkbounds(A, i) + @inbounds parent(A)[parentindex(Base.axes1(A), i)] = val + A end -@inline function Base.setindex!(A::OffsetArray, val, i::Int) - checkbounds(A, i) - @inbounds parent(A)[i] = val - val +@propagate_inbounds function Base.setindex!(A::OffsetArray, val, i::Int) + parent(A)[i] = val + A end -@inline function Base.deleteat!(A::OffsetArray, i::Int) - checkbounds(A, i) - @inbounds deleteat!(parent(A), offset(A.offsets, (i,))[1]) -end +# For fast broadcasting: ref https://discourse.julialang.org/t/why-is-there-a-performance-hit-on-broadcasting-with-offsetarrays/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) -@inline function Base.deleteat!(A::OffsetArray{T,N}, I::Vararg{Int, N}) where {T,N} - checkbounds(A, I...) - @inbounds deleteat!(parent(A), offset(A.offsets, I)...) -end +### Special handling for AbstractRange -@inline function Base.deleteat!(A::OffsetArray, i::UnitRange{Int}) - checkbounds(A, first(i)) - checkbounds(A, last(i)) - first_idx = offset(A.offsets, (first(i),))[1] - last_idx = offset(A.offsets, (last(i),))[1] - @inbounds deleteat!(parent(A), first_idx:last_idx) -end +const OffsetRange{T} = OffsetArray{T,1,<:AbstractRange{T}} +const IIUR = IdentityUnitRange{S} where S<:AbstractUnitRange{T} where T<:Integer -function Base.push!(a::OffsetArray{T,1}, item) where T - # convert first so we don't grow the array if the assignment won't work - itemT = convert(T, item) - resize!(a, length(a)+1) - a[end] = itemT - return a +Base.step(a::OffsetRange) = step(parent(a)) + +@propagate_inbounds Base.getindex(a::OffsetRange, r::OffsetRange) = OffsetArray(a[parent(r)], r.offsets) +@propagate_inbounds function Base.getindex(a::OffsetRange, r::IdOffsetRange) + OffsetArray(a.parent[r.parent .+ (r.offset - a.offsets[1])], r.offset) end +@propagate_inbounds Base.getindex(r::OffsetRange, s::IIUR) = + OffsetArray(r[s.indices], s) +@propagate_inbounds Base.getindex(a::OffsetRange, r::AbstractRange) = a.parent[r .- a.offsets[1]] +@propagate_inbounds Base.getindex(a::AbstractRange, r::OffsetRange) = OffsetArray(a[parent(r)], r.offsets) + +@propagate_inbounds Base.getindex(r::UnitRange, s::IIUR) = + OffsetArray(r[s.indices], s) + +@propagate_inbounds Base.getindex(r::StepRange, s::IIUR) = + OffsetArray(r[s.indices], s) -# Computing a shifted index (subtracting the offset) -offset(offsets::NTuple{N,Int}, inds::NTuple{N,Int}) where {N} = _offset((), offsets, inds) -_offset(out, ::Tuple{}, ::Tuple{}) = out -@inline _offset(out, offsets, inds) = _offset((out..., inds[1]-offsets[1]), Base.tail(offsets), Base.tail(inds)) +# this method is needed for ambiguity resolution +@propagate_inbounds Base.getindex(r::StepRangeLen{T,<:Base.TwicePrecision,<:Base.TwicePrecision}, s::IIUR) where T = + OffsetArray(r[s.indices], s) -indsoffset(r::AbstractRange) = first(r) - 1 -indsoffset(i::Integer) = 0 -indslength(r::AbstractRange) = length(r) -indslength(i::Integer) = i +@propagate_inbounds Base.getindex(r::StepRangeLen{T}, s::IIUR) where {T} = + OffsetArray(r[s.indices], s) +@propagate_inbounds Base.getindex(r::LinRange, s::IIUR) = + OffsetArray(r[s.indices], s) + +function Base.show(io::IO, r::OffsetRange) + show(io, r.parent) + o = r.offsets[1] + print(io, " with indices ", o+1:o+length(r)) +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 +function Base.inds2string(inds::Tuple{Vararg{Union{IdOffsetRange,IdentityUnitRange{<:IdOffsetRange}}}}) + 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, a::OffsetArray, toplevel) + print(io, "OffsetArray(") + Base.showarg(io, parent(a), false) + Base.showindices(io, axes(a)...) + print(io, ')') + toplevel && print(io, " with eltype ", eltype(a)) +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 + +function no_offset_view(A::AbstractArray) + if Base.has_offset_axes(A) + OffsetArray(A, map(r->1-first(r), axes(A))) + else + A + end +end + +no_offset_view(A::OffsetArray) = no_offset_view(parent(A)) + +end # module