PHP – точность с плавающей запятой

$a = '35'; $b = '-34.99'; echo ($a + $b); 

Результаты в 0,009999999999998

Что с этим связано? Я задавался вопросом, почему моя программа продолжала сообщать о нечетных результатах.

Почему PHP не возвращает ожидаемый 0.01?

Поскольку арифметика с плавающей запятой! = Арифметика реального числа. Иллюстрацией разницы, вызванной неточностями, является для некоторых поплавков a и b (a+b)-b != a Это относится к любому языку с использованием поплавков.

Поскольку с плавающей точкой являются двоичные числа с конечной точностью, существует конечное количество представимых чисел , что приводит к проблемам точности и неожиданностям, подобным этому. Вот еще одно интересное: « Каждый компьютерный ученый должен знать о арифметике с плавающей точкой» .


Вернемся к вашей проблеме, в основном нет возможности точно представлять 34,99 или 0,01 в двоичном формате (как в десятичном, 1/3 = 0,3333 …), поэтому вместо этого используются аппроксимации. Чтобы обойти проблему, вы можете:

  1. Используйте round($result, 2) результат, чтобы округлить его до двух знаков после запятой.

  2. Используйте целые числа. Если это валюта, скажем, доллары США, то храните 35 долларов США как 3500 и 34,99 долларов США как 3499, а затем разделите результат на 100.

Жаль, что PHP не имеет десятичного типа данных, как это делают другие языки .

Числа с плавающей запятой, как и все числа, должны храниться в памяти как строка из 0 и 1. Это все бит к компьютеру. Как плавающая точка отличается от целого, заключается в том, как мы интерпретируем 0 и 1, когда хотим посмотреть на них.

Один бит – это «знак» (0 = положительный, 1 = отрицательный), 8 бит – показатель степени (от -128 до +127), 23 бит – это число, известное как «мантисса» (фракция). Таким образом, двоичное представление (S1) (P8) (M23) имеет значение (-1 ^ S) M * 2 ^ P

«Мантисса» приобретает особую форму. В обычной научной нотации мы показываем «свое место» вместе с фракцией. Например:

4,39 х 10 2 = 439

В двоичном формате «место» – это один бит. Поскольку мы игнорируем все самые левые 0 в научной нотации (мы игнорируем любые незначительные цифры), первый бит гарантированно будет 1

1.101 x 2 ^ 3 = 1101 = 13

Поскольку мы гарантируем, что первый бит будет равен 1, мы удаляем этот бит при сохранении номера для экономии места. Таким образом, указанное число хранится как 101 (для мантиссы). Предполагается, что ведущий 1

В качестве примера возьмем двоичную строку

 00000010010110000000000000000000 

Разбивая его на компоненты:

 Sign Power Mantissa 0 00000100 10110000000000000000000 + +4 1.1011 + +4 1 + .5 + .125 + .0625 + +4 1.6875 

Применяя нашу простую формулу:

 (-1^S)M*2^P (-1^0)(1.6875)*2^(+4) (1)(1.6875)*(16) 27 

Другими словами, 00000010010110000000000000000000 составляет 27 в плавающей запятой (в соответствии со стандартами IEEE-754).

Однако для многих чисел нет точного двоичного представления. Очень похоже на то, что 1/3 = 0,3333 …. повторяется навсегда, 1/100 – 0,00000010100011110101110000 ….. с повторением «10100011110101110000». Однако 32-разрядный компьютер не может хранить все число в плавающей запятой. Поэтому он делает все возможное.

 0.0000001010001111010111000010100011110101110000 Sign Power Mantissa + -7 1.01000111101011100001010 0 -00000111 01000111101011100001010 0 11111001 01000111101011100001010 01111100101000111101011100001010 

(обратите внимание, что отрицательный 7 получается с использованием дополнения 2)

Должно быть сразу ясно, что 01111100101000111101011100001010 не выглядит как 0,01

Что еще более важно, однако, это содержит усеченную версию повторяющегося десятичного знака. Первоначальный десятичный знак содержал повторяющийся «10100011110101110000». Мы упростили это до 01000111101011100001010

Переведя это число с плавающей запятой обратно в десятичную форму по нашей формуле, получим 0,0099999979 (учтите, что это для 32-разрядного компьютера. У 64-разрядного компьютера будет гораздо больше точности)

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

Но есть немного разговоров о произвольной точности (Пикл упомянул об этом). Если вы хотите (или нуждаетесь) точную точность, единственный способ сделать это (по крайней мере, для рациональных чисел) – использовать расширение BC Math (которое на самом деле является просто реализацией BigNum, произвольной точности …

Чтобы добавить два числа:

 $number = '12345678901234.1234567890'; $number2 = '1'; echo bcadd($number, $number2); 

приведет к 12345678901235.1234567890

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

Таким образом, вы не можете сделать 1/3 со 100% -ной точностью, поскольку она имеет повторяющуюся десятичную (и, следовательно, не рациональную).

Но, если вы хотите знать, что такое 1500.0015 квадрате:

Использование 32-битных поплавков (двойная точность) дает оценочный результат:

 2250004.5000023 

Но bcmath дает точный ответ:

 2250004.50000225 

Все зависит от точности, в которой вы нуждаетесь.

Кроме того, здесь еще кое-что можно отметить. PHP может представлять только 32-битные или 64-битные целые числа (в зависимости от вашей установки). Поэтому, если целое число превышает размер собственного типа int (2,1 миллиарда для 32 бит, 9,2 x10 18 или 9,2 миллиарда миллиардов долларов для подписанных int), PHP преобразует int в float. Хотя это не проблема сразу (поскольку все ints, меньшие, чем точность поплавка системы, по определению непосредственно представляются как float), если вы попытаетесь умножить два вместе, это потеряет значительную точность.

Например, если задано $n = '40000000002' :

В качестве числа $n будет float(40000000002) , что отлично, поскольку оно точно представлено. Но если мы его квадратизируем, получаем: float(1.60000000016E+21)

Как строка (используя математику BC), $n будет точно '40000000002' . И если мы построим квадрат, получим: string(22) "1600000000160000000004"

Поэтому, если вам нужна точность с большими числами или рациональными десятичными точками, вы можете посмотреть в bcmath …

Используйте функцию round() PHP: http://php.net/manual/en/function.round.php

Этот ответ решает проблему, но не объясняет, почему. Я думал, что это очевидно [я также программирую на C ++, так что это очевидно для меня;]], но если нет, скажем, что PHP имеет свою собственную вычислительную точность, и в этой конкретной ситуации он возвратил наиболее соответствующую информации относительно этого вычисления ,

Здесь может быть полезно использовать bcadd () .

 <?PHP $a = '35'; $b = '-34.99'; echo $a + $b; echo '<br />'; echo bcadd($a,$b,2); ?> 

(неэффективный выход для ясности)

Первая строка дает мне 0.009999999999998. Второй дает мне 0,01

мой php возвращает 0,01 … alt text

возможно, у него есть todo с php-версией (я использую 5.2)

Потому что 0.01 нельзя представить точно как сумму рядов двоичных дробей. Именно так хранятся память в памяти.

Я думаю, это не то, что вы хотите услышать, но это ответ на вопрос. О том, как исправить, см. Другие ответы.

[Решено]

введите описание изображения здесь

Каждый номер будет сохранен на компьютере двоичным значением, например 0, 1. В номерах с одной точностью занимают 32 бита.

Число с плавающей запятой может быть представлено: 1 бит для знака, 8 бит для экспоненты и 23 бит, называемым мантисса (дробь).

Посмотрите пример ниже:

0,155625 = 0,00101 = 1,01 * 2 ^ (- 3)

введите описание изображения здесь

  • знак: 0 означает положительное число, 1 среднее отрицательное число, в этом случае оно равно 0.

  • Показатель: 01111100 = 127 – 3 = 124.

    Примечание: смещение = 127, поэтому смещенный показатель = -3 + «смещение». В единственной точности смещение составляет 127, поэтому в этом примере смещенный показатель равен 124;

  • На части дроби мы имеем: 1.01 среднее: 0 * 2 ^ -1 + 1 * 2 ^ -2

    Номер 1 (первая позиция 1.01) не нужно сохранять, потому что, когда в нем присутствует плавающее число, первое число всегда равно 1. Например, convert: 0.11 => 1.1 * 2 ^ (- 1), 0.01 => 1 * 2 ^ (- 2).

В другом примере всегда удаляем первый ноль: 0,1 будет представлено 1 * 2 ^ (- 1). Итак, первое alwasy должно быть 1. Настоящее число 1 * 2 ^ (- 1) будет:

  • 0: положительное число
  • 127-1 = 126 = 01111110
  • доля: 00000000000000000000000 (23 номер)

Наконец: необработанный двоичный код: 0 01111110 00000000000000000000000

Проверьте это здесь: http://www.binaryconvert.com/result_float.html?decimal=048046053

Теперь, если вы уже понимаете, как сохраняется число с плавающей запятой. Что произойдет, если номер не может сохранить в 32 бит (простая точность).

Например: в десятичной системе. 1/3 = 0,33333333333333333333333, и поскольку он бесконечен, я полагаю, у нас есть 5 бит для сохранения данных. Повторите еще раз, это не реально. просто предположим. Таким образом, данные, сохраненные на компьютере, будут:

 0.33333. 

Теперь, когда число, загруженное компьютером, подсчитывается снова:

 0.33333 = 3*10^-1 + 3*10^-2 + 3*10^-3 + 3*10^-4 + 3*10^-5. 

Об этом:

 $a = '35'; $b = '-34.99'; echo ($a + $b); 

Результат равен 0,01 (десятичный). Теперь покажите это число в двоичном формате.

 0.01 (decimal) = 0 10001111 01011100001010001111 (01011100001010001111)*(binary) 

Проверьте здесь: http://www.binaryconvert.com/result_double.html?decimal=048046048049

Потому что (01011100001010001111) повторяется точно так же, как 1/3. Поэтому компьютер не может сохранить это число в своей памяти. Он должен пожертвовать. Это не приводит к точности в работе компьютера.

Продвинутый (у вас должны быть знания о математике). Поэтому мы можем легко показать 0.01 в десятичной системе, но не в двоичном формате.

Предположим, что дробь в двоичном выражении 0,01 (десятичная) конечна.

 So 0.01 = 2^x + 2^y... 2^-z 0.01 * (2^(x+y+...z)) = (2^x + 2^y... 2^z)*(2^(x+y+...z)). This expression is true when (2^(x+y+...z)) = 100*x1. There are not integer n = x+y+...+z exists. => So 0.01 (decimal) must be infine in binary. 

не было бы проще использовать number_format(0.009999999999998, 2) или $res = $a+$b; -> number_format($res, 2); $res = $a+$b; -> number_format($res, 2); ?