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,245 @@
1
+ """
2
+ numclassify._core.recreational
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Recreational / curiosity number classifications.
5
+
6
+ All functions are registered via ``@register`` and exposed as plain
7
+ module-level callables.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from numclassify._registry import register
13
+
14
+
15
+ @register(
16
+ name="Kaprekar",
17
+ category="recreational",
18
+ oeis="A006886",
19
+ description=(
20
+ "A number n such that n² can be split into two parts (no leading zero "
21
+ "on the right part) that sum to n. n=1 is included by convention."
22
+ ),
23
+ aliases=["kaprekar number"],
24
+ )
25
+ def is_kaprekar(n: int) -> bool:
26
+ """Return ``True`` if *n* is a Kaprekar number.
27
+
28
+ A positive integer *n* is Kaprekar if its square ``n²`` can be partitioned
29
+ at some point into a left part *A* and a right part *B* (where *B* > 0 and
30
+ has no leading zero) such that ``A + B == n``.
31
+
32
+ Special case: *n* = 1 is Kaprekar by convention (1² = 1, split as 0 + 1).
33
+
34
+ Parameters
35
+ ----------
36
+ n:
37
+ Positive integer.
38
+
39
+ Returns
40
+ -------
41
+ bool
42
+
43
+ Example
44
+ -------
45
+ >>> is_kaprekar(45) # 45²=2025; split (20, 25) → 20+25=45
46
+ True
47
+ >>> is_kaprekar(9) # 9²=81; split (8, 1) → 8+1=9
48
+ True
49
+ >>> is_kaprekar(1)
50
+ True
51
+ >>> is_kaprekar(100)
52
+ False
53
+ """
54
+ if n <= 0:
55
+ return False
56
+ if n == 1:
57
+ return True
58
+ sq = n * n
59
+ sq_str = str(sq)
60
+ length = len(sq_str)
61
+ for split in range(1, length):
62
+ left_str = sq_str[:length - split]
63
+ right_str = sq_str[length - split:]
64
+ # Right part must not start with '0' (leading zero check)
65
+ if right_str[0] == "0":
66
+ continue
67
+ left = int(left_str) if left_str else 0
68
+ right = int(right_str)
69
+ if right > 0 and left + right == n:
70
+ return True
71
+ return False
72
+
73
+
74
+ @register(
75
+ name="Automorphic",
76
+ category="recreational",
77
+ oeis="A003226",
78
+ description="A number whose square ends in the number itself.",
79
+ aliases=["automorphic number"],
80
+ )
81
+ def is_automorphic(n: int) -> bool:
82
+ """Return ``True`` if *n* is an automorphic number.
83
+
84
+ An automorphic number satisfies: the last *k* digits of *n²* equal *n*,
85
+ where *k* is the number of digits in *n*.
86
+
87
+ Parameters
88
+ ----------
89
+ n:
90
+ Non-negative integer.
91
+
92
+ Returns
93
+ -------
94
+ bool
95
+
96
+ Example
97
+ -------
98
+ >>> is_automorphic(5) # 5²=25, ends in 5
99
+ True
100
+ >>> is_automorphic(6) # 6²=36, ends in 6
101
+ True
102
+ >>> is_automorphic(76) # 76²=5776, ends in 76
103
+ True
104
+ >>> is_automorphic(7) # 7²=49, does not end in 7
105
+ False
106
+ """
107
+ if n < 0:
108
+ return False
109
+ sq = n * n
110
+ n_str = str(n)
111
+ sq_str = str(sq)
112
+ return sq_str.endswith(n_str)
113
+
114
+
115
+ @register(
116
+ name="Palindrome",
117
+ category="recreational",
118
+ oeis="A002113",
119
+ description=(
120
+ "A number whose decimal representation reads the same forwards and "
121
+ "backwards."
122
+ ),
123
+ aliases=["palindromic", "palindromic number"],
124
+ )
125
+ def is_palindrome(n: int) -> bool:
126
+ """Return ``True`` if *n* is a palindromic number.
127
+
128
+ A palindromic number reads the same in both directions in base-10.
129
+
130
+ Parameters
131
+ ----------
132
+ n:
133
+ Non-negative integer.
134
+
135
+ Returns
136
+ -------
137
+ bool
138
+
139
+ Example
140
+ -------
141
+ >>> is_palindrome(121)
142
+ True
143
+ >>> is_palindrome(1221)
144
+ True
145
+ >>> is_palindrome(123)
146
+ False
147
+ """
148
+ if n < 0:
149
+ return False
150
+ s = str(n)
151
+ return s == s[::-1]
152
+
153
+
154
+ @register(
155
+ name="Strobogrammatic",
156
+ category="recreational",
157
+ oeis="",
158
+ description=(
159
+ "A number that looks the same when rotated 180°. "
160
+ "Valid digits are 0, 1, 6, 8, 9 with map 0→0, 1→1, 6→9, 8→8, 9→6."
161
+ ),
162
+ aliases=["strobogrammatic number"],
163
+ )
164
+ def is_strobogrammatic(n: int) -> bool:
165
+ """Return ``True`` if *n* is a strobogrammatic number.
166
+
167
+ A strobogrammatic number appears the same when rotated 180°. Only the
168
+ digits 0, 1, 6, 8, 9 are valid; their rotated equivalents are
169
+ 0→0, 1→1, 6→9, 8→8, 9→6. The rotated string is the reverse of the
170
+ mapped digits.
171
+
172
+ Parameters
173
+ ----------
174
+ n:
175
+ Non-negative integer.
176
+
177
+ Returns
178
+ -------
179
+ bool
180
+
181
+ Example
182
+ -------
183
+ >>> is_strobogrammatic(69) # 69 rotated → 96... wait: 6→9, 9→6; reversed: '96' → same as '96' ✓
184
+ True
185
+ >>> is_strobogrammatic(88)
186
+ True
187
+ >>> is_strobogrammatic(1)
188
+ True
189
+ >>> is_strobogrammatic(6) # 6 alone rotated → '9' ≠ '6'
190
+ False
191
+ """
192
+ if n < 0:
193
+ return False
194
+ rotate_map = {"0": "0", "1": "1", "6": "9", "8": "8", "9": "6"}
195
+ s = str(n)
196
+ for ch in s:
197
+ if ch not in rotate_map:
198
+ return False
199
+ rotated = "".join(rotate_map[ch] for ch in reversed(s))
200
+ return rotated == s
201
+
202
+
203
+ @register(
204
+ name="Bouncy",
205
+ category="recreational",
206
+ oeis="A152054",
207
+ description=(
208
+ "A positive integer whose digits are neither all non-decreasing nor "
209
+ "all non-increasing."
210
+ ),
211
+ aliases=["bouncy number"],
212
+ )
213
+ def is_bouncy(n: int) -> bool:
214
+ """Return ``True`` if *n* is a bouncy number.
215
+
216
+ A bouncy number has digits that are neither monotonically non-decreasing
217
+ nor monotonically non-increasing. Numbers with fewer than three digits
218
+ can never be bouncy.
219
+
220
+ Parameters
221
+ ----------
222
+ n:
223
+ Positive integer.
224
+
225
+ Returns
226
+ -------
227
+ bool
228
+
229
+ Example
230
+ -------
231
+ >>> is_bouncy(155349)
232
+ True
233
+ >>> is_bouncy(134468) # non-decreasing → not bouncy
234
+ False
235
+ >>> is_bouncy(66420) # non-increasing → not bouncy
236
+ False
237
+ """
238
+ if n <= 0:
239
+ return False
240
+ digits = [int(d) for d in str(n)]
241
+ if len(digits) < 3:
242
+ return False
243
+ increasing = any(digits[i] < digits[i + 1] for i in range(len(digits) - 1))
244
+ decreasing = any(digits[i] > digits[i + 1] for i in range(len(digits) - 1))
245
+ return increasing and decreasing
@@ -0,0 +1,488 @@
1
+ """
2
+ numclassify/_core/sequences.py
3
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4
+ Sequence-membership number classification functions.
5
+
6
+ All sets are precomputed at module load time up to 10^9 for O(1) lookup.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from typing import Set
11
+
12
+ from numclassify._registry import register
13
+
14
+ _LIMIT = 10 ** 9
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Precomputed sets
18
+ # ---------------------------------------------------------------------------
19
+
20
+ def _gen_fibonacci() -> Set[int]:
21
+ s: Set[int] = set()
22
+ a, b = 0, 1
23
+ while a <= _LIMIT:
24
+ s.add(a)
25
+ a, b = b, a + b
26
+ return s
27
+
28
+
29
+ def _gen_lucas() -> Set[int]:
30
+ s: Set[int] = set()
31
+ a, b = 2, 1
32
+ while a <= _LIMIT:
33
+ s.add(a)
34
+ a, b = b, a + b
35
+ return s
36
+
37
+
38
+ def _gen_tribonacci() -> Set[int]:
39
+ s: Set[int] = set()
40
+ a, b, c = 0, 0, 1
41
+ while a <= _LIMIT:
42
+ s.add(a)
43
+ a, b, c = b, c, a + b + c
44
+ return s
45
+
46
+
47
+ def _gen_tetranacci() -> Set[int]:
48
+ s: Set[int] = set()
49
+ a, b, c, d = 0, 0, 0, 1
50
+ while a <= _LIMIT:
51
+ s.add(a)
52
+ a, b, c, d = b, c, d, a + b + c + d
53
+ return s
54
+
55
+
56
+ def _gen_pell() -> Set[int]:
57
+ s: Set[int] = set()
58
+ a, b = 0, 1
59
+ while a <= _LIMIT:
60
+ s.add(a)
61
+ a, b = b, 2 * b + a
62
+ return s
63
+
64
+
65
+ def _gen_jacobsthal() -> Set[int]:
66
+ s: Set[int] = set()
67
+ a, b = 0, 1
68
+ while a <= _LIMIT:
69
+ s.add(a)
70
+ a, b = b, b + 2 * a
71
+ return s
72
+
73
+
74
+ def _gen_padovan() -> Set[int]:
75
+ s: Set[int] = set()
76
+ # P(0)=1, P(1)=1, P(2)=1, P(n)=P(n-2)+P(n-3)
77
+ seq = [1, 1, 1]
78
+ for v in seq:
79
+ s.add(v)
80
+ while True:
81
+ nxt = seq[-2] + seq[-3]
82
+ if nxt > _LIMIT:
83
+ break
84
+ s.add(nxt)
85
+ seq.append(nxt)
86
+ return s
87
+
88
+
89
+ def _gen_perrin() -> Set[int]:
90
+ s: Set[int] = set()
91
+ # P(0)=3,P(1)=0,P(2)=2, P(n)=P(n-2)+P(n-3)
92
+ seq = [3, 0, 2]
93
+ for v in seq:
94
+ s.add(v)
95
+ while True:
96
+ nxt = seq[-2] + seq[-3]
97
+ if nxt > _LIMIT:
98
+ break
99
+ s.add(nxt)
100
+ seq.append(nxt)
101
+ return s
102
+
103
+
104
+ def _gen_catalan() -> Set[int]:
105
+ s: Set[int] = set()
106
+ # C(n) = C(2n,n)/(n+1); iterative: C(0)=1, C(n+1)=C(n)*2*(2n+1)/(n+2)
107
+ from fractions import Fraction
108
+ c = Fraction(1)
109
+ n = 0
110
+ while int(c) <= _LIMIT:
111
+ s.add(int(c))
112
+ c = c * 2 * (2 * n + 1) // (n + 2)
113
+ n += 1
114
+ return s
115
+
116
+
117
+ def _gen_bell() -> Set[int]:
118
+ s: Set[int] = set()
119
+ # Bell triangle
120
+ row = [1]
121
+ while row[0] <= _LIMIT:
122
+ s.add(row[0])
123
+ new_row = [row[-1]]
124
+ for i in range(len(row)):
125
+ new_row.append(new_row[-1] + row[i])
126
+ row = new_row
127
+ return s
128
+
129
+
130
+ def _gen_motzkin() -> Set[int]:
131
+ s: Set[int] = set()
132
+ # M(n+1) = M(n) + sum_{k=0}^{n-1} M(k)*M(n-1-k)
133
+ # Simpler recurrence: M(n) = ((2n+2)*M(n-1) + (3n-3)*M(n-2)) / (n+3) — but fractions needed
134
+ # Use: M(0)=1, M(1)=1, M(n)= M(n-1) + sum(M(k)*M(n-2-k) for k=0..n-2)
135
+ # Cleaner: (n+3)*M(n) = (2n+2)*M(n-1) + (3n-3)*M(n-2) [valid for n>=2, using n->n shifted]
136
+ # Actually standard: (n+2)*M(n) = (2n)*M(n-1) + 3*M(n-2) -- let's verify small:
137
+ # M(0)=1,M(1)=1,M(2)=2: (4)*2=(2*2)*1+3*1=4+3=7 WRONG
138
+ # Use direct: M(n+1) = M(n) + sum_{k=0}^{n-1} M(k)*M(n-1-k)
139
+ memo = [1, 1]
140
+ s.update(memo)
141
+ while True:
142
+ n = len(memo)
143
+ nxt = memo[n - 1] + sum(memo[k] * memo[n - 2 - k] for k in range(n - 1))
144
+ if nxt > _LIMIT:
145
+ break
146
+ s.add(nxt)
147
+ memo.append(nxt)
148
+ return s
149
+
150
+
151
+ def _gen_recaman() -> Set[int]:
152
+ s: Set[int] = set()
153
+ # a(0)=0; a(n) = a(n-1)-n if positive and not already in sequence, else a(n-1)+n
154
+ # Generate enough terms; sequence is non-decreasing in range so stop at _LIMIT
155
+ a = [0]
156
+ s.add(0)
157
+ seen = {0}
158
+ n = 1
159
+ while True:
160
+ prev = a[n - 1]
161
+ candidate = prev - n
162
+ if candidate > 0 and candidate not in seen:
163
+ val = candidate
164
+ else:
165
+ val = prev + n
166
+ if val > _LIMIT:
167
+ break
168
+ a.append(val)
169
+ seen.add(val)
170
+ s.add(val)
171
+ n += 1
172
+ if n > 100000: # safety cap
173
+ break
174
+ return s
175
+
176
+
177
+ def _gen_look_and_say() -> Set[int]:
178
+ s: Set[int] = set()
179
+ term = "1"
180
+ while int(term) <= _LIMIT:
181
+ s.add(int(term))
182
+ # generate next term
183
+ new_term = []
184
+ i = 0
185
+ while i < len(term):
186
+ ch = term[i]
187
+ count = 1
188
+ while i + count < len(term) and term[i + count] == ch:
189
+ count += 1
190
+ new_term.append(str(count) + ch)
191
+ i += count
192
+ term = "".join(new_term)
193
+ if len(term) > 10: # terms grow fast, int would exceed _LIMIT
194
+ break
195
+ return s
196
+
197
+
198
+ def _gen_kolakoski() -> Set[int]:
199
+ s: Set[int] = set()
200
+ # Kolakoski sequence: self-describing run-length encoding over {1,2}
201
+ # a(1)=1, a(2)=2, a(3)=2, ...
202
+ seq = [1, 2, 2]
203
+ for v in seq:
204
+ s.add(v)
205
+ write = 2 # index of element being "written" (0-based)
206
+ read = 2 # index of element being "read" to determine run length
207
+ # Only 1 and 2 are in this sequence
208
+ s.update({1, 2})
209
+ return s # Kolakoski only contains 1 and 2
210
+
211
+
212
+ def _gen_sylvester() -> Set[int]:
213
+ s: Set[int] = set()
214
+ a = 2
215
+ while a <= _LIMIT:
216
+ s.add(a)
217
+ a = a * (a - 1) + 1
218
+ return s
219
+
220
+
221
+ # Build all sets at module load
222
+ _FIBONACCI: Set[int] = _gen_fibonacci()
223
+ _LUCAS: Set[int] = _gen_lucas()
224
+ _TRIBONACCI: Set[int] = _gen_tribonacci()
225
+ _TETRANACCI: Set[int] = _gen_tetranacci()
226
+ _PELL: Set[int] = _gen_pell()
227
+ _JACOBSTHAL: Set[int] = _gen_jacobsthal()
228
+ _PADOVAN: Set[int] = _gen_padovan()
229
+ _PERRIN: Set[int] = _gen_perrin()
230
+ _CATALAN: Set[int] = _gen_catalan()
231
+ _BELL: Set[int] = _gen_bell()
232
+ _MOTZKIN: Set[int] = _gen_motzkin()
233
+ _RECAMAN: Set[int] = _gen_recaman()
234
+ _LOOK_AND_SAY: Set[int] = _gen_look_and_say()
235
+ _KOLAKOSKI: Set[int] = _gen_kolakoski()
236
+ _SYLVESTER: Set[int] = _gen_sylvester()
237
+
238
+ # ---------------------------------------------------------------------------
239
+ # Registered classifiers
240
+ # ---------------------------------------------------------------------------
241
+
242
+ @register(name="Fibonacci", category="sequences", oeis="A000045",
243
+ description="Member of the Fibonacci sequence.")
244
+ def is_fibonacci(n: int) -> bool:
245
+ """Return True if n is a Fibonacci number.
246
+
247
+ Parameters
248
+ ----------
249
+ n : int
250
+
251
+ Returns
252
+ -------
253
+ bool
254
+ """
255
+ return n >= 0 and n in _FIBONACCI
256
+
257
+
258
+ @register(name="Lucas", category="sequences", oeis="A000032",
259
+ description="Member of the Lucas sequence.")
260
+ def is_lucas(n: int) -> bool:
261
+ """Return True if n is a Lucas number.
262
+
263
+ Parameters
264
+ ----------
265
+ n : int
266
+
267
+ Returns
268
+ -------
269
+ bool
270
+ """
271
+ return n >= 0 and n in _LUCAS
272
+
273
+
274
+ @register(name="Tribonacci", category="sequences", oeis="A000073",
275
+ description="Member of the Tribonacci sequence.")
276
+ def is_tribonacci(n: int) -> bool:
277
+ """Return True if n is a Tribonacci number.
278
+
279
+ Parameters
280
+ ----------
281
+ n : int
282
+
283
+ Returns
284
+ -------
285
+ bool
286
+ """
287
+ return n >= 0 and n in _TRIBONACCI
288
+
289
+
290
+ @register(name="Tetranacci", category="sequences", oeis="A000288",
291
+ description="Member of the Tetranacci sequence.")
292
+ def is_tetranacci(n: int) -> bool:
293
+ """Return True if n is a Tetranacci number.
294
+
295
+ Parameters
296
+ ----------
297
+ n : int
298
+
299
+ Returns
300
+ -------
301
+ bool
302
+ """
303
+ return n >= 0 and n in _TETRANACCI
304
+
305
+
306
+ @register(name="Pell", category="sequences", oeis="A000129",
307
+ description="Member of the Pell sequence.")
308
+ def is_pell(n: int) -> bool:
309
+ """Return True if n is a Pell number.
310
+
311
+ Parameters
312
+ ----------
313
+ n : int
314
+
315
+ Returns
316
+ -------
317
+ bool
318
+ """
319
+ return n >= 0 and n in _PELL
320
+
321
+
322
+ @register(name="Jacobsthal", category="sequences", oeis="A001045",
323
+ description="Member of the Jacobsthal sequence.")
324
+ def is_jacobsthal(n: int) -> bool:
325
+ """Return True if n is a Jacobsthal number.
326
+
327
+ Parameters
328
+ ----------
329
+ n : int
330
+
331
+ Returns
332
+ -------
333
+ bool
334
+ """
335
+ return n >= 0 and n in _JACOBSTHAL
336
+
337
+
338
+ @register(name="Padovan", category="sequences", oeis="A000931",
339
+ description="Member of the Padovan sequence.")
340
+ def is_padovan(n: int) -> bool:
341
+ """Return True if n is a Padovan number.
342
+
343
+ Parameters
344
+ ----------
345
+ n : int
346
+
347
+ Returns
348
+ -------
349
+ bool
350
+ """
351
+ return n >= 0 and n in _PADOVAN
352
+
353
+
354
+ @register(name="Perrin", category="sequences", oeis="A001608",
355
+ description="Member of the Perrin sequence.")
356
+ def is_perrin(n: int) -> bool:
357
+ """Return True if n is a Perrin number.
358
+
359
+ Parameters
360
+ ----------
361
+ n : int
362
+
363
+ Returns
364
+ -------
365
+ bool
366
+ """
367
+ return n >= 0 and n in _PERRIN
368
+
369
+
370
+ @register(name="Catalan", category="sequences", oeis="A000108",
371
+ description="Member of the Catalan number sequence.")
372
+ def is_catalan(n: int) -> bool:
373
+ """Return True if n is a Catalan number.
374
+
375
+ Parameters
376
+ ----------
377
+ n : int
378
+
379
+ Returns
380
+ -------
381
+ bool
382
+
383
+ Examples
384
+ --------
385
+ >>> is_catalan(14)
386
+ True
387
+ >>> is_catalan(13)
388
+ False
389
+ """
390
+ return n >= 0 and n in _CATALAN
391
+
392
+
393
+ @register(name="Bell", category="sequences", oeis="A000110",
394
+ description="Member of the Bell number sequence.")
395
+ def is_bell(n: int) -> bool:
396
+ """Return True if n is a Bell number.
397
+
398
+ Parameters
399
+ ----------
400
+ n : int
401
+
402
+ Returns
403
+ -------
404
+ bool
405
+ """
406
+ return n >= 0 and n in _BELL
407
+
408
+
409
+ @register(name="Motzkin", category="sequences", oeis="A001006",
410
+ description="Member of the Motzkin sequence.")
411
+ def is_motzkin(n: int) -> bool:
412
+ """Return True if n is a Motzkin number.
413
+
414
+ Parameters
415
+ ----------
416
+ n : int
417
+
418
+ Returns
419
+ -------
420
+ bool
421
+ """
422
+ return n >= 0 and n in _MOTZKIN
423
+
424
+
425
+ @register(name="Recaman", category="sequences", oeis="A005132",
426
+ description="Member of the Recaman sequence.")
427
+ def is_recaman(n: int) -> bool:
428
+ """Return True if n appears in the Recaman sequence.
429
+
430
+ Parameters
431
+ ----------
432
+ n : int
433
+
434
+ Returns
435
+ -------
436
+ bool
437
+ """
438
+ return n >= 0 and n in _RECAMAN
439
+
440
+
441
+ @register(name="Look and Say", category="sequences", oeis="A005150",
442
+ description="Member of the look-and-say sequence.")
443
+ def is_look_and_say(n: int) -> bool:
444
+ """Return True if n is a term in the look-and-say sequence.
445
+
446
+ Parameters
447
+ ----------
448
+ n : int
449
+
450
+ Returns
451
+ -------
452
+ bool
453
+ """
454
+ return n >= 0 and n in _LOOK_AND_SAY
455
+
456
+
457
+ @register(name="Kolakoski", category="sequences", oeis="A000002",
458
+ description="Member of the Kolakoski sequence (only 1 and 2).")
459
+ def is_kolakoski(n: int) -> bool:
460
+ """Return True if n appears in the Kolakoski sequence.
461
+
462
+ The Kolakoski sequence only contains 1 and 2.
463
+
464
+ Parameters
465
+ ----------
466
+ n : int
467
+
468
+ Returns
469
+ -------
470
+ bool
471
+ """
472
+ return n in _KOLAKOSKI
473
+
474
+
475
+ @register(name="Sylvester", category="sequences", oeis="A000058",
476
+ description="Member of the Sylvester sequence.")
477
+ def is_sylvester(n: int) -> bool:
478
+ """Return True if n is a term in the Sylvester sequence.
479
+
480
+ Parameters
481
+ ----------
482
+ n : int
483
+
484
+ Returns
485
+ -------
486
+ bool
487
+ """
488
+ return n >= 0 and n in _SYLVESTER