| ################################################################################ |
| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| ################################################################################ |
| |
| import re |
| from decimal import Decimal as BigDecimal |
| from decimal import Inexact |
| from decimal import ROUND_HALF_UP |
| from decimal import localcontext |
| from typing import Optional, Tuple |
| |
| _DECIMAL_PATTERN = re.compile( |
| r"^(DECIMAL|NUMERIC|DEC)(?:\((\d+)(?:,\s*(\d+))?\))?$", |
| re.IGNORECASE, |
| ) |
| |
| |
| class Decimal: |
| """ |
| An internal data structure representing data of DecimalType. |
| |
| This data structure is immutable and might store decimal values in a |
| compact representation (as an integer value) if values are small enough. |
| |
| Python implementation note: |
| ``decimal_val`` is an instance of Python's standard library |
| ``decimal.Decimal`` (imported as ``BigDecimal``), corresponding to |
| Java's ``java.math.BigDecimal``. |
| """ |
| |
| # Maximum number of decimal digits a long integer can represent. |
| # (1e18 < Long.MAX_VALUE < 1e19) |
| MAX_LONG_DIGITS = 18 |
| |
| MAX_COMPACT_PRECISION = 18 |
| |
| # Powers of 10 used for compact decimal conversion. |
| POW10 = tuple(10 ** i for i in range(MAX_COMPACT_PRECISION + 1)) |
| |
| # The semantics of the fields are as follows: |
| # |
| # - precision and scale represent the SQL decimal type. |
| # - If decimal_val is set, it stores the complete decimal value. |
| # - Otherwise, the decimal value is represented by |
| # long_val / (10 ** scale). |
| # |
| # Note that (precision, scale) must always be correct. |
| # |
| # If precision > MAX_COMPACT_PRECISION: |
| # decimal_val stores the value and long_val is undefined. |
| # Otherwise: |
| # (long_val, scale) represents the value and decimal_val may be |
| # lazily initialized and cached. |
| def __init__( |
| self, |
| precision: int, |
| scale: int, |
| long_val: int, |
| decimal_val: Optional[BigDecimal], |
| ): |
| self.precision = precision |
| self.scale = scale |
| self.long_val = long_val |
| self.decimal_val = decimal_val |
| |
| # ---------------------------------------------------------------------- |
| # Public Interfaces |
| # ---------------------------------------------------------------------- |
| |
| def to_big_decimal(self) -> BigDecimal: |
| """ |
| Converts this Decimal into a python builtin-in decimal.Decimal instance. |
| """ |
| if self.decimal_val is None: |
| with localcontext() as ctx: |
| ctx.prec = max(self.precision + abs(self.scale), 38) |
| self.decimal_val = BigDecimal(self.long_val).scaleb(-self.scale) |
| return self.decimal_val |
| |
| def to_unscaled_long(self) -> int: |
| """ |
| Returns the unscaled integer value of this Decimal. |
| |
| Raises: |
| ArithmeticError: |
| If this Decimal does not exactly fit into a long integer. |
| """ |
| if self.is_compact(): |
| return self.long_val |
| |
| with localcontext() as ctx: |
| bd = self.to_big_decimal() |
| ctx.prec = max( |
| len(bd.as_tuple().digits) + abs(self.scale), |
| self.precision, |
| 38, |
| ) |
| value = bd.scaleb(self.scale) |
| |
| if value != value.to_integral_exact(): |
| raise ArithmeticError("Decimal does not exactly fit in long.") |
| |
| int_val = int(value) |
| if not (-(1 << 63) <= int_val <= (1 << 63) - 1): |
| raise ArithmeticError("BigInteger out of long range") |
| |
| return int_val |
| |
| def to_unscaled_bytes(self) -> bytes: |
| """ |
| Returns the unscaled value encoded as a signed byte array. |
| """ |
| |
| with localcontext() as ctx: |
| bd = self.to_big_decimal() |
| ctx.prec = max( |
| len(bd.as_tuple().digits) + abs(self.scale), |
| self.precision, |
| 38, |
| ) |
| value = bd.scaleb(self.scale) |
| |
| if value != value.to_integral_exact(): |
| raise ArithmeticError("Decimal does not exactly fit in long.") |
| |
| unscaled = int(value) |
| length = self._get_compact_byte_length(unscaled) |
| return unscaled.to_bytes(length, byteorder="big", signed=True) |
| |
| def is_compact(self) -> bool: |
| """ |
| Returns whether the decimal value is small enough to be stored in a long integer. |
| """ |
| return self.precision <= self.MAX_COMPACT_PRECISION |
| |
| def copy(self) -> "Decimal": |
| """ |
| Returns a copy of this Decimal. |
| """ |
| return Decimal( |
| self.precision, |
| self.scale, |
| self.long_val, |
| self.decimal_val, |
| ) |
| |
| # ---------------------------------------------------------------------- |
| # Comparison |
| # ---------------------------------------------------------------------- |
| |
| def __eq__(self, other): |
| if not isinstance(other, Decimal): |
| return False |
| return self.compare_to(other) == 0 |
| |
| def __lt__(self, other): |
| return self.compare_to(other) < 0 |
| |
| def compare_to(self, other: "Decimal") -> int: |
| """ |
| Compares this Decimal with another Decimal. |
| """ |
| if ( |
| self.is_compact() |
| and other.is_compact() |
| and self.scale == other.scale |
| ): |
| if self.long_val < other.long_val: |
| return -1 |
| if self.long_val > other.long_val: |
| return 1 |
| return 0 |
| |
| a = self.to_big_decimal() |
| b = other.to_big_decimal() |
| |
| if a < b: |
| return -1 |
| if a > b: |
| return 1 |
| return 0 |
| |
| def __hash__(self): |
| return hash(self.to_big_decimal()) |
| |
| def __str__(self): |
| return format(self.to_big_decimal(), "f") |
| |
| # ---------------------------------------------------------------------- |
| # Constructor Utilities |
| # ---------------------------------------------------------------------- |
| |
| @classmethod |
| def from_big_decimal( |
| cls, |
| value: BigDecimal, |
| precision: int, |
| scale: int, |
| ) -> Optional["Decimal"]: |
| """ |
| Creates a ``Decimal`` from a Python's built-in ``decimal.Decimal`` with the given precision and scale. |
| |
| The value is rounded to the requested scale using ROUND_HALF_UP. |
| If the resulting precision exceeds the specified precision, None is returned. |
| |
| Note: |
| Java ``BigDecimal`` provides arbitrary precision. Paimon supports |
| DECIMAL precision up to 38 digits, so a temporary context with |
| precision >= 38 is used here to match Java semantics without |
| modifying the global decimal context. |
| """ |
| |
| if not value.is_finite(): |
| raise ArithmeticError("NaN and Infinity are not supported.") |
| |
| with localcontext() as ctx: |
| t = value.as_tuple() |
| ctx.prec = max( |
| precision + scale, |
| len(t.digits) + abs(t.exponent) + scale, |
| 38, |
| ) |
| |
| value = value.quantize( |
| BigDecimal(1).scaleb(-scale), |
| rounding=ROUND_HALF_UP, |
| ) |
| |
| digits = len(value.as_tuple().digits) |
| |
| if digits > precision: |
| return None |
| |
| long_val = -1 |
| |
| if precision <= cls.MAX_COMPACT_PRECISION: |
| unscaled = value.scaleb(scale) |
| |
| if unscaled != unscaled.to_integral_exact(): |
| raise ArithmeticError( |
| "Decimal does not exactly fit in long." |
| ) |
| |
| long_val = int(unscaled) |
| |
| return cls( |
| precision, |
| scale, |
| long_val, |
| value, |
| ) |
| |
| @classmethod |
| def from_unscaled_long( |
| cls, |
| unscaled_long: int, |
| precision: int, |
| scale: int, |
| ) -> "Decimal": |
| """ |
| Creates a Decimal from an unscaled integer value with the given precision and scale. |
| """ |
| if precision <= 0 or precision > cls.MAX_LONG_DIGITS: |
| raise ValueError( |
| "precision must be between 1 and {}".format( |
| cls.MAX_LONG_DIGITS |
| ) |
| ) |
| |
| return cls( |
| precision, |
| scale, |
| unscaled_long, |
| None, |
| ) |
| |
| @classmethod |
| def from_unscaled_bytes( |
| cls, |
| unscaled_bytes: bytes, |
| precision: int, |
| scale: int, |
| ) -> Optional["Decimal"]: |
| """ |
| Creates a Decimal from an unscaled signed byte array. |
| """ |
| value = int.from_bytes( |
| unscaled_bytes, |
| byteorder="big", |
| signed=True, |
| ) |
| |
| with localcontext() as ctx: |
| ctx.prec = max( |
| len(str(abs(value))) + abs(scale), |
| precision, |
| 38, |
| ) |
| bd = BigDecimal(value).scaleb(-scale) |
| |
| return cls.from_big_decimal( |
| bd, |
| precision, |
| scale, |
| ) |
| |
| @classmethod |
| def zero(cls, precision: int, scale: int) -> Optional["Decimal"]: |
| """ |
| Creates a Decimal representing zero with the given precision and scale. |
| |
| If the precision exceeds the supported range, None is returned. |
| """ |
| if precision <= cls.MAX_COMPACT_PRECISION: |
| return cls( |
| precision, |
| scale, |
| 0, |
| None, |
| ) |
| |
| return cls.from_big_decimal( |
| BigDecimal(0), |
| precision, |
| scale, |
| ) |
| |
| # ---------------------------------------------------------------------- |
| # Arithmetic Operations |
| # ---------------------------------------------------------------------- |
| |
| @staticmethod |
| def add(lhs: BigDecimal, rhs: BigDecimal, precision: int) -> BigDecimal: |
| """ |
| Returns the sum of two decimal values without losing precision due to |
| Python's default decimal context. |
| |
| The operation is performed in a temporary high-precision context to |
| emulate Java BigDecimal arithmetic. |
| """ |
| assert isinstance( |
| lhs, BigDecimal |
| ), f"'lhs' must be a decimal.Decimal, got {type(lhs).__name__}" |
| |
| assert isinstance( |
| rhs, BigDecimal |
| ), f"'rhs' must be a decimal.Decimal, got {type(rhs).__name__}" |
| |
| with localcontext() as ctx: |
| ctx.prec = max( |
| len(lhs.as_tuple().digits), |
| len(rhs.as_tuple().digits), |
| precision, |
| 38, |
| ) + 1 |
| |
| return lhs + rhs |
| |
| @staticmethod |
| def subtract(lhs: BigDecimal, rhs: BigDecimal, precision: int) -> BigDecimal: |
| """ |
| Returns the difference of two decimal values without losing precision due |
| to Python's default decimal context. |
| |
| The operation is performed in a temporary high-precision context to |
| emulate Java BigDecimal arithmetic. |
| """ |
| assert isinstance( |
| lhs, BigDecimal |
| ), f"'lhs' must be a decimal.Decimal, got {type(lhs).__name__}" |
| |
| assert isinstance( |
| rhs, BigDecimal |
| ), f"'rhs' must be a decimal.Decimal, got {type(rhs).__name__}" |
| |
| with localcontext() as ctx: |
| ctx.prec = max( |
| len(lhs.as_tuple().digits), |
| len(rhs.as_tuple().digits), |
| precision, |
| 38, |
| ) + 1 |
| |
| return lhs - rhs |
| |
| @staticmethod |
| def multiply(lhs: BigDecimal, rhs: BigDecimal, precision: int) -> BigDecimal: |
| """ |
| Returns the product of two decimal values without losing precision due to |
| Python's default decimal context. |
| |
| The operation is performed in a temporary high-precision context to |
| emulate Java BigDecimal arithmetic. |
| """ |
| assert isinstance( |
| lhs, BigDecimal |
| ), f"'lhs' must be a decimal.Decimal, got {type(lhs).__name__}" |
| |
| assert isinstance( |
| rhs, BigDecimal |
| ), f"'rhs' must be a decimal.Decimal, got {type(rhs).__name__}" |
| |
| with localcontext() as ctx: |
| ctx.prec = max( |
| len(lhs.as_tuple().digits) |
| + len(rhs.as_tuple().digits), |
| precision, |
| 38, |
| ) |
| |
| return lhs * rhs |
| |
| @staticmethod |
| def divide(lhs: BigDecimal, rhs: BigDecimal, precision: int) -> BigDecimal: |
| """ |
| Returns the exact quotient of two decimal values. |
| |
| Raises: |
| ArithmeticError: |
| If the division has a non-terminating decimal expansion, |
| matching the behavior of Java BigDecimal.divide(BigDecimal). |
| """ |
| assert isinstance( |
| lhs, BigDecimal |
| ), f"'lhs' must be a decimal.Decimal, got {type(lhs).__name__}" |
| |
| assert isinstance( |
| rhs, BigDecimal |
| ), f"'rhs' must be a decimal.Decimal, got {type(rhs).__name__}" |
| |
| with localcontext() as ctx: |
| ctx.prec = max(precision, 38) |
| ctx.traps[Inexact] = True |
| |
| try: |
| return lhs / rhs |
| except Inexact: |
| raise ArithmeticError( |
| "Non-terminating decimal expansion; " |
| "no exact representable decimal result." |
| ) |
| |
| # ---------------------------------------------------------------------- |
| # Utility Methods |
| # ---------------------------------------------------------------------- |
| |
| @staticmethod |
| def is_compact_precision(precision: int) -> bool: |
| """ |
| Returns whether the specified precision can be stored in compact form. |
| """ |
| return precision <= Decimal.MAX_COMPACT_PRECISION |
| |
| @staticmethod |
| def extract_decimal_precision_scale(type_str: str) -> Tuple[int, int]: |
| """ |
| Extracts the precision and scale from a DECIMAL/NUMERIC/DEC type string. |
| |
| Returns: (precision, scale) |
| |
| Examples: |
| DECIMAL -> (10, 0) |
| DECIMAL(10) -> (10, 0) |
| DECIMAL(10,2) -> (10, 2) |
| NUMERIC(20,5) -> (20, 5) |
| DEC(18,6) -> (18, 6) |
| """ |
| match = _DECIMAL_PATTERN.fullmatch(type_str.strip()) |
| if match is None: |
| raise ValueError(f"Invalid decimal type: {type_str}") |
| |
| precision = match.group(2) |
| scale = match.group(3) |
| |
| if precision is None: |
| return 10, 0 |
| |
| if scale is None: |
| return int(precision), 0 |
| |
| return int(precision), int(scale) |
| |
| @staticmethod |
| def _get_compact_byte_length(val: int) -> int: |
| if val == 0: |
| return 1 |
| |
| if val > 0: |
| bits = val.bit_length() |
| else: |
| bits = (~val).bit_length() |
| |
| return bits // 8 + 1 |