diff --git a/base/abstractarray.jl b/base/abstractarray.jl index 16fdb61e1d3fe..e5b16554522f5 100644 --- a/base/abstractarray.jl +++ b/base/abstractarray.jl @@ -1635,11 +1635,24 @@ cat_size(A::AbstractArray) = size(A) cat_size(A, d) = 1 cat_size(A::AbstractArray, d) = size(A, d) +cat_length(::Any) = 1 +cat_length(a::AbstractArray) = length(a) + +cat_ndims(a) = 0 +cat_ndims(a::AbstractArray) = ndims(a) + cat_indices(A, d) = OneTo(1) cat_indices(A::AbstractArray, d) = axes(A, d) -cat_similar(A, ::Type{T}, shape) where T = Array{T}(undef, shape) -cat_similar(A::AbstractArray, ::Type{T}, shape) where T = similar(A, T, shape) +cat_similar(A, ::Type{T}, shape::Tuple) where T = Array{T}(undef, shape) +cat_similar(A, ::Type{T}, shape::Vector) where T = Array{T}(undef, shape...) +cat_similar(A, ::Type{T}, shape::Int...) where T = Array{T}(undef, shape...) +cat_similar(A::Array, ::Type{T}, shape::Tuple) where T = Array{T}(undef, shape) +cat_similar(A::Array, ::Type{T}, shape::Vector) where T = Array{T}(undef, shape...) +cat_similar(A::Array, ::Type{T}, shape::Int...) where T = Array{T}(undef, shape...) +cat_similar(A::AbstractArray, ::Type{T}, shape::Tuple) where T = similar(A, T, shape) +cat_similar(A::AbstractArray, ::Type{T}, shape::Vector) where T = similar(A, T, shape...) +cat_similar(A::AbstractArray, ::Type{T}, shape::Int...) where T = similar(A, T, shape...) # These are for backwards compatibility (even though internal) cat_shape(dims, shape::Tuple{Vararg{Int}}) = shape @@ -2034,22 +2047,25 @@ function typed_hvcat(::Type{T}, rows::Tuple{Vararg{Int}}, as...) where T T[rs...;] end -# nd concatenation +## N-dimensional concatenation ## """ + hvncat(dim::Int, row_first, values...) hvncat(dims::Tuple{Vararg{Int}}, row_first, values...) hvncat(shape::Tuple{Vararg{Tuple}}, row_first, values...) Horizontal, vertical, and n-dimensional concatenation of many `values` in one call. -This function is called -for block matrix syntax. The first argument either specifies the shape of the concatenation, -similar to `hvcat`, as a tuple of tuples, or the dimensions that specify the key number of -elements along each axis, and is used to determine the output dimensions. The `dims` form -is more performant, and is used by default when the concatenation operation has the same -number of elements along each axis (e.g., [a b; c d;;; e f ; g h]). The `shape` form is used -when the number of elements along each axis is unbalanced (e.g., [a b ; c]). Unbalanced -syntax needs additional validation overhead. +This function is called for block matrix syntax. The first argument either specifies the +shape of the concatenation, similar to `hvcat`, as a tuple of tuples, or the dimensions that +specify the key number of elements along each axis, and is used to determine the output +dimensions. The `dims` form is more performant, and is used by default when the concatenation +operation has the same number of elements along each axis (e.g., [a b; c d;;; e f ; g h]). +The `shape` form is used when the number of elements along each axis is unbalanced +(e.g., [a b ; c]). Unbalanced syntax needs additional validation overhead. The `dim` form +is an optimization for concatenation along just one dimension. `row_first` indicates how +`values` are ordered. The meaning of the first and second elements of `shape` are also +swapped based on `row_first`. # Examples ```jldoctest @@ -2097,87 +2113,94 @@ julia> hvncat(((3, 3), (3, 3), (6,)), true, a, b, c, d, e, f) [:, :, 2] = 4 5 6 ``` + +# Examples for construction of the arguments: +[a b c ; d e f ;;; + g h i ; j k l ;;; + m n o ; p q r ;;; + s t u ; v w x] +=> dims = (2, 3, 4) + +[a b ; c ;;; d ;;;;] + ___ _ _ + 2 1 1 = elements in each row (2, 1, 1) + _______ _ + 3 1 = elements in each column (3, 1) + _____________ + 4 = elements in each 3d slice (4,) + _____________ + 4 = elements in each 4d slice (4,) + => shape = ((2, 1, 1), (3, 1), (4,), (4,)) with `rowfirst` = true """ -hvncat(::Tuple{}, ::Bool) = [] -hvncat(::Tuple{}, ::Bool, xs...) = [] -hvncat(::Tuple{Vararg{Any, 1}}, ::Bool, xs...) = vcat(xs...) # methods assume 2+ dimensions +function hvncat end + +# if any methods seem redundant here, it is to resolve a multiple dispatch ambiguity + +# top-level methods + hvncat(dimsshape::Tuple, row_first::Bool, xs...) = _hvncat(dimsshape, row_first, xs...) hvncat(dim::Int, xs...) = _hvncat(dim, true, xs...) -_hvncat(::Union{Tuple, Int}, ::Bool) = [] +typed_hvncat(T::Type, dimsshape::Tuple, row_first::Bool, xs...) = _typed_hvncat(T, dimsshape, row_first, xs...) +typed_hvncat(T::Type, dim::Int, xs...) = _typed_hvncat(T, Val(dim), xs...) + + +# intermediary methods from `hvncat` which determine a suitable element type + +_hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool) = _typed_hvncat(Any, dimsshape, row_first) _hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs...) = _typed_hvncat(promote_eltypeof(xs...), dimsshape, row_first, xs...) _hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs::T...) where T<:Number = _typed_hvncat(T, dimsshape, row_first, xs...) _hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs::Number...) = _typed_hvncat(promote_typeof(xs...), dimsshape, row_first, xs...) _hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs::AbstractArray...) = _typed_hvncat(promote_eltype(xs...), dimsshape, row_first, xs...) _hvncat(dimsshape::Union{Tuple, Int}, row_first::Bool, xs::AbstractArray{T}...) where T = _typed_hvncat(T, dimsshape, row_first, xs...) -typed_hvncat(::Type{T}, ::Tuple{}, ::Bool) where T = Vector{T}() -typed_hvncat(::Type{T}, ::Tuple{}, ::Bool, xs...) where T = Vector{T}() -typed_hvncat(T::Type, ::Tuple{Vararg{Any, 1}}, ::Bool, xs...) = typed_vcat(T, xs...) # methods assume 2+ dimensions -typed_hvncat(T::Type, dimsshape::Tuple, row_first::Bool, xs...) = _typed_hvncat(T, dimsshape, row_first, xs...) -typed_hvncat(T::Type, dim::Int, xs...) = _typed_hvncat(T, Val(dim), xs...) +# strip boolean from routing Int form through _hvncat -_typed_hvncat(::Type{T}, ::Tuple{}, ::Bool) where T = Vector{T}() -_typed_hvncat(::Type{T}, ::Tuple{}, ::Bool, xs...) where T = Vector{T}() -_typed_hvncat(::Type{T}, ::Tuple{}, ::Bool, xs::Number...) where T = Vector{T}() -function _typed_hvncat(::Type{T}, dims::Tuple{Vararg{Int, N}}, row_first::Bool, xs::Number...) where {T, N} - A = Array{T, N}(undef, dims...) - lengtha = length(A) # Necessary to store result because throw blocks are being deoptimized right now, which leads to excessive allocations - lengthx = length(xs) # Cuts from 3 allocations to 1. - if lengtha != lengthx - throw(ArgumentError("argument count does not match specified shape (expected $lengtha, got $lengthx)")) - end - hvncat_fill!(A, row_first, xs) - return A -end +_typed_hvncat(T::Type, dim::Int, ::Bool, xs...) = _typed_hvncat(T, Val(dim), xs...) -function hvncat_fill!(A::Array, row_first::Bool, xs::Tuple) - # putting these in separate functions leads to unnecessary allocations - if row_first - nr, nc = size(A, 1), size(A, 2) - nrc = nr * nc - na = prod(size(A)[3:end]) - k = 1 - for d ∈ 1:na - dd = nrc * (d - 1) - for i ∈ 1:nr - Ai = dd + i - for j ∈ 1:nc - A[Ai] = xs[k] - k += 1 - Ai += nr - end - end - end - else - for k ∈ eachindex(xs) - A[k] = xs[k] - end - end -end -_typed_hvncat(T::Type, dim::Int, ::Bool, xs...) = _typed_hvncat(T, Val(dim), xs...) # catches from _hvncat type promoters -_typed_hvncat(::Type{T}, ::Val) where T = Vector{T}() -_typed_hvncat(T::Type, ::Val{N}, xs::Number...) where N = _typed_hvncat(T, (ntuple(x -> 1, N - 1)..., length(xs)), false, xs...) +## core hvncat implementations + + +# 1-dimensional hvncat methods + +_typed_hvncat(::Type{T}, ::Val{0}) where T = Vector{T}() +_typed_hvncat(::Type{T}, ::Val{0}, x) where T = fill(T(x)) +_typed_hvncat(::Type{T}, ::Val{0}, x::AbstractArray) where T = T.(x) +_typed_hvncat(::Type, ::Val{0}, ::AbstractArray...) = + throw(ArgumentError("a 0-dimensional array may not have more than one element")) +_typed_hvncat(::Type, ::Val{0}, ::Any...) = + throw(ArgumentError("a 0-dimensional array may not have more than one element")) + +_typed_hvncat(::Type{T}, ::Val{N}) where {T, N} = + (N < 0 && throw(ArgumentError("concatenation dimension must be nonnegative"))) || + Vector{T}() + function _typed_hvncat(::Type{T}, ::Val{N}, as::AbstractArray...) where {T, N} # optimization for arrays that can be concatenated by copying them linearly into the destination - # conditions: the elements must all have 1- or 0-length dimensions above N + # conditions: the elements must all have 1-length dimensions above N + length(as) > 0 || + throw(ArgumentError("must have at least one element")) + N < 0 && + throw(ArgumentError("concatenation dimension must be nonnegative")) for a ∈ as ndims(a) <= N || all(x -> size(a, x) == 1, (N + 1):ndims(a)) || return _typed_hvncat(T, (ntuple(x -> 1, N - 1)..., length(as)), false, as...) end - nd = max(N, ndims(as[1])) - + nd = N Ndim = 0 - for i ∈ 1:lastindex(as) - Ndim += cat_size(as[i], N) - for d ∈ 1:N - 1 - cat_size(as[1], d) == cat_size(as[i], d) || throw(ArgumentError("mismatched size along axis $d in element $i")) + for i ∈ eachindex(as) + a = as[i] + Ndim += size(a, N) + nd = max(nd, ndims(a)) + for d ∈ 1:N-1 + size(a, d) == size(as[1], d) || + throw(ArgumentError("all dimensions of element $i other than $N must be of length 1")) end end - A = Array{T, nd}(undef, ntuple(d -> cat_size(as[1], d), N - 1)..., Ndim, ntuple(x -> 1, nd - N)...) + A = similar(as[1], T, ntuple(d -> size(as[1], d), N - 1)..., Ndim, ntuple(x -> 1, nd - N)...) k = 1 for a ∈ as for i ∈ eachindex(a) @@ -2188,21 +2211,21 @@ function _typed_hvncat(::Type{T}, ::Val{N}, as::AbstractArray...) where {T, N} return A end -cat_ndims(a) = 0 -cat_ndims(a::AbstractArray) = ndims(a) - function _typed_hvncat(::Type{T}, ::Val{N}, as...) where {T, N} - # optimization for scalars and 1-length arrays that can be concatenated by copying them linearly - # into the destination + length(as) > 0 || + throw(ArgumentError("must have at least one element")) + N < 0 && + throw(ArgumentError("concatenation dimension must be nonnegative")) nd = N Ndim = 0 - for a ∈ as - if a isa AbstractArray - cat_size(a, N) == length(a) || - throw(ArgumentError("all dimensions of elements other than $N must be of length 1")) - nd = max(nd, cat_ndims(a)) - end + for i ∈ eachindex(as) + a = as[i] Ndim += cat_size(a, N) + nd = max(nd, cat_ndims(a)) + for d ∈ 1:N-1 + cat_size(a, d) == 1 || + throw(ArgumentError("all dimensions of element $i other than $N must be of length 1")) + end end A = Array{T, nd}(undef, ntuple(x -> 1, N - 1)..., Ndim, ntuple(x -> 1, nd - N)...) @@ -2221,30 +2244,85 @@ function _typed_hvncat(::Type{T}, ::Val{N}, as...) where {T, N} return A end -function _typed_hvncat(::Type{T}, dims::Tuple{Vararg{Int, N}}, row_first::Bool, as...) where {T, N} + +# 0-dimensional cases for balanced and unbalanced hvncat methods + +_typed_hvncat(::Type{T}, ::Tuple{}, ::Bool) where T = Vector{T}() +_typed_hvncat(::Type{T}, ::Tuple{}, ::Bool, x) where T = fill(T(x)) +_typed_hvncat(::Type{T}, ::Tuple{}, ::Bool, x::Number) where T = fill(T(x)) +_typed_hvncat(::Type{T}, ::Tuple{}, ::Bool, x::AbstractArray) where T = T.(x) +_typed_hvncat(::Type, ::Tuple{}, ::Bool, ::Number...) = + throw(ArgumentError("a 0-dimensional array may not have more than one element")) +_typed_hvncat(::Type, ::Tuple{}, ::Bool, ::Any...) = + throw(ArgumentError("a 0-dimensional array may not have more than one element")) + + +# balanced dimensions hvncat methods + +_typed_hvncat(T::Type, dims::Tuple{Int}, ::Bool, as...) = _typed_hvncat_1d(T, dims[1], Val(false), as...) +_typed_hvncat(T::Type, dims::Tuple{Int}, ::Bool, as::Number...) = _typed_hvncat_1d(T, dims[1], Val(false), as...) + +function _typed_hvncat_1d(::Type{T}, ds::Int, ::Val{row_first}, as...) where {T, row_first} + lengthas = length(as) + ds > 0 || + throw(ArgumentError("`dimsshape` argument must consist of positive integers")) + lengthas == ds || + throw(ArgumentError("number of elements does not match `dimshape` argument; expected $ds, got $lengthas")) + return row_first ? + _typed_hvncat(T, Val(2), as...) : + _typed_hvncat(T, Val(1), as...) +end + +function _typed_hvncat(::Type{T}, dims::NTuple{N, Int}, row_first::Bool, xs::Number...) where {T, N} + length(xs) > 0 || + throw(ArgumentError("must have at least one element")) + all(>(0), dims) || + throw(ArgumentError("`dims` argument must contain positive integers")) + A = Array{T, N}(undef, dims...) + lengtha = length(A) # Necessary to store result because throw blocks are being deoptimized right now, + lengthx = length(xs) # which leads to excessive allocations. Cuts from 3 allocations to 1. + if lengtha != lengthx + throw(ArgumentError("argument count does not match specified shape (expected $lengtha, got $lengthx)")) + end + hvncat_fill!(A, row_first, xs) + return A +end + +function _typed_hvncat(T::Type, dims::NTuple{N, Int}, row_first::Bool, as...) where {N} + # function barrier after calculating the max is necessary for high performance + nd = max(maximum(cat_ndims(a) for a ∈ as), N) + return _typed_hvncat_dims(T, (dims..., ntuple(x -> 1, nd - N)...), row_first, as) +end + +function _typed_hvncat_dims(::Type{T}, dims::NTuple{N, Int}, row_first::Bool, as::Tuple) where {T, N} + length(as) > 0 || + throw(ArgumentError("must have at least one element")) + all(>(0), dims) || + throw(ArgumentError("`dims` argument must contain positive integers")) + d1 = row_first ? 2 : 1 d2 = row_first ? 1 : 2 - - # discover dimensions - nd = max(N, cat_ndims(as[1])) - outdims = zeros(Int, nd) + + outdims = zeros(Int, N) # discover number of rows or columns for i ∈ 1:dims[d1] outdims[d1] += cat_size(as[i], d1) end - currentdims = zeros(Int, nd) + currentdims = zeros(Int, N) blockcount = 0 + elementcount = 0 for i ∈ eachindex(as) + elementcount += cat_length(as[i]) currentdims[d1] += cat_size(as[i], d1) if currentdims[d1] == outdims[d1] currentdims[d1] = 0 - for d ∈ (d2, 3:nd...) + for d ∈ (d2, 3:N...) currentdims[d] += cat_size(as[i], d) if outdims[d] == 0 # unfixed dimension blockcount += 1 - if blockcount == (d > length(dims) ? 1 : dims[d]) # last expected member of dimension + if blockcount == (d > N ? 1 : dims[d]) # last expected member of dimension outdims[d] = currentdims[d] currentdims[d] = 0 blockcount = 0 @@ -2257,76 +2335,112 @@ function _typed_hvncat(::Type{T}, dims::Tuple{Vararg{Int, N}}, row_first::Bool, elseif currentdims[d] < outdims[d] # dimension in progress break else # exceeded dimension - ArgumentError("argument $i has too many elements along axis $d") |> throw + throw(ArgumentError("argument $i has too many elements along axis $d")) end end end elseif currentdims[d1] > outdims[d1] # exceeded dimension - ArgumentError("argument $i has too many elements along axis $d1") |> throw + throw(ArgumentError("argument $i has too many elements along axis $d1")) end end - # calling sum() leads to 3 extra allocations - len = 0 - for a ∈ as - len += cat_length(a) - end outlen = prod(outdims) - outlen == 0 && ArgumentError("too few elements in arguments, unable to infer dimensions") |> throw - len == outlen || ArgumentError("too many elements in arguments; expected $(outlen), got $(len)") |> throw + elementcount == outlen || + throw(ArgumentError("mismatched number of elements; expected $(outlen), got $(elementcount)")) # copy into final array - A = Array{T, nd}(undef, outdims...) + A = cat_similar(as[1], T, outdims) # @assert all(==(0), currentdims) outdims .= 0 hvncat_fill!(A, currentdims, outdims, d1, d2, as) return A end -function _typed_hvncat(::Type{T}, shape::Tuple{Vararg{Tuple, N}}, row_first::Bool, as...) where {T, N} + +# unbalanced dimensions hvncat methods + +_typed_hvncat(T::Type, shape::Tuple{Tuple}, row_first::Bool, xs...) = + (length(shape[1]) == 0 && + throw(ArgumentError("each level of `shape` argument must have at least one value"))) || + _typed_hvncat_1d(T, shape[1][1], Val(row_first), xs...) + +function _typed_hvncat(T::Type, shape::NTuple{N, Tuple}, row_first::Bool, as...) where {N} + # function barrier after calculating the max is necessary for high performance + nd = max(maximum(cat_ndims(a) for a ∈ as), N) + return _typed_hvncat_shape(T, (shape..., ntuple(x -> shape[end], nd - N)...), row_first, as) +end + +function _typed_hvncat_shape(::Type{T}, shape::NTuple{N, Tuple}, row_first, as::Tuple) where {T, N} + length(as) > 0 || + throw(ArgumentError("must have at least one element")) + all(>(0), tuple((shape...)...)) || + throw(ArgumentError("`shape` argument must consist of positive integers")) + d1 = row_first ? 2 : 1 d2 = row_first ? 1 : 2 - shape = collect(shape) # saves allocations later - shapelength = shape[end][1] - lengthas = length(as) - shapelength == lengthas || throw(ArgumentError("number of elements does not match shape; expected $(shapelength), got $lengthas)")) - # discover dimensions - nd = max(N, cat_ndims(as[1])) - outdims = zeros(Int, nd) - currentdims = zeros(Int, nd) - blockcounts = zeros(Int, nd) - shapepos = ones(Int, nd) + outdims = zeros(Int, N) + currentdims = zeros(Int, N) + blockcounts = zeros(Int, N) + shapepos = ones(Int, N) + shapev = Vector{Tuple{Vararg{Int}}}(undef, N) + for i ∈ eachindex(shape) + shapev[i] = shape[i] + end + all(!isempty, shapev) || + throw(ArgumentError("each level of `shape` argument must have at least one value")) + length(shapev[end]) == 1 || + throw(ArgumentError("last level of shape must contain only one integer")) + shapelength = shapev[end][1] + for i ∈ eachindex(shapev) + sum(shapev[i]) == shapelength || + throw(ArgumentError("all levels of `shape` argument must sum to the same value")) + end + lengthas = length(as) + shapelength == lengthas || + throw(ArgumentError("number of elements does not match shape; expected $(shapelength), got $lengthas)")) + + elementcount = 0 for i ∈ eachindex(as) + elementcount += cat_length(as[i]) wasstartblock = false for d ∈ 1:N ad = (d < 3 && row_first) ? (d == 1 ? 2 : 1) : d dsize = cat_size(as[i], ad) - blockcounts[d] += 1 + blockcounts[d] += cat_length(as[i]) > 0 ? 1 : 0 if d == 1 || i == 1 || wasstartblock currentdims[d] += dsize elseif dsize != cat_size(as[i - 1], ad) - ArgumentError("argument $i has a mismatched number of elements along axis $ad; expected $(cat_size(as[i - 1], ad)), got $dsize") |> throw + throw(ArgumentError("""argument $i has a mismatched number of elements along axis $ad; \ + expected $(cat_size(as[i - 1], ad)), got $dsize""")) end wasstartblock = blockcounts[d] == 1 # remember for next dimension - isendblock = blockcounts[d] == shape[d][shapepos[d]] + isendblock = blockcounts[d] == shapev[d][shapepos[d]] if isendblock if outdims[d] == 0 outdims[d] = currentdims[d] elseif outdims[d] != currentdims[d] - ArgumentError("argument $i has a mismatched number of elements along axis $ad; expected $(abs(outdims[d] - (currentdims[d] - dsize))), got $dsize") |> throw + throw(ArgumentError("""argument $i has a mismatched number of elements along axis $ad; \ + expected $(abs(outdims[d] - (currentdims[d] - dsize))), got $dsize""")) end currentdims[d] = 0 blockcounts[d] = 0 shapepos[d] += 1 + d > 1 && (blockcounts[d - 1] == 0 || + throw(ArgumentError("""shape in level $d is inconsistent; level counts must nest \ + evenly into each other"""))) end end end + outlen = prod(outdims) + elementcount == outlen || + throw(ArgumentError("mismatched number of elements; expected $(outlen), got $(elementcount)")) + if row_first outdims[1], outdims[2] = outdims[2], outdims[1] end @@ -2335,33 +2449,66 @@ function _typed_hvncat(::Type{T}, shape::Tuple{Vararg{Tuple, N}}, row_first::Boo # @assert all(==(0), blockcounts) # copy into final array - A = Array{T, nd}(undef, outdims...) + A = cat_similar(as[1], T, outdims) hvncat_fill!(A, currentdims, blockcounts, d1, d2, as) return A end -function hvncat_fill!(A::Array{T, N}, scratch1::Vector{Int}, scratch2::Vector{Int}, d1::Int, d2::Int, as::Tuple{Vararg}) where {T, N} +function hvncat_fill!(A::Array, row_first::Bool, xs::Tuple) + # putting these in separate functions leads to unnecessary allocations + lenxs = length(xs) + lena = length(A) + lenxs == lena || throw(ArgumentError("number of elements don't match specified shape")) + if row_first + nr, nc = size(A, 1), size(A, 2) + nrc = nr * nc + na = prod(size(A)[3:end]) + k = 1 + @inbounds for d ∈ 1:na + dd = nrc * (d - 1) + for i ∈ 1:nr + Ai = dd + i + for _ ∈ 1:nc + A[Ai] = xs[k] + k += 1 + Ai += nr + end + end + end + else + @inbounds for k ∈ eachindex(xs) + A[k] = xs[k] + end + end +end + +@inline function hvncat_fill!(A::AbstractArray{T, N}, scratch1::Vector{Int}, scratch2::Vector{Int}, + d1::Int, d2::Int, as::Tuple) where {T, N} + length(scratch1) == length(scratch2) == N || + throw(ArgumentError("scratch vectors must have as many elements as the destination array has dimensions")) + 0 < d1 < 3 && 0 < d2 < 3 && d1 != d2 || + throw(ArgumentError("d1 and d2 must be either 1 or 2, exclusive.")) outdims = size(A) offsets = scratch1 inneroffsets = scratch2 for a ∈ as if isa(a, AbstractArray) for ai ∈ a - Ai = hvncat_calcindex(offsets, inneroffsets, outdims, N) + @inbounds Ai = hvncat_calcindex(offsets, inneroffsets, outdims, N) A[Ai] = ai - for j ∈ 1:N + @inbounds for j ∈ 1:N inneroffsets[j] += 1 inneroffsets[j] < cat_size(a, j) && break inneroffsets[j] = 0 end end else - Ai = hvncat_calcindex(offsets, inneroffsets, outdims, N) + @inbounds Ai = hvncat_calcindex(offsets, inneroffsets, outdims, N) A[Ai] = a end - for j ∈ (d1, d2, 3:N...) + @inbounds for j ∈ (d1, d2, 3:N...) offsets[j] += cat_size(a, j) offsets[j] < outdims[j] && break offsets[j] = 0 @@ -2382,9 +2529,6 @@ end Ai end -cat_length(a::AbstractArray) = length(a) -cat_length(::Any) = 1 - ## Reductions and accumulates ## function isequal(A::AbstractArray, B::AbstractArray) diff --git a/test/abstractarray.jl b/test/abstractarray.jl index 65f9f4efa2cd3..de23dce3d2976 100644 --- a/test/abstractarray.jl +++ b/test/abstractarray.jl @@ -1342,6 +1342,7 @@ end end end +import Base.typed_hvncat @testset "hvncat" begin a = fill(1, (2,3,2,4,5)) b = fill(2, (1,1,2,4,5)) @@ -1389,7 +1390,130 @@ end @test [v v;;; fill(v, 1, 2)] == fill(v, 1, 2, 2) end - @test_throws BoundsError hvncat(((1, 2), (3,)), false, zeros(Int, 0, 0, 0), 7, 8) + # 0-dimension behaviors + @test hvncat(0) == [] + @test hvncat(0, 1) == fill(1) + @test hvncat(0, [1]) == [1] + @test_throws ArgumentError hvncat(0, 1, 1) + @test typed_hvncat(Float64, 0) == Float64[] + @test typed_hvncat(Float64, 0, 1) == fill(1.0) + @test typed_hvncat(Float64, 0, [1]) == Float64[1.0] + @test_throws ArgumentError typed_hvncat(Float64, 0, 1, 1) + @test hvncat((), true) == [] + @test hvncat((), true, 1) == fill(1) + @test hvncat((), true, [1]) == [1] + @test_throws ArgumentError hvncat((), true, 1, 1) + @test typed_hvncat(Float64, (), true) == Float64[] + @test typed_hvncat(Float64, (), true, 1) == fill(1.0) + @test typed_hvncat(Float64, (), true, [1]) == [1.0] + @test_throws ArgumentError typed_hvncat(Float64, (), true, 1, 1) + + # 1-dimension behaviors + # int form + @test hvncat(1) == [] + @test hvncat(1, 1) == [1] + @test hvncat(1, [1]) == [1] + @test hvncat(1, 1, 1) == [1 ; 1] + @test typed_hvncat(Float64, 1) == Float64[] + @test typed_hvncat(Float64, 1, 1) == Float64[1.0] + @test typed_hvncat(Float64, 1, [1]) == Float64[1.0] + @test typed_hvncat(Float64, 1, 1, 1) == Float64[1.0 ; 1.0] + # dims form + @test_throws ArgumentError hvncat((1,), true) + @test hvncat((2,), true, 1, 1) == [1; 1] + @test hvncat((2,), true, [1], [1]) == [1; 1] + @test_throws ArgumentError hvncat((2,), true, 1) + @test typed_hvncat(Float64, (2,), true, 1, 1) == Float64[1.0; 1.0] + @test typed_hvncat(Float64, (2,), true, [1], [1]) == Float64[1.0; 1.0] + @test_throws ArgumentError typed_hvncat(Float64, (2,), true, 1) + # row_first has no effect with just one dimension of the dims form + @test hvncat((2,), false, 1, 1) == [1; 1] + @test typed_hvncat(Float64, (2,), false, 1, 1) == Float64[1.0; 1.0] + # shape form + @test hvncat(((2,),), true, 1, 1) == [1 1] + @test hvncat(((2,),), true, [1], [1]) == [1 1] + @test_throws ArgumentError hvncat(((2,),), true, 1) + @test hvncat(((2,),), false, 1, 1) == [1; 1] + @test hvncat(((2,),), false, [1], [1]) == [1; 1] + @test typed_hvncat(Float64, ((2,),), true, 1, 1) == Float64[1.0 1.0] + @test typed_hvncat(Float64, ((2,),), true, [1], [1]) == Float64[1.0 1.0] + @test_throws ArgumentError typed_hvncat(Float64, ((2,),), true, 1) + @test typed_hvncat(Float64, ((2,),), false, 1, 1) == Float64[1.0; 1.0] + @test typed_hvncat(Float64, ((2,),), false, [1], [1]) == Float64[1.0; 1.0] + + for v ∈ ((), (1,), ([1],), (1, [1]), ([1], 1), ([1], [1])) + # reject dimension < 0 + @test_throws ArgumentError hvncat(-1, v...) + + # reject shape tuple with no elements + @test_throws ArgumentError hvncat(((),), true, v...) + end + + # reject dims or shape with negative or zero values + for v1 ∈ (-1, 0, 1) + for v2 ∈ (-1, 0, 1) + v1 == v2 == 1 && continue + for v3 ∈ ((), (1,), ([1],), (1, [1]), ([1], 1), ([1], [1])) + @test_throws ArgumentError hvncat((v1, v2), true, v3...) + @test_throws ArgumentError hvncat(((v1,), (v2,)), true, v3...) + end + end + end + + for v ∈ ((1, [1]), ([1], 1), ([1], [1])) + # reject shape with more than one end value + @test_throws ArgumentError hvncat(((1, 1),), true, v...) + end + + for v ∈ ((1, 2, 3), (1, 2, [3]), ([1], [2], [3])) + # reject shape with more values in later level + @test_throws ArgumentError hvncat(((2, 1), (1, 1, 1)), true, v...) + end + + # output dimensions are maximum of input dimensions and concatenation dimension + begin + v1 = fill(1, 1, 1) + v2 = fill(1, 1, 1, 1, 1) + v3 = fill(1, 1, 2, 1, 1) + @test [v1 ;;; v2] == [1 ;;; 1 ;;;;] + @test [v2 ;;; v1] == [1 ;;; 1 ;;;;] + @test [v3 ;;; v1 v1] == [1 1 ;;; 1 1 ;;;;] + @test [v1 v1 ;;; v3] == [1 1 ;;; 1 1 ;;;;] + @test [v2 v1 ;;; v1 v1] == [1 1 ;;; 1 1 ;;;;] + @test [v1 v1 ;;; v1 v2] == [1 1 ;;; 1 1 ;;;;] + @test [v2 ;;; 1] == [1 ;;; 1 ;;;;] + @test [1 ;;; v2] == [1 ;;; 1 ;;;;] + @test [v3 ;;; 1 v1] == [1 1 ;;; 1 1 ;;;;] + @test [v1 1 ;;; v3] == [1 1 ;;; 1 1 ;;;;] + @test [v2 1 ;;; v1 v1] == [1 1 ;;; 1 1 ;;;;] + @test [v1 1 ;;; v1 v2] == [1 1 ;;; 1 1 ;;;;] + end + + # reject shapes that don't nest evenly between levels (e.g. 1 + 2 does not fit into 2) + @test_throws ArgumentError hvncat(((1, 2, 1), (2, 2), (4,)), true, [1 2], [3], [4], [1 2; 3 4]) + + # zero-length arrays are handled appropriately + @test [zeros(Int, 1, 2, 0) ;;; 1 3] == [1 3;;;] + @test [[] ;;; [] ;;; []] == Array{Any}(undef, 0, 1, 3) + @test [[] ; 1 ;;; 2 ; []] == [1 ;;; 2] + @test [[] ; [] ;;; [] ; []] == Array{Any}(undef, 0, 1, 2) + @test [[] ; 1 ;;; 2] == [1 ;;; 2] + @test [[] ; [] ;;; [] ;;; []] == Array{Any}(undef, 0, 1, 3) + z = zeros(Int, 0, 0, 0) + [z z ; z ;;; z ;;; z] == Array{Int}(undef, 0, 0, 0) + + for v1 ∈ (zeros(Int, 0, 0), zeros(Int, 0, 0, 0), zeros(Int, 0, 0, 0, 0), zeros(Int, 0, 0, 0, 0, 0, 0, 0)) + for v2 ∈ (1, [1]) + for v3 ∈ (2, [2]) + @test_throws ArgumentError [v1 ;;; v2] + @test_throws ArgumentError [v1 ;;; v2 v3] + @test_throws ArgumentError [v1 v1 ;;; v2 v3] + end + end + end + + @test_throws ArgumentError [zeros(Int, 0, 0, 0) ;;; 1 3] # fails + @test_throws ArgumentError [zeros(Int, 0, 0, 0) ;;; [1] [3]] # fails end @testset "keepat!" begin diff --git a/test/bitarray.jl b/test/bitarray.jl index cee7624d9a81a..e05c220d1ddcc 100644 --- a/test/bitarray.jl +++ b/test/bitarray.jl @@ -1704,3 +1704,10 @@ end @check_bit_operation all!(falses(100), trues(100, 100)) @check_bit_operation all!(falses(1000), trues(1000, 100)) end + +@testset "multidimensional concatenation returns BitArrays" begin + a = BitVector(ones(5)) + typeof([a ;;; a]) <: BitArray + typeof([a a ;;; a a]) <: BitArray + typeof([a a ;;; [a a]]) <: BitArray +end