Как Excel успешно округляет числа с плавающей запятой, даже если они неточные? - PullRequest
22 голосов
/ 03 августа 2011

Например, этот блог говорит, что 0,005 не совсем 0,005, но округление этого числа дает правильный результат.

Я перепробовал все виды округления в C ++, и при округлении чисел до определенных десятичных знаков происходит сбой. Например, Round (x, y) округляет x до кратного y. Итак, раунд (37,785,0.01) должен дать вам 37,79, а не 37,78.

Я снова открываю этот вопрос, чтобы обратиться к сообществу за помощью. Проблема заключается в неточности чисел с плавающей запятой (37,785 представляется как 37,78499999999).

Вопрос в том, как Excel справляется с этой проблемой?

Решение в этом round () для float в C ++ неверно для вышеуказанной проблемы.

Ответы [ 12 ]

20 голосов
/ 27 августа 2011

"Раунд (37,785,0.01) должен дать вам 37,79, а не 37,78."

Во-первых, нет единого мнения, что 37,79 вместо 37,78 является "правильным" ответом здесь? Тай-брейки всегда немного жесткие. Хотя в случае ничьих всегда используется округление, это, конечно, не единственный подход.

Во-вторых, это не является ситуацией разрыва связи. Числовое значение в формате IEEE binary64 с плавающей запятой равно 37,784999999999997 (приблизительно). Есть много способов получить значение 37,784999999999997, кроме того, что человек набрал значение 37,785 и получилось преобразовать его в это представление с плавающей запятой. В большинстве случаев правильный ответ - 37,78, а не 37,79.

Добавление
Рассмотрим следующие формулы Excel:

=ROUND(37785/1000,2)
=ROUND(19810222/2^19+21474836/2^47,2)

Обе ячейки будут отображать одинаковое значение, 37,79. Существует законный аргумент в пользу того, следует ли округлять 37785/1000 до 37,78 или 37,79 с точностью до двух мест. Как иметь дело с этими угловыми случаями немного произвольно, и нет единого ответа. В Microsoft даже нет единого ответа: « функция Round () не реализована согласованным образом в различных продуктах Microsoft по историческим причинам. » (http://support.microsoft.com/kb/196652) Учитывая бесконечность точная машина, Microsoft VBA будет округлять с 37,785 до 37,78 (банковский круг), в то время как Excel даст 37,79 (симметричный арифметический раунд).

Нет аргументов в пользу округления последней формулы. Это строго меньше, чем 37,785, поэтому он должен округляться до 37,78, а не 37,79. Тем не менее, Excel округляет это. Почему?

Причина в том, как реальные числа представлены в компьютере. Microsoft, как и многие другие, использует 64-битный формат IEEE с плавающей запятой. Число 37785/1000 страдает от потери точности при выражении в этом формате. Эта потеря точности не возникает при 19810222/2 ^ 19 + 21474836/2 ^ 47; это «точное число».

Я специально построил это точное число, чтобы оно имело такое же представление с плавающей запятой, что и неточное 37785/1000. То, что Excel округляет это точное значение вверх, а не вниз, является ключом к определению того, как работает функция ROUND() в Excel: это вариант симметричного арифметического округления. Он округляется на основе сравнения с представлением углового случая с плавающей запятой.

Алгоритм на С ++:

#include <cmath> // std::floor

// Compute 10 to some positive integral power.
// Dealing with overflow (exponent > 308) is an exercise left to the reader.
double pow10 (unsigned int exponent) { 
   double result = 1.0;
   double base = 10.0;
   while (exponent > 0) {
      if ((exponent & 1) != 0) result *= base;
      exponent >>= 1;
      base *= base;
   }
   return result;
}   

// Round the same way Excel does.
// Dealing with nonsense such as nplaces=400 is an exercise left to the reader.
double excel_round (double x, int nplaces) {
   bool is_neg = false;

   // Excel uses symmetric arithmetic round: Round away from zero.
   // The algorithm will be easier if we only deal with positive numbers.
   if (x < 0.0) {
      is_neg = true;
      x = -x; 
   }

   // Construct the nearest rounded values and the nasty corner case.
   // Note: We really do not want an optimizing compiler to put the corner
   // case in an extended double precision register. Hence the volatile.
   double round_down, round_up;
   volatile double corner_case;
   if (nplaces < 0) {
      double scale = pow10 (-nplaces);
      round_down  = std::floor (x * scale);
      corner_case = (round_down + 0.5) / scale;
      round_up    = (round_down + 1.0) / scale;
      round_down /= scale;
   }
   else {
      double scale = pow10 (nplaces);
      round_down  = std::floor (x / scale);
      corner_case = (round_down + 0.5) * scale;
      round_up    = (round_down + 1.0) * scale;
      round_down *= scale;
   }

   // Round by comparing to the corner case.
   x = (x < corner_case) ? round_down : round_up;

   // Correct the sign if needed.
   if (is_neg) x = -x; 

   return x;
}   
4 голосов
/ 23 августа 2011

Для очень точной произвольной точности и округления чисел с плавающей запятой до фиксированного набора десятичных разрядов вы должны взглянуть на математическую библиотеку , такую ​​как GNU MPFR . Хотя это C-библиотека, размещенная мной веб-страница также содержит ссылки на несколько различных привязок C ++, если вы хотите избежать использования C.

Вы также можете прочитать статью под названием "Что должен знать каждый компьютерщик об арифметике с плавающей точкой" Дэвида Голдберга в Исследовательском центре Xerox в Пало-Альто. Это отличная статья, демонстрирующая базовый процесс, который позволяет аппроксимировать числа с плавающей запятой в компьютере, который представляет все в двоичных данных, и как ошибки округления и другие проблемы могут возникать в математике с плавающей запятой на основе FPU.

3 голосов
/ 03 августа 2011

Я не знаю, как это делает Excel, но печатать числа с плавающей запятой - сложная проблема: http://www.serpentine.com/blog/2011/06/29/here-be-dragons-advances-in-problems-you-didnt-even-know-you-had/

2 голосов
/ 27 августа 2011

Функция, которая принимает число с плавающей запятой в качестве аргумента и возвращает другое число с плавающей запятой, округленное точно до заданного числа десятичных цифр, не может быть записана, поскольку существует много чисел с конечным десятичным представлением, которые имеют бесконечное двоичное представление;Один из простейших примеров - 0.1.

. Чтобы достичь желаемого, вы должны согласиться использовать другой тип в результате выполнения функции округления.Если вам срочно нужна печать числа, вы можете использовать строку и функцию форматирования: проблема заключается в том, как получить именно то форматирование, которое вы ожидаете.В противном случае, если вам нужно сохранить это число для точных вычислений, например, если вы ведете учет, вам нужна библиотека, которая способна точно представлять десятичные числа.В этом случае наиболее распространенным подходом является использование масштабированного представления: целое число для значения вместе с количеством десятичных цифр.Разделив значение на десять, возведенное в шкалу, вы получите исходное число.

Если подходит какой-либо из этих подходов, я постараюсь дополнить свой ответ практическими предложениями.

2 голосов
/ 23 августа 2011

Итак, ваш актуальный вопрос, как получить правильно округленные числа с плавающей запятой -> строковые преобразования. В поисках этих терминов вы получите кучу статей, но если вам интересно что-то использовать, большинство платформ предоставляют достаточно компетентные реализации sprintf () / snprintf (). Так что просто используйте их, и если вы найдете ошибки, отправьте отчет поставщику.

1 голос
/ 03 августа 2011

Excel округляет числа вроде этого «правильно», выполняя РАБОТУ. Они начали в 1985 году с довольно «нормального» набора подпрограмм с плавающей запятой и добавили несколько ложных чисел с плавающей точкой с масштабированным целым числом, и с тех пор они настраивали эти вещи и добавляли специальные случаи. Приложение DID раньше имело большинство тех же «очевидных» ошибок, которые делали все остальные, просто они были в основном давным-давно. Я сам подал пару, когда еще занимался техподдержкой в ​​начале 90-х.

0 голосов
/ 21 февраля 2015

Я считаю, что следующий код C # округляет числа, так как они округлены в Excel. Чтобы точно воспроизвести поведение в C ++, вам может понадобиться специальный десятичный тип.

В простом английском языке число с двойной точностью преобразуется в десятичное число, а затем округляется до пятнадцати значащих цифр (не путать с пятнадцатью десятичными знаками). Результат округляется во второй раз до указанного числа десятичных знаков.

Это может показаться странным, но вы должны понимать, что Excel всегда отображает числа, округленные до 15 значащих цифр. Если бы функция ROUND () не использовала это отображаемое значение в качестве отправной точки и вместо этого использовала внутреннее двойное представление, то были бы случаи, когда ROUND (A1, N), по-видимому, не соответствовал фактическому значению в A1. Это было бы очень запутанным для нетехнического пользователя.

Двойник, ближайший к 37,785, имеет точное десятичное значение 37,784999999999996589394868351519107818603515625. (Любой двойник может быть представлен точно конечным десятичным десятичным основанием, потому что одна четверть, одна восьмая, одна шестнадцатая и т. Д. Имеют все конечные десятичные разложения.) Если бы это число было округлено непосредственно до двух десятичных знаков, не было бы связи с перерыв и результат будет 37,78. Если сначала округлить до 15 значащих цифр, вы получите 37,7850000000000. Если это далее округлить до двух знаков после запятой, то вы получите 37,79, так что никакой настоящей загадки в конце концов нет.

    // Convert to a floating decimal point number, round to fifteen 
    // significant digits, and then round to the number of places
    // indicated.
    static decimal SmartRoundDouble(double input, int places)
    {
        int numLeadingDigits = (int)Math.Log10(Math.Abs(input)) + 1;

        decimal inputDec = GetAccurateDecimal(input);

        inputDec = MoveDecimalPointRight(inputDec, -numLeadingDigits);

        decimal round1 = Math.Round(inputDec, 15);

        round1 = MoveDecimalPointRight(round1, numLeadingDigits);

        decimal round2 = Math.Round(round1, places, MidpointRounding.AwayFromZero);

        return round2;
    }

    static decimal MoveDecimalPointRight(decimal d, int n)
    {
        if (n > 0)
            for (int i = 0; i < n; i++)
                d *= 10.0m;
        else
            for (int i = 0; i > n; i--)
                d /= 10.0m;

        return d;
    }

    // The constructor for decimal that accepts a double does
    // some rounding by default. This gets a more exact number.
    static decimal GetAccurateDecimal(double r)
    {
        string accurateStr = r.ToString("G17", CultureInfo.InvariantCulture);
        return Decimal.Parse(accurateStr, CultureInfo.InvariantCulture);
    }
0 голосов
/ 09 августа 2013

Есть много способов оптимизировать результат значения с плавающей запятой, используя статистические, числовые ... алгоритмы

Самый простой из них, вероятно, поиск повторяющихся 9 или 0 с вдиапазон точности.Если они есть, возможно, эти 9 лишние, просто округлите их.Но это может не сработать во многих случаях.Вот пример для float с 6 цифрами точности:

2.67899999 → 2.679
12.3499999 → 12.35
1.20000001 → 1.2

Excel всегда ограничивает диапазон ввода до 15 цифр и округляет вывод до максимум 15 цифр, так что это может быть одним из способов использования Excel


Или вы можете включить точность вместе с числом.После каждого шага регулировка точности зависит от точности операндов.Например,

1.113   → 3 decimal digits
6.15634 → 5 decimal digits

Поскольку оба числа находятся в пределах диапазона точности 16-17 цифр двойного числа, их сумма будет точной для большего из них, который составляет 5 цифр.Аналогично, 3 + 5 <16, так что их произведение будет иметь точность до 8 десятичных чисел </p>

1.113 + 6.15634 = 7.26934    → 5 decimal digits
1.113 * 6.15634 = 6.85200642 → 8 decimal digits

Но 4.1341677841 * 2.251457145 будет принимать только двойную точность, потому что реальный результат превышает точность двойного


Другой эффективный алгоритм - Грису , но у меня не было возможности попробовать.

В 2010 году Флориан Лойч опубликовал замечательную статью в PLDI, "ПечатьЧисла с плавающей точкой быстро и точно с целыми числами ", что представляет собой самый большой шаг в этой области за последние 20 лет: он в основном выяснил, как использовать машинные целые числа для точного рендеринга!Почему я говорю «в основном»?Поскольку алгоритм Лойча «Grisu3» очень быстрый, он отказывается от примерно 0,5% чисел, и в этом случае вам придется прибегнуть к Dragon4 или производной

Здесь будут драконы: успехи в проблемахвы даже не знали, что у вас есть

На самом деле я думаю, что Excel должен сочетать много разных методов для достижения наилучшего результата из всех

Пример, когда значениеДостигает нуля

В Excel 95 или более ранней версии введите в новую рабочую книгу следующее:

A1: =1.333+1.225-1.333-1.225

Щелкните правой кнопкой мыши ячейку A1 и выберите Формат ячеек.На вкладке «Число» щелкните «Научный» в разделе «Категория».Установите десятичные разряды на 15.

Вместо того, чтобы отображать 0, Excel 95 отображает -2.22044604925031E-16.

Excel 97, однако, появилась оптимизация, которая пытается исправить эту проблему .Если операция сложения или вычитания приводит к значению, равному нулю или очень близкому к нему, Excel 97 и более поздних версий компенсирует любую ошибку, введенную в результате преобразования операнда в двоичный код и обратно.Приведенный выше пример при выполнении в Excel 97 и более поздних версиях правильно отображает 0 или 0,000000000000000E + 00 в научной нотации.

Арифметика с плавающей запятой может давать неточные результаты в Excel

0 голосов
/ 25 августа 2011

Что вам НУЖНО, так это:

 double f = 22.0/7.0;
    cout.setf(ios::fixed, ios::floatfield);
    cout.precision(6); 
    cout<<f<<endl;  

Как это можно реализовать (просто обзор для округления последней цифры):

long getRoundedPrec(double d,   double precision = 9)
{
    precision = (int)precision;
    stringstream s;
    long l = (d - ((double)((int)d)))* pow(10.0,precision+1);
    int lastDigit = (l-((l/10)*10));
    if( lastDigit >= 5){
        l = l/10 +1;
    }
    return l;
}
0 голосов
/ 23 августа 2011

Большинство десятичных дробей не могут быть точно представлены в двоичном виде.

double x = 0.0;
for (int i = 1; i <= 10; i++)
{
  x += 0.1;
}
// x should now be 1.0, right?
//
// it isn't. Test it and see.

Одним из решений является использование BCD.Это старое.Но это также проверено и верно.У нас есть много других старых идей, которые мы используем каждый день (например, использование 0, чтобы ничего не представлять ...).

Другой метод использует масштабирование при вводе / выводе.Преимущество почти всей математики в том, что она является целочисленной математикой.

...