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