Метод умножения Java 8 BigDecimal теряет точность при инвертировании

Java 8 здесь. Согласно Google:

Итак, я создал этот небольшой Java-код:

public class MeasurementConverter {
    private static final int SCALE = 2;

    public static void main(String[] args) {
        new MeasurementConverter().run();
    }

    private void run() {
        BigDecimal hundredLbs = new BigDecimal(100.00);
        BigDecimal hundredInches = new BigDecimal(100.00);

        System.out.println(hundredLbs + " pounds is " + poundsToKilos(hundredLbs) + " kilos and converts back to " + kilosToPounds(poundsToKilos(hundredLbs)) + " pounds.");
        System.out.println(hundredInches + " inches is " + inchesToMeters(hundredInches) + " meters and converts back to " + metersToInches(inchesToMeters(hundredInches)) + " inches.");
    }

    private BigDecimal metersToInches(BigDecimal meters) {
        return meters.multiply(new BigDecimal("39.3701").setScale(SCALE, BigDecimal.ROUND_HALF_UP));
    }

    private BigDecimal inchesToMeters(BigDecimal inches) {
        return inches.multiply(new BigDecimal("0.0254").setScale(SCALE, BigDecimal.ROUND_HALF_UP));
    }

    private BigDecimal kilosToPounds(BigDecimal kilos) {
        return kilos.multiply(new BigDecimal("2.20462").setScale(SCALE, BigDecimal.ROUND_HALF_UP));
    }

    private BigDecimal poundsToKilos(BigDecimal pounds) {
        return pounds.multiply(new BigDecimal("0.45359").setScale(SCALE, BigDecimal.ROUND_HALF_UP));
    }
}

Когда он запускается, он распечатывает:

100 pounds is 45.00 kilos and converts back to 99.0000 pounds.
100 inches is 3.00 meters and converts back to 118.1100 inches.

В то время как я ожидал его распечатать:

100.00 pounds is 45.00 kilos and converts back to 100.00 pounds.
100.00 inches is 3.00 meters and converts back to 100.00 inches.

Все, что меня волнует, это то, что преобразование в имперские ‹-> метрические единицы выполняется с точностью до 2 знаков после запятой, а конечные результаты всегда точны до 2 знаков после запятой. Может ли кто-нибудь увидеть, в чем я ошибаюсь и в чем заключается исправление?


person smeeb    schedule 19.02.2018    source источник
comment
Обратите внимание, что 100 фунтов - это 45,36 кг, а 100 дюймов - это 2,54 метра.   -  person Oliver Charlesworth    schedule 19.02.2018
comment
Помните, что передача double в new BigDecimal теряет точность, потому что вы используете не точное значение, а скорее ближайшее представимое значение как двойное. Вместо этого используйте new BigDecimal("39.37") и т. Д. Также помните, что эти обратные величины не точны, поэтому 39,37 * 0,0254 = 0,999998, а не 1.   -  person Andy Turner    schedule 19.02.2018
comment
Спасибо @AndyTurner (+1), однако, пожалуйста, просмотрите мои обновления с предложенными вами изменениями. Конверсии по-прежнему путь отключены! Любые идеи?   -  person smeeb    schedule 19.02.2018
comment
Мой предыдущий комментарий - подсказка.   -  person Oliver Charlesworth    schedule 19.02.2018
comment
Увеличьте масштаб.   -  person Andy Turner    schedule 19.02.2018
comment
Разве аргументы SCALE и HALF_UP для умножения? вы добавляете его только в BigDecimal   -  person Marcos Vasconcelos    schedule 20.02.2018


Ответы (4)


У вас перепутаны скобки. Учти это:

kilos.multiply(new BigDecimal("2.20462").setScale(SCALE, BigDecimal.ROUND_HALF_UP));

Здесь вы сначала устанавливаете масштаб на 2.20462, что дает 2.20, а затем умножаете.

Теперь рассмотрим обратное преобразование:

pounds.multiply(new BigDecimal("0.45359").setScale(SCALE, BigDecimal.ROUND_HALF_UP));

Здесь вы эффективно умножаете на 0.45. И 2.2*0.45=0.99, который объясняет результат.

Вы установили масштаб по результату умножения, а не по мультипликатору. В основном последняя скобка стоит не на том месте. Должно получиться что-то вроде:

pounds.multiply(new BigDecimal("0.45359")).setScale(SCALE, BigDecimal.ROUND_HALF_UP);

Вот исправленный код.

person lexicore    schedule 19.02.2018
comment
Но зачем вообще возиться с setScale? - person Oliver Charlesworth; 20.02.2018
comment
@OliverCharlesworth Это вопрос, как ОП. - person lexicore; 20.02.2018
comment
В самом деле, я предлагаю просто удалить setScale;) (И затем поместить его в форматирование вывода) - person Oliver Charlesworth; 20.02.2018

Разве аргументы SCALE и HALF_UP для умножения? вы добавляете его только в BigDecimal.

Я думаю, ты хочешь:

inches.multiply(new BigDecimal("0.0254"), new MathContext(SCALE, RoundingMode.HALF_UP));

Изменение SCALE на 8 и использование вашего кода с MathContext привело к:

100 pounds is 45.35900 kilos and converts back to 99.999359 pounds.
100 inches is 2.5400 meters and converts back to 100.00005 inches.
person Marcos Vasconcelos    schedule 19.02.2018
comment
Спасибо @Marcos (+1) - насколько дорогой / тяжелый этот новый экземпляр MathContext? Есть ли способ создать его только один раз при запуске приложения? Я спрашиваю, потому что в дикой природе эти методы будут использоваться все ... - person smeeb; 20.02.2018
comment
Я думаю, что MathContext будет применяться только ПОСЛЕ расчета - person Marcos Vasconcelos; 20.02.2018

Ваши коэффициенты пересчета отключены. Вы округляете коэффициенты пересчета до двух знаков после запятой:

private static final int SCALE = 2;
[...]    
new BigDecimal("0.0254").setScale(SCALE, BigDecimal.ROUND_HALF_UP)

Таким образом, «0,0254» округляется до 0,03, что не соответствует вашему «обратному» коэффициенту преобразования 39,37. Вот почему ваш разворот не равен исходному значению.

person Lykanion    schedule 19.02.2018

Вы выполняете округление в середине преобразования. Следовательно, входное значение для обратной операции уже отключено.

Если вы хотите отображать значения как:

100.00 pounds is 45.00 kilos and converts back to 100.00 pounds.
100.00 inches is 3.00 meters and converts back to 100.00 inches.

Удалите setScale из всех ваших частных методов. Например:

private BigDecimal metersToInches(BigDecimal meters) {
    return meters.multiply(new BigDecimal("39.3701"));
}

Затем вы можете просто отформатировать вывод в желаемом формате:

private BigDecimal formatOutput(BigDecimal value) {
    return value.setScale(0, BigDecimal.ROUND_HALF_UP).setScale(SCALE);
}

Наконец-то:

System.out.println(hundredLbs + " pounds is " + formatOutput(poundsToKilos(hundredLbs)) + " kilos and converts back to " + formatOutput(kilosToPounds(poundsToKilos(hundredLbs))) + " pounds.");
System.out.println(hundredInches + " inches is " + formatOutput(inchesToMeters(hundredInches)) + " meters and converts back to " + formatOutput(metersToInches(inchesToMeters(hundredInches))) + " inches.");

Дает:

100.00 pounds is 45.00 kilos and converts back to 100.00 pounds.
100.00 inches is 3.00 meters and converts back to 100.00 inches.

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

private BigDecimal formatOutput(BigDecimal value) {
    return value.setScale(SCALE, BigDecimal.ROUND_HALF_UP);
}

Которая распечатает значения следующим образом (я бы сказал, что это лучше и правильно!)

100.00 pounds is 45.36 kilos and converts back to 100.00 pounds.
100.00 inches is 2.54 meters and converts back to 100.00 inches.
person iaforek    schedule 19.02.2018