独立于区域设置的 strtod 实现

问题描述 投票:0回答:6

我有一个库需要解析总是使用点“.”的双数。作为小数分隔符。不幸的是,对于这种情况,strtod() 尊重可能使用不同分隔符的语言环境,因此解析可能会失败。我不能 setlocale() - 它不是线程安全的。所以我现在正在寻找一个干净的独立于语言环境的 strtod 实现。到目前为止,我已经找到了多种实现方式,但它们看起来都很老套,或者就像是糟糕的代码。 有人可以为我推荐一个经过良好测试、有效、干净的 (ANSI) C 实现吗?

c string locale
6个回答
3
投票

获取一些已知的实现(不依赖于

atof
),例如与 ruby 一起分发的实现: ruby_1_8/缺失/strtod.c.


3
投票

警告:建议的 ruby 实现包含错误。 我不介意 gavin 指出的微小差异,但是如果您尝试解析类似“0.00000000000000000000000000000000000783475”之类的内容,您将得到 0.0 而不是 7.834750e-37(例如股票 strtod() 返回。)

其他解决方案:

#include <sstream>
#include "strtod_locale_independent.h"

extern "C" double strtod_locale_independent(const char* s)
{
     std::istringstream text( s );
     text.imbue(std::locale::classic());
     double result;
     text >> result;
     return result;
}

虽然我不知道这有多快。


2
投票

按照上面的回答,我尝试使用 ruby_1_8/missing/strtod.c 中的 Ruby 实现。但是,对于某些输入,这会在 Mac 和 Linux 平台上对 gcc 的内置解析器和 stdlib.h 中的 strtod 给出不同的答案:

char * endptr ;
double value1 = 1.15507e-173 ;
double value2 = strtod( "1.15507e-173", &endptr ) ;
double value3 = test_strtod( "1.15507e-173", &endptr ) ;
assert( sizeof( double ) == sizeof( unsigned long ) ) ;
printf( "value1 = %lg, 0x%lx.\n", value1, *(unsigned long*)( &value1 ) ) ;
printf( "value2 = %lg, 0x%lx.\n", value2, *(unsigned long*)( &value2 ) ) ;
printf( "value3 = %lg, 0x%lx.\n", value2, *(unsigned long*)( &value3 ) ) ;
assert( value1 == value2 ) ;
assert( value1 == value3 ) ;

打印

value1 = 1.15507e-173, 0x1c06dace8bda0ee0.
value2 = 1.15507e-173, 0x1c06dace8bda0ee0.
value3 = 1.15507e-173, 0x1c06dace8bda0edf.
Assertion failed: (value1 == value3), function main, file main.c, line 16.

所以我的建议是在使用前测试选择的实现。


1
投票

netlib 上还有 gdtoa,BSD 样式许可证:http://www.netlib.org/fp/gdtoa.tgz


0
投票

虽然这已经很晚了,但它可能会为未来的读者提供一些见解。按照上面 Florian Kusche 的回复模式,这里是一个专门的“C”替代方案,我已经在各种 Linux 版本上成功测试了它。此解决方案的要求之一是在执行

strtod()
.

之前临时覆盖用户的系统区域设置
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define _LOCALE_N_ 13

int category[_LOCALE_N_] =
  {
   LC_ALL           , //   All of the locale
   LC_ADDRESS       , //   Formatting of addresses and geography-related items (*)
   LC_COLLATE       , //   String collation
   LC_CTYPE         , //   Character classification
   LC_IDENTIFICATION, //   Metadata describing the locale (*)
   LC_MEASUREMENT   , //   Settings related to measurements (metric versus US customary) (*)
   LC_MESSAGES      , //   Localizable natural-language messages
   LC_MONETARY      , //   Formatting of monetary values
   LC_NAME          , //   Formatting of salutations for persons (*)
   LC_NUMERIC       , //   Formatting of nonmonetary numeric values
   LC_PAPER         , //   Settings related to the standard paper size (*)
   LC_TELEPHONE     , //   Formats to be used with telephone services (*)
   LC_TIME            //   Formatting of date and time values
  };

void _store_locale_info(char *vals[_LOCALE_N_])
{
  /* store the current locale information in an array of strings for future use */

  int i;

  for (i=0; i<_LOCALE_N_; i++)
  {
    char *loc_str = setlocale(category[i], "");
    int L = strlen(loc_str);
    vals[i] = calloc(L+1, sizeof(char));
    strncpy(vals[i], setlocale(category[i], ""), L+1);
  }
}

void _restore_locale_info(char *vals[_LOCALE_N_])
{
  /* restore the locale information from a previosly-populated array of strings */

  int i;

  for (i=0; i<_LOCALE_N_; i++)
  {
    if (vals[i])
    {
      setlocale(category[i], vals[i]);
      free(vals[i]);
    }
  }
}

double _strtod_c (const char *string, char **endPtr)
{
  /* Wrapper function for strtod() that enforces the "C" locale before converting a floating-point
   * number from an ASCII decimal representation to internal double-precision format. */

  char *vals[_LOCALE_N_];
  double rval = 0;
  _store_locale_info(vals);
  rval = strtod(string, endPtr);
  _restore_locale_info(vals);
  return rval;
}

int main()
{
  char *str = "1024.123456";
  char **endPtr;
  char locale_str[100];

  printf("\nstr = \"%s\"\n\n", str);
  printf("Locale\n", str);

  strcpy(locale_str, setlocale(LC_ALL, "C"));
  printf("%-6s :: strtod(str, endPtr) = %.15g\n", locale_str, strtod(str, endPtr));

  strcpy(locale_str, setlocale(LC_ALL, "de_DE"));
  printf("%-6s :: strtod(str, endPtr) = %.15g\n", locale_str, strtod(str, endPtr));

  printf("---\n");

  strcpy(locale_str, setlocale(LC_ALL, "C"));
  printf("%-6s :: _strtod_c(str, endPtr) = %.15g\n", locale_str, _strtod_c(str, endPtr));

  strcpy(locale_str, setlocale(LC_ALL, "de_DE"));
  printf("%-6s :: _strtod_c(str, endPtr) = %.15g\n", locale_str, _strtod_c(str, endPtr));

  printf("\n");
}

兼容 Linux 安装的预期输出是


str = "1024.123456"

Locale
C      :: strtod(str, endPtr) = 1024.123456
de_DE  :: strtod(str, endPtr) = 1024
---
C      :: _strtod_c(str, endPtr) = 1024.123456
de_DE  :: _strtod_c(str, endPtr) = 1024.123456


0
投票

由于应用程序是多线程的,因此您无法更改区域设置,并且如果您希望获得精确的行为,重新实现

strtod
是一项艰巨的任务,这里有一个适用于大多数情况的简单替代方案:

#include <locale.h>
#include <stdlib.h>
#include <string.h>

double my_strtod(const char *s, char **endp) {
    char buf[1024];
    char *p = strchr(s, '.');
    if (p == NULL || (size_t)(p - s) >= sizeof(buf)) {
        return strtod(s, endp);
    }
    struct lconv *lp = localeconv();
    *buf = '\0';
    strncat(buf, s, sizeof(buf) - 1);
    buf[p - s] = *lp->decimal_point;
    double v = strtod(buf, &p);
    if (endp) {
        *endp = s + (p - buf);
    }
    return v;
}

如果

my_strtod()
用于非常长的字符串,分析参数字符串的初始部分以确定要从中复制多少个字符可能更有效。

如果您在仅在当前线程中使用的可修改数组上使用

my_strtod()
,您可以暂时将第一个句点
.
替换为当前语言环境中的小数点字符:

#include <locale.h>
#include <stdlib.h>
#include <string.h>

double my_strtod(char *s, char **endp) {
    char *p = strchr(s, '.');
    if (p != NULL) {
        struct lconv *lp = localeconv();
        *p = *lp->decimal_point;
    }
    double v = strtod(buf, endp);
    if (p != NULL) {
        *p = '.';
    }
    return v;
}

当然,这种方法使用

strtod()
所以它假设其他并发线程不会与当前语言环境混淆。

© www.soinside.com 2019 - 2024. All rights reserved.