PYTHON-1068 - Support for BSON Decimal128

This commit is contained in:
Bernie Hackett 2016-04-07 18:03:30 -07:00 committed by Bernie Hackett
parent e43952d2a5
commit 82db71e1e8
9 changed files with 945 additions and 4 deletions

View File

@ -34,6 +34,7 @@ from bson.code import Code
from bson.codec_options import (
CodecOptions, DEFAULT_CODEC_OPTIONS, _raw_document_class)
from bson.dbref import DBRef
from bson.decimal128 import Decimal128
from bson.errors import (InvalidBSON,
InvalidDocument,
InvalidStringData)
@ -82,6 +83,7 @@ BSONCWS = b"\x0F" # Javascript code with scope
BSONINT = b"\x10" # 32bit int
BSONTIM = b"\x11" # Timestamp
BSONLON = b"\x12" # 64bit int
BSONDEC = b"\x13" # Decimal128
BSONMIN = b"\xFF" # Min key
BSONMAX = b"\x7F" # Max key
@ -289,6 +291,12 @@ def _get_int64(data, position, dummy0, dummy1, dummy2):
return Int64(_UNPACK_LONG(data[position:end])[0]), end
def _get_decimal128(data, position, dummy0, dummy1, dummy2):
"""Decode a BSON decimal128 to bson.decimal128.Decimal128."""
end = position + 16
return Decimal128.from_bid(data[position:end]), end
# Each decoder function's signature is:
# - data: bytes
# - position: int, beginning of object in 'data' to decode
@ -313,6 +321,7 @@ _ELEMENT_GETTER = {
BSONINT: _get_int,
BSONTIM: _get_timestamp,
BSONLON: _get_int64,
BSONDEC: _get_decimal128,
BSONMIN: lambda v, w, x, y, z: (MinKey(), w),
BSONMAX: lambda v, w, x, y, z: (MaxKey(), w)}
@ -619,6 +628,11 @@ def _encode_long(name, value, dummy0, dummy1):
raise OverflowError("BSON can only handle up to 8-byte ints")
def _encode_decimal128(name, value, dummy0, dummy1):
"""Encode bson.decimal128.Decimal128."""
return b"\x13" + name + value.bid
def _encode_minkey(name, dummy0, dummy1, dummy2):
"""Encode bson.min_key.MinKey."""
return b"\xFF" + name
@ -659,6 +673,7 @@ _ENCODERS = {
SON: _encode_mapping,
Timestamp: _encode_timestamp,
UUIDLegacy: _encode_binary,
Decimal128: _encode_decimal128,
# Special case. This will never be looked up directly.
collections.Mapping: _encode_mapping,
}

View File

@ -50,6 +50,7 @@ struct module_state {
PyObject* UTC;
PyTypeObject* REType;
PyObject* BSONInt64;
PyObject* Decimal128;
PyObject* Mapping;
PyObject* CodecOptions;
};
@ -359,6 +360,7 @@ static int _load_python_objects(PyObject* module) {
_load_object(&state->UTC, "bson.tz_util", "utc") ||
_load_object(&state->Regex, "bson.regex", "Regex") ||
_load_object(&state->BSONInt64, "bson.int64", "Int64") ||
_load_object(&state->Decimal128, "bson.decimal128", "Decimal128") ||
_load_object(&state->UUID, "uuid", "UUID") ||
_load_object(&state->Mapping, "collections", "Mapping") ||
_load_object(&state->CodecOptions, "bson.codec_options", "CodecOptions")) {
@ -894,6 +896,31 @@ static int _write_element_to_buffer(PyObject* self, buffer_t buffer,
*(buffer_get_buffer(buffer) + type_byte) = 0x12;
return 1;
}
case 19:
{
/* Decimal128 */
const char* data;
PyObject* pystring = PyObject_GetAttrString(value, "bid");
if (!pystring) {
return 0;
}
#if PY_MAJOR_VERSION >= 3
data = PyBytes_AsString(pystring);
#else
data = PyString_AsString(pystring);
#endif
if (!data) {
Py_DECREF(pystring);
return 0;
}
if (!buffer_write_bytes(buffer, data, 16)) {
Py_DECREF(pystring);
return 0;
}
Py_DECREF(pystring);
*(buffer_get_buffer(buffer) + type_byte) = 0x13;
return 1;
}
case 100:
{
/* DBRef */
@ -2387,6 +2414,29 @@ static PyObject* get_value(PyObject* self, PyObject* name, const char* buffer,
Py_DECREF(bson_int64_type);
break;
}
case 19:
{
PyObject* dec128;
if (max < 16) {
goto invalid;
}
if ((dec128 = _get_object(state->Decimal128,
"bson.decimal128",
"Decimal128"))) {
value = PyObject_CallMethod(dec128,
"from_bid",
#if PY_MAJOR_VERSION >= 3
"y#",
#else
"s#",
#endif
buffer + *position,
16);
Py_DECREF(dec128);
}
*position += 16;
break;
}
case 255:
{
PyObject* minkey_type = _get_object(state->MinKey, "bson.min_key", "MinKey");

281
bson/decimal128.py Normal file
View File

@ -0,0 +1,281 @@
# Copyright 2016 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 128-bit IEEE 754-2008 decimal floating point numbers.
"""
import decimal
import struct
import sys
from bson.py3compat import (PY3 as _PY3,
string_type as _string_type)
if _PY3:
_from_bytes = int.from_bytes # pylint: disable=no-member, invalid-name
else:
import binascii
def _from_bytes(value, dummy, _int=int, _hexlify=binascii.hexlify):
"An implementation of int.from_bytes for python 2.x."
return _int(_hexlify(value), 16)
if sys.version_info[:2] == (2, 6):
def _bit_length(num):
"""bit_length for python 2.6"""
if num:
# bin() was new in 2.6. Note that this won't work
# for values less than 0, which we never have here.
return len(bin(num)) - 2
# bit_length(0) is 0, but len(bin(0)) - 2 is 1
return 0
else:
def _bit_length(num):
"""bit_length for python >= 2.7"""
# num could be int or long in python 2.7
return num.bit_length()
_PACK_64 = struct.Struct("<Q").pack
_UNPACK_64 = struct.Struct("<Q").unpack
_EXPONENT_MASK = 3 << 61
_EXPONENT_BIAS = 6176
_EXPONENT_MAX = 6111
_EXPONENT_MIN = -6176
_MAX_BIT_LENGTH = 113
_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)
def _decimal_to_128(value):
"""Converts a decimal.Decimal to BID (high bits, low bits).
:Parameters:
- `value`: An instance of decimal.Decimal
"""
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 = _bit_length(significand)
if exponent > _EXPONENT_MAX or exponent < _EXPONENT_MIN:
raise ValueError("Exponent is out of range for "
"Decimal128 encoding %d" % (exponent,))
if bit_length > _MAX_BIT_LENGTH:
raise ValueError("Unscaled value is out of range for "
"Decimal128 encoding %d" % (significand,))
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
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(object):
"""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:: 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):
if isinstance(value, _string_type):
# Really? decimal.Decimal doesn't care...
if value.startswith(' ') or value.endswith(' '):
raise ValueError("leading or trailing whitespace")
try:
dec = decimal.Decimal(value)
except decimal.InvalidOperation as exc:
raise ValueError(str(exc))
self.__high, self.__low = _decimal_to_128(dec)
elif isinstance(value, 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
else:
raise TypeError("Cannot convert %r to Decimal128" % (value,))
def to_decimal(self):
"""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'))
elif (high & _NAN) == _NAN:
return decimal.Decimal((sign, (), 'n'))
elif (high & _INF) == _INF:
return decimal.Decimal((sign, (0,), 'F'))
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
# Have to convert bytearray to bytes for python 2.6.
digits = [int(digit) for digit in str(_from_bytes(bytes(arr), 'big'))]
return decimal.Decimal((sign, digits, exponent))
@classmethod
def from_bid(cls, value):
"""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]))
@property
def bid(self):
"""The Binary Integer Decimal (BID) encoding of this instance."""
return _PACK_64(self.__low) + _PACK_64(self.__high)
def __str__(self):
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 "Decimal128('%s')" % (str(self),)
def __setstate__(self, value):
self.__high, self.__low = value
def __getstate__(self):
return self.__high, self.__low
def __eq__(self, other):
if isinstance(other, Decimal128):
return self.bid == other.bid
return NotImplemented
def __ne__(self, other):
return not self == other

View File

@ -96,6 +96,7 @@ from bson.code import Code
from bson.codec_options import CodecOptions
from bson.errors import InvalidDatetime
from bson.dbref import DBRef
from bson.decimal128 import Decimal128
from bson.int64 import Int64
from bson.max_key import MaxKey
from bson.min_key import MinKey
@ -356,6 +357,8 @@ def object_hook(dct, json_options=DEFAULT_JSON_OPTIONS):
if "$timestamp" in dct:
tsp = dct["$timestamp"]
return Timestamp(tsp["t"], tsp["i"])
if "$numberDecimal" in dct:
return Decimal128(dct["$numberDecimal"])
return dct
@ -440,4 +443,6 @@ def default(obj, json_options=DEFAULT_JSON_OPTIONS):
('$type', "%02x" % subtype)])
else:
return {"$uuid": obj.hex}
if isinstance(obj, Decimal128):
return {"$numberDecimal": str(obj)}
raise TypeError("%r is not JSON serializable" % obj)

View File

@ -0,0 +1,4 @@
:mod:`decimal128` -- Support for BSON Decimal128
================================================
.. automodule:: bson.decimal128
:members:

View File

@ -14,6 +14,7 @@ Sub-modules:
code
codec_options
dbref
decimal128
errors
int64
json_util

View File

@ -0,0 +1,490 @@
{
"description": "Decimal128",
"bson_type": "0x13",
"test_key": "d",
"valid": [
{
"description": "Special - Canonical NaN",
"subject": "180000001364000000000000000000000000000000007C00",
"string": "NaN",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"NaN\"}}"
},
{
"description": "Special - Negative NaN",
"subject": "18000000136400000000000000000000000000000000FC00",
"string": "NaN",
"from_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"NaN\"}}"
},
{
"description": "Special - Canonical SNaN",
"subject": "180000001364000000000000000000000000000000007E00",
"string": "NaN",
"from_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"NaN\"}}"
},
{
"description": "Special - Negative SNaN",
"subject": "18000000136400000000000000000000000000000000FE00",
"string": "NaN",
"from_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"NaN\"}}"
},
{
"description": "Special - NaN with a payload",
"subject": "180000001364001200000000000000000000000000007E00",
"string": "NaN",
"from_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"NaN\"}}"
},
{
"description": "Special - Canonical Positive Infinity",
"subject": "180000001364000000000000000000000000000000007800",
"string": "Infinity",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"Infinity\"}}"
},
{
"description": "Special - Canonical Negative Infinity",
"subject": "18000000136400000000000000000000000000000000F800",
"string": "-Infinity",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-Infinity\"}}"
},
{
"description": "Special - Invalid representation treated as 0",
"subject": "180000001364000000000000000000000000000000106C00",
"string": "0",
"from_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"0\"}}"
},
{
"description": "Special - Invalid representation treated as -0",
"subject": "18000000136400DCBA9876543210DEADBEEF00000010EC00",
"string": "-0",
"from_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-0\"}}"
},
{
"description": "Special - Invalid representation treated as 0E3",
"subject": "18000000136400FFFFFFFFFFFFFFFFFFFFFFFFFFFF116C00",
"string": "0E+3",
"from_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"0E+3\"}}"
},
{
"description": "Regular - Adjusted Exponent Limit",
"subject": "18000000136400F2AF967ED05C82DE3297FF6FDE3CF22F00",
"string": "0.000001234567890123456789012345678901234",
"extjson": "{\"d\": { \"$numberDecimal\": \"0.000001234567890123456789012345678901234\" }}"
},
{
"description": "Regular - Smallest",
"subject": "18000000136400D204000000000000000000000000343000",
"string": "0.001234",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"0.001234\"}}"
},
{
"description": "Regular - Smallest with Trailing Zeros",
"subject": "1800000013640040EF5A07000000000000000000002A3000",
"string": "0.00123400000",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"0.00123400000\"}}"
},
{
"description": "Regular - 0.1",
"subject": "1800000013640001000000000000000000000000003E3000",
"string": "0.1",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"0.1\"}}"
},
{
"description": "Regular - 0.1234567890123456789012345678901234",
"subject": "18000000136400F2AF967ED05C82DE3297FF6FDE3CFC2F00",
"string": "0.1234567890123456789012345678901234",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"0.1234567890123456789012345678901234\"}}"
},
{
"description": "Regular - 0",
"subject": "180000001364000000000000000000000000000000403000",
"string": "0",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"0\"}}"
},
{
"description": "Regular - -0",
"subject": "18000000136400000000000000000000000000000040B000",
"string": "-0",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-0\"}}"
},
{
"description": "Regular - -0.0",
"subject": "1800000013640000000000000000000000000000003EB000",
"string": "-0.0",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-0.0\"}}"
},
{
"description": "Regular - 2",
"subject": "180000001364000200000000000000000000000000403000",
"string": "2",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"2\"}}"
},
{
"description": "Regular - 2.000",
"subject": "18000000136400D0070000000000000000000000003A3000",
"string": "2.000",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"2.000\"}}"
},
{
"description": "Regular - Largest",
"subject": "18000000136400F2AF967ED05C82DE3297FF6FDE3C403000",
"string": "1234567890123456789012345678901234",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"1234567890123456789012345678901234\"}}"
},
{
"description": "Scientific - Tiniest",
"subject": "18000000136400FFFFFFFF638E8D37C087ADBE09ED010000",
"string": "9.999999999999999999999999999999999E-6143",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"9.999999999999999999999999999999999E-6143\"}}"
},
{
"description": "Scientific - Tiny",
"subject": "180000001364000100000000000000000000000000000000",
"string": "1E-6176",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"1E-6176\"}}"
},
{
"description": "Scientific - Negative Tiny",
"subject": "180000001364000100000000000000000000000000008000",
"string": "-1E-6176",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-1E-6176\"}}"
},
{
"description": "Scientific - Adjusted Exponent Limit",
"subject": "18000000136400F2AF967ED05C82DE3297FF6FDE3CF02F00",
"string": "1.234567890123456789012345678901234E-7",
"extjson": "{\"d\": { \"$numberDecimal\": \"1.234567890123456789012345678901234E-7\" }}"
},
{
"description": "Scientific - Fractional",
"subject": "1800000013640064000000000000000000000000002CB000",
"string": "-1.00E-8",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-1.00E-8\"}}"
},
{
"description": "Scientific - 0 with Exponent",
"subject": "180000001364000000000000000000000000000000205F00",
"string": "0E+6000",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"0E+6000\"}}"
},
{
"description": "Scientific - 0 with Negative Exponent",
"subject": "1800000013640000000000000000000000000000007A2B00",
"string": "0E-611",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"0E-611\"}}"
},
{
"description": "Scientific - No Decimal with Signed Exponent",
"subject": "180000001364000100000000000000000000000000463000",
"string": "1E+3",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"1E+3\"}}"
},
{
"description": "Scientific - Trailing Zero",
"subject": "180000001364001A04000000000000000000000000423000",
"string": "1.050E+4",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"1.050E+4\"}}"
},
{
"description": "Scientific - With Decimal",
"subject": "180000001364006900000000000000000000000000423000",
"string": "1.05E+3",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"1.05E+3\"}}"
},
{
"description": "Scientific - Full",
"subject": "18000000136400FFFFFFFFFFFFFFFFFFFFFFFFFFFF403000",
"string": "5192296858534827628530496329220095",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"5192296858534827628530496329220095\"}}"
},
{
"description": "Scientific - Large",
"subject": "18000000136400000000000A5BC138938D44C64D31FE5F00",
"string": "1.000000000000000000000000000000000E+6144",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"1.000000000000000000000000000000000E+6144\"}}"
},
{
"description": "Scientific - Largest",
"subject": "18000000136400FFFFFFFF638E8D37C087ADBE09EDFF5F00",
"string": "9.999999999999999999999999999999999E+6144",
"extjson": "{\"d\" : {\"$numberDecimal\" : \"9.999999999999999999999999999999999E+6144\"}}"
},
{
"description": "Non-Canonical Parsing - Exponent Normalization",
"subject": "1800000013640064000000000000000000000000002CB000",
"string": "-1.00E-8",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-100E-10\"}}"
},
{
"description": "Non-Canonical Parsing - Unsigned Positive Exponent",
"subject": "180000001364000100000000000000000000000000463000",
"string": "1E+3",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"1E3\"}}"
},
{
"description": "Non-Canonical Parsing - Lowercase Exponent Identifier",
"subject": "180000001364000100000000000000000000000000463000",
"string": "1E+3",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"1e+3\"}}"
},
{
"description": "Non-Canonical Parsing - Long Significand with Exponent",
"subject": "1800000013640079D9E0F9763ADA429D0200000000583000",
"string": "1.2345689012345789012345E+34",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"12345689012345789012345E+12\"}}"
},
{
"description": "Non-Canonical Parsing - Positive Sign",
"subject": "18000000136400F2AF967ED05C82DE3297FF6FDE3C403000",
"string": "1234567890123456789012345678901234",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"+1234567890123456789012345678901234\"}}"
},
{
"description": "Non-Canonical Parsing - Long Decimal String",
"subject": "180000001364000100000000000000000000000000722800",
"string": "1E-999",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \".000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001\"}}"
},
{
"description": "Non-Canonical Parsing - nan",
"subject": "180000001364000000000000000000000000000000007C00",
"string": "NaN",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"nan\"}}"
},
{
"description": "Non-Canonical Parsing - nAn",
"subject": "180000001364000000000000000000000000000000007C00",
"string": "NaN",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"nAn\"}}"
},
{
"description": "Non-Canonical Parsing - +infinity",
"subject": "180000001364000000000000000000000000000000007800",
"string": "Infinity",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"+infinity\"}}"
},
{
"description": "Non-Canonical Parsing - infinity",
"subject": "180000001364000000000000000000000000000000007800",
"string": "Infinity",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"infinity\"}}"
},
{
"description": "Non-Canonical Parsing - infiniTY",
"subject": "180000001364000000000000000000000000000000007800",
"string": "Infinity",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"infiniTY\"}}"
},
{
"description": "Non-Canonical Parsing - infinity",
"subject": "180000001364000000000000000000000000000000007800",
"string": "Infinity",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"infinity\"}}"
},
{
"description": "Non-Canonical Parsing - inFinity",
"subject": "180000001364000000000000000000000000000000007800",
"string": "Infinity",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"inFinity\"}}"
},
{
"description": "Non-Canonical Parsing - -infinity",
"subject": "18000000136400000000000000000000000000000000F800",
"string": "-Infinity",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-infinity\"}}"
},
{
"description": "Non-Canonical Parsing - -infiniTy",
"subject": "18000000136400000000000000000000000000000000F800",
"string": "-Infinity",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-infiniTy\"}}"
},
{
"description": "Non-Canonical Parsing - -Infinity",
"subject": "18000000136400000000000000000000000000000000F800",
"string": "-Infinity",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-Infinity\"}}"
},
{
"description": "Non-Canonical Parsing - -infinity",
"subject": "18000000136400000000000000000000000000000000F800",
"string": "-Infinity",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-inf\"}}"
},
{
"description": "Non-Canonical Parsing - -inFinity",
"subject": "18000000136400000000000000000000000000000000F800",
"string": "-Infinity",
"to_extjson": false,
"extjson": "{\"d\" : {\"$numberDecimal\" : \"-inFinity\"}}"
}
],
"parseErrors": [
{
"description": "Too many significand digits",
"subject": "100000000000000000000000000000000000000000000000000000000001"
},
{
"description": "Too many significand digits",
"subject": "1000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
},
{
"description": "Too many significand digits",
"subject": ".100000000000000000000000000000000000000000000000000000000000"
},
{
"description": "Incomplete Exponent",
"subject": "1e"
},
{
"description": "Exponent at the beginning",
"subject": "E01"
},
{
"description": "Exponent too large",
"subject": "1E6112"
},
{
"description": "Exponent too small",
"subject": "1E-6177"
},
{
"description": "Just a decimal place",
"subject": "."
},
{
"description": "2 decimal places",
"subject": "..3"
},
{
"description": "2 decimal places",
"subject": ".13.3"
},
{
"description": "2 decimal places",
"subject": "1..3"
},
{
"description": "2 decimal places",
"subject": "1.3.4"
},
{
"description": "2 decimal places",
"subject": "1.34."
},
{
"description": "Decimal with no digits",
"subject": ".e"
},
{
"description": "2 signs",
"subject": "+-32.4"
},
{
"description": "2 signs",
"subject": "-+32.4"
},
{
"description": "2 negative signs",
"subject": "--32.4"
},
{
"description": "2 negative signs",
"subject": "-32.-4"
},
{
"description": "End in negative sign",
"subject": "32.0-"
},
{
"description": "2 negative signs",
"subject": "32.4E--21"
},
{
"description": "2 negative signs",
"subject": "32.4E-2-1"
},
{
"description": "2 signs",
"subject": "32.4E+-21"
},
{
"description": "Empty string",
"subject": ""
},
{
"description": "leading white space positive number",
"subject": " 1"
},
{
"description": "leading white space negative number",
"subject": " -1"
},
{
"description": "trailing white space",
"subject": "1 "
},
{
"description": "Invalid",
"subject": "E"
},
{
"description": "Invalid",
"subject": "invalid"
},
{
"description": "Invalid",
"subject": "i"
},
{
"description": "Invalid",
"subject": "in"
},
{
"description": "Invalid",
"subject": "-in"
},
{
"description": "Invalid",
"subject": "Na"
},
{
"description": "Invalid",
"subject": "-Na"
},
{
"description": "Invalid",
"subject": "1.23abc"
},
{
"description": "Invalid",
"subject": "1.23abcE+02"
},
{
"description": "Invalid",
"subject": "1.23E+0aabs2"
}
]
}

View File

@ -408,13 +408,13 @@ class TestBSON(unittest.TestCase):
def test_unknown_type(self):
# Repr value differs with major python version
part = "type %r for fieldname 'foo'" % (b'\x13',)
part = "type %r for fieldname 'foo'" % (b'\x14',)
docs = [
b'\x0e\x00\x00\x00\x13foo\x00\x01\x00\x00\x00\x00',
(b'\x16\x00\x00\x00\x04foo\x00\x0c\x00\x00\x00\x130'
b'\x0e\x00\x00\x00\x14foo\x00\x01\x00\x00\x00\x00',
(b'\x16\x00\x00\x00\x04foo\x00\x0c\x00\x00\x00\x140'
b'\x00\x01\x00\x00\x00\x00\x00'),
(b' \x00\x00\x00\x04bar\x00\x16\x00\x00\x00\x030\x00\x0e\x00\x00'
b'\x00\x13foo\x00\x01\x00\x00\x00\x00\x00\x00')]
b'\x00\x14foo\x00\x01\x00\x00\x00\x00\x00\x00')]
for bs in docs:
try:
bson.BSON(bs).decode()

95
test/test_decimal128.py Normal file
View File

@ -0,0 +1,95 @@
# Copyright 2016 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.
"""Tests for Decimal128."""
import codecs
import json
import os.path
import pickle
import sys
from binascii import unhexlify
from decimal import Decimal
sys.path[0:0] = [""]
from bson import BSON
from bson.decimal128 import Decimal128
from bson.json_util import dumps, loads
from bson.py3compat import b
from test import client_context, unittest
class TestDecimal128(unittest.TestCase):
def test_round_trip(self):
coll = client_context.client.pymongo_test.test
coll.drop()
dec128 = Decimal128.from_bid(
b'\x00@cR\xbf\xc6\x01\x00\x00\x00\x00\x00\x00\x00\x1c0')
coll.insert_one({'dec128': dec128})
doc = coll.find_one({'dec128': dec128})
self.assertIsNotNone(doc)
self.assertEqual(doc['dec128'], dec128)
def test_pickle(self):
dec128 = Decimal128.from_bid(
b'\x00@cR\xbf\xc6\x01\x00\x00\x00\x00\x00\x00\x00\x1c0')
for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
pkl = pickle.dumps(dec128, protocol=protocol)
self.assertEqual(dec128, pickle.loads(pkl))
def test_special(self):
dnan = Decimal('NaN')
dnnan = Decimal('-NaN')
dsnan = Decimal('sNaN')
dnsnan = Decimal('-sNaN')
dnan128 = Decimal128(dnan)
dnnan128 = Decimal128(dnnan)
dsnan128 = Decimal128(dsnan)
dnsnan128 = Decimal128(dnsnan)
# Due to the comparison rules for decimal.Decimal we have to
# compare strings.
self.assertEqual(str(dnan), str(dnan128.to_decimal()))
self.assertEqual(str(dnnan), str(dnnan128.to_decimal()))
self.assertEqual(str(dsnan), str(dsnan128.to_decimal()))
self.assertEqual(str(dnsnan), str(dnsnan128.to_decimal()))
def test_spec(self):
path = os.path.join(
os.path.dirname(os.path.realpath(__file__)),
'decimal',
'decimal128.json')
with codecs.open(path, 'r', 'utf-8-sig') as fp:
suite = json.load(fp)
for test in suite['valid']:
subject = unhexlify(b(test['subject']))
doc = BSON(subject).decode()
self.assertEqual(BSON.encode(doc), subject)
self.assertEqual(str(doc['d']), test['string'])
if test.get('from_extjson', True):
self.assertEqual(doc, loads(test['extjson']))
if test.get('to_extjson', True):
extjson = test['extjson'].replace(' ', '')
self.assertEqual(extjson, dumps(doc).replace(' ', ''))
for test in suite['parseErrors']:
self.assertRaises(ValueError, Decimal128, test['subject'])