diff --git a/Project.toml b/Project.toml index f82f73e2..b7071507 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "OffsetArrays" uuid = "6fe1bfb0-de20-5000-8ca7-80f57d26f881" -version = "1.5.0" +version = "1.6.0" [deps] Adapt = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" diff --git a/docs/make.jl b/docs/make.jl index 7a769a95..ce7a68cd 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,6 +1,8 @@ using Documenter, JSON using OffsetArrays +DocMeta.setdocmeta!(OffsetArrays, :DocTestSetup, :(using OffsetArrays); recursive=true) + makedocs( sitename = "OffsetArrays", format = Documenter.HTML(prettyurls = get(ENV, "CI", nothing) == "true"), diff --git a/src/OffsetArrays.jl b/src/OffsetArrays.jl index 42e4223f..148592d0 100644 --- a/src/OffsetArrays.jl +++ b/src/OffsetArrays.jl @@ -7,6 +7,12 @@ else using Base: IdentityUnitRange end +@static if VERSION >= v"1.5" + const replace_ref_end! = Base.replace_ref_begin_end! +else + const replace_ref_end! = Base.replace_ref_end! +end + export OffsetArray, OffsetMatrix, OffsetVector include("axes.jl") @@ -392,6 +398,87 @@ function Base.replace_in_print_matrix(A::OffsetArray{<:Any,1}, i::Integer, j::In Base.replace_in_print_matrix(parent(A), ip, j, s) end +""" + offset_view(A::AbstractArray, I...) + +Return a view into `A` with the given indices `I` that is also indexed by `I`. + +!!! note "Fancy indexing" + Indexing with `AbstractVector` types that are not convertible to an `AbstractUnitRange`s + or to a Tuple of `AbstractUnitRanges` is not supported. For example, `Vector`s may not be + used as indices in an `@offset_view` operation. + +# Examples +```jldoctest +julia> a = 1:20; + +julia> OffsetArrays.offset_view(a, 4:5) +4:5 with indices 4:5 + +julia> b = reshape(1:12, 3, 4) +3×4 reshape(::UnitRange{$Int}, 3, 4) with eltype $Int: + 1 4 7 10 + 2 5 8 11 + 3 6 9 12 + +julia> OffsetArrays.offset_view(b, :, 3:4) +3×2 OffsetArray(view(reshape(::UnitRange{$Int}, 3, 4), :, 3:4), 1:3, 3:4) with eltype $Int with indices 1:3×3:4: + 7 10 + 8 11 + 9 12 +``` +""" +function offset_view(A::AbstractArray, I::Vararg) + v = view(A, I...) + OffsetArray(v, _filteraxes(I...)) +end + +""" + @offset_view A[I...] + +Create a view into `A` from an indexing operation `A[I...]` that is also indexed by `I`. + +!!! note "Fancy indexing" + Indexing with `AbstractVector` types that are not convertible to an `AbstractUnitRange`s + or to a Tuple of `AbstractUnitRanges` is not supported. For example, `Vector`s may not be + used as indices in an `@offset_view` operation. + +# Examples +```jldoctest +julia> a = 1:20; + +julia> OffsetArrays.@offset_view a[4:5] +4:5 with indices 4:5 + +julia> b = reshape(1:12, 3, 4) +3×4 reshape(::UnitRange{$Int}, 3, 4) with eltype $Int: + 1 4 7 10 + 2 5 8 11 + 3 6 9 12 + +julia> OffsetArrays.@offset_view b[:, 3:4] +3×2 OffsetArray(view(reshape(::UnitRange{$Int}, 3, 4), :, 3:4), 1:3, 3:4) with eltype $Int with indices 1:3×3:4: + 7 10 + 8 11 + 9 12 +``` +""" +macro offset_view(ex) + if Meta.isexpr(ex, :ref) + ex = replace_ref_end!(ex) + if Meta.isexpr(ex, :ref) + ex = Expr(:call, offset_view, ex.args...) + else # ex replaced by let ...; foo[...]; end + @assert Meta.isexpr(ex, :let) && Meta.isexpr(ex.args[2], :ref) + ex.args[2] = Expr(:call, offset_view, ex.args[2].args...) + end + Expr(:&&, true, esc(ex)) + else + throw(ArgumentError("Invalid use of @offset_view macro: argument must be a reference expression A[...].")) + end +end + + """ no_offset_view(A) diff --git a/src/axes.jl b/src/axes.jl index dcb65240..ef03bd2a 100644 --- a/src/axes.jl +++ b/src/axes.jl @@ -78,6 +78,9 @@ struct IdOffsetRange{T<:Integer,I<:AbstractUnitRange{T}} <: AbstractUnitRange{T} offset::T IdOffsetRange{T,I}(r::I, offset::T) where {T<:Integer,I<:AbstractUnitRange{T}} = new{T,I}(r, offset) + function IdOffsetRange{T,IdOffsetRange{T,I}}(r::IdOffsetRange{T,I}, offset::T) where {T<:Integer,I<:AbstractUnitRange{T}} + new{T,IdOffsetRange{T,I}}(r, offset) + end end # Construction/coercion from arbitrary AbstractUnitRanges @@ -96,13 +99,12 @@ IdOffsetRange(r::AbstractUnitRange{T}, offset::Integer = 0) where T<:Integer = 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, r.offset + offset + offset_rc) + return IdOffsetRange{T,I}(rc, convert(T, r.offset + offset + offset_rc)) end function IdOffsetRange{T}(r::IdOffsetRange, offset::Integer = 0) where T<:Integer return IdOffsetRange{T}(r.parent, r.offset + offset) end IdOffsetRange(r::IdOffsetRange) = r -IdOffsetRange(r::IdOffsetRange, offset::Integer) = typeof(r)(r.parent, offset + r.offset) # TODO: uncomment these when Julia is ready # # Conversion preserves both the values and the indexes, throwing an InexactError if this diff --git a/src/utils.jl b/src/utils.jl index 72414804..9a1e72d6 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -73,4 +73,8 @@ function _checkindices(N::Integer, indices, label) end _unwrap(r::IdOffsetRange) = r.parent .+ r.offset -_unwrap(r::IdentityUnitRange) = r.indices \ No newline at end of file +_unwrap(r::IdentityUnitRange) = r.indices + +@inline _filteraxes(x::Union{Colon, AbstractArray}, I...) = (x, _filteraxes(I...)...) +@inline _filteraxes(i1, I...) = (_filteraxes(I...)...,) +_filteraxes() = () diff --git a/test/runtests.jl b/test/runtests.jl index 70f19a16..b9ec48e9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,6 @@ using OffsetArrays using OffsetArrays: IdentityUnitRange, no_offset_view -using OffsetArrays: IdOffsetRange +using OffsetArrays: IdOffsetRange, offset_view, @offset_view using Test, Aqua, Documenter using LinearAlgebra using DelimitedFiles @@ -9,6 +9,8 @@ using EllipsisNotation using Adapt using StaticArrays +DocMeta.setdocmeta!(OffsetArrays, :DocTestSetup, :(using OffsetArrays); recursive=true) + # https://github.com/JuliaLang/julia/pull/29440 if VERSION < v"1.1.0-DEV.389" Base.:(:)(I::CartesianIndex{N}, J::CartesianIndex{N}) where N = @@ -22,6 +24,14 @@ struct TupleOfRanges{N} x ::NTuple{N, UnitRange{Int}} end +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 + @testset "Project meta quality checks" begin # Not checking compat section for test-only dependencies Aqua.test_all(OffsetArrays; project_extras=true, deps_compat=true, stale_deps=true, project_toml_formatting=true) @@ -31,13 +41,7 @@ end end @testset "IdOffsetRange" begin - 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 + function check_indexed_by(r, rindx) for i in rindx r[i] @@ -98,8 +102,16 @@ end @test same_value(r, 3:5) check_indexed_by(r, 3:5) - r = IdOffsetRange(IdOffsetRange(3:5, 2), 1) - @test parent(r) isa UnitRange + 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) # conversion preserves both the values and the axes, throwing an error if this is not possible @test @inferred(oftype(ro, ro)) === ro @@ -866,6 +878,34 @@ end @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)) + + # 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 axes(c,1) == 3:4 + d = OffsetArray(c, 1:2) + @test same_value(d, c) + @test axes(d,1) == 1:2 +end + +@testset "offset_view" begin + a = ones(3) + @test_throws Exception @eval @offset_view a[2:3] = 3 + @offset_view(a[2:3]) .= 3 + @test all(a[2:3] .== 3) + @offset_view(a[1:end]) .= 4 + @test all(a .== 4) + + av = OffsetArrays.offset_view(a, 1) + @test ndims(av) == 0 + + # couple view and offset_view + b = 1:20 + c = @view b[OffsetArrays.@offset_view b[3:4]] + @test no_offset_view(c) == b[3:4] + @test axes(c) == (3:4,) end @testset "iteration" begin