Skip to content

Commit 99ed76e

Browse files
davidanthoffclaude
andcommitted
Add the take_while, drop_while, take_last and drop_last operators
Ports Enumerable.TakeWhile, SkipWhile, TakeLast and SkipLast. The Skip operators are named drop_* to match the existing `drop` rather than LINQ's `Skip`. take_while and drop_while are lazy: take_while stops at the first element that fails the predicate and never examines the rest, and drop_while passes everything through untested once the leading run is gone. take_last holds at most n elements in a CircularBuffer while walking the source. drop_last stays n elements behind the source, emitting an element only once n further ones have been read. A count of zero or less yields nothing for take_last and leaves the source unchanged for drop_last, matching .NET. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent fefe333 commit 99ed76e

8 files changed

Lines changed: 321 additions & 0 deletions

src/QueryOperators.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ include("enumerable/enumerable_defaultifempty.jl")
2626
include("enumerable/enumerable_count.jl")
2727
include("enumerable/enumerable_take.jl")
2828
include("enumerable/enumerable_drop.jl")
29+
include("enumerable/enumerable_takewhile.jl")
30+
include("enumerable/enumerable_dropwhile.jl")
31+
include("enumerable/enumerable_takelast.jl")
32+
include("enumerable/enumerable_droplast.jl")
2933
include("enumerable/enumerable_unique.jl")
3034
include("enumerable/enumerable_concat.jl")
3135
include("enumerable/enumerable_union.jl")
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
struct EnumerableDropLast{T,S} <: Enumerable
2+
source::S
3+
n::Int
4+
end
5+
6+
# Enumerable.SkipLast: everything but the trailing `n` elements. A count of
7+
# zero or less leaves the source unchanged, as in .NET.
8+
function drop_last(source::Enumerable, n::Integer)
9+
T = eltype(source)
10+
return EnumerableDropLast{T,typeof(source)}(source, max(Int(n), 0))
11+
end
12+
13+
Base.IteratorSize(::Type{EnumerableDropLast{T,S}}) where {T,S} = haslength(S)
14+
15+
Base.eltype(::Type{EnumerableDropLast{T,S}}) where {T,S} = T
16+
17+
Base.length(iter::EnumerableDropLast) = max(length(iter.source) - iter.n, 0)
18+
19+
# Stays `n` elements behind the source: an element is only emitted once `n`
20+
# further elements have been read, which proves it is not one of the last `n`.
21+
function Base.iterate(iter::EnumerableDropLast{T,S}) where {T,S}
22+
buffer = T[]
23+
source_state = _NotStarted()
24+
25+
while length(buffer) < iter.n
26+
ret = _iterate_from(iter.source, source_state)
27+
# Fewer than n elements in total, so every one of them is dropped.
28+
ret === nothing && return nothing
29+
push!(buffer, ret[1])
30+
source_state = ret[2]
31+
end
32+
33+
return _drop_last_next(iter, buffer, source_state)
34+
end
35+
36+
function Base.iterate(iter::EnumerableDropLast, state)
37+
return _drop_last_next(iter, state.buffer, state.state)
38+
end
39+
40+
function _drop_last_next(iter::EnumerableDropLast, buffer, source_state)
41+
ret = _iterate_from(iter.source, source_state)
42+
ret === nothing && return nothing
43+
44+
push!(buffer, ret[1])
45+
element = popfirst!(buffer)
46+
47+
return element, (buffer=buffer, state=ret[2])
48+
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
struct EnumerableDropWhile{T,S,Q<:Function} <: Enumerable
2+
source::S
3+
f::Q
4+
end
5+
6+
# Enumerable.SkipWhile: discards the leading run of elements satisfying the
7+
# predicate, then yields everything that follows without testing it again.
8+
function drop_while(source::Enumerable, f::Function, f_expr::Expr)
9+
T = eltype(source)
10+
return EnumerableDropWhile{T,typeof(source),typeof(f)}(source, f)
11+
end
12+
13+
Base.eltype(::Type{EnumerableDropWhile{T,S,Q}}) where {T,S,Q} = T
14+
15+
function Base.iterate(iter::EnumerableDropWhile)
16+
ret = iterate(iter.source)
17+
18+
while ret !== nothing && iter.f(ret[1])
19+
ret = iterate(iter.source, ret[2])
20+
end
21+
22+
ret === nothing && return nothing
23+
24+
return ret[1], ret[2]
25+
end
26+
27+
Base.iterate(iter::EnumerableDropWhile, state) = iterate(iter.source, state)
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
struct EnumerableTakeLast{T,S} <: Enumerable
2+
source::S
3+
n::Int
4+
end
5+
6+
# Enumerable.TakeLast: the trailing `n` elements. A count of zero or less
7+
# yields nothing, as in .NET.
8+
function take_last(source::Enumerable, n::Integer)
9+
T = eltype(source)
10+
return EnumerableTakeLast{T,typeof(source)}(source, max(Int(n), 0))
11+
end
12+
13+
Base.IteratorSize(::Type{EnumerableTakeLast{T,S}}) where {T,S} = haslength(S)
14+
15+
Base.eltype(::Type{EnumerableTakeLast{T,S}}) where {T,S} = T
16+
17+
Base.length(iter::EnumerableTakeLast) = min(length(iter.source), iter.n)
18+
19+
# Which elements are last is only known once the source is exhausted, so the
20+
# whole source is walked, holding at most `n` elements in a ring buffer.
21+
function Base.iterate(iter::EnumerableTakeLast{T,S}) where {T,S}
22+
iter.n == 0 && return nothing
23+
24+
buffer = CircularBuffer{T}(iter.n)
25+
for i in iter.source
26+
push!(buffer, i)
27+
end
28+
29+
length(buffer)==0 && return nothing
30+
31+
elements = Base.collect(buffer)
32+
33+
return elements[1], (elements, 2)
34+
end
35+
36+
function Base.iterate(iter::EnumerableTakeLast{T,S}, state) where {T,S}
37+
if state[2]>length(state[1])
38+
return nothing
39+
else
40+
return state[1][state[2]], (state[1], state[2]+1)
41+
end
42+
end
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
struct EnumerableTakeWhile{T,S,Q<:Function} <: Enumerable
2+
source::S
3+
f::Q
4+
end
5+
6+
# Enumerable.TakeWhile: yields elements until the predicate first fails, then
7+
# stops — later elements are not examined even if they would satisfy it.
8+
function take_while(source::Enumerable, f::Function, f_expr::Expr)
9+
T = eltype(source)
10+
return EnumerableTakeWhile{T,typeof(source),typeof(f)}(source, f)
11+
end
12+
13+
Base.eltype(::Type{EnumerableTakeWhile{T,S,Q}}) where {T,S,Q} = T
14+
15+
Base.iterate(iter::EnumerableTakeWhile) = _take_while_step(iter, iterate(iter.source))
16+
17+
Base.iterate(iter::EnumerableTakeWhile, state) = _take_while_step(iter, iterate(iter.source, state))
18+
19+
function _take_while_step(iter::EnumerableTakeWhile, ret)
20+
ret === nothing && return nothing
21+
iter.f(ret[1]) || return nothing
22+
return ret[1], ret[2]
23+
end

src/operators.jl

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,14 @@ function count_by end
180180
function aggregate_by end
181181

182182
function chunk end
183+
184+
# Partitioning. `drop_while` and `drop_last` are Enumerable.SkipWhile and
185+
# SkipLast, named to match the existing `drop` rather than LINQ's `Skip`.
186+
187+
function take_while end
188+
189+
function drop_while end
190+
191+
function take_last end
192+
193+
function drop_last end

test/runtests.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ include("test_enumerable_outerjoins.jl")
55
include("test_enumerable_setops.jl")
66
include("test_enumerable_ordering.jl")
77
include("test_enumerable_keyed_aggregation.jl")
8+
include("test_enumerable_partitioning.jl")
89
include("test_enumerable_unique.jl")
910
include("test_enumerable_summarize.jl")
1011
include("test_namedtupleutilities.jl")
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
@testitem "take_while" begin
2+
source = QueryOperators.query([1, 2, 3, 4, 1, 2])
3+
4+
res = QueryOperators.take_while(source, i -> i < 3, :(i -> i < 3))
5+
6+
# Stops at the first failure; the trailing 1 and 2 are not taken.
7+
@test collect(res) == [1, 2]
8+
@test eltype(res) == Int
9+
end
10+
11+
@testitem "take_while taking everything or nothing" begin
12+
source = QueryOperators.query([1, 2, 3])
13+
14+
@test collect(QueryOperators.take_while(source, i -> true, :(i -> true))) == [1, 2, 3]
15+
@test collect(QueryOperators.take_while(source, i -> false, :(i -> false))) == Int[]
16+
end
17+
18+
@testitem "take_while on an empty source" begin
19+
source = QueryOperators.query(Int[])
20+
21+
@test collect(QueryOperators.take_while(source, i -> true, :(i -> true))) == Int[]
22+
end
23+
24+
@testitem "take_while stops walking the source at the first failure" begin
25+
import IteratorInterfaceExtensions
26+
27+
mutable struct CountedTakeWhileSource
28+
data::Vector{Int}
29+
pulled::Int
30+
end
31+
Base.eltype(::Type{CountedTakeWhileSource}) = Int
32+
Base.IteratorSize(::Type{CountedTakeWhileSource}) = Base.HasLength()
33+
Base.length(c::CountedTakeWhileSource) = length(c.data)
34+
function Base.iterate(c::CountedTakeWhileSource, i=1)
35+
i > length(c.data) && return nothing
36+
c.pulled += 1
37+
return c.data[i], i + 1
38+
end
39+
IteratorInterfaceExtensions.isiterable(::CountedTakeWhileSource) = true
40+
IteratorInterfaceExtensions.getiterator(c::CountedTakeWhileSource) = c
41+
42+
src = CountedTakeWhileSource(collect(1:100), 0)
43+
res = QueryOperators.take_while(QueryOperators.query(src), i -> i < 4, :(i -> i < 4))
44+
45+
@test collect(res) == [1, 2, 3]
46+
# Three taken plus the one that failed the predicate.
47+
@test src.pulled == 4
48+
end
49+
50+
@testitem "drop_while" begin
51+
source = QueryOperators.query([1, 2, 3, 4, 1, 2])
52+
53+
res = QueryOperators.drop_while(source, i -> i < 3, :(i -> i < 3))
54+
55+
# Only the leading run is dropped; the trailing 1 and 2 survive.
56+
@test collect(res) == [3, 4, 1, 2]
57+
@test eltype(res) == Int
58+
end
59+
60+
@testitem "drop_while dropping everything or nothing" begin
61+
source = QueryOperators.query([1, 2, 3])
62+
63+
@test collect(QueryOperators.drop_while(source, i -> true, :(i -> true))) == Int[]
64+
@test collect(QueryOperators.drop_while(source, i -> false, :(i -> false))) == [1, 2, 3]
65+
end
66+
67+
@testitem "drop_while on an empty source" begin
68+
source = QueryOperators.query(Int[])
69+
70+
@test collect(QueryOperators.drop_while(source, i -> true, :(i -> true))) == Int[]
71+
end
72+
73+
@testitem "take_while and drop_while partition the source" begin
74+
source = QueryOperators.query([1, 2, 3, 4, 5])
75+
p = i -> i < 3
76+
77+
taken = collect(QueryOperators.take_while(source, p, :(i -> i < 3)))
78+
dropped = collect(QueryOperators.drop_while(source, p, :(i -> i < 3)))
79+
80+
@test vcat(taken, dropped) == [1, 2, 3, 4, 5]
81+
end
82+
83+
@testitem "take_last" begin
84+
source = QueryOperators.query([1, 2, 3, 4, 5])
85+
86+
res = QueryOperators.take_last(source, 2)
87+
88+
@test collect(res) == [4, 5]
89+
@test eltype(res) == Int
90+
@test Base.IteratorSize(typeof(res)) == Base.HasLength()
91+
@test length(res) == 2
92+
end
93+
94+
@testitem "take_last with a count at or beyond the source length" begin
95+
source = QueryOperators.query([1, 2, 3])
96+
97+
@test collect(QueryOperators.take_last(source, 3)) == [1, 2, 3]
98+
@test collect(QueryOperators.take_last(source, 10)) == [1, 2, 3]
99+
@test length(QueryOperators.take_last(source, 10)) == 3
100+
end
101+
102+
@testitem "take_last with a count of zero or less" begin
103+
source = QueryOperators.query([1, 2, 3])
104+
105+
@test collect(QueryOperators.take_last(source, 0)) == Int[]
106+
@test collect(QueryOperators.take_last(source, -1)) == Int[]
107+
end
108+
109+
@testitem "take_last on an empty source" begin
110+
source = QueryOperators.query(Int[])
111+
112+
@test collect(QueryOperators.take_last(source, 2)) == Int[]
113+
end
114+
115+
@testitem "drop_last" begin
116+
source = QueryOperators.query([1, 2, 3, 4, 5])
117+
118+
res = QueryOperators.drop_last(source, 2)
119+
120+
@test collect(res) == [1, 2, 3]
121+
@test eltype(res) == Int
122+
@test Base.IteratorSize(typeof(res)) == Base.HasLength()
123+
@test length(res) == 3
124+
end
125+
126+
@testitem "drop_last with a count at or beyond the source length" begin
127+
source = QueryOperators.query([1, 2, 3])
128+
129+
@test collect(QueryOperators.drop_last(source, 3)) == Int[]
130+
@test collect(QueryOperators.drop_last(source, 10)) == Int[]
131+
@test length(QueryOperators.drop_last(source, 10)) == 0
132+
end
133+
134+
@testitem "drop_last with a count of zero or less is the identity" begin
135+
source = QueryOperators.query([1, 2, 3])
136+
137+
@test collect(QueryOperators.drop_last(source, 0)) == [1, 2, 3]
138+
@test collect(QueryOperators.drop_last(source, -1)) == [1, 2, 3]
139+
end
140+
141+
@testitem "drop_last on an empty source" begin
142+
source = QueryOperators.query(Int[])
143+
144+
@test collect(QueryOperators.drop_last(source, 2)) == Int[]
145+
end
146+
147+
@testitem "take_last and drop_last partition the source" begin
148+
source = QueryOperators.query([1, 2, 3, 4, 5])
149+
150+
dropped = collect(QueryOperators.drop_last(source, 2))
151+
taken = collect(QueryOperators.take_last(source, 2))
152+
153+
@test vcat(dropped, taken) == [1, 2, 3, 4, 5]
154+
end
155+
156+
@testitem "partitioning operators work 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+
last_two = collect(QueryOperators.take_last(grouped, 2))
161+
@test [QueryOperators.key(g) for g in last_two] == [2, 3]
162+
163+
all_but_last = collect(QueryOperators.drop_last(grouped, 1))
164+
@test [QueryOperators.key(g) for g in all_but_last] == [1, 2]
165+
end

0 commit comments

Comments
 (0)