diff --git a/README.md b/README.md index 9c7ed142c..9f56731e5 100644 --- a/README.md +++ b/README.md @@ -200,6 +200,8 @@ typedef struct cJSON /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */ int valueint; double valuedouble; + /* Decimal places used when printing non-integral values; 0 uses default formatting. */ + int decimal_places; char *string; } cJSON; ``` @@ -279,6 +281,44 @@ To iterate over an object, you can use the `cJSON_ArrayForEach` macro the same w cJSON also provides convenient helper functions for quickly creating a new item and adding it to an object, like `cJSON_AddNullToObject`. They return a pointer to the new item or `NULL` if they failed. +`cJSON_AddNumberWithPrecisionToObject` specifies the number of digits after the decimal point when printing a non-integral value. Non-integral values are padded or rounded as needed. Integral values are printed without a decimal point or trailing zeroes. `decimal_places` must be between 1 and 14. For example, the following rounds the representation of pi to six decimal places while leaving an integral value unchanged: + +```c +/* Returns a heap-allocated string that must be released with cJSON_free. */ +char *create_numbers_with_precision(void) +{ + char *string = NULL; + cJSON *object = cJSON_CreateObject(); + + if (object == NULL) + { + return NULL; + } + + if (cJSON_AddNumberWithPrecisionToObject(object, "pi", 3.141592653589793, 6) == NULL) + { + goto end; + } + + if (cJSON_AddNumberWithPrecisionToObject(object, "integer", 3.0, 5) == NULL) + { + goto end; + } + + string = cJSON_PrintUnformatted(object); + +end: + cJSON_Delete(object); + return string; +} +``` + +The generated JSON is: + +```json +{"pi":3.141593,"integer":3} +``` + ### Parsing JSON Given some JSON in a zero terminated string, you can parse it with `cJSON_Parse`. diff --git a/cJSON.c b/cJSON.c index 88c2d95b3..70715fb39 100644 --- a/cJSON.c +++ b/cJSON.c @@ -619,14 +619,20 @@ static cJSON_bool print_number(const cJSON * const item, printbuffer * const out } else { - /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ - length = sprintf((char*)number_buffer, "%1.15g", d); - - /* Check whether the original double can be recovered */ - if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) - { - /* If not, print with 17 decimal places of precision */ - length = sprintf((char*)number_buffer, "%1.17g", d); + /* Apply decimal-place formatting only when the value has a fractional part. */ + if (item->decimal_places && (ceil(d) != d)) { + /* Then convert it to string */ + length = sprintf((char*) number_buffer, "%.*f", item->decimal_places, d); + } else { + /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */ + length = sprintf((char*)number_buffer, "%1.15g", d); + + /* Check whether the original double can be recovered */ + if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || !compare_double((double)test, d)) + { + /* If not, print with 17 decimal places of precision */ + length = sprintf((char*)number_buffer, "%1.17g", d); + } } } @@ -2207,6 +2213,27 @@ CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * return NULL; } +CJSON_PUBLIC(cJSON*) cJSON_AddNumberWithPrecisionToObject(cJSON * const object, const char * const name, const double n, int decimal_places) +{ + cJSON *number_item; + + if (decimal_places <= 0 || decimal_places > 14) + { + return NULL; + } + + number_item = cJSON_CreateNumber(n); + + if (add_item_to_object(object, name, number_item, &global_hooks, false)) + { + number_item->decimal_places = decimal_places; + return number_item; + } + + cJSON_Delete(number_item); + return NULL; +} + CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string) { cJSON *string_item = cJSON_CreateString(string); diff --git a/cJSON.h b/cJSON.h index cab5feb42..d8b0688e1 100644 --- a/cJSON.h +++ b/cJSON.h @@ -117,6 +117,8 @@ typedef struct cJSON int valueint; /* The item's number, if type==cJSON_Number */ double valuedouble; + /* Decimal places used when printing non-integral values; 0 uses default formatting. */ + int decimal_places; /* The item's name string, if this item is the child of, or is in the list of subitems of an object. */ char *string; @@ -272,6 +274,7 @@ CJSON_PUBLIC(cJSON*) cJSON_AddTrueToObject(cJSON * const object, const char * co CJSON_PUBLIC(cJSON*) cJSON_AddFalseToObject(cJSON * const object, const char * const name); CJSON_PUBLIC(cJSON*) cJSON_AddBoolToObject(cJSON * const object, const char * const name, const cJSON_bool boolean); CJSON_PUBLIC(cJSON*) cJSON_AddNumberToObject(cJSON * const object, const char * const name, const double number); +CJSON_PUBLIC(cJSON*) cJSON_AddNumberWithPrecisionToObject(cJSON * const object, const char * const name, const double n, int decimal_places); CJSON_PUBLIC(cJSON*) cJSON_AddStringToObject(cJSON * const object, const char * const name, const char * const string); CJSON_PUBLIC(cJSON*) cJSON_AddRawToObject(cJSON * const object, const char * const name, const char * const raw); CJSON_PUBLIC(cJSON*) cJSON_AddObjectToObject(cJSON * const object, const char * const name); diff --git a/cJSON_Utils.c b/cJSON_Utils.c index 8b38eb253..17f7859c4 100644 --- a/cJSON_Utils.c +++ b/cJSON_Utils.c @@ -840,7 +840,7 @@ static int apply_patch(cJSON *object, const cJSON *patch, const cJSON_bool case_ { if (opcode == REMOVE) { - static const cJSON invalid = { NULL, NULL, NULL, cJSON_Invalid, NULL, 0, 0, NULL}; + static const cJSON invalid = { NULL, NULL, NULL, cJSON_Invalid, NULL, 0, 0, 0, NULL}; overwrite_item(object, invalid); diff --git a/tests/cjson_add.c b/tests/cjson_add.c index ac96ce75d..f99d5cfda 100644 --- a/tests/cjson_add.c +++ b/tests/cjson_add.c @@ -255,6 +255,44 @@ static void cjson_add_number_should_add_number(void) cJSON_Delete(root); } +static void assert_add_number_with_precision_prints(const char *expected, double number, int decimal_places) +{ + cJSON *root = cJSON_CreateObject(); + char *printed = NULL; + + TEST_ASSERT_NOT_NULL(cJSON_AddNumberWithPrecisionToObject(root, "number", number, decimal_places)); + + printed = cJSON_PrintUnformatted(root); + TEST_ASSERT_EQUAL_STRING(expected, printed); + + cJSON_free(printed); + cJSON_Delete(root); +} + +static void cjson_add_number_with_precision_should_format_output(void) +{ + assert_add_number_with_precision_prints("{\"number\":1.20}", 1.2, 2); + assert_add_number_with_precision_prints("{\"number\":1.24}", 1.236, 2); + assert_add_number_with_precision_prints("{\"number\":-1.200}", -1.2, 3); + assert_add_number_with_precision_prints("{\"number\":3}", 3.0, 5); + assert_add_number_with_precision_prints("{\"number\":0.12345678901234}", 0.12345678901234, 14); +} + +static void cjson_add_number_with_precision_should_reject_invalid_precision(void) +{ + cJSON *root = cJSON_CreateObject(); + char *printed = NULL; + + TEST_ASSERT_NULL(cJSON_AddNumberWithPrecisionToObject(root, "number", 1.2, 0)); + TEST_ASSERT_NULL(cJSON_AddNumberWithPrecisionToObject(root, "number", 1.2, 15)); + + printed = cJSON_PrintUnformatted(root); + TEST_ASSERT_EQUAL_STRING("{}", printed); + + cJSON_free(printed); + cJSON_Delete(root); +} + static void cjson_add_number_should_fail_with_null_pointers(void) { cJSON *root = cJSON_CreateObject(); @@ -448,6 +486,8 @@ int CJSON_CDECL main(void) RUN_TEST(cjson_add_bool_should_fail_on_allocation_failure); RUN_TEST(cjson_add_number_should_add_number); + RUN_TEST(cjson_add_number_with_precision_should_format_output); + RUN_TEST(cjson_add_number_with_precision_should_reject_invalid_precision); RUN_TEST(cjson_add_number_should_fail_with_null_pointers); RUN_TEST(cjson_add_number_should_fail_on_allocation_failure); diff --git a/tests/misc_tests.c b/tests/misc_tests.c index fe2325e96..664462895 100644 --- a/tests/misc_tests.c +++ b/tests/misc_tests.c @@ -239,7 +239,7 @@ static void cjson_should_not_follow_too_deep_circular_references(void) static void cjson_set_number_value_should_set_numbers(void) { - cJSON number[1] = {{NULL, NULL, NULL, cJSON_Number, NULL, 0, 0, NULL}}; + cJSON number[1] = {{NULL, NULL, NULL, cJSON_Number, NULL, 0, 0, 0, NULL}}; cJSON_SetNumberValue(number, 1.5); TEST_ASSERT_EQUAL(1, number->valueint); @@ -361,7 +361,7 @@ static void cjson_replace_item_via_pointer_should_replace_items(void) static void cjson_replace_item_in_object_should_preserve_name(void) { - cJSON root[1] = {{NULL, NULL, NULL, 0, NULL, 0, 0, NULL}}; + cJSON root[1] = {{NULL, NULL, NULL, 0, NULL, 0, 0, 0, NULL}}; cJSON *child = NULL; cJSON *replacement = NULL; cJSON_bool flag = false; diff --git a/tests/readme_examples.c b/tests/readme_examples.c index 09850cd40..5aaa1c0c5 100644 --- a/tests/readme_examples.c +++ b/tests/readme_examples.c @@ -165,6 +165,34 @@ static char *create_monitor_with_helpers(void) return string; } +/* Returns a heap-allocated string that must be released with cJSON_free. */ +static char *create_numbers_with_precision(void) +{ + char *string = NULL; + cJSON *object = cJSON_CreateObject(); + + if (object == NULL) + { + return NULL; + } + + if (cJSON_AddNumberWithPrecisionToObject(object, "pi", 3.141592653589793, 6) == NULL) + { + goto end; + } + + if (cJSON_AddNumberWithPrecisionToObject(object, "integer", 3.0, 5) == NULL) + { + goto end; + } + + string = cJSON_PrintUnformatted(object); + +end: + cJSON_Delete(object); + return string; +} + /* return 1 if the monitor supports full hd, 0 otherwise */ static int supports_full_hd(const char * const monitor) { @@ -232,6 +260,16 @@ static void create_monitor_with_helpers_should_create_a_monitor(void) free(monitor); } +static void create_numbers_with_precision_should_round_and_preserve_integers(void) +{ + char *numbers = create_numbers_with_precision(); + + TEST_ASSERT_NOT_NULL(numbers); + TEST_ASSERT_EQUAL_STRING("{\"pi\":3.141593,\"integer\":3}", numbers); + + cJSON_free(numbers); +} + static void supports_full_hd_should_check_for_full_hd_support(void) { static const char *monitor_without_hd = "{\n\ @@ -252,6 +290,7 @@ int CJSON_CDECL main(void) RUN_TEST(create_monitor_should_create_a_monitor); RUN_TEST(create_monitor_with_helpers_should_create_a_monitor); + RUN_TEST(create_numbers_with_precision_should_round_and_preserve_integers); RUN_TEST(supports_full_hd_should_check_for_full_hd_support); return UNITY_END();