"""
.. 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

"""

import math
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,
    Generator,
    List,
    Literal,
    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.list_to_tuple import list_to_tuple
from kanon.utils.looping_list import LoopingList

from .precision import PreciseNumber, PrecisionMode, TruncatureMode, set_precision

__all__ = ["BasedReal"]


TBasedReal = TypeVar("TBasedReal", bound="BasedReal")
RadixBase = Tuple[LoopingList[int], LoopingList[int]]


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


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: LoopingList[str]
    """List of string separators, used for displaying the integer part of the number"""
    _mixed: bool
    """Is this class with a mixed base"""
    __left: Tuple[int, ...]
    __right: Tuple[int, ...]
    __remainder: Decimal
    __sign: Literal[-1, 1]
    __slots__ = (
        "_base",
        "_integer_separators",
        "__left",
        "__right",
        "__remainder",
        "__sign",
    )

    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 > 0 for x in left)
        assert all(isinstance(x, int) and x > 0 for x in right)
        cls._base = (LoopingList(left), LoopingList(right))
        if separators is not None:
            if len(separators) != len(left):
                raise ValueError
            cls._integer_separators = LoopingList(separators)
        else:
            cls._integer_separators = LoopingList(
                ["," if x != 10 else "" for x in left]
            )
        cls._mixed = any(x != left[0] for x in tuple(left) + tuple(right))
        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 np.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])
                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 at BasedReal creation")
        elif len(args) == 1:
            if isinstance(args[0], str):
                return cls._from_string(args[0])
            raise ValueError(
                "Please specify a number of significant positions"
                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) -> Literal[-1, 1]:
        """
        Sign of this `BasedReal`

        >>> Sexagesimal(1,2,3, sign=-1).sign
        -1

        :rtype: Literal[-1, 1]
        """
        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

    def to_fraction(self) -> Fraction:
        """
        :return: this `BasedReal` as a :class:`~fractions.Fraction` object.
        """
        return Fraction(self.decimal)

    @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)):
                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:  # pragma: no cover
                    value = rleft[0]
                    rem = rleft[1:]
                left_numbers.insert(0, int(value[::-1]))
                rleft = rem.strip()
                if len(rleft) == 1:
                    break
            left_numbers.insert(0, int(rleft[::-1]))

        return cls(left_numbers, right_numbers, sign=sign)

    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 = type(self)(
                (), self.right[significant:], remainder=self.remainder
            )

            return type(self)(
                self.left,
                self.right[:significant],
                remainder=remainder.decimal,
                sign=self.sign,
            )

        raise NotImplementedError

    def __trunc__(self):
        return int(float(self.truncate(0)))

    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)

    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__()

    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__()

    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)

    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)

    @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

    @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,
        )

    @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

        factor = Decimal(1)
        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
        )

    @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)

    @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]:
        ...

    @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)

    @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)

        if self.mixed:
            return self.from_float(float(self) / float(other), self.significant)

        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

        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: PreciseNumber) -> TBasedReal:

        other = cast(BasedReal, _other)
        return self + -other

    def __rtruediv__(self: TBasedReal, other):
        """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) or self.mixed:
            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__(self: TBasedReal, other: Union[float, "BasedReal"]) -> TBasedReal:  # type: ignore
        ...

    @overload
    def __mul__(self: TBasedReal, other: Unit) -> "BasedQuantity":
        ...

    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)

    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 = math.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__(self: TBasedReal, other: Number) -> TBasedReal:  # type: ignore
        ...

    @overload
    def __truediv__(self: TBasedReal, other: Unit) -> "BasedQuantity":
        ...

    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)

    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))

    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(math.sqrt(float(self))))
        else:
            res = self.from_float(math.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):

    value: BasedReal

    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":  # pragma: no cover
        return super().__mul__(other)

    def __add__(self, other) -> "BasedQuantity":  # pragma: no cover
        return super().__add__(other)

    def __sub__(self, other) -> "BasedQuantity":  # pragma: no cover
        return super().__sub__(other)

    def __truediv__(self, other) -> "BasedQuantity":  # pragma: no cover
        return super().__truediv__(other)

    def __lshift__(self, other) -> "BasedQuantity":
        if isinstance(other, Number):
            return super(Quantity, self).__lshift__(other)
        return super().__lshift__(other)

    def __rshift__(self, other) -> "BasedQuantity":
        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":
        return self.__getattr__("__round__")(significant)

    def __abs__(self) -> "BasedQuantity":  # 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}')"

"""
.. 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

"""

import math
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,
    Generator,
    List,
    Literal,
    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.list_to_tuple import list_to_tuple
from kanon.utils.looping_list import LoopingList

from .precision import PreciseNumber, PrecisionMode, TruncatureMode, set_precision

__all__ = ["BasedReal"]


TBasedReal = TypeVar("TBasedReal", bound="BasedReal")
RadixBase = Tuple[LoopingList[int], LoopingList[int]]


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


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: LoopingList[str]
    """List of string separators, used for displaying the integer part of the number"""
    _mixed: bool
    """Is this class with a mixed base"""
    __left: Tuple[int, ...]
    __right: Tuple[int, ...]
    __remainder: Decimal
    __sign: Literal[-1, 1]
    __slots__ = (
        "_base",
        "_integer_separators",
        "__left",
        "__right",
        "__remainder",
        "__sign",
    )

    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 > 0 for x in left)
        assert all(isinstance(x, int) and x > 0 for x in right)
        cls._base = (LoopingList(left), LoopingList(right))
        if separators is not None:
            if len(separators) != len(left):
                raise ValueError
            cls._integer_separators = LoopingList(separators)
        else:
            cls._integer_separators = LoopingList(
                ["," if x != 10 else "" for x in left]
            )
        cls._mixed = any(x != left[0] for x in tuple(left) + tuple(right))
        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 np.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])
                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 at BasedReal creation")
        elif len(args) == 1:
            if isinstance(args[0], str):
                return cls._from_string(args[0])
            raise ValueError(
                "Please specify a number of significant positions"
                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) -> Literal[-1, 1]:
        """
        Sign of this `BasedReal`

        >>> Sexagesimal(1,2,3, sign=-1).sign
        -1

        :rtype: Literal[-1, 1]
        """
        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

    def to_fraction(self) -> Fraction:
        """
        :return: this `BasedReal` as a :class:`~fractions.Fraction` object.
        """
        return Fraction(self.decimal)

    @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)):
                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:  # pragma: no cover
                    value = rleft[0]
                    rem = rleft[1:]
                left_numbers.insert(0, int(value[::-1]))
                rleft = rem.strip()
                if len(rleft) == 1:
                    break
            left_numbers.insert(0, int(rleft[::-1]))

        return cls(left_numbers, right_numbers, sign=sign)

    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 = type(self)(
                (), self.right[significant:], remainder=self.remainder
            )

            return type(self)(
                self.left,
                self.right[:significant],
                remainder=remainder.decimal,
                sign=self.sign,
            )

        raise NotImplementedError

    def __trunc__(self):
        return int(float(self.truncate(0)))

    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)

    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__()

    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__()

    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)

    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)

    @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

    @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,
        )

    @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

        factor = Decimal(1)
        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
        )

    @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)

    @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]:
        ...

    @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)

    @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)

        if self.mixed:
            return self.from_float(float(self) / float(other), self.significant)

        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

        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: PreciseNumber) -> TBasedReal:

        other = cast(BasedReal, _other)
        return self + -other

    def __rtruediv__(self: TBasedReal, other):
        """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) or self.mixed:
            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__(self: TBasedReal, other: Union[float, "BasedReal"]) -> TBasedReal:  # type: ignore
        ...

    @overload
    def __mul__(self: TBasedReal, other: Unit) -> "BasedQuantity":
        ...

    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)

    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 = math.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__(self: TBasedReal, other: Number) -> TBasedReal:  # type: ignore
        ...

    @overload
    def __truediv__(self: TBasedReal, other: Unit) -> "BasedQuantity":
        ...

    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)

    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))

    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(math.sqrt(float(self))))
        else:
            res = self.from_float(math.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):

    value: BasedReal

    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":  # pragma: no cover
        return super().__mul__(other)

    def __add__(self, other) -> "BasedQuantity":  # pragma: no cover
        return super().__add__(other)

    def __sub__(self, other) -> "BasedQuantity":  # pragma: no cover
        return super().__sub__(other)

    def __truediv__(self, other) -> "BasedQuantity":  # pragma: no cover
        return super().__truediv__(other)

    def __lshift__(self, other) -> "BasedQuantity":
        if isinstance(other, Number):
            return super(Quantity, self).__lshift__(other)
        return super().__lshift__(other)

    def __rshift__(self, other) -> "BasedQuantity":
        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":
        return self.__getattr__("__round__")(significant)

    def __abs__(self) -> "BasedQuantity":  # 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}')"

.. 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 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 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 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 Abstract class to represent a number in a specific `base`. Base of this BasedReal, (integer part, fractional part) List of string separators, used for displaying the integer part of the number Is this class with a mixed base Checks that the given values are in the range of the base and are integers. # pragma: no cover Remove the useless trailing zeros in the integer part and return how many were removed 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 Tuple of values at integer positions          >>> Sexagesimal(1,2,3).left         (1, 2, 3)          :rtype: Tuple[int, ...] Tuple of values at fractional positions          >>> Sexagesimal((1,2,3), (4,5)).right         (4, 5)          :rtype: Tuple[int, ...] Base of this BasedReal, (integer part, fractional part)          >>> Sexagesimal(1).base         ([..., 60, ...], [..., 60, ...])          :rtype: `RadixBase` 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 Sign of this `BasedReal`          >>> Sexagesimal(1,2,3, sign=-1).sign         -1          :rtype: Literal[-1, 1] Precision of this `BasedReal` (equals to length of fractional part)          >>> Sexagesimal((1,2,3), (4,5)).significant         2          :rtype: int This `BasedReal` converted as a `~decimal.Decimal`          >>> Sexagesimal((1,2,3), (15,36)).decimal         Decimal('3723.26')          :rtype: Decimal :return: this `BasedReal` as a :class:`~fractions.Fraction` object. :param fraction: a `~fractions.Fraction` object         :param significant: significant precision desired         :return: a `BasedReal` object computed from a Fraction Convert to string representation.         Note that this representation is rounded (with respect to the remainder attribute) not truncated          :return: String representation of this number 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` # pragma: no cover 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` 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 Removes unnecessary zeros from fractional part of this BasedReal.          :return: Minimized BasedReal self << other          :param other: Amount to shift this BasedReal         :type other: int         :return: Shifted number         :rtype: BasedReal self >> other          :param other: Amount to shift this BasedReal         :type other: int         :return: Shifted number         :rtype: BasedReal 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 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 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 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 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 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 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 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 Range generator, equivalent to `range` builtin but yields `BasedReal` numbers.          :yield: `BasedReal` integers. 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 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 Compute the int value of this BasedReal object # pragma: no cover self + other          >>> Sexagesimal('01, 21; 47, 25') + Sexagesimal('45; 32, 14, 22')         02,07 ; 19,39,22 other + self other / self self**exponent         Negative numbers cannot be raised to a non-integer power base ** self -self +self abs(self)          >>> abs(Sexagesimal('-12; 14, 15'))         12 ; 14,15          :return: the absolute value of self # type: ignore self * other          >>> Sexagesimal('01, 12; 04, 17') * Sexagesimal('7; 45, 55')         09,19 ; 39,15 |r0.7 other * self divmod(self: TBasedReal, other) # type: ignore self // other other // self: The floor() of other/self. self % other other % self # type: ignore self / other self > other self == other 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 self != other self >= other self < other self <= other Finds the greatest Integral <= self. Finds the least Integral >= self. Returns the square root, using Babylonian method          :param iteration: Number of iterations, defaults to the significant number         :type iteration: Optional[int], optional # pragma: no cover # pragma: no cover # pragma: no cover # pragma: no cover # pragma: no cover # pragma: no cover 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)     ``` Raised when an expected int value is a float.      ```python     if isinstance(val, float):         raise IllegalFloatError(val)     ```