Skip to content

Commit 58e20a1

Browse files
ghyatzoadienesstevengjDilumAluthge
authored
adds the nth function for iterables (#56580)
Hi, I've turned the open ended issue #54454 into an actual PR. Tangentially related to #10092 ? This PR introduces the `nth(itr, n)` function to iterators to give a `getindex` type of behaviour. I've tried my best to optimize as much as possible by specializing on different types of iterators. In the spirit of iterators any OOB access returns `nothing`. (edit: instead of throwing an error, i.e. `first(itr, n)` and `last(itr, n)`) here is the comparison of running the testsuite (~22 different iterators) using generic `nth` and specialized `nth`: ```julia @Btime begin for (itr, n, _) in $testset _fallback_nth(itr, n) end end 117.750 μs (366 allocations: 17.88 KiB) @Btime begin for (itr, n, _) in $testset nth(itr, n) end end 24.250 μs (341 allocations: 16.70 KiB) ``` --------- Co-authored-by: adienes <51664769+adienes@users.noreply.github.com> Co-authored-by: Steven G. Johnson <stevenj@mit.edu> Co-authored-by: Dilum Aluthge <dilum@aluthge.com>
1 parent a60d238 commit 58e20a1

File tree

3 files changed

+142
-2
lines changed

3 files changed

+142
-2
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ New language features
55
---------------------
66

77
- New `Base.@acquire` macro for a non-closure version of `Base.acquire(f, s::Base.Semaphore)`, like `@lock`. ([#56845])
8+
- New `nth` function to access the `n`-th element of a generic iterable. ([#56580])
89
- The character U+1F8B2 🢲 (RIGHTWARDS ARROW WITH LOWER HOOK), newly added by Unicode 16,
910
is now a valid operator with arrow precedence, accessible as `\hookunderrightarrow` at the REPL.
1011
([JuliaLang/JuliaSyntax.jl#525], [#57143])

base/iterators.jl

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ using .Base:
1616
(:), |, +, -, *, !==, !, ==, !=, <=, <, >, >=, =>, missing,
1717
any, _counttuple, eachindex, ntuple, zero, prod, reduce, in, firstindex, lastindex,
1818
tail, fieldtypes, min, max, minimum, zero, oneunit, promote, promote_shape, LazyString,
19-
afoldl
19+
afoldl, mod1
2020
using .Core
2121
using Core: @doc
2222

@@ -32,7 +32,7 @@ import Base:
3232
getindex, setindex!, get, iterate,
3333
popfirst!, isdone, peek, intersect
3434

35-
export enumerate, zip, rest, countfrom, take, drop, takewhile, dropwhile, cycle, repeated, product, flatten, flatmap, partition
35+
export enumerate, zip, rest, countfrom, take, drop, takewhile, dropwhile, cycle, repeated, product, flatten, flatmap, partition, nth
3636
public accumulate, filter, map, peel, reverse, Stateful
3737

3838
"""
@@ -991,6 +991,7 @@ end
991991
reverse(it::Cycle) = Cycle(reverse(it.xs))
992992
last(it::Cycle) = last(it.xs)
993993

994+
994995
# Repeated - repeat an object infinitely many times
995996

996997
struct Repeated{O}
@@ -1602,4 +1603,83 @@ end
16021603
# be the same as the keys, so this is a valid optimization (see #51631)
16031604
pairs(s::AbstractString) = IterableStatePairs(s)
16041605

1606+
"""
1607+
nth(itr, n::Integer)
1608+
1609+
Get the `n`th element of an iterable collection. Throw a `BoundsError`[@ref] if not existing.
1610+
Will advance any `Stateful`[@ref] iterator.
1611+
1612+
See also: [`first`](@ref), [`last`](@ref)
1613+
1614+
# Examples
1615+
```jldoctest
1616+
julia> Iterators.nth(2:2:10, 4)
1617+
8
1618+
1619+
julia> Iterators.nth(reshape(1:30, (5,6)), 6)
1620+
6
1621+
1622+
julia> stateful = Iterators.Stateful(1:10); Iterators.nth(stateful, 7)
1623+
7
1624+
1625+
julia> first(stateful)
1626+
8
1627+
```
1628+
"""
1629+
nth(itr, n::Integer) = _nth(IteratorSize(itr), itr, n)
1630+
nth(itr::Cycle{I}, n::Integer) where I = _nth(IteratorSize(I), itr, n)
1631+
nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where O = _nth(IteratorSize(O), itr, n)
1632+
@propagate_inbounds nth(itr::AbstractArray, n::Integer) = itr[begin + n - 1]
1633+
1634+
function _nth(::Union{HasShape, HasLength}, itr::Cycle{I}, n::Integer) where {I}
1635+
N = length(itr.xs)
1636+
N == 0 && throw(BoundsError(itr, n))
1637+
1638+
# prevents wrap around behaviour and inherit the error handling
1639+
return nth(itr.xs, n > 0 ? mod1(n, N) : n)
1640+
end
1641+
1642+
# Flatten{Take{Repeated{O}}} is the actual type of an Iterators.cycle(iterable::O, m) iterator
1643+
function _nth(::Union{HasShape, HasLength}, itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O}
1644+
cycles = itr.it.n
1645+
torepeat = itr.it.xs.x
1646+
k = length(torepeat)
1647+
(n > k*cycles || k == 0) && throw(BoundsError(itr, n))
1648+
1649+
# prevent wrap around behaviour and inherit the error handling
1650+
return nth(torepeat, n > 0 ? mod1(n, k) : n)
1651+
end
1652+
1653+
function _nth(::IteratorSize, itr, n::Integer)
1654+
# unrolled version of `first(drop)`
1655+
n > 0 || throw(BoundsError(itr, n))
1656+
y = iterate(itr)
1657+
for _ in 1:n-1
1658+
y === nothing && break
1659+
y = iterate(itr, y[2])
1660+
end
1661+
y === nothing && throw(BoundsError(itr, n))
1662+
y[1]
1663+
end
1664+
"""
1665+
nth(n::Integer)
1666+
1667+
Return a function that gets the `n`-th element from any iterator passed to it.
1668+
Equivalent to `Base.Fix2(nth, n)` or `itr -> nth(itr, n)`.
1669+
1670+
See also: [`nth`](@ref), [`Base.Fix2`](@ref)
1671+
# Examples
1672+
```jldoctest
1673+
julia> fifth_element = Iterators.nth(5)
1674+
(::Base.Fix2{typeof(Base.Iterators.nth), Int64}) (generic function with 2 methods)
1675+
1676+
julia> fifth_element(reshape(1:30, (5,6)))
1677+
5
1678+
1679+
julia> map(fifth_element, ("Willis", "Jovovich", "Oldman"))
1680+
('i', 'v', 'a')
1681+
```
1682+
"""
1683+
nth(n::Integer) = Base.Fix2(nth, n)
1684+
16051685
end

test/iterators.jl

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1139,6 +1139,65 @@ end
11391139
end
11401140
end
11411141

1142+
@testset "nth" begin
1143+
1144+
Z = Array{Int,0}(undef)
1145+
Z[] = 17
1146+
it_result_pairs = Dict(
1147+
(Z, 1) => 17,
1148+
(collect(1:100), 23) => 23,
1149+
(10:6:1000, 123) => 10 + 6 * 122,
1150+
("∀ϵ>0", 3) => '>',
1151+
((1, 3, 5, 10, 78), 2) => 3,
1152+
(reshape(1:30, (5, 6)), 21) => 21,
1153+
(3, 1) => 3,
1154+
(true, 1) => true,
1155+
('x', 1) => 'x',
1156+
(4 => 5, 2) => 5,
1157+
(view(Z), 1) => 17,
1158+
(view(reshape(1:30, (5, 6)), 2:4, 2:6), 10) => 22,
1159+
((x^2 for x in 1:10), 9) => 81,
1160+
(Iterators.Filter(isodd, 1:10), 3) => 5,
1161+
(Iterators.flatten((1:10, 50:60)), 15) => 54,
1162+
(pairs(50:60), 7) => 7 => 56,
1163+
(zip(1:10, 21:30, 51:60), 6) => (6, 26, 56),
1164+
(Iterators.product(1:3, 10:12), 3) => (3, 10),
1165+
(Iterators.repeated(3.14159, 5), 4) => 3.14159,
1166+
((a=2, b=3, c=5, d=7, e=11), 4) => 7,
1167+
(Iterators.cycle(collect(1:100)), 9999) => 99,
1168+
(Iterators.cycle([1, 2, 3, 4, 5], 5), 25) => 5,
1169+
(Iterators.cycle("String", 10), 16) => 'i',
1170+
(Iterators.cycle(((),)), 1000) => ()
1171+
)
1172+
1173+
1174+
@testset "iter: $IT" for (IT, n) in keys(it_result_pairs)
1175+
@test it_result_pairs[(IT, n)] == nth(IT, n)
1176+
@test_throws BoundsError nth(IT, -42)
1177+
1178+
IT isa Iterators.Cycle && continue # cycles are infinite so never OOB
1179+
@test_throws BoundsError nth(IT, 999999999)
1180+
end
1181+
1182+
empty_cycle = Iterators.cycle([])
1183+
@test_throws BoundsError nth(empty_cycle, 42)
1184+
1185+
# test the size unknown branch for cycles
1186+
# only generate odd numbers so we know the actual length
1187+
# but the iterator is still SizeUnknown()
1188+
it_size_unknown = Iterators.filter(isodd, 1:2:10)
1189+
@test Base.IteratorSize(it_size_unknown) isa Base.SizeUnknown
1190+
@test length(collect(it_size_unknown)) == 5
1191+
1192+
cycle_size_unknown = Iterators.cycle(it_size_unknown)
1193+
finite_cycle_size_unknown = Iterators.cycle(it_size_unknown, 5)
1194+
@test nth(cycle_size_unknown, 2) == 3
1195+
@test nth(cycle_size_unknown, 20) == 9 # mod1(20, 5) = 5, wraps 4 times
1196+
@test nth(finite_cycle_size_unknown, 2) == 3
1197+
@test nth(finite_cycle_size_unknown, 20) == 9
1198+
@test_throws BoundsError nth(finite_cycle_size_unknown, 30) # only wraps 5 times, max n is 5 * 5 = 25
1199+
end
1200+
11421201
@testset "Iterators docstrings" begin
11431202
@test isempty(Docs.undocumented_names(Iterators))
11441203
end

0 commit comments

Comments
 (0)