Precision-preserving number decode
diff --git a/inc/qcbor/qcbor_spiffy_decode.h b/inc/qcbor/qcbor_spiffy_decode.h
index 08e1eaa..d29ebb2 100644
--- a/inc/qcbor/qcbor_spiffy_decode.h
+++ b/inc/qcbor/qcbor_spiffy_decode.h
@@ -300,77 +300,79 @@
int64_t *pnValue);
/**
- * @brief Decode next item into a signed 64-bit integer with basic conversions.
+ * @brief Decode next as a number, with precision-preserving conversions.
*
* @param[in] pCtx The decode context.
- * @param[in] uConvertTypes The integer conversion options.
* @param[out] pNumber The returned 64-bit signed integer.
*
* This will get the next item as a number and return it as a C
* data type such that no precision is lost.
*
- * Basic version takes type 0, type 1 and type 7 inputs and outputs
- * int64_t, uint64_t, double plus 65-bit negative. This gives 64-bit
- * precision for integers and 52-bit for fractions. This will try to
- * fit int64_t, then uint64_t then double, then 65-bit negative. Note that
- * this will convert doubles to ints if they are whole numbers.
+ * The CBOR input can be integers (major type 0 or 1) or floats (major type 7).
+ * If not these \ref QCBOR_ERR_UNEXPECTED_TYPE will be set.
*
- * Advanced adds decoding of big nums and big floats and
- * can output bignums and big floats. This gives arbitrary
- * precision and range with exponents in an int64_t. This will try to
- * fit into int64_t, then uint64_t, then double, then big num,
- * then big float.
+ * The conversion is as follows.
*
- * The precedence is:
- * - int64_t Whole numbers between INT64_MIN and INT64_MAX
- * - uint64_t Whole numbers between INT64_MAX and UINT64_MAX
- * - double Fractions; whole numbers from UINT64_MAX to
+ * Whole numbers between INT64_MIN and INT64_MAX will
+ * be returned as \ref QCBOR_TYPE_INT64. This includes
+ * conversion of float values that are whole number.
*
- * - big num
- * - big float
+ * Whole numbers between INT64_MAX and UINT64_MAX will
+ * be returned as \ref QCBOR_TYPE_UINT64, again including
+ * conversion of whole floating-point numbers.
*
- * The encoded number may be type 0 or type 1, half, single or double float,
- * big number or big float (probably should do decimal fraction too).
+ * Whole numbers between -2^63 - 1 to -2^64 that have more than
+ * 52-bit precision will be returned as \ref QCBOR_TYPE_65BIT_NEG_INT.
+ * That is to avoid returning QCBOR_TYPE_65BIT_NEG_INT, which is
+ * awkward to work with in C these far-from-zero negative whole
+ * numbers will be converted to double floating point. QCBOR_TYPE_65BIT_NEG_INT
+ * is only returned when the value can't convert to double because it
+ * has more than 53-bits of precision.
+ *
+ * All others are returned as a double.
*
* This is useful for dCBOR where floats are converted to ints when they are whole numbers.
*
- * @c uConvertTypes controls what conversions this will perform and
- * thus what CBOR types will be decoded. @c uConvertType is a bit map
- * listing the conversions to be allowed. This function supports
- * @ref QCBOR_CONVERT_TYPE_XINT64 and @ref QCBOR_CONVERT_TYPE_FLOAT
- * conversions.
- *
* Please see @ref Decode-Errors-Overview "Decode Errors Overview".
*
- * If the CBOR data type can never be convered by this function or the
- * conversion was not selected in @c uConversionTypes
- * @ref QCBOR_ERR_UNEXPECTED_TYPE is set.
- *
- * When converting floating-point values, the integer is rounded to
- * the nearest integer using llround(). By default, floating-point
- * suport is enabled for QCBOR.
- *
+ * TODO: is this right?
* If floating-point HW use is disabled this will set
* @ref QCBOR_ERR_HW_FLOAT_DISABLED if a single-precision number is
* encountered. If half-precision support is disabled, this will set
* @ref QCBOR_ERR_HALF_PRECISION_DISABLED if a half-precision number
* is encountered.
- *
+ * * TODO: is this right?
* If floating-point usage is disabled this will set
* @ref QCBOR_ERR_ALL_FLOAT_DISABLED if a floating point value is
* encountered.
*
- * See also QCBORDecode_GetInt64ConvertAll() which will perform the
- * same conversions as this and a lot more at the cost of adding more
- * object code to your executable.
+ * See also QCBORDecode_GetNumberConvertPreciselyBig().
*/
-static void
+void
QCBORDecode_GetNumberConvertPrecisely(QCBORDecodeContext *pCtx,
QCBORItem *pNumber);
-
-static void
+/**
+ * @brief Decode next item into a big number or big float.
+ *
+ * @param[in] pCtx The decode context.
+ * @param[in] Buffer The buffer for the big number or mantissa.
+ * @param[out] pNumber The returned 64-bit signed integer.
+ *
+ * This outputs a big number QCBOR_TYPE_POSBIGNUM or QCBOR_TYPE_NEGBIGNUM when the input is a whole number
+ * and a big float QCBOR_TYPE_BIGFLOAT_POS_BIGNUM or QCBOR_TYPE_BIGFLOAT_NEG_BIGNUM when the input is not. This can represent any
+ * number with no precision loss. The only limitation is that the
+ * exponent is a int64_t and the size of @c Buffer.
+ *
+ * Note that this will never return \ref QCBOR_TYPE_INT64 on the assumption
+ * that the caller is committed to supporting big numbers and big floats.
+ *
+ * The CBOR inputs to this can be integers (major types 0 and 1), floating-point,
+ * big numbers and big floats (eventually decimal fractions may also be supported).
+ */
+void
QCBORDecode_GetNumberConvertPreciselyBig(QCBORDecodeContext *pCtx,
+ UsefulBuf Buffer,
QCBORItem *pNumber);
/**
diff --git a/src/qcbor_decode.c b/src/qcbor_decode.c
index 090eb38..bf97722 100644
--- a/src/qcbor_decode.c
+++ b/src/qcbor_decode.c
@@ -7037,11 +7037,29 @@
#endif /* QCBOR_DISABLE_EXP_AND_MANTISSA */
+static double
+NegToD(uint64_t uNegInt)
+{
+ double d;
+ /* Improvement: do this with shifts and masks so it doesn't get disabled when float does */
+
+ /* Subtraction must be after the cast to avoid overflow. */
+ d = (double)uNegInt - 1.0;
+ if(d + 1 == (double)uNegInt) { /* Make sure it is a whole number */
+ return -d;
+ }
+
+ return NAN;
+}
+
+
void
QCBORDecode_GetNumberConvertPrecisely(QCBORDecodeContext *pMe,
QCBORItem *pNumber)
{
QCBORItem Item;
+ struct IEEE754_ToInt ToInt;
+ double d;
if(pMe->uLastError != QCBOR_SUCCESS) {
return;
@@ -7054,53 +7072,50 @@
}
switch(Item.uDataType) {
-
case QCBOR_TYPE_INT64:
case QCBOR_TYPE_UINT64:
*pNumber = Item;
break;
case QCBOR_TYPE_DOUBLE:
- /* TODO: Try to convert to int */
+ ToInt = IEEE754_DoubleToInt(Item.val.dfnum);
+ if(ToInt.type == IEEE754_ToInt_IS_INT) {
+ pNumber->uDataType = QCBOR_TYPE_INT64;
+ pNumber->val.int64 = ToInt.integer.is_signed;
+ } else if(ToInt.type == IEEE754_ToInt_IS_UINT) {
+ pNumber->uDataType = QCBOR_TYPE_UINT64;
+ pNumber->val.uint64 = ToInt.integer.un_signed;
+ } else {
+ *pNumber = Item;
+ }
break;
case QCBOR_TYPE_65BIT_NEG_INT:
- /* TODO: Try to convert to double without precision loss */
+ d = NegToD(Item.val.uint64);
+ if(d == NAN) {
+ *pNumber = Item;
+ } else {
+ pNumber->uDataType = QCBOR_TYPE_DOUBLE;
+ pNumber->val.dfnum = d;
+ }
+ break;
default:
pMe->uLastError = QCBOR_ERR_UNEXPECTED_TYPE;
+ pNumber->uDataType = QCBOR_TYPE_NONE;
break;
-
-
}
-
-
}
#if 0
-/*
-
-
- case QCBOR_TYPE_65BIT_NEG_INT:
-
- case QCBOR_TYPE_NEGBIGNUM:
- case QCBOR_TYPE_POSBIGNUM:
- /* TODO: convert to int, uint, double??? */
-
- case QCBOR_TYPE_BIGFLOAT:
-
- case QCBOR_TYPE_BIGFLOAT_NEG_BIGNUM:
- case QCBOR_TYPE_BIGFLOAT_POS_BIGNUM:
- */
-#endif
void
-Int_To_BigNum(int64_t n,
+Int_To_BigNum(uint64_t u,
UsefulBuf B,
- UsefulBufC pBigNum)
+ UsefulBufC *pBigNum)
{
/* This is just a memcpy, though watch out for endianess */
/* Have to check size */
@@ -7109,10 +7124,11 @@
}
+// TODO: error conditions
void
-Double_To_BigNum(int64_t n,
+Double_To_BigNum(double d,
UsefulBuf B,
- UsefulBufC pBigNum)
+ UsefulBufC *pBigNum)
{
/* Have to do shift thing to figure out if it is whole number */
/* Then kind of a copy and shift, possibly with a lot of zeros */
@@ -7123,7 +7139,8 @@
void
QCBORDecode_GetNumberConvertBigNum(QCBORDecodeContext *pMe,
UsefulBuf B,
- UsefulBufC pBigNum)
+ UsefulBufC *pBigNum,
+ bool *pbIsNegative)
{
QCBORItem Item;
@@ -7140,23 +7157,106 @@
switch(Item.uDataType) {
case QCBOR_TYPE_INT64:
+ if(Item.val.int64 < 0) {
+ *pbIsNegative = true;
+ Item.val.uint64 = (uint64_t)(-Item.val.int64);
+ } else {
+ Item.val.uint64 = (uint64_t)Item.val.int64;
+ }
+ /* FALLTHROUGH */
+
case QCBOR_TYPE_UINT64:
- *pNumber = Item;
+ Int_To_BigNum(Item.val.uint64, B, pBigNum);
break;
case QCBOR_TYPE_DOUBLE:
- /* TODO: Try to convert to int */
+ Double_To_BigNum(Item.val.dfnum, B, pBigNum);
+ /* TODO: sometimes this will fail */
break;
case QCBOR_TYPE_65BIT_NEG_INT:
/* TODO: Try to convert to double without precision loss */
+ case QCBOR_TYPE_NEGBIGNUM:
+ case QCBOR_TYPE_POSBIGNUM:
+ *pBigNum = UsefulBuf_Copy(B, Item.val.bigNum);
+ break;
+ // TODO: sign
+
+ case QCBOR_TYPE_BIGFLOAT:
+ // TODO: lots of work
+ // This will fail sometimes
+
+
default:
pMe->uLastError = QCBOR_ERR_UNEXPECTED_TYPE;
break;
}
-
-
}
+
+
+/* This can represent a very large range of values! */
+void
+QCBORDecode_GetNumberConvertBigFloat(QCBORDecodeContext *pMe,
+ UsefulBuf B,
+ UsefulBufC *pBigNum,
+ bool *pbIsNegative,
+ int64_t *nExponent)
+{
+ QCBORItem Item;
+
+ if(pMe->uLastError != QCBOR_SUCCESS) {
+ return;
+ }
+
+ QCBORError uError = QCBORDecode_GetNext(pMe, &Item);
+ if(uError) {
+ pMe->uLastError = (uint8_t)uError;
+ return;
+ }
+
+ switch(Item.uDataType) {
+
+ case QCBOR_TYPE_INT64:
+ if(Item.val.int64 < 0) {
+ *pbIsNegative = true;
+ Item.val.uint64 = (uint64_t)(-Item.val.int64);
+ } else {
+ Item.val.uint64 = (uint64_t)Item.val.int64;
+ }
+ /* FALLTHROUGH */
+
+ case QCBOR_TYPE_UINT64:
+ Int_To_BigNum(Item.val.uint64, B, pBigNum);
+ break;
+
+ case QCBOR_TYPE_DOUBLE:
+ Double_To_BigNum(Item.val.dfnum, B, pBigNum);
+ /* TODO: sometimes this will fail */
+ break;
+
+ case QCBOR_TYPE_65BIT_NEG_INT:
+ /* TODO: Try to convert to double without precision loss */
+
+ case QCBOR_TYPE_NEGBIGNUM:
+ case QCBOR_TYPE_POSBIGNUM:
+ *pBigNum = UsefulBuf_Copy(B, Item.val.bigNum);
+ break;
+ // TODO: sign
+
+ case QCBOR_TYPE_BIGFLOAT:
+ // TODO: lots of work
+ // This will fail sometimes
+
+
+ default:
+ pMe->uLastError = QCBOR_ERR_UNEXPECTED_TYPE;
+ break;
+
+
+ }
+}
+
+#endif
diff --git a/test/qcbor_decode_tests.c b/test/qcbor_decode_tests.c
index c8ef698..39a0f7e 100644
--- a/test/qcbor_decode_tests.c
+++ b/test/qcbor_decode_tests.c
@@ -8614,3 +8614,99 @@
return 0;
}
+
+
+struct PreciseNumberConversion {
+ char *szDescription;
+ UsefulBufC CBOR;
+ QCBORError uError;
+ uint8_t qcborType;
+ struct foo {
+ int64_t int64;
+ uint64_t uint64;
+ double d;
+ } number;
+};
+
+
+
+static const struct PreciseNumberConversion PreciseNumberConversions[] = {
+ {
+ "Zero",
+ {"\x00", 1},
+ QCBOR_SUCCESS,
+ QCBOR_TYPE_INT64,
+ {0, 0, 0}
+ },
+ {
+ "Pi",
+ {"\xFB\x40\x09\x2A\xDB\x40\x2D\x16\xB9", 9},
+ QCBOR_SUCCESS,
+ QCBOR_TYPE_DOUBLE,
+ {0, 0, 3.145926}
+ },
+ {
+ "String",
+ {"\x60", 1},
+ QCBOR_ERR_UNEXPECTED_TYPE,
+ QCBOR_TYPE_NONE,
+ {0, 0, 0}
+ }
+
+};
+
+int32_t
+PreciseTest(void)
+{
+ int i;
+ const struct PreciseNumberConversion *pTest;
+ QCBORError uErr;
+ QCBORItem Item;
+ QCBORDecodeContext DCtx;
+
+ const int count = (int)C_ARRAY_COUNT(PreciseNumberConversions, struct PreciseNumberConversion);
+
+ for(i = 0; i < count; i++) {
+ pTest = &PreciseNumberConversions[i];
+
+ QCBORDecode_Init(&DCtx, pTest->CBOR, 0);
+
+ QCBORDecode_GetNumberConvertPrecisely(&DCtx, &Item);
+
+ uErr = QCBORDecode_GetError(&DCtx);
+
+ if(uErr != pTest->uError) {
+ return i * 1000 + (int)uErr;
+ }
+
+ if(pTest->qcborType != Item.uDataType) {
+ return i * 1000 + 200;
+ }
+
+ if(pTest->qcborType == QCBOR_TYPE_NONE) {
+ continue;
+ }
+
+ switch(pTest->qcborType) {
+ case QCBOR_TYPE_INT64:
+ if(Item.val.int64 != pTest->number.int64) {
+ return i * 1000 + 300;
+ }
+ break;
+
+ case QCBOR_TYPE_UINT64:
+ if(Item.val.uint64 != pTest->number.uint64) {
+ return i * 1000 + 400;
+ }
+ break;
+
+ case QCBOR_TYPE_DOUBLE:
+ if(Item.val.dfnum != pTest->number.d) {
+ return i * 1000 + 500;
+ }
+ break;
+ }
+ }
+
+ return 0;
+}
diff --git a/test/qcbor_decode_tests.h b/test/qcbor_decode_tests.h
index 11fdc94..77f0bfc 100644
--- a/test/qcbor_decode_tests.h
+++ b/test/qcbor_decode_tests.h
@@ -318,4 +318,8 @@
*/
int32_t CBORTestIssue134(void);
+
+int32_t
+PreciseTest(void);
+
#endif /* defined(__QCBOR__qcbort_decode_tests__) */
diff --git a/test/run_tests.c b/test/run_tests.c
index bfc9dd9..1b064af 100644
--- a/test/run_tests.c
+++ b/test/run_tests.c
@@ -153,6 +153,7 @@
TEST_ENTRY(CDETest),
TEST_ENTRY(DCBORTest),
#endif /* ! USEFULBUF_DISABLE_ALL_FLOAT && ! QCBOR_DISABLE_PREFERRED_FLOAT */
+ TEST_ENTRY(PreciseTest),
TEST_ENTRY(ParseEmptyMapInMapTest),
};