numclassify 0.3.2.1__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.
Files changed (41) hide show
  1. {numclassify-0.3.2.1 → numclassify-0.3.3}/.gitignore +1 -0
  2. {numclassify-0.3.2.1 → numclassify-0.3.3}/CHANGELOG.md +23 -0
  3. {numclassify-0.3.2.1 → numclassify-0.3.3}/PKG-INFO +1 -1
  4. {numclassify-0.3.2.1 → numclassify-0.3.3}/docs/playground.html +1 -1
  5. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/__init__.py +67 -12
  6. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/_core/divisors.py +1 -1
  7. numclassify-0.3.3/numclassify/_core/exam_types.py +384 -0
  8. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/_core/figurate.py +1 -3
  9. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/_core/primes.py +2 -2
  10. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/_registry.py +8 -2
  11. {numclassify-0.3.2.1 → numclassify-0.3.3}/pyproject.toml +1 -1
  12. {numclassify-0.3.2.1 → numclassify-0.3.3}/tests/test_registry.py +30 -0
  13. {numclassify-0.3.2.1 → numclassify-0.3.3}/.github/workflows/ci.yml +0 -0
  14. {numclassify-0.3.2.1 → numclassify-0.3.3}/.github/workflows/docs.yml +0 -0
  15. {numclassify-0.3.2.1 → numclassify-0.3.3}/.github/workflows/publish.yml +0 -0
  16. {numclassify-0.3.2.1 → numclassify-0.3.3}/CONTRIBUTING.md +0 -0
  17. {numclassify-0.3.2.1 → numclassify-0.3.3}/LICENSE +0 -0
  18. {numclassify-0.3.2.1 → numclassify-0.3.3}/README.md +0 -0
  19. {numclassify-0.3.2.1 → numclassify-0.3.3}/docs/api.md +0 -0
  20. {numclassify-0.3.2.1 → numclassify-0.3.3}/docs/changelog.md +0 -0
  21. {numclassify-0.3.2.1 → numclassify-0.3.3}/docs/cli.md +0 -0
  22. {numclassify-0.3.2.1 → numclassify-0.3.3}/docs/extra/saffron.css +0 -0
  23. {numclassify-0.3.2.1 → numclassify-0.3.3}/docs/index.md +0 -0
  24. {numclassify-0.3.2.1 → numclassify-0.3.3}/docs/playground.css +0 -0
  25. {numclassify-0.3.2.1 → numclassify-0.3.3}/docs/playground.js +0 -0
  26. {numclassify-0.3.2.1 → numclassify-0.3.3}/examples/basic_usage.py +0 -0
  27. {numclassify-0.3.2.1 → numclassify-0.3.3}/examples/custom_type.py +0 -0
  28. {numclassify-0.3.2.1 → numclassify-0.3.3}/examples/find_perfect_numbers.py +0 -0
  29. {numclassify-0.3.2.1 → numclassify-0.3.3}/examples/random_classify.py +0 -0
  30. {numclassify-0.3.2.1 → numclassify-0.3.3}/examples/stream_large_range.py +0 -0
  31. {numclassify-0.3.2.1 → numclassify-0.3.3}/mkdocs.yml +0 -0
  32. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/__main__.py +0 -0
  33. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/_core/__init__.py +0 -0
  34. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/_core/combinatorial.py +0 -0
  35. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/_core/digital.py +0 -0
  36. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/_core/number_theory.py +0 -0
  37. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/_core/powers.py +0 -0
  38. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/_core/recreational.py +0 -0
  39. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/_core/sequences.py +0 -0
  40. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/cli.py +0 -0
  41. {numclassify-0.3.2.1 → numclassify-0.3.3}/numclassify/py.typed +0 -0
@@ -10,3 +10,4 @@ dist/
10
10
  .env
11
11
  .venv
12
12
  site/
13
+ .offline-markdown-preview/
@@ -6,11 +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
+
9
28
  ## [0.3.2.1] - 2026-06-12
10
29
 
11
30
  ### Fixed
31
+ - Batch classify input rejected commas/spaces — changed `type="number"` to `type="text"` with `inputmode="numeric"`
12
32
  - Search autocomplete dropdown not appearing — added fallback fetch of property names if initial Pyodide load fails, plus visible "Loading..." state in dropdown
13
33
 
34
+ ### Changed
35
+ - MkDocs site theme changed from indigo to saffron (#FF9933) via custom CSS
36
+
14
37
  ## [0.3.2] - 2026-06-12
15
38
 
16
39
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: numclassify
3
- Version: 0.3.2.1
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
@@ -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="number" id="input-classify" placeholder="Enter any integer... (or 6, 28, 496 for batch)" min="-999999" max="999999" />
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>
@@ -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, # count of True properties
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
- raw = get_true_properties(n)
94
- if isinstance(raw, dict):
95
- true_props = [k for k, v in raw.items() if v]
96
- else:
97
- true_props = list(raw)
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
- def stream(start: int, end: int):
154
- """Generator. Yields classify(n) for each n in [start, end]. Memory-safe for large ranges."""
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
- yield classify(n)
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 > 2 (checked up to length 6).")
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
- if 2 * val % k != 0:
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^7 + 7^2 = 128 + 49... wait, that's 177
803
- False
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
- for n in range(start, end + 1):
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.2.1"
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