Skip to content

Commit

Permalink
Problem: agronholm#116 Decimals are losing precision on decode
Browse files Browse the repository at this point in the history
Solution: Instead of construct the Decimal arithmetically, build an
intermediate Decimal containing the sign and digits, then build the
final result with a tuple of `(sign, digits, exponent)`. Since there is no
calculation going on, the default precision in the Decimal context does
not take effect.

Unfortunately this does not work with bigfloat because the exponent in
that case is a power of 2 instead of a power of 10.
  • Loading branch information
Sekenre committed Jul 23, 2021
1 parent 7c47adb commit 66287fd
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 15 deletions.
3 changes: 2 additions & 1 deletion cbor2/decoder.py
Expand Up @@ -427,7 +427,8 @@ def decode_fraction(self):
# Semantic tag 4
from decimal import Decimal
exp, sig = self._decode()
return self.set_shareable(Decimal(sig) * (10 ** Decimal(exp)))
tmp = Decimal(sig).as_tuple()
return self.set_shareable(Decimal((tmp.sign, tmp.digits, exp)))

def decode_bigfloat(self):
# Semantic tag 5
Expand Down
33 changes: 19 additions & 14 deletions source/decoder.c
Expand Up @@ -1189,28 +1189,33 @@ static PyObject *
CBORDecoder_decode_fraction(CBORDecoderObject *self)
{
// semantic type 4
PyObject *tuple, *tmp, *sig, *exp, *ten, *ret = NULL;
PyObject *payload_t, *tmp, *sig, *exp, *ret = NULL;
PyObject *decimal_t, *sign, *digits, *args = NULL;

if (!_CBOR2_Decimal && _CBOR2_init_Decimal() == -1)
return NULL;
// NOTE: There's no particular necessity for this to be immutable, it's
// just a performance choice
tuple = decode(self, DECODE_IMMUTABLE | DECODE_UNSHARED);
if (tuple) {
if (PyTuple_CheckExact(tuple) && PyTuple_GET_SIZE(tuple) == 2) {
exp = PyTuple_GET_ITEM(tuple, 0);
sig = PyTuple_GET_ITEM(tuple, 1);
ten = PyObject_CallFunction(_CBOR2_Decimal, "i", 10);
if (ten) {
tmp = PyNumber_Power(ten, exp, Py_None);
if (tmp) {
ret = PyNumber_Multiply(sig, tmp);
Py_DECREF(tmp);
payload_t = decode(self, DECODE_IMMUTABLE | DECODE_UNSHARED);
if (payload_t) {
if (PyTuple_CheckExact(payload_t) && PyTuple_GET_SIZE(payload_t) == 2) {
exp = PyTuple_GET_ITEM(payload_t, 0);
sig = PyTuple_GET_ITEM(payload_t, 1);
tmp = PyObject_CallFunction(_CBOR2_Decimal, "O", sig);
if (tmp) {
decimal_t = PyObject_CallMethod(tmp, "as_tuple", NULL);
if (decimal_t) {
sign = PyTuple_GET_ITEM(decimal_t, 0);
digits = PyTuple_GET_ITEM(decimal_t, 1);
args = PyTuple_Pack(3, sign, digits, exp);
ret = PyObject_CallFunction(_CBOR2_Decimal, "(O)", args);
Py_DECREF(decimal_t);
Py_DECREF(args);
}
Py_DECREF(ten);
Py_DECREF(tmp);
}
}
Py_DECREF(tuple);
Py_DECREF(payload_t);
}
set_shareable(self, ret);
return ret;
Expand Down
5 changes: 5 additions & 0 deletions tests/test_decoder.py
Expand Up @@ -384,6 +384,11 @@ def test_fraction(impl):
assert decoded == Decimal('273.15')


def test_decimal_precision(impl):
decoded = impl.loads(unhexlify('c482384dc252011f1fe37d0c70ff50456ba8b891997b07d6'))
assert decoded == Decimal('9.7703426561852468194804075821069770622934E-38')


def test_bigfloat(impl):
decoded = impl.loads(unhexlify('c5822003'))
assert decoded == Decimal('1.5')
Expand Down

0 comments on commit 66287fd

Please sign in to comment.