diff --git a/bson/__init__.py b/bson/__init__.py index 1da8ff0fb..088ed0e99 100644 --- a/bson/__init__.py +++ b/bson/__init__.py @@ -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, } diff --git a/bson/_cbsonmodule.c b/bson/_cbsonmodule.c index a52214cdf..850633288 100644 --- a/bson/_cbsonmodule.c +++ b/bson/_cbsonmodule.c @@ -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"); diff --git a/bson/decimal128.py b/bson/decimal128.py new file mode 100644 index 000000000..0a2cf6ffa --- /dev/null +++ b/bson/decimal128.py @@ -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(" _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 diff --git a/bson/json_util.py b/bson/json_util.py index 6ebb6e123..a1f3179ed 100644 --- a/bson/json_util.py +++ b/bson/json_util.py @@ -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) diff --git a/doc/api/bson/decimal128.rst b/doc/api/bson/decimal128.rst new file mode 100644 index 000000000..60ac95304 --- /dev/null +++ b/doc/api/bson/decimal128.rst @@ -0,0 +1,4 @@ +:mod:`decimal128` -- Support for BSON Decimal128 +================================================ +.. automodule:: bson.decimal128 + :members: diff --git a/doc/api/bson/index.rst b/doc/api/bson/index.rst index fd4ce731e..5f15ed99e 100644 --- a/doc/api/bson/index.rst +++ b/doc/api/bson/index.rst @@ -14,6 +14,7 @@ Sub-modules: code codec_options dbref + decimal128 errors int64 json_util diff --git a/test/decimal/decimal128.json b/test/decimal/decimal128.json new file mode 100644 index 000000000..e710aa650 --- /dev/null +++ b/test/decimal/decimal128.json @@ -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" + } + ] +} diff --git a/test/test_bson.py b/test/test_bson.py index be7ef305a..94272ba4e 100644 --- a/test/test_bson.py +++ b/test/test_bson.py @@ -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() diff --git a/test/test_decimal128.py b/test/test_decimal128.py new file mode 100644 index 000000000..d9d77daf3 --- /dev/null +++ b/test/test_decimal128.py @@ -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']) +