Iteration over OffsetArray

From this comment, I learned that for i = eachindex(v) is preferred to for i = 1:length(v) when iterating over v::AbstractVector, because AbstractVector may not support 1-based indexing (e.g., OffsecArrays.jl).

eachindex(v) is acceptable when we want to iterate over the entire length of v. However, sometimes we want to iterate over a portion of v. For example, to calculate the gap between adjacent entries, we would want to do something like

for i = 1:length(v)-1
    println(v[i+1] - v[i])
end

In this case we cannot use eachindex(v). Is there a syntax that we can use in such a case?

(Calculating the gap between adjacent entries is what diff(v) does, and diff(v) calls Base.require_one_based_indexing(v), which suggests that there may not be a simple way of handling this issue.)

for i = firstindex(v):lastindex(v)-1
2 Likes

Or

for i in eachindex(v)[begin:end-1]

which produces the same result - it’s mostly a matter of style/taste.

And would this be a good replacement:

    # only for one based arrays
    x=vec[1]
    y=vec[2]
    # generic
    x=vec[firstindex(vec)]]
    y=vec[firstindex(vec)+1]

?

this allocates a concrete array, O(N) in memory, would not recommend

x = vec[begin]
y = vec[begin + 1]

it’s exactly how the begin/end syntax sugar works under the hood:

julia> Meta.@lower vec[begin + 1]
:($(Expr(:thunk, CodeInfo(
    @ none within `top-level scope`
1 ─ %1 = Base.firstindex(vec)
│   %2 = %1 + 1
│   %3 = Base.getindex(vec, %2)
└──      return %3
))))
3 Likes

I don’t believe that’s true.

julia> let v = OffsetArray([1,2,3,4,5], -2:2)
           eachindex(v)[begin:end-1]
       end
-2:1                                                                 
                                                                     
julia> typeof(ans)
UnitRange{Int64}
1 Like

oops, Julia is smarter than I thought, my bad

1 Like

This assumes a step length of 1. For complete generality you need to index into eachindex(v).

I would create SubArrays via @view or @views:

using OffsetArrays

v = OffsetArray([1,3,2,4,8,90], -5)
v_1, v_2 = @views v[begin:end-1], v[begin+1:end];
println.(v_2 .- v_1)

If you really wanted to use a for loop

for i in eachindex(v_1, v_2)
   println(v_2[i] - v_1[i])
end

Technically, the result of axes(A) where typeof(A) <: AbstractArray must be a tuple of AbstractUnitRange{<:Integer} according to the AbstractArray interface: Interfaces · The Julia Language. The step should be 1.

The problem is that we do not have a good way of enforcing this.

TIL. I guess StarWarsArrays should not subtype AbstractArray then.

Often, the simplest solution might be to use OffsetArrays.no_offset_view, which will return a 1-based view that will (for mutable arrays) share memory with the parent array. So

v1 = OffsetArrays.no_offset_view(v)
for i = 1:length(v1)-1 # works now
    println(v1[i+1] - v1[i])
end

This might lead to code that’s simpler to understand.

1 Like

Indexing into a range with another range should produce a range. It’s not too hard to do.

I believe the most idiomatic way is

for i = axes(v, 1)[begin:end-1]
    println(v[i+1] - v[i])
end
1 Like

No, that doesn’t work. It must be axes(v, 1)[begin:end-1]:

julia> v = OffsetArray(rand(5), -1:3);

julia> eachindex(v)[1:end-1]
1:2

julia> eachindex(v)[begin:end-1]
-1:2

Oops, yes, typo. Edited, thanks.