Skip to content

Commit fac9fbb

Browse files
davidanthoffclaude
andcommitted
Add the terminal operators
Ports Enumerable.MinBy and MaxBy (.NET 6), Any, All, Contains, SequenceEqual, Aggregate, First, Last, Single and ElementAt. These return a value rather than another Enumerable, so a query ending in one is executed immediately — `count` was already the sole member of that family here. min_by and max_by return the element, not the key, and keep the first element on ties as .NET does. contains and sequence_equal compare with isequal, so DataValue nulls match each other, consistent with unique and the set operators. element_at is 1-based, matching the new `index` operator and the rest of Julia rather than .NET's 0-based ElementAt. summarize remains the idiomatic way to aggregate a table; aggregate is here for LINQ parity. Defining `any` in this module captured the Base.any call in enumerable_pivot.jl, which is now qualified. A test covers that regression. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 4389dfa commit fac9fbb

6 files changed

Lines changed: 414 additions & 1 deletion

File tree

src/QueryOperators.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ include("enumerable/enumerable_summarize.jl")
4646
include("enumerable/enumerable_countby.jl")
4747
include("enumerable/enumerable_aggregateby.jl")
4848
include("enumerable/enumerable_chunk.jl")
49+
include("enumerable/enumerable_terminal.jl")
4950
include("enumerable/show.jl")
5051

5152
include("source_iterable.jl")

src/enumerable/enumerable_pivot.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ end
165165
include_ops = (:include_name, :include_position, :include_startswith,
166166
:include_endswith, :include_occursin, :include_all,
167167
:include_range, :include_range_idx)
168-
has_positive = any(inst[1] include_ops for inst in instructions)
168+
# Base.any, not the `any` query operator this module also defines.
169+
has_positive = Base.any(inst[1] include_ops for inst in instructions)
169170

170171
result = has_positive ? Symbol[] : copy(all_names)
171172

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
# Terminal operators. Unlike every other operator here these return a value
2+
# rather than another Enumerable, so a query that ends in one is executed
3+
# immediately. `count` in enumerable_count.jl is the pre-existing member of
4+
# this family.
5+
6+
_no_elements(op) = error("$op was called on a sequence with no elements.")
7+
8+
_no_match(op) = error("$op was called on a sequence with no element matching the predicate.")
9+
10+
# --- Quantifiers ---
11+
12+
any(source::Enumerable) = iterate(source) !== nothing
13+
14+
function any(source::Enumerable, f::Function, f_expr::Expr)
15+
for i in source
16+
f(i) && return true
17+
end
18+
return false
19+
end
20+
21+
function all(source::Enumerable, f::Function, f_expr::Expr)
22+
for i in source
23+
f(i) || return false
24+
end
25+
return true
26+
end
27+
28+
# Compares with isequal rather than ==, so that DataValue nulls match each
29+
# other and NaN matches NaN, consistent with how `unique` and the set
30+
# operators identify elements.
31+
function contains(source::Enumerable, value)
32+
for i in source
33+
isequal(i, value) && return true
34+
end
35+
return false
36+
end
37+
38+
function sequence_equal(a::Enumerable, b::Enumerable)
39+
sa = iterate(a)
40+
sb = iterate(b)
41+
42+
while sa !== nothing && sb !== nothing
43+
isequal(sa[1], sb[1]) || return false
44+
sa = iterate(a, sa[2])
45+
sb = iterate(b, sb[2])
46+
end
47+
48+
# Equal only if both ran out at the same point.
49+
return sa === nothing && sb === nothing
50+
end
51+
52+
# --- Extremes ---
53+
54+
# Enumerable.MinBy/MaxBy (.NET 6) return the element itself, not the key. Ties
55+
# keep the first such element, as in .NET, because the comparison is strict.
56+
min_by(source::Enumerable, f::Function, f_expr::Expr) =
57+
_extreme_by(source, f, isless, "min_by")
58+
59+
max_by(source::Enumerable, f::Function, f_expr::Expr) =
60+
_extreme_by(source, f, (candidate, best) -> isless(best, candidate), "max_by")
61+
62+
function _extreme_by(source::Enumerable, f::Function, better::Function, op)
63+
ret = iterate(source)
64+
ret === nothing && _no_elements(op)
65+
66+
best = ret[1]
67+
best_key = f(best)
68+
69+
ret = iterate(source, ret[2])
70+
while ret !== nothing
71+
candidate_key = f(ret[1])
72+
if better(candidate_key, best_key)
73+
best = ret[1]
74+
best_key = candidate_key
75+
end
76+
ret = iterate(source, ret[2])
77+
end
78+
79+
return best
80+
end
81+
82+
# --- Folds ---
83+
84+
# Enumerable.Aggregate without a seed folds from the first element and requires
85+
# a non-empty sequence. `summarize` is the idiomatic way to aggregate a table.
86+
function aggregate(source::Enumerable, f::Function, f_expr::Expr)
87+
ret = iterate(source)
88+
ret === nothing && _no_elements("aggregate")
89+
90+
accumulated = ret[1]
91+
ret = iterate(source, ret[2])
92+
while ret !== nothing
93+
accumulated = f(accumulated, ret[1])
94+
ret = iterate(source, ret[2])
95+
end
96+
97+
return accumulated
98+
end
99+
100+
function aggregate(source::Enumerable, seed, f::Function, f_expr::Expr)
101+
accumulated = seed
102+
for i in source
103+
accumulated = f(accumulated, i)
104+
end
105+
return accumulated
106+
end
107+
108+
# --- Element access ---
109+
110+
function first(source::Enumerable)
111+
ret = iterate(source)
112+
ret === nothing && _no_elements("first")
113+
return ret[1]
114+
end
115+
116+
function first(source::Enumerable, f::Function, f_expr::Expr)
117+
for i in source
118+
f(i) && return i
119+
end
120+
_no_match("first")
121+
end
122+
123+
function last(source::Enumerable)
124+
ret = iterate(source)
125+
ret === nothing && _no_elements("last")
126+
127+
element = ret[1]
128+
ret = iterate(source, ret[2])
129+
while ret !== nothing
130+
element = ret[1]
131+
ret = iterate(source, ret[2])
132+
end
133+
134+
return element
135+
end
136+
137+
function last(source::Enumerable, f::Function, f_expr::Expr)
138+
element = Base.Ref{eltype(source)}()
139+
found = false
140+
141+
for i in source
142+
if f(i)
143+
element[] = i
144+
found = true
145+
end
146+
end
147+
148+
found || _no_match("last")
149+
150+
return element[]
151+
end
152+
153+
function single(source::Enumerable)
154+
ret = iterate(source)
155+
ret === nothing && _no_elements("single")
156+
157+
element = ret[1]
158+
iterate(source, ret[2]) === nothing ||
159+
error("single was called on a sequence with more than one element.")
160+
161+
return element
162+
end
163+
164+
function single(source::Enumerable, f::Function, f_expr::Expr)
165+
element = Base.Ref{eltype(source)}()
166+
found = false
167+
168+
for i in source
169+
if f(i)
170+
found && error("single was called on a sequence with more than one element matching the predicate.")
171+
element[] = i
172+
found = true
173+
end
174+
end
175+
176+
found || _no_match("single")
177+
178+
return element[]
179+
end
180+
181+
# 1-based, matching `index` and the rest of Julia rather than .NET's 0-based
182+
# ElementAt.
183+
function element_at(source::Enumerable, n::Integer)
184+
n < 1 && error("element_at was called with index $n; the index must be at least 1.")
185+
186+
seen = 0
187+
for i in source
188+
seen += 1
189+
seen == n && return i
190+
end
191+
192+
error("element_at was called with index $n on a sequence with only $seen elements.")
193+
end

src/operators.jl

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,3 +199,29 @@ function append end
199199
function prepend end
200200

201201
function zip end
202+
203+
# Terminal operators, which return a value rather than another Enumerable.
204+
# `count` above is the pre-existing member of this family. Several of these
205+
# shadow Base functions of the same name.
206+
207+
function min_by end
208+
209+
function max_by end
210+
211+
function any end
212+
213+
function all end
214+
215+
function contains end
216+
217+
function sequence_equal end
218+
219+
function aggregate end
220+
221+
function first end
222+
223+
function last end
224+
225+
function single end
226+
227+
function element_at end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ include("test_enumerable_ordering.jl")
77
include("test_enumerable_keyed_aggregation.jl")
88
include("test_enumerable_partitioning.jl")
99
include("test_enumerable_combining.jl")
10+
include("test_enumerable_terminal.jl")
1011
include("test_enumerable_unique.jl")
1112
include("test_enumerable_summarize.jl")
1213
include("test_namedtupleutilities.jl")

0 commit comments

Comments
 (0)