diff --git a/src/PooledArrays.jl b/src/PooledArrays.jl index b524b1d..2f11aba 100644 --- a/src/PooledArrays.jl +++ b/src/PooledArrays.jl @@ -2,7 +2,7 @@ module PooledArrays import DataAPI -export PooledArray, PooledVector, PooledMatrix +export PooledArray, PooledVector, PooledMatrix, dropunusedlevels! ############################################################################## ## @@ -18,31 +18,46 @@ mutable struct RefArray{R} a::R end -function _invert(d::Dict{K,V}) where {K,V} +function _invert(d::Dict{K, V}) where {K, V} d1 = Vector{K}(undef, length(d)) for (k, v) in d d1[v] = k end - d1 + return d1 end -mutable struct PooledArray{T, R<:Integer, N, RA} <: AbstractArray{T, N} +mutable struct PooledArray{T, R <: Integer, N, RA} <: AbstractArray{T, N} refs::RA pool::Vector{T} - invpool::Dict{T,R} + invpool::Dict{T, R} - function PooledArray(rs::RefArray{RA}, - invpool::Dict{T, R}, - pool=_invert(invpool)) where {T,R,N,RA<:AbstractArray{R, N}} + function PooledArray( + rs::RefArray{RA}, + invpool::Dict{T, R}, + pool = _invert(invpool), + ) where {T, R, N, RA <: AbstractArray{R, N}} # refs mustn't overflow pool if length(rs.a) > 0 && maximum(rs.a) > length(invpool) throw(ArgumentError("Reference array points beyond the end of the pool")) end - new{T,R,N,RA}(rs.a,pool,invpool) + return new{T, R, N, RA}(rs.a, pool, invpool) end + + function PooledArray( + rs::RefArray{RA}, + pool::Vector{T}, + invpool::Dict{T, R}=Dict{T, R}(x => i for (i, x) in enumerate(pool)) + ) where {T, R, N, RA <: AbstractArray{R, N}} + # refs mustn't overflow pool + if length(rs.a) > 0 && maximum(rs.a) > length(invpool) + throw(ArgumentError("Reference array points beyond the end of the pool")) + end + return new{T, R, N, RA}(rs.a, pool, invpool) + end + end -const PooledVector{T,R} = PooledArray{T,R,1} -const PooledMatrix{T,R} = PooledArray{T,R,2} +const PooledVector{T, R} = PooledArray{T, R, 1} +const PooledMatrix{T, R} = PooledArray{T, R, 2} ############################################################################## ## @@ -59,22 +74,26 @@ const PooledMatrix{T,R} = PooledArray{T,R,2} ############################################################################## # Echo inner constructor as an outer constructor -function PooledArray(refs::RefArray{R}, invpool::Dict{T,R}, pool=_invert(invpool)) where {T,R} - PooledArray{T,eltype(R),ndims(R),R}(refs, invpool, pool) +function PooledArray( + refs::RefArray{R}, + invpool::Dict{T, R}, + pool = _invert(invpool), +) where {T, R} + return PooledArray{T, eltype(R), ndims(R), R}(refs, invpool, pool) end PooledArray(d::PooledArray) = copy(d) -function _label(xs::AbstractArray, - ::Type{T}=eltype(xs), - ::Type{I}=DEFAULT_POOLED_REF_TYPE, - start = 1, - labels = Array{I}(undef, size(xs)), - invpool::Dict{T,I} = Dict{T, I}(), - pool::Vector{T} = T[], - nlabels = 0, - ) where {T, I<:Integer} - +function _label( + xs::AbstractArray, + ::Type{T} = eltype(xs), + ::Type{I} = DEFAULT_POOLED_REF_TYPE, + start = 1, + labels = Array{I}(undef, size(xs)), + invpool::Dict{T, I} = Dict{T, I}(), + pool::Vector{T} = T[], + nlabels = 0, +) where {T, I <: Integer} @inbounds for i in start:length(xs) x = xs[i] lbl = get(invpool, x, zero(I)) @@ -83,8 +102,16 @@ function _label(xs::AbstractArray, else if nlabels == typemax(I) I2 = _widen(I) - return _label(xs, T, I2, i, convert(Vector{I2}, labels), - convert(Dict{T, I2}, invpool), pool, nlabels) + return _label( + xs, + T, + I2, + i, + convert(Vector{I2}, labels), + convert(Dict{T, I2}, invpool), + pool, + nlabels, + ) end nlabels += 1 labels[i] = nlabels @@ -92,7 +119,7 @@ function _label(xs::AbstractArray, push!(pool, x) end end - labels, invpool, pool + return labels, invpool, pool end _widen(::Type{UInt8}) = UInt16 @@ -112,7 +139,7 @@ If `array` is not a `PooledArray` then the order of elements in `refpool` in the """ PooledArray -function PooledArray{T}(d::AbstractArray, r::Type{R}) where {T,R<:Integer} +function PooledArray{T}(d::AbstractArray, r::Type{R}) where {T, R <: Integer} refs, invpool, pool = _label(d, T, R) if length(invpool) > typemax(R) @@ -120,20 +147,20 @@ function PooledArray{T}(d::AbstractArray, r::Type{R}) where {T,R<:Integer} end # Assertions are needed since _label is not type stable - PooledArray(RefArray(refs::Vector{R}), invpool::Dict{T,R}, pool) + return PooledArray(RefArray(refs::Vector{R}), invpool::Dict{T, R}, pool) end -function PooledArray{T}(d::AbstractArray) where T +function PooledArray{T}(d::AbstractArray) where {T} refs, invpool, pool = _label(d, T) - PooledArray(RefArray(refs), invpool, pool) + return PooledArray(RefArray(refs), invpool, pool) end PooledArray(d::AbstractArray{T}, r::Type) where {T} = PooledArray{T}(d, r) PooledArray(d::AbstractArray{T}) where {T} = PooledArray{T}(d) # Construct an empty PooledVector of a specific type -PooledArray(t::Type) = PooledArray(Array(t,0)) -PooledArray(t::Type, r::Type) = PooledArray(Array(t,0), r) +PooledArray(t::Type) = PooledArray(Array(t, 0)) +PooledArray(t::Type, r::Type) = PooledArray(Array(t, 0), r) ############################################################################## ## @@ -152,27 +179,27 @@ Base.lastindex(pa::PooledArray) = lastindex(pa.refs) Base.copy(pa::PooledArray) = PooledArray(RefArray(copy(pa.refs)), copy(pa.invpool)) # TODO: Implement copy_to() -function Base.resize!(pa::PooledArray{T,R,1}, n::Integer) where {T,R} +function Base.resize!(pa::PooledArray{T, R, 1}, n::Integer) where {T, R} oldn = length(pa.refs) resize!(pa.refs, n) - pa.refs[oldn+1:n] .= zero(R) - pa + pa.refs[(oldn + 1):n] .= zero(R) + return pa end Base.reverse(x::PooledArray) = PooledArray(RefArray(reverse(x.refs)), x.invpool) -function Base.permute!!(x::PooledArray, p::AbstractVector{T}) where T<:Integer +function Base.permute!!(x::PooledArray, p::AbstractVector{T}) where {T <: Integer} Base.permute!!(x.refs, p) - x + return x end -function Base.invpermute!!(x::PooledArray, p::AbstractVector{T}) where T<:Integer +function Base.invpermute!!(x::PooledArray, p::AbstractVector{T}) where {T <: Integer} Base.invpermute!!(x.refs, p) - x + return x end -function Base.similar(pa::PooledArray{T,R}, S::Type, dims::Dims) where {T,R} - PooledArray(RefArray(zeros(R, dims)), Dict{S,R}()) +function Base.similar(pa::PooledArray{T, R}, S::Type, dims::Dims) where {T, R} + return PooledArray(RefArray(zeros(R, dims)), Dict{S, R}()) end Base.findall(pdv::PooledVector{Bool}) = findall(convert(Vector{Bool}, pdv)) @@ -184,31 +211,68 @@ Base.findall(pdv::PooledVector{Bool}) = findall(convert(Vector{Bool}, pdv)) ## ############################################################################## -function Base.map(f, x::PooledArray{T,R}) where {T,R<:Integer} - ks = collect(keys(x.invpool)) - vs = collect(values(x.invpool)) - ks1 = map(f, ks) - uks = Set(ks1) - if length(uks) < length(ks1) - # this means some keys have repeated - newinvpool = Dict{eltype(ks1), eltype(vs)}() - translate = Dict{eltype(vs), eltype(vs)}() - i = 1 - for (k, k1) in zip(ks, ks1) - if haskey(newinvpool, k1) - translate[x.invpool[k]] = newinvpool[k1] +""" + dropunusedlevels!(x::PooledArray) + +When PooledArrays are modified, via `setindex!`, or deleting elements, +pooled values that were present originally may no longer appear in the +array, yet still exist in the "pool". This can be desirable if these +values may still be reintroduced, but sometimes, it's desirable to +completely drop any reference to these pooled values. This need +may come up when calling `map` on a modified PooledArray, which +only applies the mapping function to the pool as an optimization. +This can lead to unexpected results if the mapping function +encounters pooled values that may not be present in the current array. +""" +function dropunusedlevels!(x::PooledArray{T, R}) where {T, R} + # count number of occurences of each pool value + counts = zeros(Int, length(x.pool)) + for ref in x.refs + @inbounds counts[ref] += 1 + end + if any(iszero, counts) + # if there are any 0 counts, remove the pool value + # and prepare to recode any higher refs + newpoolidx = zeros(Int, length(x.pool)) + numtoremove = 0 + for (i, count) in enumerate(counts) + if count == 0 + numtoremove += 1 else - newinvpool[k1] = i - translate[x.invpool[k]] = i - i+=1 + newpoolidx[i] = i - numtoremove end end - refarray = map(x->translate[x], x.refs) - else - newinvpool = Dict(zip(map(f, ks), vs)) - refarray = copy(x.refs) + # recode existing refs with adjusted values + for (i, val) in enumerate(x.refs) + @inbounds x.refs[i] = R(newpoolidx[val]) + end + # now to adjust pool, remove unused and recode invpool + for i = length(counts):-1:1 + if counts[i] == 0 + val = x.pool[i] + delete!(x.invpool, val) + deleteat!(x.pool, i) + else + x.invpool[x.pool[i]] = R(newpoolidx[i]) + end + end + end + return +end + +function Base.map(f, x::PooledArray{T, R}; dropunusedlevels::Bool=false) where {T, R <: Integer} + if dropunusedlevels + dropunusedlevels!(x) + end + newpool = map(x.pool) do val + try + f(val) + catch e + @warn "error applying `f` to $val; you may need to call `dropunusedlevels!(x)` first" + rethrow(e) + end end - PooledArray(RefArray(refarray), newinvpool) + return PooledArray(RefArray(copy(x.refs)), newpool) end ############################################################################## @@ -224,14 +288,14 @@ function groupsort_indexer(x::AbstractVector, ngroups::Integer, perm) n = length(x) # counts = x.invpool counts = fill(0, ngroups + 1) - @inbounds for i = 1:n + @inbounds for i in 1:n counts[x[i] + 1] += 1 end - counts[2:end] = counts[perm.+1] + counts[2:end] = counts[perm .+ 1] # mark the start of each contiguous group of like-indexed data where = fill(1, ngroups + 1) - @inbounds for i = 2:ngroups+1 + @inbounds for i in 2:(ngroups + 1) where[i] = where[i - 1] + counts[i - 1] end @@ -239,21 +303,25 @@ function groupsort_indexer(x::AbstractVector, ngroups::Integer, perm) result = fill(0, n) iperm = invperm(perm) - @inbounds for i = 1:n + @inbounds for i in 1:n label = iperm[x[i]] + 1 result[where[label]] = i where[label] += 1 end - result, where, counts + return result, where, counts end -function Base.sortperm(pa::PooledArray; alg::Base.Sort.Algorithm=Base.Sort.DEFAULT_UNSTABLE, - lt::Function=isless, by::Function=identity, - rev::Bool=false, order=Base.Sort.Forward, - _ord = Base.ord(lt, by, rev, order), - poolperm = sortperm(pa.pool, alg=alg, order=_ord)) - - groupsort_indexer(pa.refs, length(pa.pool), poolperm)[1] +function Base.sortperm( + pa::PooledArray; + alg::Base.Sort.Algorithm = Base.Sort.DEFAULT_UNSTABLE, + lt::Function = isless, + by::Function = identity, + rev::Bool = false, + order = Base.Sort.Forward, + _ord = Base.ord(lt, by, rev, order), + poolperm = sortperm(pa.pool, alg = alg, order = _ord), +) + return groupsort_indexer(pa.refs, length(pa.pool), poolperm)[1] end Base.sort(pa::PooledArray; kw...) = pa[sortperm(pa; kw...)] @@ -272,25 +340,41 @@ Base.sort(pa::PooledArray; kw...) = pa[sortperm(pa; kw...)] ## ############################################################################## -Base.convert(::Type{PooledArray{S,R1,N}}, pa::PooledArray{T,R2,N}) where {S,T,R1<:Integer,R2<:Integer,N} = - PooledArray(RefArray(convert(Array{R1,N}, pa.refs)), convert(Dict{S,R1}, pa.invpool)) -Base.convert(::Type{PooledArray{S,R,N}}, pa::PooledArray{T,R,N}) where {S,T,R<:Integer,N} = - PooledArray(RefArray(copy(pa.refs)), convert(Dict{S,R}, pa.invpool)) -Base.convert(::Type{PooledArray{T,R,N}}, pa::PooledArray{T,R,N}) where {T,R<:Integer,N} = pa -Base.convert(::Type{PooledArray{S,R1}}, pa::PooledArray{T,R2,N}) where {S,T,R1<:Integer,R2<:Integer,N} = - convert(PooledArray{S,R1,N}, pa) -Base.convert(::Type{PooledArray{S}}, pa::PooledArray{T,R,N}) where {S,T,R<:Integer,N} = - convert(PooledArray{S,R,N}, pa) -Base.convert(::Type{PooledArray}, pa::PooledArray{T,R,N}) where {T,R<:Integer,N} = pa - -Base.convert(::Type{PooledArray{S,R,N}}, a::AbstractArray{T,N}) where {S,T,R<:Integer,N} = - PooledArray(convert(Array{S,N}, a), R) -Base.convert(::Type{PooledArray{S,R}}, a::AbstractArray{T,N}) where {S,T,R<:Integer,N} = - PooledArray(convert(Array{S,N}, a), R) -Base.convert(::Type{PooledArray{S}}, a::AbstractArray{T,N}) where {S,T,N} = - PooledArray(convert(Array{S,N}, a)) -Base.convert(::Type{PooledArray}, a::AbstractArray) = - PooledArray(a) +Base.convert( + ::Type{PooledArray{S, R1, N}}, + pa::PooledArray{T, R2, N}, +) where {S, T, R1 <: Integer, R2 <: Integer, N} = + PooledArray(RefArray(convert(Array{R1, N}, pa.refs)), convert(Dict{S, R1}, pa.invpool)) +Base.convert( + ::Type{PooledArray{S, R, N}}, + pa::PooledArray{T, R, N}, +) where {S, T, R <: Integer, N} = + PooledArray(RefArray(copy(pa.refs)), convert(Dict{S, R}, pa.invpool)) +Base.convert( + ::Type{PooledArray{T, R, N}}, + pa::PooledArray{T, R, N}, +) where {T, R <: Integer, N} = pa +Base.convert( + ::Type{PooledArray{S, R1}}, + pa::PooledArray{T, R2, N}, +) where {S, T, R1 <: Integer, R2 <: Integer, N} = convert(PooledArray{S, R1, N}, pa) +Base.convert( + ::Type{PooledArray{S}}, + pa::PooledArray{T, R, N}, +) where {S, T, R <: Integer, N} = convert(PooledArray{S, R, N}, pa) +Base.convert(::Type{PooledArray}, pa::PooledArray{T, R, N}) where {T, R <: Integer, N} = pa + +Base.convert( + ::Type{PooledArray{S, R, N}}, + a::AbstractArray{T, N}, +) where {S, T, R <: Integer, N} = PooledArray(convert(Array{S, N}, a), R) +Base.convert( + ::Type{PooledArray{S, R}}, + a::AbstractArray{T, N}, +) where {S, T, R <: Integer, N} = PooledArray(convert(Array{S, N}, a), R) +Base.convert(::Type{PooledArray{S}}, a::AbstractArray{T, N}) where {S, T, N} = + PooledArray(convert(Array{S, N}, a)) +Base.convert(::Type{PooledArray}, a::AbstractArray) = PooledArray(a) function Base.convert(::Type{Array{S, N}}, pa::PooledArray{T, R, N}) where {S, T, R, N} res = Array{S}(undef, size(pa)) @@ -306,7 +390,8 @@ Base.convert(::Type{Vector}, pv::PooledVector{T, R}) where {T, R} = convert(Arra Base.convert(::Type{Matrix}, pm::PooledMatrix{T, R}) where {T, R} = convert(Array{T, 2}, pm) -Base.convert(::Type{Array}, pa::PooledArray{T, R, N}) where {T, R, N} = convert(Array{T, N}, pa) +Base.convert(::Type{Array}, pa::PooledArray{T, R, N}) where {T, R, N} = + convert(Array{T, N}, pa) ############################################################################## ## @@ -322,12 +407,15 @@ Base.@propagate_inbounds function Base.getindex(pa::PooledArray, I::Integer...) end Base.@propagate_inbounds function Base.isassigned(pa::PooledArray, I::Int...) - !iszero(pa.refs[I...]) + return !iszero(pa.refs[I...]) end # Vector case -Base.@propagate_inbounds function Base.getindex(A::PooledArray, I::Union{Real,AbstractVector}...) - PooledArray(RefArray(getindex(A.refs, I...)), copy(A.invpool)) +Base.@propagate_inbounds function Base.getindex( + A::PooledArray, + I::Union{Real, AbstractVector}..., +) + return PooledArray(RefArray(getindex(A.refs, I...)), copy(A.invpool)) end # Dispatch our implementation for these cases instead of Base @@ -342,8 +430,8 @@ Base.@propagate_inbounds Base.getindex(A::PooledArray, I::AbstractArray) = ## ############################################################################## -function getpoolidx(pa::PooledArray{T,R}, val::Any) where {T,R} - val::T = convert(T,val) +function getpoolidx(pa::PooledArray{T, R}, val::Any) where {T, R} + val::T = convert(T, val) pool_idx = get(pa.invpool, val, zero(R)) if pool_idx == zero(R) pool_idx = unsafe_pool_push!(pa, val) @@ -351,19 +439,19 @@ function getpoolidx(pa::PooledArray{T,R}, val::Any) where {T,R} return pool_idx end -function unsafe_pool_push!(pa::PooledArray{T,R}, val) where {T,R} - _pool_idx = length(pa.pool)+1 +function unsafe_pool_push!(pa::PooledArray{T, R}, val) where {T, R} + _pool_idx = length(pa.pool) + 1 if _pool_idx > typemax(R) throw(ErrorException(string( "You're using a PooledArray with ref type $R, which can only hold $(Int(typemax(R))) values,\n", "and you just tried to add the $(typemax(R)+1)th reference. Please change the ref type\n", - "to a larger int type, or use the default ref type ($DEFAULT_POOLED_REF_TYPE)." - ))) + "to a larger int type, or use the default ref type ($DEFAULT_POOLED_REF_TYPE).", + ))) end pool_idx = convert(R, _pool_idx) pa.invpool[val] = pool_idx push!(pa.pool, val) - pool_idx + return pool_idx end Base.@propagate_inbounds function Base.setindex!(x::PooledArray, val, ind::Integer) @@ -377,7 +465,7 @@ end ## ############################################################################## -function Base.push!(pv::PooledVector{S,R}, v::T) where {S,R,T} +function Base.push!(pv::PooledVector{S, R}, v::T) where {S, R, T} push!(pv.refs, getpoolidx(pv, v)) return pv end @@ -386,14 +474,14 @@ function Base.append!(pv::PooledVector, items::AbstractArray) itemindices = eachindex(items) l = length(pv) n = length(itemindices) - resize!(pv.refs, l+n) - copyto!(pv, l+1, items, first(itemindices), n) + resize!(pv.refs, l + n) + copyto!(pv, l + 1, items, first(itemindices), n) return pv end Base.pop!(pv::PooledVector) = pv.invpool[pop!(pv.refs)] -function Base.pushfirst!(pv::PooledVector{S,R}, v::T) where {S,R,T} +function Base.pushfirst!(pv::PooledVector{S, R}, v::T) where {S, R, T} pushfirst!(pv.refs, getpoolidx(pv, v)) return pv end @@ -404,27 +492,29 @@ Base.empty!(pv::PooledVector) = (empty!(pv.refs); pv) Base.deleteat!(pv::PooledVector, inds) = (deleteat!(pv.refs, inds); pv) -function _vcat!(c,a,b) +function _vcat!(c, a, b) copyto!(c, 1, a, 1, length(a)) - copyto!(c, length(a)+1, b, 1, length(b)) + return copyto!(c, length(a) + 1, b, 1, length(b)) end - function Base.vcat(a::PooledArray{<:Any, <:Integer, 1}, b::AbstractArray{<:Any, 1}) output = similar(b, promote_type(eltype(a), eltype(b)), length(b) + length(a)) - _vcat!(output, a, b) + return _vcat!(output, a, b) end function Base.vcat(a::AbstractArray{<:Any, 1}, b::PooledArray{<:Any, <:Integer, 1}) output = similar(a, promote_type(eltype(a), eltype(b)), length(b) + length(a)) - _vcat!(output, a, b) + return _vcat!(output, a, b) end -function Base.vcat(a::PooledArray{T, <:Integer, 1}, b::PooledArray{S, <:Integer, 1}) where {T, S} +function Base.vcat( + a::PooledArray{T, <:Integer, 1}, + b::PooledArray{S, <:Integer, 1}, +) where {T, S} ap = a.invpool bp = b.invpool - U = promote_type(T,S) + U = promote_type(T, S) poolmap = Dict{Int, Int}() l = length(ap) @@ -433,22 +523,22 @@ function Base.vcat(a::PooledArray{T, <:Integer, 1}, b::PooledArray{S, <:Integer, j = if x in keys(ap) poolmap[i] = ap[x] else - poolmap[i] = (l+=1) + poolmap[i] = (l += 1) end newlabels[x] = j end types = [UInt8, UInt16, UInt32, UInt64] - tidx = findfirst(t->l < typemax(t), types) + tidx = findfirst(t -> l < typemax(t), types) refT = types[tidx] - refs2 = map(r->convert(refT, poolmap[r]), b.refs) + refs2 = map(r -> convert(refT, poolmap[r]), b.refs) newrefs = Base.typed_vcat(refT, a.refs, refs2) return PooledArray(RefArray(newrefs), convert(Dict{U, refT}, newlabels)) end function fast_sortable(y::PooledArray) poolranks = invperm(sortperm(y.pool)) - newpool = Dict(j=>convert(eltype(y.refs), i) for (i,j) in enumerate(poolranks)) - PooledArray(RefArray(y.refs), newpool) + newpool = Dict(j => convert(eltype(y.refs), i) for (i, j) in enumerate(poolranks)) + return PooledArray(RefArray(y.refs), newpool) end _perm(o::F, z::V) where {F, V} = Base.Order.Perm{F, V}(o, z) diff --git a/test/runtests.jl b/test/runtests.jl index af967b5..d3531cb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,7 +4,7 @@ using DataAPI: refarray, refvalue, refpool @testset "PooledArrays" begin a = rand(10) - b = rand(10,10) + b = rand(10, 10) c = rand(1:10, 1000) @test PooledArray(a) == a @@ -44,11 +44,11 @@ using DataAPI: refarray, refvalue, refpool #@test issorted(pc.invpool) @test map(identity, pc) == pc - @test map(x->2x, pc) == map(x->2x, c) + @test map(x -> 2x, pc) == map(x -> 2x, c) # case where the outputs are one-to-many - pa = PooledArray([1,2,3,4,5,6]) - @test map(isodd, pa) == [true,false,true,false,true,false] + pa = PooledArray([1, 2, 3, 4, 5, 6]) + @test map(isodd, pa) == [true, false, true, false, true, false] px = PooledArray(rand(128)) py = PooledArray(rand(200)) @@ -65,9 +65,9 @@ using DataAPI: refarray, refvalue, refpool @test px2 !== px3 @test px2 != px3 - @test findall(PooledArray([true,false,true])) == [1,3] + @test findall(PooledArray([true, false, true])) == [1, 3] - @test PooledArray{Union{Int,Missing}}([1, 2]) isa PooledArray{Union{Int,Missing}} + @test PooledArray{Union{Int, Missing}}([1, 2]) isa PooledArray{Union{Int, Missing}} @test eltype(PooledArray(rand(128)).refs) == UInt32 @test eltype(PooledArray(rand(300)).refs) == UInt32