numclassify 0.3.2__tar.gz → 0.3.3__tar.gz
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-0.3.2 → numclassify-0.3.3}/.gitignore +1 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/CHANGELOG.md +28 -3
- {numclassify-0.3.2 → numclassify-0.3.3}/PKG-INFO +1 -1
- numclassify-0.3.3/docs/extra/saffron.css +37 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/docs/playground.html +2 -2
- {numclassify-0.3.2 → numclassify-0.3.3}/docs/playground.js +42 -6
- {numclassify-0.3.2 → numclassify-0.3.3}/mkdocs.yml +6 -2
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/__init__.py +67 -12
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/_core/divisors.py +1 -1
- numclassify-0.3.3/numclassify/_core/exam_types.py +384 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/_core/figurate.py +1 -3
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/_core/primes.py +2 -2
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/_registry.py +8 -2
- {numclassify-0.3.2 → numclassify-0.3.3}/pyproject.toml +1 -1
- {numclassify-0.3.2 → numclassify-0.3.3}/tests/test_registry.py +30 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/.github/workflows/ci.yml +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/.github/workflows/docs.yml +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/.github/workflows/publish.yml +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/CONTRIBUTING.md +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/LICENSE +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/README.md +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/docs/api.md +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/docs/changelog.md +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/docs/cli.md +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/docs/index.md +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/docs/playground.css +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/examples/basic_usage.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/examples/custom_type.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/examples/find_perfect_numbers.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/examples/random_classify.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/examples/stream_large_range.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/__main__.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/_core/__init__.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/_core/combinatorial.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/_core/digital.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/_core/number_theory.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/_core/powers.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/_core/recreational.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/_core/sequences.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/cli.py +0 -0
- {numclassify-0.3.2 → numclassify-0.3.3}/numclassify/py.typed +0 -0
|
@@ -6,6 +6,34 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.3.3] - 2026-06-13
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- 8 new exam number types in `numclassify/_core/exam_types.py`:
|
|
13
|
+
Strong, Sunny, Buzz, Magic, Fascinating, Trimorphic, Twisted Prime, Unique
|
|
14
|
+
- `classify()` now returns a `"categories"` dict grouping true properties by category
|
|
15
|
+
- `classify()` true_properties list is now sorted by category then name
|
|
16
|
+
- `stream()` now accepts `min_score` and `has_property` filter parameters
|
|
17
|
+
- `most_special_in_range()` now accepts `verbose=True` for progress output
|
|
18
|
+
- `find_any_in_range()` exported in public API (was implemented but not accessible)
|
|
19
|
+
- Auto-generated crash tests: every registered type tested on inputs 0, 1, 2, -1
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- Removed dead `else` branch in `classify()` (unreachable code)
|
|
23
|
+
- Removed `del annotations` no-op in `__init__.py`
|
|
24
|
+
- Fixed wrong docstring example in `is_leyland_prime` (593 is True, not False)
|
|
25
|
+
- Removed dead `pass` block in `is_centered_k_gonal` in `figurate.py`
|
|
26
|
+
- Documented `is_sociable` cycle detection cap (chains > 6 not detected)
|
|
27
|
+
|
|
28
|
+
## [0.3.2.1] - 2026-06-12
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Batch classify input rejected commas/spaces — changed `type="number"` to `type="text"` with `inputmode="numeric"`
|
|
32
|
+
- Search autocomplete dropdown not appearing — added fallback fetch of property names if initial Pyodide load fails, plus visible "Loading..." state in dropdown
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- MkDocs site theme changed from indigo to saffron (#FF9933) via custom CSS
|
|
36
|
+
|
|
9
37
|
## [0.3.2] - 2026-06-12
|
|
10
38
|
|
|
11
39
|
### Added
|
|
@@ -14,9 +42,6 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
14
42
|
- Keyboard shortcuts: `C` (classify), `S` (search), `N` (Number of the Day), `?`/`H` (show shortcuts overlay)
|
|
15
43
|
- `prefers-reduced-motion` support — disables all animations for accessibility
|
|
16
44
|
|
|
17
|
-
### Fixed
|
|
18
|
-
- No bugs reported since v0.3.1
|
|
19
|
-
|
|
20
45
|
## [0.3.1] - 2026-06-12
|
|
21
46
|
|
|
22
47
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: numclassify
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: The most comprehensive Python library for number classification - 3000+ number types
|
|
5
5
|
Project-URL: Homepage, https://github.com/aratrikghosh2011-tech/numclassify
|
|
6
6
|
Project-URL: Repository, https://github.com/aratrikghosh2011-tech/numclassify
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--md-primary-fg-color: #FF9933;
|
|
3
|
+
--md-primary-fg-color--light: #FFB366;
|
|
4
|
+
--md-primary-fg-color--dark: #E6821A;
|
|
5
|
+
--md-primary-bg-color: #0A0A0F;
|
|
6
|
+
--md-primary-bg-color--light: #12121A;
|
|
7
|
+
--md-accent-fg-color: #FF9933;
|
|
8
|
+
--md-accent-fg-color--light: #FFB366;
|
|
9
|
+
--md-accent-fg-color--dark: #E6821A;
|
|
10
|
+
--md-typeset-a-color: #FF9933;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
[data-md-color-scheme="slate"] {
|
|
14
|
+
--md-default-bg-color: #0A0A0F;
|
|
15
|
+
--md-code-bg-color: #12121A;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.md-header {
|
|
19
|
+
background: linear-gradient(135deg, #E6821A, #FF9933) !important;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.md-tabs {
|
|
23
|
+
background: #E6821A !important;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.md-search__form {
|
|
27
|
+
background: rgba(255,255,255,0.15) !important;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
a:hover {
|
|
31
|
+
color: #FFB366 !important;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.md-footer {
|
|
35
|
+
background: #0A0A0F !important;
|
|
36
|
+
border-top: 1px solid rgba(255,153,51,0.2);
|
|
37
|
+
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<header>
|
|
20
20
|
<button class="theme-toggle" onclick="toggleTheme()" title="Toggle theme"><span id="theme-icon">🌙</span></button>
|
|
21
21
|
<div class="logo-badge">numclassify playground</div>
|
|
22
|
-
<div class="version-badge" id="version-badge">v<span id="version-text">—</span></div>
|
|
22
|
+
<div class="version-badge" id="version-badge" data-version="0.3.2.1">v<span id="version-text">—</span></div>
|
|
23
23
|
<h1>Number Intelligence</h1>
|
|
24
24
|
<p class="subtitle">Classify any integer into 3000+ named mathematical types — powered by real Python in your browser.</p>
|
|
25
25
|
</header>
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
<div class="card">
|
|
46
46
|
<div class="card-title">Classify a Number</div>
|
|
47
47
|
<div class="input-row">
|
|
48
|
-
<input type="
|
|
48
|
+
<input type="text" id="input-classify" inputmode="numeric" pattern="-?[0-9\s,]*" placeholder="Enter any integer... (or 6, 28, 496 for batch)" />
|
|
49
49
|
<button class="btn" id="btn-classify" onclick="doClassify()">Classify</button>
|
|
50
50
|
<button class="btn btn-ghost" onclick="doRandom()">Random</button>
|
|
51
51
|
</div>
|
|
@@ -228,12 +228,18 @@ json.dumps(m)
|
|
|
228
228
|
console.warn('Could not load category map', e);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
// Fetch version number
|
|
231
|
+
// Fetch version number — prefer local source version as canonical
|
|
232
|
+
const badge = $('version-badge');
|
|
233
|
+
const srcVersion = badge ? badge.dataset.version : null;
|
|
232
234
|
try {
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
+
const pypiVer = await pyodide.runPythonAsync('nc.__version__');
|
|
236
|
+
// Use the source version (from pyproject.toml) since PyPI may lag behind
|
|
237
|
+
$('version-text').textContent = srcVersion || pypiVer;
|
|
238
|
+
if (srcVersion && pypiVer !== srcVersion) {
|
|
239
|
+
badge.title = 'Source: v' + srcVersion + ' | PyPI: v' + pypiVer;
|
|
240
|
+
}
|
|
235
241
|
} catch(e) {
|
|
236
|
-
$('version-text').textContent = '?';
|
|
242
|
+
$('version-text').textContent = srcVersion || '?';
|
|
237
243
|
}
|
|
238
244
|
|
|
239
245
|
setProgress(100, 'Ready!');
|
|
@@ -670,13 +676,43 @@ function setupAutocomplete() {
|
|
|
670
676
|
const dropdown = $('ac-dropdown');
|
|
671
677
|
let selectedIdx = -1;
|
|
672
678
|
|
|
673
|
-
input.addEventListener('input', () => {
|
|
679
|
+
input.addEventListener('input', async () => {
|
|
674
680
|
const val = input.value.trim().toLowerCase();
|
|
675
681
|
if (!val) { dropdown.classList.remove('open'); return; }
|
|
682
|
+
|
|
683
|
+
// Fallback: if allProperties is empty (Pyodide category fetch failed), try on-the-fly
|
|
684
|
+
if (!allProperties.length && pyReady && pyodide) {
|
|
685
|
+
try {
|
|
686
|
+
const sample = await pyodide.runPythonAsync(`
|
|
687
|
+
import json
|
|
688
|
+
from numclassify._registry import REGISTRY
|
|
689
|
+
keys = list(REGISTRY.keys())
|
|
690
|
+
json.dumps(keys)
|
|
691
|
+
`);
|
|
692
|
+
const keys = JSON.parse(sample);
|
|
693
|
+
if (keys.length) {
|
|
694
|
+
allProperties = keys;
|
|
695
|
+
categoryMap = {};
|
|
696
|
+
keys.forEach(k => { categoryMap[k] = ''; });
|
|
697
|
+
console.log('Autocomplete: fallback loaded', keys.length, 'properties');
|
|
698
|
+
}
|
|
699
|
+
} catch(e) {
|
|
700
|
+
console.warn('Autocomplete fallback failed', e);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
676
704
|
const matches = allProperties
|
|
677
705
|
.filter(p => p.includes(val))
|
|
678
706
|
.slice(0, 10);
|
|
679
|
-
if (!matches.length) {
|
|
707
|
+
if (!matches.length) {
|
|
708
|
+
if (!allProperties.length) {
|
|
709
|
+
dropdown.innerHTML = '<div class="ac-item" style="cursor:default;color:var(--text-muted)">Loading properties...</div>';
|
|
710
|
+
dropdown.classList.add('open');
|
|
711
|
+
} else {
|
|
712
|
+
dropdown.classList.remove('open');
|
|
713
|
+
}
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
680
716
|
selectedIdx = -1;
|
|
681
717
|
dropdown.innerHTML = matches.map((p, i) =>
|
|
682
718
|
`<div class="ac-item${i === 0 ? ' selected' : ''}" data-prop="${p}" onclick="pickAutocomplete('${p}')">
|
|
@@ -7,13 +7,17 @@ repo_name: aratrikghosh2011-tech/numclassify
|
|
|
7
7
|
theme:
|
|
8
8
|
name: material
|
|
9
9
|
palette:
|
|
10
|
-
primary:
|
|
11
|
-
accent:
|
|
10
|
+
primary: custom
|
|
11
|
+
accent: custom
|
|
12
|
+
scheme: slate
|
|
12
13
|
features:
|
|
13
14
|
- navigation.tabs
|
|
14
15
|
- navigation.top
|
|
15
16
|
- content.code.copy
|
|
16
17
|
|
|
18
|
+
extra_css:
|
|
19
|
+
- extra/saffron.css
|
|
20
|
+
|
|
17
21
|
nav:
|
|
18
22
|
- Home: index.md
|
|
19
23
|
- API Reference: api.md
|
|
@@ -26,7 +26,6 @@ Public API
|
|
|
26
26
|
"""
|
|
27
27
|
|
|
28
28
|
from __future__ import annotations
|
|
29
|
-
del annotations
|
|
30
29
|
|
|
31
30
|
from importlib.metadata import version as _version, PackageNotFoundError as _PackageNotFoundError
|
|
32
31
|
try:
|
|
@@ -44,6 +43,7 @@ from numclassify._core import sequences as _sequences # noqa: F401
|
|
|
44
43
|
from numclassify._core import powers as _powers # noqa: F401
|
|
45
44
|
from numclassify._core import number_theory as _number_theory # noqa: F401
|
|
46
45
|
from numclassify._core import combinatorial as _combinatorial # noqa: F401
|
|
46
|
+
from numclassify._core import exam_types as _exam_types # noqa: F401
|
|
47
47
|
|
|
48
48
|
# --- Re-export key functions at top level ---
|
|
49
49
|
from numclassify._core.primes import is_prime # noqa: F401
|
|
@@ -56,6 +56,7 @@ from numclassify._registry import ( # noqa: F401
|
|
|
56
56
|
print_properties,
|
|
57
57
|
find_in_range,
|
|
58
58
|
find_all_in_range,
|
|
59
|
+
find_any_in_range,
|
|
59
60
|
count_properties,
|
|
60
61
|
most_special_in_range,
|
|
61
62
|
)
|
|
@@ -86,20 +87,35 @@ def classify(n: int) -> dict:
|
|
|
86
87
|
-------
|
|
87
88
|
{
|
|
88
89
|
"number": n,
|
|
89
|
-
"true_properties": [list of property names that are True],
|
|
90
|
-
"score": int,
|
|
90
|
+
"true_properties": [list of property names that are True, sorted by category],
|
|
91
|
+
"score": int,
|
|
92
|
+
"categories": {category_name: [list of property names]},
|
|
91
93
|
}
|
|
92
94
|
"""
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
from numclassify._registry import REGISTRY, _normalize
|
|
96
|
+
|
|
97
|
+
all_props = get_all_properties(n)
|
|
98
|
+
true_props_with_cat = []
|
|
99
|
+
for name, val in all_props.items():
|
|
100
|
+
if val:
|
|
101
|
+
key = _normalize(name)
|
|
102
|
+
cat = REGISTRY[key].category if key in REGISTRY else "other"
|
|
103
|
+
true_props_with_cat.append((name, cat))
|
|
104
|
+
|
|
105
|
+
# Sort by category then name
|
|
106
|
+
true_props_with_cat.sort(key=lambda x: (x[1], x[0]))
|
|
107
|
+
true_props = [name for name, _ in true_props_with_cat]
|
|
108
|
+
|
|
109
|
+
# Build categories dict
|
|
110
|
+
categories: dict = {}
|
|
111
|
+
for name, cat in true_props_with_cat:
|
|
112
|
+
categories.setdefault(cat, []).append(name)
|
|
98
113
|
|
|
99
114
|
return {
|
|
100
115
|
"number": n,
|
|
101
116
|
"true_properties": true_props,
|
|
102
117
|
"score": len(true_props),
|
|
118
|
+
"categories": categories,
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
|
|
@@ -150,10 +166,48 @@ def find_by_property(start: int = 1, end: int = 1000,
|
|
|
150
166
|
return results
|
|
151
167
|
|
|
152
168
|
|
|
153
|
-
|
|
154
|
-
|
|
169
|
+
from typing import Optional
|
|
170
|
+
|
|
171
|
+
def stream(start: int, end: int, min_score: int = 0, has_property: Optional[str] = None):
|
|
172
|
+
"""Generator. Yields classify(n) for each n in [start, end]. Memory-safe.
|
|
173
|
+
|
|
174
|
+
Parameters
|
|
175
|
+
----------
|
|
176
|
+
start, end : int
|
|
177
|
+
Inclusive range.
|
|
178
|
+
min_score : int, optional
|
|
179
|
+
Only yield results with score >= min_score. Default 0 (yield all).
|
|
180
|
+
has_property : str, optional
|
|
181
|
+
Only yield results where this property is True.
|
|
182
|
+
Accepts any registered property name (case-insensitive, spaces or underscores).
|
|
183
|
+
|
|
184
|
+
Example
|
|
185
|
+
-------
|
|
186
|
+
# Only numbers with more than 20 true properties
|
|
187
|
+
for result in nc.stream(1, 10000, min_score=20):
|
|
188
|
+
print(result)
|
|
189
|
+
|
|
190
|
+
# Only fibonacci numbers in a range
|
|
191
|
+
for result in nc.stream(1, 1000, has_property='fibonacci'):
|
|
192
|
+
print(result['number'])
|
|
193
|
+
"""
|
|
194
|
+
from numclassify._registry import REGISTRY, _normalize, get_all_properties as _gap
|
|
195
|
+
|
|
196
|
+
prop_key = _normalize(has_property) if has_property else None
|
|
197
|
+
|
|
155
198
|
for n in range(start, end + 1):
|
|
156
|
-
|
|
199
|
+
result = classify(n)
|
|
200
|
+
if result["score"] < min_score:
|
|
201
|
+
continue
|
|
202
|
+
if prop_key is not None:
|
|
203
|
+
props = _gap(n)
|
|
204
|
+
# find by normalized key
|
|
205
|
+
matched = any(
|
|
206
|
+
_normalize(k) == prop_key for k, v in props.items() if v
|
|
207
|
+
)
|
|
208
|
+
if not matched:
|
|
209
|
+
continue
|
|
210
|
+
yield result
|
|
157
211
|
|
|
158
212
|
|
|
159
213
|
# ---------------------------------------------------------------------------
|
|
@@ -170,6 +224,7 @@ __all__ = [
|
|
|
170
224
|
"print_properties",
|
|
171
225
|
"find_in_range",
|
|
172
226
|
"find_all_in_range",
|
|
227
|
+
"find_any_in_range",
|
|
173
228
|
"count_properties",
|
|
174
229
|
"most_special_in_range",
|
|
175
230
|
"classify",
|
|
@@ -182,4 +237,4 @@ __all__ = [
|
|
|
182
237
|
# Clean up internal names that leak into dir(nc)
|
|
183
238
|
del (_primes, _figurate, _digital, _recreational,
|
|
184
239
|
_divisors, _sequences, _powers, _number_theory,
|
|
185
|
-
_combinatorial, _core, _registry)
|
|
240
|
+
_combinatorial, _exam_types, _core, _registry)
|
|
@@ -273,7 +273,7 @@ def is_amicable(n: int) -> bool:
|
|
|
273
273
|
|
|
274
274
|
|
|
275
275
|
@register(name="Sociable", category="divisors", oeis="A003416",
|
|
276
|
-
description="Part of an aliquot cycle of length
|
|
276
|
+
description="Part of an aliquot cycle of length 3–6 (longer chains not detected).")
|
|
277
277
|
def is_sociable(n: int) -> bool:
|
|
278
278
|
"""Return True if n is part of a sociable cycle of length 3–6.
|
|
279
279
|
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""
|
|
2
|
+
numclassify._core.exam_types
|
|
3
|
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
4
|
+
Number types frequently asked in school/college Python exams (ICSE, CBSE,
|
|
5
|
+
competitive programming, interview prep). All functions registered via @register.
|
|
6
|
+
|
|
7
|
+
Types added:
|
|
8
|
+
- Strong Number
|
|
9
|
+
- Sunny Number
|
|
10
|
+
- Buzz Number
|
|
11
|
+
- Magic Number
|
|
12
|
+
- Fascinating Number
|
|
13
|
+
- Trimorphic Number
|
|
14
|
+
- Twisted Prime
|
|
15
|
+
- Unique Number (no repeated digits)
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import math
|
|
20
|
+
from typing import List
|
|
21
|
+
|
|
22
|
+
from numclassify._registry import register
|
|
23
|
+
from numclassify._core.primes import is_prime
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Helpers
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def _digit_sum(n: int) -> int:
|
|
31
|
+
return sum(int(d) for d in str(abs(n)))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _is_perfect_square_exact(n: int) -> bool:
|
|
35
|
+
if n < 0:
|
|
36
|
+
return False
|
|
37
|
+
r = math.isqrt(n)
|
|
38
|
+
return r * r == n
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Strong Number
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
@register(
|
|
46
|
+
name="Strong",
|
|
47
|
+
category="digital",
|
|
48
|
+
oeis="A014080",
|
|
49
|
+
description=(
|
|
50
|
+
"A number equal to the sum of the factorials of its digits. "
|
|
51
|
+
"Also called a factorion. Examples: 1, 2, 145, 40585."
|
|
52
|
+
),
|
|
53
|
+
aliases=["strong_number", "factorion"],
|
|
54
|
+
)
|
|
55
|
+
def is_strong(n: int) -> bool:
|
|
56
|
+
"""Return True if n equals the sum of the factorials of its digits.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
n : int
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
bool
|
|
65
|
+
|
|
66
|
+
Examples
|
|
67
|
+
--------
|
|
68
|
+
>>> is_strong(145) # 1! + 4! + 5! = 1 + 24 + 120 = 145
|
|
69
|
+
True
|
|
70
|
+
>>> is_strong(2) # 2! = 2
|
|
71
|
+
True
|
|
72
|
+
>>> is_strong(40585)
|
|
73
|
+
True
|
|
74
|
+
>>> is_strong(100)
|
|
75
|
+
False
|
|
76
|
+
"""
|
|
77
|
+
if n <= 0:
|
|
78
|
+
return False
|
|
79
|
+
return sum(math.factorial(int(d)) for d in str(n)) == n
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# Sunny Number
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
@register(
|
|
87
|
+
name="Sunny",
|
|
88
|
+
category="sequences",
|
|
89
|
+
oeis="A005563",
|
|
90
|
+
description=(
|
|
91
|
+
"A number n where n+1 is a perfect square. "
|
|
92
|
+
"Examples: 3, 8, 15, 24, 35, 48."
|
|
93
|
+
),
|
|
94
|
+
aliases=["sunny_number"],
|
|
95
|
+
)
|
|
96
|
+
def is_sunny(n: int) -> bool:
|
|
97
|
+
"""Return True if n+1 is a perfect square.
|
|
98
|
+
|
|
99
|
+
Parameters
|
|
100
|
+
----------
|
|
101
|
+
n : int
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
bool
|
|
106
|
+
|
|
107
|
+
Examples
|
|
108
|
+
--------
|
|
109
|
+
>>> is_sunny(3) # 3+1 = 4 = 2^2
|
|
110
|
+
True
|
|
111
|
+
>>> is_sunny(8) # 8+1 = 9 = 3^2
|
|
112
|
+
True
|
|
113
|
+
>>> is_sunny(24) # 24+1 = 25 = 5^2
|
|
114
|
+
True
|
|
115
|
+
>>> is_sunny(5) # 5+1 = 6, not a perfect square
|
|
116
|
+
False
|
|
117
|
+
"""
|
|
118
|
+
if n <= 0:
|
|
119
|
+
return False
|
|
120
|
+
return _is_perfect_square_exact(n + 1)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# Buzz Number
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
@register(
|
|
128
|
+
name="Buzz",
|
|
129
|
+
category="recreational",
|
|
130
|
+
description=(
|
|
131
|
+
"A number divisible by 7 or ending in 7 (or both). "
|
|
132
|
+
"Named after the Buzz game where players say 'buzz' for such numbers."
|
|
133
|
+
),
|
|
134
|
+
aliases=["buzz_number"],
|
|
135
|
+
)
|
|
136
|
+
def is_buzz(n: int) -> bool:
|
|
137
|
+
"""Return True if n is divisible by 7 or ends in the digit 7.
|
|
138
|
+
|
|
139
|
+
Parameters
|
|
140
|
+
----------
|
|
141
|
+
n : int
|
|
142
|
+
|
|
143
|
+
Returns
|
|
144
|
+
-------
|
|
145
|
+
bool
|
|
146
|
+
|
|
147
|
+
Examples
|
|
148
|
+
--------
|
|
149
|
+
>>> is_buzz(7) # divisible by 7 and ends in 7
|
|
150
|
+
True
|
|
151
|
+
>>> is_buzz(14) # divisible by 7
|
|
152
|
+
True
|
|
153
|
+
>>> is_buzz(17) # ends in 7
|
|
154
|
+
True
|
|
155
|
+
>>> is_buzz(10)
|
|
156
|
+
False
|
|
157
|
+
"""
|
|
158
|
+
if n <= 0:
|
|
159
|
+
return False
|
|
160
|
+
return n % 7 == 0 or n % 10 == 7
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Magic Number
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
@register(
|
|
168
|
+
name="Magic",
|
|
169
|
+
category="digital",
|
|
170
|
+
description=(
|
|
171
|
+
"A number whose repeated digit sum (digital root) equals 1. "
|
|
172
|
+
"Examples: 1, 10, 19, 28, 100."
|
|
173
|
+
),
|
|
174
|
+
aliases=["magic_number"],
|
|
175
|
+
)
|
|
176
|
+
def is_magic(n: int) -> bool:
|
|
177
|
+
"""Return True if the digital root of n equals 1.
|
|
178
|
+
|
|
179
|
+
Repeatedly sum the digits until a single digit remains.
|
|
180
|
+
If that digit is 1, the number is magic.
|
|
181
|
+
|
|
182
|
+
Parameters
|
|
183
|
+
----------
|
|
184
|
+
n : int
|
|
185
|
+
|
|
186
|
+
Returns
|
|
187
|
+
-------
|
|
188
|
+
bool
|
|
189
|
+
|
|
190
|
+
Examples
|
|
191
|
+
--------
|
|
192
|
+
>>> is_magic(1)
|
|
193
|
+
True
|
|
194
|
+
>>> is_magic(10) # 1+0 = 1
|
|
195
|
+
True
|
|
196
|
+
>>> is_magic(19) # 1+9=10, 1+0=1
|
|
197
|
+
True
|
|
198
|
+
>>> is_magic(28) # 2+8=10, 1+0=1
|
|
199
|
+
True
|
|
200
|
+
>>> is_magic(12) # 1+2=3 ≠ 1
|
|
201
|
+
False
|
|
202
|
+
"""
|
|
203
|
+
if n <= 0:
|
|
204
|
+
return False
|
|
205
|
+
while n >= 10:
|
|
206
|
+
n = _digit_sum(n)
|
|
207
|
+
return n == 1
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# Fascinating Number
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
@register(
|
|
215
|
+
name="Fascinating",
|
|
216
|
+
category="digital",
|
|
217
|
+
description=(
|
|
218
|
+
"A 3-digit number n where the concatenation of n, 2n, and 3n "
|
|
219
|
+
"contains all digits 1-9 exactly once (no zeros, no repeats). "
|
|
220
|
+
"Examples: 192, 219, 273, 327, 192."
|
|
221
|
+
),
|
|
222
|
+
aliases=["fascinating_number"],
|
|
223
|
+
)
|
|
224
|
+
def is_fascinating(n: int) -> bool:
|
|
225
|
+
"""Return True if n is a fascinating number.
|
|
226
|
+
|
|
227
|
+
For a 3-digit n, concatenate str(n) + str(2*n) + str(3*n).
|
|
228
|
+
The result must use each digit 1-9 exactly once and contain no 0.
|
|
229
|
+
|
|
230
|
+
Parameters
|
|
231
|
+
----------
|
|
232
|
+
n : int
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
-------
|
|
236
|
+
bool
|
|
237
|
+
|
|
238
|
+
Examples
|
|
239
|
+
--------
|
|
240
|
+
>>> is_fascinating(192) # '192' + '384' + '576' = '192384576'
|
|
241
|
+
True
|
|
242
|
+
>>> is_fascinating(273) # '273' + '546' + '819' = '273546819'
|
|
243
|
+
True
|
|
244
|
+
>>> is_fascinating(100)
|
|
245
|
+
False
|
|
246
|
+
>>> is_fascinating(123)
|
|
247
|
+
False
|
|
248
|
+
|
|
249
|
+
Notes
|
|
250
|
+
-----
|
|
251
|
+
Only 3-digit numbers qualify by definition. Numbers outside [100, 999]
|
|
252
|
+
always return False.
|
|
253
|
+
"""
|
|
254
|
+
if n < 100 or n > 999:
|
|
255
|
+
return False
|
|
256
|
+
s = str(n) + str(2 * n) + str(3 * n)
|
|
257
|
+
return len(s) == 9 and set(s) == set('123456789')
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# Trimorphic Number
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
@register(
|
|
265
|
+
name="Trimorphic",
|
|
266
|
+
category="digital",
|
|
267
|
+
description=(
|
|
268
|
+
"A number whose cube ends with the number itself. "
|
|
269
|
+
"Examples: 1, 5, 6, 25, 76, 376."
|
|
270
|
+
),
|
|
271
|
+
aliases=["trimorphic_number", "automorphic_cube"],
|
|
272
|
+
)
|
|
273
|
+
def is_trimorphic(n: int) -> bool:
|
|
274
|
+
"""Return True if n^3 ends with n.
|
|
275
|
+
|
|
276
|
+
Parameters
|
|
277
|
+
----------
|
|
278
|
+
n : int
|
|
279
|
+
|
|
280
|
+
Returns
|
|
281
|
+
-------
|
|
282
|
+
bool
|
|
283
|
+
|
|
284
|
+
Examples
|
|
285
|
+
--------
|
|
286
|
+
>>> is_trimorphic(5) # 5^3 = 125, ends in 5
|
|
287
|
+
True
|
|
288
|
+
>>> is_trimorphic(25) # 25^3 = 15625, ends in 25
|
|
289
|
+
True
|
|
290
|
+
>>> is_trimorphic(376) # 376^3 = 53157376, ends in 376
|
|
291
|
+
True
|
|
292
|
+
>>> is_trimorphic(4) # 4^3 = 64, does not end in 4
|
|
293
|
+
False
|
|
294
|
+
|
|
295
|
+
Notes
|
|
296
|
+
-----
|
|
297
|
+
n <= 0 always returns False.
|
|
298
|
+
"""
|
|
299
|
+
if n <= 0:
|
|
300
|
+
return False
|
|
301
|
+
return str(n ** 3).endswith(str(n))
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
# ---------------------------------------------------------------------------
|
|
305
|
+
# Twisted Prime
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
|
|
308
|
+
@register(
|
|
309
|
+
name="Twisted Prime",
|
|
310
|
+
category="primes",
|
|
311
|
+
description=(
|
|
312
|
+
"A prime number whose digit sum is also prime. "
|
|
313
|
+
"Examples: 2, 3, 5, 7, 11, 23, 29, 41."
|
|
314
|
+
),
|
|
315
|
+
aliases=["twisted_prime"],
|
|
316
|
+
)
|
|
317
|
+
def is_twisted_prime(n: int) -> bool:
|
|
318
|
+
"""Return True if n is prime and the sum of its digits is also prime.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
n : int
|
|
323
|
+
|
|
324
|
+
Returns
|
|
325
|
+
-------
|
|
326
|
+
bool
|
|
327
|
+
|
|
328
|
+
Examples
|
|
329
|
+
--------
|
|
330
|
+
>>> is_twisted_prime(2) # prime, digit sum 2 (prime)
|
|
331
|
+
True
|
|
332
|
+
>>> is_twisted_prime(23) # prime, digit sum 5 (prime)
|
|
333
|
+
True
|
|
334
|
+
>>> is_twisted_prime(13) # prime, digit sum 4 (not prime)
|
|
335
|
+
False
|
|
336
|
+
>>> is_twisted_prime(29) # prime, digit sum 11 (prime)
|
|
337
|
+
True
|
|
338
|
+
"""
|
|
339
|
+
if not is_prime(n):
|
|
340
|
+
return False
|
|
341
|
+
return is_prime(_digit_sum(n))
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
# Unique Number
|
|
346
|
+
# ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
@register(
|
|
349
|
+
name="Unique",
|
|
350
|
+
category="digital",
|
|
351
|
+
description=(
|
|
352
|
+
"A number with no repeated digits. "
|
|
353
|
+
"Examples: 1, 12, 123, 1234, 9876543210."
|
|
354
|
+
),
|
|
355
|
+
aliases=["unique_number", "unique_digit"],
|
|
356
|
+
)
|
|
357
|
+
def is_unique(n: int) -> bool:
|
|
358
|
+
"""Return True if n has no repeated digits.
|
|
359
|
+
|
|
360
|
+
Parameters
|
|
361
|
+
----------
|
|
362
|
+
n : int
|
|
363
|
+
|
|
364
|
+
Returns
|
|
365
|
+
-------
|
|
366
|
+
bool
|
|
367
|
+
|
|
368
|
+
Examples
|
|
369
|
+
--------
|
|
370
|
+
>>> is_unique(1234) # all digits distinct
|
|
371
|
+
True
|
|
372
|
+
>>> is_unique(1123) # digit 1 repeated
|
|
373
|
+
False
|
|
374
|
+
>>> is_unique(9876)
|
|
375
|
+
True
|
|
376
|
+
>>> is_unique(0)
|
|
377
|
+
True
|
|
378
|
+
|
|
379
|
+
Notes
|
|
380
|
+
-----
|
|
381
|
+
Single-digit numbers and 0 are always unique.
|
|
382
|
+
"""
|
|
383
|
+
s = str(abs(n))
|
|
384
|
+
return len(s) == len(set(s))
|
|
@@ -73,9 +73,7 @@ def is_centered_k_gonal(m: int, k: int) -> bool:
|
|
|
73
73
|
# k*n^2 - k*n - 2*(m-1) = 0
|
|
74
74
|
# discriminant: k^2 + 4*k*2*(m-1) = k^2 + 8*k*(m-1)
|
|
75
75
|
val = m - 1
|
|
76
|
-
|
|
77
|
-
# Check if 2*(m-1) is divisible by k for integer n check via disc
|
|
78
|
-
pass # disc check handles this
|
|
76
|
+
|
|
79
77
|
disc = k * k + 8 * k * val
|
|
80
78
|
if not _is_perfect_square(disc):
|
|
81
79
|
return False
|
|
@@ -799,8 +799,8 @@ def is_leyland_prime(n: int) -> bool:
|
|
|
799
799
|
-------
|
|
800
800
|
>>> is_leyland_prime(17) # 2^3 + 3^2 = 8 + 9 = 17
|
|
801
801
|
True
|
|
802
|
-
>>> is_leyland_prime(593) # 2^
|
|
803
|
-
|
|
802
|
+
>>> is_leyland_prime(593) # 2^9 + 9^2 = 512 + 81 = 593
|
|
803
|
+
True
|
|
804
804
|
>>> is_leyland_prime(32993) # 2^15 + 15^2 ... verified Leyland prime
|
|
805
805
|
True
|
|
806
806
|
|
|
@@ -388,7 +388,7 @@ def find_any_in_range(
|
|
|
388
388
|
return [n for n in range(start, end + 1) if any(f(n) for f in funcs)]
|
|
389
389
|
|
|
390
390
|
|
|
391
|
-
def most_special_in_range(start: int, end: int) -> int:
|
|
391
|
+
def most_special_in_range(start: int, end: int, verbose: bool = False) -> int:
|
|
392
392
|
"""Return the integer in ``[start, end]`` with the most ``True`` properties.
|
|
393
393
|
|
|
394
394
|
Ties are broken by returning the smallest such integer.
|
|
@@ -399,6 +399,8 @@ def most_special_in_range(start: int, end: int) -> int:
|
|
|
399
399
|
Lower bound (inclusive).
|
|
400
400
|
end:
|
|
401
401
|
Upper bound (inclusive).
|
|
402
|
+
verbose : bool, optional
|
|
403
|
+
If True, prints progress every 1000 numbers. Default False.
|
|
402
404
|
|
|
403
405
|
Returns
|
|
404
406
|
-------
|
|
@@ -412,9 +414,13 @@ def most_special_in_range(start: int, end: int) -> int:
|
|
|
412
414
|
"""
|
|
413
415
|
best_n = start
|
|
414
416
|
best_count = -1
|
|
415
|
-
|
|
417
|
+
total = end - start + 1
|
|
418
|
+
for i, n in enumerate(range(start, end + 1)):
|
|
416
419
|
c = count_properties(n)
|
|
417
420
|
if c > best_count:
|
|
418
421
|
best_count = c
|
|
419
422
|
best_n = n
|
|
423
|
+
if verbose and (i + 1) % 1000 == 0:
|
|
424
|
+
pct = (i + 1) / total * 100
|
|
425
|
+
print(f" most_special_in_range: {i+1}/{total} ({pct:.1f}%) — best so far: {best_n} ({best_count} props)")
|
|
420
426
|
return best_n
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "numclassify"
|
|
7
|
-
version = "0.3.
|
|
7
|
+
version = "0.3.3"
|
|
8
8
|
description = "The most comprehensive Python library for number classification - 3000+ number types"
|
|
9
9
|
authors = [{name = "Aratrik Ghosh", email = "aratrikghosh2011@gmail.com"}]
|
|
10
10
|
readme = "README.md"
|
|
@@ -346,3 +346,33 @@ def test_is_keith():
|
|
|
346
346
|
def test_is_factorial():
|
|
347
347
|
assert is_factorial(24) is True
|
|
348
348
|
assert is_factorial(25) is False
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
# ---------------------------------------------------------------------------
|
|
352
|
+
# Auto-generated crash tests — every registered type, 4 inputs
|
|
353
|
+
# ---------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
def _get_all_registered_funcs():
|
|
356
|
+
"""Return list of (name, func) for every canonical registry entry."""
|
|
357
|
+
from numclassify._registry import REGISTRY, _normalize
|
|
358
|
+
seen = set()
|
|
359
|
+
result = []
|
|
360
|
+
for key, entry in REGISTRY.items():
|
|
361
|
+
if key == _normalize(entry.name): # canonical key only
|
|
362
|
+
if key not in seen:
|
|
363
|
+
seen.add(key)
|
|
364
|
+
result.append((entry.name, entry.func))
|
|
365
|
+
return result
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@pytest.mark.parametrize("name,func", _get_all_registered_funcs())
|
|
369
|
+
@pytest.mark.parametrize("n", [0, 1, 2, -1])
|
|
370
|
+
def test_no_crash_on_edge_inputs(name, func, n):
|
|
371
|
+
"""Every registered type must not raise on inputs 0, 1, 2, -1."""
|
|
372
|
+
try:
|
|
373
|
+
result = func(n)
|
|
374
|
+
assert isinstance(result, bool), (
|
|
375
|
+
f"{name}({n}) returned {type(result)}, expected bool"
|
|
376
|
+
)
|
|
377
|
+
except Exception as e:
|
|
378
|
+
pytest.fail(f"{name}({n}) raised {type(e).__name__}: {e}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|