Skip to content

Commit e4091bd

Browse files
davidanthoffclaude
andcommitted
Add the of_type and cast operators
Ports Enumerable.OfType and Cast. of_type keeps the elements that are instances of T and narrows the element type to T, which is useful for sources whose element type is Any or a Union and a no-op filter on a homogeneously typed table. .NET's Cast is a type assertion; the Julia counterpart is a conversion, so cast runs every element through convert and fails the way convert would on an element that cannot be represented as T. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent fac9fbb commit e4091bd

5 files changed

Lines changed: 152 additions & 0 deletions

File tree

src/QueryOperators.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ include("enumerable/enumerable_index.jl")
4141
include("enumerable/enumerable_append.jl")
4242
include("enumerable/enumerable_prepend.jl")
4343
include("enumerable/enumerable_zip.jl")
44+
include("enumerable/enumerable_oftype.jl")
4445
include("enumerable/enumerable_pivot.jl")
4546
include("enumerable/enumerable_summarize.jl")
4647
include("enumerable/enumerable_countby.jl")
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
struct EnumerableOfType{T,S} <: Enumerable
2+
source::S
3+
end
4+
5+
# Enumerable.OfType: keeps only the elements that are instances of T, and
6+
# narrows the element type to T. Useful for sources whose element type is Any
7+
# or a Union; a no-op filter on a homogeneously typed table.
8+
function of_type(source::Enumerable, ::Type{T}) where {T}
9+
return EnumerableOfType{T,typeof(source)}(source)
10+
end
11+
12+
Base.eltype(::Type{EnumerableOfType{T,S}}) where {T,S} = T
13+
14+
Base.iterate(iter::EnumerableOfType) = _of_type_next(iter, _NotStarted())
15+
16+
Base.iterate(iter::EnumerableOfType, state) = _of_type_next(iter, state)
17+
18+
function _of_type_next(iter::EnumerableOfType{T,S}, source_state) where {T,S}
19+
while true
20+
ret = _iterate_from(iter.source, source_state)
21+
ret === nothing && return nothing
22+
23+
element, source_state = ret
24+
element isa T && return element, source_state
25+
end
26+
end
27+
28+
struct EnumerableCast{T,S} <: Enumerable
29+
source::S
30+
end
31+
32+
# Enumerable.Cast. .NET's Cast is a type assertion; the Julia counterpart is a
33+
# conversion, so `cast` runs every element through `convert` and fails the same
34+
# way `convert` would on an element that cannot be represented as T.
35+
function cast(source::Enumerable, ::Type{T}) where {T}
36+
return EnumerableCast{T,typeof(source)}(source)
37+
end
38+
39+
Base.IteratorSize(::Type{EnumerableCast{T,S}}) where {T,S} = haslength(S)
40+
41+
Base.eltype(::Type{EnumerableCast{T,S}}) where {T,S} = T
42+
43+
Base.length(iter::EnumerableCast) = length(iter.source)
44+
45+
function Base.iterate(iter::EnumerableCast{T,S}) where {T,S}
46+
ret = iterate(iter.source)
47+
ret === nothing && return nothing
48+
return convert(T, ret[1]), ret[2]
49+
end
50+
51+
function Base.iterate(iter::EnumerableCast{T,S}, state) where {T,S}
52+
ret = iterate(iter.source, state)
53+
ret === nothing && return nothing
54+
return convert(T, ret[1]), ret[2]
55+
end

src/operators.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,3 +225,9 @@ function last end
225225
function single end
226226

227227
function element_at end
228+
229+
# Type filtering.
230+
231+
function of_type end
232+
233+
function cast end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ include("test_enumerable_keyed_aggregation.jl")
88
include("test_enumerable_partitioning.jl")
99
include("test_enumerable_combining.jl")
1010
include("test_enumerable_terminal.jl")
11+
include("test_enumerable_typefiltering.jl")
1112
include("test_enumerable_unique.jl")
1213
include("test_enumerable_summarize.jl")
1314
include("test_namedtupleutilities.jl")
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
@testitem "of_type" begin
2+
source = QueryOperators.query(Any[1, "a", 2, "b", 3.0])
3+
4+
ints = QueryOperators.of_type(source, Int)
5+
6+
@test collect(ints) == [1, 2]
7+
@test eltype(ints) == Int
8+
9+
strings = QueryOperators.of_type(source, String)
10+
@test collect(strings) == ["a", "b"]
11+
@test eltype(strings) == String
12+
end
13+
14+
@testitem "of_type keeps subtypes" begin
15+
source = QueryOperators.query(Any[1, 2.0, "a"])
16+
17+
numbers = QueryOperators.of_type(source, Number)
18+
19+
@test collect(numbers) == [1, 2.0]
20+
@test eltype(numbers) == Number
21+
end
22+
23+
@testitem "of_type over a Union element type" begin
24+
source = QueryOperators.query(Union{Int,String}[1, "a", 2])
25+
26+
@test collect(QueryOperators.of_type(source, Int)) == [1, 2]
27+
end
28+
29+
@testitem "of_type matching nothing or everything" begin
30+
source = QueryOperators.query(Any[1, 2, 3])
31+
32+
@test collect(QueryOperators.of_type(source, String)) == String[]
33+
@test collect(QueryOperators.of_type(source, Int)) == [1, 2, 3]
34+
end
35+
36+
@testitem "of_type on an empty source" begin
37+
source = QueryOperators.query(Any[])
38+
39+
@test collect(QueryOperators.of_type(source, Int)) == Int[]
40+
end
41+
42+
@testitem "cast" begin
43+
source = QueryOperators.query([1, 2, 3])
44+
45+
res = QueryOperators.cast(source, Float64)
46+
47+
@test collect(res) == [1.0, 2.0, 3.0]
48+
@test eltype(res) == Float64
49+
@test Base.IteratorSize(typeof(res)) == Base.HasLength()
50+
@test length(res) == 3
51+
end
52+
53+
@testitem "cast widens an Any source to a concrete type" begin
54+
source = QueryOperators.query(Any[1, 2, 3])
55+
56+
res = QueryOperators.cast(source, Int)
57+
58+
@test collect(res) == [1, 2, 3]
59+
@test eltype(res) == Int
60+
end
61+
62+
@testitem "cast fails on an element that cannot be converted" begin
63+
source = QueryOperators.query(Any[1, "a"])
64+
65+
res = QueryOperators.cast(source, Int)
66+
67+
@test_throws MethodError collect(res)
68+
end
69+
70+
@testitem "cast fails on a lossy conversion" begin
71+
source = QueryOperators.query([1.5])
72+
73+
@test_throws InexactError collect(QueryOperators.cast(source, Int))
74+
end
75+
76+
@testitem "cast on an empty source" begin
77+
source = QueryOperators.query(Int[])
78+
79+
@test collect(QueryOperators.cast(source, Float64)) == Float64[]
80+
end
81+
82+
@testitem "of_type and cast compose with other operators" begin
83+
source = QueryOperators.query(Any[1, "a", 2, "b", 3])
84+
85+
res = QueryOperators.cast(QueryOperators.of_type(source, Int), Float64)
86+
87+
@test collect(res) == [1.0, 2.0, 3.0]
88+
@test eltype(res) == Float64
89+
end

0 commit comments

Comments
 (0)