impuls/lib/python3.11/site-packages/bson/decimal128.py

315 lines
10 KiB
Python
Raw Normal View History

# Copyright 2016-present MongoDB, Inc.
#
# Licensed 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.
"""Tools for working with the BSON decimal128 type.
.. versionadded:: 3.4
"""
import decimal
import struct
from typing import Any, Sequence, Tuple, Type, Union
_PACK_64 = struct.Struct("<Q").pack
_UNPACK_64 = struct.Struct("<Q").unpack
_EXPONENT_MASK = 3 << 61
_EXPONENT_BIAS = 6176
_EXPONENT_MAX = 6144
_EXPONENT_MIN = -6143
_MAX_DIGITS = 34
_INF = 0x7800000000000000
_NAN = 0x7C00000000000000
_SNAN = 0x7E00000000000000
_SIGN = 0x8000000000000000
_NINF = (_INF + _SIGN, 0)
_PINF = (_INF, 0)
_NNAN = (_NAN + _SIGN, 0)
_PNAN = (_NAN, 0)
_NSNAN = (_SNAN + _SIGN, 0)
_PSNAN = (_SNAN, 0)
_CTX_OPTIONS = {
"prec": _MAX_DIGITS,
"rounding": decimal.ROUND_HALF_EVEN,
"Emin": _EXPONENT_MIN,
"Emax": _EXPONENT_MAX,
"capitals": 1,
"flags": [],
"traps": [decimal.InvalidOperation, decimal.Overflow, decimal.Inexact],
"clamp": 1,
}
_DEC128_CTX = decimal.Context(**_CTX_OPTIONS.copy()) # type: ignore
_VALUE_OPTIONS = Union[decimal.Decimal, float, str, Tuple[int, Sequence[int], int]]
def create_decimal128_context() -> decimal.Context:
"""Returns an instance of :class:`decimal.Context` appropriate
for working with IEEE-754 128-bit decimal floating point values.
"""
opts = _CTX_OPTIONS.copy()
opts["traps"] = []
return decimal.Context(**opts) # type: ignore
def _decimal_to_128(value: _VALUE_OPTIONS) -> Tuple[int, int]:
"""Converts a decimal.Decimal to BID (high bits, low bits).
:Parameters:
- `value`: An instance of decimal.Decimal
"""
with decimal.localcontext(_DEC128_CTX) as ctx:
value = ctx.create_decimal(value)
if value.is_infinite():
return _NINF if value.is_signed() else _PINF
sign, digits, exponent = value.as_tuple()
if value.is_nan():
if digits:
raise ValueError("NaN with debug payload is not supported")
if value.is_snan():
return _NSNAN if value.is_signed() else _PSNAN
return _NNAN if value.is_signed() else _PNAN
significand = int("".join([str(digit) for digit in digits]))
bit_length = significand.bit_length()
high = 0
low = 0
for i in range(min(64, bit_length)):
if significand & (1 << i):
low |= 1 << i
for i in range(64, bit_length):
if significand & (1 << i):
high |= 1 << (i - 64)
biased_exponent = exponent + _EXPONENT_BIAS # type: ignore[operator]
if high >> 49 == 1:
high = high & 0x7FFFFFFFFFFF
high |= _EXPONENT_MASK
high |= (biased_exponent & 0x3FFF) << 47
else:
high |= biased_exponent << 49
if sign:
high |= _SIGN
return high, low
class Decimal128:
"""BSON Decimal128 type::
>>> Decimal128(Decimal("0.0005"))
Decimal128('0.0005')
>>> Decimal128("0.0005")
Decimal128('0.0005')
>>> Decimal128((3474527112516337664, 5))
Decimal128('0.0005')
:Parameters:
- `value`: An instance of :class:`decimal.Decimal`, string, or tuple of
(high bits, low bits) from Binary Integer Decimal (BID) format.
.. note:: :class:`~Decimal128` uses an instance of :class:`decimal.Context`
configured for IEEE-754 Decimal128 when validating parameters.
Signals like :class:`decimal.InvalidOperation`, :class:`decimal.Inexact`,
and :class:`decimal.Overflow` are trapped and raised as exceptions::
>>> Decimal128(".13.1")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
...
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
>>>
>>> Decimal128("1E-6177")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
...
decimal.Inexact: [<class 'decimal.Inexact'>]
>>>
>>> Decimal128("1E6145")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
...
decimal.Overflow: [<class 'decimal.Overflow'>, <class 'decimal.Rounded'>]
To ensure the result of a calculation can always be stored as BSON
Decimal128 use the context returned by
:func:`create_decimal128_context`::
>>> import decimal
>>> decimal128_ctx = create_decimal128_context()
>>> with decimal.localcontext(decimal128_ctx) as ctx:
... Decimal128(ctx.create_decimal(".13.3"))
...
Decimal128('NaN')
>>>
>>> with decimal.localcontext(decimal128_ctx) as ctx:
... Decimal128(ctx.create_decimal("1E-6177"))
...
Decimal128('0E-6176')
>>>
>>> with decimal.localcontext(DECIMAL128_CTX) as ctx:
... Decimal128(ctx.create_decimal("1E6145"))
...
Decimal128('Infinity')
To match the behavior of MongoDB's Decimal128 implementation
str(Decimal(value)) may not match str(Decimal128(value)) for NaN values::
>>> Decimal128(Decimal('NaN'))
Decimal128('NaN')
>>> Decimal128(Decimal('-NaN'))
Decimal128('NaN')
>>> Decimal128(Decimal('sNaN'))
Decimal128('NaN')
>>> Decimal128(Decimal('-sNaN'))
Decimal128('NaN')
However, :meth:`~Decimal128.to_decimal` will return the exact value::
>>> Decimal128(Decimal('NaN')).to_decimal()
Decimal('NaN')
>>> Decimal128(Decimal('-NaN')).to_decimal()
Decimal('-NaN')
>>> Decimal128(Decimal('sNaN')).to_decimal()
Decimal('sNaN')
>>> Decimal128(Decimal('-sNaN')).to_decimal()
Decimal('-sNaN')
Two instances of :class:`Decimal128` compare equal if their Binary
Integer Decimal encodings are equal::
>>> Decimal128('NaN') == Decimal128('NaN')
True
>>> Decimal128('NaN').bid == Decimal128('NaN').bid
True
This differs from :class:`decimal.Decimal` comparisons for NaN::
>>> Decimal('NaN') == Decimal('NaN')
False
"""
__slots__ = ("__high", "__low")
_type_marker = 19
def __init__(self, value: _VALUE_OPTIONS) -> None:
if isinstance(value, (str, decimal.Decimal)):
self.__high, self.__low = _decimal_to_128(value)
elif isinstance(value, (list, tuple)):
if len(value) != 2:
raise ValueError(
"Invalid size for creation of Decimal128 "
"from list or tuple. Must have exactly 2 "
"elements."
)
self.__high, self.__low = value # type: ignore
else:
raise TypeError(f"Cannot convert {value!r} to Decimal128")
def to_decimal(self) -> decimal.Decimal:
"""Returns an instance of :class:`decimal.Decimal` for this
:class:`Decimal128`.
"""
high = self.__high
low = self.__low
sign = 1 if (high & _SIGN) else 0
if (high & _SNAN) == _SNAN:
return decimal.Decimal((sign, (), "N")) # type: ignore
elif (high & _NAN) == _NAN:
return decimal.Decimal((sign, (), "n")) # type: ignore
elif (high & _INF) == _INF:
return decimal.Decimal((sign, (), "F")) # type: ignore
if (high & _EXPONENT_MASK) == _EXPONENT_MASK:
exponent = ((high & 0x1FFFE00000000000) >> 47) - _EXPONENT_BIAS
return decimal.Decimal((sign, (0,), exponent))
else:
exponent = ((high & 0x7FFF800000000000) >> 49) - _EXPONENT_BIAS
arr = bytearray(15)
mask = 0x00000000000000FF
for i in range(14, 6, -1):
arr[i] = (low & mask) >> ((14 - i) << 3)
mask = mask << 8
mask = 0x00000000000000FF
for i in range(6, 0, -1):
arr[i] = (high & mask) >> ((6 - i) << 3)
mask = mask << 8
mask = 0x0001000000000000
arr[0] = (high & mask) >> 48
# cdecimal only accepts a tuple for digits.
digits = tuple(int(digit) for digit in str(int.from_bytes(arr, "big")))
with decimal.localcontext(_DEC128_CTX) as ctx:
return ctx.create_decimal((sign, digits, exponent))
@classmethod
def from_bid(cls: Type["Decimal128"], value: bytes) -> "Decimal128":
"""Create an instance of :class:`Decimal128` from Binary Integer
Decimal string.
:Parameters:
- `value`: 16 byte string (128-bit IEEE 754-2008 decimal floating
point in Binary Integer Decimal (BID) format).
"""
if not isinstance(value, bytes):
raise TypeError("value must be an instance of bytes")
if len(value) != 16:
raise ValueError("value must be exactly 16 bytes")
return cls((_UNPACK_64(value[8:])[0], _UNPACK_64(value[:8])[0])) # type: ignore
@property
def bid(self) -> bytes:
"""The Binary Integer Decimal (BID) encoding of this instance."""
return _PACK_64(self.__low) + _PACK_64(self.__high)
def __str__(self) -> str:
dec = self.to_decimal()
if dec.is_nan():
# Required by the drivers spec to match MongoDB behavior.
return "NaN"
return str(dec)
def __repr__(self):
return f"Decimal128('{str(self)}')"
def __setstate__(self, value: Tuple[int, int]) -> None:
self.__high, self.__low = value
def __getstate__(self) -> Tuple[int, int]:
return self.__high, self.__low
def __eq__(self, other: Any) -> bool:
if isinstance(other, Decimal128):
return self.bid == other.bid
return NotImplemented
def __ne__(self, other: Any) -> bool:
return not self == other