Source code for kanon.units.radices

"""
.. testsetup::

    >>> import builtins
    >>> from .definitions import Sexagesimal, Historical
    >>> builtins.Sexagesimal = Sexagesimal
    >>> builtins.Historical = Historical

>>> class ExampleBase(
...    BasedReal, base=([20, 5, 18], [24, 60]), separators=[" ","u ","sep "]
... ):
...     pass
...
>>> number = ExampleBase((8, 12, 3, 1), (23, 31))
>>> number
08 12u 3sep 01 ; 23,31
>>> float(number)
15535.979861111111

"""

from decimal import Decimal
from fractions import Fraction
from functools import cached_property, lru_cache
from numbers import Number
from numbers import Real as _Real
from typing import (
    Any,
    Dict,
    Generator,
    Generic,
    List,
    Optional,
    Sequence,
    SupportsFloat,
    Tuple,
    Type,
    TypeVar,
    Union,
    cast,
    overload,
)

import numpy as np
from astropy.units.core import Unit, UnitBase, UnitTypeError
from astropy.units.quantity import Quantity
from astropy.units.quantity_helper.converters import UFUNC_HELPERS
from astropy.units.quantity_helper.helpers import _d

from kanon.utils import Sign
from kanon.utils.list_to_tuple import list_to_tuple
from kanon.utils.looping_list import LoopingSList

from .precision import PreciseNumber, PrecisionMode, TruncatureMode, set_precision

__all__ = ["BasedReal"]


TBasedReal = TypeVar("TBasedReal", bound="BasedReal")
TTypeBasedReal = TypeVar("TTypeBasedReal", bound="Type[BasedReal]")
RadixBase = Tuple[LoopingSList[int], LoopingSList[int]]

_NORMAL_BASES: Dict[LoopingSList[int], Type["BasedReal"]] = {}


def ndigit_for_radix(radix: int) -> int:
    """
    Compute how many digits are needed to represent a position of
    the specified radix.

    >>> ndigit_for_radix(10)
    1
    >>> ndigit_for_radix(60)
    2

    :param radix:
    :return:
    """
    return int(np.ceil(np.log10(radix)))


def radix_at_pos(base: RadixBase, pos: int):
    """
    Return the radix at the specified position. Position 0 represents the last integer
    position before the fractional part (i.e. the position just before the ';' in
    sexagesimal notation, or just before the '.' in decimal notation). Positive
    positions represent the fractional positions, negative positions represent
    the integer positions.

    :param base: Base
    :type base: `RadixBase`
    :param pos: Position. <= 0 for integer part \
        (with 0 being the right-most integer position), > 0 for fractional part
    :return: Radix at the specified position
    """

    if pos <= 0:
        return base[0][pos - 1]
    else:
        return base[1][pos - 1]


def factor_at_pos(base: RadixBase, pos: int):
    """
    Returns an int factor corresponding to a digit at position pos.
    This factor is an integer, when dealing with fractional position you should invert
    the result to find relevant factor.

    >>> base = Sexagesimal(0).base
    >>> factor_at_pos(base, -2)
    3600
    >>> factor_at_pos(base, 0)
    1

    :param base: Base
    :type base: `RadixBase`
    :param pos: Position of the digit
    :type pos: int
    :return: Factor at pos
    :rtype: int
    """

    factor = 1
    for i in range(abs(pos)):
        factor *= radix_at_pos(base, i if pos > 0 else -i)
    return factor


[docs]class BasedReal(PreciseNumber, _Real): """ Abstract class to represent a number in a specific `base`. """ _base: RadixBase """Base of this BasedReal, (integer part, fractional part)""" _integer_separators: LoopingSList[str] """List of string separators, used for displaying the integer part of the number""" __mixed: bool """Is the base used mixed""" __normal_base: Optional[Type["BasedReal"]] = None """If the base is mixed and the fractional part is of one element, \ uses a normal base to perform arithmetics operations""" __left: Tuple[int, ...] __right: Tuple[int, ...] __remainder: Decimal __sign: Sign __slots__ = ( "_base", "_integer_separators", "__left", "__right", "__remainder", "__sign", "__mixed", ) def __init_subclass__( cls: Type[TBasedReal], base: Tuple[Sequence[int], Sequence[int]], separators: Optional[Sequence[str]] = None, ) -> None: left, right = base assert left and right assert all(isinstance(x, int) and x > 1 for x in left) assert all(isinstance(x, int) and x > 1 for x in right) cls._base = (LoopingSList(left), LoopingSList(right)) if separators is not None: if len(separators) != len(left): raise ValueError cls._integer_separators = LoopingSList(separators) else: cls._integer_separators = LoopingSList( ["," if x != 10 else "" for x in left] ) right_loop = cls._base[1] cls.__mixed = any(x != cls._base[0][0] for x in cls._base[0] + right_loop) if cls.__mixed: normal_base = _NORMAL_BASES.get(right_loop) if len(right_loop) == 1 and not normal_base: normal_base = type( f"normal{right_loop[0]}", (BasedReal,), {}, base=([right_loop[0]],) * 2, ) _NORMAL_BASES[right_loop] = normal_base cls.__normal_base = normal_base else: _NORMAL_BASES[right_loop] = cls return super().__init_subclass__() def __check_range(self): """ Checks that the given values are in the range of the base and are integers. """ if self.sign not in (-1, 1): raise ValueError("Sign should be -1 or 1") if not (isinstance(self.remainder, Decimal) and 0 <= self.remainder < 1): if self.remainder == 1: # pragma: no cover self += (self.one() * self.sign) >> self.significant else: raise ValueError( f"Illegal remainder value ({self.remainder}),\ should be a Decimal between [0.,1.[" ) for x in self[:]: if isinstance(x, float): raise IllegalFloatError(x) if not isinstance(x, int): raise TypeError(f"{x} not an int") for i, s in enumerate(self[:]): if s < 0.0 or s >= radix_at_pos(self.base, i - len(self.left) + 1): raise IllegalBaseValueError( self.__class__, radix_at_pos(self.base, i - len(self.left) + 1), s ) def __simplify_integer_part(self) -> int: """ Remove the useless trailing zeros in the integer part and return how many were removed """ count = 0 for i in self.left[:-1]: if i != 0: break count += 1 if count > 0: self.__left = self.left[count:] return count != 0 @list_to_tuple def __new__( cls: Type[TBasedReal], *args, remainder=Decimal(0.0), sign=1 ) -> TBasedReal: """Constructs a number with a given radix. Arguments: - `str` >>> Sexagesimal("-2,31;12,30") -02,31 ; 12,30 - 2 `Sequence[int]` representing integral part and fractional part >>> Sexagesimal((2,31), (12,30), sign=-1) -02,31 ; 12,30 >>> Sexagesimal([2,31], [12,30]) 02,31 ; 12,30 - a `BasedReal` with a significant number of digits, >>> Sexagesimal(Sexagesimal("-2,31;12,30"), 1) -02,31 ; 12 |r0.5 - multiple `int` representing an integral number in current `base` >>> Sexagesimal(21, 1, 3) 21,01,03 ; :param remainder: When a computation requires more precision than the \ precision of this number, we store a :class:`~decimal.Decimal` remainder \ to keep track of it, defaults to 0.0 :type remainder: ~decimal.Decimal, optional :param sign: The sign of this number, defaults to 1 :type sign: int, optional :raises ValueError: Unexpected or illegal arguments :rtype: BasedReal """ if cls is BasedReal: raise TypeError("Can't instanciate abstract class BasedReal") self: TBasedReal = super().__new__(cls) self.__left = () self.__right = () self.__remainder = remainder self.__sign = sign if all(isinstance(x, int) for x in args): return cls.__new__(cls, args, (), remainder=remainder, sign=sign) if len(args) == 2: if isinstance(args[0], BasedReal): if isinstance(args[0], cls): return args[0].resize(args[1]) if args[0]._base[1] == cls._base[1]: return cls.__new__(cls, args[0]).resize(args[1]) return cls.from_decimal(args[0].decimal, args[1]) if isinstance(args[0], tuple) and isinstance(args[1], tuple): self.__left = args[0] self.__right = args[1] else: raise ValueError("Incorrect parameters for BasedReal") elif len(args) == 1: if isinstance(args[0], str): return cls._from_string(args[0]) if isinstance(args[0], BasedReal): if cls._base[1] == args[0].base[1]: return cls.from_int(int(args[0])) + cls.__new__( cls, (0,), args[0].right, remainder=args[0].remainder, sign=args[0].sign, ) raise ValueError( "Please specify a number of significant positions for " "numbers with incompatible fractional bases" if isinstance(args[0], BasedReal) else "Incorrect parameters at BasedReal creation" ) else: raise ValueError("Incorrect number of parameter at BasedReal creation") self.__check_range() if self.__simplify_integer_part() or not self.left: return cls.__new__( cls, self.left or (0,), self.right, remainder=self.remainder, sign=self.sign, ) return self @property def left(self) -> Tuple[int, ...]: """ Tuple of values at integer positions >>> Sexagesimal(1,2,3).left (1, 2, 3) :rtype: Tuple[int, ...] """ return self.__left @property def right(self) -> Tuple[int, ...]: """ Tuple of values at fractional positions >>> Sexagesimal((1,2,3), (4,5)).right (4, 5) :rtype: Tuple[int, ...] """ return self.__right @property def base(self) -> RadixBase: """ Base of this BasedReal, (integer part, fractional part) >>> Sexagesimal(1).base ((..., 60, ...), (..., 60, ...)) :rtype: `RadixBase` """ return self._base @property def mixed(self) -> bool: return self.__mixed @property def remainder(self) -> Decimal: """ When a computation requires more significant figures than the precision of this number, we store a :class:`~decimal.Decimal` remainder to keep track of it >>> Sexagesimal(1,2,3, remainder=Decimal("0.2")).remainder Decimal('0.2') :return: Remainder of this `BasedReal` :rtype: ~decimal.Decimal """ return self.__remainder @property def sign(self) -> Sign: """ Sign of this `BasedReal` >>> Sexagesimal(1,2,3, sign=-1).sign -1 :rtype: Sign """ return self.__sign @property def significant(self) -> int: """ Precision of this `BasedReal` (equals to length of fractional part) >>> Sexagesimal((1,2,3), (4,5)).significant 2 :rtype: int """ return len(self.right) @cached_property def decimal(self) -> Decimal: """ This `BasedReal` converted as a `~decimal.Decimal` >>> Sexagesimal((1,2,3), (15,36)).decimal Decimal('3723.26') :rtype: Decimal """ value = Decimal(abs(int(self))) factor = Decimal(1) for i in range(self.significant): factor *= self.base[1][i] value += self.right[i] / factor value += self.remainder / factor return value * self.sign
[docs] def to_fraction(self) -> Fraction: """ :return: this `BasedReal` as a :class:`~fractions.Fraction` object. """ return Fraction(self.decimal)
[docs] @classmethod def from_fraction( cls: Type[TBasedReal], fraction: Fraction, significant: Optional[int] = None, ) -> TBasedReal: """ :param fraction: a `~fractions.Fraction` object :param significant: significant precision desired :return: a `BasedReal` object computed from a Fraction """ if not isinstance(fraction, Fraction): raise TypeError(f"Argument {fraction} is not a Fraction") num, den = fraction.as_integer_ratio() res = cls.from_decimal(Decimal(num) / Decimal(den), significant or 100) return res if significant else res.minimize_precision()
def __repr__(self) -> str: """ Convert to string representation. Note that this representation is rounded (with respect to the remainder attribute) not truncated :return: String representation of this number """ res = "" if self.sign < 0: res += "-" for i in range(len(self.left)): if i > 0: res += self._integer_separators[i - len(self.left)] num = str(self.left[i]) digit = ndigit_for_radix(self.base[0][i - len(self.left)]) res += "0" * (digit - len(num)) + num res += " ; " for i in range(len(self.right)): num = str(self.right[i]) digit = ndigit_for_radix(self.base[1][i]) res += "0" * (digit - len(num)) + num if i < len(self.right) - 1: res += "," if self.remainder: res += f" |r{self.remainder:3.1f}" return res __str__ = __repr__ @classmethod def _from_string(cls: Type[TBasedReal], string: str) -> TBasedReal: """ Parses and instantiate a `BasedReal` object from a string >>> Sexagesimal('1, 12; 4, 25') 01,12 ; 04,25 >>> Historical('2r 7s 29; 45, 2') 2r 07s 29 ; 45,02 >>> Sexagesimal('0 ; 4, 45') 00 ; 04,45 :param string: `str` representation of the number :return: a new instance of `BasedReal` """ if not isinstance(string, str): raise TypeError(f"Argument {string} is not a str") string = string.strip().lower() if len(string) == 0: raise EmptyStringException("String is empty") if string[0] == "-": sign = -1 string = string[1:] else: sign = 1 left_right = string.split(";") if len(left_right) < 2: left = left_right[0] right = "" elif len(left_right) == 2: left, right = left_right else: raise TooManySeparators("Too many separators in string") left = left.strip() right = right.strip() left_numbers: List[int] = [] right_numbers: List[int] = [] if len(right) > 0: right_numbers = [int(i) for i in right.split(",")] if len(left) > 0: rleft = left[::-1] for i in range(len(left)): if len(rleft.strip()) == 1: break separator = cls._integer_separators[-i - 1].strip().lower() if separator != "": split = rleft.split(separator, 1) if len(split) == 1: rem = split[0] break value, rem = split else: value = rleft[0] rem = rleft[1:] left_numbers.insert(0, int(value[::-1])) rleft = rem.strip() left_numbers.insert(0, int(rleft[::-1])) return cls(left_numbers, right_numbers, sign=sign)
[docs] def resize(self: TBasedReal, significant: int) -> TBasedReal: """ Resizes and returns a new `BasedReal` object to the specified precision >>> n = Sexagesimal('02, 02; 07, 23, 55, 11, 51, 21, 36') >>> n 02,02 ; 07,23,55,11,51,21,36 >>> n.remainder Decimal('0') >>> n1 = n.resize(4) >>> n1.right (7, 23, 55, 11) >>> n1.remainder Decimal('0.8560000000000000000000000000') >>> n1.resize(7) 02,02 ; 07,23,55,11,51,21,36 :param significant: Number of desired significant positions :return: Resized `BasedReal` """ if significant == self.significant: return self if significant > self.significant: rem = type(self).from_decimal( self.sign * self.remainder, significant - self.significant ) return type(self)( self.left, self.right + rem.right, remainder=rem.remainder, sign=self.sign, ) if significant >= 0: remainder = Decimal(0) factor = Decimal(1) for idx, number in enumerate(self.right[significant:]): factor *= Decimal(self.base[1][significant + idx]) remainder += Decimal(number) / factor remainder += self.remainder / factor return type(self)( self.left, self.right[:significant], remainder=remainder, sign=self.sign, ) raise NotImplementedError
def __trunc__(self): return int(float(self.truncate(0)))
[docs] def truncate(self: TBasedReal, significant: Optional[int] = None) -> TBasedReal: """ Truncate this BasedReal object to the specified precision >>> n = Sexagesimal('02, 02; 07, 23, 55, 11, 51, 21, 36') >>> n 02,02 ; 07,23,55,11,51,21,36 >>> n = n.truncate(3); n 02,02 ; 07,23,55 >>> n = n.resize(7); n 02,02 ; 07,23,55,00,00,00,00 :param n: Desired significant positions :return: Truncated BasedReal """ if significant is None: significant = self.significant if significant > self.significant: return self left = self.left if significant >= 0 else self.left[:-significant] right = self.right[:significant] if significant >= 0 else () return type(self)(left, right, sign=self.sign)
[docs] def floor(self: TBasedReal, significant: Optional[int] = None) -> TBasedReal: resized = self.resize(significant) if significant else self if resized.remainder == 0 or self.sign == 1: return resized.truncate() return resized._set_remainder(Decimal(0.5)).__round__()
[docs] def ceil(self: TBasedReal, significant: Optional[int] = None) -> TBasedReal: resized = self.resize(significant) if significant else self if resized.remainder == 0 or self.sign == -1: return resized.truncate() return resized._set_remainder(Decimal(0.5)).__round__()
[docs] def minimize_precision(self: TBasedReal) -> TBasedReal: """ Removes unnecessary zeros from fractional part of this BasedReal. :return: Minimized BasedReal """ if self.remainder > 0 or self.significant == 0 or self.right[-1] > 0: return self count = 0 for x in self.right[::-1]: if x != 0: break count += 1 return self.truncate(self.significant - count)
def __lshift__(self: TBasedReal, other: int) -> TBasedReal: """self << other :param other: Amount to shift this BasedReal :type other: int :return: Shifted number :rtype: BasedReal """ return self.shift(-other) def __rshift__(self: TBasedReal, other: int) -> TBasedReal: """self >> other :param other: Amount to shift this BasedReal :type other: int :return: Shifted number :rtype: BasedReal """ return self.shift(other)
[docs] def shift(self: TBasedReal, i: int) -> TBasedReal: """ Shifts number to the left (-) or the right (+). Prefer using >> and << operators (right-shift and left-shift). >>> Sexagesimal(3).shift(-1) 03,00 ; >>> Sexagesimal(3).shift(2) 00 ; 00,03 :param i: Amount to shift this BasedReal :return: Shifted number :rtype: BasedReal """ if i == 0: return self if self.mixed: raise NotImplementedError offset = len(self.left) if i > 0 else len(self.left) - i br_rem = self.from_decimal(self.remainder, max(0, offset - len(self[:]))) left_right = (0,) * i + self[:] + br_rem.right left = left_right[:offset] right = left_right[offset : -i if -i > offset else None] return type(self)(left, right, remainder=br_rem.remainder, sign=self.sign)
[docs] @lru_cache def subunit_quantity(self: TBasedReal, i: int) -> int: """Convert this sexagesimal to the integer value from the specified fractional point. >>> number = Sexagesimal("1,0;2,30") Amount of minutes in `number` >>> number.subunit_quantity(1) 3602 Amount of zodiacal signs in `number` >>> number.subunit_quantity(-1) 1 :param i: Rank of the subunit to compute from. :type i: int :return: Integer amount of the specified subunit. :rtype: int """ res = 0 factor = 1 for idx, v in enumerate(self.resize(max(0, i + 1))[i::-1]): res += v * factor factor *= radix_at_pos(self.base, i - idx) return self.sign * res
def __round__(self: TBasedReal, significant: Optional[int] = None): """ Round this BasedReal object to the specified precision. If no precision is specified, the rounding is performed with respect to the remainder attribute. >>> n = Sexagesimal('02, 02; 07, 23, 55, 11, 51, 21, 36') >>> n 02,02 ; 07,23,55,11,51,21,36 >>> round(n, 4) 02,02 ; 07,23,55,12 :param significant: Number of desired significant positions :return: self """ if significant is None: significant = self.significant n = self.resize(significant) if n.remainder >= 0.5: with set_precision( pmode=PrecisionMode.MAX, tmode=TruncatureMode.NONE, recording=False ): values = [0] * significant + [1] n += type(self)(values[:1], values[1:], sign=self.sign) return n.truncate(significant) @overload def __getitem__(self, key: int) -> int: ... @overload def __getitem__(self, key: slice) -> Tuple[int, ...]: ... def __getitem__(self: TBasedReal, key): """ Allow to get a specific position value of this BasedReal object by specifying an index. The position 0 corresponds to the right-most integer position. Negative positions correspond to the other integer positions, positive positions correspond to the fractional positions. :param key: desired index :return: value at the specified position """ if isinstance(key, slice): array = self.left + self.right start = key.start + len(self.left) - 1 if key.start is not None else None stop = key.stop + len(self.left) - 1 if key.stop is not None else None return array[start : stop : key.step] if isinstance(key, int): if -len(self.left) < key <= 0: return self.left[key - 1] if self.significant >= key > 0: return self.right[key - 1] raise IndexError raise TypeError
[docs] @classmethod def from_float( cls: Type[TBasedReal], floa: float, significant: int, remainder_threshold: float = 0.999999, ) -> TBasedReal: """ Class method to produce a new BasedReal object from a floating number >>> Sexagesimal.from_float(1/3, 4) 00 ; 20,00,00,00 :param floa: floating value of the number :param significant: precision of the number :return: a new BasedReal object """ if not isinstance(floa, (int, float)): raise TypeError(f"Argument {floa} is not a float") integer_part = cls.from_int(int(floa), significant=significant) value = abs(floa - int(integer_part)) right = [0] * significant factor = 1.0 if value != 0: for i in range(significant): factor = cls._base[1][i] value *= factor if value - int(value) > remainder_threshold and value + 1 < factor: value = int(value) + 1 elif value - int(value) < 1 - remainder_threshold and any( x != 0 for x in right ): value = int(value) position_value = int(value) value -= position_value right[i] = position_value return cls( integer_part.left, tuple(right), remainder=Decimal(value), sign=-1 if floa < 0 else 1, )
[docs] @classmethod def from_decimal( cls: Type[TBasedReal], dec: Decimal, significant: int ) -> TBasedReal: """ Class method to produce a new BasedReal object from a Decimal number >>> Sexagesimal.from_decimal(Decimal('0.1'), 4) 00 ; 06,00,00,00 :param dec: floating value of the number :param significant: precision of the number :return: a new BasedReal object """ if not isinstance(dec, Decimal): raise TypeError(f"Argument {dec} is not a Decimal") integer_part = cls.from_int(int(dec), significant=significant) value = abs(dec - int(integer_part)) right = [0] * significant for i in range(significant): factor = cls._base[1][i] value *= factor position_value = int(value) value -= position_value right[i] = position_value return cls( integer_part.left, tuple(right), remainder=value, sign=-1 if dec < 0 else 1 )
[docs] @classmethod def zero(cls: Type[TBasedReal], significant=0) -> TBasedReal: """ Class method to produce a zero number of the specified precision >>> Sexagesimal.zero(7) 00 ; 00,00,00,00,00,00,00 :param significant: desired precision :return: a zero number """ return cls((0,), (0,) * significant)
[docs] @classmethod def one(cls: Type[TBasedReal], significant=0) -> TBasedReal: """ Class method to produce a unit number of the specified precision >>> Sexagesimal.one(5) 01 ; 00,00,00,00,00 :param significant: desired precision :return: a unit number """ return cls((1,), (0,) * significant)
@classmethod @overload def range(cls: Type[TBasedReal], stop: int) -> Generator["BasedReal", None, None]: ... @classmethod @overload def range( cls: Type[TBasedReal], start: int, stop: int, step=1 ) -> Generator["BasedReal", None, None]: ...
[docs] @classmethod def range( cls: Type[TBasedReal], *args, **kwargs ) -> Generator["BasedReal", None, None]: """ Range generator, equivalent to `range` builtin but yields `BasedReal` numbers. :yield: `BasedReal` integers. """ for i in range(*args, **kwargs): yield cls.from_int(i)
[docs] @classmethod def from_int(cls: Type[TBasedReal], value: int, significant=0) -> TBasedReal: """ Class method to produce a new BasedReal object from an integer number >>> Sexagesimal.from_int(12, 4) 12 ; 00,00,00,00 :param value: integer value of the number :param significant: precision of the number :return: a new BasedReal object """ if not np.issubdtype(type(value), np.integer): raise TypeError(f"Argument {value} is not an int") base = cls._base sign = -1 if value < 0 else 1 value *= sign pos = 0 int_factor = 1 while value >= int_factor: int_factor *= base[0][-1 - pos] pos += 1 left = [0] * pos for i in range(pos): int_factor //= base[0][-pos + i] position_value = value // int_factor value -= position_value * int_factor left[i] = position_value return cls(left, (0,) * significant, sign=sign)
def __float__(self) -> float: """ Compute the float value of this BasedReal object >>> float(Sexagesimal('01;20,00')) 1.3333333333333333 >>> float(Sexagesimal('14;30,00')) 14.5 :return: float representation of this BasedReal object """ value = float(abs(int(self))) factor = 1.0 for i in range(self.significant): factor /= self.base[1][i] value += factor * self.right[i] value += factor * float(self.remainder) return float(value * self.sign) def __int__(self) -> int: """ Compute the int value of this BasedReal object """ value = 0 factor = 1 for i in range(len(self.left)): value += factor * self.left[-i - 1] factor *= self.base[0][-i - 1] return value * self.sign def _truediv(self: TBasedReal, _other: PreciseNumber) -> TBasedReal: other = cast(BasedReal, _other) max_significant = max(self.significant, other.significant) if self == 0: return self.zero(significant=max_significant) elif other == 1: return self elif other == -1: return -self elif other == 0: raise ZeroDivisionError if self.mixed: if normal_base := self.__normal_base: return type(self)(normal_base(self) / other) return self.from_float(float(self) / float(other), self.significant) sign = self.sign * other.sign q_res = self.zero(max_significant) right = list(q_res.right) numerator = abs(cast(BasedReal, self)) denominator = abs(cast(BasedReal, other)) q, r = divmod(numerator, denominator) q_res += q for i in range(0, max_significant): numerator = r * self.base[1][i] q, r = divmod(numerator, denominator) if q == self.base[1][i]: # pragma: no cover q_res += 1 r = self.zero() break right[i] = int(q) return type(self)( q_res.left, right, remainder=r.decimal / denominator.decimal, sign=sign ) def _add(self: TBasedReal, _other: PreciseNumber) -> TBasedReal: other = cast(BasedReal, _other) if self.decimal == -other.decimal: return self.zero() maxright = max(self.significant, other.significant) maxleft = max(len(self.left), len(other.left)) va = self.resize(maxright) vb = other.resize(maxright) sign = va.sign if abs(cast(BasedReal, va)) > abs(vb) else vb.sign if sign < 0: va = -va vb = -vb maxlen = max(len(va[:]), len(vb[:])) values = ( [v.sign * x for x in v[::-1]] + [0] * (maxlen - len(v[:])) for v in (cast(BasedReal, va), vb) ) numbers: List[int] = [a + b for a, b in zip(*values)] + [0] remainder = va.remainder * va.sign + vb.remainder * vb.sign fn = remainder if remainder >= 0 else remainder - 1 remainder -= int(fn) numbers[0] += int(fn) for i, r in enumerate(numbers): factor = radix_at_pos(self.base, maxright - i) if r < 0 or r >= factor: numbers[i] = r % factor numbers[i + 1] += 1 if r > 0 else -1 numbers = [abs(x) for x in numbers[::-1]] left = numbers[: maxleft + 1] right = numbers[maxleft + 1 :] return type(self)(left, right, remainder=abs(remainder), sign=sign) def __add__(self: TBasedReal, other) -> TBasedReal: """ self + other >>> Sexagesimal('01, 21; 47, 25') + Sexagesimal('45; 32, 14, 22') 02,07 ; 19,39,22 """ if not np.isreal(other): raise NotImplementedError if type(self) is not type(other): return self + self.from_float(float(other), significant=self.significant) return super().__add__(other) def __radd__(self: TBasedReal, other) -> TBasedReal: """other + self""" return self + other def __sub__(self: TBasedReal, other) -> TBasedReal: """self - other""" return super().__sub__(other) def __rsub__(self: TBasedReal, other) -> TBasedReal: """other - self""" return super().__rsub__(other) def _sub(self: TBasedReal, _other: PreciseNumber) -> TBasedReal: other = cast(BasedReal, _other) return self + -other def __rtruediv__(self: TBasedReal, other) -> TBasedReal: """other / self""" return other / float(self) def __pow__(self: TBasedReal, exponent): """self**exponent Negative numbers cannot be raised to a non-integer power """ res = self.one(self.significant) if exponent == 0: return res if self == 0: return self if self < 0 and int(exponent) != exponent: raise ValueError( "Negative BasedReal cannot be raised to a non-integer power" ) int_exp = int(exponent) f_exp = float(exponent - int_exp) if int_exp > 0: for _ in range(0, int_exp): res *= self else: for _ in range(0, -int_exp): res /= self res *= float(self) ** f_exp return res def __rpow__(self: TBasedReal, base): """base ** self""" return self.from_float(float(base), self.significant) ** self def __neg__(self: TBasedReal) -> TBasedReal: """-self""" return type(self)( self.left, self.right, remainder=self.remainder, sign=-self.sign ) def __pos__(self: TBasedReal) -> TBasedReal: """+self""" return self def __abs__(self: TBasedReal) -> TBasedReal: """ abs(self) >>> abs(Sexagesimal('-12; 14, 15')) 12 ; 14,15 :return: the absolute value of self """ if self.sign >= 0: return self return -self def _mul(self: TBasedReal, _other: PreciseNumber) -> TBasedReal: other = cast(BasedReal, _other) if other in (1, -1): return self if other == 1 else -self if self == 0 or other == 0: return self.zero() if self in (1, -1): return type(self)(other if self == 1 else -other, self.significant) if self.mixed: if normal_base := self.__normal_base: return type(self)(normal_base(self) * other) return self.from_float(float(self) * float(other), self.significant) max_right = max(self.significant, other.significant) va = self.resize(max_right) vb = other.resize(max_right) res_int = int(va << max_right) * int(vb << max_right) res = self.from_int(res_int) >> 2 * max_right factor = factor_at_pos(self.base, max_right) vb_rem = vb.sign * vb.remainder / factor va_rem = va.sign * va.remainder / factor rem = ( va.truncate().decimal * vb_rem + vb.truncate().decimal * va_rem + va_rem * vb_rem ) if rem: res += float(rem) return res @overload def __mul__( # type: ignore self: TBasedReal, other: Union[float, "BasedReal"] ) -> TBasedReal: ... @overload def __mul__(self: TBasedReal, other: Unit) -> "BasedQuantity[TBasedReal]": ... def __mul__(self: TBasedReal, other): """ self * other >>> Sexagesimal('01, 12; 04, 17') * Sexagesimal('7; 45, 55') 09,19 ; 39,15 |r0.7 """ if isinstance(other, UnitBase): return BasedQuantity(self, unit=other) if not np.isreal(other) or not isinstance(other, SupportsFloat): raise NotImplementedError if type(self) is not type(other): return self * self.from_float(float(other), self.significant) return super().__mul__(other) @overload def __rmul__( # type: ignore self: TBasedReal, other: Union[float, "BasedReal"] ) -> TBasedReal: ... @overload def __rmul__(self: TBasedReal, other: Unit) -> "BasedQuantity[TBasedReal]": ... def __rmul__(self: TBasedReal, other): """other * self""" return self * other def __divmod__(self: TBasedReal, other: Any) -> Tuple["BasedReal", "BasedReal"]: """divmod(self: TBasedReal, other)""" if type(self) is type(other): if self.mixed: res = divmod(float(self), float(other)) return ( self.from_float(res[0], self.significant), self.from_float(res[1], self.significant), ) max_sig = max(self.significant, other.significant) if self == 0: zero = self.zero(max_sig) return (zero, zero) max_significant = max(self.significant, other.significant) s_self = self.resize(max_significant) s_other = other.resize(max_significant) if s_self.remainder == s_other.remainder == 0: qself = s_self.subunit_quantity(max_significant) qother = s_other.subunit_quantity(max_significant) fdiv, mod = divmod(qself, qother) return ( self.from_int(fdiv, max_sig), self.from_int(mod) >> max_significant, ) fdiv = np.floor(self.decimal / other.decimal) if fdiv == self.decimal / other.decimal: mod = Decimal(0) else: mod = self.decimal % other.decimal + ( 0 if self.sign == other.sign else other.decimal ) return self.from_int(fdiv, max_sig), self.from_decimal(mod, max_sig) if np.isreal(other): return divmod(self, self.from_float(float(other), self.significant)) raise NotImplementedError def __floordiv__(self: TBasedReal, other) -> TBasedReal: # type: ignore """self // other""" return divmod(self, other)[0] def __rfloordiv__(self: TBasedReal, other): """other // self: The floor() of other/self.""" return other // float(self) def __mod__(self: TBasedReal, other) -> TBasedReal: """self % other""" return divmod(self, other)[1] def __rmod__(self: TBasedReal, other): """other % self""" return other % float(self) @overload def __truediv__( # type: ignore self: TBasedReal, other: Union[float, "BasedReal"] ) -> TBasedReal: ... @overload def __truediv__(self: TBasedReal, other: Unit) -> "BasedQuantity[TBasedReal]": ... def __truediv__(self: TBasedReal, other): """self / other""" if isinstance(other, UnitBase): return self * (other**-1) if type(self) is type(other): return super().__truediv__(other) return self / self.from_float(float(other), significant=self.significant) def __gt__(self: TBasedReal, other) -> bool: """self > other""" if not isinstance(other, Number): return other <= self if isinstance(other, BasedReal): return self.decimal > other.decimal other = cast(SupportsFloat, other) return float(self) > float(other) def __eq__(self: TBasedReal, other) -> bool: """self == other""" if not isinstance(other, SupportsFloat): return False if isinstance(other, BasedReal): return self.decimal == other.decimal return float(self) == float(other)
[docs] def equals(self: TBasedReal, other: "BasedReal") -> bool: """Tests strict equivalence between this BasedReal and another >>> Sexagesimal("1,2;3").equals(Sexagesimal("1,2;3")) True >>> Sexagesimal("1,2;3").equals(Sexagesimal("1,2;3,0")) False :param other: The other BasedReal to be compared with the first :type other: BasedReal :return: True if both objects are the same, False otherwise :rtype: bool """ if type(self) is not type(other): return False return ( self.left == other.left and self.right == other.right and self.sign == other.sign and self.remainder == other.remainder )
def __ne__(self: TBasedReal, other) -> bool: """self != other""" return not self == other def __ge__(self: TBasedReal, other) -> bool: """self >= other""" return self > other or self == other def __lt__(self: TBasedReal, other) -> bool: """self < other""" return not self >= other def __le__(self: TBasedReal, other) -> bool: """self <= other""" return not self > other def __floor__(self): """Finds the greatest Integral <= self.""" return self.__trunc__() + (1 if self.sign < 0 else 0) def __ceil__(self): """Finds the least Integral >= self.""" return self.__trunc__() + (1 if self.sign > 0 else 0) def __hash__(self) -> int: if self.remainder == 0 and all([x == 0 for x in self.right]): return int(self) return hash((self.left, self.right, self.sign, self.remainder))
[docs] def sqrt(self: TBasedReal, iteration: Optional[int] = None) -> TBasedReal: """Returns the square root, using Babylonian method :param iteration: Number of iterations, defaults to the significant number :type iteration: Optional[int], optional """ if self.sign < 0: raise ValueError("Square root domain error") if self == 0: return self if iteration is None: iteration = self._get_significant(self) if self >= 1: res = self.from_int(int(np.sqrt(float(self)))) else: res = self.from_float(np.sqrt(float(self)), self.significant) iteration = 0 for _ in range(iteration): res += self / res res /= 2 return res
def _set_remainder(self: TBasedReal, remainder: Decimal) -> TBasedReal: return type(self)(self.left, self.right, sign=self.sign, remainder=remainder)
class BasedQuantity(Quantity, Generic[TBasedReal]): value: TBasedReal def __new__(cls, value, unit, **kwargs): if ( not isinstance(value, BasedReal) or isinstance(value, (Sequence, np.ndarray)) and not all(isinstance(v, BasedReal) for v in value) ): return Quantity(value, unit, **kwargs) def _len(_): del type(value).__len__ return 0 type(value).__len__ = _len self = super().__new__(cls, value, unit=unit, dtype=object, **kwargs) return self def __mul__(self, other) -> "BasedQuantity[TBasedReal]": # pragma: no cover return super().__mul__(other) def __add__(self, other) -> "BasedQuantity[TBasedReal]": # pragma: no cover return super().__add__(other) def __sub__(self, other) -> "BasedQuantity[TBasedReal]": # pragma: no cover return super().__sub__(other) def __truediv__(self, other) -> "BasedQuantity[TBasedReal]": # pragma: no cover return super().__truediv__(other) def __lshift__(self, other) -> "BasedQuantity[TBasedReal]": if isinstance(other, Number): return super(Quantity, self).__lshift__(other) return super().__lshift__(other) def __rshift__(self, other) -> "BasedQuantity[TBasedReal]": if isinstance(other, Number): return super(Quantity, self).__rshift__(other) return super().__rshift__(other) def __getattr__(self, attr: str): if attr.startswith(("_", "__")) and not attr.endswith("__"): raise AttributeError vect = np.frompyfunc(lambda x: getattr(x, attr), 1, 1) properties = ("left", "right", "significant", "sign", "remainder", "base") unit = _d(self.unit) if attr not in properties else None UFUNC_HELPERS[vect] = lambda *_: ([None, None], unit) if callable(getattr(BasedReal, attr)): def _new_func(*args): vfunc = np.frompyfunc(lambda x: x(*args), 1, 1) UFUNC_HELPERS[vfunc] = lambda *_: ([None, None], unit) return vfunc(vect(self)) return _new_func return vect(self) def __round__( self, significant: Optional[int] = None ) -> "BasedQuantity[TBasedReal]": return self.__getattr__("__round__")(significant) def __abs__(self) -> "BasedQuantity[TBasedReal]": # pragma: no cover return self.__getattr__("__abs__")() def __quantity_subclass__(self, _): return type(self), True def _shift_helper(f, unit1, unit2): if unit2: # pragma: no cover raise UnitTypeError( "Can only apply '{}' function to " "dimensionless quantities".format(f.__name__) ) return [None, None], _d(unit1) UFUNC_HELPERS[np.left_shift] = _shift_helper UFUNC_HELPERS[np.right_shift] = _shift_helper class BasedRealException(Exception): pass class EmptyStringException(BasedRealException, ValueError): pass class TooManySeparators(BasedRealException, ValueError): pass class IllegalBaseValueError(BasedRealException, ValueError): """ Raised when a value is not in the range of the specified base. ```python if not 0 <= val < radix_at_pos(radix, i): raise IllegalBaseValueError(radix, radix_at_pos(radix, i), val) ``` """ def __init__(self, radix, base, num): super().__init__() self.radix = radix self.base = base self.num = num def __str__(self): return f"An invalid value for ({self.radix.__name__}) was found \ ('{self.num}'); should be in the range [0,{self.base}[)." class IllegalFloatError(BasedRealException, TypeError): """ Raised when an expected int value is a float. ```python if isinstance(val, float): raise IllegalFloatError(val) ``` """ def __init__(self, num): super().__init__() self.num = num def __str__(self): return f"An illegal float value was found ('{self.num}')"