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