Source code for polymerist.maths.numbersys.positional

'''Conversion tools for representing positive integers in fixed and mixed radix positional bases'''

__author__ = 'Timotej Bernat'
__email__ = 'timotej.bernat@colorado.edu'

from typing import Any, Generator, Sequence, Union
from math import inf


[docs] class PositionalNumberingSystem: '''For representing positive integers in standard and mixed-radix positional numbering systems''' def __init__(self, radices : Sequence[int]) -> None: '''Radices should be passed in ascending order of significance (i.e. least-significant bases first)''' self._radices = radices # add check for integers # descriptor for place values access (read-only) @property def radices(self) -> Sequence[int]: return self._radices bases = radices # NOTE: specifically opted not to include an iter() conversion property, as this would reset a generator on each call inside the loop @property def places(self) -> Generator[int, None, None]: '''Generates the values of each position in a general mixed-radix positional notation''' p = 1 # TOSELF: should also yield 1? for radix in self.radices: yield p p *= radix # implementations of general base conversions
[docs] def int_to_digits_iter(self, n : int) -> Generator[int, None, None]: '''Convert a non-negative integer to its digit representation under the specified radices Yields digits in ascending order of significance (i.e. starting from least significant digit)''' if n < 0: raise ValueError('Cannot generate digits of a negative integer') radices = iter(self.radices) # convert to iterable to generator-like parsing of sequences while n: try: radix = next(radices) except StopIteration: # if radices have run out, yield residual as "infinitieth" digit yield n break n, digit = divmod(n, radix) if digit < 0: # account for downwards rounding when encountering negative radices n += 1 digit -= radix yield digit
[docs] def int_to_digits(self, n : int, as_str : bool=False) -> Union[str, list[int]]: '''Returns digits of number in descending order of significance (i.e. most significant digit first)''' digits = [digit for digit in self.int_to_digits_iter(n)] digits.reverse() if as_str: return ''.join(str(digit) for digit in digits) return digits
[docs] def digits_to_int(self, digits : Sequence[int]) -> int: '''Convert a sequence of digits to an integer in the specified numbering system Digits should be passed in descending order (i.e. most-significant digit first)''' n : int = 0 radices = iter(self.radices) for i, digit, place in enumerate(zip(reversed(digits), self.places), start=0): digit = int(digit) # allows for string passing try: radix = next(radices) except StopIteration:# radix is deliberately one position ahead in sequence, to allow "peek" ahead due to radix = inf if not (0 <= digit <= radix): raise ValueError(f'Digit {digit} in position {i} exceeds max allowed digit value of {radix} for that place') n += digit*place return n
# dunders for concise calling def __call__(self, *args, **kwargs) -> list[int]: '''Syntactic sugar for digit-to-int conversion (expects a Sequence)''' if (len(args) == 1) and isinstance(args[0], Sequence): args = args[0] return self.digits_to_int(args, **kwargs) def __getitem__(self, value : Any) -> int: '''Syntactic sugar for int-to-digit conversion''' if isinstance(value, int): return self.int_to_digits(value, as_str=True) else: raise TypeError