MathUtils.java

package com.tradecloud.domain.base.utils;

import org.apache.commons.lang3.StringUtils;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.NumberFormat;
import java.util.Locale;
import java.util.Map;

/**
 *
 */
public class MathUtils {

    public final static int SCALE_DISPLAY = 2;

    public final static int SCALE_FOUR = 4;

    public final static int SCALE_ACCURATE = 12;

    public final static int SCALE = SCALE_ACCURATE;

    public final static int SCALE_VERY_ACCURATE = 19;

    public final static int SCALE_VERY_VERY_ACCURATE = 19;

    public final static RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP;

    // Use this for calculations so accuracy is not lost
    public final static MathContext MC_ACCURATE = new MathContext(SCALE_ACCURATE, ROUNDING_MODE);

    public final static MathContext MC_VERY_VERY_ACCURATE = new MathContext(SCALE_VERY_VERY_ACCURATE, ROUNDING_MODE);

    // Use this MathContext for results that get displayed to the user
    public final static MathContext MC = new MathContext(SCALE, ROUNDING_MODE);

    // Use this MathContext for results that get displayed to the user
    public final static MathContext MC_DISPLAY = new MathContext(SCALE_DISPLAY, ROUNDING_MODE);

    public static final BigDecimal ZERO = BigDecimal.ZERO.setScale(2);

    public static BigDecimal ONE_HUNDRED = new BigDecimal(100);

    public static BigDecimal multiply(BigDecimal bd1, BigDecimal bd2) {
        BigDecimal bd = bd1.multiply(bd2);
        return bd.setScale(SCALE, ROUNDING_MODE);
    }

    public static BigDecimal multiplyVA(BigDecimal bd1, BigDecimal bd2) {
        BigDecimal bd = bd1.multiply(bd2);
        return bd.setScale(SCALE_VERY_ACCURATE, ROUNDING_MODE);
    }

    public static BigDecimal divide(BigDecimal bd1, BigDecimal bd2) {
        return bd1.divide(bd2, SCALE, ROUNDING_MODE);
    }

    public static BigDecimal safeDivide(BigDecimal bd1, BigDecimal bd2) {
        return bd2.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : divide(bd1, bd2);
    }

    public static BigDecimal divideVA(BigDecimal bd1, BigDecimal bd2) {
        return bd1.divide(bd2, SCALE_VERY_ACCURATE, ROUNDING_MODE);
    }

    public static BigDecimal safeDivideVA(BigDecimal bd1, BigDecimal bd2) {
        return bd2.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : divideVA(bd1, bd2);
    }

    public static BigDecimal safeDivide(BigDecimal quantity, BigDecimal bigDecimal, int scale) {
        return bigDecimal.compareTo(BigDecimal.ZERO) == 0 ? BigDecimal.ZERO : quantity.divide(bigDecimal, scale, ROUNDING_MODE);
    }

    public static BigDecimal setScale(BigDecimal bg, MathContext mc) {
        return bg.setScale(mc.getPrecision(), mc.getRoundingMode());
    }

    public static BigDecimal setScale(BigDecimal bg, int scale) {
        return bg.setScale(scale, ROUNDING_MODE);
    }

    public static BigDecimal setDisplayScale(BigDecimal bg) {
        if (bg != null) {
            return bg.setScale(MC_DISPLAY.getPrecision(), MC_DISPLAY.getRoundingMode());
        } else {
            return bg;
        }
    }

    public static BigDecimal setAccurateScale(BigDecimal bg) {
        return bg.setScale(MC_ACCURATE.getPrecision(), MC_ACCURATE.getRoundingMode());
    }

    public static BigDecimal max(BigDecimal bg1, BigDecimal bg2) {
        return bg1.compareTo(bg2) == 1 ? bg1 : bg2;
    }

    public static BigDecimal min(BigDecimal bg1, BigDecimal bg2) {
        return bg1.compareTo(bg2) == -1 ? bg1 : bg2;
    }

    public static boolean gt(BigDecimal bg1, BigDecimal bg2) {
        return bg1.compareTo(bg2) == 1;
    }

    public static boolean gte(BigDecimal bg1, BigDecimal bg2) {
        return bg1.compareTo(bg2) != -1;
    }

    public static boolean lt(BigDecimal bg1, BigDecimal bg2) {
        return bg1.compareTo(bg2) == -1;
    }

    public static boolean lte(BigDecimal bg1, BigDecimal bg2) {
        return lt(bg1, bg2) || areNumbersCloselyEqual(bg1, bg2, SCALE_FOUR);
    }

    public static BigDecimal toPercent(BigDecimal bg) {
        return bg.divide(ONE_HUNDRED, MC_ACCURATE);
    }

    public static BigDecimal fromPercent(BigDecimal bd) {
        return bd.multiply(ONE_HUNDRED).setScale(SCALE, ROUNDING_MODE);
    }

    public static BigDecimal zeroIfNull(BigDecimal bg) {
        return bg != null ? bg : BigDecimal.ZERO;
    }

    public static BigDecimal zeroIntIfNull(Integer bg) {
        return bg != null ? new BigDecimal(bg) : BigDecimal.ZERO;
    }

    public static boolean isZero(BigDecimal bg) {
        // do not use equals, it will not factor in scale and precision
        return bg.compareTo(BigDecimal.ZERO) == 0;
    }

    public static boolean isNonZero(BigDecimal bg) {
        // do not use equals, it will not factor in scale and precision
        return bg != null && !isZero(bg);
    }

    public static boolean isNegative(BigDecimal bg) {
        return bg.compareTo(BigDecimal.ZERO) == -1;
    }

    public static boolean isPositive(BigDecimal bg) {
        return bg.compareTo(BigDecimal.ZERO) == 1;
    }

    public static boolean isPercentage(BigDecimal bg) {
        // is not greater than 100 and is not less than 0
        return bg.compareTo(ONE_HUNDRED) != 1 && bg.compareTo(BigDecimal.ZERO) != -1;
    }

    /**
     * Used when keeping a map against a value. Put a 0 in if the map entry does not exist.
     */
    public static <X> void zeroIfEmpty(Map<X, BigDecimal> map, X key) {
        if (!map.containsKey(key)) {
            map.put(key, BigDecimal.ZERO);
        }
    }

    /**
     * Used when keeping a map against a value. Add the new value to the new or existing key value.
     */
    public static <X> void add(Map<X, BigDecimal> map, X key, BigDecimal value) {
        zeroIfEmpty(map, key);
        map.put(key, map.get(key).add(value));
    }

    /**
     * Used when keeping a map against a value. Subtract the new value to the new or existing key value.
     */
    public static <X> void subtract(Map<X, BigDecimal> map, X key, BigDecimal value) {
        zeroIfEmpty(map, key);
        map.put(key, map.get(key).subtract(value));
    }

    public static BigDecimal subtract(BigDecimal a, BigDecimal b) {
        return a.subtract(b).setScale(SCALE, ROUNDING_MODE);
    }

    public static BigDecimal subtractNoScale(BigDecimal a, BigDecimal b) {
        return a.subtract(b);
    }

    public static BigDecimal sum(BigDecimal a, BigDecimal b) {
        return a.add(b).setScale(SCALE_VERY_ACCURATE, ROUNDING_MODE);
    }

    public static BigDecimal sumSafe(BigDecimal a, BigDecimal b) {
        if (a == null) a = BigDecimal.ZERO;
        if (b == null) b = BigDecimal.ZERO;
        return a.add(b).setScale(SCALE_VERY_ACCURATE, ROUNDING_MODE);
    }

    public static boolean areNumbersCloselyEqual(BigDecimal lhsNumber, BigDecimal rhsNumber, int precision) {
        if (lhsNumber != rhsNumber) {
            if (lhsNumber != null && rhsNumber != null) {
                return areNumbersCloselyEqual(lhsNumber.doubleValue(), rhsNumber.doubleValue(), precision);
            } else {
                return false;
            }
        } else {
            return true;
        }

    }

    public static boolean areNumbersCloselyEqual(double lhsNumber, double rhsNumber, int precision) {
        if (Double.isNaN(lhsNumber) && Double.isNaN(rhsNumber)) {
            return true;
        }
        double tolerance = calculateFactor((-1) * (precision + 1)) * 9;
        return Math.abs(lhsNumber - rhsNumber) < tolerance;
    }

    public static double calculateFactor(int decimals) {
        return Math.pow(10, decimals);
    }

    public static String toNumberFormat(BigDecimal amountValue, String format) {
        if (StringUtils.isEmpty(format)) {
            return amountValue != null ? amountValue.toPlainString() : "";
        }
        NumberFormat numberFormat = getNumberFormat(format);

        return amountValue != null ? numberFormat.format(amountValue) : "";
    }

    public static DecimalFormat getNumberFormat(String format) {
        DecimalFormatSymbols symbols = new DecimalFormatSymbols(Locale.ENGLISH);
        symbols.setDecimalSeparator('.');
        //symbols.setGroupingSeparator(' ');
        return new DecimalFormat(format, symbols);
    }

    public static BigDecimal defaultIfBlank(BigDecimal value, BigDecimal zero) {
        return value == null ? zero : value;
    }

    public static BigDecimal getPercentageOrZero(BigDecimal value) {
        BigDecimal lessPercentage = MathUtils.defaultIfBlank(value, BigDecimal.ZERO);
        lessPercentage = MathUtils.toPercent(lessPercentage);
        return lessPercentage;
    }

    public static BigDecimal roundTo2DecimalPlaces(BigDecimal number) {
        return roundTo(number, 2);
    }

    public static BigDecimal roundTo4DecimalPlaces(BigDecimal number) {
        return roundTo(number, 4);
    }

    public static BigDecimal roundTo(BigDecimal number, int newScale) {
        if (number == null) {
            return null;
        }
        return number.setScale(newScale, ROUNDING_MODE);
    }

    public static BigDecimal divideContainerUsage(BigDecimal numberAtPOD, BigDecimal oldContainerQuantity,
                                                  BigDecimal newContainerQuantity) {
        BigDecimal numberAtPODToPercentage = safeDivide(numberAtPOD, oldContainerQuantity, 1);

        return numberAtPODToPercentage.divide(ONE_HUNDRED).multiply(newContainerQuantity).multiply(ONE_HUNDRED);
    }

    public static boolean allValidNonZero(BigDecimal... bigDecimals) {
        if (bigDecimals != null && bigDecimals.length > 0) {
            for (BigDecimal bigDecimal : bigDecimals) {
                if (!isNonZero(bigDecimal)) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }

    public static boolean inRange(double lowerLimit, double upperLimit, BigDecimal numberToCheck) {
        double v = numberToCheck.setScale(2, ROUNDING_MODE).doubleValue();
        return v >= lowerLimit && v <= upperLimit;
    }

    public static int adjustQuantityToNearest10(int forecast) {
        if (forecast <= 0) return 0;
        return ((forecast + 9) / 10) * 10;
    }
}