1 | /* fpconv - Floating point conversion routines |
2 | * |
3 | * Copyright (c) 2011-2012 Mark Pulford <[email protected]> |
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 | /* Lua CJSON assumes the locale is the same for all threads within a |
39 | * process and doesn't change after initialisation. |
40 | * |
41 | * This avoids the need for per thread storage or expensive checks |
42 | * for call. */ |
43 | static char locale_decimal_point = '.'; |
44 | |
45 | /* In theory multibyte decimal_points are possible, but |
46 | * Lua CJSON only supports UTF-8 and known locales only have |
47 | * single byte decimal points ([.,]). |
48 | * |
49 | * localconv() may not be thread safe (=>crash), and nl_langinfo() is |
50 | * not supported on some platforms. Use sprintf() instead - if the |
51 | * locale does change, at least Lua CJSON won't crash. */ |
52 | static void fpconv_update_locale() |
53 | { |
54 | char buf[8]; |
55 | |
56 | snprintf(buf, sizeof(buf), "%g" , 0.5); |
57 | |
58 | /* Failing this test might imply the platform has a buggy dtoa |
59 | * implementation or wide characters */ |
60 | if (buf[0] != '0' || buf[2] != '5' || buf[3] != 0) { |
61 | fprintf(stderr, "Error: wide characters found or printf() bug." ); |
62 | abort(); |
63 | } |
64 | |
65 | locale_decimal_point = buf[1]; |
66 | } |
67 | |
68 | /* Check for a valid number character: [-+0-9a-yA-Y.] |
69 | * Eg: -0.6e+5, infinity, 0xF0.F0pF0 |
70 | * |
71 | * Used to find the probable end of a number. It doesn't matter if |
72 | * invalid characters are counted - strtod() will find the valid |
73 | * number if it exists. The risk is that slightly more memory might |
74 | * be allocated before a parse error occurs. */ |
75 | static inline int valid_number_character(char ch) |
76 | { |
77 | char lower_ch; |
78 | |
79 | if ('0' <= ch && ch <= '9') |
80 | return 1; |
81 | if (ch == '-' || ch == '+' || ch == '.') |
82 | return 1; |
83 | |
84 | /* Hex digits, exponent (e), base (p), "infinity",.. */ |
85 | lower_ch = ch | 0x20; |
86 | if ('a' <= lower_ch && lower_ch <= 'y') |
87 | return 1; |
88 | |
89 | return 0; |
90 | } |
91 | |
92 | /* Calculate the size of the buffer required for a strtod locale |
93 | * conversion. */ |
94 | static int strtod_buffer_size(const char *s) |
95 | { |
96 | const char *p = s; |
97 | |
98 | while (valid_number_character(*p)) |
99 | p++; |
100 | |
101 | return p - s; |
102 | } |
103 | |
104 | /* Similar to strtod(), but must be passed the current locale's decimal point |
105 | * character. Guaranteed to be called at the start of any valid number in a string */ |
106 | double fpconv_strtod(const char *nptr, char **endptr) |
107 | { |
108 | char localbuf[FPCONV_G_FMT_BUFSIZE]; |
109 | char *buf, *endbuf, *dp; |
110 | int buflen; |
111 | double value; |
112 | |
113 | /* System strtod() is fine when decimal point is '.' */ |
114 | if (locale_decimal_point == '.') |
115 | return strtod(nptr, endptr); |
116 | |
117 | buflen = strtod_buffer_size(nptr); |
118 | if (!buflen) { |
119 | /* No valid characters found, standard strtod() return */ |
120 | *endptr = (char *)nptr; |
121 | return 0; |
122 | } |
123 | |
124 | /* Duplicate number into buffer */ |
125 | if (buflen >= FPCONV_G_FMT_BUFSIZE) { |
126 | /* Handle unusually large numbers */ |
127 | buf = malloc(buflen + 1); |
128 | if (!buf) { |
129 | fprintf(stderr, "Out of memory" ); |
130 | abort(); |
131 | } |
132 | } else { |
133 | /* This is the common case.. */ |
134 | buf = localbuf; |
135 | } |
136 | memcpy(buf, nptr, buflen); |
137 | buf[buflen] = 0; |
138 | |
139 | /* Update decimal point character if found */ |
140 | dp = strchr(buf, '.'); |
141 | if (dp) |
142 | *dp = locale_decimal_point; |
143 | |
144 | value = strtod(buf, &endbuf); |
145 | *endptr = (char *)&nptr[endbuf - buf]; |
146 | if (buflen >= FPCONV_G_FMT_BUFSIZE) |
147 | free(buf); |
148 | |
149 | return value; |
150 | } |
151 | |
152 | /* "fmt" must point to a buffer of at least 6 characters */ |
153 | static void set_number_format(char *fmt, int precision) |
154 | { |
155 | int d1, d2, i; |
156 | |
157 | assert(1 <= precision && precision <= 14); |
158 | |
159 | /* Create printf format (%.14g) from precision */ |
160 | d1 = precision / 10; |
161 | d2 = precision % 10; |
162 | fmt[0] = '%'; |
163 | fmt[1] = '.'; |
164 | i = 2; |
165 | if (d1) { |
166 | fmt[i++] = '0' + d1; |
167 | } |
168 | fmt[i++] = '0' + d2; |
169 | fmt[i++] = 'g'; |
170 | fmt[i] = 0; |
171 | } |
172 | |
173 | /* Assumes there is always at least 32 characters available in the target buffer */ |
174 | int fpconv_g_fmt(char *str, double num, int precision) |
175 | { |
176 | char buf[FPCONV_G_FMT_BUFSIZE]; |
177 | char fmt[6]; |
178 | int len; |
179 | char *b; |
180 | |
181 | set_number_format(fmt, precision); |
182 | |
183 | /* Pass through when decimal point character is dot. */ |
184 | if (locale_decimal_point == '.') |
185 | return snprintf(str, FPCONV_G_FMT_BUFSIZE, fmt, num); |
186 | |
187 | /* snprintf() to a buffer then translate for other decimal point characters */ |
188 | len = snprintf(buf, FPCONV_G_FMT_BUFSIZE, fmt, num); |
189 | |
190 | /* Copy into target location. Translate decimal point if required */ |
191 | b = buf; |
192 | do { |
193 | *str++ = (*b == locale_decimal_point ? '.' : *b); |
194 | } while(*b++); |
195 | |
196 | return len; |
197 | } |
198 | |
199 | void fpconv_init() |
200 | { |
201 | fpconv_update_locale(); |
202 | } |
203 | |
204 | /* vi:ai et sw=4 ts=4: |
205 | */ |
206 | |