primes.py
View source code here on GitHub!
Includes
- python.src.lib.primes.primes(stop: int | None = None) Iterator[int]
Iterates over the prime numbers up to an (optional) limit, with caching.
This iterator leverages the
modified_eratosthenes()
iterator, but adds additional features. The biggest is astop
argument, which will prevent it from yielding any numbers larger than that. The next is caching of any yielded prime numbers.
- python.src.lib.primes.modified_eratosthenes() Iterator[int]
Iterates over prime numbers using the Sieve of Eratosthenes.
This function implements the Sieve of Eratosthenes. In general, it will iterate over all of the prime numbers. Below is a gif (courtesy of Wikipedia) that demonstrates the principle of the sieve.
- python.src.lib.primes.prime_factors(num: int) Iterator[int]
Iterates over the prime factors of a number.
- python.src.lib.primes.is_prime(num: int, count: int = 1, distinct: bool = False) bool
Detects if a number is prime or not.
If a count other than 1 is given, it returns True only if the number has exactly count prime factors.
- python.src.lib.primes.primes_and_negatives(*args: int) Iterator[int]
Iterate over the primes and their negatives using
primes()
.
1from itertools import count, filterfalse, takewhile
2from math import ceil, sqrt
3from pathlib import Path
4from typing import Callable, Collection, Dict, Iterator, Optional
5
6from sortedcontainers import SortedSet
7from umsgpack import load
8
9cache_filename = 'primes_cache.mpack'
10
11try:
12 with Path(__file__).parent.joinpath(cache_filename).open('rb') as f:
13 cache = SortedSet(load(f))
14except Exception: # pragma: no cover
15 cache = SortedSet([
16 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61
17 ])
18last_cached: int = cache[-1]
19
20
21def primes(stop: Optional[int] = None) -> Iterator[int]:
22 """Iterates over the prime numbers up to an (optional) limit, with caching.
23
24 This iterator leverages the :py:func:`modified_eratosthenes` iterator, but adds
25 additional features. The biggest is a ``stop`` argument, which will prevent it
26 from yielding any numbers larger than that. The next is caching of any yielded
27 prime numbers."""
28 if stop is None:
29 yield from cache
30 else:
31 yield from takewhile(stop.__gt__, cache)
32 global last_cached
33 if stop and last_cached > stop:
34 return
35 if stop is None:
36 secondary = modified_eratosthenes()
37 else:
38 secondary = takewhile(stop.__gt__, modified_eratosthenes())
39 for p in filterfalse(cache.__contains__, secondary):
40 cache.add(p)
41 last_cached = p
42 yield p
43
44
45def modified_eratosthenes() -> Iterator[int]:
46 """Iterates over prime numbers using the Sieve of Eratosthenes.
47
48 This function implements the `Sieve of Eratosthenes <https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes>`_. In
49 general, it will iterate over all of the prime numbers. Below is a gif (courtesy of Wikipedia) that demonstrates
50 the principle of the sieve.
51
52 .. image:: https://upload.wikimedia.org/wikipedia/commons/9/94/Animation_Sieve_of_Eratosth.gif
53 :alt: Any animated example of the Sieve of Eratosthenes"""
54 # modified_eratosthenes taken, refactored from https://stackoverflow.com/a/19391111
55 yield from (2, 3, 5, 7)
56 sieve: Dict[int, int] = {}
57 recurse = primes()
58 next(recurse)
59 prime = next(recurse)
60 if prime != 3:
61 raise ValueError() # pragma: no cover
62 prime_squared = prime * prime
63 for candidate in count(9, 2):
64 if candidate in sieve: # if c is a multiple of some base prime, or
65 step = sieve.pop(candidate)
66 elif candidate < prime_squared: # prime, or
67 yield candidate
68 continue
69 else: # the next base prime's square
70 # if candidate != prime_squared:
71 # raise ValueError()
72 step = prime * 2
73 prime = next(recurse)
74 prime_squared = prime * prime
75 candidate += step # the next multiple
76 while candidate in sieve: # make sure to not overwrite another sieve entry
77 candidate += step
78 sieve[candidate] = step
79
80
81def prime_factors(num: int) -> Iterator[int]:
82 """Iterates over the prime factors of a number."""
83 if num < 0:
84 yield -1
85 num = -num
86 if num == 0:
87 yield 0
88 else:
89 root = ceil(sqrt(num))
90 for factor in primes():
91 modulo = num % factor
92 if modulo == 0:
93 while modulo == 0: # double-check to call sqrt once
94 yield factor
95 num //= factor
96 modulo = num % factor
97 root = ceil(sqrt(num))
98 if num <= 1:
99 break
100 if factor > root:
101 yield num
102 break
103
104
105def is_prime(
106 num: int,
107 count: int = 1,
108 distinct: bool = False
109) -> bool:
110 """Detects if a number is prime or not.
111
112 If a count other than 1 is given, it returns True only if the number has
113 exactly count prime factors."""
114 if num in (0, 1):
115 return False
116 factors = iter(prime_factors(num))
117 if count == 1:
118 if num in cache: # always has 2
119 return True
120 if num % 2 == 0:
121 return False
122 return next(factors) == num
123 seen: Collection[Optional[int]]
124 seen_add: Callable[[Optional[int]], None]
125 if distinct:
126 seen = set()
127 seen_add = seen.add
128 else:
129 seen = []
130 seen_add = seen.append
131 while None not in seen and count != len(seen):
132 seen_add(next(factors, None))
133 return None not in seen and next(factors, None) is None
134
135
136def primes_and_negatives(*args: int) -> Iterator[int]:
137 """Iterate over the primes and their negatives using :py:func:`primes`."""
138 for p in primes(*args):
139 yield p
140 yield -p