numclassify 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- numclassify/__init__.py +63 -0
- numclassify/__main__.py +4 -0
- numclassify/_core/__init__.py +0 -0
- numclassify/_core/combinatorial.py +392 -0
- numclassify/_core/digital.py +403 -0
- numclassify/_core/divisors.py +756 -0
- numclassify/_core/figurate.py +357 -0
- numclassify/_core/number_theory.py +533 -0
- numclassify/_core/powers.py +349 -0
- numclassify/_core/primes.py +2100 -0
- numclassify/_core/recreational.py +245 -0
- numclassify/_core/sequences.py +488 -0
- numclassify/_registry.py +417 -0
- numclassify/cli.py +525 -0
- numclassify-0.1.0.dist-info/METADATA +220 -0
- numclassify-0.1.0.dist-info/RECORD +19 -0
- numclassify-0.1.0.dist-info/WHEEL +4 -0
- numclassify-0.1.0.dist-info/entry_points.txt +2 -0
- numclassify-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numclassify/_core/number_theory.py
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Number-theoretic classifiers plus math utility functions.
|
|
5
|
+
|
|
6
|
+
Registered classifiers appear in the global registry.
|
|
7
|
+
Math utilities (euler_totient, mobius, gcd, lcm, mod_pow, mod_inv) are
|
|
8
|
+
NOT registered but are importable by other modules.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import math
|
|
13
|
+
from typing import List
|
|
14
|
+
|
|
15
|
+
from numclassify._registry import register
|
|
16
|
+
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
# Math utilities (NOT registered)
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
def euler_totient(n: int) -> int:
|
|
22
|
+
"""Return Euler's totient φ(n): count of integers 1..n coprime to n.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
n : int
|
|
27
|
+
|
|
28
|
+
Returns
|
|
29
|
+
-------
|
|
30
|
+
int
|
|
31
|
+
"""
|
|
32
|
+
if n < 1:
|
|
33
|
+
return 0
|
|
34
|
+
result = n
|
|
35
|
+
p = 2
|
|
36
|
+
temp = n
|
|
37
|
+
while p * p <= temp:
|
|
38
|
+
if temp % p == 0:
|
|
39
|
+
while temp % p == 0:
|
|
40
|
+
temp //= p
|
|
41
|
+
result -= result // p
|
|
42
|
+
p += 1
|
|
43
|
+
if temp > 1:
|
|
44
|
+
result -= result // temp
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def mobius(n: int) -> int:
|
|
49
|
+
"""Return the Möbius function μ(n).
|
|
50
|
+
|
|
51
|
+
Returns 0 if n has a squared prime factor, otherwise (-1)^k
|
|
52
|
+
where k is the number of distinct prime factors.
|
|
53
|
+
|
|
54
|
+
Parameters
|
|
55
|
+
----------
|
|
56
|
+
n : int
|
|
57
|
+
|
|
58
|
+
Returns
|
|
59
|
+
-------
|
|
60
|
+
int
|
|
61
|
+
"""
|
|
62
|
+
if n < 1:
|
|
63
|
+
return 0
|
|
64
|
+
if n == 1:
|
|
65
|
+
return 1
|
|
66
|
+
k = 0
|
|
67
|
+
f = 2
|
|
68
|
+
while f * f <= n:
|
|
69
|
+
if n % f == 0:
|
|
70
|
+
k += 1
|
|
71
|
+
n //= f
|
|
72
|
+
if n % f == 0:
|
|
73
|
+
return 0 # squared factor
|
|
74
|
+
f += 1
|
|
75
|
+
if n > 1:
|
|
76
|
+
k += 1
|
|
77
|
+
return (-1) ** k
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def gcd(a: int, b: int) -> int:
|
|
81
|
+
"""Return the greatest common divisor of a and b.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
a : int
|
|
86
|
+
b : int
|
|
87
|
+
|
|
88
|
+
Returns
|
|
89
|
+
-------
|
|
90
|
+
int
|
|
91
|
+
"""
|
|
92
|
+
return math.gcd(a, b)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def lcm(a: int, b: int) -> int:
|
|
96
|
+
"""Return the least common multiple of a and b.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
a : int
|
|
101
|
+
b : int
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
int
|
|
106
|
+
"""
|
|
107
|
+
if a == 0 or b == 0:
|
|
108
|
+
return 0
|
|
109
|
+
return abs(a * b) // math.gcd(a, b)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def mod_pow(base: int, exp: int, mod: int) -> int:
|
|
113
|
+
"""Return base^exp % mod using fast modular exponentiation.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
base : int
|
|
118
|
+
exp : int
|
|
119
|
+
mod : int
|
|
120
|
+
|
|
121
|
+
Returns
|
|
122
|
+
-------
|
|
123
|
+
int
|
|
124
|
+
"""
|
|
125
|
+
return pow(base, exp, mod)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def mod_inv(a: int, mod: int) -> int:
|
|
129
|
+
"""Return the modular inverse of a mod m (requires gcd(a,m)=1).
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
a : int
|
|
134
|
+
mod : int
|
|
135
|
+
|
|
136
|
+
Returns
|
|
137
|
+
-------
|
|
138
|
+
int
|
|
139
|
+
"""
|
|
140
|
+
return pow(a, -1, mod)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
# Internal helpers
|
|
145
|
+
# ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
def _is_prime(n: int) -> bool:
|
|
148
|
+
"""Miller-Rabin primality (local to avoid circular import)."""
|
|
149
|
+
if n < 2:
|
|
150
|
+
return False
|
|
151
|
+
if n < 4:
|
|
152
|
+
return True
|
|
153
|
+
if n % 2 == 0 or n % 3 == 0:
|
|
154
|
+
return False
|
|
155
|
+
witnesses = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
|
|
156
|
+
d, r = n - 1, 0
|
|
157
|
+
while d % 2 == 0:
|
|
158
|
+
d //= 2
|
|
159
|
+
r += 1
|
|
160
|
+
for a in witnesses:
|
|
161
|
+
if a >= n:
|
|
162
|
+
continue
|
|
163
|
+
x = pow(a, d, n)
|
|
164
|
+
if x == 1 or x == n - 1:
|
|
165
|
+
continue
|
|
166
|
+
for _ in range(r - 1):
|
|
167
|
+
x = x * x % n
|
|
168
|
+
if x == n - 1:
|
|
169
|
+
break
|
|
170
|
+
else:
|
|
171
|
+
return False
|
|
172
|
+
return True
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _digit_sum(n: int) -> int:
|
|
176
|
+
return sum(int(d) for d in str(n))
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Registered classifiers
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
@register(name="Evil", category="number theory", oeis="A001969",
|
|
184
|
+
description="Has an even number of 1s in its binary representation.")
|
|
185
|
+
def is_evil(n: int) -> bool:
|
|
186
|
+
"""Return True if n has an even number of 1-bits (evil number).
|
|
187
|
+
|
|
188
|
+
Parameters
|
|
189
|
+
----------
|
|
190
|
+
n : int
|
|
191
|
+
|
|
192
|
+
Returns
|
|
193
|
+
-------
|
|
194
|
+
bool
|
|
195
|
+
|
|
196
|
+
Examples
|
|
197
|
+
--------
|
|
198
|
+
>>> is_evil(9) # 1001 -> two 1s
|
|
199
|
+
True
|
|
200
|
+
>>> is_evil(7) # 111 -> three 1s
|
|
201
|
+
False
|
|
202
|
+
"""
|
|
203
|
+
return n >= 0 and bin(n).count("1") % 2 == 0
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@register(name="Odious", category="number theory", oeis="A000069",
|
|
207
|
+
description="Has an odd number of 1s in its binary representation.")
|
|
208
|
+
def is_odious(n: int) -> bool:
|
|
209
|
+
"""Return True if n has an odd number of 1-bits (odious number).
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
n : int
|
|
214
|
+
|
|
215
|
+
Returns
|
|
216
|
+
-------
|
|
217
|
+
bool
|
|
218
|
+
"""
|
|
219
|
+
return n >= 0 and bin(n).count("1") % 2 == 1
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@register(name="Pernicious", category="number theory", oeis="A052294",
|
|
223
|
+
description="The number of 1s in binary representation is prime.")
|
|
224
|
+
def is_pernicious(n: int) -> bool:
|
|
225
|
+
"""Return True if the popcount of n is a prime number.
|
|
226
|
+
|
|
227
|
+
Parameters
|
|
228
|
+
----------
|
|
229
|
+
n : int
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
bool
|
|
234
|
+
"""
|
|
235
|
+
return n >= 0 and _is_prime(bin(n).count("1"))
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@register(name="Blum Integer", category="number theory", oeis="A016105",
|
|
239
|
+
description="Semiprime n=p*q where both p and q are ≡ 3 mod 4.")
|
|
240
|
+
def is_blum_integer(n: int) -> bool:
|
|
241
|
+
"""Return True if n is a Blum integer (semiprime p*q, p≡q≡3 mod 4, p≠q).
|
|
242
|
+
|
|
243
|
+
Parameters
|
|
244
|
+
----------
|
|
245
|
+
n : int
|
|
246
|
+
|
|
247
|
+
Returns
|
|
248
|
+
-------
|
|
249
|
+
bool
|
|
250
|
+
"""
|
|
251
|
+
if n < 15:
|
|
252
|
+
return False
|
|
253
|
+
# Find first prime factor
|
|
254
|
+
f = 2
|
|
255
|
+
while f * f <= n:
|
|
256
|
+
if n % f == 0:
|
|
257
|
+
p = f
|
|
258
|
+
q = n // f
|
|
259
|
+
# Must be semiprime: q must be prime and p != q
|
|
260
|
+
if q != p and _is_prime(p) and _is_prime(q):
|
|
261
|
+
return p % 4 == 3 and q % 4 == 3
|
|
262
|
+
return False
|
|
263
|
+
f += 1
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
@register(name="Carmichael", category="number theory", oeis="A002997",
|
|
268
|
+
description="Composite n satisfying Fermat's little theorem for all coprime bases.")
|
|
269
|
+
def is_carmichael(n: int) -> bool:
|
|
270
|
+
"""Return True if n is a Carmichael number (Korselt's criterion).
|
|
271
|
+
|
|
272
|
+
n is Carmichael iff: composite, squarefree, and for every prime p|n, (p-1)|(n-1).
|
|
273
|
+
|
|
274
|
+
Parameters
|
|
275
|
+
----------
|
|
276
|
+
n : int
|
|
277
|
+
|
|
278
|
+
Returns
|
|
279
|
+
-------
|
|
280
|
+
bool
|
|
281
|
+
|
|
282
|
+
Examples
|
|
283
|
+
--------
|
|
284
|
+
>>> is_carmichael(561)
|
|
285
|
+
True
|
|
286
|
+
"""
|
|
287
|
+
if n < 2 or _is_prime(n):
|
|
288
|
+
return False
|
|
289
|
+
# Must be squarefree
|
|
290
|
+
temp = n
|
|
291
|
+
f = 2
|
|
292
|
+
factors: List[int] = []
|
|
293
|
+
while f * f <= temp:
|
|
294
|
+
if temp % f == 0:
|
|
295
|
+
factors.append(f)
|
|
296
|
+
temp //= f
|
|
297
|
+
if temp % f == 0:
|
|
298
|
+
return False # not squarefree
|
|
299
|
+
f += 1
|
|
300
|
+
if temp > 1:
|
|
301
|
+
factors.append(temp)
|
|
302
|
+
# Korselt: (p-1) | (n-1) for each prime factor p
|
|
303
|
+
for p in factors:
|
|
304
|
+
if (n - 1) % (p - 1) != 0:
|
|
305
|
+
return False
|
|
306
|
+
return len(factors) >= 2
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@register(name="Primary Pseudoprime", category="number theory", oeis="A001567",
|
|
310
|
+
description="Composite number that passes the Fermat test for base 2.")
|
|
311
|
+
def is_primary_pseudoprime(n: int) -> bool:
|
|
312
|
+
"""Return True if n is a base-2 Fermat pseudoprime (composite, 2^(n-1) ≡ 1 mod n).
|
|
313
|
+
|
|
314
|
+
Parameters
|
|
315
|
+
----------
|
|
316
|
+
n : int
|
|
317
|
+
|
|
318
|
+
Returns
|
|
319
|
+
-------
|
|
320
|
+
bool
|
|
321
|
+
"""
|
|
322
|
+
if n < 4 or _is_prime(n):
|
|
323
|
+
return False
|
|
324
|
+
return pow(2, n - 1, n) == 1
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@register(name="Perfect Totient", category="number theory", oeis="A082897",
|
|
328
|
+
description="n equals the sum of its iterated totients until reaching 1.")
|
|
329
|
+
def is_perfect_totient(n: int) -> bool:
|
|
330
|
+
"""Return True if n equals the sum of iterated Euler totients until reaching 1.
|
|
331
|
+
|
|
332
|
+
Parameters
|
|
333
|
+
----------
|
|
334
|
+
n : int
|
|
335
|
+
|
|
336
|
+
Returns
|
|
337
|
+
-------
|
|
338
|
+
bool
|
|
339
|
+
"""
|
|
340
|
+
if n < 3:
|
|
341
|
+
return False
|
|
342
|
+
total = 0
|
|
343
|
+
k = euler_totient(n)
|
|
344
|
+
while k >= 1:
|
|
345
|
+
total += k
|
|
346
|
+
if total > n:
|
|
347
|
+
return False
|
|
348
|
+
if k == 1:
|
|
349
|
+
break
|
|
350
|
+
k = euler_totient(k)
|
|
351
|
+
return total == n
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@register(name="Giuga", category="number theory", oeis="A007850",
|
|
355
|
+
description="Composite n where for each prime p|n, p divides (n/p - 1).")
|
|
356
|
+
def is_giuga(n: int) -> bool:
|
|
357
|
+
"""Return True if n is a Giuga number.
|
|
358
|
+
|
|
359
|
+
n is Giuga iff composite and for each prime p dividing n, p | (n/p - 1).
|
|
360
|
+
|
|
361
|
+
Parameters
|
|
362
|
+
----------
|
|
363
|
+
n : int
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
bool
|
|
368
|
+
"""
|
|
369
|
+
if n < 2 or _is_prime(n):
|
|
370
|
+
return False
|
|
371
|
+
temp = n
|
|
372
|
+
f = 2
|
|
373
|
+
while f * f <= temp:
|
|
374
|
+
if temp % f == 0:
|
|
375
|
+
if (n // f - 1) % f != 0:
|
|
376
|
+
return False
|
|
377
|
+
while temp % f == 0:
|
|
378
|
+
temp //= f
|
|
379
|
+
f += 1
|
|
380
|
+
if temp > 1:
|
|
381
|
+
if (n // temp - 1) % temp != 0:
|
|
382
|
+
return False
|
|
383
|
+
return True
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
@register(name="Self Number", category="number theory", oeis="A003052",
|
|
387
|
+
description="Cannot be expressed as k + digit_sum(k) for any k.")
|
|
388
|
+
def is_self_number(n: int) -> bool:
|
|
389
|
+
"""Return True if n is a self number (Colombian number).
|
|
390
|
+
|
|
391
|
+
n is self iff no integer k satisfies k + digit_sum(k) = n.
|
|
392
|
+
|
|
393
|
+
Parameters
|
|
394
|
+
----------
|
|
395
|
+
n : int
|
|
396
|
+
|
|
397
|
+
Returns
|
|
398
|
+
-------
|
|
399
|
+
bool
|
|
400
|
+
|
|
401
|
+
Examples
|
|
402
|
+
--------
|
|
403
|
+
>>> is_self_number(20)
|
|
404
|
+
True
|
|
405
|
+
>>> is_self_number(21)
|
|
406
|
+
False
|
|
407
|
+
"""
|
|
408
|
+
if n < 1:
|
|
409
|
+
return False
|
|
410
|
+
# k + digit_sum(k) = n => k is in range [n - 9*len(str(n)), n)
|
|
411
|
+
digits_n = len(str(n))
|
|
412
|
+
for k in range(max(1, n - 9 * digits_n), n):
|
|
413
|
+
if k + _digit_sum(k) == n:
|
|
414
|
+
return False
|
|
415
|
+
return True
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
@register(name="Colombian", category="number theory", oeis="A003052",
|
|
419
|
+
description="Alias for self number: cannot be expressed as k + digit_sum(k).")
|
|
420
|
+
def is_colombian(n: int) -> bool:
|
|
421
|
+
"""Return True if n is a Colombian (self) number.
|
|
422
|
+
|
|
423
|
+
Parameters
|
|
424
|
+
----------
|
|
425
|
+
n : int
|
|
426
|
+
|
|
427
|
+
Returns
|
|
428
|
+
-------
|
|
429
|
+
bool
|
|
430
|
+
"""
|
|
431
|
+
return is_self_number(n)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
@register(name="Keith", category="number theory", oeis="A007629",
|
|
435
|
+
description="Starts with its own digits; each subsequent term is sum of previous n terms.")
|
|
436
|
+
def is_keith(n: int) -> bool:
|
|
437
|
+
"""Return True if n is a Keith number.
|
|
438
|
+
|
|
439
|
+
Example: 14 → sequence 1,4,5,9,14 (each term = sum of previous 2).
|
|
440
|
+
|
|
441
|
+
Parameters
|
|
442
|
+
----------
|
|
443
|
+
n : int
|
|
444
|
+
|
|
445
|
+
Returns
|
|
446
|
+
-------
|
|
447
|
+
bool
|
|
448
|
+
|
|
449
|
+
Examples
|
|
450
|
+
--------
|
|
451
|
+
>>> is_keith(14)
|
|
452
|
+
True
|
|
453
|
+
>>> is_keith(15)
|
|
454
|
+
False
|
|
455
|
+
"""
|
|
456
|
+
if n < 10:
|
|
457
|
+
return False
|
|
458
|
+
digits = [int(d) for d in str(n)]
|
|
459
|
+
seq = list(digits)
|
|
460
|
+
while seq[-1] < n:
|
|
461
|
+
seq.append(sum(seq[-len(digits):]))
|
|
462
|
+
return seq[-1] == n
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
@register(name="Autobiographical", category="number theory", oeis="A046043",
|
|
466
|
+
description="Digit i counts how many times i appears in n.")
|
|
467
|
+
def is_autobiographical(n: int) -> bool:
|
|
468
|
+
"""Return True if n is an autobiographical (self-describing) number.
|
|
469
|
+
|
|
470
|
+
Digit at position i (0-indexed) equals the count of digit i in n.
|
|
471
|
+
|
|
472
|
+
Parameters
|
|
473
|
+
----------
|
|
474
|
+
n : int
|
|
475
|
+
|
|
476
|
+
Returns
|
|
477
|
+
-------
|
|
478
|
+
bool
|
|
479
|
+
|
|
480
|
+
Examples
|
|
481
|
+
--------
|
|
482
|
+
>>> is_autobiographical(1210)
|
|
483
|
+
True
|
|
484
|
+
>>> is_autobiographical(1211)
|
|
485
|
+
False
|
|
486
|
+
"""
|
|
487
|
+
s = str(n)
|
|
488
|
+
k = len(s)
|
|
489
|
+
for i, ch in enumerate(s):
|
|
490
|
+
if s.count(str(i)) != int(ch):
|
|
491
|
+
return False
|
|
492
|
+
return True
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
@register(name="Narcissistic", category="number theory", oeis="A005188",
|
|
496
|
+
description="Alias for Armstrong: sum of digits each raised to number of digits equals n.")
|
|
497
|
+
def is_narcissistic(n: int) -> bool:
|
|
498
|
+
"""Return True if n is a narcissistic (Armstrong) number.
|
|
499
|
+
|
|
500
|
+
Parameters
|
|
501
|
+
----------
|
|
502
|
+
n : int
|
|
503
|
+
|
|
504
|
+
Returns
|
|
505
|
+
-------
|
|
506
|
+
bool
|
|
507
|
+
"""
|
|
508
|
+
digits = str(n)
|
|
509
|
+
k = len(digits)
|
|
510
|
+
return sum(int(d) ** k for d in digits) == n
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@register(name="Perfect Digital Invariant", category="number theory", oeis="A023052",
|
|
514
|
+
description="Sum of digits raised to some fixed power k equals n.")
|
|
515
|
+
def is_perfect_digital_invariant(n: int) -> bool:
|
|
516
|
+
"""Return True if sum(d^k for d in digits(n)) == n for some k >= 1.
|
|
517
|
+
|
|
518
|
+
Parameters
|
|
519
|
+
----------
|
|
520
|
+
n : int
|
|
521
|
+
|
|
522
|
+
Returns
|
|
523
|
+
-------
|
|
524
|
+
bool
|
|
525
|
+
"""
|
|
526
|
+
if n < 1:
|
|
527
|
+
return False
|
|
528
|
+
digits = [int(d) for d in str(n)]
|
|
529
|
+
max_k = len(str(n)) + 3 # generous upper bound
|
|
530
|
+
for k in range(1, max_k + 1):
|
|
531
|
+
if sum(d ** k for d in digits) == n:
|
|
532
|
+
return True
|
|
533
|
+
return False
|