【问题标题】:How to manually parse a floating point number from a string如何从字符串中手动解析浮点数
【发布时间】:2010-09-10 06:10:45
【问题描述】:

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

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

查找和处理单个数字很容易,但是如何将它们组合成 floatdouble 类型的值而不损失精度?

我正在考虑将整数部分乘以 10^n,其中 n 是小数部分的位数,然后将小数部分添加到整数部分并从指数中减去 n。例如,这有效地将4.2e1 转换为42e0。然后我可以使用pow 函数计算 10^exponent 并将结果与​​新的整数部分相乘。问题是,这种方法能保证始终保持最大精度吗?

对此有什么想法吗?

【问题讨论】:

    标签: parsing floating-point precision


    【解决方案1】:

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

    【讨论】:

    • 解析不是问题,是结果浮点数的构造给我带来了麻烦。
    【解决方案2】:

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

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

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

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

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

        现在你可以组合你的浮点数了。首先要做的是扫描数字的整数表示以查找第一个设置的一位(从最高到最低)。

        紧跟第一个位的位是尾数。

        获取指数也不难。您知道科学记数法中的第一位、小数点的位置和可选的指数。结合它们并添加浮点指数偏差(我认为是 127,但请查看一些参考资料)。

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

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

        最高有效位只是符号。 1 表示负数,0 表示正数。

        描述起来比实际更难,试着分解一个浮点数,看看指数和尾数,你会发现它真的很容易。

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

        【讨论】:

        • @Nils:您忽略了舍入模式等。看看 strtod 以了解什么是必要的。
        • 是的,我知道。我还遗漏了更多内容,例如处理非正规和零。但在我看来,最初的海报是为了学习目的而不是为了生产。
        • 部分正确。我想从一个字符串中读取一个浮点数,但是字符串里面还有其他的东西。 Java 无法处理。但是由于这个问题非常困难,我将解析浮点数,将其放入字符串中,然后将其扔到 Float.parseFloat() ;)
        • 此描述忘记了 IEEE-754 指数是二进制指数,因此必须将尾数相乘:1e2 => 1010b => 1.01e11b。当然,你不能天真地做到这一点,这需要一个 1024 位的数字,你需要通过长乘法来做到这一点。体面的浮点解析实现使用以 5 为底的 bignum。
        • @JonHarrop 如果累积几个舍入误差,则没有双精度是不够的,您可以将 1011.1000 转换为 1011.0111,然后舍入到 1011。而不是绑定到最接近的甚至 1100。您需要足够的精度来保持精确的中间值结果直到最后的除法或乘法,除非你能确保你的最终结果离平局足够远......
        【解决方案5】:

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

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

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

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

        无论如何,祝你好运!

        【讨论】:

          【解决方案6】:

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

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

          【讨论】:

            【解决方案7】:

            所有其他答案都错过了正确执行此操作的困难。您可以在此采用第一种方法,这在一定程度上是准确的,但在您考虑 IEEE 舍入模式(等)之前,您永远不会得到正确的答案。我之前写过幼稚的实现,但有相当多的错误。

            如果您不害怕数学,我强烈建议您阅读以下 David Goldberg 的文章,What Every Computer Scientist Should Know About Floating-Point Arithmetic。您将更好地了解幕后发生的事情,以及这些位为何如此布局。

            我最好的建议是从一个有效的 atoi 实施开始,然后从那里搬出去。您会很快发现自己遗漏了一些东西,但只要看看strtod 的源代码,您就会走上正确的道路(这是一条漫长而漫长的道路)。最终你会称赞 insert diey here 有标准库。

            /* use this to start your atof implementation */
            
            /* atoi - christopher.watford@gmail.com */
            /* 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);
            }
            

            【讨论】:

            【解决方案8】:

            将十进制数转换为最佳浮点近似值的“标准”算法是 William Clinger 的 How to read floating point numbers accurately,可从 here 下载。请注意,正确地执行此操作需要多精度整数,至少在一定百分比的时间内,才能处理极端情况。

            在 Burger 和 Dybvig 的 Printing Floating-Point Numbers Quickly and Accurately,可下载 here 中找到另一种方式的算法,从浮点数中打印最佳十进制数。这也需要多精度整数运算

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

            【讨论】:

            【解决方案9】:

            我同意总站。状态机是完成这项任务的最佳方式,因为有许多愚蠢的方式可以破坏解析器。我现在正在做一个,我认为它已经完成了,我认为它有 13 个州。

            问题不小。

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

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

            第 18 页提供了一些有趣的测试用例。

            是的,我读过 Clinger 的文章,但作为一个头脑简单的硬件工程师,我无法理解所提供的代码。 Knuth 的文章中提到的 Steele 算法对我很有帮助。输入输出都有问题。

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

            我还没有在这里注册,但是当我注册时,假设没有登录,那就是兄弟。 (兄弟点)。

            克莱德

            【讨论】:

              【解决方案10】:

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

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

              【讨论】:

              • 是的,还不错……但是 p*2^q 总是近似于 10 的负幂,对吧?取前 18 位也是近似值(例如,0.001 的精确值已经有 58 个十进制数字,不考虑前导零)。通过两次不精确的操作,我想我总能得出一个倒霉的数字,它会落在平局的另一边,从而被错误地四舍五入。罕见但并非不存在。即使您将长度限制为 18 位,最终舍入 128->53 位也是另一个不精确的操作,太多了...
              【解决方案11】:

              是的,你可以将构造分解成浮点运算只要这些运算是EXACT,并且你能负担得起一个单个最终不精确操作。

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

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

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

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

              如果整数尾数有很多位,则在累积整数尾数时以及将 10 提高到 biasedExponent 的幂时,都存在超过精度的风险......

              幸运的是,如果前两个运算是精确的,那么您可以承受最终的不精确运算 * 或 /,感谢 IEEE 属性,结果将被正确舍入。

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

              10^8 > 2^24 > 10^7
              

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

              5^11 > 2^24 > 5^10
              

              不过,整数尾数的精度可以达到 7 位,biasedExponent 介于 -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 更加优化。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2018-06-13
                • 1970-01-01
                • 2015-03-15
                • 2021-06-30
                • 2017-09-30
                • 1970-01-01
                • 2013-02-27
                相关资源
                最近更新 更多