Skip to content

Commit 7933d5a

Browse files
committed
PYTHON-1068 - Update Decimal128 for latest spec changes
1 parent e5984d3 commit 7933d5a

File tree

9 files changed

+3724
-190
lines changed

9 files changed

+3724
-190
lines changed

bson/decimal128.py

Lines changed: 87 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
"""Tools for working with 128-bit IEEE 754-2008 decimal floating point numbers.
15+
"""Tools for working with the BSON decimal128 type.
16+
17+
.. versionadded:: 3.4
18+
19+
.. note:: The Decimal128 BSON type requires MongoDB 3.4+.
1620
"""
1721

1822
import decimal
@@ -52,9 +56,9 @@ def _bit_length(num):
5256

5357
_EXPONENT_MASK = 3 << 61
5458
_EXPONENT_BIAS = 6176
55-
_EXPONENT_MAX = 6111
56-
_EXPONENT_MIN = -6176
57-
_MAX_BIT_LENGTH = 113
59+
_EXPONENT_MAX = 6144
60+
_EXPONENT_MIN = -6143
61+
_MAX_DIGITS = 34
5862

5963
_INF = 0x7800000000000000
6064
_NAN = 0x7c00000000000000
@@ -68,13 +72,44 @@ def _bit_length(num):
6872
_NSNAN = (_SNAN + _SIGN, 0)
6973
_PSNAN = (_SNAN, 0)
7074

75+
_CTX_OPTIONS = {
76+
'prec': _MAX_DIGITS,
77+
'rounding': decimal.ROUND_HALF_EVEN,
78+
'Emin': _EXPONENT_MIN,
79+
'Emax': _EXPONENT_MAX,
80+
'capitals': 1,
81+
'flags': [],
82+
'traps': [decimal.InvalidOperation,
83+
decimal.Overflow,
84+
decimal.Inexact]
85+
}
86+
87+
if _PY3:
88+
_CTX_OPTIONS['clamp'] = 1
89+
else:
90+
_CTX_OPTIONS['_clamp'] = 1
91+
92+
_DEC128_CTX = decimal.Context(**_CTX_OPTIONS.copy())
93+
94+
95+
def create_decimal128_context():
96+
"""Returns an instance of :class:`decimal.Context` appropriate
97+
for working with IEEE-754 128-bit decimal floating point values.
98+
"""
99+
opts = _CTX_OPTIONS.copy()
100+
opts['traps'] = []
101+
return decimal.Context(**opts)
102+
71103

72104
def _decimal_to_128(value):
73105
"""Converts a decimal.Decimal to BID (high bits, low bits).
74106
75107
:Parameters:
76108
- `value`: An instance of decimal.Decimal
77109
"""
110+
with decimal.localcontext(_DEC128_CTX) as ctx:
111+
value = ctx.create_decimal(value)
112+
78113
if value.is_infinite():
79114
return _NINF if value.is_signed() else _PINF
80115

@@ -89,12 +124,6 @@ def _decimal_to_128(value):
89124

90125
significand = int("".join([str(digit) for digit in digits]))
91126
bit_length = _bit_length(significand)
92-
if exponent > _EXPONENT_MAX or exponent < _EXPONENT_MIN:
93-
raise ValueError("Exponent is out of range for "
94-
"Decimal128 encoding %d" % (exponent,))
95-
if bit_length > _MAX_BIT_LENGTH:
96-
raise ValueError("Unscaled value is out of range for "
97-
"Decimal128 encoding %d" % (significand,))
98127

99128
high = 0
100129
low = 0
@@ -135,7 +164,51 @@ class Decimal128(object):
135164
- `value`: An instance of :class:`decimal.Decimal`, string, or tuple of
136165
(high bits, low bits) from Binary Integer Decimal (BID) format.
137166
138-
.. note:: To match the behavior of MongoDB's Decimal128 implementation
167+
.. note:: :class:`~Decimal128` uses an instance of :class:`decimal.Context`
168+
configured for IEEE-754 Decimal128 when validating parameters.
169+
Signals like :class:`decimal.InvalidOperation`, :class:`decimal.Inexact`,
170+
and :class:`decimal.Overflow` are trapped and raised as exceptions::
171+
172+
>>> Decimal128(".13.1")
173+
Traceback (most recent call last):
174+
File "<stdin>", line 1, in <module>
175+
...
176+
decimal.InvalidOperation: [<class 'decimal.ConversionSyntax'>]
177+
>>>
178+
>>> Decimal128("1E-6177")
179+
Traceback (most recent call last):
180+
File "<stdin>", line 1, in <module>
181+
...
182+
decimal.Inexact: [<class 'decimal.Inexact'>]
183+
>>>
184+
>>> Decimal128("1E6145")
185+
Traceback (most recent call last):
186+
File "<stdin>", line 1, in <module>
187+
...
188+
decimal.Overflow: [<class 'decimal.Overflow'>, <class 'decimal.Rounded'>]
189+
190+
To ensure the result of a calculation can always be stored as BSON
191+
Decimal128 use the context returned by
192+
:func:`create_decimal128_context`::
193+
194+
>>> import decimal
195+
>>> decimal128_ctx = create_decimal128_context()
196+
>>> with decimal.localcontext(decimal128_ctx) as ctx:
197+
... Decimal128(ctx.create_decimal(".13.3"))
198+
...
199+
Decimal128('NaN')
200+
>>>
201+
>>> with decimal.localcontext(decimal128_ctx) as ctx:
202+
... Decimal128(ctx.create_decimal("1E-6177"))
203+
...
204+
Decimal128('0E-6176')
205+
>>>
206+
>>> with decimal.localcontext(DECIMAL128_CTX) as ctx:
207+
... Decimal128(ctx.create_decimal("1E6145"))
208+
...
209+
Decimal128('Infinity')
210+
211+
To match the behavior of MongoDB's Decimal128 implementation
139212
str(Decimal(value)) may not match str(Decimal128(value)) for NaN values::
140213
141214
>>> Decimal128(Decimal('NaN'))
@@ -176,16 +249,7 @@ class Decimal128(object):
176249
_type_marker = 19
177250

178251
def __init__(self, value):
179-
if isinstance(value, _string_type):
180-
# Really? decimal.Decimal doesn't care...
181-
if value.startswith(' ') or value.endswith(' '):
182-
raise ValueError("leading or trailing whitespace")
183-
try:
184-
dec = decimal.Decimal(value)
185-
except decimal.InvalidOperation as exc:
186-
raise ValueError(str(exc))
187-
self.__high, self.__low = _decimal_to_128(dec)
188-
elif isinstance(value, decimal.Decimal):
252+
if isinstance(value, (_string_type, decimal.Decimal)):
189253
self.__high, self.__low = _decimal_to_128(value)
190254
elif isinstance(value, (list, tuple)):
191255
if len(value) != 2:
@@ -234,7 +298,8 @@ def to_decimal(self):
234298
# Have to convert bytearray to bytes for python 2.6.
235299
digits = [int(digit) for digit in str(_from_bytes(bytes(arr), 'big'))]
236300

237-
return decimal.Decimal((sign, digits, exponent))
301+
with decimal.localcontext(_DEC128_CTX) as ctx:
302+
return ctx.create_decimal((sign, digits, exponent))
238303

239304
@classmethod
240305
def from_bid(cls, value):

test/decimal/decimal128.json renamed to test/decimal/decimal128-1.json

Lines changed: 10 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -339,152 +339,19 @@
339339
"string": "-Infinity",
340340
"to_extjson": false,
341341
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-inF\"}}"
342-
}
343-
],
344-
"parseErrors": [
345-
{
346-
"description": "Too many significand digits",
347-
"subject": "100000000000000000000000000000000000000000000000000000000001"
348-
},
349-
{
350-
"description": "Too many significand digits",
351-
"subject": "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
352-
},
353-
{
354-
"description": "Too many significand digits",
355-
"subject": ".100000000000000000000000000000000000000000000000000000000000"
356-
},
357-
{
358-
"description": "Incomplete Exponent",
359-
"subject": "1e"
360-
},
361-
{
362-
"description": "Exponent at the beginning",
363-
"subject": "E01"
364-
},
365-
{
366-
"description": "Exponent too large",
367-
"subject": "1E6112"
368-
},
369-
{
370-
"description": "Exponent too small",
371-
"subject": "1E-6177"
372-
},
373-
{
374-
"description": "Just a decimal place",
375-
"subject": "."
376-
},
377-
{
378-
"description": "2 decimal places",
379-
"subject": "..3"
380-
},
381-
{
382-
"description": "2 decimal places",
383-
"subject": ".13.3"
384-
},
385-
{
386-
"description": "2 decimal places",
387-
"subject": "1..3"
388-
},
389-
{
390-
"description": "2 decimal places",
391-
"subject": "1.3.4"
392-
},
393-
{
394-
"description": "2 decimal places",
395-
"subject": "1.34."
396-
},
397-
{
398-
"description": "Decimal with no digits",
399-
"subject": ".e"
400-
},
401-
{
402-
"description": "2 signs",
403-
"subject": "+-32.4"
404-
},
405-
{
406-
"description": "2 signs",
407-
"subject": "-+32.4"
408-
},
409-
{
410-
"description": "2 negative signs",
411-
"subject": "--32.4"
412-
},
413-
{
414-
"description": "2 negative signs",
415-
"subject": "-32.-4"
416-
},
417-
{
418-
"description": "End in negative sign",
419-
"subject": "32.0-"
420-
},
421-
{
422-
"description": "2 negative signs",
423-
"subject": "32.4E--21"
424-
},
425-
{
426-
"description": "2 negative signs",
427-
"subject": "32.4E-2-1"
428-
},
429-
{
430-
"description": "2 signs",
431-
"subject": "32.4E+-21"
432-
},
433-
{
434-
"description": "Empty string",
435-
"subject": ""
436-
},
437-
{
438-
"description": "leading white space positive number",
439-
"subject": " 1"
440-
},
441-
{
442-
"description": "leading white space negative number",
443-
"subject": " -1"
444-
},
445-
{
446-
"description": "trailing white space",
447-
"subject": "1 "
448-
},
449-
{
450-
"description": "Invalid",
451-
"subject": "E"
452-
},
453-
{
454-
"description": "Invalid",
455-
"subject": "invalid"
456-
},
457-
{
458-
"description": "Invalid",
459-
"subject": "i"
460-
},
461-
{
462-
"description": "Invalid",
463-
"subject": "in"
464-
},
465-
{
466-
"description": "Invalid",
467-
"subject": "-in"
468-
},
469-
{
470-
"description": "Invalid",
471-
"subject": "Na"
472-
},
473-
{
474-
"description": "Invalid",
475-
"subject": "-Na"
476-
},
477-
{
478-
"description": "Invalid",
479-
"subject": "1.23abc"
480-
},
342+
},
481343
{
482-
"description": "Invalid",
483-
"subject": "1.23abcE+02"
344+
"description": "Clamped",
345+
"subject": "180000001364000a00000000000000000000000000fe5f00",
346+
"string": "1E6112",
347+
"match_string": "1.0E+6112"
484348
},
485349
{
486-
"description": "Invalid",
487-
"subject": "1.23E+0aabs2"
350+
"description": "Exact rounding",
351+
"subject": "18000000136400000000000a5bc138938d44c64d31cc3700",
352+
"string": "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
353+
"match_string": "1.000000000000000000000000000000000E+999"
488354
}
355+
489356
]
490357
}

0 commit comments

Comments
 (0)