5

I am working on a business logic where I need to divide and multiply BigDecimal variable to produce business result but I am facing the problem to maintain the accuracy.

Actual business I can't put here so I created a sample program and included here. I need to use only BigDecimal so I am strict to it but I am open to use any scale, mode or any thing which help me to get the maximum accuracy.

Suggestions are always welcome.

Sample Code

public class Test {
    public static void main(String[] args) {
        BigDecimal hoursInADay = new BigDecimal("24");
        BigDecimal fraction = BigDecimal.ONE.divide(hoursInADay, 3,
                RoundingMode.HALF_UP);
        BigDecimal count = BigDecimal.ZERO;

        for (int i = 1; i <= 24; i++) {
            count = count.add(fraction);
        }

        if (BigDecimal.ONE.equals(count)) {
            // accuracy level 100%
        }
    }
}
dom
  • 1,086
  • 11
  • 24
  • I dont think more accuracy then BigDecimal is possible because the base2 logic limitations of arithmetic in computers – Starmixcraft Apr 16 '19 at 08:12
  • 1
    Possible duplicate of [Java more precision in arithmetic](https://stackoverflow.com/questions/12172630/java-more-precision-in-arithmetic) – Starmixcraft Apr 16 '19 at 08:14
  • @Starmixcraft my need to execute the if, is this possible by changing the code ? – dom Apr 16 '19 at 08:19
  • Does this example follow your real use-case? If you have to compute the `fraction` value and then be able to add it back to the original value then it's unlikely that you can achieve it. `1/24` is non-terminating, regardless of how precise the data type is. If you must store the value, maybe you should look into symbolic calculations... Otherwise it boils down to your domain knowledge which will guide the scales and errors you may expect to deal with. – ernest_k Apr 16 '19 at 08:19

2 Answers2

3

Just an experiment (and for fun), I attempted to implement this Fraction class, which wraps BigDecimal but shuns division until the final result is required.

The method implementations are based on:

  • add: a/b + c/d = (ad + bc)/bd
  • multiply: (a/b) * (c/d) = ac/bd
  • divide: (a/b)/(c/d) = ad/bc

This is not used because BigDecimal has insufficient accuracy, but because premature division necessarily leads to rounding errors in case of non-terminating values.

Code:

class Fraction {

    private final BigDecimal numerator;
    private final BigDecimal denominator;

    public Fraction(BigDecimal numerator, BigDecimal denumerator) {
        this.numerator = numerator;
        this.denominator = denumerator;
    }

    public static final Fraction ZERO = new Fraction(BigDecimal.ZERO, 
                            BigDecimal.ONE);
    public static final Fraction ONE = new Fraction(BigDecimal.ONE, 
                            BigDecimal.ONE);

    public static Fraction of(BigDecimal numerator) {
        return new Fraction(numerator, BigDecimal.ONE);
    }

    public static Fraction of(BigDecimal numerator, BigDecimal denominator) {
        return new Fraction(numerator, denominator);
    }

    public Fraction add(Fraction other) {
        return Fraction.of(other.denominator.multiply(this.numerator)
                                .add(other.numerator.multiply(this.denominator)),
                           this.denominator.multiply(other.denominator));
    }

    public Fraction multiply(Fraction other) {
        return new Fraction(this.numerator.multiply(other.numerator), 
                            this.denominator.multiply(other.denominator));
    }

    public Fraction divide(Fraction other) {
        return new Fraction(this.numerator.multiply(other.denominator), 
                            this.denominator.multiply(other.numerator));
    }

    public BigDecimal value() {
        try {
            return this.numerator.divide(this.denominator);
        } catch (ArithmeticException ae) {
            return this.numerator.divide(this.denominator, 6, 
                        RoundingMode.HALF_UP);
        }
    }

    @Override
    public String toString() {
        return String.format("%s/%s", this.numerator, this.denominator);
    }
}

And using it to perform your original calculations:

public static void main(String[] args) {
    Fraction twentyFour = Fraction.of(BigDecimal.valueOf(24));
    Fraction fraction = Fraction.ONE.divide(twentyFour);
    System.out.println("Fraction = " + fraction);

    Fraction count = new Fraction(BigDecimal.ZERO, BigDecimal.ONE);
    for (int i = 1; i <= 24; i++) {
        count = count.add(fraction);
    }

    if (BigDecimal.ONE.equals(count.value())) {
        System.out.println("100%");
    } else {
        System.out.println(count);
    }
}

Output:

Fraction = 1/24
100%

It's important to note that this is in no way optimized. For example, fractions are not simplified (1/24 + 1/24 will be stored as 48/576 instead of 1/12, and that may have a non-negligible storage and compute cost)

ernest_k
  • 44,416
  • 5
  • 53
  • 99
0

I'm not sure if this has any sense, but i will just post some changes to your code

public static void main(String[] args) {
    BigDecimal hoursInADay = new BigDecimal("24");
    BigDecimal fraction = BigDecimal.ONE.divide(hoursInADay, MathContext.DECIMAL64.getPrecision(),
            RoundingMode.HALF_UP);
    BigDecimal count = BigDecimal.ZERO;

    for (int i = 1; i <= 24; i++) {
        count = count.add(fraction);
    }
    count = count.round(MathContext.DECIMAL32);
    System.out.println(BigDecimal.ONE.compareTo(count) == 0);
}

Operation of dividing is performed with more precision than we need in actual result. These numbers are to adjust of course, based on your needs. That should (?) give you more precise final result. Also i used compareTo method because it sees 1 and 1.000 as equal numbers, when equals method don't.

That's just an proposition, wonder what you think about it.

Mershel
  • 542
  • 1
  • 9
  • 17