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,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numclassify._core.digital
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Digital (digit-based) number classification functions.
|
|
5
|
+
|
|
6
|
+
Helper functions ``digit_sum`` and ``digital_root`` are provided as plain
|
|
7
|
+
module-level utilities and are **not** registered (they return integers, not
|
|
8
|
+
booleans). All boolean predicates are registered via ``@register``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from functools import reduce
|
|
14
|
+
from typing import List
|
|
15
|
+
|
|
16
|
+
from numclassify._registry import register
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Helpers (not registered — return int, not bool)
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
def digit_sum(n: int) -> int:
|
|
24
|
+
"""Return the sum of the decimal digits of *n*.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
n:
|
|
29
|
+
Non-negative integer.
|
|
30
|
+
|
|
31
|
+
Returns
|
|
32
|
+
-------
|
|
33
|
+
int
|
|
34
|
+
|
|
35
|
+
Example
|
|
36
|
+
-------
|
|
37
|
+
>>> digit_sum(493)
|
|
38
|
+
16
|
|
39
|
+
>>> digit_sum(0)
|
|
40
|
+
0
|
|
41
|
+
"""
|
|
42
|
+
return sum(int(d) for d in str(abs(n)))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def digital_root(n: int) -> int:
|
|
46
|
+
"""Return the digital root of *n* (repeated digit sum until single digit).
|
|
47
|
+
|
|
48
|
+
For *n* = 0 the digital root is 0. For *n* > 0, ``digital_root(n)`` is
|
|
49
|
+
equivalent to ``1 + (n - 1) % 9``.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
n:
|
|
54
|
+
Non-negative integer.
|
|
55
|
+
|
|
56
|
+
Returns
|
|
57
|
+
-------
|
|
58
|
+
int in [0, 9]
|
|
59
|
+
|
|
60
|
+
Example
|
|
61
|
+
-------
|
|
62
|
+
>>> digital_root(493)
|
|
63
|
+
7
|
|
64
|
+
>>> digital_root(0)
|
|
65
|
+
0
|
|
66
|
+
"""
|
|
67
|
+
if n == 0:
|
|
68
|
+
return 0
|
|
69
|
+
return 1 + (n - 1) % 9
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
# Boolean predicates — all registered
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
@register(
|
|
77
|
+
name="Armstrong",
|
|
78
|
+
category="digital",
|
|
79
|
+
oeis="A005188",
|
|
80
|
+
description=(
|
|
81
|
+
"A number equal to the sum of its own digits each raised to the power "
|
|
82
|
+
"of the number of digits (narcissistic number)."
|
|
83
|
+
),
|
|
84
|
+
aliases=["narcissistic", "pluperfect digital invariant"],
|
|
85
|
+
)
|
|
86
|
+
def is_armstrong(n: int) -> bool:
|
|
87
|
+
"""Return ``True`` if *n* is an Armstrong (narcissistic) number.
|
|
88
|
+
|
|
89
|
+
For a *d*-digit number, ``is_armstrong(n)`` iff
|
|
90
|
+
``n == sum(digit^d for digit in digits(n))``.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
n:
|
|
95
|
+
Non-negative integer.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
bool
|
|
100
|
+
|
|
101
|
+
Example
|
|
102
|
+
-------
|
|
103
|
+
>>> is_armstrong(153) # 1^3 + 5^3 + 3^3 = 1 + 125 + 27 = 153
|
|
104
|
+
True
|
|
105
|
+
>>> is_armstrong(370)
|
|
106
|
+
True
|
|
107
|
+
>>> is_armstrong(100)
|
|
108
|
+
False
|
|
109
|
+
"""
|
|
110
|
+
if n < 0:
|
|
111
|
+
return False
|
|
112
|
+
digits = [int(d) for d in str(n)]
|
|
113
|
+
power = len(digits)
|
|
114
|
+
return sum(d ** power for d in digits) == n
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@register(
|
|
118
|
+
name="Spy",
|
|
119
|
+
category="digital",
|
|
120
|
+
oeis="A059716",
|
|
121
|
+
description=(
|
|
122
|
+
"A number whose digit sum equals its digit product. "
|
|
123
|
+
"All single-digit numbers are spy numbers (sum = digit = product)."
|
|
124
|
+
),
|
|
125
|
+
aliases=["spy number"],
|
|
126
|
+
)
|
|
127
|
+
def is_spy(n: int) -> bool:
|
|
128
|
+
"""Return ``True`` if *n* is a spy number.
|
|
129
|
+
|
|
130
|
+
A spy number satisfies ``digit_sum(n) == digit_product(n)``.
|
|
131
|
+
Single-digit numbers (0–9) trivially qualify because their digit sum and
|
|
132
|
+
digit product are both equal to the digit itself.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
n:
|
|
137
|
+
Non-negative integer.
|
|
138
|
+
|
|
139
|
+
Returns
|
|
140
|
+
-------
|
|
141
|
+
bool
|
|
142
|
+
|
|
143
|
+
Example
|
|
144
|
+
-------
|
|
145
|
+
>>> is_spy(1124) # 1+1+2+4=8 and 1*1*2*4=8
|
|
146
|
+
True
|
|
147
|
+
>>> is_spy(5) # single digit: sum=5=product
|
|
148
|
+
True
|
|
149
|
+
>>> is_spy(123) # sum=6, product=6 → True
|
|
150
|
+
True
|
|
151
|
+
>>> is_spy(124) # sum=7, product=8 → False
|
|
152
|
+
False
|
|
153
|
+
"""
|
|
154
|
+
if n < 0:
|
|
155
|
+
return False
|
|
156
|
+
digits = [int(d) for d in str(n)]
|
|
157
|
+
s = sum(digits)
|
|
158
|
+
p = 1
|
|
159
|
+
for d in digits:
|
|
160
|
+
p *= d
|
|
161
|
+
return s == p
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@register(
|
|
165
|
+
name="Harshad",
|
|
166
|
+
category="digital",
|
|
167
|
+
oeis="A005349",
|
|
168
|
+
description=(
|
|
169
|
+
"A number divisible by its own digit sum (also called a Niven number)."
|
|
170
|
+
),
|
|
171
|
+
aliases=["niven", "harshad number", "niven number"],
|
|
172
|
+
)
|
|
173
|
+
def is_harshad(n: int) -> bool:
|
|
174
|
+
"""Return ``True`` if *n* is a Harshad (Niven) number.
|
|
175
|
+
|
|
176
|
+
A Harshad number is divisible by its digit sum: ``n % digit_sum(n) == 0``.
|
|
177
|
+
|
|
178
|
+
Parameters
|
|
179
|
+
----------
|
|
180
|
+
n:
|
|
181
|
+
Positive integer.
|
|
182
|
+
|
|
183
|
+
Returns
|
|
184
|
+
-------
|
|
185
|
+
bool
|
|
186
|
+
|
|
187
|
+
Example
|
|
188
|
+
-------
|
|
189
|
+
>>> is_harshad(18) # digit_sum=9, 18 % 9 == 0
|
|
190
|
+
True
|
|
191
|
+
>>> is_harshad(19) # digit_sum=10, 19 % 10 != 0
|
|
192
|
+
False
|
|
193
|
+
"""
|
|
194
|
+
if n <= 0:
|
|
195
|
+
return False
|
|
196
|
+
ds = digit_sum(n)
|
|
197
|
+
if ds == 0:
|
|
198
|
+
return False
|
|
199
|
+
return n % ds == 0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@register(
|
|
203
|
+
name="Disarium",
|
|
204
|
+
category="digital",
|
|
205
|
+
oeis="A032799",
|
|
206
|
+
description=(
|
|
207
|
+
"A number equal to the sum of its digits each raised to the power of "
|
|
208
|
+
"their 1-based position from the left."
|
|
209
|
+
),
|
|
210
|
+
aliases=["disarium number"],
|
|
211
|
+
)
|
|
212
|
+
def is_disarium(n: int) -> bool:
|
|
213
|
+
"""Return ``True`` if *n* is a Disarium number.
|
|
214
|
+
|
|
215
|
+
For digits ``d₁ d₂ … dₖ`` (1-indexed from the left),
|
|
216
|
+
``is_disarium(n)`` iff ``n == d₁¹ + d₂² + … + dₖᵏ``.
|
|
217
|
+
|
|
218
|
+
Parameters
|
|
219
|
+
----------
|
|
220
|
+
n:
|
|
221
|
+
Non-negative integer.
|
|
222
|
+
|
|
223
|
+
Returns
|
|
224
|
+
-------
|
|
225
|
+
bool
|
|
226
|
+
|
|
227
|
+
Example
|
|
228
|
+
-------
|
|
229
|
+
>>> is_disarium(135) # 1^1 + 3^2 + 5^3 = 1 + 9 + 125 = 135
|
|
230
|
+
True
|
|
231
|
+
>>> is_disarium(89) # 8^1 + 9^2 = 8 + 81 = 89
|
|
232
|
+
True
|
|
233
|
+
>>> is_disarium(136)
|
|
234
|
+
False
|
|
235
|
+
"""
|
|
236
|
+
if n < 0:
|
|
237
|
+
return False
|
|
238
|
+
digits = [int(d) for d in str(n)]
|
|
239
|
+
return sum(d ** (i + 1) for i, d in enumerate(digits)) == n
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
@register(
|
|
243
|
+
name="Happy",
|
|
244
|
+
category="digital",
|
|
245
|
+
oeis="A007770",
|
|
246
|
+
description=(
|
|
247
|
+
"A number that eventually reaches 1 under iteration of the sum-of-"
|
|
248
|
+
"squared-digits map."
|
|
249
|
+
),
|
|
250
|
+
aliases=["happy number"],
|
|
251
|
+
)
|
|
252
|
+
def is_happy(n: int) -> bool:
|
|
253
|
+
"""Return ``True`` if *n* is a happy number.
|
|
254
|
+
|
|
255
|
+
Starting from *n*, repeatedly replace the number by the sum of the squares
|
|
256
|
+
of its digits. If the process reaches 1, *n* is happy. Otherwise it
|
|
257
|
+
enters a cycle not including 1 (the cycle always contains 4 for unhappy
|
|
258
|
+
numbers).
|
|
259
|
+
|
|
260
|
+
Cycle detection uses a *seen* set.
|
|
261
|
+
|
|
262
|
+
Parameters
|
|
263
|
+
----------
|
|
264
|
+
n:
|
|
265
|
+
Positive integer.
|
|
266
|
+
|
|
267
|
+
Returns
|
|
268
|
+
-------
|
|
269
|
+
bool
|
|
270
|
+
|
|
271
|
+
Example
|
|
272
|
+
-------
|
|
273
|
+
>>> is_happy(19) # 1²+9²=82 → 8²+2²=68 → … → 1
|
|
274
|
+
True
|
|
275
|
+
>>> is_happy(4) # enters cycle: 4→16→37→58→89→145→42→20→4
|
|
276
|
+
False
|
|
277
|
+
"""
|
|
278
|
+
if n <= 0:
|
|
279
|
+
return False
|
|
280
|
+
seen = set()
|
|
281
|
+
current = n
|
|
282
|
+
while current != 1:
|
|
283
|
+
if current in seen:
|
|
284
|
+
return False
|
|
285
|
+
seen.add(current)
|
|
286
|
+
current = sum(int(d) ** 2 for d in str(current))
|
|
287
|
+
return True
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@register(
|
|
291
|
+
name="Neon",
|
|
292
|
+
category="digital",
|
|
293
|
+
oeis="A208854",
|
|
294
|
+
description=(
|
|
295
|
+
"A number whose digit sum equals the number itself when applied to n²."
|
|
296
|
+
),
|
|
297
|
+
aliases=["neon number"],
|
|
298
|
+
)
|
|
299
|
+
def is_neon(n: int) -> bool:
|
|
300
|
+
"""Return ``True`` if *n* is a neon number.
|
|
301
|
+
|
|
302
|
+
A neon number satisfies ``digit_sum(n²) == n``.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
n:
|
|
307
|
+
Non-negative integer.
|
|
308
|
+
|
|
309
|
+
Returns
|
|
310
|
+
-------
|
|
311
|
+
bool
|
|
312
|
+
|
|
313
|
+
Example
|
|
314
|
+
-------
|
|
315
|
+
>>> is_neon(9) # 9² = 81, digit_sum(81) = 9
|
|
316
|
+
True
|
|
317
|
+
>>> is_neon(1) # 1² = 1, digit_sum(1) = 1
|
|
318
|
+
True
|
|
319
|
+
>>> is_neon(2) # 2² = 4, digit_sum(4) = 4 ≠ 2
|
|
320
|
+
False
|
|
321
|
+
"""
|
|
322
|
+
if n < 0:
|
|
323
|
+
return False
|
|
324
|
+
return digit_sum(n * n) == n
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@register(
|
|
328
|
+
name="Duck",
|
|
329
|
+
category="digital",
|
|
330
|
+
oeis="",
|
|
331
|
+
description=(
|
|
332
|
+
"A positive number that contains the digit 0 but does not start with 0."
|
|
333
|
+
),
|
|
334
|
+
aliases=["duck number"],
|
|
335
|
+
)
|
|
336
|
+
def is_duck(n: int) -> bool:
|
|
337
|
+
"""Return ``True`` if *n* is a duck number.
|
|
338
|
+
|
|
339
|
+
A duck number is a positive integer that contains at least one digit ``0``
|
|
340
|
+
and does not have a leading zero (i.e. it is not zero itself).
|
|
341
|
+
|
|
342
|
+
Parameters
|
|
343
|
+
----------
|
|
344
|
+
n:
|
|
345
|
+
Integer to test.
|
|
346
|
+
|
|
347
|
+
Returns
|
|
348
|
+
-------
|
|
349
|
+
bool
|
|
350
|
+
|
|
351
|
+
Example
|
|
352
|
+
-------
|
|
353
|
+
>>> is_duck(1230)
|
|
354
|
+
True
|
|
355
|
+
>>> is_duck(1234)
|
|
356
|
+
False
|
|
357
|
+
>>> is_duck(100)
|
|
358
|
+
True
|
|
359
|
+
"""
|
|
360
|
+
if n <= 0:
|
|
361
|
+
return False
|
|
362
|
+
s = str(n)
|
|
363
|
+
return "0" in s # leading zero is impossible for n > 0 as int
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
@register(
|
|
367
|
+
name="Nude",
|
|
368
|
+
category="digital",
|
|
369
|
+
oeis="",
|
|
370
|
+
description=(
|
|
371
|
+
"A number divisible by each of its non-zero digits."
|
|
372
|
+
),
|
|
373
|
+
aliases=["nude number"],
|
|
374
|
+
)
|
|
375
|
+
def is_nude(n: int) -> bool:
|
|
376
|
+
"""Return ``True`` if *n* is a nude number.
|
|
377
|
+
|
|
378
|
+
A nude number is divisible by every one of its non-zero digits.
|
|
379
|
+
|
|
380
|
+
Parameters
|
|
381
|
+
----------
|
|
382
|
+
n:
|
|
383
|
+
Positive integer.
|
|
384
|
+
|
|
385
|
+
Returns
|
|
386
|
+
-------
|
|
387
|
+
bool
|
|
388
|
+
|
|
389
|
+
Example
|
|
390
|
+
-------
|
|
391
|
+
>>> is_nude(12) # 12 % 1 == 0 and 12 % 2 == 0
|
|
392
|
+
True
|
|
393
|
+
>>> is_nude(13) # 13 % 3 != 0
|
|
394
|
+
False
|
|
395
|
+
>>> is_nude(36) # 36 % 3 == 0 and 36 % 6 == 0
|
|
396
|
+
True
|
|
397
|
+
"""
|
|
398
|
+
if n <= 0:
|
|
399
|
+
return False
|
|
400
|
+
digits = [int(d) for d in str(n) if d != "0"]
|
|
401
|
+
if not digits:
|
|
402
|
+
return False
|
|
403
|
+
return all(n % d == 0 for d in digits)
|