Skip to content

Add an offset-preserving view function #187

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 2 additions & 0 deletions docs/make.jl
Original file line number Diff line number Diff line change
@@ -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"),
Expand Down
87 changes: 87 additions & 0 deletions src/OffsetArrays.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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)

Expand Down
6 changes: 4 additions & 2 deletions src/axes.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion src/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,8 @@ function _checkindices(N::Integer, indices, label)
end

_unwrap(r::IdOffsetRange) = r.parent .+ r.offset
_unwrap(r::IdentityUnitRange) = r.indices
_unwrap(r::IdentityUnitRange) = r.indices

@inline _filteraxes(x::Union{Colon, AbstractArray}, I...) = (x, _filteraxes(I...)...)
@inline _filteraxes(i1, I...) = (_filteraxes(I...)...,)
_filteraxes() = ()
60 changes: 50 additions & 10 deletions test/runtests.jl
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 =
Expand All @@ -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)
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down