Source code for terra_sdk.core.numeric

"""Numeric types."""

from __future__ import annotations

import re
from decimal import Decimal
from typing import Union

from terra_sdk.util.json import JSONSerializable

DEC_NUM_DIGITS = 18
"""Number of digits for Decimal."""

DEC_ONE = 10**DEC_NUM_DIGITS
DEC_PATTERN = re.compile(r"^(\-)?(\d+)(\.(\d+))?\Z")

__all__ = ["DEC_NUM_DIGITS", "Dec", "Numeric"]


def convert_to_dec_bignum(arg: Union[str, int, float, Decimal]):
    if isinstance(arg, int):
        return arg * DEC_ONE
    if isinstance(arg, float):
        arg = str("%f" % arg)
    if isinstance(arg, str):
        parts = DEC_PATTERN.match(arg)
        if parts is None:
            raise ValueError(f"Unable to parse Dec from string: {arg}")
        result = int(parts.group(2)) * DEC_ONE  # whole part
        if parts.group(3):
            fraction = int(parts.group(4)[0:DEC_NUM_DIGITS].ljust(DEC_NUM_DIGITS, "0"))
            result += fraction
        if parts.group(1):
            result *= -1
        return result
    elif isinstance(arg, Decimal):
        whole = int(arg)
        fraction = int(arg % 1)
        return int((whole * DEC_ONE) + (fraction * DEC_ONE))
    else:
        raise TypeError(
            f"Unable to parse Dec integer representation from given argument {arg}"
        )


def chop_precision_and_round(d: int) -> int:
    """Cosmos-SDK's banker's rounding:
    https://github.com/cosmos/cosmos-sdk/blob/1d75e0e984e7132efd54c3526e36b3585e2d91c0/types/decimal.go#L491
    """
    if d < 0:
        return -1 * chop_precision_and_round(d * -1)

    quo, rem = d // DEC_ONE, d % DEC_ONE

    if rem == 0:
        return quo

    if rem < DEC_ONE / 2:
        return quo
    elif rem > DEC_ONE / 2:
        return quo + 1
    else:
        if quo % 2 == 0:
            return quo
        return quo + 1


[docs]class Dec(JSONSerializable): """BigInt-based Decimal representation with basic arithmetic operations with compatible Python numeric types (int, float, Decimal). Does not work with ``NaN``, ``Infinity``, ``+0``, ``-0``, etc. Serializes as a string with 18 points of decimal precision. >>> Dec(5) Dec("5.0") >>> Dec("121.1232") Dec("121.1232") >>> Dec(121.1232) Dec("121.1232") Args: arg (Union[str, int, float, Decimal, Dec]): argument to coerce into Dec """ _i: int = 0 def __init__(self, arg: Union[str, int, float, Decimal, Dec]): if isinstance(arg, Dec): self._i = arg._i return else: self._i = int(convert_to_dec_bignum(arg))
[docs] @classmethod def zero(cls) -> Dec: """Dec representation of zero. Returns: Dec: zero """ return cls(0)
[docs] @classmethod def one(cls): """Dec representation of one. Returns: Dec: one """ nd = cls(0) nd._i = DEC_ONE return nd
def __str__(self) -> str: """Converts to a string using all 18 decimal precision points. Returns: str: string representation """ if self._i == 0: return "0." + DEC_NUM_DIGITS * "0" parity = "-" if self._i < 0 else "" return f"{parity}{self.whole}.{self.frac}"
[docs] def to_short_str(self) -> str: """Converts to a string, but truncates all unnecessary zeros. Returns: str: string representation """ parity = "-" if self._i < 0 else "" frac = self.frac.rstrip("0") dot = "." if len(frac) > 0 else "" return f"{parity}{self.whole}{dot}{frac}"
def __repr__(self): return f"Dec('{self.to_short_str()}')" # short representation def __int__(self) -> int: int_part = abs(self._i) // DEC_ONE int_part *= -1 if self._i < 0 else 1 return int_part def __float__(self) -> float: # NOTE: This is not robust enough for: float(Dec(float)) to give the same output # and should mainly be used as getting a rough value from the Dec object. return float(self._i) / DEC_ONE @property def parity(self) -> int: """Get the parity of the Dec value. Returns -1 if value is below 0, and 1 otherwise. Returns: int: parity """ return -1 if self._i < 0 else 1 @property def whole(self) -> str: """Get the integral part of the Dec value. Returns: str: integer, as string """ return str(abs(self._i) // DEC_ONE) @property def frac(self) -> str: """Get the fractional part of the Dec value. Returns: str: fraction, as string """ return str(abs(self._i) % DEC_ONE).rjust(DEC_NUM_DIGITS, "0")
[docs] def to_data(self) -> str: return str(self)
def __eq__(self, other) -> bool: if isinstance(other, str): return False else: return self._i == Dec(other)._i
[docs] def lt(self, other: Union[str, int, float, Decimal, Dec]) -> bool: """Check less than. Args: other (Union[str, int, float, Decimal, Dec]): compared object """ if isinstance(other, Dec): return self._i < other._i return (Decimal(self._i) / DEC_ONE) < other
def __lt__(self, other: Union[str, int, float, Decimal, Dec]) -> bool: return self.lt(other)
[docs] def le(self, other: Union[str, int, float, Decimal, Dec]) -> bool: """Check less than or equal to. Args: other (Union[str, int, float, Decimal, Dec]): compared object """ return self < other or self.__eq__(other)
def __le__(self, other: Union[str, int, float, Decimal, Dec]) -> bool: return self.le(other)
[docs] def gt(self, other: Union[str, int, float, Decimal, Dec]) -> bool: """Check greater than. Args: other (Union[str, int, float, Decimal, Dec]): compared object """ if isinstance(other, Dec): return self._i > other._i return (Decimal(self._i) / DEC_ONE) > other
def __gt__(self, other) -> bool: return self.gt(other)
[docs] def ge(self, other) -> bool: """Check greater than or equal to. Args: other (Union[str, int, float, Decimal, Dec]): compared object """ return self.gt(other) or self.__eq__(other)
def __ge__(self, other) -> bool: return self.ge(other)
[docs] def add(self, addend: Union[str, int, float, Decimal, Dec]) -> Dec: """Performs addition. ``addend`` is first converted into Dec. Args: addend (Union[str, int, float, Decimal, Dec]): addend Returns: Dec: sum """ nd = Dec.zero() nd._i = self._i + Dec(addend)._i return nd
def __add__(self, addend: Union[str, int, float, Decimal, Dec]) -> Dec: return self.add(addend) def __radd__(self, addend: Union[str, int, float, Decimal, Dec]): return Dec(addend).add(self)
[docs] def sub(self, subtrahend: Union[str, int, float, Decimal, Dec]) -> Dec: """Performs subtraction. ``subtrahend`` is first converted into Dec. Args: subtrahend (Union[str, int, float, Decimal, Dec]): subtrahend Returns: Dec: difference """ nd = Dec.zero() nd._i = self._i - Dec(subtrahend)._i return nd
def __sub__(self, subtrahend: Union[str, int, float, Decimal, Dec]) -> Dec: return self.sub(subtrahend) def __rsub__(self, minuend: Dec) -> Dec: return Dec(minuend).sub(self)
[docs] def mul(self, multiplier: Union[str, int, float, Decimal, Dec]) -> Dec: """Performs multiplication. ``multiplier`` is first converted into Dec. Args: multiplier (Union[str, int, float, Decimal, Dec]): multiplier Returns: Dec: product """ x = self._i y = Dec(multiplier)._i nd = Dec.zero() nd._i = chop_precision_and_round(x * y) return nd
def __mul__(self, multiplier: Union[str, int, float, Decimal, Dec]) -> Dec: return self.mul(multiplier) def __rmul__(self, multiplicand: Union[str, int, float, Decimal, Dec]): return Dec(multiplicand).mul(self)
[docs] def div(self, divisor: Union[str, int, float, Decimal, Dec]) -> Dec: """Performs division. ``divisor`` is first converted into Dec. Args: divisor (Union[str, int, float, Decimal, Dec]): divisor Raises: ZeroDivisionError: if ``divisor`` is 0 Returns: Dec: quotient """ if Dec(divisor)._i == 0: raise ZeroDivisionError(f"tried to divide by 0: {self!r} / {divisor!r}") nd = Dec.zero() nd._i = chop_precision_and_round(self._i * DEC_ONE * DEC_ONE // Dec(divisor)._i) return nd
def __truediv__(self, divisor) -> Dec: return self.div(divisor) def __rtruediv__(self, divisor) -> Dec: return Dec(divisor).div(self) def __floordiv__(self, divisor): return self.div(int(divisor))
[docs] def mod(self, modulo: Union[str, int, float, Decimal, Dec]) -> Dec: """Performs modulus. ``modulo`` is first converted into Dec. Args: modulo (Union[str, int, float, Decimal, Dec]): modulo Returns: Dec: modulus """ return self.sub(self.div(modulo).mul(self))
def __mod__(self, modulo) -> Dec: return self.mod(modulo) def __neg__(self) -> Dec: x = Dec(self) x._i *= -1 return x def __abs__(self) -> Dec: x = Dec(self) x._i = abs(x._i) return x def __pos__(self) -> Dec: return abs(self)
[docs] @classmethod def from_data(cls, data: str) -> Dec: """Converts Dec-formatted string into proper :class:`Dec` object.""" return cls(data)
[docs] @classmethod def with_prec(cls, i: Union[int, str], prec: int) -> Dec: """Replicates Cosmos SDK's ``Dec.withPreic(i, prec)``. Args: i (Union[int, str]): numeric value prec (int): precision Returns: Dec: decimal """ d = cls(0) i = int(i) d._i = i * 10 ** (DEC_NUM_DIGITS - int(prec)) return d
[docs]class Numeric: Input = Union[str, int, float, Decimal, Dec] """""" Output = Union[int, Dec] """"""
[docs] @staticmethod def parse(value: Numeric.Input) -> Numeric.Output: """Parses the value and coerces it into an ``int`` or :class:`Dec`. Args: value (Numeric.Input): value to be parsed Raises: TypeError: if supplied value could not be parsed Returns: Numeric.Output: coerced number """ if isinstance(value, int) or isinstance(value, Dec): return value elif isinstance(value, str): if "." in value: return Dec(value) else: return int(value) elif isinstance(value, float) or isinstance(value, Decimal): return Dec(value) else: raise TypeError(f"could not parse numeric value to Dec or int: {value}")