1 /* gtkdataformat - data formatter
2 * Copyright 2011 Fredy Paquet <fredy@opag.ch>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public
15 * License along with this library; if not, write to the
16 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17 * Boston, MA 02111-1307, USA.
18 */
19
20 /* References
21 https://en.wikipedia.org/wiki/Decimal_mark
22 https://en.wikipedia.org/wiki/Indian_numbering_system
23 */
24
25 #include <stdlib.h>
26 #include <stdio.h>
27 #include <string.h>
28 #include <math.h>
29 #include <locale.h>
30 #include <gtk/gtk.h>
31
32 #define __GTKEXTRA_H_INSIDE__
33
34 #include "gtkextra-compat.h"
35 #include "gtkdataformat.h"
36
37 #ifdef DEBUG
38 # define GTK_DATA_FORMAT_DEBUG 0 /* define to activate debug output */
39 #endif
40
41 /**
42 * SECTION: gtkdataformat
43 * @short_description: a data formatting library
44 *
45 * the widget property 'dataformat' may contain formatting
46 * instructions for the field contents. Any unrecognized
47 * formatting instruction is silently skipped.
48 *
49 * The formatting process should always be reversible. Thus
50 * formatting can be applied when input focus leaves a field and
51 * removed again when the focus enters a field, without the need
52 * of an additional content buffer.
53 *
54 * the library can be easily extended by adding more
55 * instructions to the list above.
56 *
57 */
58
59 #define DEFAULT_DECIMAL_POINT "." /* default radix char */
60 #define DEFAULT_THOUSANDS_SEP "'" /* default thousands grouping char */
61 #define DEFAULT_GROUPING "\3" /* default grouping */
62
63 #define MAX_NUM_STRLEN 64
64 #define NULL_TEXT_REP ""
65 #define INVALID_DATA "?"
66
67 #define SIGNIFICANT_DIGITS 16
68
69 /* Cached locale data */
70 static gchar *radix_str = NULL;
71 static gchar *thousands_c = NULL;
72 static guchar *grouping = NULL;
73
_cache_localedata_utf8(gboolean recheck)74 static void _cache_localedata_utf8(gboolean recheck)
75 {
76 if (radix_str && ! recheck) return;
77
78 struct lconv *lc = localeconv();
79 GError *err = NULL;
80
81 gchar *r = (lc && lc->decimal_point) ?
82 lc->decimal_point : DEFAULT_DECIMAL_POINT;
83
84 if (radix_str) { g_free(radix_str); radix_str = NULL; }
85 radix_str = g_locale_to_utf8(r, strlen(r), NULL, NULL, &err);
86
87 if (!radix_str && err) {
88 g_warning("_get_localedata_utf8: failed to convert decimal_point <%s> to UTF8", r);
89 radix_str = g_strdup(r);
90 }
91
92 gchar *tc = (lc && lc->thousands_sep) ?
93 lc->thousands_sep : DEFAULT_THOUSANDS_SEP;
94
95 if (thousands_c) { g_free(thousands_c); thousands_c = NULL; }
96 thousands_c = g_locale_to_utf8(tc, strlen(r), NULL, NULL, &err);
97
98 if (!thousands_c && err) {
99 g_warning("_get_localedata_utf8: failed to convert thousands_setp <%s> to UTF8", tc);
100 thousands_c = g_strdup(tc);
101 }
102
103 guchar *gp = (guchar *) (
104 (lc && lc->grouping && lc->grouping[0]) ?
105 lc->grouping : DEFAULT_GROUPING);
106
107 if (grouping) { g_free(grouping); grouping = NULL; }
108 grouping = (guchar *) g_strdup((gchar *) gp);
109
110 #if GTK_DATA_FORMAT_DEBUG>0
111 g_debug("_cache_localedata_utf8: <%s> <%s>", radix_str, thousands_c);
112 #endif
113 }
114
insert_thousands_seps(const gchar * cp)115 static gchar *insert_thousands_seps(const gchar *cp)
116 {
117 static gchar buf[MAX_NUM_STRLEN];
118 gchar *radix_cp, c, *dst;
119 const gchar *src;
120 gint pos; /* position of radix_str */
121 gint tpos; /* position of next thousands_sep */
122
123 _cache_localedata_utf8(FALSE);
124
125 gint thousands_len = strlen(thousands_c);
126 guchar *grp_ptr = grouping;
127 gint grp_size = *grp_ptr++;
128 gint len = strlen(cp);
129
130 if (len == 0) return("");
131
132 radix_cp = strstr(cp, radix_str);
133 if (radix_cp)
134 pos = (radix_cp - cp) - len;
135 else
136 pos = 0;
137
138 tpos = grp_size;
139 if (*grp_ptr) grp_size = *grp_ptr++;
140
141 #if GTK_DATA_FORMAT_DEBUG>0
142 g_debug("its: start grp_size %d pos %d cp <%s>",
143 grp_size, pos, cp);
144 #endif
145
146 /* reverse copy, inserting thousands_c on the fly */
147 src = &cp[len]; /* also copy terminator '\0' */
148 dst = &buf[MAX_NUM_STRLEN-1]; /* end of buffer */
149
150 for (;(src >= cp) && (dst > buf);pos++) {
151 c = *dst-- = *src--;
152
153 #if GTK_DATA_FORMAT_DEBUG>0
154 g_debug("its: loop grp_size %d pos %d c %c", grp_size, pos, c);
155 g_debug("its: dst = <%s>", dst+1);
156 #endif
157 if ((pos > 0) /* left of radix_str */
158 && (pos == tpos) /* position at grouping */
159 && (src >= cp) /* not at beginning of number */
160 && (*src != '-') && (*src != '+') /* skip sign */
161 )
162 {
163 /* note: use unterminated copy */
164 strncpy(dst - thousands_len + 1, thousands_c, thousands_len);
165 dst -= thousands_len;
166
167 tpos += grp_size;
168 if (*grp_ptr) grp_size = *grp_ptr++;
169 }
170 }
171 #if GTK_DATA_FORMAT_DEBUG>0
172 g_debug("its: result = <%s>", dst+1);
173 #endif
174 return(dst+1);
175 }
176
remove_thousands_seps(const gchar * src)177 static gchar *remove_thousands_seps(const gchar *src)
178 {
179 static gchar buf[MAX_NUM_STRLEN];
180 gchar *dst = buf;
181 gboolean found=FALSE;
182 gint i=0, l = strlen(src);
183
184 _cache_localedata_utf8(FALSE);
185
186 gint thousands_len = strlen(thousands_c);
187
188 if (!src) return((gchar *) src);
189
190 if ((l > 1) && (src[l-1] == '-')) /* handle trailing minus sign */
191 {
192 if (src[0] == '-')
193 {
194 ++i;
195 --l;
196 }
197 else
198 {
199 *dst++ = '-';
200 --l;
201 }
202 found=TRUE;
203 }
204
205 while (i<l)
206 {
207 if ((src[i] == thousands_c[0])
208 && (strncmp(&src[i], thousands_c, thousands_len) == 0))
209 {
210 i += thousands_len;
211 found=TRUE;
212 }
213 else
214 *dst++ = src[i++]; /* beware: minor risc to hit a UTF-8 radix_str */
215 }
216 *dst = '\0';
217
218 if (found) return(buf);
219 return((gchar *) src);
220 }
221
format_double(gdouble d,gint comma_digits,gboolean do_numseps)222 static gchar *format_double(gdouble d,
223 gint comma_digits, gboolean do_numseps)
224 {
225 static gchar str_buf[MAX_NUM_STRLEN], *cp;
226
227 if (comma_digits >= 0)
228 sprintf(str_buf, "%.*f", comma_digits, d);
229 else
230 sprintf(str_buf, "%.*g", SIGNIFICANT_DIGITS, d);
231
232 cp = str_buf;
233
234 if (do_numseps) cp = insert_thousands_seps(str_buf);
235
236 return(cp);
237 }
238
format_int(gint i,gint num_bytes)239 static gchar *format_int(gint i, gint num_bytes)
240 {
241 static gchar str_buf[MAX_NUM_STRLEN];
242 sprintf(str_buf, "%d", i);
243 return(str_buf);
244 }
245
246
247 /**
248 * gtk_data_format:
249 * @str: the string to be formatted
250 * @dataformat: formatting instructions
251 *
252 * format @str according to @dataformat.
253 *
254 * formatting instructions:
255 *
256 * '' (the empty string) does no formatting at all.
257 *
258 * 'int8' is formatted as a singed 8-bit integer value with
259 * optional '-' sign.
260 *
261 * 'int16' is formatted as a signed 16-bit integer with optional
262 * '-' sign.
263 *
264 * 'int32' is formatted as a signed 32-bit integer with optional
265 * '-' sign.
266 *
267 * 'money' is formatted as a double float value with 2 decimal
268 * digits and 1000s-separators
269 *
270 * 'float,N' is formatted as a double float value with N decimal
271 * digits and 1000s-separators
272 *
273 * 'bit' is formatted as a boolean value [0,1].
274 *
275 *
276 * Returns: a pointer to an internal static buffer, with the
277 * formatted data
278 */
gtk_data_format(const gchar * str,const gchar * dataformat)279 gchar *gtk_data_format(const gchar *str, const gchar *dataformat)
280 {
281 if (!str || !str[0] || !dataformat || !dataformat[0]) return((gchar *) str);
282
283 switch (dataformat[0])
284 {
285 case 'i':
286 if (strcmp(dataformat, "int8") == 0)
287 {
288 gint i;
289 str = remove_thousands_seps(str);
290 if (sscanf(str, "%d", &i) == 1) return(format_int(i, 1));
291 return(INVALID_DATA);
292 }
293 else if (strcmp(dataformat, "int16") == 0)
294 {
295 gint i;
296 str = remove_thousands_seps(str);
297 if (sscanf(str, "%d", &i) == 1) return(format_int(i, 2));
298 return(INVALID_DATA);
299 }
300 else if (strcmp(dataformat, "int32") == 0)
301 {
302 gint i;
303 str = remove_thousands_seps(str);
304 if (sscanf(str, "%d", &i) == 1) return(format_int(i, 4));
305 return(INVALID_DATA);
306 }
307 break;
308
309 case 'm':
310 if (strcmp(dataformat, "money") == 0)
311 {
312 gdouble d;
313
314 str = remove_thousands_seps(str);
315
316 if (sscanf(str, "%lg", &d) == 1)
317 return(format_double(d, 2, TRUE));
318
319 return(INVALID_DATA);
320 }
321 break;
322
323 case 'f':
324 if (strncmp(dataformat, "float,", 6) == 0)
325 {
326 gint precision;
327
328 if (sscanf(&dataformat[6], "%d", &precision) == 1)
329 {
330 gdouble d;
331
332 str = remove_thousands_seps(str);
333
334 if (sscanf(str, "%lg", &d) == 1)
335 return(format_double(d, precision, TRUE));
336
337 return(INVALID_DATA);
338 }
339 }
340 break;
341
342 case 'b':
343 if (strcmp(dataformat, "bit") == 0)
344 {
345 if (strcmp(str, "1") == 0) return(format_int(1, 1));
346 else if (strcmp(str, "0") == 0) return(format_int(0, 1));
347 else if (strcmp(str, "true") == 0) return(format_int(1, 1));
348 else if (strcmp(str, "false") == 0) return(format_int(0, 1));
349 return(INVALID_DATA);
350 }
351 break;
352
353 default: break;
354 }
355 return((gchar *) str);
356 }
357
358 /**
359 * gtk_data_format_remove:
360 * @str: the string to be unformatted
361 * @dataformat: formatting instructions
362 *
363 * reverse the effect of #gtk_data_format, i.e. remove all
364 * formatting characters, apply trailing dash
365 *
366 * Returns: a pointer to an internal static buffer, with the
367 * unformatted data
368 */
gtk_data_format_remove(const gchar * str,const gchar * dataformat)369 gchar *gtk_data_format_remove(const gchar *str, const gchar *dataformat)
370 {
371 if (!str || !dataformat || !dataformat[0]) return((gchar *) str);
372
373 switch (dataformat[0])
374 {
375 case 'i':
376 if (strcmp(dataformat, "int8") == 0)
377 {
378 str = remove_thousands_seps(str);
379 }
380 else if (strcmp(dataformat, "int16") == 0)
381 {
382 str = remove_thousands_seps(str);
383 }
384 else if (strcmp(dataformat, "int32") == 0)
385 {
386 str = remove_thousands_seps(str);
387 }
388 break;
389
390 case 'm':
391 if (strcmp(dataformat, "money") == 0)
392 {
393 str = remove_thousands_seps(str);
394 }
395 break;
396
397 case 'f':
398 if (strncmp(dataformat, "float,", 6) == 0)
399 {
400 gint precision;
401
402 if (sscanf(&dataformat[6], "%d", &precision) == 1)
403 {
404 str = remove_thousands_seps(str);
405 }
406 }
407 break;
408
409 default: break;
410 }
411 return((gchar *) str);
412 }
413
414