Форум программистов, компьютерный форум, киберфорум
Kantaria
Войти
Регистрация
Восстановить пароль
Интересно Вкусно Весело.
В скором времени буду добавлять информацию в данном разделе.
Рейтинг: 5.00. Голосов: 1.

Арифметика JavaScript и PHP

Запись от Kantaria размещена 29.11.2012 в 23:02
Обновил(-а) Kantaria 29.11.2012 в 23:05

Сегодня во время учебы Js, я столкнулся с интересной проблемой.
Я написал простенький код:

Javascript
1
2
3
4
5
6
7
<script type="text/javascript">
var a = 10;
a /= 50;
a++;
a--;
document.write(a);
</script>
И ожидал получить на него разумный по логике ответ 0.2
Но как ни странно ответ получился - 0.19999999999999996
После долгих раздумий я решил выяснить из-за чего возникла данная проблема, и почему
программа выводит совершенно не адекватный ответ для простой человеческой логике.
На ум пришло только то-что JavaScript написан не верно, но это просто НЕ возможно.
Тогда я решил проверить тоже самое в языке PHP.
Ответ получился верным:
PHP
1
2
3
4
5
6
7
<?php
$a = 10;
$a = $a / 50;
$a++;
$a--;
echo $a;
?>
Ответ я нашел на нашем любимом киберфоруме.
Где мне начали обьяснять что да как и почему так а не иначе.
Ответ является таковым.

Многие программисты годами пишут свои программы, не понимая, что такое числа с плавающей запятой, и чем они отличаются от "обычных", целых чисел. Это не мешает им создавать хорошие программы. Но в конце концов каждый сталкивается с "необъяснимым" явлением:

$a = 1.1 - 1;
$b = 0.1;
if ($a == $b)
{
print "$a равно $b";
}
else
{
print "$a не равно $b";
}
Здесь и далее все примеры простоты ради приводятся на Perl. Они должны быть понятны и тем, кто не знаком с этим языком программирования, а обсуждаемые проблемы одинаковы для всех языков.

Эта программа печатает "0.1 не равно 0.1". В чём дело? Напрашивается вывод, что в языке программирования что-то не в порядке. В сети можно найти немало переписок с разработчиками языков о подобных "ошибках". На самом же деле, этот пример демонстрирует некоторые важные свойства чисел с плавающей запятой — их эта статья и попытается объяснить.

Оглавление
Как узнать, что используются числа с плавающей запятой?
Откуда берётся неточность?
Как бороться с погрешностями?
Бесконечность и прочие вкусности
Представление чисел с плавающей запятой в памяти
Реализация арифметических операций
Ссылки
Как узнать, что используются числа с плавающей запятой?
В языках программирования со строгой типизацией существуют, как правило, специальные типы данных для чисел с плавающей запятой (float/double/long double в Си, single/double/extended в Паскале). Если в вычислении участвует хотя бы одна переменная или константа с плавающей запятой, все другие числа тоже преобразовываются к этому типу.

В языках без строгой типизации, как Perl, PHP или JavaScript, заметить использование чисел с плавающей запятой сложнее. Для программиста все числа выглядят одинаково, переключение с целочисленных типов на типы с плавающей запятой происходит автоматически. Можно исходить из того, что используются операции для чисел с плавающей запятой, если какая-нибудь из участвующих переменных содержит дробную часть или её значение выходит за пределы диапазона целых чисел. Но бывают и случаи, когда числа с плавающей запятой используются для целочисленных значений:

$a = 0.5;
$a *= 2;
Здесь переменная $a равна единице (это покажет и сравнение, в отличие от примера в начале статьи), но её значение всё равно хранится как число с плавающей запятой потому, что её значение раньше содержало дробную часть.

Некоторые языки, к примеру dBase, содержат дополнительный тип данных для чисел с фиксированной запятой. Они предназначены для более точных расчётов, к примеру в бухгалтерии. По сути, это "обычные" целые числа, у которых несколько последних знаков определены, как знаки после запятой. Соответственно, они и ведут себя так же, как целые числа. Всё, что написано в этой статье — не о них.

Откуда берётся неточность?
Основная причина неточности при использовании чисел с плавающей запятой в том, что компьютер не может работать с бесконечными дробями, которые мы знаем из математики — для них понадобилось бы бесконечное количество памяти. Это и понятно, мы тоже округляем числа до какого-то знака, когда имеем дело с десятичными дробями. Но это не объясняет приведённого в начале статьи примера — ведь там всего один знак после запятой?

Существует ещё один фактор — компьютер считает не в десятичной системе, а в двоичной. А если представить 0.1 как двоичную дробь, то она окажется периодической: 0.0(0011). Соответственно, в памяти компьютера число 0.1 представлено как 1.1001100110011001100110011001100110011001100110011010b * 2^(-4). Обратите внимание на округление в конце числа. Если перевести его обратно в десятичную систему, то получится 0.10000000000000000555111512.

Почему тогда показывается не это число, а 0.1? Дело в том, что числа с плавающей запятой на выводе всегда округляются. Perl, к примеру, по умолчанию выводит только пятнадцать значащих знаков. Но, если использовать функцию printf(), можно вывести до семнадцати значащих знаков, и тогда мы вдруг увидим 0.10000000000000001. В некоторых браузерах метод Number.toPrecision() JavaScript'а позволяет выводить числа даже с пятьюдесятью значащими знаками.

Так почему всё-таки 1.1 − 1 не равно 0.1? Если посмотреть значение числа 1.1 − 1, то мы увидим 0.10000000000000008881784197. Оно, очевидно, не равно компьютерному представлению числа 0.1, хотя при стандартном округлении и выглядит точно так же. Разница объясняется тем, что мы считали с округлённой версией числа 1.1.

Как бороться с погрешностями?
Если использовать числа с плавающей запятой, то погрешность результатов оценить сложно. До сих пор не существует удовлетворительной математической теории, которая позволяла бы это делать. С другой стороны, погрешность при операциях с целыми числами оценивается легко. Поэтому одна возможность избавиться от неточностей — это использование чисел с фиксированной запятой, упомянутых выше. Эта возможность нашла наиболее широкое применении в финансовых программах, где заранее известно нужное количество знаков после запятой. Для других же приложений ограничения чисел с фиксированной запятой часто оказываются проблематичными — с их помощью нельзя представить ни очень большие числа, ни очень маленькие.

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

if (abs($a - $b) < 0.000001 * min($a, $b))
{
print "$a равно $b";
}
else
{
print "$a не равно $b";
}
Проблема здесь, опять же, состоит в том, что сложно оценить размер возможных погрешностей — неизвестно, с какой максимальной точностью можно считать, чтобы погрешности не попали в результат. Выражать это число через размер переменных, с которыми мы работаем, как это сделано в примере — первый шаг, но он не решает всех проблем.

Бесконечность и прочие вкусности
Для чисел с плавающей запятой определены несколько специальных значений, которые весьма непривычны для программистов, привыкших к целочисленным операциям. Так, если взять самое большое целое число и прибавить к нему единицу, произойдёт переполнение, и число станет отрицательным. Если же прибавить единицу к самому большому числу с плавающей запятой, то не произойдёт ровным счётом ничего; в результате мы получим то же самое число. Это явление объясняется ниже. Переполнения можно добиться, к примеру, умножив это число на два. Но результат будет несколько необычным — "число" Inf (от англ. infinity = бесконечность). Аналогичным образом можно получить отрицательную бесконечность — -Inf.

Бесконечность получается и при делении на ноль, причём и здесь она может быть как положительной, так и отрицательной (никакого исключения, как при работе с целыми числами, не возникает). И с ней действительно можно решать! Так, если разделить любое число на бесконечность, получится ноль. Произведение двух бесконечностей опять даёт бесконечность, как и сумма бесконечностей с одинаковым знаком.

А вот сумма бесконечностей с разными знаками не определена, результатом получается NaN, другое специальное значение (от англ. Not a Number = не число). То же самое выйдет, если попытаться умножить бесконечность на ноль или поделить ноль на ноль. В некоторых языках программирования NaN является ещё и результатом неудачного преобразования строки в число. С NaN тоже можно решать, но результат любой операции будет опять же NaN.

Ну и под конец ещё одно необычное явление: если в JavaScript написать 1/0, то результатом будет Inf, а вот 1/-0 вернёт -Inf. Для чисел с плавающей запятой действительно определены два нуля: положительный и отрицательный! К счастью, в программе это обычно не нужно учитывать. Оба нуля при сравнении равны и на выводе они, в большинстве языков программирования, тоже выглядят одинаково. Знак нуля важен только для операций деления и умножения. Поэтому во многих языках программирования нельзя даже определить константу со значением −0, она автоматически преобразуется в положительный ноль (именно по этой причине пришлось использовать JavaScript в примере).


Это все я прочитал, но для меня осталось загадкой тот фактор , что , 1,1- 1 , в любом конечном итоге должен получиться 0,1 не зависимо сколько после него нулей.
Можно точно так же сделать вывод , что 10-1 не равно 9 , а равно 9,000... и не понятному числу в конце.
Меня интересует сам факт того, каким образом компьютер вычисляет 2 цифры и получает совершенно другое число.
И в данном случае плавающая точка не фактор по которому можно делать выводы.

А вот и ответ на него.

а) и 1.1, и 1 компьютер сначала переводит в двоичную систему, причём первое число ОКРУГЛЯЕТ
б) затем отнимает от одного двоичного числа другое двоичное
в) в результате получает третье число - двоичную периодическую дробь, тоже ОКРУГЛЕННУЮ
г) затем преобразует его двоичное представление в десятичное
д) и вот тут-то из-за этой самой ОКРУГЛЕННОСТИ первого преобразования и возникает погрешность


Но так же встает интересная неурядица , каким образом JavaScript и [COLOR="rgb(255, 140, 0)"]PHP[/COLOR] считают тот же самый алгоритм совершенно разным образом.

А вот и ответ:

считает компьютер точно также
только вот последнее преобразование (пункт Г выше) происходит иначе

где-то читал, что в PHP братья-израильтяне "вшили" небольшой "еврео-интеллектуальный" алгоритм округления результата в зависимости от представления исходных данных

что-то вроде того, что если исходные числа-операнды написаны с точностью до 2-х знаков после запятой, то результат показывает не более 4-х знаков после запятой
причем зависимость не прямая, а квадратичная:
0 знаков в исходных - 1 знак в результате
2 в исходных - 4 в результате
...
5 в исходных - 25 в результате


И под конец хотелось бы добавить еще немножечко информации.
Для закрепления знаний.

Числа с плавающей точкой

Числа с плавающей точкой (также известные как "float", "double", или "real") могут быть определены следующими синтаксисами:

<?php
$a = 1.234;
$b = 1.2e3;
$c = 7E-10;
?>
Формально:

LNUM [0-9]+
DNUM ([0-9]*[\.]{LNUM}) | ({LNUM}[\.][0-9]*)
EXPONENT_DNUM [+-]?(({LNUM} | {DNUM}) [eE][+-]? {LNUM})
Размер числа с плавающей точкой зависит от платформы, хотя максимум, как правило составляет ~1.8e308 с точностью около 14 десятичных цифр (64-битный IEEE формат).

Внимание
Точность чисел с плавающей точкой

Числа с плавающей точкой имеют ограниченную точность. Хотя это зависит от операционной системы, в PHP обычно используется формат двойной точности IEEE 754, дающий максимальную относительную ошибку округления порядка 1.11e-16. Неэлементарные арифметические операции могут давать большие ошибки, и, разумеется, необходимо принимать во внимание распространение ошибок при совместном использовании нескольких операций.

Кроме того, рациональные числа, которые могут быть точно представлены в виде чисел с плавающей точкой с основанием 10, например, 0.1 или 0.7, не имеют точного внутреннего представления в качестве чисел с плавающей точкой с основанием 2, вне зависимости от размера мантиссы. Поэтому они и не могут быть преобразованы в их внутреннюю двоичную форму без небольшой потери точности. Это может привести к неожиданным результатам: например, floor((0.1+0.7)*10) скорее всего вернет 7 вместо ожидаемого 8, так как результат внутреннего представления будет чем-то вроде 7.9999999999999991118....

Так что никогда не доверяйте точности чисел с плавающей точкой до последней цифры, и не проверяйте напрямую их равенство. Если вам действительно необходима высокая точность, используйте математические функции произвольной точности и gmp-функции.

Преобразование в число с плавающей точкой

Информацию о преобразовании строк в числа с плавающей точкой смотрите в разделе Преобразование строк в числа. Для значений других типов преобразование будет сначала осуществлено в в integer и затем в число с плавающей точкой. Дополнительную информацию смотрите в разделе Преобразование к целому. Начиная с версии PHP 5, при преобразовании объекта к числу с плавающей точкой выводится замечание об ошибке.

Сравнение чисел с плавающей точкой

Как указано выше, проверять числа с плавающей точкой на равенство проблематично из-за их внутреннего представления. Тем не менее, существуют способы для их сравнения, которые работают несмотря на все эти ограничения.

Для сравнения чисел с плавающей точкой используется верхняя граница относительной ошибки при округлении. Эта величина называется машинной эпсилон или единица округления(unit roundoff) и представляет собой самую маленькую допустимую разницу при расчетах.

$a и $b равны до 5-ти знаков после запятой.

<?php
$a = 1.23456789;
$b = 1.23456780;
$epsilon = 0.00001;

if(abs($a-$b) < $epsilon) {
echo "true";
}
?>
NaN

Некоторые числовые операции могут возвращать значение, представляемое константой NAN. Данный результат означает неопределенное или непредставимое значение в операциях с плавающей точкой. Любое строгое или нестрогое сравнение данного значения с другим значением, включая его самого, возвратит FALSE.

Так как NAN представляет собой неограниченное количество различных значений, то NAN не следует сравнивать с другими значениями, включая ее саму. Вместо этого, для определения ее наличия необходимо использовать функцию is_nan().
Размещено в Без категории
Просмотров 19866 Комментарии 11
Всего комментариев 11
Комментарии
  1. Старый комментарий
    Аватар для Зверушь
    Коротко о проблеме также можно почитать в книге Фленагана - JavaScript, 6е издание. Глава 3.1.4 "Двоичное представление вещественных чисел и ошибки округления" на странице 55
    Запись от Зверушь размещена 13.12.2012 в 13:41 Зверушь вне форума
  2. Старый комментарий
    Аватар для Kantaria
    Интересно будет почитать.
    Можешь дать ссылку на книгу?
    Запись от Kantaria размещена 13.12.2012 в 14:52 Kantaria вне форума
  3. Старый комментарий
    Аватар для Зверушь
    Ту ссылку уже удалили. Можешь в гугле поискать или скажы куда скинуть
    Запись от Зверушь размещена 14.12.2012 в 11:15 Зверушь вне форума
  4. Старый комментарий
    Аватар для Kantaria

    Ссылка

    Скинь его пожалуйста на мой e-mail : kantariamo@yandex.ru
    Буду тебе очень благодарен.
    Запись от Kantaria размещена 14.12.2012 в 14:38 Kantaria вне форума
  5. Старый комментарий
    Аватар для EPMAK
    данная заминка с округлятором интерпритаторов дает о себе знать при написании калькуляторов, в часности у меня были проблемы при написании подсчета пеней по кредитам.

    Флэнагана могу дать. если не нашел в гугли
    Запись от EPMAK размещена 14.12.2012 в 21:46 EPMAK вне форума
    Обновил(-а) EPMAK 14.12.2012 в 21:47
  6. Старый комментарий
    Аватар для Зверушь
    Отправил по почте
    Запись от Зверушь размещена 15.12.2012 в 22:24 Зверушь вне форума
  7. Старый комментарий
    Спасибо! Познавательно, изучая C# с таким не сталкивался, а вот с JS и PHP мог бы столкнуться.
    Запись от Lovrentiy размещена 01.06.2013 в 11:03 Lovrentiy вне форума
  8. Старый комментарий
    Аватар для Kantaria
    Цитата:
    Сообщение от Lovrentiy Просмотреть комментарий
    Спасибо! Познавательно, изучая C# с таким не сталкивался, а вот с JS и PHP мог бы столкнуться.
    Честно говоря, не изучал C#,C++,Basic и т.д., но думаю, что такая проблема так же может возникнуть в данных языках.
    Запись от Kantaria размещена 01.06.2013 в 11:25 Kantaria вне форума
  9. Старый комментарий
    Аватар для BANO
    Цитата:
    Сообщение от Kantaria Просмотреть комментарий
    Честно говоря, не изучал C#,C++,Basic и т.д., но думаю, что такая проблема так же может возникнуть в данных языках.
    конечно может возникнуть, ведь там есть числа с плавающей запятой, сами же писали)
    Запись от BANO размещена 28.02.2016 в 12:31 BANO вне форума
  10. Старый комментарий
    Аватар для Avazart
    Цитата:
    Спасибо! Познавательно, изучая C# с таким не сталкивался, а вот с JS и PHP мог бы столкнуться.
    В C# вроде есть отдельный тип decimal. (Как я понимаю работает он медленнее обычных)
    Если нужна точность то обычно используют библиотеки для длинной арифметики.
    Запись от Avazart размещена 29.02.2016 в 23:52 Avazart на форуме
    Обновил(-а) Avazart 29.02.2016 в 23:53
  11. Старый комментарий
    Аватар для volodin661
    perl6
    в отличие от многих языков
    Perl
    1
    
    say 0.1 + 0.2 == 0.3
    вернёт True
    Запись от volodin661 размещена 03.10.2016 в 00:03 volodin661 на форуме
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin® Version 3.8.9
Copyright ©2000 - 2021, vBulletin Solutions, Inc.