Search code examples
javabigdecimal

Calculated BigDecimal values returning correct value but adding extra zeros onto number


As part of a school assignment, I am writing a program that will help determine the total price of a bag of bagels and the values that are being produced by my getTotalPrice() method is pushing out the correct values, but for some of them, there are a couple of zeros added to the end value. I tried using the setScale() method to round up the decimal to two spaces, but I was told that this was not a desirable solution. I've never used BigDecimals before, so I believe there's something I am missing within my code, any suggestions? Please let me know if more information is needed, I wasn't too sure what code to include/exclude.

// test that fails
@ParameterizedTest
@ArgumentsSource(BagArgumentsProvider_getTotalPrice.class)
void getTotalPrice_assertEquals_DiffBagelAmounts(Bag bag, BigDecimal cost) {
    assertEquals(cost, bag.getTotalPrice());
}

//some to the produced errors
expected 3.99 but was 3.9900
expected 1.99 but was 1.9950


/**
 * A bag of any quantity of a single type of bagel.
 *
 * @author Ellen Spertus
 *
 */
public class Bagel {
    private final Type type;
    private Category currentCategory;
    // Visible for Testing
    protected static final BigDecimal OLD_FASHIONED_PRICE = new BigDecimal("0.50");
    protected static final BigDecimal GOURMET_PRICE = new BigDecimal("0.70");
    protected static final BigDecimal DAY_OLD_PRICE = new BigDecimal("0.35");


    enum Type {
        PLAIN("plain", Category.OLD_FASHIONED), POPPY_SEED("poppy seed",
                Category.OLD_FASHIONED), SESAME_SEED("sesame seed", Category.OLD_FASHIONED), ONION(
                        "onion", Category.OLD_FASHIONED), EVERYTHING("everthing",
                                Category.OLD_FASHIONED), ASIAGO("asiago",
                                        Category.GOURMET), BLUEBERRY("blueberry",
                                                Category.GOURMET), CINNAMON_RAISIN(
                                                        "cinnamon raisin",
                                                        Category.GOURMET), SUN_DRIED_TOMATO(
                                                                "sun dried tomato",
                                                                Category.GOURMET);

        private final String name;
        private final Category category;

        private Type(String name, Category category) {
            this.name = name;
            this.category = category;
        }// end constructor

        @Override
        public String toString() {
            return name;
        }// end toString

    }// end enum

    enum Category {
        OLD_FASHIONED(OLD_FASHIONED_PRICE), GOURMET(GOURMET_PRICE), DAY_OLD(DAY_OLD_PRICE);

        private final BigDecimal price;

        private Category(BigDecimal price) {
            this.price = price;
        }// end constructor

        public BigDecimal getPrice() {
            return price;
        }// end getPrice
    }// end enum

    /**
     * Constructs a bagel of the given type.
     *
     * @param the type
     */
    public Bagel(Type type) {
        this.type = type;
        this.currentCategory = type.category;
    }// end constructor

    /**
     * Gets the type of the bagel.
     *
     * @return the type of bagel
     */
    public Type getType() {
        return type;
    }// end getType

    /**
     * Gets the category of the bagel.
     *
     * @return the category
     */
    public Category getCategory() {
        return currentCategory;
    }// end get Category

    /**
     * Marks down this bagel.
     */
    public void markDown() {
        currentCategory = Category.DAY_OLD;
    }// end markDown
}// end class

 /**
 * A type of bagel.
 *
 * @author Ellen Spertus
 */
public class Bag {
    // We provide a Baker's Dozen: 13 bagels for the price of 12.
    private static final int BUY_ONE_GET_ONE_FREE_QUANTITY = 13;

    // If the Baker's Dozen discount doesn't apply, we give a percentage discount.
    private static final int BULK_DISCOUNT_MINIMUM = 6;
    private static final BigDecimal BULK_DISCOUNT_PERCENTAGE = new BigDecimal(".05");
    private static final BigDecimal BULK_DISCOUNT_MULTIPLIER =
            new BigDecimal("1.00").subtract(BULK_DISCOUNT_PERCENTAGE);

    private final Bagel bagel;
    private final int quantity;

    /**
     * Constructs a bag with the given quantity of the given type of bagel.
     *
     * @param bagel the type of bagel
     * @param quantity the quantity
     * @throws IllegalArguementException if the quantity passed is out of range from 1 to 13
     */
    public Bag(Bagel bagel, int quantity) {
        this.bagel = bagel;
        this.quantity = quantity;
        if (this.quantity > 13) {
            throw new IllegalArgumentException("Orders must be under 13 bagels");
        }

        if (this.quantity < 1) {
            throw new IllegalArgumentException("Not a valid order");
        }

    }// end constructor

    /**
     * Gets the bagel held in this bag.
     *
     * @return the bagel
     */
    public Bagel getBagel() {
        return bagel;
    }// end getBagel

    /**
     * Gets the number of bagels in this bag.
     *
     * @return the number of bagels in this bag
     */
    public int getQuantity() {
        return quantity;
    }// end getQuantity

    /**
     * Gets the total price for this bag of bagels. This may be less than the per-bagel price times
     * the number of bagels because of quantity discounts.
     *
     * @return the total price
     */
    public BigDecimal getTotalPrice() {
        BigDecimal undiscountedPrice = getPerBagelPrice().multiply(new BigDecimal(quantity));
        if (quantity == BUY_ONE_GET_ONE_FREE_QUANTITY) {
            undiscountedPrice = undiscountedPrice.subtract(getPerBagelPrice());
        }
        if (quantity >= BULK_DISCOUNT_MINIMUM) {
            undiscountedPrice = undiscountedPrice.multiply(BULK_DISCOUNT_MULTIPLIER);
        }
        //undiscountedPrice = undiscountedPrice.setScale(2, RoundingMode.HALF_UP);
        return undiscountedPrice;
    }// end getTotalPrice

    /**
     * Returns the price associated with the bagel type.
     *
     * @return price based on bagel type
     */
    public BigDecimal getPerBagelPrice() {
        return bagel.getCategory().getPrice();
    }// end getPerBagelPrice
}

Solution

  • oh! I believe I've found the problem! the .multiply() method increases the decimal precision from 2 to 4. Solution then was to just create a new MathContext with precision of 3 and then use RoundingMode.Half_DOWN to push out the correct price value.

    Let me know if there's better solution.

    public BigDecimal getTotalPrice() {
        System.out.println(getPerBagelPrice());
        BigDecimal undiscountedPrice = getPerBagelPrice().multiply(new BigDecimal(quantity));
        if (quantity == BUY_ONE_GET_ONE_FREE_QUANTITY) {
            undiscountedPrice = undiscountedPrice.subtract(getPerBagelPrice());
        }
        if (quantity >= BULK_DISCOUNT_MINIMUM && quantity != 13) {
            undiscountedPrice = undiscountedPrice.multiply(BULK_DISCOUNT_MULTIPLIER, new MathContext(3, RoundingMode.HALF_DOWN));
        }
        System.out.println(undiscountedPrice);
        return undiscountedPrice;
    }