diff options
author | Mark Pulford <mark@kyne.com.au> | 2011-12-30 07:47:44 +0400 |
---|---|---|
committer | Mark Pulford <mark@kyne.com.au> | 2011-12-30 07:47:44 +0400 |
commit | 2416b145073211b840781da6abf4b6d97f4657a6 (patch) | |
tree | 6e92a13a7cc8ef8357245bc3ef320f5841350991 /fpconv.c | |
parent | 6cc88e3ac5275868e168acaf60203563f131355b (diff) |
Add fpconv to work around comma decimal points
Create a separate buffer and translate comma <> dot before calling
strtod(), and after calling sprintf() as required.
- Add "update_locale" Lua API call and init locale on module load.
- Move sprintf format string to fpconv
Diffstat (limited to 'fpconv.c')
-rw-r--r-- | fpconv.c | 155 |
1 files changed, 155 insertions, 0 deletions
diff --git a/fpconv.c b/fpconv.c new file mode 100644 index 0000000..3ff79dc --- /dev/null +++ b/fpconv.c @@ -0,0 +1,155 @@ +#include <stdio.h> +#include <stdlib.h> +#include <assert.h> +#include <string.h> + +#include "fpconv.h" + +static char locale_decimal_point = '.'; + +/* In theory multibyte decimal_points are possible, but + * Lua CJSON only supports UTF-8 and known locales only have + * single byte decimal points ([.,]). + * + * localconv() may not be thread safe, and nl_langinfo() is not + * supported on some platforms. Use sprintf() instead. */ +void fpconv_update_locale() +{ + char buf[8]; + + snprintf(buf, sizeof(buf), "%g", 0.5); + + /* Failing this test might imply the platform has a buggy dtoa + * implementation or wide characters */ + if (buf[0] != '0' || buf[2] != '5' || buf[3] != 0) { + fprintf(stderr, "Error: wide characters found or printf() bug."); + abort(); + } + + locale_decimal_point = buf[1]; +} + +/* Check for a valid number character: [-+0-9a-fA-FpPxX.] + * It doesn't matter if actual invalid characters are counted - strtod() + * will find the valid number if it exists. The risk is that slightly more + * memory might be allocated before a parse error occurs. */ +static int valid_number_character(char ch) +{ + char lower_ch; + + if ('0' <= ch && ch <= '9') + return 1; + if (ch == '-' || ch == '+' || ch == '.') + return 1; + + /* Hex digits, exponent (e), base (p), "infinity",.. + * The main purpose is to not include a "comma". If any other invalid + * characters are included, the will only generate a parse error later. */ + lower_ch = ch | 0x20; + if ('a' <= lower_ch && lower_ch <= 'y') + return 1; + + return 0; +} + +/* Calculate the size of the buffer required for a locale + * conversion. Returns 0 if conversion is not required */ +static int strtod_buffer_size(const char *s) +{ + const char *p = s; + + while (valid_number_character(*p)) + p++; + + return p - s; +} + +/* Similar to strtod(), but must be passed the current locale's decimal point + * character. Guaranteed to be called at the start of any valid number in a string */ +double fpconv_strtod(const char *nptr, char **endptr) +{ + char *num, *endnum, *dp; + int numlen; + double value; + + /* System strtod() is fine when decimal point is '.' */ + if (locale_decimal_point == '.') + return strtod(nptr, endptr); + + numlen = strtod_buffer_size(nptr); + if (!numlen) { + /* No valid characters found, standard strtod() return */ + *endptr = (char *)nptr; + return 0; + } + + /* Duplicate number into buffer */ + num = malloc(numlen + 1); + if (!num) { + fprintf(stderr, "Out of memory"); + abort(); + } + memcpy(num, nptr, numlen); + num[numlen] = 0; + + /* Update decimal point character if found */ + dp = strchr(num, '.'); + if (dp) + *dp = locale_decimal_point; + + value = strtod(num, &endnum); + *endptr = (char *)&nptr[endnum - num]; + free(num); + + return value; +} + +/* "fmt" must point to a buffer of at least 6 characters */ +static void set_number_format(char *fmt, int precision) +{ + int d1, d2, i; + + assert(1 <= precision && precision <= 14); + + /* Create printf format (%.14g) from precision */ + d1 = precision / 10; + d2 = precision % 10; + fmt[0] = '%'; + fmt[1] = '.'; + i = 2; + if (d1) { + fmt[i++] = '0' + d1; + } + fmt[i++] = '0' + d2; + fmt[i++] = 'g'; + fmt[i++] = 0; +} + +/* Assumes there is always at least 32 characters available in the target buffer */ +int fpconv_g_fmt(char *str, double num, int precision) +{ + char buf[FPCONV_G_FMT_BUFSIZE]; + char fmt[6]; + int len; + char *b; + + set_number_format(fmt, precision); + + /* Pass through when decimal point character is dot. */ + if (locale_decimal_point == '.') + return snprintf(str, FPCONV_G_FMT_BUFSIZE, fmt, num); + + /* snprintf() to a buffer then translate for other decimal point characters */ + len = snprintf(buf, FPCONV_G_FMT_BUFSIZE, fmt, num); + + /* Returned 'len' includes the null terminator */ + b = buf; + do { + *str++ = (*b == locale_decimal_point ? '.' : *b); + } while(*b++); + + return len; +} + +/* vi:ai et sw=4 ts=4: + */ |