add fractional number formatting - relates to #526 default tip

Thu, 09 Jan 2025 22:37:10 +0100

author
Mike Becker <universe@uap-core.de>
date
Thu, 09 Jan 2025 22:37:10 +0100
changeset 1117
54df904472b0
parent 1116
b381da3a9b19

add fractional number formatting - relates to #526

src/cx/json.h file | annotate | diff | comparison | revisions
src/json.c file | annotate | diff | comparison | revisions
tests/test_json.c file | annotate | diff | comparison | revisions
--- a/src/cx/json.h	Thu Jan 09 21:19:52 2025 +0100
+++ b/src/cx/json.h	Thu Jan 09 22:37:10 2025 +0100
@@ -449,6 +449,8 @@
     bool sort_members;
     /**
      * The maximum number of fractional digits in a number value.
+     * The default value is 6 and values larger than 15 are reduced to 15.
+     * Note, that the actual number of digits may be lower, depending on the concrete number.
      */
     uint8_t frac_max_digits;
     /**
--- a/src/json.c	Thu Jan 09 21:19:52 2025 +0100
+++ b/src/json.c	Thu Jan 09 22:37:10 2025 +0100
@@ -988,7 +988,7 @@
 static const CxJsonWriter cx_json_writer_default = {
     false,
     true,
-    255,
+    6,
     false,
     4
 };
@@ -1001,7 +1001,7 @@
     return (CxJsonWriter) {
         true,
         true,
-        255,
+        6,
         use_spaces,
         4
     };
@@ -1050,7 +1050,7 @@
     size_t actual = 0, expected = 0;
 
     // small buffer for number to string conversions
-    char numbuf[32];
+    char numbuf[40];
 
     // recursively write the values
     switch (value->type) {
@@ -1160,12 +1160,71 @@
             break;
         }
         case CX_JSON_NUMBER: {
-            // TODO: locale bullshit
-            // TODO: formatting settings
-            snprintf(numbuf, 32, "%g", value->value.number);
-            size_t len = strlen(numbuf);
-            actual += wfunc(numbuf, 1, len, target);
-            expected += len;
+            int precision = settings->frac_max_digits;
+            // because of the way how %g is defined, we need to
+            // double the precision and truncate ourselves
+            precision = 1 + (precision > 15 ? 30 : 2 * precision);
+            snprintf(numbuf, 40, "%.*g", precision, value->value.number);
+            char *dot, *exp;
+            unsigned char max_digits;
+            // find the decimal separator and hope that it's one of . or ,
+            dot = strchr(numbuf, '.');
+            if (dot == NULL) {
+                dot = strchr(numbuf, ',');
+            }
+            if (dot == NULL) {
+                // no decimal separator found
+                // output everything until a possible exponent
+                max_digits = 30;
+                dot = numbuf;
+            } else {
+                // found a decimal separator
+                // output everything until the separator
+                // and set max digits to what the settings say
+                size_t len = dot - numbuf;
+                actual += wfunc(numbuf, 1, len, target);
+                expected += len;
+                max_digits = settings->frac_max_digits;
+                if (max_digits > 15) {
+                    max_digits = 15;
+                }
+                // locale independent separator
+                if (max_digits > 0) {
+                    actual += wfunc(".", 1, 1, target);
+                    expected++;
+                }
+                dot++;
+            }
+            // find the exponent
+            exp = strchr(dot, 'e');
+            if (exp == NULL) {
+                // no exponent - output the rest
+                if (max_digits > 0) {
+                    size_t len = strlen(dot);
+                    if (len > max_digits) {
+                        len = max_digits;
+                    }
+                    actual += wfunc(dot, 1, len, target);
+                    expected += len;
+                }
+            } else {
+                // exponent found - truncate the frac digits
+                // and then output the rest
+                if (max_digits > 0) {
+                    size_t len = exp - dot - 1;
+                    if (len > max_digits) {
+                        len = max_digits;
+                    }
+                    actual += wfunc(dot, 1, len, target);
+                    expected += len;
+                }
+                actual += wfunc("e", 1, 1, target);
+                expected++;
+                exp++;
+                size_t len = strlen(exp);
+                actual += wfunc(exp, 1, len, target);
+                expected += len;
+            }
             break;
         }
         case CX_JSON_INTEGER: {
--- a/tests/test_json.c	Thu Jan 09 21:19:52 2025 +0100
+++ b/tests/test_json.c	Thu Jan 09 22:37:10 2025 +0100
@@ -887,6 +887,58 @@
     cx_testing_allocator_destroy(&talloc);
 }
 
+CX_TEST(test_json_write_frac_max_digits) {
+    CxJsonValue* num = cxJsonCreateNumber(NULL, 3.141592653589793);
+    CxJsonWriter writer = cxJsonWriterCompact();
+    CxBuffer buf;
+    cxBufferInit(&buf, NULL, 32, NULL, 0);
+    CX_TEST_DO {
+        // test default settings (6 digits)
+        cxJsonWrite(&buf,num, cxBufferWriteFunc, &writer);
+        CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("3.141592")));
+
+        // test too many digits
+        cxBufferReset(&buf);
+        writer.frac_max_digits = 200;
+        cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer);
+        CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("3.141592653589793")));
+        
+        // test 0 digits
+        cxBufferReset(&buf);
+        writer.frac_max_digits = 0;
+        cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer);
+        CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("3")));
+
+        // test 2 digits
+        cxBufferReset(&buf);
+        writer.frac_max_digits = 2;
+        cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer);
+        CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("3.14")));
+
+        // test 3 digits
+        cxBufferReset(&buf);
+        writer.frac_max_digits = 3;
+        cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer);
+        CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("3.141")));
+
+        // test 6 digits, but two are left of the decimal point
+        num->value.number = 47.110815;
+        cxBufferReset(&buf);
+        writer.frac_max_digits = 6;
+        cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer);
+        CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("47.110815")));
+
+        // test 4 digits with exponent
+        num->value.number = 5.11223344e23;
+        cxBufferReset(&buf);
+        writer.frac_max_digits = 4;
+        cxJsonWrite(&buf, num, cxBufferWriteFunc, &writer);
+        CX_TEST_ASSERT(0 == cx_strcmp(cx_strn(buf.space, buf.size), CX_STR("5.1122e+23")));
+    }
+    cxBufferDestroy(&buf);
+    cxJsonValueFree(num);
+}
+
 CxTestSuite *cx_test_suite_json(void) {
     CxTestSuite *suite = cx_test_suite_new("json");
 
@@ -909,6 +961,7 @@
     cx_test_register(suite, test_json_write_pretty_default_spaces);
     cx_test_register(suite, test_json_write_pretty_default_tabs);
     cx_test_register(suite, test_json_write_pretty_preserve_order);
+    cx_test_register(suite, test_json_write_frac_max_digits);
     
     return suite;
 }

mercurial