Вступление
При выводе результатов числовых расчётов часто бывает ситуация, когда вместо десятичной дроби, например, 3,7, отображается число 3,6999999999999997. Это происходит из-за того, что далеко не все десятичные дроби можно точно представить в двоичном коде. В случае невозможности записать десятичную дробь точно, используется самое близкое к ней значение, которое можно выразить двоичным числом. При обратном переводе в десятичный формат это значение и даёт такие непонятные длинные дроби.
Перевод десятичных дробей в двоичную систему счисления
Невозможность записать некоторые десятичные дроби в двоичной системе (нулями и единицами) можно привести на примере обычного небольшого десятичного числа 20.1. Целая часть десятичных дробей переводится в двоичный вид точно так же, как целые числа, то есть складыванием положительных степеней двойки так, чтобы в итоге получить искомую сумму:
Получаем, что 2010 = 000101002
Как мы видим по таблице, справа налево вес каждого разряда двоичного числа увеличивается вдвое (положительные степени двойки). Набираем число 20, суммируя результаты этих степеней и отмечая единичками те степени, которые мы использовали. Вариант у нас только один – использовать степени 2 и 4, то есть числа 4 и 16, в сумме как раз дающие 20. Отметив их единичками, получаем двоичное число 00010100 или, если отбросить незначащие нули, 10100.
Дробная часть десятичных дробей переводится в двоичный код точно так же, только используются сумма отрицательных степеней двойки. Отрицательная степень двойки, в отличие от положительной, не увеличивает число вдвое, а уменьшает его в два раза. Продолжим верхнюю таблицу вправо отрицательными степенями:
Эту таблицу мы можем продолжать вправо до бесконечности, но никогда не наберём полные 0.1. В итоге получаем, что 0,110 ≈ 00011001100112 = 0,099975585937510.
Таким образом, 20,110 ≈ 10100,00011001100112.
Хранение чисел с плавающей точкой в компьютере
В компьютере числа двойной точности (в Java тип Double) хранятся в экспоненциальном (т.н. научном, инженерном) формате. Этот формат, например, записывает десятичное число 20,1 как 2,01*101, или сокращённо, как 2,01E+1. То есть, число после буквы E указывает, на сколько знаков следует сдвинуть запятую вправо (или влево, если число отрицательное) в числе, стоящем перед буквой
Переведённое в двоичную систему число 20,1, выглядящее как 10100,0001100110011 в экспоненциальной форме будет выглядеть как 1,01000001100110011 * 2100, или сокращённо 1,01000001100110011E+100 (двоичное 100 — это десятичное 4, то есть сдвигать запятую в числе 1,01000001100110011 следует на 4 знака вправо).
Для хранения этих чисел в компьютере отводится 64 бита (8 байт). Биты распределяются следующим образом. Для знака числа (плюс или минус) отводится 1 бит, для записи порядка (в нашем случае 100) — 11 бит, а для записи мантиссы (в нашем случае 1,01000001100110011) — 52 бита.
Схематично это выглядит так:
Чтобы иметь возможность записывать отрицательные порядки, за ноль в поле порядка принято число 1023 (в двоичном коде — 01111111111), что делит весь возможный диапазон порядков примерно пополам (от -1023 до 0 и от 0 до 1024). Поэтому наш порядок 100 фактически выглядит в битах, как 10000000011, ведь 1023 + 4 = 1027.
Кроме того, по причинам экономии на аппаратных ресурсах, самого левого бита мантиссы «в железе» не существует, но программно он всегда подразумевается. Этот бит принимается равным 1 во всех случаях, кроме случая нахождения в поле порядка специального числа -1023 (значения битов 00000000000). В нашем случае в поле порядка занесено число 4, а значит, левый бит признаётся единичным, и поэтому у нас в мантиссе фактически лежит лишь дробная часть, то есть число 01000001100110011.
Исходя из вышесказанного, для нашего числа 10100,0001100110011 значения всех 64 реальных битов и 1 подразумеваемого таковы:
Несколько слов про магическое число -1023 (значение битов 00000000000) в поле порядка. Как я уже говорил, это число просто устанавливает подразумеваемый бит целой части мантиссы в ноль. Если все остальные биты мантиссы также будут нулевыми, то хранимое таким образом в компьютере число, очевидно, будет являться нулём.
В случае же наличия в мантиссе ненулевых битов при том же числе -1023 в поле порядка, подразумеваемый бит целой части мантиссы также принимается равным нулю, но значение порядка подразумевается равным -1022. Грубо говоря, смена чисел в поле порядка с -1022 на -1023 и обратно влияют только на снятие или установку единичного бита целой части мантиссы, тогда как и то, и другое значение порядка подразумевается равным -1022.
Для чего же нужно было приравнивать целую часть мантиссы к нулю в ущерб возможности организовать ещё один порядок? Дело в том, что насколько бы ни был велик отрицательный порядок, то есть, насколько бы ни было мало выраженное им число, между ним и нулём у нас всегда будет оставаться незаполненная числами дырка.
Это легко понять, представив, как восстанавливается число из экспоненциального формата. Основание системы счисления (2) возводится в степень порядка и умножается на мантиссу. Самое маленькое число типа Double, которое мы можем так выразить, равно 1 * 2-1022. Ведь подразумеваемый бит равен единице, а значит, мантисса не может быть меньше единицы. Постепенно увеличивая мантиссу с 1 до почти 2 мы можем равномерно заполнить числами весь диапазон до предыдущего порядка 1 * 2-1021. Но у нас всегда остаётся незаполненный диапазон с другой стороны — в сторону нуля. И вот его то мы можем целиком заполнить только делая мантиссу меньше единицы. А для этого надо просто переключить подразумеваемый бит в ноль, что мы и делаем!
Существует ещё одно магическое число в поле порядка — 1024 (значение битов 11111111111). В зависимости от содержимого мантиссы, оно означает либо бесконечность (1 / 0 = ∞) либо неопределённость (неупорядоченное число, не число) (0 / 0 = NaN). Если мантисса нулевая — перед нами бесконечность, иначе — неопределённость.
Правильное округление в Java
Конечно, в Java есть функции, которые умеют округлять числа до нужного знака после запятой. Но иногда может встретится число, которое в десятичном виде округляется в большую сторону, а будучи записано в двоичном виде становится чуть меньше, и уже по правилам должно округляться в меньшую сторону. В качестве примера такого числа можно привести число 13,5465. Очевидно, что оно должно округляться в большую сторону до 13,547. Но при записи в двоичный код число становится равным 13,5464999…, и оно уже должно округляться в меньшую сторону до 13,546.
Но ведь мы мыслим и думаем в десятичной системе счисления, а значит, хотим видеть числа, всегда округлённые в правильную сторону вне зависимости от того, как они там внутри компьютера выглядят. Чтобы провести такое округление и отобразить число правильно, мы должны написать на Java небольшой код:
Теперь мы можем применить этот метод, например, если программируем под Андроид, при выводе результата result в поле EditText активности:
Рассмотрим, как же это работает. Метод roundUp принимает подлежащее округлению число value типа double и желаемое количество знаков после запятой digits типа int. Возвращает метод результат в виде числа типа BigDecimal. Тип BigDecimal хранит данные не в виде двоичного представления числа с плавающей запятой, о котором я рассказывал выше, а в виде числа с фиксированной запятой. Число с фиксированной запятой содержит в себе два отдельных целых числа. Это исходное число, но без запятой, и число, содержащее количество знаков после запятой. А целые числа всегда можно записать в двоичный формат абсолютно точно. Так что на выходе мы потенциально можем получить любую точную десятичную дробь.
Внутри нашего метода создаётся объект BigDecimal, который принимает на вход число с плавающей запятой value типа double, и методом setScale округляет его. Но при обычном использовании этого метода число округляется всё по тем же правилам, что приводит к неправильному округлению. В нашем же методе есть одна хитрость. Чтобы округление всегда происходило в нужную сторону, значение на вход метода BigDecimal мы передаём не в виде числа, а в виде строки. На этапе преобразования числа в строку, язык Java сам автоматически округляет число с достаточным количеством знаков, до степени, имеющей смысл (округляя весь хвост, начинающийся с последовательности девяток или нулей, подозревая в этом погрешность). Таким образом, мы подаём на вход BigDecimal строку с уже исправленной погрешностью.
Чтобы перевести число в строку, мы использовали трюк ""+value. При сложении числа со строкой (даже пустой) результат окажется тоже строкой.
Сравним результаты округления без предварительного перевода числа в строку и с переводом.
Вариант 1:
Результат: 13,546 — неправильно!
Вариант 2:
Результат: 13,547 — правильно.
Вообще, такую удобную особенность преобразования double в строку, как исправление погрешности, можно объяснить тем, что строки обычно используются только для отображения информации, и не участвуют в расчётах, не влияют на них. Поэтому не будет большого криминала даже если погрешность будет исправлена ошибочно. Всё равно такая внезапно округлённая строка при правильной архитектуре приложения не будет участвовать в дальнейших расчётах, но покажет пользователю очень близкий к истинному результат, к тому же более удобный для восприятия.
Если нам нужно вести расчёты вообще без погрешностей, возникающих из-за двоичного представления чисел, то вместо переменных типа Double следует сразу использовать тип BigDecimal, и все расчёты производить с использованием методов этого типа. Обычно такие расчёты используют в бухгалтерии, где при любых суммах для правильной отчётности важна точность до копейки. В других же случаях такой неестественной точности, как правило, не требуется, поскольку погрешность двоичного представления теряется на фоне погрешностей, уже существующих во входных данных из внешнего мира.
При выводе результатов числовых расчётов часто бывает ситуация, когда вместо десятичной дроби, например, 3,7, отображается число 3,6999999999999997. Это происходит из-за того, что далеко не все десятичные дроби можно точно представить в двоичном коде. В случае невозможности записать десятичную дробь точно, используется самое близкое к ней значение, которое можно выразить двоичным числом. При обратном переводе в десятичный формат это значение и даёт такие непонятные длинные дроби.
Перевод десятичных дробей в двоичную систему счисления
Невозможность записать некоторые десятичные дроби в двоичной системе (нулями и единицами) можно привести на примере обычного небольшого десятичного числа 20.1. Целая часть десятичных дробей переводится в двоичный вид точно так же, как целые числа, то есть складыванием положительных степеней двойки так, чтобы в итоге получить искомую сумму:
Получаем, что 2010 = 000101002
Как мы видим по таблице, справа налево вес каждого разряда двоичного числа увеличивается вдвое (положительные степени двойки). Набираем число 20, суммируя результаты этих степеней и отмечая единичками те степени, которые мы использовали. Вариант у нас только один – использовать степени 2 и 4, то есть числа 4 и 16, в сумме как раз дающие 20. Отметив их единичками, получаем двоичное число 00010100 или, если отбросить незначащие нули, 10100.
Дробная часть десятичных дробей переводится в двоичный код точно так же, только используются сумма отрицательных степеней двойки. Отрицательная степень двойки, в отличие от положительной, не увеличивает число вдвое, а уменьшает его в два раза. Продолжим верхнюю таблицу вправо отрицательными степенями:
Эту таблицу мы можем продолжать вправо до бесконечности, но никогда не наберём полные 0.1. В итоге получаем, что 0,110 ≈ 00011001100112 = 0,099975585937510.
Таким образом, 20,110 ≈ 10100,00011001100112.
Хранение чисел с плавающей точкой в компьютере
В компьютере числа двойной точности (в Java тип Double) хранятся в экспоненциальном (т.н. научном, инженерном) формате. Этот формат, например, записывает десятичное число 20,1 как 2,01*101, или сокращённо, как 2,01E+1. То есть, число после буквы E указывает, на сколько знаков следует сдвинуть запятую вправо (или влево, если число отрицательное) в числе, стоящем перед буквой
Переведённое в двоичную систему число 20,1, выглядящее как 10100,0001100110011 в экспоненциальной форме будет выглядеть как 1,01000001100110011 * 2100, или сокращённо 1,01000001100110011E+100 (двоичное 100 — это десятичное 4, то есть сдвигать запятую в числе 1,01000001100110011 следует на 4 знака вправо).
Для хранения этих чисел в компьютере отводится 64 бита (8 байт). Биты распределяются следующим образом. Для знака числа (плюс или минус) отводится 1 бит, для записи порядка (в нашем случае 100) — 11 бит, а для записи мантиссы (в нашем случае 1,01000001100110011) — 52 бита.
Схематично это выглядит так:
Чтобы иметь возможность записывать отрицательные порядки, за ноль в поле порядка принято число 1023 (в двоичном коде — 01111111111), что делит весь возможный диапазон порядков примерно пополам (от -1023 до 0 и от 0 до 1024). Поэтому наш порядок 100 фактически выглядит в битах, как 10000000011, ведь 1023 + 4 = 1027.
Кроме того, по причинам экономии на аппаратных ресурсах, самого левого бита мантиссы «в железе» не существует, но программно он всегда подразумевается. Этот бит принимается равным 1 во всех случаях, кроме случая нахождения в поле порядка специального числа -1023 (значения битов 00000000000). В нашем случае в поле порядка занесено число 4, а значит, левый бит признаётся единичным, и поэтому у нас в мантиссе фактически лежит лишь дробная часть, то есть число 01000001100110011.
Исходя из вышесказанного, для нашего числа 10100,0001100110011 значения всех 64 реальных битов и 1 подразумеваемого таковы:
Несколько слов про магическое число -1023 (значение битов 00000000000) в поле порядка. Как я уже говорил, это число просто устанавливает подразумеваемый бит целой части мантиссы в ноль. Если все остальные биты мантиссы также будут нулевыми, то хранимое таким образом в компьютере число, очевидно, будет являться нулём.
В случае же наличия в мантиссе ненулевых битов при том же числе -1023 в поле порядка, подразумеваемый бит целой части мантиссы также принимается равным нулю, но значение порядка подразумевается равным -1022. Грубо говоря, смена чисел в поле порядка с -1022 на -1023 и обратно влияют только на снятие или установку единичного бита целой части мантиссы, тогда как и то, и другое значение порядка подразумевается равным -1022.
Для чего же нужно было приравнивать целую часть мантиссы к нулю в ущерб возможности организовать ещё один порядок? Дело в том, что насколько бы ни был велик отрицательный порядок, то есть, насколько бы ни было мало выраженное им число, между ним и нулём у нас всегда будет оставаться незаполненная числами дырка.
Это легко понять, представив, как восстанавливается число из экспоненциального формата. Основание системы счисления (2) возводится в степень порядка и умножается на мантиссу. Самое маленькое число типа Double, которое мы можем так выразить, равно 1 * 2-1022. Ведь подразумеваемый бит равен единице, а значит, мантисса не может быть меньше единицы. Постепенно увеличивая мантиссу с 1 до почти 2 мы можем равномерно заполнить числами весь диапазон до предыдущего порядка 1 * 2-1021. Но у нас всегда остаётся незаполненный диапазон с другой стороны — в сторону нуля. И вот его то мы можем целиком заполнить только делая мантиссу меньше единицы. А для этого надо просто переключить подразумеваемый бит в ноль, что мы и делаем!
Существует ещё одно магическое число в поле порядка — 1024 (значение битов 11111111111). В зависимости от содержимого мантиссы, оно означает либо бесконечность (1 / 0 = ∞) либо неопределённость (неупорядоченное число, не число) (0 / 0 = NaN). Если мантисса нулевая — перед нами бесконечность, иначе — неопределённость.
Правильное округление в Java
Конечно, в Java есть функции, которые умеют округлять числа до нужного знака после запятой. Но иногда может встретится число, которое в десятичном виде округляется в большую сторону, а будучи записано в двоичном виде становится чуть меньше, и уже по правилам должно округляться в меньшую сторону. В качестве примера такого числа можно привести число 13,5465. Очевидно, что оно должно округляться в большую сторону до 13,547. Но при записи в двоичный код число становится равным 13,5464999…, и оно уже должно округляться в меньшую сторону до 13,546.
Но ведь мы мыслим и думаем в десятичной системе счисления, а значит, хотим видеть числа, всегда округлённые в правильную сторону вне зависимости от того, как они там внутри компьютера выглядят. Чтобы провести такое округление и отобразить число правильно, мы должны написать на Java небольшой код:
public BigDecimal roundUp(double value, int digits){ return new BigDecimal(""+value).setScale(digits, BigDecimal.ROUND_HALF_UP); }
Теперь мы можем применить этот метод, например, если программируем под Андроид, при выводе результата result в поле EditText активности:
EditText.setText(String.valueOf(roundUp(result,3)));
Рассмотрим, как же это работает. Метод roundUp принимает подлежащее округлению число value типа double и желаемое количество знаков после запятой digits типа int. Возвращает метод результат в виде числа типа BigDecimal. Тип BigDecimal хранит данные не в виде двоичного представления числа с плавающей запятой, о котором я рассказывал выше, а в виде числа с фиксированной запятой. Число с фиксированной запятой содержит в себе два отдельных целых числа. Это исходное число, но без запятой, и число, содержащее количество знаков после запятой. А целые числа всегда можно записать в двоичный формат абсолютно точно. Так что на выходе мы потенциально можем получить любую точную десятичную дробь.
Внутри нашего метода создаётся объект BigDecimal, который принимает на вход число с плавающей запятой value типа double, и методом setScale округляет его. Но при обычном использовании этого метода число округляется всё по тем же правилам, что приводит к неправильному округлению. В нашем же методе есть одна хитрость. Чтобы округление всегда происходило в нужную сторону, значение на вход метода BigDecimal мы передаём не в виде числа, а в виде строки. На этапе преобразования числа в строку, язык Java сам автоматически округляет число с достаточным количеством знаков, до степени, имеющей смысл (округляя весь хвост, начинающийся с последовательности девяток или нулей, подозревая в этом погрешность). Таким образом, мы подаём на вход BigDecimal строку с уже исправленной погрешностью.
Чтобы перевести число в строку, мы использовали трюк ""+value. При сложении числа со строкой (даже пустой) результат окажется тоже строкой.
Сравним результаты округления без предварительного перевода числа в строку и с переводом.
Вариант 1:
value = 13.5465 digits = 3 return new BigDecimal(value).setScale(digits, BigDecimal.ROUND_HALF_UP);
Результат: 13,546 — неправильно!
Вариант 2:
value = 13.5465 digits = 3 return new BigDecimal(""+value).setScale(digits, BigDecimal.ROUND_HALF_UP);
Результат: 13,547 — правильно.
Вообще, такую удобную особенность преобразования double в строку, как исправление погрешности, можно объяснить тем, что строки обычно используются только для отображения информации, и не участвуют в расчётах, не влияют на них. Поэтому не будет большого криминала даже если погрешность будет исправлена ошибочно. Всё равно такая внезапно округлённая строка при правильной архитектуре приложения не будет участвовать в дальнейших расчётах, но покажет пользователю очень близкий к истинному результат, к тому же более удобный для восприятия.
Если нам нужно вести расчёты вообще без погрешностей, возникающих из-за двоичного представления чисел, то вместо переменных типа Double следует сразу использовать тип BigDecimal, и все расчёты производить с использованием методов этого типа. Обычно такие расчёты используют в бухгалтерии, где при любых суммах для правильной отчётности важна точность до копейки. В других же случаях такой неестественной точности, как правило, не требуется, поскольку погрешность двоичного представления теряется на фоне погрешностей, уже существующих во входных данных из внешнего мира.
Этот комментарий был удален автором.
ОтветитьУдалитьВроде как хороший метод, одно но: он ВСЕГДА округляет до числа выше, что не нужно в тех случаях, когда...когда это не нужно)
ОтветитьУдалитьПроще ручками задавать, пожалуй, критерии округления, ибо толком способа адекватного округления я лично так и не нашёл
Так если задать вместо ROUND_HALF_UP нужный тип округления, то, думаю, всё будет работать тоже правильно.
УдалитьТак в статье и говорилось про правильный, не машинный, вариант округления)
УдалитьВедь, в противном случае, мы бы и увидели там нужный тип округления у вас
Во всяком случае, спасибо за метод)
А как округлить до конкретного числа, например 0.43 до 0.5 ? Нигде такого не нашёл.
ОтветитьУдалитьhttp://developer.alexanderklimov.ru/android/java/bigdecimal.php
ОтветитьУдалитьROUND_CEILING вместо ROUND_HALF_UP