【问题标题】:How do libraries/programming languages convert floats to strings库/编程语言如何将浮点数转换为字符串
【发布时间】:2019-10-17 22:06:11
【问题描述】:

这是我 15 岁时试图弄清楚的一个谜,但我失败了。我还是不知道答案。

这是一个幼稚且有缺陷的解决方案(就像我在 Stack Overflow 上看到的其他一些失败的尝试一样):

const numberToString = number => {
  let result = '';
  let multiplier = Math.floor(Math.log10(number));
  while (number > 0) {
    const currentDigit = Math.floor(number / 10 ** multiplier);
    if (multiplier === -1) result += '.';
    result += `${currentDigit}`;
    number -= 10 ** multiplier * currentDigit;
    multiplier -= 1;
  }

  if (multiplier >= 0) {
    result += Array(multiplier + 1)
      .fill('0')
      .join('');
  }
  return result;
};

numberToString(0.3) //.29999999999999998010382707025852380980776467160900842259699366886095386217478302201335914442574948883370288946713085380211028267974348864228883494754227105763273602317743416839701366257194448416238466245093684421946526875873398794558223163136792877759774069929483218021428696258138483228158055137040848084556063610493291767

这里的语言是 Javascript,但问题与语言无关。但是,如果可能,请随时改进现有代码。

如果它的工作方式取决于语言,我会很感激一些见解,这在各种编程语言中可能看起来如何,例如 Javascript。

【问题讨论】:

  • 很难将此问题视为与语言无关,因为每种语言对数字的处理方式略有不同。
  • 感谢您的反馈,我已经澄清了我的问题
  • 你检查this了吗?它定义了 Number.toString() 的行为。
  • 我只是觉得这很有趣并且与您的问题相关。它本身并不能回答你的问题,所以我把它作为评论。
  • 注意:float 是许多语言中的数据类型,通常指的是 IEEE-754 单精度浮点格式(又名 Single)。 JavaScript 使用 IEEE-754 双精度浮点格式(又名 Double)。

标签: javascript string algorithm type-conversion programming-languages


【解决方案1】:

(我没有足够的声誉来发表评论,所以我求助于使用答案...)

我注意到您的精度达到了 300 多位,远远超出了浮点数的精度,因此结果不精确。如果您正在寻找一种进行高精度计算的方法,您可能会求助于 BigInt,并相应地扩大数字。 (我说“可能”,因为 BigInt 可以强制转换为固定精度计算,而不是浮点数,因此根据目标,BigInt 可能无法满足要求。)

例如,计算 1000 / 17 到 100 位有效数字可以通过以下函数处理,该函数本质上是按比例放大 1000 和 17 以确保 100 个有效数字。 (请注意,这只是一个用于处理两个整数之间的高精度除法的概念函数,但可以通过扩大 dividenddivisor 直到它们'是整数,并相应地调整 digits。另外,您可能需要捏造一些额外的“隐藏”精度数字来处理舍入)...

function divideN(dividend, divisor, digits) {
  dividend = dividend * 10n ** (BigInt(digits) * 2n);
  divisor = divisor * 10n ** BigInt(digits);
  var s = (dividend/divisor).toString();
  if (s.length < digits) {
    s = "0".repeat(digits - s.length) + s;
  }
  s = s.slice(0, s.length - digits) + "." + s.slice(-digits);
  return s;
}

BigInt 要求数字以“n”结尾,所以该函数需要调用如下...

divideN(1000n,17n,100)

...在这种情况下返回...

"58.8235294117647058823529411764705882352941176470588235294117647058823529411764705882352941176470588235"

请注意,在这种情况下,由于被除数 (1000) 与除数 (17) 的相对大小,将返回 102 位精度而不是 100。

【讨论】:

    【解决方案2】:

    我不是 JAVASCRIPT 编码员,所以我坚持使用 C++ ...

    十进制基数中将数字转换为字符串比使用二进制或其幂基数(bin、oct、hex)更复杂,因为普通计算机上的所有数字都以二进制而不是十年。如果您转换整数或小数部分,它也不相同。假设我们有数字 x 并且想要在 ASCII 中编码的字符串 s,所以这就是基本转换的工作原理:

    1. 处理sign

      s="+";
      if (x<0.0)  { x=-x; s="-"; }
      

      你可以看到它很容易。一些数字格式有一个单独的符号位(通常是 msb 位),因此在这种情况下,代码可以转换为位操作,例如 32 位 float:

      DWORD* dw=(DWORD*)(&x); // allow bit manipulation
      s="+";
      s[0]+=(((*dw)>>30)&2);  // ASCII +,- codes are 2 apart
      (*dw)&=0x7FFFFFFF;      // x=abs(x)
      

      所以我们为我们的字符串提取了符号字符并使x无符号。

    2. 处理x的整数部分

      整数通过被打印基数转换为字符串,所以:

      y=floor(x); // integer part
      if (y)
       for (;y;) // until number is nonzero
       {
       s+='0'+(y%10); // works only for up to 10 base
       y/=10;
       }
      else s+='0'; // handle y=0 separately 
      

      所以每个除法的其余部分是字符串的所需数字,但顺序相反。因此,转换后通过单个 for 循环反转字符串中的数字,或者您可以直接以相反的顺序存储数字。但是对于 tat,您需要知道数字的整数部分的位数。这是由

      digits = ceil(log(y)/log(base)) + 1
      

      所以对于十进制:

      digits = ceil(log10(y)) + 1
      
    3. 处理x的小数部分

      这是通过乘以转换基数来转换的。

      z=x-floor(x); // fractional part
      if (z)
       for (s+='.';z;) // until number is nonzero here you can limit to number of digits
       {
       z*=10.0;
       s+='0'+int(floor(z)); // works only for up to 10 base
       z-=floor(z);
       }
      

      这是按顺序返回数字,所以这次没有反转......

    我直接在 SO 编辑器中对所有代码进行了编码,因此可能存在隐藏的语法错误。

    现在通常的打印函数也具有添加零或空格填充或截断高于某个值等的小数位的格式...

    如果你有一个 bignum x,那么这会慢得多,因为你不能像 O(1) 那样处理基本的 +,-,*,/ 操作,而且创建 hex 字符串并将字符串转换为 decadic on 通常更快8位算术或使用适合存储bignum的已用DATA WORD的10的最大幂。 hex -&gt; dec 转换可以这样完成:

    但是对于非常大的字符串,它会很慢。在这种情况下,可以通过使用类似于 Schönhage-Strassen 乘法FFT/NTT 方法来加快速度,但我之前从未尝试将其用于打印,因此我对这种方法。

    还要注意,对于数字的小数部分,确定值的位数是不规则的(请参阅上面的链接),因此您需要注意,您可能会偏离1-2 位数。

    [Edit1] 对字符串进行四舍五入

    如果您在小数部分(在任何非零数字之后)检测到n 后续零或九,您需要停止打印并舍入。零只是被切掉,而你也需要切掉九,并将字符串中的其余部分加一。此类操作可能会溢出到字符串中不存在的 1 位数字,因此在这种情况下只需插入 1

    当我把所有东西放在一起时,我想出了这个 C++/VCL 代码(基于 VCL AnsiString 数据类型):

    AnsiString print(double x)
        {
        char c;
        int i,j;
        double y,a;
        AnsiString s;
    
        const int B=10;                 // chose base 2...16
        const double b=B;               // base
        const double _b=1.0/b;          // 1/base
        const char digit[16]="0123456789ABCDEF";
    
        #define _enable_rounding
    
        #ifdef _enable_rounding
        const int round_digits=5;       // min consequent 0s or B-1s to triger rounding
        int cnt0=0,cnt1=0;              // consequent digit counters
        int ena=0;                      // enabled consequent digit counters? after first nonzero digit
        #endif
    
        // here you should handle NaN and Inf cases
    
        // handle sign
        s="+";
        if (x<0.0)  { x=-x; s="-"; }
        // integer part
        y=floor(x);
        if (y) for (;y>0.0;)        // until number is nonzero
            {
            a=y; y=floor(y*_b);     // the same as y/=10 on integers
            a-=y*b;                 // the same as a=y%10 on integers
            i=int(a);
            s+=digit[i];
            #ifdef _enable_rounding
            ena|=i;
            #endif
            }
        else s+='0';                // handle y=0 separately
        // reverse string skipping +/- sign (beware AnsiString is indexed from 1 up to its length included!!!)
        for (i=2,j=s.Length();i<j;i++,j--){ c=s[i]; s[i]=s[j]; s[j]=c; }
        // fractional part
        y=x-floor(x);
        if (y) for (s+='.';y>0.0;)      // until number is nonzero here you can limit to number of digits
            {
            y*=b;
            a=floor(y);
            y-=a;
            i=int(a);
            s+=digit[i];
            #ifdef _enable_rounding
            ena|=i;
            // detect consequent rounding digits
            if (ena)
                {
                     if (i==  0){ cnt0++; cnt1=0; }
                else if (i==B-1){ cnt1++; cnt0=0; }
                else            { cnt0=0; cnt1=0; }
                }
            // round down .???00000000 by cut of zeros
            if (cnt0>=round_digits)
                {
                s=s.SubString(1,s.Length()-cnt0);       // by cut of zeros
                break;
                }
            // round up .???999999999 by increment and cut of zeros (only base 10) !!!
            if (cnt1>=round_digits)
                {
                s=s.SubString(1,s.Length()-cnt1);       // cut off nines
                for (j=1,i=s.Length();(i>=2)&&(j);i--)
                    {
                    c=s[i];
                    if (c=='.') continue;
                    if (c=='9'){ s[i]='0'; continue; }
                    j=0; s[i]++;
                    }
                if (j) s=s.Insert("1",i+1); // overflow -> insert "1" after sign
                if (s[s.Length()]=='.')     // cut off decimal point if no fractional part left
                 s=s.SubString(1,s.Length()-1);
                break;
                }
            #endif
            }
        return s;
        }
    

    您可以选择基址B=&lt;2,16&gt;。您可以通过使用/注释#define _enable_rounding 来启用禁用舍入。请注意,舍入例程仅适用于基数 10,因为对于不同的基数,增量例程将有一些不同的代码/常量,并且懒得通用(它会更长且更难理解的代码)。 round_digits 常量是触发舍入的后续 0 或 9 的阈值。

    【讨论】:

    • 如果我正确地按照你的解释,它会不会和我的例子有同样的缺陷?浮点运算的问题,其中0.33-0.03=0.3000000004
    • 或者在你的情况下 3.3 - 3=0.2999999999999
    • @Karamell 是的,因为 0.3 不能用二进制中的小数部分表示……但是您可以在格式化结果时对结果字符串进行四舍五入,甚至可以检测类似模式的上溢/下溢:How to deal with overflow and underflow? .所以如果你的问题是四舍五入你应该在你的问题中提到它我在那里什么都看不到....通常的四舍五入方案是将转换后的余数与0.5或到有限数量的小数位数进行比较检测字符串末尾的9999 模式...
    • 是的,我的问题是关于不丢失十进制表示的精度,这就是为什么我通过展示我的尝试来介绍我的问题,该尝试没有准确地对数字进行字符串化。我不确定我是否理解我们如何通过检测99999 准确地知道存在错误,因为这可能首先出现在数字中
    • @Karamell 你并没有因为打印而失去精度......你很久以前就失去了它,因为 0.3 不能用 2 个数字/权重的幂的总和来表示,所以你打印的数字真的是 @ 987654354@ 如果最后一位数字是 9s(除了少数可能更大的最后一位数字),则舍入很简单,将整个字符串增加 1 和最后一位 9 的位置,并削减结果零。这必须在没有更多浮点的字符串上完成...
    猜你喜欢
    • 2011-11-25
    • 2016-01-30
    • 1970-01-01
    • 2018-02-15
    • 2011-12-18
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多