decimal128.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. # Copyright 2016-present MongoDB, Inc.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Tools for working with the BSON decimal128 type.
  15. .. versionadded:: 3.4
  16. .. note:: The Decimal128 BSON type requires MongoDB 3.4+.
  17. """
  18. import decimal
  19. import struct
  20. import sys
  21. from bson.py3compat import (PY3 as _PY3,
  22. string_type as _string_type)
  23. if _PY3:
  24. _from_bytes = int.from_bytes # pylint: disable=no-member, invalid-name
  25. else:
  26. import binascii
  27. def _from_bytes(value, dummy, _int=int, _hexlify=binascii.hexlify):
  28. "An implementation of int.from_bytes for python 2.x."
  29. return _int(_hexlify(value), 16)
  30. _PACK_64 = struct.Struct("<Q").pack
  31. _UNPACK_64 = struct.Struct("<Q").unpack
  32. _EXPONENT_MASK = 3 << 61
  33. _EXPONENT_BIAS = 6176
  34. _EXPONENT_MAX = 6144
  35. _EXPONENT_MIN = -6143
  36. _MAX_DIGITS = 34
  37. _INF = 0x7800000000000000
  38. _NAN = 0x7c00000000000000
  39. _SNAN = 0x7e00000000000000
  40. _SIGN = 0x8000000000000000
  41. _NINF = (_INF + _SIGN, 0)
  42. _PINF = (_INF, 0)
  43. _NNAN = (_NAN + _SIGN, 0)
  44. _PNAN = (_NAN, 0)
  45. _NSNAN = (_SNAN + _SIGN, 0)
  46. _PSNAN = (_SNAN, 0)
  47. _CTX_OPTIONS = {
  48. 'prec': _MAX_DIGITS,
  49. 'rounding': decimal.ROUND_HALF_EVEN,
  50. 'Emin': _EXPONENT_MIN,
  51. 'Emax': _EXPONENT_MAX,
  52. 'capitals': 1,
  53. 'flags': [],
  54. 'traps': [decimal.InvalidOperation,
  55. decimal.Overflow,
  56. decimal.Inexact]
  57. }
  58. try:
  59. # Python >= 3.3, cdecimal
  60. decimal.Context(clamp=1) # pylint: disable=unexpected-keyword-arg
  61. _CTX_OPTIONS['clamp'] = 1
  62. except TypeError:
  63. # Python < 3.3
  64. _CTX_OPTIONS['_clamp'] = 1
  65. _DEC128_CTX = decimal.Context(**_CTX_OPTIONS.copy())
  66. def create_decimal128_context():
  67. """Returns an instance of :class:`decimal.Context` appropriate
  68. for working with IEEE-754 128-bit decimal floating point values.
  69. """
  70. opts = _CTX_OPTIONS.copy()
  71. opts['traps'] = []
  72. return decimal.Context(**opts)
  73. def _decimal_to_128(value):
  74. """Converts a decimal.Decimal to BID (high bits, low bits).
  75. :Parameters:
  76. - `value`: An instance of decimal.Decimal
  77. """
  78. with decimal.localcontext(_DEC128_CTX) as ctx:
  79. value = ctx.create_decimal(value)
  80. if value.is_infinite():
  81. return _NINF if value.is_signed() else _PINF
  82. sign, digits, exponent = value.as_tuple()
  83. if value.is_nan():
  84. if digits:
  85. raise ValueError("NaN with debug payload is not supported")
  86. if value.is_snan():
  87. return _NSNAN if value.is_signed() else _PSNAN
  88. return _NNAN if value.is_signed() else _PNAN
  89. significand = int("".join([str(digit) for digit in digits]))
  90. bit_length = significand.bit_length()
  91. high = 0
  92. low = 0
  93. for i in range(min(64, bit_length)):
  94. if significand & (1 << i):
  95. low |= 1 << i
  96. for i in range(64, bit_length):
  97. if significand & (1 << i):
  98. high |= 1 << (i - 64)
  99. biased_exponent = exponent + _EXPONENT_BIAS
  100. if high >> 49 == 1:
  101. high = high & 0x7fffffffffff
  102. high |= _EXPONENT_MASK
  103. high |= (biased_exponent & 0x3fff) << 47
  104. else:
  105. high |= biased_exponent << 49
  106. if sign:
  107. high |= _SIGN
  108. return high, low
  109. class Decimal128(object):
  110. """BSON Decimal128 type::
  111. >>> Decimal128(Decimal("0.0005"))
  112. Decimal128('0.0005')
  113. >>> Decimal128("0.0005")
  114. Decimal128('0.0005')
  115. >>> Decimal128((3474527112516337664, 5))
  116. Decimal128('0.0005')
  117. :Parameters:
  118. - `value`: An instance of :class:`decimal.Decimal`, string, or tuple of
  119. (high bits, low bits) from Binary Integer Decimal (BID) format.
  120. .. note:: :class:`~Decimal128` uses an instance of :class:`decimal.Context`
  121. configured for IEEE-754 Decimal128 when validating parameters.
  122. Signals like :class:`decimal.InvalidOperation`, :class:`decimal.Inexact`,
  123. and :class:`decimal.Overflow` are trapped and raised as exceptions::
  124. >>> Decimal128(".13.1")
  125. Traceback (most recent call last):
  126. File "<stdin>", line 1, in <module>
  127. ...
  128. decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
  129. >>>
  130. >>> Decimal128("1E-6177")
  131. Traceback (most recent call last):
  132. File "<stdin>", line 1, in <module>
  133. ...
  134. decimal.Inexact: [<class 'decimal.Inexact'>]
  135. >>>
  136. >>> Decimal128("1E6145")
  137. Traceback (most recent call last):
  138. File "<stdin>", line 1, in <module>
  139. ...
  140. decimal.Overflow: [<class 'decimal.Overflow'>, <class 'decimal.Rounded'>]
  141. To ensure the result of a calculation can always be stored as BSON
  142. Decimal128 use the context returned by
  143. :func:`create_decimal128_context`::
  144. >>> import decimal
  145. >>> decimal128_ctx = create_decimal128_context()
  146. >>> with decimal.localcontext(decimal128_ctx) as ctx:
  147. ... Decimal128(ctx.create_decimal(".13.3"))
  148. ...
  149. Decimal128('NaN')
  150. >>>
  151. >>> with decimal.localcontext(decimal128_ctx) as ctx:
  152. ... Decimal128(ctx.create_decimal("1E-6177"))
  153. ...
  154. Decimal128('0E-6176')
  155. >>>
  156. >>> with decimal.localcontext(DECIMAL128_CTX) as ctx:
  157. ... Decimal128(ctx.create_decimal("1E6145"))
  158. ...
  159. Decimal128('Infinity')
  160. To match the behavior of MongoDB's Decimal128 implementation
  161. str(Decimal(value)) may not match str(Decimal128(value)) for NaN values::
  162. >>> Decimal128(Decimal('NaN'))
  163. Decimal128('NaN')
  164. >>> Decimal128(Decimal('-NaN'))
  165. Decimal128('NaN')
  166. >>> Decimal128(Decimal('sNaN'))
  167. Decimal128('NaN')
  168. >>> Decimal128(Decimal('-sNaN'))
  169. Decimal128('NaN')
  170. However, :meth:`~Decimal128.to_decimal` will return the exact value::
  171. >>> Decimal128(Decimal('NaN')).to_decimal()
  172. Decimal('NaN')
  173. >>> Decimal128(Decimal('-NaN')).to_decimal()
  174. Decimal('-NaN')
  175. >>> Decimal128(Decimal('sNaN')).to_decimal()
  176. Decimal('sNaN')
  177. >>> Decimal128(Decimal('-sNaN')).to_decimal()
  178. Decimal('-sNaN')
  179. Two instances of :class:`Decimal128` compare equal if their Binary
  180. Integer Decimal encodings are equal::
  181. >>> Decimal128('NaN') == Decimal128('NaN')
  182. True
  183. >>> Decimal128('NaN').bid == Decimal128('NaN').bid
  184. True
  185. This differs from :class:`decimal.Decimal` comparisons for NaN::
  186. >>> Decimal('NaN') == Decimal('NaN')
  187. False
  188. """
  189. __slots__ = ('__high', '__low')
  190. _type_marker = 19
  191. def __init__(self, value):
  192. if isinstance(value, (_string_type, decimal.Decimal)):
  193. self.__high, self.__low = _decimal_to_128(value)
  194. elif isinstance(value, (list, tuple)):
  195. if len(value) != 2:
  196. raise ValueError('Invalid size for creation of Decimal128 '
  197. 'from list or tuple. Must have exactly 2 '
  198. 'elements.')
  199. self.__high, self.__low = value
  200. else:
  201. raise TypeError("Cannot convert %r to Decimal128" % (value,))
  202. def to_decimal(self):
  203. """Returns an instance of :class:`decimal.Decimal` for this
  204. :class:`Decimal128`.
  205. """
  206. high = self.__high
  207. low = self.__low
  208. sign = 1 if (high & _SIGN) else 0
  209. if (high & _SNAN) == _SNAN:
  210. return decimal.Decimal((sign, (), 'N'))
  211. elif (high & _NAN) == _NAN:
  212. return decimal.Decimal((sign, (), 'n'))
  213. elif (high & _INF) == _INF:
  214. return decimal.Decimal((sign, (), 'F'))
  215. if (high & _EXPONENT_MASK) == _EXPONENT_MASK:
  216. exponent = ((high & 0x1fffe00000000000) >> 47) - _EXPONENT_BIAS
  217. return decimal.Decimal((sign, (0,), exponent))
  218. else:
  219. exponent = ((high & 0x7fff800000000000) >> 49) - _EXPONENT_BIAS
  220. arr = bytearray(15)
  221. mask = 0x00000000000000ff
  222. for i in range(14, 6, -1):
  223. arr[i] = (low & mask) >> ((14 - i) << 3)
  224. mask = mask << 8
  225. mask = 0x00000000000000ff
  226. for i in range(6, 0, -1):
  227. arr[i] = (high & mask) >> ((6 - i) << 3)
  228. mask = mask << 8
  229. mask = 0x0001000000000000
  230. arr[0] = (high & mask) >> 48
  231. # cdecimal only accepts a tuple for digits.
  232. digits = tuple(
  233. int(digit) for digit in str(_from_bytes(arr, 'big')))
  234. with decimal.localcontext(_DEC128_CTX) as ctx:
  235. return ctx.create_decimal((sign, digits, exponent))
  236. @classmethod
  237. def from_bid(cls, value):
  238. """Create an instance of :class:`Decimal128` from Binary Integer
  239. Decimal string.
  240. :Parameters:
  241. - `value`: 16 byte string (128-bit IEEE 754-2008 decimal floating
  242. point in Binary Integer Decimal (BID) format).
  243. """
  244. if not isinstance(value, bytes):
  245. raise TypeError("value must be an instance of bytes")
  246. if len(value) != 16:
  247. raise ValueError("value must be exactly 16 bytes")
  248. return cls((_UNPACK_64(value[8:])[0], _UNPACK_64(value[:8])[0]))
  249. @property
  250. def bid(self):
  251. """The Binary Integer Decimal (BID) encoding of this instance."""
  252. return _PACK_64(self.__low) + _PACK_64(self.__high)
  253. def __str__(self):
  254. dec = self.to_decimal()
  255. if dec.is_nan():
  256. # Required by the drivers spec to match MongoDB behavior.
  257. return "NaN"
  258. return str(dec)
  259. def __repr__(self):
  260. return "Decimal128('%s')" % (str(self),)
  261. def __setstate__(self, value):
  262. self.__high, self.__low = value
  263. def __getstate__(self):
  264. return self.__high, self.__low
  265. def __eq__(self, other):
  266. if isinstance(other, Decimal128):
  267. return self.bid == other.bid
  268. return NotImplemented
  269. def __ne__(self, other):
  270. return not self == other