diff options
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 @@ | |||
1 | #include <stdio.h> | ||
2 | #include <stdlib.h> | ||
3 | #include <assert.h> | ||
4 | #include <string.h> | ||
5 | |||
6 | #include "fpconv.h" | ||
7 | |||
8 | static char locale_decimal_point = '.'; | ||
9 | |||
10 | /* In theory multibyte decimal_points are possible, but | ||
11 | * Lua CJSON only supports UTF-8 and known locales only have | ||
12 | * single byte decimal points ([.,]). | ||
13 | * | ||
14 | * localconv() may not be thread safe, and nl_langinfo() is not | ||
15 | * supported on some platforms. Use sprintf() instead. */ | ||
16 | void fpconv_update_locale() | ||
17 | { | ||
18 | char buf[8]; | ||
19 | |||
20 | snprintf(buf, sizeof(buf), "%g", 0.5); | ||
21 | |||
22 | /* Failing this test might imply the platform has a buggy dtoa | ||
23 | * implementation or wide characters */ | ||
24 | if (buf[0] != '0' || buf[2] != '5' || buf[3] != 0) { | ||
25 | fprintf(stderr, "Error: wide characters found or printf() bug."); | ||
26 | abort(); | ||
27 | } | ||
28 | |||
29 | locale_decimal_point = buf[1]; | ||
30 | } | ||
31 | |||
32 | /* Check for a valid number character: [-+0-9a-fA-FpPxX.] | ||
33 | * It doesn't matter if actual invalid characters are counted - strtod() | ||
34 | * will find the valid number if it exists. The risk is that slightly more | ||
35 | * memory might be allocated before a parse error occurs. */ | ||
36 | static int valid_number_character(char ch) | ||
37 | { | ||
38 | char lower_ch; | ||
39 | |||
40 | if ('0' <= ch && ch <= '9') | ||
41 | return 1; | ||
42 | if (ch == '-' || ch == '+' || ch == '.') | ||
43 | return 1; | ||
44 | |||
45 | /* Hex digits, exponent (e), base (p), "infinity",.. | ||
46 | * The main purpose is to not include a "comma". If any other invalid | ||
47 | * characters are included, the will only generate a parse error later. */ | ||
48 | lower_ch = ch | 0x20; | ||
49 | if ('a' <= lower_ch && lower_ch <= 'y') | ||
50 | return 1; | ||
51 | |||
52 | return 0; | ||
53 | } | ||
54 | |||
55 | /* Calculate the size of the buffer required for a locale | ||
56 | * conversion. Returns 0 if conversion is not required */ | ||
57 | static int strtod_buffer_size(const char *s) | ||
58 | { | ||
59 | const char *p = s; | ||
60 | |||
61 | while (valid_number_character(*p)) | ||
62 | p++; | ||
63 | |||
64 | return p - s; | ||
65 | } | ||
66 | |||
67 | /* Similar to strtod(), but must be passed the current locale's decimal point | ||
68 | * character. Guaranteed to be called at the start of any valid number in a string */ | ||
69 | double fpconv_strtod(const char *nptr, char **endptr) | ||
70 | { | ||
71 | char *num, *endnum, *dp; | ||
72 | int numlen; | ||
73 | double value; | ||
74 | |||
75 | /* System strtod() is fine when decimal point is '.' */ | ||
76 | if (locale_decimal_point == '.') | ||
77 | return strtod(nptr, endptr); | ||
78 | |||
79 | numlen = strtod_buffer_size(nptr); | ||
80 | if (!numlen) { | ||
81 | /* No valid characters found, standard strtod() return */ | ||
82 | *endptr = (char *)nptr; | ||
83 | return 0; | ||
84 | } | ||
85 | |||
86 | /* Duplicate number into buffer */ | ||
87 | num = malloc(numlen + 1); | ||
88 | if (!num) { | ||
89 | fprintf(stderr, "Out of memory"); | ||
90 | abort(); | ||
91 | } | ||
92 | memcpy(num, nptr, numlen); | ||
93 | num[numlen] = 0; | ||
94 | |||
95 | /* Update decimal point character if found */ | ||
96 | dp = strchr(num, '.'); | ||
97 | if (dp) | ||
98 | *dp = locale_decimal_point; | ||
99 | |||
100 | value = strtod(num, &endnum); | ||
101 | *endptr = (char *)&nptr[endnum - num]; | ||
102 | free(num); | ||
103 | |||
104 | return value; | ||
105 | } | ||
106 | |||
107 | /* "fmt" must point to a buffer of at least 6 characters */ | ||
108 | static void set_number_format(char *fmt, int precision) | ||
109 | { | ||
110 | int d1, d2, i; | ||
111 | |||
112 | assert(1 <= precision && precision <= 14); | ||
113 | |||
114 | /* Create printf format (%.14g) from precision */ | ||
115 | d1 = precision / 10; | ||
116 | d2 = precision % 10; | ||
117 | fmt[0] = '%'; | ||
118 | fmt[1] = '.'; | ||
119 | i = 2; | ||
120 | if (d1) { | ||
121 | fmt[i++] = '0' + d1; | ||
122 | } | ||
123 | fmt[i++] = '0' + d2; | ||
124 | fmt[i++] = 'g'; | ||
125 | fmt[i++] = 0; | ||
126 | } | ||
127 | |||
128 | /* Assumes there is always at least 32 characters available in the target buffer */ | ||
129 | int fpconv_g_fmt(char *str, double num, int precision) | ||
130 | { | ||
131 | char buf[FPCONV_G_FMT_BUFSIZE]; | ||
132 | char fmt[6]; | ||
133 | int len; | ||
134 | char *b; | ||
135 | |||
136 | set_number_format(fmt, precision); | ||
137 | |||
138 | /* Pass through when decimal point character is dot. */ | ||
139 | if (locale_decimal_point == '.') | ||
140 | return snprintf(str, FPCONV_G_FMT_BUFSIZE, fmt, num); | ||
141 | |||
142 | /* snprintf() to a buffer then translate for other decimal point characters */ | ||
143 | len = snprintf(buf, FPCONV_G_FMT_BUFSIZE, fmt, num); | ||
144 | |||
145 | /* Returned 'len' includes the null terminator */ | ||
146 | b = buf; | ||
147 | do { | ||
148 | *str++ = (*b == locale_decimal_point ? '.' : *b); | ||
149 | } while(*b++); | ||
150 | |||
151 | return len; | ||
152 | } | ||
153 | |||
154 | /* vi:ai et sw=4 ts=4: | ||
155 | */ | ||