Печать точного значения вещественного числа в Java
Число 12.000001 не представимо точно в вещественном формате. Хранимое значение равно 12.0000009999999992515995472786016762256622314453125. Вопрос в том как его распечатать точно средствами языка?
Я попробовал такие способы:
double a = 12.000001;
System.out.println("- " + a);
System.out.println(String.format("%%f %f", a));
System.out.println(String.format("%%.20f %.20f", a));
System.out.println(String.format("%%.60f %.60f", a));
System.out.println(String.format("%%g %g", a));
System.out.println(String.format("%%.20g %.20g", a));
System.out.println(String.format("%%.60g %.60g", a));
Получается что точной печати не получается:
- 12.000001 %f 12.000001 %.20f 12.00000100000000000000 %.60f 12.000001000000000000000000000000000000000000000000000000000000 %g 12.0000 %.20g 12.000001000000000000 %.60g 12.0000010000000000000000000000000000000000000000000000000000
P.S. Зачем это нужно? Для отладки например. Или чтобы написать код, иллюстрирующий проблему в вопросе При сравнении двух вещественных чисел получаем разный результат.
Ответы (2 шт):
Можно воспользоваться BigDecimal
:
String.format("%%.51g %.51g", new BigDecimal(a))
Интересно, что String.format
"знает" о количестве значимых разрядов в float (23 бита т.е. приблизительно 7 десятичных знаков) и double (52 бита и ~15 десятичных знаков) и не показывает незначимые.
15 разрядов для 12.0000009999999992515995472786016762256622314453125
получается 12.0000010000000
. Без ненужных нулей получается 12.000001
.
У BigDecimal
нет этой проблемы, т.к. BigDecimal
хранит полностью все разряды числа и в случаи инициализации из double
делает точную конвертацию значения представленного в double
без округлений.
Вот что по этому поводу пишет документация на BigDecimal(double)
:
The results of this constructor can be somewhat unpredictable. One might assume that writing new BigDecimal(0.1) in Java creates a BigDecimal which is exactly equal to 0.1 (an unscaled value of 1, with a scale of 1), but it is actually equal to 0.1000000000000000055511151231257827021181583404541015625. This is because 0.1 cannot be represented exactly as a double (or, for that matter, as a binary fraction of any finite length). Thus, the value that is being passed in to the constructor is not exactly equal to 0.1, appearances notwithstanding.
При форматировании можно использовать все доступные разряды хранимого числа.
Точное значения вещественного числа double
в Java, в том смысле, что напечатанное число отличается от хранимого меньше чем на 0.5 ulp (0.5 единицы последнего разряда) может быть выдан форматами: "%.18g", "%.13a" или методом .toString()
.
Это, в частности, означает, что любой корректный ввод, этого напечатанного числа, приведёт к точному совпадению хранимого числа.
Как вариант, для особых ценителей, преобразование в BigDecimal и формат уже только "%.18g", что б не плодить не имеющих смысла цифр.
Для примера, возьмём пять идущих подряд double
в районе 12.000001:
public static void main(String[] args) {
double[] a = new double [5];
a[2] = 12.000001;
a[3] = Math.nextAfter(a[2], Double.MAX_VALUE);
a[4] = Math.nextAfter(a[3], Double.MAX_VALUE);
a[1] = Math.nextAfter(a[2], -Double.MAX_VALUE);
a[0] = Math.nextAfter(a[1], -Double.MAX_VALUE);
for(String f : new String[]{"", "%f", "%.18g", "%.13a"}) {
String out = "Double format " + f + "\n";
for (Double d : a) {
String s = String.format(f, d);
BigDecimal bd = new BigDecimal(d);
BigDecimal bs;
if(f == "") {
s = "" + d;
} else {
s = String.format(f, d);
}
if(s.startsWith("0x") || s.startsWith("0X")) {
// From https://stackoverflow.com/a/27235713/8585880
bs = toBigDecimal(s);
} else {
bs = new BigDecimal(s);
}
out += s + " fmterr:" +
(bd.subtract(bs)).doubleValue()/Math.ulp(d) +
" ulp\n";
}
System.out.println(out);
}
for(String f : new String[]{"%.18g", "%.51g"}) {
String out = "BigDecimal fortmat " + f + "\n";
for (Double d : a) {
out += String.format(f, new BigDecimal(d)) + "\n";
}
System.out.println(out);
}
}
Получаем:
Double format
12.000000999999996 fmterr:-0.169512186314752 ulp
12.000000999999997 fmterr:0.267537860263936 ulp
12.000001 fmterr:-0.421312 ulp
12.000001000000001 fmterr:0.015738046578688 ulp
12.000001000000003 fmterr:-0.110161860263936 ulp
Double format %f
12.000001 fmterr:-2.421312 ulp
12.000001 fmterr:-1.421312 ulp
12.000001 fmterr:-0.421312 ulp
12.000001 fmterr:0.578688 ulp
12.000001 fmterr:1.578688 ulp
Double format %.18g
12.0000009999999960 fmterr:-0.169512186314752 ulp
12.0000009999999970 fmterr:0.267537860263936 ulp
12.0000010000000000 fmterr:-0.421312 ulp
12.0000010000000010 fmterr:0.015738046578688 ulp
12.0000010000000030 fmterr:-0.110161860263936 ulp
Double format %.13a
0x1.80000218def3fp3 fmterr:0.0 ulp
0x1.80000218def40p3 fmterr:0.0 ulp
0x1.80000218def41p3 fmterr:0.0 ulp
0x1.80000218def42p3 fmterr:0.0 ulp
0x1.80000218def43p3 fmterr:0.0 ulp
BigDecimal fortmat %.18g
12.0000009999999957
12.0000009999999975
12.0000009999999993
12.0000010000000010
12.0000010000000028
BigDecimal fortmat %.51g
12.0000009999999956988858684781007468700408935546875
12.0000009999999974752427078783512115478515625000000
12.0000009999999992515995472786016762256622314453125
12.0000010000000010279563866788521409034729003906250
12.0000010000000028043132260791026055812835693359375
Конечно, double "%.18g" и .toString()
не идеал, но все числа чётко различаются, хотя последний разряд изменяется неравномерно, то на 1, то на 3.
Идеальным, для разрешения проблем или переноса данных - шестнадцатиричный формат, всё кратко и точно.
А для десятичного вывода, идеальным был бы вывод не этого мусора от BigDecimal "%.51g", а вывод ошибки округления последней операции или форматного преобразования этого числа (для Java это к ближайшему c привязкой к четному), т.е. 0.5 ulp. К примеру, выдавать погрешность по ГОСТ, как-то так:
double with 0.5ulp
12.0000009999999960(9)
12.0000009999999970(9)
12.0000010000000000(9)
12.0000010000000010(9)
12.0000010000000030(9)
BigDecimal with 0.5ulp
12.0000009999999957(9)
12.0000009999999975(9)
12.0000009999999993(9)
12.0000010000000010(9)
12.0000010000000028(9)
Т.е., в реальной жизни, возможно последняя операция или форматный ввод этого числа получил не 12.000001, а нечто в интервале 12.0000009999999984..12.0000010000000002.