From b5657830eda136f2eb8e09017aeb1ed7c0da52cb Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 16 Nov 2024 13:00:25 +0100 Subject: [PATCH 01/27] adds `nth` function to iterators plus tests --- base/iterators.jl | 71 +++++++++++++++++++++++++++++++++++++++++++++++ test/iterators.jl | 24 ++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/base/iterators.jl b/base/iterators.jl index 4b956073f0c04..6b27c9e54e8a0 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1602,4 +1602,75 @@ end # be the same as the keys, so this is a valid optimization (see #51631) pairs(s::AbstractString) = IterableStatePairs(s) +""" + nth(itr, n::Integer) + +Get the `n`th element of an iterable collection. Return `nothing` if not existing. + +See also: [`first`](@ref), [`last`](@ref) + +# Examples +```jldoctest +julia> nth(2:2:10, 4) +8 + +julia> nth(reshape(1:30, (5,6)), 6) +6 +``` +""" +nth(itr, n::Integer) = _nth(IteratorSize(itr), itr, n) +nth(itr::AbstractArray, n::Integer) = n > length(itr) ? nothing : itr[n] + +_nth(::SizeUnknown, itr, n) = _fallback_nth(itr, n) +_nth(::Union{HasShape,HasLength}, itr, n) = _withlength_nth(itr, n, length(itr)) +_nth(::IsInfinite, itr, n) = _inbounds_nth(itr, n) + +_inbounds_nth(itr, n) = iterate(Base.Iterators.drop(itr, n - 1))[1] +_inbounds_nth(itr::AbstractArray, n) = itr[n] + +_withlength_nth(itr, n, N) = n > N ? nothing : _inbounds_nth(itr, n) + +function _fallback_nth(itr, n) + y = iterate(Iterators.drop(itr, n - 1)) + y === nothing && return nothing + y[1] +end + +# specialized versions to better interact with existing iterators +# Count +nth(itr::Count, n::Integer) = n > 0 ? itr.start + itr.step * (n - 1) : nothing + +# Repeated +nth(itr::Repeated, n::Integer) = itr.x + +# Take(Repeated) +nth(itr::Take{Repeated{O}}, n::Integer) where {O} = n > itr.n ? nothing : itr.xs.x + +# infinite cycle +nth(itr::Iterators.Cycle{I}, n::Integer) where {I} = _nth_inf_cycle(IteratorSize(I), itr, n) +_nth_inf_cycle(::IsInfinite, itr, n) = _inbounds_nth(itr.xs, n) +_nth_inf_cycle(::SizeUnknown, itr, n) = _fallback_nth(itr.xs, n) +_nth_inf_cycle(::Union{HasShape,HasLength}, itr, n) = _repeating_cycle_nth(itr.xs, n, length(itr.xs)) + +# finite cycle +# a finite cycle iterator is in reality a Flatten{Take{Repeated{O}}} iterator +nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} = _nth_finite_cycle(IteratorSize(O), itr, n) +_nth_finite_cycle(::IsInfinite, itr, n) = _inbounds_nth(itr, n) +_nth_finite_cycle(::SizeUnknown, itr, n) = _fallback_nth(itr, n) +_nth_finite_cycle(::Union{HasShape,HasLength}, itr, n) = begin + N = itr.it.n # `Take` iterator n + torepeat = itr.it.xs.x # repeated object + K = length(torepeat) + n > K * N && return nothing + _repeating_cycle_nth(torepeat, n, K) +end +# O = [....] # K length +# Repeat(O) = [ [....] [....] [....] ... +# Take(Repeat(O), N) = [ [....] [....] [....] ] # N times +# Flatten(...) = [....|....|....] # K*N elements + +_repeating_cycle_nth(inner_itr, n, inner_N) = _inbounds_nth(inner_itr, 1 + ((n - 1) % inner_N)) + + + end diff --git a/test/iterators.jl b/test/iterators.jl index df4fa63b433b8..7a9ea465f30a5 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1133,6 +1133,30 @@ end end end +@testset "nth" begin + Z = Array{Int,0}(undef) + Z[] = 17 + itrs = (collect(1:1000), 10:6:1000, "∀ϵ>0", (1, 3, 5, 10, 78), reshape(1:30, (5, 6)), + Z, 3, true, 'x', 4 => 5, view(Z), view(reshape(1:30, (5, 6)), 2:4, 2:6), + (x^2 for x in 1:10), Iterators.Filter(isodd, 1:10), Iterators.flatten((1:10, 50:60)), + pairs(50:60), zip(1:10, 21:30, 51:60), Iterators.product(1:3, 10:12), + Iterators.repeated(3.14159, 5), (a=2, b=3, c=5, d=7, e=11), Iterators.cycle(collect(1:100)), + Iterators.cycle([1, 2, 3, 4, 5], 5)) + ns = ( + 234, 123, 3, 2, 21, 1, 1, 1, 1, 1, 1, 10, 9, 3, 15, 7, 6, 3, 4, 4, 99999, 25 + ) + expected = ( + 234, 742, '>', 3, 21, 17, 3, true, 'x', 4, 17, 22, 81, 5, 54, (7 => 56), (6, 26, 56), (3, 10), 3.14159, 7, 99, 5 + ) + @test length(itrs) == length(ns) == length(expected) + testset = zip(itrs, ns, expected) + @testset "iter: $IT" for (IT, n, exp) in testset + @test exp == nth(IT, n) + IT isa Cycle && continue # cycles are infinite so never OOB + @test nth(IT, 999999999) === nothing + end +end + @testset "Iterators docstrings" begin @test isempty(Docs.undocumented_names(Iterators)) end From ed7a9842fd8f26ae7d0a9e40b150dda8c0d1fffc Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 16 Nov 2024 13:21:32 +0100 Subject: [PATCH 02/27] removed useless module annotations added nth to export list --- base/iterators.jl | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 6b27c9e54e8a0..2e785454ab335 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -32,7 +32,7 @@ import Base: getindex, setindex!, get, iterate, popfirst!, isdone, peek, intersect -export enumerate, zip, rest, countfrom, take, drop, takewhile, dropwhile, cycle, repeated, product, flatten, flatmap, partition +export enumerate, zip, rest, countfrom, take, drop, takewhile, dropwhile, cycle, repeated, product, flatten, flatmap, partition, nth public accumulate, filter, map, peel, reverse, Stateful """ @@ -1625,13 +1625,13 @@ _nth(::SizeUnknown, itr, n) = _fallback_nth(itr, n) _nth(::Union{HasShape,HasLength}, itr, n) = _withlength_nth(itr, n, length(itr)) _nth(::IsInfinite, itr, n) = _inbounds_nth(itr, n) -_inbounds_nth(itr, n) = iterate(Base.Iterators.drop(itr, n - 1))[1] +_inbounds_nth(itr, n) = iterate(drop(itr, n - 1))[1] _inbounds_nth(itr::AbstractArray, n) = itr[n] _withlength_nth(itr, n, N) = n > N ? nothing : _inbounds_nth(itr, n) function _fallback_nth(itr, n) - y = iterate(Iterators.drop(itr, n - 1)) + y = iterate(drop(itr, n - 1)) y === nothing && return nothing y[1] end @@ -1647,7 +1647,7 @@ nth(itr::Repeated, n::Integer) = itr.x nth(itr::Take{Repeated{O}}, n::Integer) where {O} = n > itr.n ? nothing : itr.xs.x # infinite cycle -nth(itr::Iterators.Cycle{I}, n::Integer) where {I} = _nth_inf_cycle(IteratorSize(I), itr, n) +nth(itr::Cycle{I}, n::Integer) where {I} = _nth_inf_cycle(IteratorSize(I), itr, n) _nth_inf_cycle(::IsInfinite, itr, n) = _inbounds_nth(itr.xs, n) _nth_inf_cycle(::SizeUnknown, itr, n) = _fallback_nth(itr.xs, n) _nth_inf_cycle(::Union{HasShape,HasLength}, itr, n) = _repeating_cycle_nth(itr.xs, n, length(itr.xs)) @@ -1664,10 +1664,7 @@ _nth_finite_cycle(::Union{HasShape,HasLength}, itr, n) = begin n > K * N && return nothing _repeating_cycle_nth(torepeat, n, K) end -# O = [....] # K length -# Repeat(O) = [ [....] [....] [....] ... -# Take(Repeat(O), N) = [ [....] [....] [....] ] # N times -# Flatten(...) = [....|....|....] # K*N elements + _repeating_cycle_nth(inner_itr, n, inner_N) = _inbounds_nth(inner_itr, 1 + ((n - 1) % inner_N)) From 1bfc0806b1aca9adc3277675531c6edb173b9e5e Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 16 Nov 2024 15:18:37 +0100 Subject: [PATCH 03/27] fix one based indexing, more generic add docs explaining interaction with Stateful iterators change test to be Any vectors instead of tuples (actually way faster as well) --- base/iterators.jl | 12 ++++++++++-- test/iterators.jl | 16 ++++++++-------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 2e785454ab335..d520b20c2788c 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1606,6 +1606,7 @@ pairs(s::AbstractString) = IterableStatePairs(s) nth(itr, n::Integer) Get the `n`th element of an iterable collection. Return `nothing` if not existing. +Will advance any `Stateful`[@ref] iterator. See also: [`first`](@ref), [`last`](@ref) @@ -1616,17 +1617,24 @@ julia> nth(2:2:10, 4) julia> nth(reshape(1:30, (5,6)), 6) 6 + +julia> stateful = Stateful(1:10); nth(stateful, 7) +7 + +julia> first(stateful) +8 ``` """ nth(itr, n::Integer) = _nth(IteratorSize(itr), itr, n) -nth(itr::AbstractArray, n::Integer) = n > length(itr) ? nothing : itr[n] +nth(itr::AbstractArray, n::Integer) = n > length(itr) ? nothing : _inbounds_nth(itr, n) +nth(itr::AbstractRange, n) = getindex(itr, n) _nth(::SizeUnknown, itr, n) = _fallback_nth(itr, n) _nth(::Union{HasShape,HasLength}, itr, n) = _withlength_nth(itr, n, length(itr)) _nth(::IsInfinite, itr, n) = _inbounds_nth(itr, n) _inbounds_nth(itr, n) = iterate(drop(itr, n - 1))[1] -_inbounds_nth(itr::AbstractArray, n) = itr[n] +_inbounds_nth(itr::AbstractArray, n) = getindex(itr, nth(eachindex(IndexLinear(), itr), n)) _withlength_nth(itr, n, N) = n > N ? nothing : _inbounds_nth(itr, n) diff --git a/test/iterators.jl b/test/iterators.jl index 7a9ea465f30a5..0623ef81a5a46 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1136,18 +1136,18 @@ end @testset "nth" begin Z = Array{Int,0}(undef) Z[] = 17 - itrs = (collect(1:1000), 10:6:1000, "∀ϵ>0", (1, 3, 5, 10, 78), reshape(1:30, (5, 6)), - Z, 3, true, 'x', 4 => 5, view(Z), view(reshape(1:30, (5, 6)), 2:4, 2:6), + itrs = Any[collect(1:1000), 10:6:1000, "∀ϵ>0", (1, 3, 5, 10, 78), reshape(1:30, (5, 6)), + Z, 3, true, 'x', 4=>5, view(Z), view(reshape(1:30, (5, 6)), 2:4, 2:6), (x^2 for x in 1:10), Iterators.Filter(isodd, 1:10), Iterators.flatten((1:10, 50:60)), pairs(50:60), zip(1:10, 21:30, 51:60), Iterators.product(1:3, 10:12), Iterators.repeated(3.14159, 5), (a=2, b=3, c=5, d=7, e=11), Iterators.cycle(collect(1:100)), - Iterators.cycle([1, 2, 3, 4, 5], 5)) - ns = ( + Iterators.cycle([1, 2, 3, 4, 5], 5)] + ns = Any[ 234, 123, 3, 2, 21, 1, 1, 1, 1, 1, 1, 10, 9, 3, 15, 7, 6, 3, 4, 4, 99999, 25 - ) - expected = ( - 234, 742, '>', 3, 21, 17, 3, true, 'x', 4, 17, 22, 81, 5, 54, (7 => 56), (6, 26, 56), (3, 10), 3.14159, 7, 99, 5 - ) + ] + expected = Any[ + 234, 742, '>', 3, 21, 17, 3, true, 'x', 4, 17, 22, 81, 5, 54, (7=>56), (6, 26, 56), (3, 10), 3.14159, 7, 99, 5 + ] @test length(itrs) == length(ns) == length(expected) testset = zip(itrs, ns, expected) @testset "iter: $IT" for (IT, n, exp) in testset From b3fca0b409c952e7b5ebe569d313be3c802a1b5c Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 17 Nov 2024 09:51:54 +0100 Subject: [PATCH 04/27] fix: simpler generic-based indexing --- base/iterators.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/iterators.jl b/base/iterators.jl index d520b20c2788c..780b85ae98253 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1634,7 +1634,7 @@ _nth(::Union{HasShape,HasLength}, itr, n) = _withlength_nth(itr, n, length(itr)) _nth(::IsInfinite, itr, n) = _inbounds_nth(itr, n) _inbounds_nth(itr, n) = iterate(drop(itr, n - 1))[1] -_inbounds_nth(itr::AbstractArray, n) = getindex(itr, nth(eachindex(IndexLinear(), itr), n)) +_inbounds_nth(itr::AbstractArray, n) = itr[begin + n-1] _withlength_nth(itr, n, N) = n > N ? nothing : _inbounds_nth(itr, n) From 26913e6e9ec13d24fc274333c56805c8f06b47e6 Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 5 Jan 2025 13:46:33 +0100 Subject: [PATCH 05/27] feat: adds the fix2 wrapper. chore: small reordering. feat: nth is only throwing. with explicit errors. --- base/iterators.jl | 77 ++++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 28 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 780b85ae98253..4031293bd2e02 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1605,10 +1605,10 @@ pairs(s::AbstractString) = IterableStatePairs(s) """ nth(itr, n::Integer) -Get the `n`th element of an iterable collection. Return `nothing` if not existing. +Get the `n`th element of an iterable collection. Throw a `BoundsError`[@ref] if not existing. Will advance any `Stateful`[@ref] iterator. -See also: [`first`](@ref), [`last`](@ref) +See also: [`first`](@ref), [`last`](@ref), [`nth`](@ref) # Examples ```jldoctest @@ -1626,56 +1626,77 @@ julia> first(stateful) ``` """ nth(itr, n::Integer) = _nth(IteratorSize(itr), itr, n) -nth(itr::AbstractArray, n::Integer) = n > length(itr) ? nothing : _inbounds_nth(itr, n) -nth(itr::AbstractRange, n) = getindex(itr, n) +nth(itr::AbstractArray, n::Integer) = _withlength_nth_throw(itr, n, length(itr)) +# specialized versions to better interact with existing iterators +# Count +nth(itr::Count, n::Integer) = n > 0 ? itr.start + itr.step * (n - 1) : throw(ArgumentError("n must be positive.")) +# # Repeated +nth(itr::Repeated, n::Integer) = itr.x +# # Take(Repeated) +nth(itr::Take{Repeated{O}}, n::Integer) where {O} = begin + n > itr.n ? throw(BoundsError("attempted to access $(itr.n)-element $(typeof(itr)) at position $n")) : itr.xs.x +end +# infinite cycle +nth(itr::Cycle{I}, n::Integer) where {I} = _nth_inf_cycle(IteratorSize(I), itr, n) +# finite cycle: in reality a Flatten{Take{Repeated{O}}} iterator +nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} = _nth_finite_cycle(IteratorSize(O), itr, n) -_nth(::SizeUnknown, itr, n) = _fallback_nth(itr, n) -_nth(::Union{HasShape,HasLength}, itr, n) = _withlength_nth(itr, n, length(itr)) +_nth(::SizeUnknown, itr, n) = _fallback_nth_throw(itr, n) +_nth(::Union{HasShape,HasLength}, itr, n) = _withlength_nth_throw(itr, n, length(itr)) _nth(::IsInfinite, itr, n) = _inbounds_nth(itr, n) _inbounds_nth(itr, n) = iterate(drop(itr, n - 1))[1] _inbounds_nth(itr::AbstractArray, n) = itr[begin + n-1] -_withlength_nth(itr, n, N) = n > N ? nothing : _inbounds_nth(itr, n) +function _withlength_nth_throw(itr, n, N) + n > N && throw(BoundsError("attempted to access $N-element $(typeof(itr)) at position $n")) + _inbounds_nth(itr, n) +end -function _fallback_nth(itr, n) +function _fallback_nth_throw(itr, n) y = iterate(drop(itr, n - 1)) - y === nothing && return nothing + y === nothing && throw(BoundsError("Iterator $(typeof(itr)) has less than $n elements")) y[1] end -# specialized versions to better interact with existing iterators -# Count -nth(itr::Count, n::Integer) = n > 0 ? itr.start + itr.step * (n - 1) : nothing - -# Repeated -nth(itr::Repeated, n::Integer) = itr.x - -# Take(Repeated) -nth(itr::Take{Repeated{O}}, n::Integer) where {O} = n > itr.n ? nothing : itr.xs.x - -# infinite cycle -nth(itr::Cycle{I}, n::Integer) where {I} = _nth_inf_cycle(IteratorSize(I), itr, n) _nth_inf_cycle(::IsInfinite, itr, n) = _inbounds_nth(itr.xs, n) _nth_inf_cycle(::SizeUnknown, itr, n) = _fallback_nth(itr.xs, n) _nth_inf_cycle(::Union{HasShape,HasLength}, itr, n) = _repeating_cycle_nth(itr.xs, n, length(itr.xs)) -# finite cycle -# a finite cycle iterator is in reality a Flatten{Take{Repeated{O}}} iterator -nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} = _nth_finite_cycle(IteratorSize(O), itr, n) _nth_finite_cycle(::IsInfinite, itr, n) = _inbounds_nth(itr, n) -_nth_finite_cycle(::SizeUnknown, itr, n) = _fallback_nth(itr, n) -_nth_finite_cycle(::Union{HasShape,HasLength}, itr, n) = begin +_nth_finite_cycle(::SizeUnknown, itr, n) = _fallback_nth_throw(itr, n) +_nth_finite_cycle(::Union{HasShape,HasLength}, itr, n) = _walk_cycle_throw(itr, n) + +_repeating_cycle_nth(inner_itr, n, inner_N) = _inbounds_nth(inner_itr, 1 + ((n - 1) % inner_N)) + +function _walk_cycle_throw(itr, n) N = itr.it.n # `Take` iterator n torepeat = itr.it.xs.x # repeated object K = length(torepeat) - n > K * N && return nothing + n > K * N && throw(BoundsError("attempted to access $(N*K)-element $(typeof(itr)) at position $n")) _repeating_cycle_nth(torepeat, n, K) end +""" + nth(n::Integer) -_repeating_cycle_nth(inner_itr, n, inner_N) = _inbounds_nth(inner_itr, 1 + ((n - 1) % inner_N)) +return a function that gets the `n`-th element from any iterator passed to it. +Fixes the second element with +Throw a `BoundsError`[@ref] if not existing. +Will advance any `Stateful`[@ref] iterator. +See also: [`nth`](@ref), [`Base.Fix2`](@ref) +# Examples +```jldoctest +julia> fifth_element = nth(5) +(::Base.Fix2{typeof(nth), Int64}) (generic function with 1 method) +julia> fifth_element(reshape(1:30, (5,6))) +5 + +julia> map(nth(3), my_vec) +``` +""" +nth(n::Integer) = Base.Fix2(nth, n) end From 53b58f24e64ec593df413a3ef1b0d64b79788670 Mon Sep 17 00:00:00 2001 From: cschen Date: Fri, 10 Jan 2025 12:58:19 +0100 Subject: [PATCH 06/27] fix: better wording for the fix2 version --- base/iterators.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 4031293bd2e02..d8a38c8b37213 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1680,9 +1680,10 @@ end """ nth(n::Integer) -return a function that gets the `n`-th element from any iterator passed to it. -Fixes the second element with +Return a function that gets the `n`-th element from any iterator passed to it. Throw a `BoundsError`[@ref] if not existing. + +Fixes the second element. Equivalent to `Base.Fix2(nth, n)`. Will advance any `Stateful`[@ref] iterator. See also: [`nth`](@ref), [`Base.Fix2`](@ref) From 97f8104d139550b86dcb4c79df22ee91f8a7ad80 Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 23 Mar 2025 18:39:50 +0100 Subject: [PATCH 07/27] considerably cleaner implementation based on first(drop) --- base/iterators.jl | 70 ++++++++++++++++++----------------------------- test/iterators.jl | 2 +- 2 files changed, 27 insertions(+), 45 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index d8a38c8b37213..f829f71aed17a 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1625,58 +1625,40 @@ julia> first(stateful) 8 ``` """ -nth(itr, n::Integer) = _nth(IteratorSize(itr), itr, n) -nth(itr::AbstractArray, n::Integer) = _withlength_nth_throw(itr, n, length(itr)) -# specialized versions to better interact with existing iterators +nth(itr, n::Integer) = _nth(itr, n) + # Count nth(itr::Count, n::Integer) = n > 0 ? itr.start + itr.step * (n - 1) : throw(ArgumentError("n must be positive.")) -# # Repeated -nth(itr::Repeated, n::Integer) = itr.x -# # Take(Repeated) -nth(itr::Take{Repeated{O}}, n::Integer) where {O} = begin - n > itr.n ? throw(BoundsError("attempted to access $(itr.n)-element $(typeof(itr)) at position $n")) : itr.xs.x -end -# infinite cycle -nth(itr::Cycle{I}, n::Integer) where {I} = _nth_inf_cycle(IteratorSize(I), itr, n) -# finite cycle: in reality a Flatten{Take{Repeated{O}}} iterator -nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} = _nth_finite_cycle(IteratorSize(O), itr, n) - -_nth(::SizeUnknown, itr, n) = _fallback_nth_throw(itr, n) -_nth(::Union{HasShape,HasLength}, itr, n) = _withlength_nth_throw(itr, n, length(itr)) -_nth(::IsInfinite, itr, n) = _inbounds_nth(itr, n) - -_inbounds_nth(itr, n) = iterate(drop(itr, n - 1))[1] -_inbounds_nth(itr::AbstractArray, n) = itr[begin + n-1] - -function _withlength_nth_throw(itr, n, N) - n > N && throw(BoundsError("attempted to access $N-element $(typeof(itr)) at position $n")) - _inbounds_nth(itr, n) +# Repeated +nth(itr::Repeated, ::Integer) = itr.x +# Take(Repeated) +nth(itr::Take{Repeated}, n::Integer) = begin + n > itr.n ? throw(BoundsError(itr, n)) : nth(itr.xs) end -function _fallback_nth_throw(itr, n) - y = iterate(drop(itr, n - 1)) - y === nothing && throw(BoundsError("Iterator $(typeof(itr)) has less than $n elements")) - y[1] +# infinite cycle +nth(itr::Cycle{I}, n::Integer) where {I} = begin + if IteratorSize(I) isa Union{HasShape, HasLength} + _nth(itr.xs, mod1(n, length(itr.xs))) + else + _nth(itr, n) + end end -_nth_inf_cycle(::IsInfinite, itr, n) = _inbounds_nth(itr.xs, n) -_nth_inf_cycle(::SizeUnknown, itr, n) = _fallback_nth(itr.xs, n) -_nth_inf_cycle(::Union{HasShape,HasLength}, itr, n) = _repeating_cycle_nth(itr.xs, n, length(itr.xs)) - -_nth_finite_cycle(::IsInfinite, itr, n) = _inbounds_nth(itr, n) -_nth_finite_cycle(::SizeUnknown, itr, n) = _fallback_nth_throw(itr, n) -_nth_finite_cycle(::Union{HasShape,HasLength}, itr, n) = _walk_cycle_throw(itr, n) - -_repeating_cycle_nth(inner_itr, n, inner_N) = _inbounds_nth(inner_itr, 1 + ((n - 1) % inner_N)) - -function _walk_cycle_throw(itr, n) - N = itr.it.n # `Take` iterator n - torepeat = itr.it.xs.x # repeated object - K = length(torepeat) - n > K * N && throw(BoundsError("attempted to access $(N*K)-element $(typeof(itr)) at position $n")) - _repeating_cycle_nth(torepeat, n, K) +# finite cycle: in reality a Flatten{Take{Repeated{O}}} iterator +nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} = begin + if IteratorSize(O) isa Union{HasShape, HasLength} + cycles = itr.it.n + repeated = itr.it.xs.x + k = length(repeated) + n > k*cycles ? throw(BoundsError(itr, n)) : _nth(repeated, mod1(n, k)) + else + _nth(itr, n) + end end +@inline _nth(itr, n) = first(drop(itr, n-1)) +@inline _nth(itr::AbstractArray, n) = itr[begin + n-1] """ nth(n::Integer) diff --git a/test/iterators.jl b/test/iterators.jl index 0623ef81a5a46..02e7e95cbfecb 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1153,7 +1153,7 @@ end @testset "iter: $IT" for (IT, n, exp) in testset @test exp == nth(IT, n) IT isa Cycle && continue # cycles are infinite so never OOB - @test nth(IT, 999999999) === nothing + @test_throws Union{ArgumentError,BoundsError} nth(IT, 999999999) end end From 1bd0cdb882cd824782f0aa80878b0df1e8d5a9e5 Mon Sep 17 00:00:00 2001 From: cschen Date: Mon, 24 Mar 2025 20:24:51 +0100 Subject: [PATCH 08/27] fix tests --- base/iterators.jl | 3 +-- test/iterators.jl | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index f829f71aed17a..730a1c89e771b 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -6,7 +6,6 @@ Methods for working with Iterators. baremodule Iterators # small dance to make this work from Base or Intrinsics -import Base: @__MODULE__, parentmodule const Base = parentmodule(@__MODULE__) using .Base: @inline, Pair, Pairs, AbstractDict, IndexLinear, IndexStyle, AbstractVector, Vector, @@ -16,7 +15,7 @@ using .Base: (:), |, +, -, *, !==, !, ==, !=, <=, <, >, >=, =>, missing, any, _counttuple, eachindex, ntuple, zero, prod, reduce, in, firstindex, lastindex, tail, fieldtypes, min, max, minimum, zero, oneunit, promote, promote_shape, LazyString, - afoldl + afoldl, mod1 using .Core using Core: @doc diff --git a/test/iterators.jl b/test/iterators.jl index 02e7e95cbfecb..7e9467bd8027b 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1152,7 +1152,7 @@ end testset = zip(itrs, ns, expected) @testset "iter: $IT" for (IT, n, exp) in testset @test exp == nth(IT, n) - IT isa Cycle && continue # cycles are infinite so never OOB + IT isa Iterators.Cycle && continue # cycles are infinite so never OOB @test_throws Union{ArgumentError,BoundsError} nth(IT, 999999999) end end From 7422eca6d44f0af604707161886e99ce94df43e7 Mon Sep 17 00:00:00 2001 From: cschen Date: Mon, 24 Mar 2025 06:51:05 +0100 Subject: [PATCH 09/27] Update iterators.jl Co-authored-by: adienes <51664769+adienes@users.noreply.github.com> --- base/iterators.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 730a1c89e771b..87b456a39b6a1 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1631,8 +1631,8 @@ nth(itr::Count, n::Integer) = n > 0 ? itr.start + itr.step * (n - 1) : throw(Arg # Repeated nth(itr::Repeated, ::Integer) = itr.x # Take(Repeated) -nth(itr::Take{Repeated}, n::Integer) = begin - n > itr.n ? throw(BoundsError(itr, n)) : nth(itr.xs) +nth(itr::Take{Repeated{T}}, n::Integer) where T = begin + n > itr.n ? throw(BoundsError(itr, n)) : nth(itr.xs, n) end # infinite cycle From c49db01ba914285ddbe6a371a596036ebf63c10b Mon Sep 17 00:00:00 2001 From: cschen Date: Mon, 24 Mar 2025 21:48:45 +0100 Subject: [PATCH 10/27] fix doctests --- base/iterators.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 87b456a39b6a1..823d58e2cb154 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1611,13 +1611,13 @@ See also: [`first`](@ref), [`last`](@ref), [`nth`](@ref) # Examples ```jldoctest -julia> nth(2:2:10, 4) +julia> Iterators.nth(2:2:10, 4) 8 -julia> nth(reshape(1:30, (5,6)), 6) +julia> Iterators.nth(reshape(1:30, (5,6)), 6) 6 -julia> stateful = Stateful(1:10); nth(stateful, 7) +julia> stateful = Iterators.Stateful(1:10); nth(stateful, 7) 7 julia> first(stateful) @@ -1670,13 +1670,13 @@ Will advance any `Stateful`[@ref] iterator. See also: [`nth`](@ref), [`Base.Fix2`](@ref) # Examples ```jldoctest -julia> fifth_element = nth(5) +julia> fifth_element = Iterators.nth(5) (::Base.Fix2{typeof(nth), Int64}) (generic function with 1 method) julia> fifth_element(reshape(1:30, (5,6))) 5 -julia> map(nth(3), my_vec) +julia> map(Iterators.nth(3), my_vec) ``` """ nth(n::Integer) = Base.Fix2(nth, n) From 4e4967359cb4d088de370349cdfbd8cb32fbca20 Mon Sep 17 00:00:00 2001 From: cschen Date: Wed, 26 Mar 2025 13:28:29 +0100 Subject: [PATCH 11/27] Apply suggestions from code review Co-authored-by: Steven G. Johnson --- base/iterators.jl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 823d58e2cb154..94c277b12ea95 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1631,12 +1631,11 @@ nth(itr::Count, n::Integer) = n > 0 ? itr.start + itr.step * (n - 1) : throw(Arg # Repeated nth(itr::Repeated, ::Integer) = itr.x # Take(Repeated) -nth(itr::Take{Repeated{T}}, n::Integer) where T = begin +nth(itr::Take{<:Repeated}, n::Integer) = n > itr.n ? throw(BoundsError(itr, n)) : nth(itr.xs, n) -end # infinite cycle -nth(itr::Cycle{I}, n::Integer) where {I} = begin +function nth(itr::Cycle{I}, n::Integer) where {I} if IteratorSize(I) isa Union{HasShape, HasLength} _nth(itr.xs, mod1(n, length(itr.xs))) else @@ -1645,7 +1644,7 @@ nth(itr::Cycle{I}, n::Integer) where {I} = begin end # finite cycle: in reality a Flatten{Take{Repeated{O}}} iterator -nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} = begin +function nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} if IteratorSize(O) isa Union{HasShape, HasLength} cycles = itr.it.n repeated = itr.it.xs.x From 0082a01818c67993d573277e04b771dbd028ea86 Mon Sep 17 00:00:00 2001 From: cschen Date: Wed, 26 Mar 2025 22:45:44 +0100 Subject: [PATCH 12/27] shorten doc of single argument, and fix doctests --- base/iterators.jl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 94c277b12ea95..9c73d50b3dbb5 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1661,21 +1661,19 @@ end nth(n::Integer) Return a function that gets the `n`-th element from any iterator passed to it. -Throw a `BoundsError`[@ref] if not existing. - -Fixes the second element. Equivalent to `Base.Fix2(nth, n)`. -Will advance any `Stateful`[@ref] iterator. +Equivalent to `Base.Fix2(nth, n)` or `itr -> nth(itr, n)`. See also: [`nth`](@ref), [`Base.Fix2`](@ref) # Examples ```jldoctest julia> fifth_element = Iterators.nth(5) -(::Base.Fix2{typeof(nth), Int64}) (generic function with 1 method) +(::Base.Fix2{typeof(Base.Iterators.nth), Int64}) (generic function with 2 methods) julia> fifth_element(reshape(1:30, (5,6))) 5 -julia> map(Iterators.nth(3), my_vec) +julia> map(fifth_element, ("Willis", "Jovovich", "Oldman")) +('i', 'v', 'a') ``` """ nth(n::Integer) = Base.Fix2(nth, n) From 6a6cfa11ea5654071037280efbdca7db72f8bfd7 Mon Sep 17 00:00:00 2001 From: cschen Date: Tue, 6 May 2025 16:10:02 +0200 Subject: [PATCH 13/27] remove `@inline`s --- base/iterators.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 9c73d50b3dbb5..088036397ad9a 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1655,8 +1655,8 @@ function nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} end end -@inline _nth(itr, n) = first(drop(itr, n-1)) -@inline _nth(itr::AbstractArray, n) = itr[begin + n-1] +_nth(itr, n) = first(drop(itr, n-1)) +_nth(itr::AbstractArray, n) = itr[begin + n-1] """ nth(n::Integer) From 111a7e79b736a246bfbb9320c2ee8a361e2e587e Mon Sep 17 00:00:00 2001 From: cschen Date: Tue, 6 May 2025 16:30:55 +0200 Subject: [PATCH 14/27] add back line importing MODULE macro and parentmodule --- base/iterators.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/base/iterators.jl b/base/iterators.jl index 088036397ad9a..abe0f5ab02bfc 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -6,6 +6,7 @@ Methods for working with Iterators. baremodule Iterators # small dance to make this work from Base or Intrinsics +import Base: @__MODULE__, parentmodule const Base = parentmodule(@__MODULE__) using .Base: @inline, Pair, Pairs, AbstractDict, IndexLinear, IndexStyle, AbstractVector, Vector, From eb605f230346f1a771a83649d60057f57a3a92d1 Mon Sep 17 00:00:00 2001 From: cschen Date: Mon, 12 May 2025 20:06:33 +0200 Subject: [PATCH 15/27] remove unneeded specializations, throw the same error as `first(drop())` whitespace --- base/iterators.jl | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index abe0f5ab02bfc..ebafe77c8c5d8 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -991,6 +991,7 @@ end reverse(it::Cycle) = Cycle(reverse(it.xs)) last(it::Cycle) = last(it.xs) + # Repeated - repeat an object infinitely many times struct Repeated{O} @@ -1627,30 +1628,28 @@ julia> first(stateful) """ nth(itr, n::Integer) = _nth(itr, n) -# Count -nth(itr::Count, n::Integer) = n > 0 ? itr.start + itr.step * (n - 1) : throw(ArgumentError("n must be positive.")) -# Repeated -nth(itr::Repeated, ::Integer) = itr.x -# Take(Repeated) -nth(itr::Take{<:Repeated}, n::Integer) = - n > itr.n ? throw(BoundsError(itr, n)) : nth(itr.xs, n) - # infinite cycle function nth(itr::Cycle{I}, n::Integer) where {I} + n < 0 && throw(ArgumentError("Drop length must be non-negative")) + if IteratorSize(I) isa Union{HasShape, HasLength} - _nth(itr.xs, mod1(n, length(itr.xs))) + N = length(itr.xs) + N > 0 ? _nth(itr.xs, mod1(n, N)) : throw(ArgumentError("collection must be non-empty")) else _nth(itr, n) end end -# finite cycle: in reality a Flatten{Take{Repeated{O}}} iterator +# Flatten{Take{Repeated{O}}} is the actual type of an Iterators.cycle(iterable::O, m) iterator function nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} + n < 0 && throw(ArgumentError("Drop length must be non-negative")) + if IteratorSize(O) isa Union{HasShape, HasLength} cycles = itr.it.n repeated = itr.it.xs.x k = length(repeated) - n > k*cycles ? throw(BoundsError(itr, n)) : _nth(repeated, mod1(n, k)) + n > k*cycles ? + throw(ArgumentError("collection must be non-empty")) : _nth(repeated, mod1(n, k)) else _nth(itr, n) end From 320a7d49766e118ac126fb73133fe9d324d2b7f2 Mon Sep 17 00:00:00 2001 From: cschen Date: Tue, 13 May 2025 19:44:24 +0200 Subject: [PATCH 16/27] clean up tests and adds a few test cases --- test/iterators.jl | 48 +++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/test/iterators.jl b/test/iterators.jl index 7e9467bd8027b..4e45d722fa7a0 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1134,26 +1134,42 @@ end end @testset "nth" begin + Z = Array{Int,0}(undef) Z[] = 17 - itrs = Any[collect(1:1000), 10:6:1000, "∀ϵ>0", (1, 3, 5, 10, 78), reshape(1:30, (5, 6)), - Z, 3, true, 'x', 4=>5, view(Z), view(reshape(1:30, (5, 6)), 2:4, 2:6), - (x^2 for x in 1:10), Iterators.Filter(isodd, 1:10), Iterators.flatten((1:10, 50:60)), - pairs(50:60), zip(1:10, 21:30, 51:60), Iterators.product(1:3, 10:12), - Iterators.repeated(3.14159, 5), (a=2, b=3, c=5, d=7, e=11), Iterators.cycle(collect(1:100)), - Iterators.cycle([1, 2, 3, 4, 5], 5)] - ns = Any[ - 234, 123, 3, 2, 21, 1, 1, 1, 1, 1, 1, 10, 9, 3, 15, 7, 6, 3, 4, 4, 99999, 25 - ] - expected = Any[ - 234, 742, '>', 3, 21, 17, 3, true, 'x', 4, 17, 22, 81, 5, 54, (7=>56), (6, 26, 56), (3, 10), 3.14159, 7, 99, 5 - ] - @test length(itrs) == length(ns) == length(expected) + it_result_pairs = Dict( + (Z, 1) => 17, + (collect(1:1000), 234) => 234, + (10:6:1000, 123) => 10 + 6 * 122, + ("∀ϵ>0", 3) => '>', + ((1, 3, 5, 10, 78), 2) => 3, + (reshape(1:30, (5, 6)), 21) => 21, + (3, 1) => 3, + (true, 1) => true, + ('x', 1) => 'x', + (4 => 5, 2) => 5, + (view(Z), 1) => 17, + (view(reshape(1:30, (5, 6)), 2:4, 2:6), 10) => 22, + ((x^2 for x in 1:10), 9) => 81, + (Iterators.Filter(isodd, 1:10), 3) => 5, + (Iterators.flatten((1:10, 50:60)), 15) => 54, + (pairs(50:60), 7) => 7 => 56, + (zip(1:10, 21:30, 51:60), 6) => (6, 26, 56), + (Iterators.product(1:3, 10:12), 3) => (3, 10), + (Iterators.repeated(3.14159, 5), 4) => 3.14159, + ((a=2, b=3, c=5, d=7, e=11), 4) => 7, + (Iterators.cycle(collect(1:100)), 9999) => 99, + (Iterators.cycle([1, 2, 3, 4, 5], 5), 25) => 5 + (Iterators.cycle("String", 10), 16) => 'i', + (Iterators.cycle((), 1000)) => () + ) + testset = zip(itrs, ns, expected) - @testset "iter: $IT" for (IT, n, exp) in testset - @test exp == nth(IT, n) + @testset "iter: $IT" for (IT, n) in keys(it_result_pairs) + @test it_result_pairs[(IT, n)] == nth(IT, n) + IT isa Iterators.Cycle && continue # cycles are infinite so never OOB - @test_throws Union{ArgumentError,BoundsError} nth(IT, 999999999) + @test_throws Union{ArgumentError} nth(IT, 999999999) end end From 8e76592c27d73fc81ee76b29c19aba327dc899dc Mon Sep 17 00:00:00 2001 From: cschen Date: Thu, 29 May 2025 13:24:01 +0200 Subject: [PATCH 17/27] * decouple `nth` from `first` and `drop`, by manually write the unrolled version of `first(drop(itr, n-1))` * uniform the errors to be just `BoundsErrors`, instead of parrotting the cryptic "collection must be non empty" arising from the interplay between `first` and `drop`. * This would allow defining the generic `first` in terms of `nth(itr, 1)` in the future maybe * add check to prevent negative `n` in the cycle version, inheriting the error from the base version * adds propagate inbounds for the array version --- base/iterators.jl | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index ebafe77c8c5d8..95f051104a2ab 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1630,33 +1630,46 @@ nth(itr, n::Integer) = _nth(itr, n) # infinite cycle function nth(itr::Cycle{I}, n::Integer) where {I} - n < 0 && throw(ArgumentError("Drop length must be non-negative")) - if IteratorSize(I) isa Union{HasShape, HasLength} N = length(itr.xs) - N > 0 ? _nth(itr.xs, mod1(n, N)) : throw(ArgumentError("collection must be non-empty")) + N == 0 && throw(BoundsError(itr, n)) + + # prevent wrap around behaviour + return _nth(itr.xs, ifelse(n > 0, mod1(n, N), n)) else - _nth(itr, n) + return _nth(itr, n) end end # Flatten{Take{Repeated{O}}} is the actual type of an Iterators.cycle(iterable::O, m) iterator function nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} - n < 0 && throw(ArgumentError("Drop length must be non-negative")) - if IteratorSize(O) isa Union{HasShape, HasLength} cycles = itr.it.n - repeated = itr.it.xs.x - k = length(repeated) - n > k*cycles ? - throw(ArgumentError("collection must be non-empty")) : _nth(repeated, mod1(n, k)) + torepeat = itr.it.xs.x + k = length(torepeat) + (n > k*cycles || k == 0) && throw(BoundsError(itr, n)) + + # prevent wrap around behaviour + return _nth(torepeat, ifelse(n > 0, mod1(n, k), n)) else - _nth(itr, n) + return _nth(itr, n) + end +end + +Base.@propagate_inbounds _nth(itr::AbstractArray, n) = itr[begin + n-1] + +function _nth(itr, n) + # unrolled version of `first(drop)` + n > 0 || throw(ArgumentError("n must be positive")) + y = iterate(itr) + for i in 1:n-1 + y === nothing && break + y = iterate(itr, y[2]) end + y === nothing && throw(BoundsError(itr, n)) + y[1] end -_nth(itr, n) = first(drop(itr, n-1)) -_nth(itr::AbstractArray, n) = itr[begin + n-1] """ nth(n::Integer) From 8be01c0ea22bd9669d4d8716be475ff641e5285b Mon Sep 17 00:00:00 2001 From: cschen Date: Thu, 29 May 2025 13:26:34 +0200 Subject: [PATCH 18/27] * adds test to correctly throw on negative indices * adds test to correctly throw on empty cycles * fix typos in tests --- test/iterators.jl | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/iterators.jl b/test/iterators.jl index 4e45d722fa7a0..52b1146b96aa7 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1139,7 +1139,7 @@ end Z[] = 17 it_result_pairs = Dict( (Z, 1) => 17, - (collect(1:1000), 234) => 234, + (collect(1:100), 23) => 23, (10:6:1000, 123) => 10 + 6 * 122, ("∀ϵ>0", 3) => '>', ((1, 3, 5, 10, 78), 2) => 3, @@ -1159,18 +1159,21 @@ end (Iterators.repeated(3.14159, 5), 4) => 3.14159, ((a=2, b=3, c=5, d=7, e=11), 4) => 7, (Iterators.cycle(collect(1:100)), 9999) => 99, - (Iterators.cycle([1, 2, 3, 4, 5], 5), 25) => 5 + (Iterators.cycle([1, 2, 3, 4, 5], 5), 25) => 5, (Iterators.cycle("String", 10), 16) => 'i', - (Iterators.cycle((), 1000)) => () + (Iterators.cycle(((),)), 1000) => () ) - testset = zip(itrs, ns, expected) @testset "iter: $IT" for (IT, n) in keys(it_result_pairs) @test it_result_pairs[(IT, n)] == nth(IT, n) + @test_throws ArgumentError nth(IT, -42) IT isa Iterators.Cycle && continue # cycles are infinite so never OOB - @test_throws Union{ArgumentError} nth(IT, 999999999) + @test_throws BoundsError nth(IT, 999999999) end + + empty_cycle = Iterators.cycle([]) + @test_throws BoundsError nth(empty_cycle, 42) end @testset "Iterators docstrings" begin From 9afd84fad59cb75e48d7f8a2361d5fd91660b631 Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 31 May 2025 11:05:46 +0200 Subject: [PATCH 19/27] remove ifelse, use normal ternary change negative indices to be boundserrors to match array's behaviours fix docstrings tests --- base/iterators.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index 95f051104a2ab..a0ef18785c2b8 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1619,7 +1619,7 @@ julia> Iterators.nth(2:2:10, 4) julia> Iterators.nth(reshape(1:30, (5,6)), 6) 6 -julia> stateful = Iterators.Stateful(1:10); nth(stateful, 7) +julia> stateful = Iterators.Stateful(1:10); Iterators.nth(stateful, 7) 7 julia> first(stateful) @@ -1635,7 +1635,7 @@ function nth(itr::Cycle{I}, n::Integer) where {I} N == 0 && throw(BoundsError(itr, n)) # prevent wrap around behaviour - return _nth(itr.xs, ifelse(n > 0, mod1(n, N), n)) + return _nth(itr.xs, n > 0 ? mod1(n, N) : n) else return _nth(itr, n) end @@ -1650,7 +1650,7 @@ function nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} (n > k*cycles || k == 0) && throw(BoundsError(itr, n)) # prevent wrap around behaviour - return _nth(torepeat, ifelse(n > 0, mod1(n, k), n)) + return _nth(torepeat, n > 0 ? mod1(n, k) : n) else return _nth(itr, n) end @@ -1660,7 +1660,7 @@ Base.@propagate_inbounds _nth(itr::AbstractArray, n) = itr[begin + n-1] function _nth(itr, n) # unrolled version of `first(drop)` - n > 0 || throw(ArgumentError("n must be positive")) + n > 0 || throw(BoundsError(itr, n)) y = iterate(itr) for i in 1:n-1 y === nothing && break From 11ebb19e15518ac16d91e843873f9328af0559a9 Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 31 May 2025 13:15:29 +0200 Subject: [PATCH 20/27] fix tests to check for bouds error instead of argument error --- test/iterators.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/iterators.jl b/test/iterators.jl index 52b1146b96aa7..80ec3743ffb30 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1167,6 +1167,7 @@ end @testset "iter: $IT" for (IT, n) in keys(it_result_pairs) @test it_result_pairs[(IT, n)] == nth(IT, n) @test_throws ArgumentError nth(IT, -42) + @test_throws BoundsError nth(IT, -42) IT isa Iterators.Cycle && continue # cycles are infinite so never OOB @test_throws BoundsError nth(IT, 999999999) From 1bcaf4aba9dab724b1b47fa949523fca86633b06 Mon Sep 17 00:00:00 2001 From: cschen Date: Sat, 31 May 2025 18:33:04 +0200 Subject: [PATCH 21/27] fix leftover --- test/iterators.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/test/iterators.jl b/test/iterators.jl index 80ec3743ffb30..b7d5bfaa46359 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1166,7 +1166,6 @@ end @testset "iter: $IT" for (IT, n) in keys(it_result_pairs) @test it_result_pairs[(IT, n)] == nth(IT, n) - @test_throws ArgumentError nth(IT, -42) @test_throws BoundsError nth(IT, -42) IT isa Iterators.Cycle && continue # cycles are infinite so never OOB From cff3b8393fcf40777191a3ce3516a0a0c65cbcde Mon Sep 17 00:00:00 2001 From: cschen Date: Thu, 12 Jun 2025 23:00:04 +0200 Subject: [PATCH 22/27] added tests for sizeunknown path for cycles --- base/iterators.jl | 6 +++--- test/iterators.jl | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index a0ef18785c2b8..a836145d00c06 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1634,7 +1634,7 @@ function nth(itr::Cycle{I}, n::Integer) where {I} N = length(itr.xs) N == 0 && throw(BoundsError(itr, n)) - # prevent wrap around behaviour + # prevents wrap around behaviour and inherit the error handling return _nth(itr.xs, n > 0 ? mod1(n, N) : n) else return _nth(itr, n) @@ -1649,7 +1649,7 @@ function nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} k = length(torepeat) (n > k*cycles || k == 0) && throw(BoundsError(itr, n)) - # prevent wrap around behaviour + # prevent wrap around behaviour and inherit the error handling return _nth(torepeat, n > 0 ? mod1(n, k) : n) else return _nth(itr, n) @@ -1662,7 +1662,7 @@ function _nth(itr, n) # unrolled version of `first(drop)` n > 0 || throw(BoundsError(itr, n)) y = iterate(itr) - for i in 1:n-1 + for _ in 1:n-1 y === nothing && break y = iterate(itr, y[2]) end diff --git a/test/iterators.jl b/test/iterators.jl index b7d5bfaa46359..15e8ed84e179b 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1164,6 +1164,7 @@ end (Iterators.cycle(((),)), 1000) => () ) + @testset "iter: $IT" for (IT, n) in keys(it_result_pairs) @test it_result_pairs[(IT, n)] == nth(IT, n) @test_throws BoundsError nth(IT, -42) @@ -1174,6 +1175,21 @@ end empty_cycle = Iterators.cycle([]) @test_throws BoundsError nth(empty_cycle, 42) + + # test the size unknown branch for cycles + # only generate odd numbers so we know the actual length + # but the iterator is still SizeUnknown() + it_size_unknown = Iterators.filter(isodd, 1:2:10) + @test IteratorSize(it_size_unknown) isa Base.SizeUnknown + @test length(collect(it_size_unknown)) == 5 + + cycle_size_unknown = Iterators.cycle(it_size_unknown) + finite_cycle_size_unknown = Iterators.cycle(it_size_unknown, 5) + @test nth(cycle_size_unknown, 2) == 3 + @test nth(cycle_size_unknown, 20) == 9 # mod1(20, 5) = 5, wraps 4 times + @test nth(finite_cycle_size_unknown, 2) == 3 + @test nth(finite_cycle_size_unknown, 20) == 9 + @test_throws BoundsError nth(finite_cycle_size_unknown, 30) # only wraps 5 times, max n is 5 * 5 = 25 end @testset "Iterators docstrings" begin From 0b05ca079a88aac9f54b6d556e1f071286fd2e65 Mon Sep 17 00:00:00 2001 From: cschen Date: Fri, 13 Jun 2025 19:20:59 +0200 Subject: [PATCH 23/27] fix tests --- test/iterators.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/iterators.jl b/test/iterators.jl index 15e8ed84e179b..0c9ae438d73a7 100644 --- a/test/iterators.jl +++ b/test/iterators.jl @@ -1180,7 +1180,7 @@ end # only generate odd numbers so we know the actual length # but the iterator is still SizeUnknown() it_size_unknown = Iterators.filter(isodd, 1:2:10) - @test IteratorSize(it_size_unknown) isa Base.SizeUnknown + @test Base.IteratorSize(it_size_unknown) isa Base.SizeUnknown @test length(collect(it_size_unknown)) == 5 cycle_size_unknown = Iterators.cycle(it_size_unknown) From 3e522c78cc2f36a77515e134c3a4ee01f62c32ab Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 15 Jun 2025 16:41:10 +0200 Subject: [PATCH 24/27] reintroduce the traits methods to dispatch on iteratorsize instead of union splitting. --- base/iterators.jl | 45 ++++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/base/iterators.jl b/base/iterators.jl index a836145d00c06..9670996ee709a 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1626,39 +1626,31 @@ julia> first(stateful) 8 ``` """ -nth(itr, n::Integer) = _nth(itr, n) +nth(itr, n::Integer) = _nth(IteratorSize(itr), itr, n) +nth(itr::Cycle{I}, n::Integer) where I = _nth(IteratorSize(I), itr, n) +nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where O = _nth(IteratorSize(O), itr, n) +@propagate_inbounds nth(itr::AbstractArray, n::Integer) = itr[begin + n - 1] -# infinite cycle -function nth(itr::Cycle{I}, n::Integer) where {I} - if IteratorSize(I) isa Union{HasShape, HasLength} - N = length(itr.xs) - N == 0 && throw(BoundsError(itr, n)) +function _nth(::Union{HasShape, HasLength}, itr::Cycle{I}, n::Integer) where {I} + N = length(itr.xs) + N == 0 && throw(BoundsError(itr, n)) - # prevents wrap around behaviour and inherit the error handling - return _nth(itr.xs, n > 0 ? mod1(n, N) : n) - else - return _nth(itr, n) - end + # prevents wrap around behaviour and inherit the error handling + return nth(itr.xs, n > 0 ? mod1(n, N) : n) end # Flatten{Take{Repeated{O}}} is the actual type of an Iterators.cycle(iterable::O, m) iterator -function nth(itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} - if IteratorSize(O) isa Union{HasShape, HasLength} - cycles = itr.it.n - torepeat = itr.it.xs.x - k = length(torepeat) - (n > k*cycles || k == 0) && throw(BoundsError(itr, n)) - - # prevent wrap around behaviour and inherit the error handling - return _nth(torepeat, n > 0 ? mod1(n, k) : n) - else - return _nth(itr, n) - end -end +function _nth(::Union{HasShape, HasLength}, itr::Flatten{Take{Repeated{O}}}, n::Integer) where {O} + cycles = itr.it.n + torepeat = itr.it.xs.x + k = length(torepeat) + (n > k*cycles || k == 0) && throw(BoundsError(itr, n)) -Base.@propagate_inbounds _nth(itr::AbstractArray, n) = itr[begin + n-1] + # prevent wrap around behaviour and inherit the error handling + return nth(torepeat, n > 0 ? mod1(n, k) : n) +end -function _nth(itr, n) +function _nth(::IteratorSize, itr, n::Integer) # unrolled version of `first(drop)` n > 0 || throw(BoundsError(itr, n)) y = iterate(itr) @@ -1669,7 +1661,6 @@ function _nth(itr, n) y === nothing && throw(BoundsError(itr, n)) y[1] end - """ nth(n::Integer) From 526905338492a337e3a8cc9eda1bce1fce43190d Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 15 Jun 2025 17:51:11 +0200 Subject: [PATCH 25/27] Update base/iterators.jl remove self reference in docstrings Co-authored-by: Andy Dienes <51664769+adienes@users.noreply.github.com> --- base/iterators.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/iterators.jl b/base/iterators.jl index 9670996ee709a..53d7b28316ee1 100644 --- a/base/iterators.jl +++ b/base/iterators.jl @@ -1609,7 +1609,7 @@ pairs(s::AbstractString) = IterableStatePairs(s) Get the `n`th element of an iterable collection. Throw a `BoundsError`[@ref] if not existing. Will advance any `Stateful`[@ref] iterator. -See also: [`first`](@ref), [`last`](@ref), [`nth`](@ref) +See also: [`first`](@ref), [`last`](@ref) # Examples ```jldoctest From 5d262149d25951da2a593f60be2a9ec418c5c28d Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 15 Jun 2025 18:04:15 +0200 Subject: [PATCH 26/27] update NEWS.md file --- NEWS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.md b/NEWS.md index 1bd8f16f13b76..f1c6122adf4d5 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,7 @@ New language features --------------------- - New `Base.@acquire` macro for a non-closure version of `Base.acquire(f, s::Base.Semaphore)`, like `@lock`. ([#56845]) + - New `nth` function to access the `n`-th element of a generic iterable. Also comes with a single argument version to define a function that access always the same element ([#56580]) Language changes ---------------- From 6f76d649f27118d46027f526f6a80b1cdd888da0 Mon Sep 17 00:00:00 2001 From: cschen Date: Sun, 15 Jun 2025 18:31:20 +0200 Subject: [PATCH 27/27] Fix NEWS.md Co-authored-by: Andy Dienes <51664769+adienes@users.noreply.github.com> --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index 3ceed8a7e865b..55b5a09d5b525 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,7 +5,7 @@ New language features --------------------- - New `Base.@acquire` macro for a non-closure version of `Base.acquire(f, s::Base.Semaphore)`, like `@lock`. ([#56845]) - - New `nth` function to access the `n`-th element of a generic iterable. Also comes with a single argument version to define a function that access always the same element ([#56580]) + - New `nth` function to access the `n`-th element of a generic iterable. ([#56580]) - The character U+1F8B2 🢲 (RIGHTWARDS ARROW WITH LOWER HOOK), newly added by Unicode 16, is now a valid operator with arrow precedence, accessible as `\hookunderrightarrow` at the REPL. ([JuliaLang/JuliaSyntax.jl#525], [#57143])