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. */
43static 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. */
52static 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. */
75static 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. */
94static 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 */
106double 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 */
153static 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 */
174int 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
199void fpconv_init()
200{
201 fpconv_update_locale();
202}
203
204/* vi:ai et sw=4 ts=4:
205 */
206