Skip to content

Commit 29c1bc2

Browse files
committed
factor: Add factor.py
1 parent abd1302 commit 29c1bc2

File tree

1 file changed

+143
-0
lines changed

1 file changed

+143
-0
lines changed

src/factor.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
#!/usr/bin/python3
2+
3+
import math
4+
import sys
5+
from optparse import OptionParser
6+
from typing import Generator, Iterable
7+
8+
# List of small primes greater than 2; used for lookup.
9+
SMALL_PRIMES = [3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
10+
11+
12+
def miller_rabin(n: int) -> bool:
13+
"""
14+
A deterministic version of the Miller-Rabin primality test.
15+
"""
16+
d = n - 1
17+
s = 0
18+
19+
while not d & 1:
20+
d >>= 1
21+
s += 1
22+
23+
for a in (x % n + 2 for x in range(50)):
24+
if pow(a, d, n) == 1:
25+
continue
26+
27+
for i in range(s):
28+
if pow(a, 2**i * d, n) == n - 1:
29+
break
30+
else:
31+
return False
32+
33+
return True
34+
35+
36+
def pollards_rho(n: int, step: int) -> int | None:
37+
"""
38+
Pollard's rho algorithm for factorization.
39+
40+
Assumes that n is an odd integer greater than 2.
41+
"""
42+
x = y = 2
43+
factor = 1
44+
while factor == 1:
45+
x = (x * x + step) % n
46+
y = ((y * y + step) ** 2 + step) % n
47+
factor = math.gcd(abs(x - y) % n, n)
48+
49+
return factor
50+
51+
52+
def factorize(
53+
n: int, cache: dict[int, int] = {p: p for p in SMALL_PRIMES}
54+
) -> Generator[int]:
55+
"""
56+
Generates prime factors of the integer n.
57+
58+
Results may not be in strictly ascending order.
59+
"""
60+
while n > 1:
61+
factor: int | None = None
62+
63+
if not n & 1:
64+
yield (factor := 2)
65+
elif factor := cache.get(n):
66+
yield factor
67+
else:
68+
if miller_rabin(n):
69+
cache[n] = factor = n
70+
yield factor
71+
else:
72+
for i in range(1, n - 1):
73+
if (factor := pollards_rho(n, i)) and factor != n:
74+
yield from factorize(min(n, factor), cache)
75+
break
76+
77+
if factor == n:
78+
break
79+
80+
n //= factor
81+
82+
83+
def format_exponents(factors: Iterable[int]) -> str:
84+
processed: list[str] = []
85+
86+
last_factor = 0
87+
exp = 0
88+
89+
for factor in factors:
90+
if exp and last_factor != factor:
91+
processed.append(f"{last_factor}{"" if exp == 1 else f"^{exp}"}")
92+
exp = 1
93+
else:
94+
exp += 1
95+
96+
last_factor = factor
97+
98+
processed.append(f"{last_factor}{"" if exp == 1 else f"^{exp}"}")
99+
100+
return " ".join(processed)
101+
102+
103+
def factor(opts, numbers: list[int]):
104+
try:
105+
for n in numbers:
106+
if n < 2:
107+
print(f"{n}:")
108+
continue
109+
110+
factors = sorted(factorize(n))
111+
112+
print(
113+
f"{n}: {format_exponents(factors) if opts.exponents
114+
else " ".join(map(str, factors))}"
115+
)
116+
except KeyboardInterrupt:
117+
print()
118+
sys.exit(130)
119+
120+
121+
if __name__ == "__main__":
122+
parser = OptionParser(
123+
usage="Usage: %prog [OPTION] [NUMBER]...", add_help_option=False
124+
)
125+
parser.add_option("--help", action="help", help="show usage information and exit")
126+
127+
parser.add_option("-h", "--exponents", action="store_true")
128+
129+
opts, args = parser.parse_args()
130+
131+
numbers: list[int] = []
132+
133+
for arg in args:
134+
try:
135+
num = int(arg)
136+
if num < 0:
137+
raise ValueError
138+
except ValueError:
139+
parser.error(f"'{arg}' is not a valid positive integer")
140+
141+
numbers.append(num)
142+
143+
factor(opts, numbers)

0 commit comments

Comments
 (0)