Strange behavior when rounding floating point numbers in Java

65 Views Asked by At

I need to implement a small metric converter class which has the main task to convert metrics. For example convert meters to miles. An additional acceptance criteria is to round the results with one digit accuracy except if the number is less than 0.1, then it should be rounded to 2 digits of accuracy.

I have programmed for rounding following method:

public static String round(double input) {
        DecimalFormat df;

        if(input < 0.1) {
            df = new DecimalFormat("#.##");
        } else {
            df = new DecimalFormat("#.#");
        }

        df.setRoundingMode(RoundingMode.HALF_UP);
        Locale currentLocale = LocaleContextHolder.getLocale();
        df.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(currentLocale));

        return df.format(input);
    }

Now i wrote some unit tests in order to validate the code.

package de.helper;

import org.junit.jupiter.api.Test;
import org.springframework.context.i18n.LocaleContextHolder;

import java.util.Locale;

import static org.assertj.core.api.Assertions.assertThat;


class MetricsConverterTest {

    @Test
    public void roundNumberWith1DigitAccuracy() {

        //arrange
        LocaleContextHolder.setLocale(Locale.GERMAN);

        //act
        String actualRoundDown = MetricsConverter.round(10.123456);
        String actualRoundUp = MetricsConverter.round(10.153456);

        //assert
        assertThat(actualRoundDown).isEqualTo("10,1");
        assertThat(actualRoundUp).isEqualTo("10,2");
    }

    @Test
    public void roundSmallNumberWith2DigitAccuracy() {

        //arrange
        LocaleContextHolder.setLocale(Locale.GERMAN);

        //act
        String actualRoundDown = MetricsConverter.round(0.091);
        String actualRoundUp = MetricsConverter.round(0.076);

        //assert
        assertThat(actualRoundDown).isEqualTo("0,09");
        assertThat(actualRoundUp).isEqualTo("0,08");

    }

    @Test
    public void roundSmallNumberWith2DigitAccuracyStrangeEdgeCase() {

        //arrange
        LocaleContextHolder.setLocale(Locale.GERMAN);

        //act
        String actualRoundUp = MetricsConverter.round(0.075);

        //assert
        assertThat(actualRoundUp).isEqualTo("0,08");
    }

    @Test
    public void roundSmallNumberEdgeCase1DigitAccuracy() {

        //arrange
        LocaleContextHolder.setLocale(Locale.GERMAN);

        //act
        String actualRoundUp = MetricsConverter.round(0.095);

        //assert
        assertThat(actualRoundUp).isEqualTo("0,1");
    }
}

But the test method roundSmallNumberWith2DigitAccuracyStrangeEdgeCase fails. The actual output is "0.07" but for my understanding it should be 0.08. What do I miss?

2

There are 2 best solutions below

0
k314159 On BEST ANSWER

The value 0.075 cannot be represented by a double with absolute precision. You can see that if you print the value of new BigDecimal(0.075):

0.07499999999999999722444243843710864894092082977294921875

That number is definitely closer to 0.07 than it is to 0.08, hence you get the result that you see.

5
Bohemian On

You can't due to double being an imprecise representation of a number.

However, you can make it work if you use a precise representation. Use BigDecimal and first round it to two places:

public static String round(double input) {
    BigDecimal n = BigDecimal.valueOf(input).setScale(2, RoundingMode.HALF_UP); // will be 0.75
    // your code here
}

Since you're learning, I've left the rest of the implementation up to you to complete.