Threadsafe locale-independent string to/from double conversions



Here is a patch that adds/fixes locale-independent threadsafe g_strtod() 
and g_dtostr() to glib. These are needed when reading config files etc. 
Using setlocale (LC_NUMERIC, "C") is not threadsafe, so the old code was 
broken.

/ Alex

Index: docs/reference/glib/glib-sections.txt
===================================================================
RCS file: /cvs/gnome/glib/docs/reference/glib/glib-sections.txt,v
retrieving revision 1.46
diff -u -p -r1.46 glib-sections.txt
--- docs/reference/glib/glib-sections.txt	2001/10/01 20:26:10	1.46
+++ docs/reference/glib/glib-sections.txt	2001/10/04 01:43:09
@@ -921,7 +921,11 @@ g_strncasecmp
 
 <SUBSECTION>
 g_strreverse
+
+<SUBSECTION>
+G_DTOSTR_BUF_SIZE
 g_strtod
+g_dtostr
 
 <SUBSECTION>
 g_strchug
Index: docs/reference/glib/tmpl/string_utils.sgml
===================================================================
RCS file: /cvs/gnome/glib/docs/reference/glib/tmpl/string_utils.sgml,v
retrieving revision 1.16
diff -u -p -r1.16 string_utils.sgml
--- docs/reference/glib/tmpl/string_utils.sgml	2001/10/01 22:54:48	1.16
+++ docs/reference/glib/tmpl/string_utils.sgml	2001/10/04 01:43:09
@@ -562,19 +562,44 @@ For example, g_strreverse ("abcdef") wou
 @Returns: 
 
 
+<!-- ##### MACRO G_DTOSTR_BUF_SIZE ##### -->
+<para>
+A good size for a buffer to be passed into <function>g_dtostr</function>.
+It is guaranteed to be enough for all output of that function on systems
+with 64bit IEEE compatible doubles.
+</para>
+<para>
+The typical usage would be something like:
+</para>
+<para>
+<literal>
+  char buf[G_DTOSTR_BUF_SIZE];
+
+  fprintf (out, "value='%s'\n", g_dtostr (value, buf, sizeof (buf)));
+</literal>
+</para>
+
+
+
 <!-- ##### FUNCTION g_strtod ##### -->
+<para>
+
+</para>
+
+ nptr: 
+ endptr: 
+ Returns:
+
+
+<!-- ##### FUNCTION g_dtostr ##### -->
 <para>
-Converts a string to a gdouble value.
-It calls the standard <function>strtod()</function> function
-to handle the conversion, but if the string is not completely converted
-it attempts the conversion again in the "C" locale, and returns the best
-match.
+
 </para>
 
- nptr: the string to convert to a numeric value.
- endptr: if non-NULL, it returns the character after the last character used
-in the conversion.
- Returns: the gdouble value.
+ d: 
+ buffer: 
+ len: 
+ Returns: 
 
 
 <!-- ##### FUNCTION g_strchug ##### -->
Index: glib/gstrfuncs.c
===================================================================
RCS file: /cvs/gnome/glib/glib/gstrfuncs.c,v
retrieving revision 1.76
diff -u -p -r1.76 gstrfuncs.c
--- glib/gstrfuncs.c	2001/10/02 23:09:50	1.76
+++ glib/gstrfuncs.c	2001/10/04 01:43:09
@@ -253,46 +253,212 @@ g_strconcat (const gchar *string1, ...)
   return concat;
 }
 
+/**
+ * g_strtod:
+ * @nptr:    the string to convert to a numeric value.
+ * @endptr:  if non-NULL, it returns the character after
+ *           the last character used in the conversion.
+ * 
+ * Converts a string to a gdouble value.
+ * This function behaves like the standard strtod() function
+ * does in the C locale. It does this without actually
+ * changing the current locale, since that would not be
+ * thread-safe.
+ *
+ * This function is typically used when reading configuration
+ * files or other non-user input that should be locale dependent.
+ * To handle input from the user you should normally use the
+ * locale-sensitive system strtod function.
+ *
+ * To convert from a string to double in a locale-insensitive
+ * way, use @g_dtostr.
+ * 
+ * Return value: the gdouble value.
+ **/
 gdouble
 g_strtod (const gchar *nptr,
-	  gchar **endptr)
+	  gchar      **endptr)
 {
-  gchar *fail_pos_1;
-  gchar *fail_pos_2;
-  gdouble val_1;
-  gdouble val_2 = 0;
+  gchar *fail_pos;
+  gdouble val;
+  struct lconv *locale_data;
+  const char *decimal_point;
+  int decimal_point_len;
+  const char *p, *end, *decimal_point_pos;
 
   g_return_val_if_fail (nptr != NULL, 0);
 
-  fail_pos_1 = NULL;
-  fail_pos_2 = NULL;
+  fail_pos = NULL;
 
-  val_1 = strtod (nptr, &fail_pos_1);
-
-  if (fail_pos_1 && fail_pos_1[0] != 0)
+  locale_data = localeconv ();
+  decimal_point = locale_data->decimal_point;
+  decimal_point_len = strlen (decimal_point);
+
+  g_assert (decimal_point_len != 0);
+  
+  decimal_point_pos = NULL;
+  if (decimal_point[0] != '.' ||
+      decimal_point[1] != 0)
     {
-      gchar *old_locale;
-
-      old_locale = g_strdup (setlocale (LC_NUMERIC, NULL));
-      setlocale (LC_NUMERIC, "C");
-      val_2 = strtod (nptr, &fail_pos_2);
-      setlocale (LC_NUMERIC, old_locale);
-      g_free (old_locale);
+      p = nptr;
+      /* Skip leading space */
+      while (isspace ((guchar)*p))
+	p++;
+      
+      /* Skip leading optional sign */
+      if (*p == '+' || *p == '-')
+	p++;
+      
+      if (p[0] == '0' &&
+	  (p[1] == 'x' || p[1] == 'X'))
+	{
+	  p += 2;
+	  /* HEX - find the (optional) decimal point */
+	  
+	  while (isxdigit ((guchar)*p))
+	    p++;
+	  
+	  if (*p == '.')
+	    {
+	      decimal_point_pos = p++;
+	      
+	      while (isxdigit ((guchar)*p))
+		p++;
+	      
+	      if (*p == 'p' || *p == 'P')
+		p++;
+	      if (*p == '+' || *p == '-')
+		p++;
+	      while (isdigit ((guchar)*p))
+		p++;
+	      end = p;
+	    }
+	}
+      else
+	{
+	  while (isdigit ((guchar)*p))
+	    p++;
+	  
+	  if (*p == '.')
+	    {
+	      decimal_point_pos = p++;
+	      
+	      while (isdigit ((guchar)*p))
+		p++;
+	      
+	      if (*p == 'e' || *p == 'E')
+		p++;
+	      if (*p == '+' || *p == '-')
+		p++;
+	      while (isdigit ((guchar)*p))
+		p++;
+	      end = p;
+	    }
+	}
+      /* For the other cases, we need not convert the decimal point */
     }
-
-  if (!fail_pos_1 || fail_pos_1[0] == 0 || fail_pos_1 >= fail_pos_2)
+  
+  if (decimal_point_pos)
     {
-      if (endptr)
-	*endptr = fail_pos_1;
-      return val_1;
+      char *copy, *c;
+
+      /* We need to convert the '.' to the locale specific decimal point */
+      copy = g_malloc (end - nptr + 1 + decimal_point_len);
+      
+      c = copy;
+      memcpy (c, nptr, decimal_point_pos - nptr);
+      c += decimal_point_pos - nptr;
+      memcpy (c, decimal_point, decimal_point_len);
+      c += decimal_point_len;
+      memcpy (c, decimal_point_pos + 1, end - (decimal_point_pos + 1));
+      c += end - (decimal_point_pos + 1);
+      *c = 0;
+      
+      val = strtod (copy, &fail_pos);
+      
+      fail_pos = (char *)nptr + (fail_pos - copy);
+      
+      g_free (copy);
+	  
     }
   else
+    val = strtod (nptr, &fail_pos);
+
+  if (endptr)
+    *endptr = fail_pos;
+  
+  return val;
+}
+
+/**
+ * g_dtostr:
+ * @d:      The double to convert
+ * @buffer: A buffer to place the resulting string in
+ * @len:    The length of the buffer.
+ *
+ * Converts a double to a string, using the format of
+ * %g in the C locale.
+ *
+ * It generates enough precision that converting the string
+ * back using @g_strtod gives the same machine-number, at
+ * least on machines with IEEE compatible 64bit doubles.
+ * (This does depend on the quality of the system strtod and
+ * printf implementations.)
+ *
+ * A good size to use for the buffer is @G_DTOSTR_BUF_SIZE.
+ * The result will never be larger than that.
+ *
+ * Return value: The pointer to the buffer with the converted string.
+ **/
+gchar *
+g_dtostr (gdouble  d,
+	  gchar   *buffer,
+	  gint     len)
+{
+  struct lconv *locale_data;
+  const char *decimal_point;
+  int decimal_point_len;
+  gchar *p;
+  int rest_len;
+
+  g_return_val_if_fail (buffer != NULL, NULL);
+
+  g_snprintf (buffer, len, "%.17g", d);
+
+  locale_data = localeconv ();
+  decimal_point = locale_data->decimal_point;
+  decimal_point_len = strlen (decimal_point);
+
+  g_assert (decimal_point_len != 0);
+
+  if (decimal_point[0] != '.' ||
+      decimal_point[1] != 0)
     {
-      if (endptr)
-	*endptr = fail_pos_2;
-      return val_2;
+      p = buffer;
+
+      if (*p == '+' || *p == '-')
+	p++;
+
+      while (isdigit ((guchar)*p))
+	p++;
+
+      if (strncmp (p, decimal_point, decimal_point_len) == 0)
+	{
+	  *p = '.';
+	  p++;
+	  if (decimal_point_len > 1) {
+	    rest_len = strlen (p + (decimal_point_len-1));
+	    memmove (p, p + (decimal_point_len-1),
+		     rest_len);
+	    p[rest_len] = 0;
+	    
+	  }
+	}
     }
+  
+  return buffer;
 }
+
 
 G_CONST_RETURN gchar*
 g_strerror (gint errnum)
Index: glib/gstrfuncs.h
===================================================================
RCS file: /cvs/gnome/glib/glib/gstrfuncs.h,v
retrieving revision 1.16
diff -u -p -r1.16 gstrfuncs.h
--- glib/gstrfuncs.h	2001/09/29 09:42:19	1.16
+++ glib/gstrfuncs.h	2001/10/04 01:43:09
@@ -93,13 +93,11 @@ gint                  g_ascii_xdigit_val
  */
 #define	 G_STR_DELIMITERS	"_-|> <."
 gchar*	              g_strdelimit     (gchar	     *string,
-					const gchar *delimiters,
+					const gchar  *delimiters,
 					gchar	      new_delimiter);
-gchar*	              g_strcanon       (gchar       *string,
-					const gchar *valid_chars,
-					gchar        substitutor);
-gdouble	              g_strtod	       (const gchar *nptr,
-					gchar	    **endptr);
+gchar*	              g_strcanon       (gchar        *string,
+					const gchar  *valid_chars,
+					gchar         substitutor);
 G_CONST_RETURN gchar* g_strerror       (gint	      errnum) G_GNUC_CONST;
 G_CONST_RETURN gchar* g_strsignal      (gint	      signum) G_GNUC_CONST;
 gchar*	              g_strreverse     (gchar	     *string);
@@ -117,6 +115,18 @@ gchar *               g_strrstr        (
 gchar *               g_strrstr_len    (const gchar  *haystack,
 					gssize        haystack_len,
 					const gchar  *needle);
+
+/* String to/from double conversion functions */
+
+/* 29 bytes should enough for all possible values that
+ * g_dtostr can produce. Then add 10 for good measure */
+#define G_DTOSTR_BUF_SIZE (29 + 10)
+gdouble	              g_strtod	       (const gchar  *nptr,
+					gchar	    **endptr);
+gchar *               g_dtostr         (gdouble       d,
+					gchar        *buffer,
+					gint          len);
+
 
 /* removes leading spaces */
 gchar*                g_strchug        (gchar        *string);
Index: tests/Makefile.am
===================================================================
RCS file: /cvs/gnome/glib/tests/Makefile.am,v
retrieving revision 1.43
diff -u -p -r1.43 Makefile.am
--- tests/Makefile.am	2001/09/03 22:13:16	1.43
+++ tests/Makefile.am	2001/10/04 01:43:09
@@ -71,6 +71,7 @@ test_programs =					\
 	spawn-test				\
 	strfunc-test				\
 	string-test				\
+	strtod-test				\
 	thread-test				\
 	threadpool-test				\
 	tree-test				\
@@ -113,6 +114,7 @@ slist_test_LDADD = $(progs_LDADD)
 spawn_test_LDADD = $(progs_LDADD)
 strfunc_test_LDADD = $(progs_LDADD)
 string_test_LDADD = $(progs_LDADD)
+strtod_test_LDADD = $(progs_LDADD) -lm
 thread_test_LDADD = $(thread_LDADD)
 threadpool_test_LDADD = $(thread_LDADD)
 tree_test_LDADD = $(progs_LDADD)
--- /dev/null	Thu Aug 30 16:30:55 2001
+++ tests/strtod-test.c	Wed Oct  3 21:36:55 2001
@@ -0,0 +1,50 @@
+#include <glib.h>
+#include <locale.h>
+#include <string.h>
+#include <math.h>
+
+void
+test_string (char *number, double res)
+{
+  gdouble d;
+  char *locales[] = {"sv_SE", "en_US", "fa_IR", "C"};
+  int l;
+
+  for (l = 0; l < G_N_ELEMENTS (locales); l++)
+    {
+      setlocale (LC_NUMERIC, locales[l]);
+      d = g_strtod (number, NULL);
+      if (d != res)
+	g_print ("g_strtod for locale %s failed\n", locales[l]);
+    }
+}
+
+
+int 
+main ()
+{
+  gdouble d;
+  char buffer[G_DTOSTR_BUF_SIZE];
+
+  test_string ("123.123", 123.123);
+  test_string ("123.123e2", 123.123e2);
+  test_string ("123.123e-2", 123.123e-2);
+  test_string ("-123.123", -123.123);
+  test_string ("-123.123e2", -123.123e2);
+  test_string ("-123.123e-2", -123.123e-2);
+  
+  d = 179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0;
+  g_assert (d == g_strtod (g_dtostr (d, buffer, sizeof (buffer)), NULL));
+
+  d = -179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368.0;
+  g_assert (d == g_strtod (g_dtostr (d, buffer, sizeof (buffer)), NULL));
+  
+  d = pow (2.0, -1024.1);
+  g_assert (d == g_strtod (g_dtostr (d, buffer, sizeof (buffer)), NULL));
+  
+  d = -pow (2.0, -1024.1);
+  g_assert (d == g_strtod (g_dtostr (d, buffer, sizeof (buffer)), NULL));
+  
+
+  return 0;
+}





[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]