|
| 1 | +@testitem "count_by with a scalar key" begin |
| 2 | + source = QueryOperators.query([(k="a", v=1), (k="b", v=2), (k="a", v=3)]) |
| 3 | + |
| 4 | + res = QueryOperators.count_by(source, i -> i.k, :(i -> i.k)) |
| 5 | + |
| 6 | + # A scalar key becomes a column called `key`, as summarize names it. |
| 7 | + @test collect(res) == [(key="a", count=2), (key="b", count=1)] |
| 8 | + @test eltype(res) == NamedTuple{(:key, :count),Tuple{String,Int}} |
| 9 | +end |
| 10 | + |
| 11 | +@testitem "count_by with a NamedTuple key splats the key columns" begin |
| 12 | + source = QueryOperators.query([(a=1, b=1), (a=1, b=1), (a=1, b=2)]) |
| 13 | + |
| 14 | + res = QueryOperators.count_by(source, i -> (a=i.a, b=i.b), :(i -> (a=i.a, b=i.b))) |
| 15 | + |
| 16 | + @test collect(res) == [(a=1, b=1, count=2), (a=1, b=2, count=1)] |
| 17 | +end |
| 18 | + |
| 19 | +@testitem "count_by preserves first-seen key order" begin |
| 20 | + source = QueryOperators.query([3, 1, 3, 2, 1]) |
| 21 | + |
| 22 | + res = QueryOperators.count_by(source, i -> i, :(i -> i)) |
| 23 | + |
| 24 | + @test collect(res) == [(key=3, count=2), (key=1, count=2), (key=2, count=1)] |
| 25 | +end |
| 26 | + |
| 27 | +@testitem "count_by on an empty source" begin |
| 28 | + source = QueryOperators.query(Int[]) |
| 29 | + |
| 30 | + @test collect(QueryOperators.count_by(source, i -> i, :(i -> i))) == NamedTuple{(:key, :count),Tuple{Int,Int}}[] |
| 31 | +end |
| 32 | + |
| 33 | +@testitem "count_by agrees with groupby plus summarize" begin |
| 34 | + using Statistics |
| 35 | + |
| 36 | + data = [(k=1, v=10), (k=2, v=20), (k=1, v=30), (k=3, v=40), (k=1, v=50)] |
| 37 | + |
| 38 | + by_count = collect(QueryOperators.count_by(QueryOperators.query(data), i -> i.k, :(i -> i.k))) |
| 39 | + |
| 40 | + grouped = QueryOperators.@groupby_simple(QueryOperators.query(data), i -> i.k) |
| 41 | + by_summarize = collect(QueryOperators.summarize( |
| 42 | + grouped, |
| 43 | + g -> Base.merge(QueryOperators._key_namedtuple(QueryOperators.key(g)), (count = length(g),)), |
| 44 | + :(g -> (count = length(g),)))) |
| 45 | + |
| 46 | + @test by_count == by_summarize |
| 47 | +end |
| 48 | + |
| 49 | +@testitem "aggregate_by" begin |
| 50 | + source = QueryOperators.query([(id="0", score=42), (id="1", score=5), (id="2", score=4), (id="1", score=10), (id="0", score=25)]) |
| 51 | + |
| 52 | + res = QueryOperators.aggregate_by( |
| 53 | + source, |
| 54 | + i -> i.id, :(i -> i.id), |
| 55 | + 0, |
| 56 | + (total, cur) -> total + cur.score) |
| 57 | + |
| 58 | + # The example from the .NET 9 release notes. |
| 59 | + @test collect(res) == [(key="0", value=67), (key="1", value=15), (key="2", value=4)] |
| 60 | + @test eltype(res) == NamedTuple{(:key, :value),Tuple{String,Int}} |
| 61 | +end |
| 62 | + |
| 63 | +@testitem "aggregate_by with a NamedTuple key" begin |
| 64 | + source = QueryOperators.query([(a=1, b=1, v=2), (a=1, b=1, v=3), (a=2, b=1, v=5)]) |
| 65 | + |
| 66 | + res = QueryOperators.aggregate_by( |
| 67 | + source, |
| 68 | + i -> (a=i.a, b=i.b), :(i -> (a=i.a, b=i.b)), |
| 69 | + 1, |
| 70 | + (acc, cur) -> acc * cur.v) |
| 71 | + |
| 72 | + @test collect(res) == [(a=1, b=1, value=6), (a=2, b=1, value=5)] |
| 73 | +end |
| 74 | + |
| 75 | +@testitem "aggregate_by on an empty source" begin |
| 76 | + source = QueryOperators.query(Int[]) |
| 77 | + |
| 78 | + res = QueryOperators.aggregate_by(source, i -> i, :(i -> i), 0, (acc, cur) -> acc + cur) |
| 79 | + |
| 80 | + @test collect(res) == NamedTuple{(:key, :value),Tuple{Int,Int}}[] |
| 81 | +end |
| 82 | + |
| 83 | +@testitem "aggregate_by never shares the seed between keys" begin |
| 84 | + source = QueryOperators.query([1, 1, 2, 2, 2]) |
| 85 | + |
| 86 | + res = QueryOperators.aggregate_by(source, i -> i, :(i -> i), Int[], (acc, cur) -> vcat(acc, cur)) |
| 87 | + |
| 88 | + @test collect(res) == [(key=1, value=[1, 1]), (key=2, value=[2, 2, 2])] |
| 89 | +end |
| 90 | + |
| 91 | +@testitem "chunk" begin |
| 92 | + source = QueryOperators.query([1, 2, 3, 4, 5]) |
| 93 | + |
| 94 | + res = QueryOperators.chunk(source, 2) |
| 95 | + |
| 96 | + # The final chunk is short when the source does not divide evenly. |
| 97 | + @test collect(res) == [[1, 2], [3, 4], [5]] |
| 98 | + @test eltype(res) == Vector{Int} |
| 99 | + @test Base.IteratorSize(typeof(res)) == Base.HasLength() |
| 100 | + @test length(res) == 3 |
| 101 | +end |
| 102 | + |
| 103 | +@testitem "chunk when the size divides evenly" begin |
| 104 | + source = QueryOperators.query([1, 2, 3, 4]) |
| 105 | + |
| 106 | + @test collect(QueryOperators.chunk(source, 2)) == [[1, 2], [3, 4]] |
| 107 | + @test length(QueryOperators.chunk(source, 2)) == 2 |
| 108 | +end |
| 109 | + |
| 110 | +@testitem "chunk with a size larger than the source" begin |
| 111 | + source = QueryOperators.query([1, 2]) |
| 112 | + |
| 113 | + @test collect(QueryOperators.chunk(source, 10)) == [[1, 2]] |
| 114 | +end |
| 115 | + |
| 116 | +@testitem "chunk on an empty source" begin |
| 117 | + source = QueryOperators.query(Int[]) |
| 118 | + |
| 119 | + @test collect(QueryOperators.chunk(source, 3)) == Vector{Int}[] |
| 120 | +end |
| 121 | + |
| 122 | +@testitem "chunk rejects a size below 1" begin |
| 123 | + source = QueryOperators.query([1, 2, 3]) |
| 124 | + |
| 125 | + @test_throws ErrorException QueryOperators.chunk(source, 0) |
| 126 | + @test_throws ErrorException QueryOperators.chunk(source, -1) |
| 127 | +end |
| 128 | + |
| 129 | +@testitem "chunk only walks the source as far as the batches consumed" begin |
| 130 | + import IteratorInterfaceExtensions |
| 131 | + |
| 132 | + mutable struct CountedChunkSource |
| 133 | + data::Vector{Int} |
| 134 | + pulled::Int |
| 135 | + end |
| 136 | + Base.eltype(::Type{CountedChunkSource}) = Int |
| 137 | + Base.IteratorSize(::Type{CountedChunkSource}) = Base.HasLength() |
| 138 | + Base.length(c::CountedChunkSource) = length(c.data) |
| 139 | + function Base.iterate(c::CountedChunkSource, i=1) |
| 140 | + i > length(c.data) && return nothing |
| 141 | + c.pulled += 1 |
| 142 | + return c.data[i], i + 1 |
| 143 | + end |
| 144 | + IteratorInterfaceExtensions.isiterable(::CountedChunkSource) = true |
| 145 | + IteratorInterfaceExtensions.getiterator(c::CountedChunkSource) = c |
| 146 | + |
| 147 | + src = CountedChunkSource(collect(1:100), 0) |
| 148 | + res = QueryOperators.chunk(QueryOperators.query(src), 3) |
| 149 | + |
| 150 | + it = iterate(res) |
| 151 | + |
| 152 | + @test it[1] == [1, 2, 3] |
| 153 | + @test src.pulled == 3 |
| 154 | +end |
| 155 | + |
| 156 | +@testitem "chunk works downstream of groupby" begin |
| 157 | + source = QueryOperators.query([(k=1, v=1), (k=2, v=2), (k=3, v=3)]) |
| 158 | + grouped = QueryOperators.@groupby_simple(source, i -> i.k) |
| 159 | + |
| 160 | + chunks = collect(QueryOperators.chunk(grouped, 2)) |
| 161 | + |
| 162 | + @test length(chunks) == 2 |
| 163 | + @test [QueryOperators.key(g) for g in chunks[1]] == [1, 2] |
| 164 | + @test [QueryOperators.key(g) for g in chunks[2]] == [3] |
| 165 | +end |
0 commit comments