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.
@@ -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