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
numclassify/_registry.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numclassify._registry
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Central registry for all number-classification functions.
|
|
5
|
+
|
|
6
|
+
Every classification function is stored as a :class:`NumberType` entry keyed by
|
|
7
|
+
a normalised name (lowercase, spaces replaced with underscores). The
|
|
8
|
+
:func:`register` decorator factory is the sole public mechanism for adding
|
|
9
|
+
entries; it is used by every ``_core`` submodule.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import threading
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
from typing import Callable, Dict, List, Optional, Union
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Data model
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class NumberType:
|
|
25
|
+
"""Metadata and implementation for a single number classification.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
name:
|
|
30
|
+
Human-readable name, e.g. ``"Prime"``.
|
|
31
|
+
category:
|
|
32
|
+
Broad category, e.g. ``"primes"`` or ``"digital"``.
|
|
33
|
+
func:
|
|
34
|
+
A callable ``(n: int) -> bool`` that returns ``True`` when *n*
|
|
35
|
+
satisfies the classification.
|
|
36
|
+
oeis:
|
|
37
|
+
Optional OEIS sequence identifier, e.g. ``"A000040"``.
|
|
38
|
+
description:
|
|
39
|
+
One-line mathematical description.
|
|
40
|
+
aliases:
|
|
41
|
+
Alternative names under which this entry is also registered.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
category: str
|
|
46
|
+
func: Callable[[int], bool]
|
|
47
|
+
oeis: str = ""
|
|
48
|
+
description: str = ""
|
|
49
|
+
aliases: List[str] = field(default_factory=list)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Global registry
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
REGISTRY: Dict[str, NumberType] = {}
|
|
57
|
+
_REGISTRY_LOCK = threading.Lock()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _normalize(name: str) -> str:
|
|
61
|
+
"""Return *name* normalised to a registry key.
|
|
62
|
+
|
|
63
|
+
>>> _normalize("Twin Prime")
|
|
64
|
+
'twin_prime'
|
|
65
|
+
"""
|
|
66
|
+
return name.strip().lower().replace(" ", "_").replace("-", "_")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# @register decorator factory
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def register(
|
|
74
|
+
name: str,
|
|
75
|
+
category: str,
|
|
76
|
+
oeis: str = "",
|
|
77
|
+
description: str = "",
|
|
78
|
+
aliases: Optional[List[str]] = None,
|
|
79
|
+
) -> Callable[[Callable[[int], bool]], Callable[[int], bool]]:
|
|
80
|
+
"""Decorator factory that registers a classification function.
|
|
81
|
+
|
|
82
|
+
The function is stored under the normalised *name* **and** under every
|
|
83
|
+
entry in *aliases*. Registration is thread-safe.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
name:
|
|
88
|
+
Canonical name for the number type.
|
|
89
|
+
category:
|
|
90
|
+
Broad category grouping (used in the printed report).
|
|
91
|
+
oeis:
|
|
92
|
+
OEIS sequence identifier (optional).
|
|
93
|
+
description:
|
|
94
|
+
Short mathematical description (optional).
|
|
95
|
+
aliases:
|
|
96
|
+
List of alternative lookup names (optional).
|
|
97
|
+
|
|
98
|
+
Returns
|
|
99
|
+
-------
|
|
100
|
+
decorator:
|
|
101
|
+
A transparent decorator — the original function is returned unchanged
|
|
102
|
+
so it can still be called directly.
|
|
103
|
+
|
|
104
|
+
Example
|
|
105
|
+
-------
|
|
106
|
+
>>> @register(name="Even", category="basic", description="Divisible by 2")
|
|
107
|
+
... def is_even(n: int) -> bool:
|
|
108
|
+
... return n % 2 == 0
|
|
109
|
+
>>> "even" in REGISTRY
|
|
110
|
+
True
|
|
111
|
+
"""
|
|
112
|
+
_aliases: List[str] = aliases if aliases is not None else []
|
|
113
|
+
|
|
114
|
+
def decorator(func: Callable[[int], bool]) -> Callable[[int], bool]:
|
|
115
|
+
entry = NumberType(
|
|
116
|
+
name=name,
|
|
117
|
+
category=category,
|
|
118
|
+
func=func,
|
|
119
|
+
oeis=oeis,
|
|
120
|
+
description=description,
|
|
121
|
+
aliases=_aliases,
|
|
122
|
+
)
|
|
123
|
+
key = _normalize(name)
|
|
124
|
+
with _REGISTRY_LOCK:
|
|
125
|
+
REGISTRY[key] = entry
|
|
126
|
+
for alias in _aliases:
|
|
127
|
+
REGISTRY[_normalize(alias)] = entry
|
|
128
|
+
return func
|
|
129
|
+
|
|
130
|
+
return decorator
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# Public query helpers
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def _resolve(func_or_name: Union[Callable[[int], bool], str]) -> Callable[[int], bool]:
|
|
138
|
+
"""Return the callable for *func_or_name*.
|
|
139
|
+
|
|
140
|
+
Accepts either a raw callable or a string name (normalised before lookup).
|
|
141
|
+
|
|
142
|
+
Raises
|
|
143
|
+
------
|
|
144
|
+
KeyError
|
|
145
|
+
If the string name is not found in :data:`REGISTRY`.
|
|
146
|
+
TypeError
|
|
147
|
+
If *func_or_name* is neither a string nor a callable.
|
|
148
|
+
"""
|
|
149
|
+
if callable(func_or_name):
|
|
150
|
+
return func_or_name # type: ignore[return-value]
|
|
151
|
+
if isinstance(func_or_name, str):
|
|
152
|
+
key = _normalize(func_or_name)
|
|
153
|
+
if key not in REGISTRY:
|
|
154
|
+
raise KeyError(
|
|
155
|
+
f"No number type named {func_or_name!r} is registered. "
|
|
156
|
+
f"Available: {sorted(REGISTRY)}"
|
|
157
|
+
)
|
|
158
|
+
return REGISTRY[key].func
|
|
159
|
+
raise TypeError(
|
|
160
|
+
f"Expected a callable or a type name string, got {type(func_or_name)!r}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def get_all_properties(n: int) -> Dict[str, bool]:
|
|
165
|
+
"""Run every registered classification function against *n*.
|
|
166
|
+
|
|
167
|
+
Parameters
|
|
168
|
+
----------
|
|
169
|
+
n:
|
|
170
|
+
The integer to classify.
|
|
171
|
+
|
|
172
|
+
Returns
|
|
173
|
+
-------
|
|
174
|
+
dict
|
|
175
|
+
Mapping of canonical name → bool for **every** registered type.
|
|
176
|
+
|
|
177
|
+
Example
|
|
178
|
+
-------
|
|
179
|
+
>>> result = get_all_properties(6)
|
|
180
|
+
>>> isinstance(result, dict)
|
|
181
|
+
True
|
|
182
|
+
"""
|
|
183
|
+
with _REGISTRY_LOCK:
|
|
184
|
+
snapshot = dict(REGISTRY)
|
|
185
|
+
result: Dict[str, bool] = {}
|
|
186
|
+
for key, entry in snapshot.items():
|
|
187
|
+
if key == _normalize(entry.name): # skip alias duplicates
|
|
188
|
+
try:
|
|
189
|
+
result[entry.name] = bool(entry.func(n))
|
|
190
|
+
except Exception:
|
|
191
|
+
result[entry.name] = False
|
|
192
|
+
return result
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def get_true_properties(n: int) -> Dict[str, bool]:
|
|
196
|
+
"""Return only the properties of *n* that are ``True``.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
n:
|
|
201
|
+
The integer to classify.
|
|
202
|
+
|
|
203
|
+
Returns
|
|
204
|
+
-------
|
|
205
|
+
dict
|
|
206
|
+
Subset of :func:`get_all_properties` where value is ``True``.
|
|
207
|
+
|
|
208
|
+
Example
|
|
209
|
+
-------
|
|
210
|
+
>>> all(get_true_properties(28).values())
|
|
211
|
+
True
|
|
212
|
+
"""
|
|
213
|
+
return {k: v for k, v in get_all_properties(n).items() if v}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def count_properties(n: int) -> int:
|
|
217
|
+
"""Return the number of registered types satisfied by *n*.
|
|
218
|
+
|
|
219
|
+
Parameters
|
|
220
|
+
----------
|
|
221
|
+
n:
|
|
222
|
+
The integer to classify.
|
|
223
|
+
|
|
224
|
+
Returns
|
|
225
|
+
-------
|
|
226
|
+
int
|
|
227
|
+
Count of ``True`` entries in :func:`get_all_properties`.
|
|
228
|
+
|
|
229
|
+
Example
|
|
230
|
+
-------
|
|
231
|
+
>>> count_properties(2) >= 1
|
|
232
|
+
True
|
|
233
|
+
"""
|
|
234
|
+
return sum(1 for v in get_all_properties(n).values() if v)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def print_properties(n: int) -> None:
|
|
238
|
+
"""Print a formatted classification report for *n*.
|
|
239
|
+
|
|
240
|
+
The report uses box-drawing characters and is divided into three
|
|
241
|
+
sections: TRUE (properties satisfied), FALSE (properties not
|
|
242
|
+
satisfied), and a summary line.
|
|
243
|
+
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
n:
|
|
247
|
+
The integer to classify.
|
|
248
|
+
|
|
249
|
+
Example
|
|
250
|
+
-------
|
|
251
|
+
>>> print_properties(153) # doctest: +SKIP
|
|
252
|
+
┌─────────────────────────────────────────┐
|
|
253
|
+
│ Classification report for n = 153 │
|
|
254
|
+
...
|
|
255
|
+
"""
|
|
256
|
+
props = get_all_properties(n)
|
|
257
|
+
true_props = {k: v for k, v in props.items() if v}
|
|
258
|
+
false_props = {k: v for k, v in props.items() if not v}
|
|
259
|
+
|
|
260
|
+
width = 50
|
|
261
|
+
border = "─" * (width - 2)
|
|
262
|
+
|
|
263
|
+
print(f"┌{border}┐")
|
|
264
|
+
title = f" Classification report for n = {n}"
|
|
265
|
+
print(f"│{title:<{width - 2}}│")
|
|
266
|
+
print(f"├{border}┤")
|
|
267
|
+
|
|
268
|
+
if true_props:
|
|
269
|
+
header = " ✓ TRUE"
|
|
270
|
+
print(f"│{header:<{width - 2}}│")
|
|
271
|
+
for name in sorted(true_props):
|
|
272
|
+
line = f" • {name}"
|
|
273
|
+
print(f"│{line:<{width - 2}}│")
|
|
274
|
+
else:
|
|
275
|
+
line = " (no properties satisfied)"
|
|
276
|
+
print(f"│{line:<{width - 2}}│")
|
|
277
|
+
|
|
278
|
+
print(f"├{border}┤")
|
|
279
|
+
|
|
280
|
+
if false_props:
|
|
281
|
+
header = " ✗ FALSE"
|
|
282
|
+
print(f"│{header:<{width - 2}}│")
|
|
283
|
+
for name in sorted(false_props):
|
|
284
|
+
line = f" • {name}"
|
|
285
|
+
print(f"│{line:<{width - 2}}│")
|
|
286
|
+
|
|
287
|
+
print(f"├{border}┤")
|
|
288
|
+
summary = f" Total TRUE: {len(true_props)} / {len(props)}"
|
|
289
|
+
print(f"│{summary:<{width - 2}}│")
|
|
290
|
+
print(f"└{border}┘")
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
# ---------------------------------------------------------------------------
|
|
294
|
+
# Range search helpers
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
def find_in_range(
|
|
298
|
+
func_or_name: Union[Callable[[int], bool], str],
|
|
299
|
+
start: int,
|
|
300
|
+
end: int,
|
|
301
|
+
) -> List[int]:
|
|
302
|
+
"""Return all integers in ``[start, end]`` satisfying *func_or_name*.
|
|
303
|
+
|
|
304
|
+
Parameters
|
|
305
|
+
----------
|
|
306
|
+
func_or_name:
|
|
307
|
+
Either a callable ``(int) -> bool`` or a registered type name string.
|
|
308
|
+
start:
|
|
309
|
+
Lower bound (inclusive).
|
|
310
|
+
end:
|
|
311
|
+
Upper bound (inclusive).
|
|
312
|
+
|
|
313
|
+
Returns
|
|
314
|
+
-------
|
|
315
|
+
list[int]
|
|
316
|
+
Sorted list of matching integers.
|
|
317
|
+
|
|
318
|
+
Example
|
|
319
|
+
-------
|
|
320
|
+
>>> from numclassify._core.primes import is_prime
|
|
321
|
+
>>> find_in_range(is_prime, 1, 10)
|
|
322
|
+
[2, 3, 5, 7]
|
|
323
|
+
"""
|
|
324
|
+
func = _resolve(func_or_name)
|
|
325
|
+
return [n for n in range(start, end + 1) if func(n)]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def find_all_in_range(
|
|
329
|
+
funcs_or_names: List[Union[Callable[[int], bool], str]],
|
|
330
|
+
start: int,
|
|
331
|
+
end: int,
|
|
332
|
+
) -> List[int]:
|
|
333
|
+
"""Return integers in ``[start, end]`` satisfying **all** given types.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
funcs_or_names:
|
|
338
|
+
List of callables or registered name strings.
|
|
339
|
+
start:
|
|
340
|
+
Lower bound (inclusive).
|
|
341
|
+
end:
|
|
342
|
+
Upper bound (inclusive).
|
|
343
|
+
|
|
344
|
+
Returns
|
|
345
|
+
-------
|
|
346
|
+
list[int]
|
|
347
|
+
Sorted list of integers satisfying every predicate.
|
|
348
|
+
|
|
349
|
+
Example
|
|
350
|
+
-------
|
|
351
|
+
>>> find_all_in_range(["prime", "palindrome"], 1, 200)
|
|
352
|
+
[2, 3, 5, 7, 11]
|
|
353
|
+
"""
|
|
354
|
+
funcs = [_resolve(f) for f in funcs_or_names]
|
|
355
|
+
return [n for n in range(start, end + 1) if all(f(n) for f in funcs)]
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def find_any_in_range(
|
|
359
|
+
funcs_or_names: List[Union[Callable[[int], bool], str]],
|
|
360
|
+
start: int,
|
|
361
|
+
end: int,
|
|
362
|
+
) -> List[int]:
|
|
363
|
+
"""Return integers in ``[start, end]`` satisfying **at least one** type.
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
funcs_or_names:
|
|
368
|
+
List of callables or registered name strings.
|
|
369
|
+
start:
|
|
370
|
+
Lower bound (inclusive).
|
|
371
|
+
end:
|
|
372
|
+
Upper bound (inclusive).
|
|
373
|
+
|
|
374
|
+
Returns
|
|
375
|
+
-------
|
|
376
|
+
list[int]
|
|
377
|
+
Sorted list of integers satisfying at least one predicate.
|
|
378
|
+
|
|
379
|
+
Example
|
|
380
|
+
-------
|
|
381
|
+
>>> find_any_in_range(["prime", "palindrome"], 1, 15)
|
|
382
|
+
[2, 3, 5, 7, 11, 11, 13]
|
|
383
|
+
"""
|
|
384
|
+
funcs = [_resolve(f) for f in funcs_or_names]
|
|
385
|
+
return [n for n in range(start, end + 1) if any(f(n) for f in funcs)]
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def most_special_in_range(start: int, end: int) -> int:
|
|
389
|
+
"""Return the integer in ``[start, end]`` with the most ``True`` properties.
|
|
390
|
+
|
|
391
|
+
Ties are broken by returning the smallest such integer.
|
|
392
|
+
|
|
393
|
+
Parameters
|
|
394
|
+
----------
|
|
395
|
+
start:
|
|
396
|
+
Lower bound (inclusive).
|
|
397
|
+
end:
|
|
398
|
+
Upper bound (inclusive).
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
int
|
|
403
|
+
The most-classified integer in the range.
|
|
404
|
+
|
|
405
|
+
Example
|
|
406
|
+
-------
|
|
407
|
+
>>> most_special_in_range(1, 10) >= 1
|
|
408
|
+
True
|
|
409
|
+
"""
|
|
410
|
+
best_n = start
|
|
411
|
+
best_count = -1
|
|
412
|
+
for n in range(start, end + 1):
|
|
413
|
+
c = count_properties(n)
|
|
414
|
+
if c > best_count:
|
|
415
|
+
best_count = c
|
|
416
|
+
best_n = n
|
|
417
|
+
return best_n
|