如何手动解析字符串中的浮点数

How to manually parse a floating point number from a string

提问人:Thomas 提问时间:9/18/2008 更新时间:11/18/2018 访问量:20582

问:

当然,大多数语言都有库函数,但假设我想自己做。

假设浮点数像在 C 或 Java 程序中一样给出(除了“f”或“d”后缀),例如“”、“”或简称为“”。一般来说,我们有小数点前的“整数部分”,小数点后的“小数部分”和“指数”。这三个都是整数。4.2e1.42e242

查找和处理单个数字很容易,但是如何在不损失精度的情况下将它们组合成浮点型或双精度型的值呢?

我正在考虑将整数部分乘以 10^n,其中 n 是小数部分的位数,然后将小数部分添加到整数部分并从指数中减去 n例如,这实际上变成了 。然后,我可以使用该函数计算 10^ 指数并将结果与新的整数部分相乘。问题是,这种方法是否保证了整个过程的最大精度?4.2e142e0pow

对此有什么想法吗?

解析 浮点 精度

评论


答:

-1赞 terminus 9/18/2008 #1

使用状态机。这很容易做到,甚至在数据流中断时也能工作(你只需要保留状态和部分结果)。您还可以使用解析器生成器(如果您正在执行更复杂的事情)。

评论

1赞 Thomas 9/18/2008
解析不是问题,而是生成的浮点数的构造给我带来了麻烦。
0赞 Jorge Ferreira 9/18/2008 #2

为此,您必须了解标准 IEEE 754 才能获得正确的二进制表示。之后,您可以使用 Float.intBitsToFloat 或 Double.longBitsToDouble

http://en.wikipedia.org/wiki/IEEE_754

0赞 Adam Rosenfield 9/18/2008 #3

如果想要尽可能精确的结果,则应使用更高的内部工作精度,然后将结果下变频到所需的精度。如果您不介意一些 ULP 的误差,那么您可以根据需要以所需的精度重复乘以 10。我会避免使用 pow() 函数,因为它会为大型指数产生不精确的结果。

10赞 Nils Pipenbrinck 9/18/2008 #4

我将使用浮点数的二进制表示直接组装浮点数。

一个接一个地读入数字,首先找到所有数字。在整数算术中执行此操作。还要跟踪小数点和指数。这个稍后会很重要。

现在,您可以组合浮点数。首先要做的是扫描数字的整数表示形式,以查找第一个集合的位(从高到低)。

紧跟在第一个 1 位之后的位是你的尾数。

获得指数也不难。您知道科学记数法中的第一个 1 位位置、小数点的位置和可选指数。将它们组合起来并添加浮点指数偏差(我认为是 127,但请检查一些参考)。

该指数应在 0 到 255 的范围内。如果它更大或更小,则有一个正数或负数的无限数(特殊情况)。

将指数存储到浮点数的 24 到 30 位中。

最重要的一点就是符号。一表示负数,零表示正数。

它比实际情况更难描述,试着分解一个浮点数,看看指数和尾数,你就会发现它真的是多么容易。

顺便说一句 - 在浮点本身进行算术运算是一个坏主意,因为您总是会强制尾数被截断为 23 位有效位。这样你就不会得到一个确切的表示。

评论

0赞 user7116 9/18/2008
@Nils:你忽略了舍入模式等。看看 strtod 以了解必要的内容。
0赞 Nils Pipenbrinck 9/18/2008
是的,我明白。我遗漏了更多,比如处理非正态和零。但在我看来,最初的海报是出于学习目的,而不是为了生产。
0赞 Thomas 9/18/2008
部分正确。我想从字符串中读取浮点数,但是字符串内还有其他东西。Java 无法处理这个问题。但是由于这个问题变得非常困难,我将解析浮点数,将其放入字符串中,然后将其扔到 Float.parseFloat() ;)
11赞 Simon Buchan 7/26/2010
此描述忘记了 IEEE-754 指数是二进制指数,因此尾数必须相乘: => => 。当然,你不能天真地这样做,那需要一个 1024 位的数字,你需要通过长乘法来做到这一点。体面的浮点解析实现使用 base-5 bignum 来做到这一点。1e21010b1.01e11b
1赞 aka.nice 3/27/2013
@JonHarrop没有双精度是不够的,如果累积几个舍入误差,您可以将 1011.1000 变成 1011.0111,然后四舍五入到 1011。而不是与最近的 1100 平局。你需要足够的精度来保持精确的中间结果,直到最终的除法或乘法,除非你能确保你的最终结果离平局足够远......
2赞 billjamesdev 9/18/2008 #5

解析时可以忽略小数点(位置除外)。假设输入是: 156.7834e10...这可以很容易地解析为整数1567834后跟 e10,然后将其修改为 e6,因为小数点距浮点数的“数字”部分末尾是 4 位数字。

精度是一个问题。您需要检查您正在使用的语言的 IEEE 规范。如果尾数(或分数)中的位数大于整数类型中的位数,则当有人键入以下数字时,您可能会失去精度:

5123.123123e0 - 在我们的方法中转换为 5123123123,它不适合整数,但 5.123123123 的位可能适合浮点规格的尾数。

当然,您可以使用一种方法,将每个数字放在小数点前,将当前总数(在浮点数中)乘以 10,然后添加新数字。对于小数点后的数字,将数字乘以 10 的增进方,然后再添加到当前总数。然而,这种方法似乎回避了为什么要这样做的问题,因为它需要使用浮点原语而不使用现成的解析库。

无论如何,祝你好运!

0赞 sk. 9/18/2008 #6

不可能在不损失精度的情况下将任何表示数字的任意字符串转换为双精度或浮点数。有许多小数可以精确地用十进制表示(例如“0.1”),只能用二进制浮点数或双精度数来近似。这类似于分数 1/3 不能精确地用十进制表示,你只能写 0.333333...

如果您不想直接使用库函数,为什么不查看这些库函数的源代码呢?你提到了 Java;大多数 JDK 都附带了类库的源代码,因此您可以查找 java.lang.Double.parseDouble(String) 方法的工作原理。当然,像 BigDecimal 这样的东西更适合控制精度和舍入模式,但你说它需要是浮点数或双精度数。

26赞 user7116 9/18/2008 #7

所有其他答案都忽略了正确地做到这一点是多么困难。你可以在这一点上做一个初步切割的方法,这在一定程度上是准确的,但除非你考虑到IEEE舍入模式(等),否则你永远不会得到正确的答案。我以前写过幼稚的实现,有相当多的错误。

如果你不害怕数学,我强烈建议你阅读大卫·戈德堡(David Goldberg)的以下文章,《每个计算机科学家都应该知道的浮点运算》。您将更好地了解引擎盖下发生的事情,以及为什么这些位是这样布置的。

我最好的建议是从有效的 atoi 实现开始,然后从那里开始。你很快就会发现你遗漏了一些东西,但只要看几眼strtod的来源,你就会走上正确的道路(这是一条很长很长的路)。最终,你会称赞这里插入饮食,那里有标准库。

/* use this to start your atof implementation */

/* atoi - [email protected] */
/* PUBLIC DOMAIN */
long atoi(const char *value) {
  unsigned long ival = 0, c, n = 1, i = 0, oval;
  for( ; c = value[i]; ++i) /* chomp leading spaces */
    if(!isspace(c)) break;
  if(c == '-' || c == '+') { /* chomp sign */
    n = (c != '-' ? n : -1);
    i++;
  }
  while(c = value[i++]) { /* parse number */
    if(!isdigit(c)) return 0;
    ival = (ival * 10) + (c - '0'); /* mult/accum */
    if((n > 0 && ival > LONG_MAX)
    || (n < 0 && ival > (LONG_MAX + 1UL))) {
      /* report overflow/underflow */
      errno = ERANGE;
      return (n > 0 ? LONG_MAX : LONG_MIN);
    }
  }
  return (n>0 ? (long)ival : -(long)ival);
}

评论

3赞 R.. GitHub STOP HELPING ICE 7/14/2011
Overflow 调用 UB;事后你无法检测到它。要么使用无符号类型,要么在执行可能溢出的算术运算之前进行测试。
0赞 Caesar 3/11/2021
看起来太阳已经落在那个链接上。存档:web.archive.org/web/20080406035949/http://docs.sun.com/source/...
22赞 Peter S. Housel 9/30/2008 #8

将十进制数转换为最佳浮点近似值的“标准”算法是 William Clinger 的《如何准确读取浮点数》,可从此处下载。请注意,要正确执行此操作,至少需要一定百分比的多精度整数,以便处理极端情况。

另一种方式的算法,从浮点数打印出最佳十进制数,可以在 Burger 和 Dybvig 的快速准确地打印浮点数中找到,可在此处下载。这也需要多精度整数算术

另请参阅 David M Gay 的 Correctly Rounded Binary-Decimal 和 Decimal-Binary Conconversions,了解双向算法。

评论

0赞 J D 6/29/2012
“正确地做到这一点需要多精度整数”。为什么?
4赞 flaviut 5/16/2014
对于那些不能打扰谷歌的人的PDF:cesura17.net/~will/professional/research/papers/howtoread.pdf
0赞 Nicholas Obert 8/23/2023
不幸的是,howtoread 链接指向一个空白页
-1赞 Clyde R. Shappee 8/8/2009 #9

我同意terminus。状态机是完成此任务的最佳方式,因为解析器可以通过许多愚蠢的方式被破坏。我现在正在研究一个,我认为它已经完成,我认为它有 13 个状态。

问题不是微不足道的。

我是一名硬件工程师,对设计浮点硬件感兴趣。我正在进行第二次实施。

我今天发现这个 http://speleotrove.com/decimal/decarith.pdf

第 18 页给出了一些有趣的测试用例。

是的,我已经阅读了 Clinger 的文章,但作为一个头脑简单的硬件工程师,我无法理解所呈现的代码。在Knuth的文本中引用了Steele的算法,这对我很有帮助。输入和输出都有问题。

上述所有对各种文章的引用都非常出色。

我还没有在这里注册,但是当我注册时,假设登录没有被占用,它将是broh。(broh-dot)。

克莱德

1赞 J D 6/29/2012 #10

我的第一个想法是仅使用尾数的前 18 位数字将字符串解析为尾数和十进制指数。例如,1.2345e-5 将被解析为 12345 和 -9。然后,我会继续将尾数乘以 10 并递减指数,直到尾数长度为 18 位(精度为 >56 位)。然后,我会在表格中查找十进制指数,以找到一个因子和二进制指数,可用于将数字从十进制 n*10^m 转换为二进制 p*2^q 形式。这个因素是另一个因素,所以我将尾数乘以它,这样我就得到了得到的 128 位数字的前 64 位。这个尾数可以被投射到一个浮点数上,只损失必要的精度,而 2^q 指数可以使用乘法来应用,而不会损失精度。int64intint64int64

我希望这非常准确且非常快,但您可能还想处理特殊数字 NaN、-infinity、-0.0 和 infinity。我没有考虑过非规范化数字或舍入模式。

评论

1赞 aka.nice 3/27/2013
是的,没那么糟糕......但是 p*2^q 总是近似于 10 的负幂,对吧?取前 18 个数字也是近似值(例如,0.001 的精确值已经包含 58 位小数位,不考虑前导零)。通过两次不精确的操作,我想我总是可以制作一个不吉利的数字,它会落在领带的另一边,从而被错误地舍入。罕见但并非不存在。即使您将长度限制为 18 位,最终舍入 128->53 位也是另一个不精确的运算,这太多了......
2赞 aka.nice 7/29/2012 #11

是的只要这些运算是精确的,您就可以将构造分解为浮点运算,并且您可以承受单个最终的不精确运算。

不幸的是,浮点运算很快就会变得不精确,当您超过尾数的精度时,结果就会四舍五入。一旦引入了舍入“误差”,它将在进一步的操作中累积......
所以,一般来说,,你不能使用这种幼稚的算法来转换任意小数,这可能会导致一个错误的四舍五入数字,就像其他人已经告诉你的那样,相差几个正确的ulp。

但是,让我们看看我们能走多远:

如果你像这样仔细地重建浮点数:

if(biasedExponent >= 0)
    return integerMantissa * (10^biasedExponent);
else
    return integerMantissa / (10^(-biasedExponent));

在累加整数尾数(如果它有很多位数)时,以及将 10 提高到偏岔指数的幂时,都存在超过精度的风险......

幸运的是,如果前两个操作是精确的,那么你可以承受最终的不精确操作 * 或 /,由于 IEEE 属性,结果将被正确舍入。

让我们将其应用于精度为 24 位的单精度浮点数。

10^8 > 2^24 > 10^7

请注意,2 的倍数只会增加指数并使尾数保持不变,我们只需要处理 5 的幂以获得 10 的幂:

5^11 > 2^24 > 5^10

不过,您可以获得整数尾数中的 7 位精度和 -10 到 10 之间的有偏差指数。

在双精度 53 位中,

10^16 > 2^53 > 10^15
5^23 > 2^53 > 5^22

因此,您可以获得 15 位十进制数字,以及 -22 和 22 之间的偏置指数。

由你来决定你的数字是否总是在正确的范围内......(如果你真的很棘手,你可以通过插入/删除尾零来平衡尾数和指数)。

否则,您将不得不使用一些扩展精度。
如果你的语言提供了任意精度的整数,那么要把它做好有点棘手,但并不难,我在 Smalltalk 中做到了这一点,并在 http://smallissimo.blogspot.fr/2011/09/clarifying-and-optimizing.htmlhttp://smallissimo.blogspot.fr/2011/09/reviewing-fraction-asfloat.html 上写了一篇关于它的博客

请注意,这些都是简单而朴素的实现。幸运的是,libc 更加优化。