fpconv.c (6173B)
1 /* fpconv - Floating point conversion routines 2 * 3 * Copyright (c) 2011-2012 Mark Pulford <mark@kyne.com.au> 4 * 5 * Permission is hereby granted, free of charge, to any person obtaining 6 * a copy of this software and associated documentation files (the 7 * "Software"), to deal in the Software without restriction, including 8 * without limitation the rights to use, copy, modify, merge, publish, 9 * distribute, sublicense, and/or sell copies of the Software, and to 10 * permit persons to whom the Software is furnished to do so, subject to 11 * the following conditions: 12 * 13 * The above copyright notice and this permission notice shall be 14 * included in all copies or substantial portions of the Software. 15 * 16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 */ 24 25 /* JSON uses a '.' decimal separator. strtod() / sprintf() under C libraries 26 * with locale support will break when the decimal separator is a comma. 27 * 28 * fpconv_* will around these issues with a translation buffer if required. 29 */ 30 31 #include <stdio.h> 32 #include <stdlib.h> 33 #include <assert.h> 34 #include <string.h> 35 36 #include "fpconv.h" 37 38 /* Workaround for MSVC */ 39 #ifdef _MSC_VER 40 #define inline __inline 41 #define snprintf sprintf_s 42 #endif 43 44 /* Lua CJSON assumes the locale is the same for all threads within a 45 * process and doesn't change after initialisation. 46 * 47 * This avoids the need for per thread storage or expensive checks 48 * for call. */ 49 static char locale_decimal_point = '.'; 50 51 /* In theory multibyte decimal_points are possible, but 52 * Lua CJSON only supports UTF-8 and known locales only have 53 * single byte decimal points ([.,]). 54 * 55 * localconv() may not be thread safe (=>crash), and nl_langinfo() is 56 * not supported on some platforms. Use sprintf() instead - if the 57 * locale does change, at least Lua CJSON won't crash. */ 58 static void fpconv_update_locale(void) 59 { 60 char buf[8]; 61 62 snprintf(buf, sizeof(buf), "%g", 0.5); 63 64 /* Failing this test might imply the platform has a buggy dtoa 65 * implementation or wide characters */ 66 if (buf[0] != '0' || buf[2] != '5' || buf[3] != 0) { 67 fprintf(stderr, "Error: wide characters found or printf() bug."); 68 abort(); 69 } 70 71 locale_decimal_point = buf[1]; 72 } 73 74 /* Check for a valid number character: [-+0-9a-yA-Y.] 75 * Eg: -0.6e+5, infinity, 0xF0.F0pF0 76 * 77 * Used to find the probable end of a number. It doesn't matter if 78 * invalid characters are counted - strtod() will find the valid 79 * number if it exists. The risk is that slightly more memory might 80 * be allocated before a parse error occurs. */ 81 static inline int valid_number_character(char ch) 82 { 83 char lower_ch; 84 85 if ('0' <= ch && ch <= '9') 86 return 1; 87 if (ch == '-' || ch == '+' || ch == '.') 88 return 1; 89 90 /* Hex digits, exponent (e), base (p), "infinity",.. */ 91 lower_ch = ch | 0x20; 92 if ('a' <= lower_ch && lower_ch <= 'y') 93 return 1; 94 95 return 0; 96 } 97 98 /* Calculate the size of the buffer required for a strtod locale 99 * conversion. */ 100 static int strtod_buffer_size(const char *s) 101 { 102 const char *p = s; 103 104 while (valid_number_character(*p)) 105 p++; 106 107 return p - s; 108 } 109 110 /* Similar to strtod(), but must be passed the current locale's decimal point 111 * character. Guaranteed to be called at the start of any valid number in a string */ 112 double fpconv_strtod(const char *nptr, char **endptr) 113 { 114 char localbuf[FPCONV_G_FMT_BUFSIZE]; 115 char *buf, *endbuf, *dp; 116 int buflen; 117 double value; 118 119 /* System strtod() is fine when decimal point is '.' */ 120 if (locale_decimal_point == '.') 121 return strtod(nptr, endptr); 122 123 buflen = strtod_buffer_size(nptr); 124 if (!buflen) { 125 /* No valid characters found, standard strtod() return */ 126 *endptr = (char *)nptr; 127 return 0; 128 } 129 130 /* Duplicate number into buffer */ 131 if (buflen >= FPCONV_G_FMT_BUFSIZE) { 132 /* Handle unusually large numbers */ 133 buf = (char *)malloc(buflen + 1); 134 if (!buf) { 135 fprintf(stderr, "Out of memory"); 136 abort(); 137 } 138 } else { 139 /* This is the common case.. */ 140 buf = localbuf; 141 } 142 memcpy(buf, nptr, buflen); 143 buf[buflen] = 0; 144 145 /* Update decimal point character if found */ 146 dp = strchr(buf, '.'); 147 if (dp) 148 *dp = locale_decimal_point; 149 150 value = strtod(buf, &endbuf); 151 *endptr = (char *)&nptr[endbuf - buf]; 152 if (buflen >= FPCONV_G_FMT_BUFSIZE) 153 free(buf); 154 155 return value; 156 } 157 158 /* "fmt" must point to a buffer of at least 6 characters */ 159 static void set_number_format(char *fmt, int precision) 160 { 161 int d1, d2, i; 162 163 assert(1 <= precision && precision <= 16); 164 165 /* Create printf format (%.14g) from precision */ 166 d1 = precision / 10; 167 d2 = precision % 10; 168 fmt[0] = '%'; 169 fmt[1] = '.'; 170 i = 2; 171 if (d1) { 172 fmt[i++] = '0' + d1; 173 } 174 fmt[i++] = '0' + d2; 175 fmt[i++] = 'g'; 176 fmt[i] = 0; 177 } 178 179 /* Assumes there is always at least 32 characters available in the target buffer */ 180 int fpconv_g_fmt(char *str, double num, int precision) 181 { 182 char buf[FPCONV_G_FMT_BUFSIZE]; 183 char fmt[6]; 184 int len; 185 char *b; 186 187 set_number_format(fmt, precision); 188 189 /* Pass through when decimal point character is dot. */ 190 if (locale_decimal_point == '.') 191 return snprintf(str, FPCONV_G_FMT_BUFSIZE, fmt, num); 192 193 /* snprintf() to a buffer then translate for other decimal point characters */ 194 len = snprintf(buf, FPCONV_G_FMT_BUFSIZE, fmt, num); 195 196 /* Copy into target location. Translate decimal point if required */ 197 b = buf; 198 do { 199 *str++ = (*b == locale_decimal_point ? '.' : *b); 200 } while(*b++); 201 202 return len; 203 } 204 205 void fpconv_init(void) 206 { 207 fpconv_update_locale(); 208 } 209 210 /* vi:ai et sw=4 ts=4: 211 */