342 lines
10 KiB
C
342 lines
10 KiB
C
/* Convert string to floating-point number, using the C locale.
|
|
|
|
Copyright (C) 2003-2004, 2006, 2009-2025 Free Software Foundation, Inc.
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
|
|
|
/* Written by Paul Eggert and Bruno Haible. */
|
|
|
|
#include <config.h>
|
|
|
|
#include "c-strtod.h"
|
|
|
|
#include <errno.h>
|
|
#include <locale.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#if FLOAT
|
|
# define C_STRTOD c_strtof
|
|
# define DOUBLE float
|
|
# define STRTOD_L strtof_l
|
|
# define HAVE_GOOD_STRTOD_L (HAVE_STRTOF_L && !GNULIB_defined_strtof_function)
|
|
# define STRTOD strtof
|
|
#elif LONG
|
|
# define C_STRTOD c_strtold
|
|
# define DOUBLE long double
|
|
# define STRTOD_L strtold_l
|
|
# define HAVE_GOOD_STRTOD_L (HAVE_STRTOLD_L && !GNULIB_defined_strtold_function)
|
|
# define STRTOD strtold
|
|
#else
|
|
# define C_STRTOD c_strtod
|
|
# define DOUBLE double
|
|
# define STRTOD_L strtod_l
|
|
# define HAVE_GOOD_STRTOD_L (HAVE_STRTOD_L && !GNULIB_defined_strtod_function)
|
|
# define STRTOD strtod
|
|
#endif
|
|
|
|
/* Here we don't need the newlocale override that supports gl_locale_name(). */
|
|
#undef newlocale
|
|
|
|
#if defined LC_ALL_MASK && (HAVE_GOOD_STRTOD_L || HAVE_WORKING_USELOCALE)
|
|
|
|
/* Cache for the C locale object.
|
|
Marked volatile so that different threads see the same value
|
|
(avoids locking). */
|
|
static volatile locale_t c_locale_cache;
|
|
|
|
/* Return the C locale object, or (locale_t) 0 with errno set
|
|
if it cannot be created. */
|
|
static locale_t
|
|
c_locale (void)
|
|
{
|
|
if (!c_locale_cache)
|
|
c_locale_cache = newlocale (LC_ALL_MASK, "C", (locale_t) 0);
|
|
return c_locale_cache;
|
|
}
|
|
|
|
#else
|
|
|
|
# if HAVE_NL_LANGINFO
|
|
# include <langinfo.h>
|
|
# endif
|
|
# include "c-ctype.h"
|
|
|
|
/* Determine the decimal-point character according to the current locale. */
|
|
static char
|
|
decimal_point_char (void)
|
|
{
|
|
const char *point;
|
|
/* Determine it in a multithread-safe way. We know nl_langinfo is
|
|
multithread-safe on glibc systems and Mac OS X systems, but is not required
|
|
to be multithread-safe by POSIX. sprintf(), however, is multithread-safe.
|
|
localeconv() is rarely multithread-safe. */
|
|
# if HAVE_NL_LANGINFO && (__GLIBC__ || defined __UCLIBC__ || (defined __APPLE__ && defined __MACH__))
|
|
point = nl_langinfo (RADIXCHAR);
|
|
# elif 1
|
|
char pointbuf[5];
|
|
sprintf (pointbuf, "%#.0f", 1.0);
|
|
point = &pointbuf[1];
|
|
# else
|
|
point = localeconv () -> decimal_point;
|
|
# endif
|
|
/* The decimal point is always a single byte: either '.' or ','. */
|
|
return (point[0] != '\0' ? point[0] : '.');
|
|
}
|
|
|
|
#endif
|
|
|
|
DOUBLE
|
|
C_STRTOD (char const *nptr, char **endptr)
|
|
{
|
|
DOUBLE r;
|
|
|
|
#if defined LC_ALL_MASK && (HAVE_GOOD_STRTOD_L || HAVE_WORKING_USELOCALE)
|
|
|
|
locale_t locale = c_locale ();
|
|
if (!locale)
|
|
{
|
|
if (endptr)
|
|
*endptr = (char *) nptr;
|
|
return 0; /* errno is set here */
|
|
}
|
|
|
|
# if HAVE_GOOD_STRTOD_L
|
|
|
|
r = STRTOD_L (nptr, endptr, locale);
|
|
|
|
# else /* HAVE_WORKING_USELOCALE */
|
|
|
|
locale_t old_locale = uselocale (locale);
|
|
if (old_locale == (locale_t)0)
|
|
{
|
|
if (endptr)
|
|
*endptr = (char *) nptr;
|
|
return 0; /* errno is set here */
|
|
}
|
|
|
|
r = STRTOD (nptr, endptr);
|
|
|
|
int saved_errno = errno;
|
|
if (uselocale (old_locale) == (locale_t)0)
|
|
/* We can't switch back to the old locale. The thread is hosed. */
|
|
abort ();
|
|
errno = saved_errno;
|
|
|
|
# endif
|
|
|
|
#else
|
|
|
|
char decimal_point = decimal_point_char ();
|
|
|
|
if (decimal_point == '.')
|
|
/* In this locale, C_STRTOD and STRTOD behave the same way. */
|
|
r = STRTOD (nptr, endptr);
|
|
else
|
|
{
|
|
/* Start and end of the floating-point number. */
|
|
char const *start;
|
|
char const *end;
|
|
/* Position of the decimal point '.' in the floating-point number.
|
|
Either decimal_point_p == NULL or start <= decimal_point_p < end. */
|
|
char const *decimal_point_p = NULL;
|
|
/* Set to true if we encountered decimal_point while parsing. */
|
|
int seen_decimal_point = 0;
|
|
|
|
/* Parse
|
|
1. a sequence of white-space characters,
|
|
2. a subject sequence possibly containing a floating-point number,
|
|
as described in POSIX
|
|
<https://pubs.opengroup.org/onlinepubs/9699919799/functions/strtod.html>.
|
|
*/
|
|
{
|
|
char const *p;
|
|
|
|
p = nptr;
|
|
|
|
/* Parse a sequence of white-space characters. */
|
|
while (*p != '\0' && c_isspace ((unsigned char) *p))
|
|
p++;
|
|
start = p;
|
|
end = p;
|
|
|
|
/* Start to parse a subject sequence. */
|
|
if (*p == '+' || *p == '-')
|
|
p++;
|
|
if (*p == '0')
|
|
{
|
|
end = p + 1;
|
|
if (p[1] == 'x' || p[1] == 'X')
|
|
{
|
|
size_t num_hex_digits = 0;
|
|
p += 2;
|
|
/* Parse a non-empty sequence of hexadecimal digits optionally
|
|
containing the decimal point character '.'. */
|
|
while (*p != '\0')
|
|
{
|
|
if (c_isxdigit ((unsigned char) *p))
|
|
{
|
|
num_hex_digits++;
|
|
p++;
|
|
}
|
|
else if (*p == '.')
|
|
{
|
|
if (decimal_point_p == NULL)
|
|
{
|
|
decimal_point_p = p;
|
|
p++;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
seen_decimal_point = (*p == decimal_point);
|
|
if (num_hex_digits > 0)
|
|
{
|
|
end = p;
|
|
/* Parse the character 'p' or the character 'P', optionally
|
|
followed by a '+' or '-' character, and then followed by
|
|
one or more decimal digits. */
|
|
if (*p == 'p' || *p == 'P')
|
|
{
|
|
p++;
|
|
if (*p == '+' || *p == '-')
|
|
p++;
|
|
if (*p != '\0' && c_isdigit ((unsigned char) *p))
|
|
{
|
|
p++;
|
|
while (*p != '\0' && c_isdigit ((unsigned char) *p))
|
|
p++;
|
|
end = p;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
decimal_point_p = NULL;
|
|
goto done_parsing;
|
|
}
|
|
}
|
|
{
|
|
size_t num_digits = 0;
|
|
/* Parse a non-empty sequence of decimal digits optionally containing
|
|
the decimal point character '.'. */
|
|
while (*p != '\0')
|
|
{
|
|
if (c_isdigit ((unsigned char) *p))
|
|
{
|
|
num_digits++;
|
|
p++;
|
|
}
|
|
else if (*p == '.')
|
|
{
|
|
if (decimal_point_p == NULL)
|
|
{
|
|
decimal_point_p = p;
|
|
p++;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
else
|
|
break;
|
|
}
|
|
seen_decimal_point = (*p == decimal_point);
|
|
if (num_digits > 0)
|
|
{
|
|
end = p;
|
|
/* Parse the character 'e' or the character 'E', optionally
|
|
followed by a '+' or '-' character, and then followed by one or
|
|
more decimal digits. */
|
|
if (*p == 'e' || *p == 'E')
|
|
{
|
|
p++;
|
|
if (*p == '+' || *p == '-')
|
|
p++;
|
|
if (*p != '\0' && c_isdigit ((unsigned char) *p))
|
|
{
|
|
p++;
|
|
while (*p != '\0' && c_isdigit ((unsigned char) *p))
|
|
p++;
|
|
end = p;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
decimal_point_p = NULL;
|
|
}
|
|
}
|
|
done_parsing:
|
|
if (end == start || (decimal_point_p == NULL && !seen_decimal_point))
|
|
/* If end == start, we have not parsed anything. The given string
|
|
might be "INF", "INFINITY", "NAN", "NAN(chars)", or invalid.
|
|
We can pass it to STRTOD.
|
|
If end > start and decimal_point_p == NULL, we have parsed a
|
|
floating-point number, and it does not have a '.' (and not a ','
|
|
either, of course). If !seen_decimal_point, we did not
|
|
encounter decimal_point while parsing. In this case, the
|
|
locale-dependent STRTOD function can handle it. */
|
|
r = STRTOD (nptr, endptr);
|
|
else
|
|
{
|
|
/* Create a modified floating-point number, in which the character '.'
|
|
is replaced with the locale-dependent decimal_point. */
|
|
size_t len = end - start;
|
|
char *buf;
|
|
char *buf_malloced = NULL;
|
|
char stackbuf[1000];
|
|
char *end_in_buf;
|
|
|
|
if (len < sizeof (stackbuf))
|
|
buf = stackbuf;
|
|
else
|
|
{
|
|
buf = malloc (len + 1);
|
|
if (buf == NULL)
|
|
{
|
|
/* Out of memory.
|
|
Callers may not be prepared to see errno == ENOMEM. But
|
|
they should be prepared to see errno == EINVAL. */
|
|
errno = EINVAL;
|
|
if (endptr != NULL)
|
|
*endptr = (char *) nptr;
|
|
return 0;
|
|
}
|
|
buf_malloced = buf;
|
|
}
|
|
|
|
memcpy (buf, start, len);
|
|
if (decimal_point_p != NULL)
|
|
buf[decimal_point_p - start] = decimal_point;
|
|
buf[len] = '\0';
|
|
|
|
r = STRTOD (buf, &end_in_buf);
|
|
if (endptr != NULL)
|
|
*endptr = (char *) (start + (end_in_buf - buf));
|
|
|
|
if (buf_malloced != NULL)
|
|
{
|
|
int saved_errno = errno;
|
|
free (buf_malloced);
|
|
errno = saved_errno;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
return r;
|
|
}
|