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
1822import 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
72104def _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 ):
0 commit comments