vedicmaths 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,107 @@
1
+ """Vedic mathematics helpers with optional step-by-step workings."""
2
+
3
+ from .arithmetic import (
4
+ OperationTrace,
5
+ Step,
6
+ all_from_9_last_from_10,
7
+ anurupyena_multiply,
8
+ anurupyena_steps,
9
+ casting_out_nines_check,
10
+ complement,
11
+ cube_ending_in_5,
12
+ cube_root_if_perfect,
13
+ digit_sum,
14
+ digital_root,
15
+ divide_by_5,
16
+ divide_by_25,
17
+ fraction_to_percent,
18
+ is_divisible_by_3,
19
+ is_divisible_by_9,
20
+ is_divisible_by_11,
21
+ left_to_right_add,
22
+ left_to_right_add_steps,
23
+ multiply_by_5,
24
+ multiply_by_9,
25
+ multiply_by_11,
26
+ multiply_by_11_steps,
27
+ multiply_by_12,
28
+ multiply_by_15,
29
+ multiply_by_25,
30
+ multiply_by_50,
31
+ multiply_by_75,
32
+ multiply_by_99,
33
+ multiply_by_125,
34
+ multiply_by_999,
35
+ multiply_by_repeated_9,
36
+ nikhilam_multiply,
37
+ nikhilam_steps,
38
+ osculator_for_ending_9,
39
+ percent_of,
40
+ recurring_decimal_unit_fraction,
41
+ square_ending_in_5,
42
+ square_ending_in_5_steps,
43
+ square_near_50,
44
+ square_near_base,
45
+ square_near_base_steps,
46
+ square_root_if_perfect,
47
+ subtract_using_complement,
48
+ subtract_using_complement_steps,
49
+ sum_digits_until_single,
50
+ transpose_percent,
51
+ urdhva_tiryagbhyam_multiply,
52
+ urdhva_tiryagbhyam_steps,
53
+ vinculum_digits,
54
+ )
55
+
56
+ __all__ = [
57
+ "OperationTrace",
58
+ "Step",
59
+ "all_from_9_last_from_10",
60
+ "anurupyena_multiply",
61
+ "anurupyena_steps",
62
+ "casting_out_nines_check",
63
+ "complement",
64
+ "cube_ending_in_5",
65
+ "cube_root_if_perfect",
66
+ "digit_sum",
67
+ "digital_root",
68
+ "divide_by_5",
69
+ "divide_by_25",
70
+ "fraction_to_percent",
71
+ "is_divisible_by_3",
72
+ "is_divisible_by_9",
73
+ "is_divisible_by_11",
74
+ "left_to_right_add",
75
+ "left_to_right_add_steps",
76
+ "multiply_by_5",
77
+ "multiply_by_9",
78
+ "multiply_by_11",
79
+ "multiply_by_11_steps",
80
+ "multiply_by_12",
81
+ "multiply_by_15",
82
+ "multiply_by_25",
83
+ "multiply_by_50",
84
+ "multiply_by_75",
85
+ "multiply_by_99",
86
+ "multiply_by_125",
87
+ "multiply_by_999",
88
+ "multiply_by_repeated_9",
89
+ "nikhilam_multiply",
90
+ "nikhilam_steps",
91
+ "osculator_for_ending_9",
92
+ "percent_of",
93
+ "recurring_decimal_unit_fraction",
94
+ "square_ending_in_5",
95
+ "square_ending_in_5_steps",
96
+ "square_near_50",
97
+ "square_near_base",
98
+ "square_near_base_steps",
99
+ "square_root_if_perfect",
100
+ "subtract_using_complement",
101
+ "subtract_using_complement_steps",
102
+ "sum_digits_until_single",
103
+ "transpose_percent",
104
+ "urdhva_tiryagbhyam_multiply",
105
+ "urdhva_tiryagbhyam_steps",
106
+ "vinculum_digits",
107
+ ]
@@ -0,0 +1,683 @@
1
+ """Arithmetic techniques inspired by common Vedic mathematics methods.
2
+
3
+ The functions return plain integer answers by default. Companion ``*_steps``
4
+ functions return a trace that can be rendered in notebooks, CLIs, or teaching
5
+ apps.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class Step:
15
+ """One human-readable step in a worked calculation."""
16
+
17
+ title: str
18
+ expression: str
19
+ value: int | str
20
+
21
+
22
+ @dataclass(frozen=True)
23
+ class OperationTrace:
24
+ """A worked calculation result."""
25
+
26
+ method: str
27
+ inputs: tuple[int, ...]
28
+ result: int
29
+ steps: tuple[Step, ...]
30
+
31
+
32
+ def _sign(value: int) -> int:
33
+ return -1 if value < 0 else 1
34
+
35
+
36
+ def _digits_reversed(value: int) -> list[int]:
37
+ return [int(char) for char in reversed(str(abs(value)))]
38
+
39
+
40
+ def _require_non_negative(value: int, name: str) -> None:
41
+ if value < 0:
42
+ raise ValueError(f"{name} must be non-negative")
43
+
44
+
45
+ def _nearest_power_of_ten(value: int) -> int:
46
+ if value == 0:
47
+ return 10
48
+ digits = len(str(abs(value)))
49
+ lower = 10 ** (digits - 1)
50
+ upper = 10**digits
51
+ return lower if abs(value - lower) < abs(value - upper) else upper
52
+
53
+
54
+ def digital_root(number: int) -> int:
55
+ """Return the digital root of an integer.
56
+
57
+ ``digital_root(0)`` is ``0``. Negative values are treated by magnitude,
58
+ which keeps the helper useful for arithmetic verification.
59
+ """
60
+
61
+ number = abs(number)
62
+ if number == 0:
63
+ return 0
64
+ return 1 + ((number - 1) % 9)
65
+
66
+
67
+ def digit_sum(number: int) -> int:
68
+ """Return the sum of decimal digits in ``number``."""
69
+
70
+ return sum(int(char) for char in str(abs(number)))
71
+
72
+
73
+ def sum_digits_until_single(number: int) -> int:
74
+ """Repeatedly sum digits until a single digit remains."""
75
+
76
+ return digital_root(number)
77
+
78
+
79
+ def complement(number: int, base: int | None = None) -> int:
80
+ """Return the complement of ``number`` from a base.
81
+
82
+ If no base is supplied, the next power of ten is used. For example,
83
+ ``complement(87)`` returns ``13`` because ``100 - 87 = 13``.
84
+ """
85
+
86
+ _require_non_negative(number, "number")
87
+ if base is None:
88
+ base = 10 ** len(str(number))
89
+ if base <= number:
90
+ raise ValueError("base must be greater than number")
91
+ return base - number
92
+
93
+
94
+ def all_from_9_last_from_10(number: int) -> int:
95
+ """Return the Nikhilam complement from the next power of ten."""
96
+
97
+ return complement(number)
98
+
99
+
100
+ def left_to_right_add(a: int, b: int) -> int:
101
+ """Add two non-negative integers from left to right."""
102
+
103
+ return left_to_right_add_steps(a, b).result
104
+
105
+
106
+ def left_to_right_add_steps(a: int, b: int) -> OperationTrace:
107
+ """Return a trace for left-to-right addition with carries."""
108
+
109
+ _require_non_negative(a, "a")
110
+ _require_non_negative(b, "b")
111
+ width = max(len(str(a)), len(str(b)))
112
+ left = str(a).zfill(width)
113
+ right = str(b).zfill(width)
114
+ carry = 0
115
+ digits: list[str] = []
116
+ steps: list[Step] = []
117
+
118
+ for index in range(width - 1, -1, -1):
119
+ total = int(left[index]) + int(right[index]) + carry
120
+ digits.append(str(total % 10))
121
+ steps.append(
122
+ Step(
123
+ f"Column {width - index}",
124
+ f"{left[index]} + {right[index]} + carry {carry}",
125
+ f"write {total % 10}, carry {total // 10}",
126
+ )
127
+ )
128
+ carry = total // 10
129
+ if carry:
130
+ digits.append(str(carry))
131
+ steps.append(Step("Final carry", str(carry), carry))
132
+
133
+ result = int("".join(reversed(digits)))
134
+ return OperationTrace(
135
+ method="Left-to-right addition",
136
+ inputs=(a, b),
137
+ result=result,
138
+ steps=tuple(reversed(steps)),
139
+ )
140
+
141
+
142
+ def subtract_using_complement(a: int, b: int) -> int:
143
+ """Subtract ``b`` from ``a`` using complements."""
144
+
145
+ return subtract_using_complement_steps(a, b).result
146
+
147
+
148
+ def subtract_using_complement_steps(a: int, b: int) -> OperationTrace:
149
+ """Return a trace for subtraction by complement."""
150
+
151
+ _require_non_negative(a, "a")
152
+ _require_non_negative(b, "b")
153
+ if b > a:
154
+ trace = subtract_using_complement_steps(b, a)
155
+ return OperationTrace(
156
+ method="Subtraction by complement",
157
+ inputs=(a, b),
158
+ result=-trace.result,
159
+ steps=trace.steps + (Step("Apply sign", f"{a} - {b}", -trace.result),),
160
+ )
161
+ base = 10 ** max(len(str(a)), len(str(b)))
162
+ comp = base - b
163
+ raw = a + comp
164
+ result = raw - base
165
+ return OperationTrace(
166
+ method="Subtraction by complement",
167
+ inputs=(a, b),
168
+ result=result,
169
+ steps=(
170
+ Step("Choose base", f"base = {base}", base),
171
+ Step("Find complement", f"{base} - {b}", comp),
172
+ Step("Add complement", f"{a} + {comp}", raw),
173
+ Step("Drop base", f"{raw} - {base}", result),
174
+ ),
175
+ )
176
+
177
+
178
+ def vinculum_digits(number: int) -> tuple[int, ...]:
179
+ """Represent a number with vinculum-style signed digits.
180
+
181
+ Digits greater than 5 are replaced by their negative complement and a carry
182
+ is moved left. The tuple is ordered from most significant to least.
183
+ """
184
+
185
+ _require_non_negative(number, "number")
186
+ digits = _digits_reversed(number)
187
+ result: list[int] = []
188
+ carry = 0
189
+ for digit in digits:
190
+ value = digit + carry
191
+ if value > 5:
192
+ result.append(value - 10)
193
+ carry = 1
194
+ else:
195
+ result.append(value)
196
+ carry = 0
197
+ if carry:
198
+ result.append(carry)
199
+ return tuple(reversed(result))
200
+
201
+
202
+ def multiply_by_9(number: int) -> int:
203
+ """Multiply by 9 using ``10n - n``."""
204
+
205
+ return number * 10 - number
206
+
207
+
208
+ def multiply_by_99(number: int) -> int:
209
+ """Multiply by 99 using ``100n - n``."""
210
+
211
+ return number * 100 - number
212
+
213
+
214
+ def multiply_by_999(number: int) -> int:
215
+ """Multiply by 999 using ``1000n - n``."""
216
+
217
+ return number * 1000 - number
218
+
219
+
220
+ def multiply_by_repeated_9(number: int, count: int) -> int:
221
+ """Multiply by a number made of ``count`` nines, such as 9, 99, or 999."""
222
+
223
+ if count <= 0:
224
+ raise ValueError("count must be positive")
225
+ return number * ((10**count) - 1)
226
+
227
+
228
+ def multiply_by_11(number: int) -> int:
229
+ """Multiply by 11 using the adjacent-sum shortcut."""
230
+
231
+ return multiply_by_11_steps(number).result
232
+
233
+
234
+ def multiply_by_11_steps(number: int) -> OperationTrace:
235
+ """Return a trace for multiplying by 11."""
236
+
237
+ sign = _sign(number)
238
+ digits = [int(char) for char in str(abs(number))]
239
+ work = [digits[0]]
240
+ steps = [Step("First digit", str(digits[0]), digits[0])]
241
+ for left, right in zip(digits, digits[1:]):
242
+ total = left + right
243
+ work.append(total)
244
+ steps.append(Step("Adjacent sum", f"{left} + {right}", total))
245
+ if len(digits) > 1:
246
+ work.append(digits[-1])
247
+ steps.append(Step("Last digit", str(digits[-1]), digits[-1]))
248
+ carry = 0
249
+ output: list[int] = []
250
+ for value in reversed(work):
251
+ total = value + carry
252
+ output.append(total % 10)
253
+ carry = total // 10
254
+ while carry:
255
+ output.append(carry % 10)
256
+ carry //= 10
257
+ result = int("".join(str(digit) for digit in reversed(output))) * sign
258
+ return OperationTrace(
259
+ method="Multiply by 11 using adjacent sums",
260
+ inputs=(number,),
261
+ result=result,
262
+ steps=tuple(steps + [Step("Carry and combine", str(work), result)]),
263
+ )
264
+
265
+
266
+ def multiply_by_12(number: int) -> int:
267
+ """Multiply by 12 as ``10n + 2n``."""
268
+
269
+ return number * 10 + number * 2
270
+
271
+
272
+ def multiply_by_15(number: int) -> int:
273
+ """Multiply by 15 as ``10n + half of 10n``."""
274
+
275
+ return number * 10 + number * 5
276
+
277
+
278
+ def multiply_by_25(number: int) -> int:
279
+ """Multiply by 25 as ``100n / 4``."""
280
+
281
+ return (number * 100) // 4
282
+
283
+
284
+ def multiply_by_5(number: int) -> int:
285
+ """Multiply by 5 as ``10n / 2``."""
286
+
287
+ return (number * 10) // 2
288
+
289
+
290
+ def multiply_by_50(number: int) -> int:
291
+ """Multiply by 50 as ``100n / 2``."""
292
+
293
+ return (number * 100) // 2
294
+
295
+
296
+ def multiply_by_125(number: int) -> int:
297
+ """Multiply by 125 as ``1000n / 8``."""
298
+
299
+ return (number * 1000) // 8
300
+
301
+
302
+ def multiply_by_75(number: int) -> int:
303
+ """Multiply by 75 as ``3 * 100n / 4``."""
304
+
305
+ return (number * 300) // 4
306
+
307
+
308
+ def square_near_base(number: int, base: int | None = None) -> int:
309
+ """Square a number near a power-of-ten base."""
310
+
311
+ return square_near_base_steps(number, base).result
312
+
313
+
314
+ def square_near_base_steps(number: int, base: int | None = None) -> OperationTrace:
315
+ """Return a trace for squaring a number near a base."""
316
+
317
+ if base is None:
318
+ base = _nearest_power_of_ten(abs(number))
319
+ if base <= 0:
320
+ raise ValueError("base must be positive")
321
+ x = abs(number)
322
+ diff = x - base
323
+ left = x + diff
324
+ right = diff * diff
325
+ result = (left * base) + right
326
+ return OperationTrace(
327
+ method="Squaring near a base",
328
+ inputs=(number,),
329
+ result=result,
330
+ steps=(
331
+ Step("Choose base", f"base = {base}", base),
332
+ Step("Find deviation", f"{x} - {base}", diff),
333
+ Step("Add deviation", f"{x} + ({diff})", left),
334
+ Step("Square deviation", f"{diff} * {diff}", right),
335
+ Step("Combine", f"({left} * {base}) + {right}", result),
336
+ ),
337
+ )
338
+
339
+
340
+ def square_near_50(number: int) -> int:
341
+ """Square a number near 50 using base 100 and half adjustment."""
342
+
343
+ diff = number - 50
344
+ return ((25 + diff) * 100) + (diff * diff)
345
+
346
+
347
+ def cube_ending_in_5(number: int) -> int:
348
+ """Return the cube of a number ending in 5."""
349
+
350
+ if abs(number) % 10 != 5:
351
+ raise ValueError("number must end in 5")
352
+ return number**3
353
+
354
+
355
+ def divide_by_5(number: int) -> float:
356
+ """Divide by 5 as ``2n / 10``."""
357
+
358
+ return (number * 2) / 10
359
+
360
+
361
+ def divide_by_25(number: int) -> float:
362
+ """Divide by 25 as ``4n / 100``."""
363
+
364
+ return (number * 4) / 100
365
+
366
+
367
+ def fraction_to_percent(numerator: int, denominator: int) -> float:
368
+ """Convert a fraction to percent."""
369
+
370
+ if denominator == 0:
371
+ raise ZeroDivisionError("denominator cannot be zero")
372
+ return (numerator * 100) / denominator
373
+
374
+
375
+ def percent_of(percent: float, number: float) -> float:
376
+ """Find ``percent`` percent of ``number``."""
377
+
378
+ return (percent / 100) * number
379
+
380
+
381
+ def transpose_percent(percent: float, number: float) -> float:
382
+ """Use ``a% of b = b% of a``."""
383
+
384
+ return percent_of(number, percent)
385
+
386
+
387
+ def is_divisible_by_3(number: int) -> bool:
388
+ """Return whether ``number`` is divisible by 3."""
389
+
390
+ return digit_sum(number) % 3 == 0
391
+
392
+
393
+ def is_divisible_by_9(number: int) -> bool:
394
+ """Return whether ``number`` is divisible by 9."""
395
+
396
+ return digit_sum(number) % 9 == 0
397
+
398
+
399
+ def is_divisible_by_11(number: int) -> bool:
400
+ """Return whether ``number`` is divisible by 11."""
401
+
402
+ digits = [int(char) for char in str(abs(number))]
403
+ alternating = sum(digits[::2]) - sum(digits[1::2])
404
+ return alternating % 11 == 0
405
+
406
+
407
+ def casting_out_nines_check(a: int, b: int, result: int, operator: str = "*") -> bool:
408
+ """Check an arithmetic result using the digital-root method.
409
+
410
+ Supported operators are ``"+"``, ``"-"``, and ``"*"``. This is a
411
+ consistency check, not a proof: different wrong answers can share the same
412
+ digital root.
413
+ """
414
+
415
+ if operator == "+":
416
+ expected = digital_root(digital_root(a) + digital_root(b))
417
+ elif operator == "-":
418
+ expected = digital_root(digital_root(a) - digital_root(b))
419
+ elif operator == "*":
420
+ expected = digital_root(digital_root(a) * digital_root(b))
421
+ else:
422
+ raise ValueError("operator must be one of '+', '-', or '*'")
423
+ return expected == digital_root(result)
424
+
425
+
426
+ def nikhilam_multiply(a: int, b: int, base: int | None = None) -> int:
427
+ """Multiply using the Nikhilam base method and return the result."""
428
+
429
+ return nikhilam_steps(a, b, base).result
430
+
431
+
432
+ def anurupyena_multiply(
433
+ a: int,
434
+ b: int,
435
+ working_base: int,
436
+ theoretical_base: int | None = None,
437
+ ) -> int:
438
+ """Multiply using a proportionate working base.
439
+
440
+ This follows the Anurupyena idea used when numbers are nearer to a
441
+ convenient multiple or sub-multiple of a power-of-ten base. For example,
442
+ ``41 * 41`` can use working base ``50`` and theoretical base ``100``.
443
+ """
444
+
445
+ return anurupyena_steps(a, b, working_base, theoretical_base).result
446
+
447
+
448
+ def anurupyena_steps(
449
+ a: int,
450
+ b: int,
451
+ working_base: int,
452
+ theoretical_base: int | None = None,
453
+ ) -> OperationTrace:
454
+ """Return a trace for proportionate-base multiplication."""
455
+
456
+ if working_base <= 0:
457
+ raise ValueError("working_base must be positive")
458
+ if theoretical_base is None:
459
+ theoretical_base = 10 ** len(str(abs(working_base)))
460
+ if theoretical_base <= 0:
461
+ raise ValueError("theoretical_base must be positive")
462
+ if theoretical_base % working_base != 0 and working_base % theoretical_base != 0:
463
+ raise ValueError("bases must have an integer multiple/sub-multiple relationship")
464
+
465
+ sign = _sign(a) * _sign(b)
466
+ x, y = abs(a), abs(b)
467
+ diff_x = x - working_base
468
+ diff_y = y - working_base
469
+ cross = x + diff_y
470
+ right = diff_x * diff_y
471
+ scale_num = working_base
472
+ scale_den = theoretical_base
473
+ left_scaled = (cross * scale_num) // scale_den
474
+ if (cross * scale_num) % scale_den != 0:
475
+ raise ValueError("scaled left side is fractional for these bases")
476
+ result = sign * ((left_scaled * theoretical_base) + right)
477
+
478
+ return OperationTrace(
479
+ method="Anurupyena: proportionately",
480
+ inputs=(a, b),
481
+ result=result,
482
+ steps=(
483
+ Step("Choose bases", f"working {working_base}, theoretical {theoretical_base}", working_base),
484
+ Step("Find deviations", f"{x} - {working_base}, {y} - {working_base}", f"{diff_x}, {diff_y}"),
485
+ Step("Cross add/subtract", f"{x} + ({diff_y})", cross),
486
+ Step("Scale left side", f"{cross} * {working_base} / {theoretical_base}", left_scaled),
487
+ Step("Multiply deviations", f"({diff_x}) * ({diff_y})", right),
488
+ Step("Combine", f"({left_scaled} * {theoretical_base}) + {right}", result),
489
+ ),
490
+ )
491
+
492
+
493
+ def recurring_decimal_unit_fraction(denominator: int, limit: int | None = None) -> str:
494
+ """Return the recurring decimal expansion of ``1 / denominator``.
495
+
496
+ The helper uses remainder tracking, which mirrors the teaching focus on
497
+ recurring decimal cycles while staying reliable for any integer denominator.
498
+ Repeating parts are wrapped in parentheses, e.g. ``1/19 = 0.(052631...)``.
499
+ """
500
+
501
+ if denominator == 0:
502
+ raise ZeroDivisionError("denominator cannot be zero")
503
+ sign = "-" if denominator < 0 else ""
504
+ denominator = abs(denominator)
505
+ remainder = 1 % denominator
506
+ integer = 1 // denominator
507
+ seen: dict[int, int] = {}
508
+ digits: list[str] = []
509
+
510
+ while remainder and remainder not in seen:
511
+ if limit is not None and len(digits) >= limit:
512
+ return f"{sign}{integer}." + "".join(digits)
513
+ seen[remainder] = len(digits)
514
+ remainder *= 10
515
+ digits.append(str(remainder // denominator))
516
+ remainder %= denominator
517
+
518
+ if remainder == 0:
519
+ return f"{sign}{integer}." + "".join(digits)
520
+ start = seen[remainder]
521
+ non_repeating = "".join(digits[:start])
522
+ repeating = "".join(digits[start:])
523
+ return f"{sign}{integer}.{non_repeating}({repeating})"
524
+
525
+
526
+ def osculator_for_ending_9(divisor: int) -> int:
527
+ """Return the Ekadhika-style osculator for divisors ending in 9."""
528
+
529
+ if divisor % 10 != 9:
530
+ raise ValueError("divisor must end in 9")
531
+ return (abs(divisor) // 10) + 1
532
+
533
+
534
+ def square_root_if_perfect(number: int) -> int | None:
535
+ """Return the square root when ``number`` is a perfect square."""
536
+
537
+ if number < 0:
538
+ return None
539
+ root = int(number**0.5)
540
+ if root * root == number:
541
+ return root
542
+ if (root + 1) * (root + 1) == number:
543
+ return root + 1
544
+ return None
545
+
546
+
547
+ def cube_root_if_perfect(number: int) -> int | None:
548
+ """Return the cube root when ``number`` is a perfect cube."""
549
+
550
+ sign = _sign(number)
551
+ value = abs(number)
552
+ root = round(value ** (1 / 3))
553
+ for candidate in range(max(0, root - 2), root + 3):
554
+ if candidate**3 == value:
555
+ return candidate * sign
556
+ return None
557
+
558
+
559
+ def nikhilam_steps(a: int, b: int, base: int | None = None) -> OperationTrace:
560
+ """Work multiplication around a nearby power-of-ten base.
561
+
562
+ Example: ``98 * 97`` uses base ``100``. Differences are ``-2`` and ``-3``;
563
+ left side is ``98 - 3 = 95`` and right side is ``(-2) * (-3) = 6``, padded
564
+ to two digits, giving ``9506``.
565
+ """
566
+
567
+ if base is None:
568
+ highest_digits = max(len(str(abs(a))), len(str(abs(b))))
569
+ base = 10**highest_digits
570
+ if base <= 0:
571
+ raise ValueError("base must be a positive integer")
572
+
573
+ sign = _sign(a) * _sign(b)
574
+ x, y = abs(a), abs(b)
575
+ diff_x = x - base
576
+ diff_y = y - base
577
+ left = x + diff_y
578
+ right = diff_x * diff_y
579
+ result = sign * ((left * base) + right)
580
+
581
+ return OperationTrace(
582
+ method="Nikhilam: all from 9 and the last from 10",
583
+ inputs=(a, b),
584
+ result=result,
585
+ steps=(
586
+ Step("Choose base", f"base = {base}", base),
587
+ Step("Find deviations", f"{x} - {base}, {y} - {base}", f"{diff_x}, {diff_y}"),
588
+ Step("Cross subtract", f"{x} + ({diff_y})", left),
589
+ Step("Multiply deviations", f"({diff_x}) * ({diff_y})", right),
590
+ Step("Combine", f"({left} * {base}) + ({right})", result),
591
+ ),
592
+ )
593
+
594
+
595
+ def urdhva_tiryagbhyam_multiply(a: int, b: int) -> int:
596
+ """Multiply using the vertically-and-crosswise digit method."""
597
+
598
+ return urdhva_tiryagbhyam_steps(a, b).result
599
+
600
+
601
+ def urdhva_tiryagbhyam_steps(a: int, b: int) -> OperationTrace:
602
+ """Return a trace for vertically-and-crosswise multiplication.
603
+
604
+ This implementation handles any integer size by convolving digit columns
605
+ from right to left and carrying forward.
606
+ """
607
+
608
+ sign = _sign(a) * _sign(b)
609
+ left_digits = _digits_reversed(a)
610
+ right_digits = _digits_reversed(b)
611
+ column_count = len(left_digits) + len(right_digits) - 1
612
+ carry = 0
613
+ output_digits: list[int] = []
614
+ steps: list[Step] = []
615
+
616
+ for column in range(column_count):
617
+ pairs = [
618
+ (i, column - i)
619
+ for i in range(len(left_digits))
620
+ if 0 <= column - i < len(right_digits)
621
+ ]
622
+ subtotal = sum(left_digits[i] * right_digits[j] for i, j in pairs)
623
+ total = subtotal + carry
624
+ output_digits.append(total % 10)
625
+ next_carry = total // 10
626
+ expression = " + ".join(
627
+ f"{left_digits[i]}*{right_digits[j]}" for i, j in pairs
628
+ )
629
+ if carry:
630
+ expression = f"{expression} + carry {carry}"
631
+ steps.append(
632
+ Step(
633
+ f"Column {column + 1}",
634
+ expression,
635
+ f"write {total % 10}, carry {next_carry}",
636
+ )
637
+ )
638
+ carry = next_carry
639
+
640
+ while carry:
641
+ output_digits.append(carry % 10)
642
+ steps.append(Step("Carry", str(carry), f"write {carry % 10}"))
643
+ carry //= 10
644
+
645
+ result = int("".join(str(digit) for digit in reversed(output_digits))) * sign
646
+ steps.append(Step("Apply sign", f"sign({a}) * sign({b})", result))
647
+
648
+ return OperationTrace(
649
+ method="Urdhva Tiryagbhyam: vertically and crosswise",
650
+ inputs=(a, b),
651
+ result=result,
652
+ steps=tuple(steps),
653
+ )
654
+
655
+
656
+ def square_ending_in_5(number: int) -> int:
657
+ """Square an integer ending in 5 using the Ekadhikena Purvena pattern."""
658
+
659
+ return square_ending_in_5_steps(number).result
660
+
661
+
662
+ def square_ending_in_5_steps(number: int) -> OperationTrace:
663
+ """Return a trace for squaring numbers ending in 5.
664
+
665
+ For ``n = 10a + 5``, ``n^2 = a * (a + 1)`` followed by ``25``.
666
+ """
667
+
668
+ if abs(number) % 10 != 5:
669
+ raise ValueError("number must end in 5")
670
+
671
+ prefix = abs(number) // 10
672
+ left = prefix * (prefix + 1)
673
+ result = (left * 100) + 25
674
+ return OperationTrace(
675
+ method="Ekadhikena Purvena: by one more than the previous one",
676
+ inputs=(number,),
677
+ result=result,
678
+ steps=(
679
+ Step("Separate prefix", f"{abs(number)} = 10*{prefix} + 5", prefix),
680
+ Step("Multiply by next number", f"{prefix} * {prefix + 1}", left),
681
+ Step("Append 25", f"{left} followed by 25", result),
682
+ ),
683
+ )
vedicmaths/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Compatibility import alias for the vedicmathematics package."""
2
+
3
+ from vedicmathematics import * # noqa: F401,F403
4
+ from vedicmathematics import __all__ # noqa: F401
@@ -0,0 +1,206 @@
1
+ Metadata-Version: 2.4
2
+ Name: vedicmaths
3
+ Version: 0.1.0
4
+ Summary: Python helpers for learning and applying Vedic mathematics techniques.
5
+ Author: Karthik Srinivasan
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/ksriniv2/vedicmaths
8
+ Project-URL: Issues, https://github.com/ksriniv2/vedicmaths/issues
9
+ Keywords: vedic mathematics,vedic maths,mental math,math,education,arithmetic
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Education
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Education
17
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
18
+ Requires-Python: >=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # Vedic Mathematics
26
+
27
+ Python helpers for learning and applying Vedic mathematics techniques. The
28
+ library focuses on two things:
29
+
30
+ - returning correct arithmetic results as simple Python values
31
+ - exposing step-by-step workings that teachers, students, notebooks, and apps
32
+ can display
33
+
34
+ This project is early-stage and intended to grow into an open-source package of
35
+ well-tested Vedic mathematics methods.
36
+
37
+ The first implementation pass is guided by the parsed source chunks in the
38
+ larger Mr Rishi project, especially
39
+ `data/processed/Vedic_mathematics_chunks.jsonl`.
40
+
41
+ ## Install for Development
42
+
43
+ ```powershell
44
+ git clone https://github.com/ksriniv2/vedicmaths.git
45
+ cd vedicmaths
46
+ python -m pip install -e ".[dev]"
47
+ ```
48
+
49
+ If you are working from this local starter folder:
50
+
51
+ ```powershell
52
+ cd C:\Users\KARTH\OneDrive\Documents\vedicmathematics
53
+ python -m pip install -e ".[dev]"
54
+ ```
55
+
56
+ ## Quick Start
57
+
58
+ ```python
59
+ from vedicmathematics import (
60
+ nikhilam_multiply,
61
+ nikhilam_steps,
62
+ square_ending_in_5,
63
+ urdhva_tiryagbhyam_multiply,
64
+ )
65
+
66
+ print(nikhilam_multiply(98, 97))
67
+ # 9506
68
+
69
+ print(urdhva_tiryagbhyam_multiply(123, 456))
70
+ # 56088
71
+
72
+ print(square_ending_in_5(35))
73
+ # 1225
74
+
75
+ trace = nikhilam_steps(98, 97)
76
+ for step in trace.steps:
77
+ print(step.title, step.expression, "=>", step.value)
78
+ ```
79
+
80
+ You can also use the shorter package alias:
81
+
82
+ ```python
83
+ from vedicmaths import nikhilam_multiply
84
+ ```
85
+
86
+ ## Available Functions
87
+
88
+ | Function | Purpose |
89
+ |---|---|
90
+ | `digit_sum(number)` | Sums the decimal digits of a number. |
91
+ | `sum_digits_until_single(number)` | Repeated digit sum, equivalent to digital root. |
92
+ | `nikhilam_multiply(a, b, base=None)` | Multiplies numbers using a nearby base, commonly a power of ten. |
93
+ | `nikhilam_steps(a, b, base=None)` | Returns the worked steps for Nikhilam multiplication. |
94
+ | `anurupyena_multiply(a, b, working_base, theoretical_base=None)` | Proportionate-base multiplication when a multiple or sub-multiple is more convenient. |
95
+ | `anurupyena_steps(a, b, working_base, theoretical_base=None)` | Worked steps for proportionate-base multiplication. |
96
+ | `urdhva_tiryagbhyam_multiply(a, b)` | General multiplication using the vertically-and-crosswise digit method. |
97
+ | `urdhva_tiryagbhyam_steps(a, b)` | Returns the worked steps for vertically-and-crosswise multiplication. |
98
+ | `left_to_right_add(a, b)` | Addition with a trace-friendly column process. |
99
+ | `left_to_right_add_steps(a, b)` | Worked steps for addition. |
100
+ | `subtract_using_complement(a, b)` | Subtraction by complementing the subtrahend from a power of ten. |
101
+ | `subtract_using_complement_steps(a, b)` | Worked steps for complement subtraction. |
102
+ | `complement(number, base=None)` | Complement from a supplied base or next power of ten. |
103
+ | `all_from_9_last_from_10(number)` | Nikhilam-style complement helper. |
104
+ | `vinculum_digits(number)` | Signed digit representation for digits above 5. |
105
+ | `multiply_by_5(number)` | Shortcut for multiplication by 5. |
106
+ | `multiply_by_9(number)` | Shortcut for multiplication by 9. |
107
+ | `multiply_by_11(number)` | Adjacent-sum shortcut for multiplication by 11. |
108
+ | `multiply_by_11_steps(number)` | Worked steps for multiplication by 11. |
109
+ | `multiply_by_12(number)` | Shortcut for multiplication by 12. |
110
+ | `multiply_by_15(number)` | Shortcut for multiplication by 15. |
111
+ | `multiply_by_25(number)` | Shortcut for multiplication by 25. |
112
+ | `multiply_by_50(number)` | Shortcut for multiplication by 50. |
113
+ | `multiply_by_75(number)` | Shortcut for multiplication by 75. |
114
+ | `multiply_by_99(number)` | Shortcut for multiplication by 99. |
115
+ | `multiply_by_125(number)` | Shortcut for multiplication by 125. |
116
+ | `multiply_by_999(number)` | Shortcut for multiplication by 999. |
117
+ | `multiply_by_repeated_9(number, count)` | Multiply by 9, 99, 999, etc. |
118
+ | `square_ending_in_5(number)` | Squares numbers ending in 5 using the "one more than the previous" pattern. |
119
+ | `square_ending_in_5_steps(number)` | Returns the worked steps for squaring a number ending in 5. |
120
+ | `square_near_base(number, base=None)` | Squares numbers close to a power-of-ten base. |
121
+ | `square_near_base_steps(number, base=None)` | Worked steps for near-base squaring. |
122
+ | `square_near_50(number)` | Shortcut for squaring values near 50. |
123
+ | `cube_ending_in_5(number)` | Cubes numbers ending in 5. |
124
+ | `divide_by_5(number)` | Division by 5 as doubling then dividing by 10. |
125
+ | `divide_by_25(number)` | Division by 25 as multiplying by 4 then dividing by 100. |
126
+ | `recurring_decimal_unit_fraction(denominator, limit=None)` | Decimal expansion of `1 / denominator`, with repeating cycles in parentheses. |
127
+ | `osculator_for_ending_9(divisor)` | Ekadhika-style osculator for divisors ending in 9. |
128
+ | `fraction_to_percent(numerator, denominator)` | Converts fractions to percentages. |
129
+ | `percent_of(percent, number)` | Calculates percent of a number. |
130
+ | `transpose_percent(percent, number)` | Uses `a% of b = b% of a`. |
131
+ | `is_divisible_by_3(number)` | Divisibility by 3 using digit sum. |
132
+ | `is_divisible_by_9(number)` | Divisibility by 9 using digit sum. |
133
+ | `is_divisible_by_11(number)` | Divisibility by 11 using alternating digit sums. |
134
+ | `square_root_if_perfect(number)` | Returns the integer square root for perfect squares, otherwise `None`. |
135
+ | `cube_root_if_perfect(number)` | Returns the integer cube root for perfect cubes, otherwise `None`. |
136
+ | `digital_root(number)` | Computes a number's digital root. |
137
+ | `casting_out_nines_check(a, b, result, operator="*")` | Verifies `+`, `-`, or `*` results using digital roots. |
138
+
139
+ ## Example Step Trace
140
+
141
+ ```python
142
+ from vedicmathematics import nikhilam_steps
143
+
144
+ trace = nikhilam_steps(98, 97)
145
+ print(trace.result)
146
+ # 9506
147
+
148
+ for step in trace.steps:
149
+ print(f"{step.title}: {step.expression} = {step.value}")
150
+ ```
151
+
152
+ Output:
153
+
154
+ ```text
155
+ Choose base: base = 100 = 100
156
+ Find deviations: 98 - 100, 97 - 100 = -2, -3
157
+ Cross subtract: 98 + (-3) = 95
158
+ Multiply deviations: (-2) * (-3) = 6
159
+ Combine: (95 * 100) + (6) = 9506
160
+ ```
161
+
162
+ ## Roadmap
163
+
164
+ Good next functions to add:
165
+
166
+ - division using Paravartya Yojayet
167
+ - multiplication by 9, 99, 999, and related bases
168
+ - straight division and auxiliary fractions
169
+ - simple and complex oscillators for divisibility
170
+ - cube-root techniques
171
+ - fraction simplification and recurring decimal patterns
172
+ - richer renderers for traces, such as Markdown and HTML
173
+
174
+ ## Development
175
+
176
+ Run tests:
177
+
178
+ ```powershell
179
+ python -m pytest
180
+ ```
181
+
182
+ Project structure:
183
+
184
+ ```text
185
+ vedicmathematics/
186
+ vedicmathematics/
187
+ __init__.py
188
+ arithmetic.py
189
+ tests/
190
+ test_arithmetic.py
191
+ pyproject.toml
192
+ README.md
193
+ ```
194
+
195
+ ## Contributing
196
+
197
+ Contributions are welcome. Please include:
198
+
199
+ - a clear function name and docstring
200
+ - tests for regular cases and edge cases
201
+ - a `*_steps` variant when the method is teachable step by step
202
+ - examples in the README when adding a major new method
203
+
204
+ ## License
205
+
206
+ MIT
@@ -0,0 +1,8 @@
1
+ vedicmathematics/__init__.py,sha256=H2lpXSXqIvi-tOYiInAtOa_LBawIh5_m_iQNJ7viuF8,2549
2
+ vedicmathematics/arithmetic.py,sha256=tKHn5YKrIA0wDZSo87uWZinr52AlCStW-1g7IExdf9Q,20459
3
+ vedicmaths/__init__.py,sha256=QWyAfE3LlhVgmXvuRgTq1He327VNwHYuxGvf2pOuS9Q,169
4
+ vedicmaths-0.1.0.dist-info/licenses/LICENSE,sha256=wTEayTs-16iL7xGXCiNPOl_OKJEWfIzuIj1O6_lMmBg,1075
5
+ vedicmaths-0.1.0.dist-info/METADATA,sha256=VrR3xdW4j08RyMFYbepC6m3TBax5pu1zW42Ek6HqwqE,7956
6
+ vedicmaths-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ vedicmaths-0.1.0.dist-info/top_level.txt,sha256=-OO63FTjLwxyoJD2gGjj6EK_oJAgiX__shY1PzV2IEk,28
8
+ vedicmaths-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Karthik Srinivasan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ vedicmathematics
2
+ vedicmaths