Как библиотеки / языки программирования конвертируют числа с плавающей точкой в ​​строки - PullRequest
3 голосов
/ 01 июня 2019

Это загадка, которую я пытался разгадать, когда мне было 15, но мне это не удалось.Я до сих пор не знаю ответа.

Вот наивное и ошибочное решение (как и некоторые другие неудачные попытки, которые я видел здесь при переполнении стека):

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.

Ответы [ 2 ]

0 голосов
/ 04 июня 2019

Я не JAVASCRIPT кодер, поэтому я придерживаюсь C ++ ...

Преобразование числа в строку в десятичное основание большеусложняется использование двоичного кода или его базовых степеней (bin, oct, hex) из-за того, что все числа на обычном компьютере хранятся в двоичном, а не в десятичном виде.Также это не то же самое, если вы конвертируете целую или дробную часть.Предположим, что у нас есть номер x и мы хотим, чтобы строка s была закодирована в ASCII , так как работает базовое преобразование:

  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, или вы можете сохранить цифры в обратном порядке напрямую.Но для тата нужно знать количество цифр целой части числа.Это делается с помощью

    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 и преобразовывать строку в десятичную на 8-битной арифметике или использовать большую степень 10, которая подходитв используемом СЛОВЕ ДАННЫХ Bignum хранится с.hex -> dec преобразование может быть сделано следующим образом:

, но опять же для очень больших строк это будетмедленный.В этом случае его можно ускорить, используя FFT / NTT подходы, подобные умножению Schönhage-Strassen , но я никогда раньше не пытался использовать его для печати, поэтому мне не хватает понимания такого подхода.

Также имейте в виду, что определение количества цифр значения не является регулярным для дробной части числа (см. Ссылку выше), поэтому вам следует иметь в виду, что вас могут отключить цифры 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=<2,16>.Вы можете включить отключение округления, используя / комментируя #define _enable_rounding.Осторожно, подпрограмма округления работает только для базы 10, поскольку для разных баз подпрограмма инкремента будет иметь немного другой код / ​​константы и будет слишком ленива, чтобы делать это универсально (это будет более длинный и менее понятный код).Константа round_digits является пороговым значением количества последовательных нулей или девяток, запускающих округление.

0 голосов
/ 02 июня 2019

(у меня недостаточно репутации, чтобы комментировать, поэтому я прибегаю к ответу ...)

Я заметил, что вы довели точность до 300+ цифр, что намного выше точностичисла с плавающей запятой, отсюда и неточный результат.Если вы ищете средство для выполнения высокоточных вычислений, вы, вероятно, можете прибегнуть к BigInt и соответственно увеличить числа.(Я говорю «вероятно», потому что BigInt может быть приведен к вычислениям с фиксированной точностью, а не к плавающей точке, и, следовательно, в зависимости от цели, BigInt может не соответствовать требованию.)

Например, вычисление от 1000/17 до 100значимые цифры могут быть обработаны с помощью следующей функции, которая существенно увеличивает 1000 и 17, чтобы обеспечить 100 значащих цифр.(Обратите внимание, что это просто концептуальная функция для обработки деления высокой точности между двумя целыми числами, но она может быть основой для нецелых чисел путем увеличения делителей дивидендов и , пока они не станутцелые числа и соответственно корректирующие цифр . Кроме того, вам может понадобиться выдумать некоторые дополнительные «скрытые» цифры точности для обработки округления) ...

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"

Примечание в этом случае,Возвращается 102 цифры точности, а не 100 из-за относительного размера делимого (1000) делителю (17).

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...