Skip to content

Commit 9d357fc

Browse files
timholygoretkin
andauthored
setindex!: convert to eltype (fixes #216) (#227)
This introduces the internal utility `maybe_convert_elt` used to convert values to the element type of the StructArray before assignment. This fixes errors that are caused by assigning a value that does not have the same fields as the struct but which can be converted to such a type (e.g., `T<:Real` -> `Complex{T}`). Rather than calling `convert` directly, `maybe_convert_elt` can be specialized for particular types, like LazyRow, which should not be converted. Fixes #131 Fixes #216 Closes #184 Co-authored-by: Gustavo Goretkin <[email protected]>
1 parent cd95f19 commit 9d357fc

File tree

5 files changed

+88
-12
lines changed

5 files changed

+88
-12
lines changed

src/lazy.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ iscompatible(::Type{<:LazyRow{R}}, ::Type{S}) where {R, S<:StructArray} = iscomp
5757

5858
(s::ArrayInitializer)(::Type{<:LazyRow{T}}, d) where {T} = buildfromschema(typ -> s(typ, d), T)
5959

60+
maybe_convert_elt(::Type{T}, vals::LazyRow) where T = vals
61+
6062
"""
6163
LazyRows(s::StructArray)
6264

src/structarray.jl

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -351,34 +351,40 @@ end
351351
StructArray{T}(map(v -> @inbounds(view(v, I...)), components(s)))
352352
end
353353

354-
Base.@propagate_inbounds function Base.setindex!(s::StructArray{<:Any, <:Any, <:Any, CartesianIndex{N}}, vals, I::Vararg{Int, N}) where {N}
354+
Base.@propagate_inbounds function Base.setindex!(s::StructArray{T, <:Any, <:Any, CartesianIndex{N}}, vals, I::Vararg{Int, N}) where {T,N}
355355
@boundscheck checkbounds(s, I...)
356-
foreachfield((col, val) -> (@inbounds col[I...] = val), s, vals)
357-
s
356+
valsT = maybe_convert_elt(T, vals)
357+
foreachfield((col, val) -> (@inbounds col[I...] = val), s, valsT)
358+
return s
358359
end
359360

360-
Base.@propagate_inbounds function Base.setindex!(s::StructArray{<:Any, <:Any, <:Any, Int}, vals, I::Int)
361+
Base.@propagate_inbounds function Base.setindex!(s::StructArray{T, <:Any, <:Any, Int}, vals, I::Int) where T
361362
@boundscheck checkbounds(s, I)
362-
foreachfield((col, val) -> (@inbounds col[I] = val), s, vals)
363-
s
363+
valsT = maybe_convert_elt(T, vals)
364+
foreachfield((col, val) -> (@inbounds col[I] = val), s, valsT)
365+
return s
364366
end
365367

366368
for f in (:push!, :pushfirst!)
367-
@eval function Base.$f(s::StructVector, vals)
368-
foreachfield($f, s, vals)
369+
@eval function Base.$f(s::StructVector{T}, vals) where T
370+
valsT = maybe_convert_elt(T, vals)
371+
foreachfield($f, s, valsT)
369372
return s
370373
end
371374
end
372375

373376
for f in (:append!, :prepend!)
374-
@eval function Base.$f(s::StructVector, vals::StructVector)
377+
@eval function Base.$f(s::StructVector{T}, vals::StructVector{T}) where T
378+
# If these aren't the same type, there's no guarantee that x.a "means" the same thing as y.a,
379+
# even when all the field names match.
375380
foreachfield($f, s, vals)
376381
return s
377382
end
378383
end
379384

380-
function Base.insert!(s::StructVector, i::Integer, vals)
381-
foreachfield((v, val) -> insert!(v, i, val), s, vals)
385+
function Base.insert!(s::StructVector{T}, i::Integer, vals) where T
386+
valsT = maybe_convert_elt(T, vals)
387+
foreachfield((v, val) -> insert!(v, i, val), s, valsT)
382388
return s
383389
end
384390

src/tables.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ function try_compatible_columns(rows::R, s::StructArray) where {R}
2020
_schema(NT) == Tables.schema(rows) || return nothing
2121
return Tables.columntable(rows)
2222
end
23+
try_compatible_columns(rows::StructArray{T}, s::StructArray{T}) where {T} = Tables.columntable(rows)
24+
try_compatible_columns(rows::StructArray{R}, s::StructArray{S}) where {R,S} = nothing
2325

2426
for (f, g) in zip((:append!, :prepend!), (:push!, :pushfirst!))
2527
@eval function Base.$f(s::StructVector, rows)

src/utils.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,3 +186,14 @@ possible internal constructors. `T` should be a concrete type.
186186
construct = Expr(:new, :T, vars...)
187187
Expr(:block, assign..., construct)
188188
end
189+
190+
# Specialize this for types like LazyRow that shouldn't convert
191+
"""
192+
StructArrays.maybe_convert_elt(T, x)
193+
194+
Element conversion before assignment in a StructArray.
195+
By default, this calls `convert(T, x)`; however, you can specialize it for other types.
196+
"""
197+
maybe_convert_elt(::Type{T}, vals) where T = convert(T, vals)
198+
maybe_convert_elt(::Type{T}, vals::Tuple) where T = T <: Tuple ? convert(T, vals) : vals # assignment of fields by position
199+
maybe_convert_elt(::Type{T}, vals::NamedTuple) where T = T<:NamedTuple ? convert(T, vals) : vals # assignment of fields by name

test/runtests.jl

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,72 @@ using Adapt: adapt, Adapt
99
using Test
1010

1111
using Documenter: doctest
12-
if Base.VERSION >= v"1.6"
12+
if Base.VERSION >= v"1.6" && Int === Int64
1313
doctest(StructArrays)
1414
end
1515

16+
# Most types should not be viewed as equivalent merely
17+
# because they have the same field names. (Exception:
18+
# NamedTuples are distinguished only by field names, so they
19+
# are treated as equivalent to any struct with the same
20+
# field names.) To test proper behavior, define two types
21+
# that are "structurally" equivalent...
22+
struct Meters
23+
x::Float64
24+
end
25+
struct Millimeters
26+
x::Float64
27+
end
28+
# ...but not naively transferrable
29+
Base.convert(::Type{Meters}, x::Millimeters) = Meters(x.x/1000)
30+
Base.convert(::Type{Millimeters}, x::Meters) = Millimeters(x.x*1000)
31+
32+
1633
@testset "index" begin
1734
a, b = [1 2; 3 4], [4 5; 6 7]
1835
t = StructArray((a = a, b = b))
1936
@test (@inferred t[2,2]) == (a = 4, b = 7)
2037
@test (@inferred t[2,1:2]) == StructArray((a = [3, 4], b = [6, 7]))
2138
@test_throws BoundsError t[3,3]
2239
@test (@inferred view(t, 2, 1:2)) == StructArray((a = view(a, 2, 1:2), b = view(b, 2, 1:2)))
40+
41+
# Element type conversion (issue #216)
42+
x = StructArray{Complex{Int}}((a, b))
43+
x[1,1] = 10
44+
x[2,2] = 20im
45+
@test x[1,1] === 10 + 0im
46+
@test x[2,2] === 0 + 20im
47+
48+
# Test that explicit `setindex!` returns the entire array
49+
# (Julia's parser ensures that chained assignment returns the value)
50+
@test setindex!(x, 22, 3) === x
51+
end
52+
53+
@testset "eltype conversion" begin
54+
v = StructArray{Complex{Int}}(([1,2,3], [4,5,6]))
55+
@test append!(v, [7, 8]) == [1+4im, 2+5im, 3+6im, 7+0im, 8+0im]
56+
push!(v, (im=12, re=11)) # NamedTuples support field assignment by name
57+
@test v[end] === 11 + 12im
58+
v[end] = (re=9, im=10)
59+
@test v[end] === 9 + 10im
60+
61+
# For some eltypes, the structarray is "nameless" and we can use regular Tuples
62+
v = StructArray([SVector(true, false), SVector(false, false)])
63+
v[end] = (true, true)
64+
@test v[end] === SVector(true, true)
65+
push!(v, (false, false))
66+
@test v[end] === SVector(false, false)
67+
68+
z = StructArray{Meters}(undef, 0)
69+
push!(z, Millimeters(1100))
70+
@test length(z) == 1
71+
@test z[1] === Meters(1.1)
72+
append!(z, [Millimeters(1200)])
73+
@test z[2] === Meters(1.2)
74+
append!(z, StructArray{Millimeters}(([1500.0],)))
75+
@test z[3] === Meters(1.5)
76+
insert!(z, 3, Millimeters(2000))
77+
@test z[3] === Meters(2.0)
2378
end
2479

2580
@testset "components" begin

0 commit comments

Comments
 (0)