Skip to content

Commit b365157

Browse files
authored
more powerful reverse(A; dims) (#37367)
* more powerful reverse(A; dims) * fix reverse for offsetarray * bugfix for odd dimensions + test * typo * fix bootstrap failure * explicit reverse! and dims=: tests * NEWS * more offsetarray tests * compat annotation * further simplify and speed up reverse (and reverse! is now down to 0 allocations) * rm redundant inbounds * whoops extra space * use ÷ 2 instead of >> 1 * fix type output in doc * fix type instability in slice ranges
1 parent f902584 commit b365157

File tree

8 files changed

+120
-121
lines changed

8 files changed

+120
-121
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ New library features
7474
Standard library changes
7575
------------------------
7676
* The `nextprod` function now accepts tuples and other array types for its first argument ([#35791]).
77+
* The `reverse(A; dims)` function for multidimensional `A` can now reverse multiple dimensions at once
78+
by passing a tuple for `dims`, and defaults to reversing all dimensions; there is also a multidimensional
79+
in-place `reverse!(A; dims)` ([#37367]).
7780
* The function `isapprox(x,y)` now accepts the `norm` keyword argument also for numeric (i.e., non-array) arguments `x` and `y` ([#35883]).
7881
* `view`, `@view`, and `@views` now work on `AbstractString`s, returning a `SubString` when appropriate ([#35879]).
7982
* All `AbstractUnitRange{<:Integer}`s now work with `SubString`, `view`, `@view` and `@views` on strings ([#35879]).

base/abstractarraymath.jl

Lines changed: 0 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -127,56 +127,6 @@ julia> selectdim(A, 2, 3)
127127
return view(A, idxs...)
128128
end
129129

130-
"""
131-
reverse(A; dims::Integer)
132-
133-
Reverse `A` in dimension `dims`.
134-
135-
# Examples
136-
```jldoctest
137-
julia> b = [1 2; 3 4]
138-
2×2 Matrix{Int64}:
139-
1 2
140-
3 4
141-
142-
julia> reverse(b, dims=2)
143-
2×2 Matrix{Int64}:
144-
2 1
145-
4 3
146-
```
147-
"""
148-
function reverse(A::AbstractArray; dims::Integer)
149-
nd = ndims(A); d = dims
150-
1 d nd || throw(ArgumentError("dimension $d is not 1 ≤ $d$nd"))
151-
if isempty(A)
152-
return copy(A)
153-
elseif nd == 1
154-
return reverse(A)
155-
end
156-
inds = axes(A)
157-
B = similar(A)
158-
nnd = 0
159-
for i = 1:nd
160-
nnd += Int(length(inds[i])==1 || i==d)
161-
end
162-
indsd = inds[d]
163-
sd = first(indsd)+last(indsd)
164-
if nnd==nd
165-
# reverse along the only non-singleton dimension
166-
for i in indsd
167-
B[i] = A[sd-i]
168-
end
169-
return B
170-
end
171-
let B=B # workaround #15276
172-
alli = [ axes(B,n) for n in 1:nd ]
173-
for i in indsd
174-
B[[ n==d ? sd-i : alli[n] for n in 1:nd ]...] = selectdim(A, d, i)
175-
end
176-
end
177-
return B
178-
end
179-
180130
function circshift(a::AbstractArray, shiftamt::Real)
181131
circshift!(similar(a), a, (Integer(shiftamt),))
182132
end

base/array.jl

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1589,22 +1589,33 @@ julia> reverse(A, 3, 5)
15891589
3
15901590
```
15911591
"""
1592-
function reverse(A::AbstractVector, s=first(LinearIndices(A)), n=last(LinearIndices(A)))
1592+
function reverse(A::AbstractVector, start::Integer, stop::Integer=lastindex(A))
1593+
s, n = Int(start), Int(stop)
15931594
B = similar(A)
1594-
for i = first(LinearIndices(A)):s-1
1595+
for i = firstindex(A):s-1
15951596
B[i] = A[i]
15961597
end
15971598
for i = s:n
15981599
B[i] = A[n+s-i]
15991600
end
1600-
for i = n+1:last(LinearIndices(A))
1601+
for i = n+1:lastindex(A)
16011602
B[i] = A[i]
16021603
end
16031604
return B
16041605
end
16051606

1606-
# to resolve ambiguity with reverse(A; dims)
1607-
reverse(A::Vector) = invoke(reverse, Tuple{AbstractVector}, A)
1607+
# 1d special cases of reverse(A; dims) and reverse!(A; dims):
1608+
for (f,_f) in ((:reverse,:_reverse), (:reverse!,:_reverse!))
1609+
@eval begin
1610+
$f(A::AbstractVector; dims=:) = $_f(A, dims)
1611+
$_f(A::AbstractVector, ::Colon) = $f(A, firstindex(A), lastindex(A))
1612+
$_f(A::AbstractVector, dim::Tuple{Integer}) = $_f(A, first(dim))
1613+
function $_f(A::AbstractVector, dim::Integer)
1614+
dim == 1 || throw(ArgumentError("invalid dimension $dim ≠ 1"))
1615+
return $_f(A, :)
1616+
end
1617+
end
1618+
end
16081619

16091620
function reverseind(a::AbstractVector, i::Integer)
16101621
li = LinearIndices(a)
@@ -1637,7 +1648,8 @@ julia> A
16371648
1
16381649
```
16391650
"""
1640-
function reverse!(v::AbstractVector, s=first(LinearIndices(v)), n=last(LinearIndices(v)))
1651+
function reverse!(v::AbstractVector, start::Integer, stop::Integer=lastindex(v))
1652+
s, n = Int(start), Int(stop)
16411653
liv = LinearIndices(v)
16421654
if n <= s # empty case; ok
16431655
elseif !(first(liv) s last(liv))

base/arraymath.jl

Lines changed: 65 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -58,69 +58,78 @@ end
5858

5959
## data movement ##
6060

61-
reverse(A::Array; dims::Integer) = _reverse_int(A, Int(dims))
62-
function _reverse_int(A::Array{T}, d::Int) where T
63-
nd = ndims(A)
64-
1 d nd || throw(ArgumentError("dimension $d is not 1 ≤ $d$nd"))
65-
sd = size(A, d)
66-
if sd == 1 || isempty(A)
67-
return copy(A)
68-
end
61+
"""
62+
reverse(A; dims=:)
6963
70-
B = similar(A)
64+
Reverse `A` along dimension `dims`, which can be an integer (a
65+
single dimension), a tuple of integers (a tuple of dimensions)
66+
or `:` (reverse along all the dimensions, the default). See
67+
also [`reverse!`](@ref) for in-place reversal.
7168
72-
nnd = 0
73-
for i = 1:nd
74-
nnd += size(A,i)==1 || i==d
75-
end
76-
if nnd==nd
77-
# reverse along the only non-singleton dimension
78-
for i = 1:sd
79-
B[i] = A[sd+1-i]
80-
end
81-
return B
69+
# Examples
70+
```jldoctest
71+
julia> b = Int64[1 2; 3 4]
72+
2×2 Matrix{Int64}:
73+
1 2
74+
3 4
75+
76+
julia> reverse(b, dims=2)
77+
2×2 Matrix{Int64}:
78+
2 1
79+
4 3
80+
81+
julia> reverse(b)
82+
2×2 Matrix{Int64}:
83+
4 3
84+
2 1
85+
```
86+
87+
!!! compat "Julia 1.6"
88+
Prior to Julia 1.6, only single-integer `dims` are supported in `reverse`.
89+
"""
90+
reverse(A::AbstractArray; dims=:) = _reverse(A, dims)
91+
_reverse(A, dims) = reverse!(copymutable(A); dims)
92+
93+
"""
94+
reverse!(A; dims=:)
95+
96+
Like [`reverse`](@ref), but operates in-place in `A`.
97+
98+
!!! compat "Julia 1.6"
99+
Multidimensional `reverse!` requires Julia 1.6.
100+
"""
101+
reverse!(A::AbstractArray; dims=:) = _reverse!(A, dims)
102+
_reverse!(A::AbstractArray{<:Any,N}, ::Colon) where {N} = _reverse!(A, ntuple(identity, Val{N}()))
103+
_reverse!(A, dim::Integer) = _reverse!(A, (Int(dim),))
104+
_reverse!(A, dims::NTuple{M,Integer}) where {M} = _reverse!(A, Int.(dims))
105+
function _reverse!(A::AbstractArray{<:Any,N}, dims::NTuple{M,Int}) where {N,M}
106+
dimrev = ntuple(k -> k in dims, Val{N}()) # boolean tuple indicating reversed dims
107+
108+
if N < M || M != sum(dimrev)
109+
throw(ArgumentError("invalid dimensions $dims in reverse!"))
82110
end
111+
M == 0 && return A # nothing to reverse
83112

84-
d_in = size(A)
85-
M = 1
86-
for i = 1:d-1
87-
M *= d_in[i]
113+
# swapping loop only needs to traverse ≈half of the array
114+
halfsz = ntuple(k -> k == dims[1] ? size(A,k) ÷ 2 : size(A,k), Val{N}())
115+
116+
last1 = ntuple(k -> lastindex(A,k)+firstindex(A,k), Val{N}()) # offset for reversed index
117+
for i in CartesianIndices(ntuple(k -> firstindex(A,k):firstindex(A,k)-1+@inbounds(halfsz[k]), Val{N}()))
118+
iₜ = Tuple(i)
119+
iᵣ = CartesianIndex(ifelse.(dimrev, last1 .- iₜ, iₜ))
120+
@inbounds A[iᵣ], A[i] = A[i], A[iᵣ]
88121
end
89-
N = length(A)
90-
stride = M * sd
91-
92-
if M==1
93-
for j = 0:stride:(N-stride)
94-
for i = 1:sd
95-
ri = sd+1-i
96-
B[j + ri] = A[j + i]
97-
end
98-
end
99-
else
100-
if allocatedinline(T) && M>200
101-
for i = 1:sd
102-
ri = sd+1-i
103-
for j=0:stride:(N-stride)
104-
offs = j + 1 + (i-1)*M
105-
boffs = j + 1 + (ri-1)*M
106-
copyto!(B, boffs, A, offs, M)
107-
end
108-
end
109-
else
110-
for i = 1:sd
111-
ri = sd+1-i
112-
for j=0:stride:(N-stride)
113-
offs = j + 1 + (i-1)*M
114-
boffs = j + 1 + (ri-1)*M
115-
for k=0:(M-1)
116-
B[boffs + k] = A[offs + k]
117-
end
118-
end
119-
end
120-
end
122+
if M > 1 && isodd(size(A, dims[1]))
123+
# middle slice for odd dimensions must be recursively flipped
124+
mid = firstindex(A, dims[1]) + (size(A, dims[1]) ÷ 2)
125+
midslice = CartesianIndices(ntuple(k -> k == dims[1] ? (mid:mid) : (firstindex(A,k):lastindex(A,k)), Val{N}()))
126+
_reverse!(view(A, midslice), dims[2:end])
121127
end
122-
return B
128+
return A
123129
end
130+
# fix ambiguity with array.jl:
131+
_reverse!(A::AbstractVector, dim::Tuple{Int}) = _reverse!(A, first(dim))
132+
124133

125134
"""
126135
rotl90(A)

base/bitarray.jl

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,8 +1163,8 @@ end
11631163

11641164
# TODO some of this could be optimized
11651165

1166-
reverse(A::BitArray; dims::Integer) = _reverse_int(A, Int(dims))
1167-
function _reverse_int(A::BitArray, d::Int)
1166+
_reverse(A::BitArray, d::Tuple{Integer}) = _reverse(A, d[1])
1167+
function _reverse(A::BitArray, d::Int)
11681168
nd = ndims(A)
11691169
1 d nd || throw(ArgumentError("dimension $d is not 1 ≤ $d$nd"))
11701170
sd = size(A, d)
@@ -1210,7 +1210,7 @@ function _reverse_int(A::BitArray, d::Int)
12101210
return B
12111211
end
12121212

1213-
function reverse!(B::BitVector)
1213+
function _reverse!(B::BitVector, ::Colon)
12141214
# Basic idea: each chunk is divided into two blocks of size k = n % 64, and
12151215
# h = 64 - k. Walk from either end (with indices i and j) reversing chunks
12161216
# and separately ORing their two blocks into place.
@@ -1264,9 +1264,6 @@ function reverse!(B::BitVector)
12641264
return B
12651265
end
12661266

1267-
reverse(v::BitVector) = reverse!(copy(v))
1268-
1269-
12701267
function (<<)(B::BitVector, i::UInt)
12711268
n = length(B)
12721269
i == 0 && return copy(B)

base/range.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -990,15 +990,15 @@ end
990990
Array{T,1}(r::AbstractRange{T}) where {T} = vcat(r)
991991
collect(r::AbstractRange) = vcat(r)
992992

993-
reverse(r::OrdinalRange) = (:)(last(r), -step(r), first(r))
994-
function reverse(r::StepRangeLen)
993+
_reverse(r::OrdinalRange, ::Colon) = (:)(last(r), -step(r), first(r))
994+
function _reverse(r::StepRangeLen, ::Colon)
995995
# If `r` is empty, `length(r) - r.offset + 1 will be nonpositive hence
996996
# invalid. As `reverse(r)` is also empty, any offset would work so we keep
997997
# `r.offset`
998998
offset = isempty(r) ? r.offset : length(r)-r.offset+1
999999
StepRangeLen(r.ref, -r.step, length(r), offset)
10001000
end
1001-
reverse(r::LinRange{T}) where {T} = LinRange{T}(r.stop, r.start, length(r))
1001+
_reverse(r::LinRange{T}, ::Colon) where {T} = LinRange{T}(r.stop, r.start, length(r))
10021002

10031003
## sorting ##
10041004

test/arrayops.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1555,6 +1555,28 @@ end
15551555
@test reverse(["a" "b"; "c" "d"], dims = 2) == ["b" "a"; "d" "c"]
15561556
end
15571557

1558+
@testset "reverse multiple dims" begin
1559+
for A in (zeros(2,4), zeros(3,5))
1560+
A[:] .= 1:length(A) # unique-ify elements
1561+
@test reverse(A) == reverse!(reverse(A, dims=1), dims=2) ==
1562+
reverse(A, dims=(1,2)) == reverse(A, dims=(2,1))
1563+
@test_throws ArgumentError reverse(A, dims=(1,2,3))
1564+
@test_throws ArgumentError reverse(A, dims=(1,2,2))
1565+
end
1566+
for A in (zeros(2,4,6), zeros(3,5,7))
1567+
A[:] .= 1:length(A) # unique-ify elements
1568+
@test reverse(A) == reverse(A, dims=:) == reverse!(copy(A)) == reverse!(copy(A), dims=:) ==
1569+
reverse!(reverse!(reverse(A, dims=1), dims=2), dims=3) ==
1570+
reverse!(reverse(A, dims=(1,2)), dims=3) ==
1571+
reverse!(reverse(A, dims=(2,3)), dims=1) ==
1572+
reverse!(reverse(A, dims=(1,3)), dims=2) ==
1573+
reverse(A, dims=(1,2,3)) == reverse(A, dims=(3,2,1)) == reverse(A, dims=(2,1,3))
1574+
@test reverse(A, dims=(1,2)) == reverse!(reverse(A, dims=1), dims=2)
1575+
@test reverse(A, dims=(1,3)) == reverse!(reverse(A, dims=1), dims=3)
1576+
@test reverse(A, dims=(2,3)) == reverse!(reverse(A, dims=2), dims=3)
1577+
end
1578+
end
1579+
15581580
@testset "isdiag, istril, istriu" begin
15591581
@test isdiag(3)
15601582
@test istril(4)

test/offsetarray.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,12 @@ soa = OffsetArray([2,2,3], typemax(Int)-4)
522522
@test rotr90(A) == OffsetArray(rotr90(parent(A)), A.offsets[[2,1]])
523523
@test reverse(A, dims=1) == OffsetArray(reverse(parent(A), dims=1), A.offsets)
524524
@test reverse(A, dims=2) == OffsetArray(reverse(parent(A), dims=2), A.offsets)
525+
@test reverse(A) == reverse!(reverse(A, dims=1), dims=2)
526+
527+
Aodd = OffsetArray(rand(3,5), (-3,5))
528+
@test reverse(Aodd, dims=1) == OffsetArray(reverse(parent(Aodd), dims=1), Aodd.offsets)
529+
@test reverse(Aodd, dims=2) == OffsetArray(reverse(parent(Aodd), dims=2), Aodd.offsets)
530+
@test reverse(Aodd) == reverse!(reverse(Aodd, dims=1), dims=2)
525531

526532
@test A .+ 1 == OffsetArray(parent(A) .+ 1, A.offsets)
527533
@test 2*A == OffsetArray(2*parent(A), A.offsets)

0 commit comments

Comments
 (0)