So as a fun weekend project, I decided to implement from scratch without AI assistance a (probably useless) Python class for handling basic arithmetic (addition, subtraction, multiplication and division) between arbitrary-precision natural numbers in any user-defined base system.
natural.py
# TODO reimplement everything in c++, base 2^30 (use 30 bit digits), karatsuba algorithm, better division algorithm
import copy
binary = '01'
octal = '01234567'
decimal = '0123456789'
hexadecimal = '0123456789abcdef'
base64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
class Natural:
"""
A class to represent a nonnegative integer of arbitrary size in any base system.
Attributes:
digits (list): List of integers where each integer represents position in base string. It represents our arbitrary-sized nonnegative integer starting with least significant digit.
base (str): String of arbitrary symbols. It represents a custom alphabet for our base system.
b (int): Length of base string.
"""
def __init__(self, input, base=decimal):
"""
Args:
input (Union[str, int, list]): Raw number in string, nonnegative integer, or list of chars (least significant digit must be stored first in list). If input of type str or list of chars, it can be empty or assumed to contain chars from base.
base (str): String of arbitrary symbols. Defaults to decimal.
Raises:
ValueError: Raised when input is of wrong type.
"""
self.digits = []
self.base = base
self.b = len(base)
if isinstance(input, str):
if input == '':
self.digits.append(0)
else:
for i in range(len(input) - 1, -1, -1):
self.digits.append(self.base[:self.b].index(input[i]))
elif isinstance(input, int):
if input == 0:
self.digits.append(0)
else:
while input > 0:
self.digits.append(input % self.b)
input //= self.b
elif isinstance(input, list):
self.digits = input
else:
raise ValueError('Your input is not of type str or int or list.')
def __add__(self, other):
"""Performs a simple grade school addition between this Natural and other and returns its sum (inputs remain unchanged).
Args:
other (Natural): Another Natural to be added to this Natural.
Returns:
Natural: Sum of this Natural and other.
"""
result = []
top, bottom = (self, other) if self > other else (other, self)
carry = False
i = 0
while i < bottom.size():
x = top.digits[i] + bottom.digits[i]
if carry:
x += 1
carry = False
if x >= self.b:
carry = True
result.append(x - self.b if carry else x)
i += 1
if top.size() != bottom.size():
while i < top.size():
x = top.digits[i]
if carry:
x += 1
carry = False
if x >= self.b:
carry = True
result.append(x - self.b if carry else x)
i += 1
if carry:
result.append(1)
return Natural(result, self.base)
def __sub__(self, other):
"""Performs a simple grade school subtraction between this Natural and other and returns its difference (inputs remain unchanged).
Args:
other (Natural): Another Natural to be subtracted from this Natural. It must be less than or equal to this Natural.
Raises:
ValueError: Raised when this Natural is smaller than other.
Returns:
Natural: Difference of this Natural and other.
"""
if self < other:
raise ValueError('other is too big')
result = []
top, bottom = copy.deepcopy(self), other
i = 0
while i < bottom.size():
x = top.digits[i] - bottom.digits[i]
if x < 0:
x += self.b
# need to borrow...
for j in range(i + 1, top.size()):
if top.digits[j] > 0:
top.digits[j] -= 1
for k in range(j - 1, i, -1):
top.digits[k] = self.b - 1
break
result.append(x)
i += 1
if top.size() != bottom.size():
while i < top.size():
result.append(top.digits[i])
i += 1
difference = Natural(result, self.base)
difference._strip_leading_zeros()
return difference
def __mul__(self, other):
"""Performs a simple grade school multiplication between this Natural and other and returns its product (inputs remain unchanged).
Args:
other (Natural): Another Natural to be multiplied with this Natural.
Returns:
Natural: Product of this Natural and other.
"""
total = Natural(0, self.base)
top, bottom = (self, other) if self > other else (other, self)
a = 0 # carry
for i in range(bottom.size()):
result = []
for k in range(i):
result.append(0)
for j in range(top.size()):
x = top.digits[j] * bottom.digits[i] + a
a, b = x // self.b, x % self.b
result.append(b)
if a > 0:
result.append(a)
a = 0
total = total + Natural(result, self.base)
total._strip_leading_zeros()
return total
def __pow__(self, n):
"""Returns this Natural raised to power of n.
Args:
n (int): Exponent represented as nonnegative integer.
Raises:
ValueError: Raised when n is less than zero.
Returns:
Natural: Result of exponentiation.
"""
if n < 0:
raise ValueError('n < 0')
r = Natural(1, self.base)
if n == 0:
return r
for i in range(n):
r = r * self
return r
def __truediv__(self, other):
"""Performs a simple grade school division between this Natural and other and returns quotient and remainder (inputs remain unchanged).
Args:
other (Natural): Divisor which is nonzero.
Raises:
ValueError: Raised when divisor happens to be zero.
Returns:
tuple[Natural, Natural]: A tuple containing quotient and remainder.
"""
if other.is_zero():
raise ValueError('divisor cannot be zero')
result = [0]
dividend, divisor = self, other
remainder = Natural(0, self.base)
one = Natural(1, self.base)
for i in range(dividend.size() - 1, -1, -1):
remainder.digits.insert(0, dividend.digits[i])
remainder._strip_leading_zeros()
if divisor > remainder:
result.insert(0, 0)
else:
k = copy.deepcopy(one)
while divisor * k <= remainder:
k = k + one
k = k - one
result.insert(0, k.digits[0])
remainder = remainder - divisor * k
quotient = Natural(result, self.base)
quotient._strip_leading_zeros()
return quotient, remainder
def __eq__(self, other):
"""Checks if this Natural is equal to other.
Args:
other (Natural): Another Natural to be compared with this Natural.
Returns:
bool: True if this Natural is equal to other.
"""
if self.size() != other.size():
return False
for i in range(self.size()):
if self[i] != other[i]:
return False
return True
def __ne__(self, other):
"""Checks if this Natural is not equal to other.
Args:
other (Natural): Another Natural to be compared with this Natural.
Returns:
bool: True if this Natural is not equal to other.
"""
return not self == other
def __lt__(self, other):
"""Checks if this Natural is less than other.
Args:
other (Natural): Another Natural to be compared with this Natural.
Returns:
bool: True if this Natural is less than other.
"""
if self.size() < other.size():
return True
if self.size() > other.size():
return False
for i in range(self.size() - 1, -1, -1):
if self.digits[i] < other.digits[i]:
return True
if self.digits[i] > other.digits[i]:
return False
return False
def __le__(self, other):
"""Checks if this Natural is less than or equal to other.
Args:
other (Natural): Another Natural to be compared with this Natural.
Returns:
bool: True if this Natural is less than or equal to other.
"""
return self < other or self == other
def __gt__(self, other):
"""Checks if this Natural is greater than other.
Args:
other (Natural): Another Natural to be compared with this Natural.
Returns:
bool: True if this Natural is greater than other.
"""
if self.size() > other.size():
return True
if self.size() < other.size():
return False
for i in range(self.size() - 1, -1, -1):
if self.digits[i] > other.digits[i]:
return True
if self.digits[i] < other.digits[i]:
return False
return False
def __ge__(self, other):
"""Checks if this Natural is greater than or equal to other.
Args:
other (Natural): Another Natural to be compared with this Natural.
Returns:
bool: True if this Natural is greater than or equal to other.
"""
return self > other or self == other
def __getitem__(self, i):
"""Returns ith digit of this Natural.
Args:
i (int): Desired position.
Returns:
str: ith digit of this Natural.
"""
return self.base[self.digits[i]]
def __str__(self):
"""Returns human-readable string form of this Natural.
Returns:
str: String representation of this Natural.
"""
s = ''
for i in range(self.size()):
s = self[i] + s
return s
def _strip_leading_zeros(self):
"""Internal method to strip leading zeros from digits.
"""
zeros = 0
while zeros < self.size()-1 and self.digits[self.size() - 1 - zeros] == 0:
zeros += 1
self.digits = self.digits[:self.size() - zeros]
def size(self):
"""Returns the number of digits of this Natural in its base system.
Returns:
int: Number of digits.
"""
return len(self.digits)
def is_zero(self):
"""Returns true if this Natural is zero in its base system.
Returns:
bool: Indicates whether it is zero or not.
"""
return self.size() == 1 and self.digits[0] == 0
def change_base(self, newbase):
"""Changes underlying base to newbase and modifies digits of this Natural accordingly.
Args:
newbase (str): New base string.
"""
if newbase != self.base:
v = 0
for i in range(self.size()):
v += self.digits[i] * self.b**i
self.base = newbase
self.b = len(newbase)
self.digits = []
while v > 0:
self.digits.append(v % self.b)
v //= self.b
natural_test.py
import copy
import operator
from natural import Natural, binary, octal, decimal, hexadecimal, base64
def testBase10(op, maxn, flag=False):
if flag:
for i in range(maxn + 1):
for j in range(i + 1):
a = Natural(i)
b = Natural(j)
_a = copy.deepcopy(a)
_b = copy.deepcopy(b)
result = op(a, b)
if result.__str__() != (op(i, j)).__str__():
print(f'Yuh oh, for {a} and {b} with {op}, we have {result} instead of {op(i, j)}')
assert False
if a != _a or b != _b:
print(f'Yuh oh, for {op}, inputs {_a} and {_b} got modified to {a} and {b}')
assert False
print(f'Base 10 test passed for {op} :)')
else:
for i in range(maxn + 1):
for j in range(maxn + 1):
a = Natural(i)
b = Natural(j)
_a = copy.deepcopy(a)
_b = copy.deepcopy(b)
result = op(a, b)
if result.__str__() != (op(i, j)).__str__():
print(f'Yuh oh, for {a} and {b} with {op}, we have {result} instead of {op(i, j)}')
assert False
if a != _a or b != _b:
print(f'Yuh oh, for {op}, inputs {_a} and {_b} got modified to {a} and {b}')
assert False
print(f'Base 10 test passed for {op} :)')
testBase10(operator.add, 100)
testBase10(operator.sub, 100, True)
testBase10(operator.mul, 100)
def testBase16(op, maxn, flag=False):
if flag:
for i in range(maxn + 1):
for j in range(i + 1):
a = Natural(i, hexadecimal)
b = Natural(j, hexadecimal)
_a = copy.deepcopy(a)
_b = copy.deepcopy(b)
result = op(a, b)
if result.__str__() != hex(op(i, j))[2:]:
print(f'Yuh oh, for {a} and {b} with {op}, we have {result} instead of {hex(op(i, j))[2:]}')
assert False
if a != _a or b != _b:
print(f'Yuh oh, for {op}, inputs {_a} and {_b} got modified to {a} and {b}')
assert False
print(f'Base 16 test passed for {op} :)')
else:
for i in range(maxn + 1):
for j in range(maxn + 1):
a = Natural(i, hexadecimal)
b = Natural(j, hexadecimal)
_a = copy.deepcopy(a)
_b = copy.deepcopy(b)
result = op(a, b)
if result.__str__() != hex(op(i, j))[2:]:
print(f'Yuh oh, for {a} and {b} with {op}, we have {result} instead of {hex(op(i, j))[2:]}')
assert False
if a != _a or b != _b:
print(f'Yuh oh, for {op}, inputs {_a} and {_b} got modified to {a} and {b}')
assert False
print(f'Base 16 test passed for {op} :)')
testBase16(operator.add, 100)
testBase16(operator.sub, 100, True)
testBase16(operator.mul, 100)
def testBase2(op, maxn, flag=False):
if flag:
for i in range(maxn + 1):
for j in range(i + 1):
a = Natural(i, binary)
b = Natural(j, binary)
_a = copy.deepcopy(a)
_b = copy.deepcopy(b)
result = op(a, b)
if result.__str__() != bin(op(i, j))[2:]:
print(f'Yuh oh, for {a} and {b} with {op}, we have {result} instead of {bin(op(i, j))[2:]}')
assert False
if a != _a or b != _b:
print(f'Yuh oh, for {op}, inputs {_a} and {_b} got modified to {a} and {b}')
assert False
print(f'Base 2 test passed for {op} :)')
else:
for i in range(maxn + 1):
for j in range(maxn + 1):
a = Natural(i, binary)
b = Natural(j, binary)
_a = copy.deepcopy(a)
_b = copy.deepcopy(b)
result = op(a, b)
if result.__str__() != bin(op(i, j))[2:]:
print(f'Yuh oh, for {a} and {b} with {op}, we have {result} instead of {bin(op(i, j))[2:]}')
assert False
if a != _a or b != _b:
print(f'Yuh oh, for {op}, inputs {_a} and {_b} got modified to {a} and {b}')
assert False
print(f'Base 2 test passed for {op} :)')
testBase2(operator.add, 100)
testBase2(operator.sub, 100, True)
testBase2(operator.mul, 100)
for i in range(101):
for j in range(1, 101):
a = Natural(i)
b = Natural(j)
_a = copy.deepcopy(a)
_b = copy.deepcopy(b)
q, r = a / b
if q.__str__() != (i//j).__str__() or r.__str__() != (i%j).__str__():
print(f'hmm, dividing {a} by {b} results in q={q} and r={r} (expected: q={i // j} and r={i % j})')
assert False
if a != _a or b != _b:
print(f'inputs {_a} and {_b} got modified to {a} and {b}')
assert False
print('base 10 division test: pass :)')
for i in range(101):
for j in range(1, 101):
a = Natural(i, hexadecimal)
b = Natural(j, hexadecimal)
_a = copy.deepcopy(a)
_b = copy.deepcopy(b)
q, r = a / b
if q.__str__() != hex(i//j)[2:] or r.__str__() != hex(i%j)[2:]:
print(f'hmm, dividing {a} by {b} results in q={q} and r={r} (expected: q={hex(i//j)[2:]} and r={hex(i%j)[2:]})')
assert False
if a != _a or b != _b:
print(f'inputs {_a} and {_b} got modified to {a} and {b}')
assert False
print('base 16 division test: pass :)')
for i in range(101):
for j in range(1, 101):
a = Natural(i, binary)
b = Natural(j, binary)
_a = copy.deepcopy(a)
_b = copy.deepcopy(b)
q, r = a / b
if q.__str__() != bin(i//j)[2:] or r.__str__() != bin(i%j)[2:]:
print(f'hmm, dividing {a} by {b} results in q={q} and r={r} (expected: q={bin(i//j)[2:]} and r={bin(i%j)[2:]})')
assert False
if a != _a or b != _b:
print(f'inputs {_a} and {_b} got modified to {a} and {b}')
assert False
print('base 2 division test: pass :)')
x = Natural(143785)
print(x)
x.change_base(hexadecimal)
print(x)
x.change_base(binary)
print(x)
x.change_base(octal)
print(x)
x.change_base(decimal)
print(x)
# https://cs.stackexchange.com/a/10495/146253
weird = 'αβγδρζξ'
b = Natural('βξ', weird)
x1 = Natural('ββ', weird)
x2 = Natural('βζ', weird)
x3 = Natural('α', weird)
x4 = Natural('ξ', weird)
c = x1 + x2*b + x3*b**2 + x4*b**3
print(c)
c.change_base('0123456789XYZ')
print(c)
a = Natural('60Z8', '0123456789XYZ')
a.change_base(weird)
print(a)
x = Natural(1343435783, base64)
y = Natural(2456, base64)
q, r = x / y
print(f'q={q}, r={r}')
x = Natural(1343435783, binary)
y = Natural(2456, binary)
q, r = x / y
print(f'q={q}, r={r}')
q.change_base(base64)
r.change_base(base64)
print(f'q={q}, r={r}')
I am requesting a code review for both files above. Here are some of my specific concerns:
- I wonder if I documented my class well. I am following Google-style documentation.
- Is there anything to improve in my natural_test.py?
- Are my tests comprehensive enough?
- What are possible ways to optimize my code, in particular my code for methods
__add__,__sub__,__mul__and__div__.
Any other comments or suggestions are welcome!
change_baseimplementation technically "cheats" asvcan grow arbitrarily large - while that's okay with Python'sint(I think), it's basically using a different "arbitrarily largeint", if that makes any sense, instead of doing everything digit by digit as other functions do. \$\endgroup\$