From 8a94412667bf81515b683882452ead9c89024c27 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Thu, 5 Mar 2026 22:09:39 -0800 Subject: [PATCH 1/4] Remove gather --- src/QueryOperators.jl | 1 - src/enumerable/enumerable_gather.jl | 81 ----------------------------- src/operators.jl | 6 --- test/runtests.jl | 5 -- 4 files changed, 93 deletions(-) delete mode 100644 src/enumerable/enumerable_gather.jl diff --git a/src/QueryOperators.jl b/src/QueryOperators.jl index 69aacfa..51b1fc5 100644 --- a/src/QueryOperators.jl +++ b/src/QueryOperators.jl @@ -16,7 +16,6 @@ include("enumerable/enumerable_join.jl") include("enumerable/enumerable_groupjoin.jl") include("enumerable/enumerable_orderby.jl") include("enumerable/enumerable_map.jl") -include("enumerable/enumerable_gather.jl") include("enumerable/enumerable_filter.jl") include("enumerable/enumerable_mapmany.jl") include("enumerable/enumerable_defaultifempty.jl") diff --git a/src/enumerable/enumerable_gather.jl b/src/enumerable/enumerable_gather.jl deleted file mode 100644 index f892f99..0000000 --- a/src/enumerable/enumerable_gather.jl +++ /dev/null @@ -1,81 +0,0 @@ -struct EnumerableGather{T,S,F,I,A} <: Enumerable - source::S - fields::F - indexFields::I - savedFields::A - key::Symbol - value::Symbol -end - -struct Not{T} - val::T -end - -function gather(source::Enumerable, args...; key::Symbol = :key, value::Symbol = :value) - fields = fieldnames(eltype(source)) - if length(args) > 0 - indexFields_vector = Vector{Symbol}(undef, 0) - firstArg = true - for arg in args - if typeof(arg) == Symbol - push!(indexFields_vector, arg) - else typeof(arg) == Not{Symbol} - if firstArg - indexFields_vector = [a for a in fields if a != arg.val] - else - indexFields_vector = [a for a in indexFields_vector if a != arg.val] - end - end - firstArg = false - end - indexFields = tuple(indexFields_vector...) - else - indexFields = fields - end - - savedFields = (n for n in fields if !(n in indexFields)) # fields that are not in `indexFields` - savedFieldsType = (fieldtype(eltype(source), savedField) for savedField in savedFields) - - valueTypes = (fieldtype(eltype(source), indexField) for indexField in indexFields) - valueType = reduce(promote_type, valueTypes) - - T = NamedTuple{(savedFields..., key, value), Tuple{savedFieldsType..., Symbol, valueType}} - return EnumerableGather{T, typeof(source), typeof(fields), typeof(indexFields), typeof(savedFields)}(source, - fields, indexFields, savedFields, key, value) -end - -function Base.iterate(iter::EnumerableGather{T, S, F, I, A}) where {T, S, F, I, A} - source_iterate = iterate(iter.source) - if source_iterate == nothing || length(iter.indexFields) == 0 - return nothing - end - key = iter.indexFields[1] - current_source_row = source_iterate[1] - value = current_source_row[key] - return (T((Base.map(n->current_source_row[n], iter.savedFields)..., key, value)), - (current_source_row=current_source_row, source_state=source_iterate[2], current_index_field_index=1)) -end - -function Base.iterate(iter::EnumerableGather{T, S, F, I, A}, state) where {T, S, F, I, A} - current_index_field_index = state.current_index_field_index + 1 - if current_index_field_index > length(iter.indexFields) - source_iterate = iterate(iter.source, state.source_state) - if source_iterate == nothing || length(iter.indexFields) == 0 - return nothing - end - current_index_field_index = 1 - source_state = source_iterate[2] - current_source_row = source_iterate[1] - else - source_state = state.source_state - current_source_row = state.current_source_row - end - key = iter.indexFields[current_index_field_index] - value = current_source_row[key] - return (T((Base.map(n->current_source_row[n], iter.savedFields)..., key, value)), - (current_source_row=current_source_row, source_state=source_state, current_index_field_index=current_index_field_index)) -end - -function Base.eltype(iter::EnumerableGather{T, S, F, I, A}) where {T, S, F, I, A} - return T -end diff --git a/src/operators.jl b/src/operators.jl index 29fd4f5..cfd2d91 100644 --- a/src/operators.jl +++ b/src/operators.jl @@ -26,12 +26,6 @@ macro filter(source, f) :(QueryOperators.filter($(esc(source)), $(esc(f)), $(esc(q)))) end -function gather end - -macro gather(source, withIndex = False) - :(groupby($(esc(source)), $(esc(withIndex)))) -end - function groupby end macro groupby(source,elementSelector,resultSelector) diff --git a/test/runtests.jl b/test/runtests.jl index da271c0..95b89a9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -147,11 +147,6 @@ a │ b │ c @test sprint((stream,data)->show(stream, "text/html", data), ntups) == "
abc
123
456
" -gather_result1 = QueryOperators.gather(QueryOperators.query([(US=1, EU=1, CN=1), (US=2, EU=2, CN=2), (US=3, EU=3, CN=3)])) -@test sprint(show, gather_result1) == """9x2 query result\nkey │ value\n────┼──────\n:US │ 1 \n:EU │ 1 \n:CN │ 1 \n:US │ 2 \n:EU │ 2 \n:CN │ 2 \n:US │ 3 \n:EU │ 3 \n:CN │ 3 """ -gather_result2 = QueryOperators.gather(QueryOperators.query([(Year=2017, US=1, EU=1, CN=1), (Year=2018, US=2, EU=2, CN=2), (Year=2019, US=3, EU=3, CN=3)]), :US, :EU, :CN) -@test sprint(show, gather_result2) == "9x3 query result\nYear │ key │ value\n─────┼─────┼──────\n2017 │ :US │ 1 \n2017 │ :EU │ 1 \n2017 │ :CN │ 1 \n2018 │ :US │ 2 \n2018 │ :EU │ 2 \n2018 │ :CN │ 2 \n2019 │ :US │ 3 \n2019 │ :EU │ 3 \n2019 │ :CN │ 3 " - @test sprint((stream,data)->show(stream, "application/vnd.dataresource+json", data), ntups) == "{\"schema\":{\"fields\":[{\"name\":\"a\",\"type\":\"integer\"},{\"name\":\"b\",\"type\":\"integer\"},{\"name\":\"c\",\"type\":\"integer\"}]},\"data\":[{\"a\":1,\"b\":2,\"c\":3},{\"a\":4,\"b\":5,\"c\":6}]}" From f59819ee853c9972a43a73280cc9df3349cf6840 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Fri, 6 Mar 2026 08:35:27 -0800 Subject: [PATCH 2/4] Add pivot_longer and pivot_wider --- src/QueryOperators.jl | 1 + src/enumerable/enumerable_pivot.jl | 243 +++++++++++++++++++++++++++++ src/operators.jl | 4 + test/runtests.jl | 2 + test/test_pivot.jl | 126 +++++++++++++++ 5 files changed, 376 insertions(+) create mode 100644 src/enumerable/enumerable_pivot.jl create mode 100644 test/test_pivot.jl diff --git a/src/QueryOperators.jl b/src/QueryOperators.jl index 51b1fc5..6a2a3d8 100644 --- a/src/QueryOperators.jl +++ b/src/QueryOperators.jl @@ -23,6 +23,7 @@ include("enumerable/enumerable_count.jl") include("enumerable/enumerable_take.jl") include("enumerable/enumerable_drop.jl") include("enumerable/enumerable_unique.jl") +include("enumerable/enumerable_pivot.jl") include("enumerable/show.jl") include("source_iterable.jl") diff --git a/src/enumerable/enumerable_pivot.jl b/src/enumerable/enumerable_pivot.jl new file mode 100644 index 0000000..c38e2eb --- /dev/null +++ b/src/enumerable/enumerable_pivot.jl @@ -0,0 +1,243 @@ +# ===== pivot_longer ===== +# Transforms wide data to long format by pivoting named columns into key/value rows. + +struct EnumerablePivotLonger{T, S, COLS, ID_COLS} <: Enumerable + source::S +end + +Base.IteratorSize(::Type{EnumerablePivotLonger{T,S,COLS,ID_COLS}}) where {T,S,COLS,ID_COLS} = haslength(S) + +Base.eltype(::Type{EnumerablePivotLonger{T,S,COLS,ID_COLS}}) where {T,S,COLS,ID_COLS} = T + +Base.length(iter::EnumerablePivotLonger{T,S,COLS,ID_COLS}) where {T,S,COLS,ID_COLS} = + length(iter.source) * length(COLS) + +function pivot_longer(source::Enumerable, cols::NTuple{N,Symbol}; + names_to::Symbol=:variable, values_to::Symbol=:value) where N + N == 0 && error("pivot_longer requires at least one column to pivot") + TS = eltype(source) + all_fields = fieldnames(TS) + id_cols = tuple((f for f in all_fields if f ∉ cols)...) + + value_type = reduce(promote_type, (fieldtype(TS, c) for c in cols)) + + out_names = (id_cols..., names_to, values_to) + out_types = Tuple{(fieldtype(TS, f) for f in id_cols)..., Symbol, value_type} + T = NamedTuple{out_names, out_types} + + return EnumerablePivotLonger{T, typeof(source), cols, id_cols}(source) +end + +# Type-stable row construction: generates a static if/elseif chain over COLS at compile time. +@generated function _pivot_longer_row(row::NamedTuple, ::Val{ID_COLS}, col_idx::Int, + ::Val{COLS}, ::Type{T}) where {ID_COLS, COLS, T} + id_exprs = [:(getfield(row, $(QuoteNode(f)))) for f in ID_COLS] + value_type = fieldtype(T, length(ID_COLS) + 2) # fields: id..., names_to, values_to + + branches = Expr[] + for (i, col) in enumerate(COLS) + push!(branches, quote + if col_idx == $i + return T(($(id_exprs...), $(QuoteNode(col)), + convert($value_type, getfield(row, $(QuoteNode(col)))))) + end + end) + end + + return quote + $(branches...) + error("pivot_longer: col_idx out of range") + end +end + +function Base.iterate(iter::EnumerablePivotLonger{T,S,COLS,ID_COLS}) where {T,S,COLS,ID_COLS} + source_ret = iterate(iter.source) + source_ret === nothing && return nothing + row, source_state = source_ret + out = _pivot_longer_row(row, Val(ID_COLS), 1, Val(COLS), T) + return out, (row=row, source_state=source_state, col_idx=1) +end + +function Base.iterate(iter::EnumerablePivotLonger{T,S,COLS,ID_COLS}, state) where {T,S,COLS,ID_COLS} + next_idx = state.col_idx + 1 + if next_idx <= length(COLS) + out = _pivot_longer_row(state.row, Val(ID_COLS), next_idx, Val(COLS), T) + return out, (row=state.row, source_state=state.source_state, col_idx=next_idx) + else + source_ret = iterate(iter.source, state.source_state) + source_ret === nothing && return nothing + row, source_state = source_ret + out = _pivot_longer_row(row, Val(ID_COLS), 1, Val(COLS), T) + return out, (row=row, source_state=source_state, col_idx=1) + end +end + +# ===== pivot_wider ===== +# Transforms long data to wide format by spreading a key column into multiple value columns. + +struct EnumerablePivotWider{T} <: Enumerable + results::Vector{T} +end + +Base.IteratorSize(::Type{EnumerablePivotWider{T}}) where T = Base.HasLength() + +Base.eltype(::Type{EnumerablePivotWider{T}}) where T = T + +Base.length(iter::EnumerablePivotWider{T}) where T = length(iter.results) + +function pivot_wider(source::Enumerable, names_from::Symbol, values_from::Symbol; + id_cols=nothing) + TS = eltype(source) + all_fields = fieldnames(TS) + + id_col_names = if id_cols === nothing + tuple((f for f in all_fields if f != names_from && f != values_from)...) + else + tuple(id_cols...) + end + + val_type = fieldtype(TS, values_from) + out_val_type = DataValues.DataValue{val_type} + + all_rows = collect(source) + + # Collect unique name values in order of first appearance + seen_names = OrderedDict{Symbol, Nothing}() + for row in all_rows + seen_names[Symbol(getfield(row, names_from))] = nothing + end + new_col_names = tuple(keys(seen_names)...) + + out_names = (id_col_names..., new_col_names...) + out_types = Tuple{(fieldtype(TS, f) for f in id_col_names)..., + (out_val_type for _ in new_col_names)...} + T = NamedTuple{out_names, out_types} + + # Group rows by their id-column values + id_to_values = OrderedDict{Any, Dict{Symbol, val_type}}() + for row in all_rows + id_key = ntuple(i -> getfield(row, id_col_names[i]), length(id_col_names)) + name_sym = Symbol(getfield(row, names_from)) + value = getfield(row, values_from) + if !haskey(id_to_values, id_key) + id_to_values[id_key] = Dict{Symbol, val_type}() + end + id_to_values[id_key][name_sym] = value + end + + na = out_val_type() + results = Vector{T}(undef, length(id_to_values)) + for (i, (id_key, vals_dict)) in enumerate(id_to_values) + new_vals = ntuple( + j -> haskey(vals_dict, new_col_names[j]) ? + out_val_type(vals_dict[new_col_names[j]]) : na, + length(new_col_names)) + results[i] = T((id_key..., new_vals...)) + end + + return EnumerablePivotWider{T}(results) +end + +# ===== Column selector resolution ===== +# Resolves a tuple of column names from a NamedTuple type according to a list of +# selector instructions (encoded in a Val type parameter for compile-time evaluation). +# +# Each instruction is a 2-tuple (op, arg): +# (:include_name, sym) — include field by name +# (:exclude_name, sym) — exclude field by name +# (:include_position, idx) — include field at 1-based position +# (:exclude_position, idx) — exclude field at 1-based position +# (:include_startswith, prefix_sym) — include fields whose name starts with prefix +# (:exclude_startswith, prefix_sym) — exclude fields whose name starts with prefix +# (:include_endswith, suffix_sym) — include fields whose name ends with suffix +# (:exclude_endswith, suffix_sym) — exclude fields whose name ends with suffix +# (:include_occursin, sub_sym) — include fields whose name contains sub +# (:exclude_occursin, sub_sym) — exclude fields whose name contains sub +# (:include_range, (from, to)) — include field names from :from to :to (inclusive) +# (:include_range_idx, (a, b)) — include fields at 1-based positions a through b +# (:include_all, :_) — include all remaining fields +# +# If all instructions are "exclude" ops, the starting set is ALL field names; otherwise +# the starting set is empty and "include" instructions accumulate into it. +@generated function _resolve_pivot_cols(::Type{NT}, ::Val{instructions}) where {NT<:NamedTuple, instructions} + all_names = collect(fieldnames(NT)) + + include_ops = (:include_name, :include_position, :include_startswith, + :include_endswith, :include_occursin, :include_all, + :include_range, :include_range_idx) + has_positive = any(inst[1] ∈ include_ops for inst in instructions) + + result = has_positive ? Symbol[] : copy(all_names) + + for inst in instructions + op = inst[1] + arg = inst[2] + + if op == :include_all + for n in all_names + n ∉ result && push!(result, n) + end + elseif op == :include_name + arg ∉ result && push!(result, arg) + elseif op == :exclude_name + filter!(!=( arg), result) + elseif op == :include_position + n = all_names[arg] + n ∉ result && push!(result, n) + elseif op == :exclude_position + n = all_names[arg] + filter!(!=(n), result) + elseif op == :include_startswith + for n in all_names + if Base.startswith(String(n), String(arg)) && n ∉ result + push!(result, n) + end + end + elseif op == :exclude_startswith + filter!(n -> !Base.startswith(String(n), String(arg)), result) + elseif op == :include_endswith + for n in all_names + if Base.endswith(String(n), String(arg)) && n ∉ result + push!(result, n) + end + end + elseif op == :exclude_endswith + filter!(n -> !Base.endswith(String(n), String(arg)), result) + elseif op == :include_occursin + for n in all_names + if Base.occursin(String(arg), String(n)) && n ∉ result + push!(result, n) + end + end + elseif op == :exclude_occursin + filter!(n -> !Base.occursin(String(arg), String(n)), result) + elseif op == :include_range + from_sym, to_sym = arg + in_range = false + for n in all_names + n == from_sym && (in_range = true) + in_range && n ∉ result && push!(result, n) + n == to_sym && (in_range = false; break) + end + elseif op == :include_range_idx + from_idx, to_idx = arg + for i in from_idx:to_idx + n = all_names[i] + n ∉ result && push!(result, n) + end + end + end + + names = tuple(result...) + return :($names) +end + +function Base.iterate(iter::EnumerablePivotWider{T}) where T + isempty(iter.results) && return nothing + return iter.results[1], 2 +end + +function Base.iterate(iter::EnumerablePivotWider{T}, state) where T + state > length(iter.results) && return nothing + return iter.results[state], state + 1 +end diff --git a/src/operators.jl b/src/operators.jl index cfd2d91..9dc7eb2 100644 --- a/src/operators.jl +++ b/src/operators.jl @@ -123,3 +123,7 @@ macro unique(source, f) q = Expr(:quote, f) :(unique($(esc(source)), $(esc(f)), $(esc(q)))) end + +function pivot_longer end + +function pivot_wider end diff --git a/test/runtests.jl b/test/runtests.jl index 95b89a9..2e504a0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -156,4 +156,6 @@ include("test_enumerable_unique.jl") include("test_namedtupleutilities.jl") +include("test_pivot.jl") + end diff --git a/test/test_pivot.jl b/test/test_pivot.jl new file mode 100644 index 0000000..950f515 --- /dev/null +++ b/test/test_pivot.jl @@ -0,0 +1,126 @@ +@testset "pivot_longer" begin + + # Basic: pivot all columns + data = QueryOperators.query([(US=1, EU=2, CN=3), (US=4, EU=5, CN=6)]) + result = QueryOperators.pivot_longer(data, (:US, :EU, :CN)) |> collect + + @test length(result) == 6 + @test eltype(result) == NamedTuple{(:variable, :value), Tuple{Symbol, Int64}} + @test result[1] == (variable=:US, value=1) + @test result[2] == (variable=:EU, value=2) + @test result[3] == (variable=:CN, value=3) + @test result[4] == (variable=:US, value=4) + @test result[5] == (variable=:EU, value=5) + @test result[6] == (variable=:CN, value=6) + + # With id columns retained + data2 = QueryOperators.query([(year=2017, US=1, EU=2), (year=2018, US=3, EU=4)]) + result2 = QueryOperators.pivot_longer(data2, (:US, :EU)) |> collect + + @test length(result2) == 4 + @test eltype(result2) == NamedTuple{(:year, :variable, :value), Tuple{Int64, Symbol, Int64}} + @test result2[1] == (year=2017, variable=:US, value=1) + @test result2[2] == (year=2017, variable=:EU, value=2) + @test result2[3] == (year=2018, variable=:US, value=3) + @test result2[4] == (year=2018, variable=:EU, value=4) + + # Custom names_to and values_to + result3 = QueryOperators.pivot_longer(data2, (:US, :EU); names_to=:country, values_to=:sales) |> collect + + @test eltype(result3) == NamedTuple{(:year, :country, :sales), Tuple{Int64, Symbol, Int64}} + @test result3[1] == (year=2017, country=:US, sales=1) + + # Type promotion: mixing Int and Float + data3 = QueryOperators.query([(id=1, a=1, b=2.0)]) + result4 = QueryOperators.pivot_longer(data3, (:a, :b)) |> collect + + @test eltype(result4) == NamedTuple{(:id, :variable, :value), Tuple{Int64, Symbol, Float64}} + @test result4[1] == (id=1, variable=:a, value=1.0) + @test result4[2] == (id=1, variable=:b, value=2.0) + + # Type stability + @test Base.return_types(iterate, (QueryOperators.EnumerablePivotLonger,)) |> only <: + Union{Nothing, Tuple} + +end + +@testset "_resolve_pivot_cols" begin + NT = NamedTuple{(:year, :wk1, :wk2, :total), Tuple{Int,Int,Int,Int}} + + # Include by name + @test QueryOperators._resolve_pivot_cols(NT, Val(((:include_name, :wk1), (:include_name, :wk2)))) == (:wk1, :wk2) + + # Include by startswith + @test QueryOperators._resolve_pivot_cols(NT, Val(((:include_startswith, :wk),))) == (:wk1, :wk2) + + # Include by endswith + @test QueryOperators._resolve_pivot_cols(NT, Val(((:include_endswith, Symbol("1")),))) == (:wk1,) + + # Include by occursin + @test QueryOperators._resolve_pivot_cols(NT, Val(((:include_occursin, :wk),))) == (:wk1, :wk2) + + # Exclude by name from all (only-negative → starts from all) + @test QueryOperators._resolve_pivot_cols(NT, Val(((:exclude_name, :year), (:exclude_name, :total)))) == (:wk1, :wk2) + + # Exclude by startswith from all + @test QueryOperators._resolve_pivot_cols(NT, Val(((:exclude_startswith, :wk),))) == (:year, :total) + + # Mix: include startswith then exclude one + @test QueryOperators._resolve_pivot_cols(NT, Val(((:include_startswith, :wk), (:exclude_name, :wk2)))) == (:wk1,) + + # Include by position + @test QueryOperators._resolve_pivot_cols(NT, Val(((:include_position, 2), (:include_position, 3)))) == (:wk1, :wk2) + + # Include range by index + @test QueryOperators._resolve_pivot_cols(NT, Val(((:include_range_idx, (2, 3)),))) == (:wk1, :wk2) + + # Include range by name + @test QueryOperators._resolve_pivot_cols(NT, Val(((:include_range, (:wk1, :wk2)),))) == (:wk1, :wk2) + + # include_all adds everything not yet in set + @test QueryOperators._resolve_pivot_cols(NT, Val(((:include_name, :year), (:include_all, :_), (:exclude_name, :total)))) == (:year, :wk1, :wk2) +end + +@testset "pivot_wider" begin + + # Basic: long to wide + data = QueryOperators.query([ + (year=2017, country=:US, value=1), + (year=2017, country=:EU, value=2), + (year=2018, country=:US, value=3), + (year=2018, country=:EU, value=4), + ]) + result = QueryOperators.pivot_wider(data, :country, :value) |> collect + + @test length(result) == 2 + T = eltype(result) + @test fieldnames(T) == (:year, :US, :EU) + @test fieldtype(T, :US) == DataValues.DataValue{Int64} + @test result[1].year == 2017 + @test result[1].US == DataValues.DataValue(1) + @test result[1].EU == DataValues.DataValue(2) + @test result[2].year == 2018 + @test result[2].US == DataValues.DataValue(3) + @test result[2].EU == DataValues.DataValue(4) + + # Absent combinations become NA DataValues + data2 = QueryOperators.query([ + (year=2017, country=:US, value=1), + (year=2017, country=:EU, value=2), + (year=2018, country=:US, value=3), + # year=2018, country=:EU is absent + ]) + result2 = QueryOperators.pivot_wider(data2, :country, :value) |> collect + + @test length(result2) == 2 + @test result2[1].year == 2017 + @test result2[1].US == DataValues.DataValue(1) + @test result2[1].EU == DataValues.DataValue(2) + @test result2[2].US == DataValues.DataValue(3) + @test DataValues.isna(result2[2].EU) + + # Length is known + wide = QueryOperators.pivot_wider(data, :country, :value) + @test length(wide) == 2 + +end From 2d2bf0f0612497d13c24606f28b03cf08af85159 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Fri, 6 Mar 2026 11:12:40 -0800 Subject: [PATCH 3/4] Fix doctests --- docs/make.jl | 6 +++++- src/NamedTupleUtilities.jl | 10 +++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 2884aec..6a41c0b 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,5 +1,8 @@ using Documenter, QueryOperators +# Configure DocMeta to automatically import QueryOperators for all doctests +DocMeta.setdocmeta!(QueryOperators, :DocTestSetup, :(using QueryOperators); recursive=true) + makedocs( modules = [QueryOperators], sitename = "QueryOperators.jl", @@ -8,7 +11,8 @@ makedocs( ), pages = [ "Introduction" => "index.md" - ] + ], + warnonly = [:missing_docs] ) deploydocs( diff --git a/src/NamedTupleUtilities.jl b/src/NamedTupleUtilities.jl index 1ebbb70..3027914 100644 --- a/src/NamedTupleUtilities.jl +++ b/src/NamedTupleUtilities.jl @@ -185,11 +185,11 @@ end oftype(a::NamedTuple, b::DataType) Returns a NamedTuple which retains the fields whose elements have type `b`. ```jldoctest -julia> QueryOperators.NamedTupleUtilities.oftype((a = [4,5,6], b = [3.,2.,1.], c = ["He","llo","World!"]), Val(Int64)) -(a = [4, 5, 6],) -julia> QueryOperators.NamedTupleUtilities.oftype((a = [4,5,6], b = [3.,2.,1.], c = ["He","llo","World!"]), Val(Number)) -(a = [4, 5, 6], b = [3., 2., 1.]) -julia> QueryOperators.NamedTupleUtilities.oftype((a = [4,5,6], b = [3.,2.,1.], c = ["He","llo","World!"]), Val(Float32)) +julia> QueryOperators.NamedTupleUtilities.oftype((a = 4, b = 3., c = "He"), Val(Int64)) +(a = 4,) +julia> QueryOperators.NamedTupleUtilities.oftype((a = 4, b = 3., c = "He"), Val(Number)) +(a = 4, b = 3.0) +julia> QueryOperators.NamedTupleUtilities.oftype((a = 4, b = 3., c = "He"), Val(Float32)) NamedTuple() ``` """ From f3e44368f79a729e8c22ea8c412954df358ff552 Mon Sep 17 00:00:00 2001 From: David Anthoff Date: Fri, 6 Mar 2026 11:33:55 -0800 Subject: [PATCH 4/4] Fix tests on x86 --- test/test_pivot.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_pivot.jl b/test/test_pivot.jl index 7161944..b9bea47 100644 --- a/test/test_pivot.jl +++ b/test/test_pivot.jl @@ -6,7 +6,7 @@ result = QueryOperators.pivot_longer(data, (:US, :EU, :CN)) |> collect @test length(result) == 6 - @test eltype(result) == NamedTuple{(:variable, :value), Tuple{Symbol, Int64}} + @test eltype(result) == NamedTuple{(:variable, :value), Tuple{Symbol, Int}} @test result[1] == (variable=:US, value=1) @test result[2] == (variable=:EU, value=2) @test result[3] == (variable=:CN, value=3) @@ -19,7 +19,7 @@ result2 = QueryOperators.pivot_longer(data2, (:US, :EU)) |> collect @test length(result2) == 4 - @test eltype(result2) == NamedTuple{(:year, :variable, :value), Tuple{Int64, Symbol, Int64}} + @test eltype(result2) == NamedTuple{(:year, :variable, :value), Tuple{Int, Symbol, Int}} @test result2[1] == (year=2017, variable=:US, value=1) @test result2[2] == (year=2017, variable=:EU, value=2) @test result2[3] == (year=2018, variable=:US, value=3) @@ -28,14 +28,14 @@ # Custom names_to and values_to result3 = QueryOperators.pivot_longer(data2, (:US, :EU); names_to=:country, values_to=:sales) |> collect - @test eltype(result3) == NamedTuple{(:year, :country, :sales), Tuple{Int64, Symbol, Int64}} + @test eltype(result3) == NamedTuple{(:year, :country, :sales), Tuple{Int, Symbol, Int}} @test result3[1] == (year=2017, country=:US, sales=1) # Type promotion: mixing Int and Float data3 = QueryOperators.query([(id=1, a=1, b=2.0)]) result4 = QueryOperators.pivot_longer(data3, (:a, :b)) |> collect - @test eltype(result4) == NamedTuple{(:id, :variable, :value), Tuple{Int64, Symbol, Float64}} + @test eltype(result4) == NamedTuple{(:id, :variable, :value), Tuple{Int, Symbol, Float64}} @test result4[1] == (id=1, variable=:a, value=1.0) @test result4[2] == (id=1, variable=:b, value=2.0) @@ -97,7 +97,7 @@ end @test length(result) == 2 T = eltype(result) @test fieldnames(T) == (:year, :US, :EU) - @test fieldtype(T, :US) == DataValue{Int64} + @test fieldtype(T, :US) == DataValue{Int} @test result[1].year == 2017 @test result[1].US == DataValue(1) @test result[1].EU == DataValue(2)