cJSON icon indicating copy to clipboard operation
cJSON copied to clipboard

limiting print decimal point

Open ajaybhargav opened this issue 7 years ago • 27 comments

Currently JSON number is printed with upto 15 decimal places (e.g. 21.379983333333332) most of the time such long number is not needed and is a wastage of space in embedded system. Can we have a provision where in we can pass number of decimal places needed.

for instance: cJSON_CreateNumber(double num, int digits)

This will save lot of space when complete decimal number is not needed.

ajaybhargav avatar May 26 '17 09:05 ajaybhargav

From what I understand it's not possible to do this via a special cJSON_CreateNumber function in a good way. In theory an additional number type could be added or an additional flag, but that doesn't lie in the scope of the library.

So this woul have to be either an additional cJSON_Print function or a compile time flag. I'm not sure how to use a precision from a variable in a sprintf format string though.

Another potential idea would be to find out if the number can be truncated in such a way that is is printed as a shorter number.

But in any case I'm quite reluctant to add such functionality because it increases complexity and configurable knobs again.

If such functionality were to be added, it would probably be set at compile time.

FSMaxB avatar May 26 '17 12:05 FSMaxB

I think we already have a good complex logic where we first try one format and then try to check if retrieval is same or not and then try another format. A simplest implementation could be to have a dynamic format. e.g.

char format[10];
/* create a format */
sprintf(format, "%%1.%dg", decimal_places);
/* Then convert it to string */
sprintf((char*)number_buffer, format, d);

ajaybhargav avatar May 27 '17 18:05 ajaybhargav

In general I would be ok with this. (better at runtime than compile time since this means that I can unit test it more easily, I've come around on that one!)

To keep the overhead low this format string should be created once and then passed through via e.g. the printbuffer.

The problem is though that I don't really want to add more and more functions to the API for every possible configuration option, and I don't want it to be stored in a global variable either because the goal for cJSON v2 is thread safety. (or reentrancy in general)

There won't be less configuration options in the future, so I would like to have a way to only have one Function for custom parsing, one function for custom printing etc and passing a configuration "object" to them via a pointer.

Problem: If I use a struct, I would break ABI and API compatibility every time I add a new option. The API compatibility can be dealt with using a default settings struct that can be assigned, but the ABI would still break.

Maybe it's ok to store it in a global variable for the time being and having another configuration method ready for v2.

FSMaxB avatar May 30 '17 08:05 FSMaxB

I don't think this is needed to be stored as an option since its not always needed to be the same. For instance, If I am putting temperature value in JSON Number format then 2 decimal places are enough however I will need atleast 6 or 7 decimal places if I want to put a GPS coordinate in JSON. So a per call decimal place control will be the best choice.

I know you do not want to add too many functions and maintain ABI compatibility, Can we do something like say.. cJSON_Print calls internally (with maybe -1 as decimal place to indicate default format) cJSON_PrintEx (extended function) which takes decimal places as input can be used if user wants to change number format.

Edit: I am thinking more from usage prospective in real scenarios.

ajaybhargav avatar May 30 '17 09:05 ajaybhargav

It can be stored as an option, the idea of #177 is to create as many configuration objects as you need and store them for later use. Then you just pass the appropriate one in when you need it.

I am not willing to add another cJSON_Print function. For now I am only willing to add a function that accesses a global variable, similar to cJSON_InitHooks. As long as you don't do threading you can just call that every time you need to change the precision.

What do you think?

FSMaxB avatar May 31 '17 12:05 FSMaxB

Would not be better if we find somewhere in the wild a 'number beautifier' function?

Tangerino avatar Feb 24 '18 09:02 Tangerino

@Tangerino I don't quite understand what you mean by that.

The current situation is like that: If possible, the shortest decimal representation is printed out, but in a way that it can still be parsed back to exactly the same double. (see https://github.com/DaveGamble/cJSON/pull/153).

What has been asked here is to allow losing information in order to get even smaller number representations.

FSMaxB avatar Feb 24 '18 09:02 FSMaxB

Hello,

I have the same problem: Floats are stored with a lot of unused bytes, So I think a cJSON_AddNumberToObject with an additional argument for giving the digits - which is used in the print function - would solve this. If this argument is -1 the default - as it is now - is used.

At the moment I'am using strings for sending numbers ...... not really good

Reiner1210 avatar May 03 '18 13:05 Reiner1210

I'm having the same issue here. I have a float object with value 0.000081 (parsed with atof()).

When printed with cJSON_Print() (or sprintf()) I get "0.0000809999983175658". But if I use precision parameter in sprintf (sprintf(buf, "%.6f", 0.000081)) I get the expected output.

A decimals property in the cJSON struct would be great, so it can be defaulted to no specified precision, or custom precision.

marianorenzi avatar Jan 06 '20 20:01 marianorenzi

I made few changes to CJSON.h to add decimal places variable.

/* The cJSON structure: */

typedef struct cJSON
{
    /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
    struct cJSON *next;
    struct cJSON *prev;
    /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
    struct cJSON *child;

    /* The type of the item, as above. */
    int type;

    /* The item's string, if type==cJSON_String  and type == cJSON_Raw */
    char *valuestring;
    /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
    int valueint;
    /* The item's number, if type==cJSON_Number */
    double valuedouble;
    /* Decimal places */
    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;
} cJSON;

And in print_number function modified the following:

    	/* If decimal places are present and value is not an integer type */
    	if ((item->decimal_places) && (ceil(d) != d)) {
            /* create a format */
            sprintf(number_format, "%%1.%df", item->decimal_places);
            /* Then convert it to string */
            length = snprintf((char*) number_buffer, 26, number_format, d);
        } else {
            /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */
            length = snprintf((char*)number_buffer, 26, "%1.15g", d);

            /* Check whether the original double can be recovered */
            if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || ((double)test != d))
            {
                /* If not, print with 17 decimal places of precision */
                length = snprintf((char*)number_buffer, 26, "%1.17g", d);
            }
        }

decimal_places is the only thing I added to the cjson library. I had no other choice.

In order not to break the API, I added a new function outside cjson library

void cJSON_AddNumberToObjectOpts(cJSON *object, char *name, double n, int places)
{
	cJSON *number_item = cJSON_CreateNumber(n);

	number_item->decimal_places = places;

	cJSON_AddItemToObject(object, name, number_item);
}

So I can now print number with specified number of digits. Hope this helps.

ajaybhargav avatar Jan 07 '20 07:01 ajaybhargav

I made few changes to CJSON.h to add decimal places variable.

/* The cJSON structure: */

typedef struct cJSON
{
    /* next/prev allow you to walk array/object chains. Alternatively, use GetArraySize/GetArrayItem/GetObjectItem */
    struct cJSON *next;
    struct cJSON *prev;
    /* An array or object item will have a child pointer pointing to a chain of the items in the array/object. */
    struct cJSON *child;

    /* The type of the item, as above. */
    int type;

    /* The item's string, if type==cJSON_String  and type == cJSON_Raw */
    char *valuestring;
    /* writing to valueint is DEPRECATED, use cJSON_SetNumberValue instead */
    int valueint;
    /* The item's number, if type==cJSON_Number */
    double valuedouble;
    /* Decimal places */
    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;
} cJSON;

And in print_number function modified the following:

    	/* If decimal places are present and value is not an integer type */
    	if ((item->decimal_places) && (ceil(d) != d)) {
            /* create a format */
            sprintf(number_format, "%%1.%df", item->decimal_places);
            /* Then convert it to string */
            length = snprintf((char*) number_buffer, 26, number_format, d);
        } else {
            /* Try 15 decimal places of precision to avoid nonsignificant nonzero digits */
            length = snprintf((char*)number_buffer, 26, "%1.15g", d);

            /* Check whether the original double can be recovered */
            if ((sscanf((char*)number_buffer, "%lg", &test) != 1) || ((double)test != d))
            {
                /* If not, print with 17 decimal places of precision */
                length = snprintf((char*)number_buffer, 26, "%1.17g", d);
            }
        }

decimal_places is the only thing I added to the cjson library. I had no other choice.

In order not to break the API, I added a new function outside cjson library

void cJSON_AddNumberToObjectOpts(cJSON *object, char *name, double n, int places)
{
	cJSON *number_item = cJSON_CreateNumber(n);

	number_item->decimal_places = places;

	cJSON_AddItemToObject(object, name, number_item);
}

So I can now print number with specified number of digits. Hope this helps.

Another option for the format string could be snprintf(number_buffer, 26, "%.*f", item->decimal_places, d); But precision argument may not be supported on lightweight implementations of printf().

marianorenzi avatar Jan 07 '20 21:01 marianorenzi

My implementation works with newlib nano (lightweight version) with printf enabled for float and double. I can test your method too. Thanks for sharing.

ajaybhargav avatar Jan 08 '20 08:01 ajaybhargav

do you think you can push your changes to github ajay?

ethangoldstein avatar Jan 22 '20 16:01 ethangoldstein

Sure I can, but I am not sure if it will be accepted by maintainer. But I will link to my personal repo here. I am sure you've noticed I have used snprintf instead of sprintf just to be on safe side. Do you want changes with sprintf or snprintf?

ajaybhargav avatar Jan 23 '20 09:01 ajaybhargav

sprintf would be best, as I am using it for primarily embedded systems. thank you!

ethangoldstein avatar Jan 23 '20 18:01 ethangoldstein

@ethangoldstein I completely forgot 😄 here is the repo with changes https://github.com/ajaybhargav/cJSON/tree/number_decimal_point

let me know if I made any mistake (just in case 😉)

ajaybhargav avatar Feb 03 '20 15:02 ajaybhargav

awesome, thank you!

ethangoldstein avatar Feb 06 '20 21:02 ethangoldstein

@ajaybhargav Your solution looks pretty sane to me. When glancing over the commit itdoes not seem to brake the current API. @DaveGamble any chance we'll see this merged into main repo?

eflukx avatar May 05 '20 16:05 eflukx

Thanks @eflukx. I can create pull request if @DaveGamble / @FSMaxB suggests.

ajaybhargav avatar May 06 '20 07:05 ajaybhargav

I've been using Raw for this. If all you do is print it, isn't that sufficient?

    char print_num[18];
    snprintf(print_num, 18, "%.2f", my_value);
    cJSON_AddRawToObject(jobj, "field_name", print_num);

IanMercer avatar Oct 08 '20 18:10 IanMercer

Looks awesome @ajaybhargav! Are you sure you want to wait for the suggestion before creating that PR?

bakeromso avatar Nov 12 '20 21:11 bakeromso

@dussentue I can wait.. we can set a deadline for suggestions. code is ready on this branch. If needed, I can re-base and push again whenever you say. https://github.com/ajaybhargav/cJSON/tree/number_decimal_point

ajaybhargav avatar Nov 18 '20 07:11 ajaybhargav

@bakeromso I have made a pull request #558 tested it with newlib and newlib-nano

ajaybhargav avatar Feb 23 '21 11:02 ajaybhargav

Is this going to be merged soon?

gsantamarina avatar Aug 12 '22 15:08 gsantamarina

Any news on this?

rodmaz avatar Sep 14 '22 17:09 rodmaz

I think this is very helpful and should be merged as cJSON is being used in many systems.

law-ko avatar Nov 02 '22 21:11 law-ko