From 7580f64a5d215ad443a9b9baa483a13f2ffb6375 Mon Sep 17 00:00:00 2001 From: s-celles Date: Tue, 17 Mar 2026 08:09:41 +0100 Subject: [PATCH] feat: add lazy prime iterator API (primes_from, primes_upto) Add PrimeIterator{T, Up} unified parametric type with primes_from(n) for ascending and primes_upto(n) for descending lazy iteration over primes. Fully composable with Base.Iterators (take, takewhile, zip, etc.). Closes JuliaMath/Primes.jl#176. --- .gitignore | 61 +++++++++++++++++++++++-- docs/src/api.md | 3 ++ src/Primes.jl | 114 ++++++++++++++++++++++++++++++++++++++++++++++- test/runtests.jl | 93 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 267 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 9847c84..9dfd7d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,62 @@ +* + +### Allowed files and directories ### + +!.gitignore +!.github/ +!.github/workflows/ +!.github/workflows/*.yml +!.github/dependabot.yml + +!LICENSE.md +!README.md +!Project.toml + +!src/ +!src/*.jl + +!test/ +!test/*.jl + +!docs/ +!docs/src/ +!docs/src/*.md +!docs/make.jl +!docs/Project.toml + +!benchmarks/ +!benchmarks/*.jl +!benchmarks/Project.toml +!benchmarks/README.md + +### Denied even if allowed above ### + +# Files generated by invoking Julia with --code-coverage *.jl.cov *.jl.*.cov + +# Files generated by invoking Julia with --track-allocation *.jl.mem -Manifest.toml -docs/build -docs/site + +# System-specific files and directories generated by the BinaryProvider and BinDeps packages +# They contain absolute paths specific to the host computer, and so should not be committed +deps/deps.jl +deps/build.log +deps/downloads/ +deps/usr/ +deps/src/ + +# Build artifacts for creating documentation generated by the Documenter package +docs/build/ +docs/site/ docs/Manifest.toml + +# File generated by Pkg, the package manager, based on a corresponding Project.toml +# It records a fixed state of all packages used by the project. As such, it should not be +# committed for packages, but should be committed for applications that require a static +# environment. +Manifest*.toml + +# File generated by the Preferences package to store local preferences +LocalPreferences.toml +JuliaLocalPreferences.toml \ No newline at end of file diff --git a/docs/src/api.md b/docs/src/api.md index df79a59..e5d2caa 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -19,6 +19,9 @@ Primes.primes Primes.nextprime Primes.prevprime Primes.prime +Primes.primes_from +Primes.primes_upto +Primes.PrimeIterator ``` ## Identifying prime numbers diff --git a/src/Primes.jl b/src/Primes.jl index b95fdac..cb73b4e 100644 --- a/src/Primes.jl +++ b/src/Primes.jl @@ -9,7 +9,8 @@ using Base.Checked: checked_neg using IntegerMathUtils export isprime, primes, primesmask, factor, eachfactor, divisors, ismersenneprime, isrieselprime, - nextprime, nextprimes, prevprime, prevprimes, prime, prodfactors, radical, totient + nextprime, nextprimes, prevprime, prevprimes, prime, prodfactors, radical, totient, + primes_from, primes_upto, PrimeIterator include("factorization.jl") @@ -968,6 +969,117 @@ julia> prevprimes(10, 10) prevprimes(start::T, n::Integer) where {T<:Integer} = collect(T, Iterators.take(prevprimes(start), n)) +""" + PrimeIterator{T<:Integer, Up} + +A lazy iterator over prime numbers. When `Up` is `true`, iterates in ascending order +(constructed via [`primes_from`](@ref)). When `Up` is `false`, iterates in descending +order (constructed via [`primes_upto`](@ref)). + +The type parameter `T` determines the integer type of yielded primes, and `Up` is a +compile-time boolean controlling iteration direction. +""" +struct PrimeIterator{T<:Integer, Up} + start::T + function PrimeIterator{T, Up}(start::T) where {T<:Integer, Up} + start < zero(start) && throw(ArgumentError("start must be non-negative, got $start")) + new{T, Up}(start) + end +end + +function iterate(it::PrimeIterator{T, true}, state=it.start) where {T} + p = nextprime(state) + (p, add(p, one(p))) +end + +function iterate(it::PrimeIterator{T, false}, state=it.start) where {T} + state < T(2) && return nothing + p = prevprime(state) + (p, p - one(p)) +end + +IteratorSize(::Type{<:PrimeIterator{T, true}}) where {T} = Base.IsInfinite() +IteratorSize(::Type{<:PrimeIterator{T, false}}) where {T} = Base.SizeUnknown() +IteratorEltype(::Type{<:PrimeIterator}) = Base.HasEltype() +eltype(::Type{PrimeIterator{T, Up}}) where {T, Up} = T + +function Base.show(io::IO, it::PrimeIterator{T, true}) where {T} + print(io, "primes_from(", it.start, ")") +end + +function Base.show(io::IO, it::PrimeIterator{T, false}) where {T} + print(io, "primes_upto(", it.start, ")") +end + +""" + primes_from(n::Integer) + +Return a lazy, infinite iterator over all primes greater than or equal to `n`, +in ascending order. + +# Examples + +```jldoctest +julia> collect(Iterators.take(primes_from(10), 5)) +5-element Vector{Int64}: + 11 + 13 + 17 + 19 + 23 + +julia> first(primes_from(100)) +101 +``` +""" +primes_from(n::T) where {T<:Integer} = PrimeIterator{T, true}(n) + +""" + primes_from() + +Return a lazy, infinite iterator over all primes starting from `2`. +Equivalent to `primes_from(2)`. + +# Examples + +```jldoctest +julia> collect(Iterators.take(primes_from(), 5)) +5-element Vector{Int64}: + 2 + 3 + 5 + 7 + 11 +``` +""" +primes_from() = primes_from(2) + +""" + primes_upto(n::Integer) + +Return a lazy iterator over all primes less than or equal to `n`, +in descending order. Terminates when no more primes remain. + +# Examples + +```jldoctest +julia> collect(primes_upto(20)) +8-element Vector{Int64}: + 19 + 17 + 13 + 11 + 7 + 5 + 3 + 2 + +julia> collect(primes_upto(1)) +Int64[] +``` +""" +primes_upto(n::T) where {T<:Integer} = PrimeIterator{T, false}(n) + """ divisors(n::Integer) -> Vector diff --git a/test/runtests.jl b/test/runtests.jl index f63e59a..e30c7e5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -505,6 +505,99 @@ end @test Base.IteratorSize(prevprimes(10)) == Base.SizeUnknown() end +@testset "PrimeIterator - primes_from" begin + @test first(primes_from(10)) == 11 + @test first(primes_from(11)) == 11 + @test collect(Iterators.take(primes_from(10), 4)) == [11, 13, 17, 19] + + @test first(primes_from()) == 2 + @test collect(Iterators.take(primes_from(), 5)) == [2, 3, 5, 7, 11] + + @test primes_from(10) isa PrimeIterator{Int, true} + @test primes_from(Int32(10)) isa PrimeIterator{Int32, true} + @test primes_from(big(10)) isa PrimeIterator{BigInt, true} + + @test Base.IteratorSize(primes_from(10)) == Base.IsInfinite() + + @test eltype(primes_from(10)) == Int + @test eltype(primes_from(Int32(10))) == Int32 + @test eltype(primes_from(big(10))) == BigInt + + @test collect(Iterators.take(primes_from(100), 3)) == [101, 103, 107] + + @test collect(Iterators.takewhile(p -> p < 20, primes_from(10))) == [11, 13, 17, 19] + + for n in [2, 3, 10, 100, 1000] + @test first(primes_from(n)) == nextprime(n) + end + + @test repr(primes_from(10)) == "primes_from(10)" + @test repr(primes_from(big(42))) == "primes_from(42)" + + @test typeof(primes_from(10)) == PrimeIterator{Int, true} + + @test_throws ArgumentError primes_from(-1) + @test_throws ArgumentError primes_from(-10) + + for n in [2, 5, 10, 100] + for i in 1:5 + @test nextprime(n, i) == collect(Iterators.take(primes_from(n), i))[end] + end + end +end + +@testset "PrimeIterator - primes_upto" begin + @test first(primes_upto(10)) == 7 + @test first(primes_upto(11)) == 11 + @test collect(Iterators.take(primes_upto(20), 4)) == [19, 17, 13, 11] + + @test primes_upto(10) isa PrimeIterator{Int, false} + @test primes_upto(Int32(10)) isa PrimeIterator{Int32, false} + @test primes_upto(big(10)) isa PrimeIterator{BigInt, false} + + @test Base.IteratorSize(primes_upto(10)) == Base.SizeUnknown() + @test collect(primes_upto(10)) == [7, 5, 3, 2] + @test collect(primes_upto(2)) == [2] + + @test eltype(primes_upto(10)) == Int + @test eltype(primes_upto(Int32(10))) == Int32 + @test eltype(primes_upto(big(10))) == BigInt + + @test collect(Iterators.take(primes_upto(100), 3)) == [97, 89, 83] + + pairs = collect(Iterators.take(Iterators.zip(primes_from(10), primes_upto(100)), 3)) + @test pairs == [(11, 97), (13, 89), (17, 83)] + + for n in [2, 3, 10, 100, 1000] + @test first(primes_upto(n)) == prevprime(n) + end + + @test repr(primes_upto(10)) == "primes_upto(10)" + @test repr(primes_upto(big(42))) == "primes_upto(42)" + + @test_throws ArgumentError primes_upto(-1) + @test_throws ArgumentError primes_upto(-10) + + @test collect(primes_upto(1)) == [] + @test collect(primes_upto(0)) == [] + + big_primes = collect(Iterators.take(primes_upto(big(100)), 3)) + @test big_primes == [prevprime(big(100), i) for i in 1:3] +end + +@testset "PrimeIterator - overflow propagation" begin + @test_throws OverflowError collect(Iterators.take(primes_from(typemax(Int) - 1), 2)) +end + +@testset "PrimeIterator - existing API unchanged" begin + @test nextprime(10) == 11 + @test nextprime(10, 3) == 17 + @test prevprime(10) == 7 + @test prevprime(10, 3) == 3 + @test nextprimes(10, 3) == [11, 13, 17] + @test prevprimes(10, 3) == [7, 5, 3] +end + @testset "primes with huge arguments" begin if Base.Sys.WORD_SIZE == 64 @test primes(2^63-200, 2^63-1) == [9223372036854775643, 9223372036854775783]