OceanusNewDecimal.java

/**
 * Provides classes to represent decimal numbers with fixed numbers of decimal digits
 * {@link #theScale} as Long integers. The decimal value is multiplied by 10 to the power of the
 * number of decimals for the number ({@link #theFactor}). The integral part of the number can be
 * expressed as (Value / Factor) and the fractional part as (Value % Factor). Arithmetic is then
 * performed as whole number arithmetic on these values, with due care taken on multiplication and
 * division to express the result to the correct number of decimals without losing any part of the
 * answer to overflow.
 */
package net.sourceforge.joceanus.oceanus.decimal;

import java.math.BigDecimal;
import java.math.RoundingMode;

/**
 * Decimal class performing integer arithmetic on large decimals.
 */
public class OceanusNewDecimal {
    /**
     * The Decimal radix.
     */
    private static final int RADIX_TEN = 10;

    /**
     * The Maximum # of Decimals.
     */
    public static final int MAX_DECIMALS = 9;

    /**
     * The Integer boost.
     */
    private static final long INTEGER_BOOST = 0x100000000L;

    /**
     * The Integer mask.
     */
    private static final long INTEGER_MASK = 0xFFFFFFFFL;

    /**
     * Powers of Ten.
     */
    private static final int[] POWERS_OF_TEN = getPowersOfTen(MAX_DECIMALS);

    /**
     * The number of decimal digits.
     */
    private final int theScale;

    /**
     * The Decimal factor, used for rounding fractional parts.
     */
    private final int theFactor;

    /**
     * sign.
     */
    private int theSign;

    /**
     * Positive Integral part.
     */
    private long theIntegral;

    /**
     * Positive Fractional part.
     */
    private int theFractional;

    /**
     * Constructor.
     * @param pScale the number of decimal digits
     */
    public OceanusNewDecimal(final int pScale) {
        this(0, 0, 0, pScale);
    }

    /**
     * Constructor.
     * @param pSource the source BigDecimal
     */
    public OceanusNewDecimal(final BigDecimal pSource) {
        /* Store sign and scale */
        theSign = pSource.signum();
        theScale = pSource.scale();
        checkValidScale(theScale);
        theFactor = getFactor(theScale);

        /* Extract the integral and fractional parts */
        theIntegral = pSource.longValue();
        theIntegral *= theSign;
        long myFractional = pSource.movePointRight(theScale).longValue() * theSign;
        myFractional %= theFactor;
        theFractional = (int) myFractional;
    }

    /**
     * Constructor.
     * @param pIntegral the integral part of the decimal.
     * @param pFractional the fractional part of the decimal
     * @param pSign the sign of the decimal
     * @param pScale the number of decimal digits
     */
    public OceanusNewDecimal(final long pIntegral,
                             final int pFractional,
                             final int pSign,
                             final int pScale) {
        /* Check that scale if valid */
        checkValidScale(pScale);

        /* Store details */
        theIntegral = pIntegral;
        theFractional = pFractional;
        theSign = pSign;
        theScale = pScale;
        theFactor = getFactor(theScale);
    }

    /**
     * Check that the scale is valid.
     * @param pScale the scale
     */
    private static void checkValidScale(final int pScale) {
        if (pScale < 0 || pScale > MAX_DECIMALS) {
            throw new IllegalArgumentException("Invalid scale - " + pScale);
        }
    }

    /**
     * Obtain the integral part of the decimal.
     * @return the integral part of the decimal
     */
    public long integralValue() {
        return theIntegral * theSign;
    }

    /**
     * Obtain the sign of the decimal.
     * @return -1, 0, or 1 as the value of this Decimal is negative, zero, or positive
     */
    public int signum() {
        return theSign;
    }

    /**
     * Obtain the fractional part of the decimal.
     * @return the fractional part of the decimal
     */
    public int fractionalValue() {
        return theFractional * theSign;
    }

    /**
     * Obtain the scale of the decimal.
     * @return the scale of the decimal
     */
    public int scale() {
        return theScale;
    }

    /**
     * Add a decimal to value.
     * @param pDecimal the decimal to add to this value
     */
    public void add(final OceanusNewDecimal pDecimal) {
        /* Access the integral/fractional part of the second decimal at the same scale */
        long myIntegral = pDecimal.theIntegral;
        long myFractional = adjustDecimals(pDecimal.theFractional, theScale - pDecimal.theScale);
        if (myFractional >= theFactor) {
            myFractional %= theFactor;
            myIntegral++;
        }
        add(myIntegral, (int) myFractional);
    }

    /**
     * Subtract a decimal from value.
     * @param pDecimal the decimal to subtract from this value
     */
    public void subtract(final OceanusNewDecimal pDecimal) {
        /* Access the integral/fractional part of the second decimal at the same scale */
        long myIntegral = pDecimal.theIntegral;
        long myFractional = adjustDecimals(pDecimal.theFractional, theScale - pDecimal.theScale);
        if (myFractional >= theFactor) {
            myFractional %= theFactor;
            myIntegral++;
        }
        add(-myIntegral, (int) -myFractional);
    }

    /**
     * Add a decimal to this value.
     * @param pIntegral the integral part of the decimal.
     * @param pFractional the fractional part of the decimal
     */
    private void add(final long pIntegral,
                     final int pFractional) {
         /* Add the fractional and non-fractional parts of the sum */
        theFractional = pFractional + fractionalValue();
        theIntegral = pIntegral + integralValue();

        /* If we have a positive integral # */
        if (theIntegral > 0) {
            /* Handle fractional too small */
            if (theFractional < 0) {
                theFractional += theFactor;
                theIntegral--;

                /* Handle fractional too large */
            } else if (theFractional >= theFactor) {
                theFractional -= theFactor;
                theIntegral++;
            }

            /* Set sign */
            theSign = 1;

            /* If we have a negative integral # */
        } else if (theIntegral < 0) {
            /* Handle fractional too large */
            if (theFractional > 0) {
                theFractional -= theFactor;
                theIntegral++;

                /* Handle fractional too small */
            } else if (theFractional <= -theFactor) {
                theFractional += theFactor;
                theIntegral--;
            }

            /* Set sign */
            theSign = -1;
            theFractional = -theFractional;
            theIntegral = -theIntegral;

            /* else we have a zero integral */
        } else {
            /* Handle fractional too large */
            if (theFractional >= theFactor) {
                theFractional -= theFactor;
                theIntegral = 1;
                theSign = 1;

                /* Handle Fractional too small */
            } else if (theFractional <= -theFactor) {
                theFractional += theFactor;
                theSign = -1;
                theIntegral = 1;
                theFractional = -theFractional;

                /* Handle positive fractional */
            } else if (theFractional > 0) {
                theSign = 1;

                /* Handle negative fractional */
            } else if (theFractional < 0) {
                theSign = -1;
                theFractional = -theFractional;

                /* Handle zero fractional */
            } else {
                theSign = 0;
            }
        }
    }

    /**
     * Multiply by another decimal
     * <p>
     * This function splits the values into three separate integers and then performs long arithmetic to
     * prevent loss of precision. The value is represented as (x,y,z,s) where the decimal may be written as
     * x*2<sup>32</sup> + y + z*10<sup>-s</sup> and x,y,z,s are all integers.
     * <p>
     * The product of (x<sub>1</sub>, y<sub>1</sub>, z<sub>1</sub>, s) by
     * (x<sub>2</sub>, y<sub>2</sub>, z<sub>2</sub>, t)
     * is therefore x<sub>1</sub>*x<sub>2</sub>*2<sup>64</sup> (discardable)
     * + (x<sub>1</sub>*y<sub>2</sub> + x<sub>2</sub>*y<sub>1</sub>)*2<sup>32</sup>
     * + x<sub>2</sub>*y<sub>2</sub>
     * + x<sub>1</sub>*z<sub>2</sub>*2<sup>32</sup>*10<sup>-t</sup>
     * + x<sub>2</sub>*z<sub>1</sub>*2<sup>32</sup>*10<sup>-s</sup>
     * + y<sub>1</sub>*z<sub>2</sub>*10<sup>-t</sup>
     * + y<sub>2</sub>*z<sub>1</sub>*10<sup>-s</sup>
     * + z<sub>1</sub>*z<sub>2</sub>*10<sup>-s-t</sup>
     *
     * @param pMultiplicand the decimal to multiply by
     */
    public void multiply(final OceanusNewDecimal pMultiplicand) {
        /* Access the parts of this value */
        final long myX1 = theIntegral >>> Integer.SIZE;
        final long myY1 = theIntegral & INTEGER_MASK;
        final long myZ1 = theFractional;
        final int myS = theScale;
        final int mySFactor = theFactor;

        /* Access the parts of the multiplicand */
        final long myX2 = pMultiplicand.theIntegral >>> Integer.SIZE;
        final long myY2 = pMultiplicand.theIntegral & INTEGER_MASK;
        final long myZ2 = pMultiplicand.theFractional;
        final int myT = pMultiplicand.theScale;
        final int myTFactor = pMultiplicand.theFactor;
        final long mySTFactor = mySFactor * (long) myTFactor;

        /* Calculate integral products */
        long myIntegral = ((myX1 * myY2) + (myY1 * myX2)) << Integer.SIZE;
        myIntegral += myY2 * myX2;

        /* Calculate product of X2 and Z1 and multiply by 2^32 */
        long myProduct = myX2 * myZ1;
        long myIntPart = myProduct / mySFactor;
        long myFracPart = myProduct % mySFactor;
        myIntegral += myIntPart << Integer.SIZE;
        myFracPart *= INTEGER_BOOST;
        myIntegral += myFracPart / mySFactor;
        long myFractional = adjustDecimals(myFracPart % mySFactor, myT);

        /* Calculate products of Y2 and Z1 */
        myProduct = myY2 * myZ1;
        myIntegral += myProduct / mySFactor;
        myFractional += adjustDecimals(myProduct % mySFactor, myT);

        /* Calculate product of X1 and Z2 and multiply by 2^32 */
        myProduct = myX1 * myZ2;
        myIntPart = myProduct / myTFactor;
        myFracPart = myProduct % myTFactor;
        myIntegral += myIntPart << Integer.SIZE;
        myFracPart *= INTEGER_BOOST;
        myIntegral += myFracPart / myTFactor;
        myFractional += adjustDecimals(myFracPart % myTFactor, myS);

        /* Calculate products of Y1 and Z2 */
        myProduct = myY1 * myZ2;
        myIntegral += myProduct / myTFactor;
        myFractional += adjustDecimals(myProduct % myTFactor, myS);

        /* Calculate products of Z1 and Z2 */
        myFractional += myZ1 * myZ2;

        /* Handle wrap of fractional */
        myIntegral += myFracPart / mySTFactor;
        myFractional %= mySTFactor;

        /* Adjust decimals */
        myFractional = adjustDecimals(myFractional, -myT);
        if (myFractional >= mySFactor) {
            myFractional %= mySFactor;
            myIntegral++;
        }

        /* Store the result */
        theIntegral = myIntegral;
        theFractional = (int) myFractional;
        theSign *= pMultiplicand.theSign;
    }

    /**
     * Divide by another decimal.
     * <p>
     * This function uses BigDecimal to perform the calculation
     * @param pDivisor the decimal to divide by
     */
    public void divide(final OceanusNewDecimal pDivisor) {
        /* Calculate the result */
        final BigDecimal myNumerator = toBigDecimal();
        final BigDecimal myDenominator = pDivisor.toBigDecimal();
        final BigDecimal myResult = myNumerator.divide(myDenominator, theScale, RoundingMode.HALF_UP);

        /* Extract the integral and fractional parts */
        theSign = myResult.signum();
        theIntegral = myResult.longValue();
        theIntegral *= theSign;
        long myFractional = myResult.movePointRight(theScale).longValue() * theSign;
        myFractional %= theFactor;
        theFractional = (int) myFractional;
    }

    /**
     * Convert to BigDecimal.
     * @return the BigDecimal equivalent
     */
    public BigDecimal toBigDecimal() {
        final BigDecimal myIntegral = new BigDecimal(theIntegral);
        final BigDecimal myFractional = new BigDecimal(theFractional).movePointLeft(theScale);
        final BigDecimal myResult = myIntegral.add(myFractional);
        return theSign == -1 ? myResult.negate() : myResult;
    }

    @Override
    public String toString() {
        /* Format the string */
        final StringBuilder myString = new StringBuilder(100);
        if (theSign == -1) {
            myString.append('-');
        }
        myString.append(theIntegral);
        if (theScale > 0) {
            final int myLen = myString.length();
            myString.append(theFractional + theFactor);
            myString.setCharAt(myLen, '.');
        }

        /* Return the string */
        return myString.toString();
    }

    /**
     * Build powers of ten.
     * @param pMax maximum power of ten
     * @return array of powers of ten
     */
    private static int[] getPowersOfTen(final int pMax) {
        /* Allocate the array */
        final int[] myArray = new int[pMax + 1];

        /* Initialise array */
        int myValue = 1;
        myArray[0] = myValue;

        /* Loop through array */
        for (int i = 1; i < pMax + 1; i++) {
            /* Adjust value and record it */
            myValue *= RADIX_TEN;
            myArray[i] = myValue;
        }

        /* Return the array */
        return myArray;
    }

    /**
     * Obtain factor.
     * @param pDecimals the number of decimals
     * @return the decimal part of the number
     */
    private static int getFactor(final int pDecimals) {
        return POWERS_OF_TEN[pDecimals];
    }

    /**
     * Adjust a value to a different number of decimals.
     * <p>
     * If the adjustment is to reduce the number of decimals, the most significant digit of the
     * discarded digits is examined to determine whether to round up. If the number of decimals is
     * to be increased, zeros are simply added to the end.
     * @param pValue the value to adjust
     * @param iAdjust the adjustment (positive if # of decimals are to increase, negative if they
     * are to decrease)
     * @return the adjusted value
     */
    private static long adjustDecimals(final long pValue,
                                       final int iAdjust) {
        /* Take a copy of the value */
        long myValue = pValue;

        /* If we need to reduce decimals */
        if (iAdjust < 0) {
            /* If we have more than one decimal to remove */
            if (iAdjust + 1 < 0) {
                /* Calculate division factor (minus one) */
                final long myFactor = getFactor(-(iAdjust + 1));

                /* Reduce to 10 times required value */
                myValue /= myFactor;
            }

            /* Access last digit */
            long myDigit = myValue
                    % RADIX_TEN;

            /* Handle negative values */
            int myAdjust = 1;
            if (myDigit < 0) {
                myAdjust = -1;
                myDigit = -myDigit;
            }

            /* Reduce final decimal and round up if required */
            myValue /= RADIX_TEN;
            if (myDigit >= (RADIX_TEN >> 1)) {
                myValue += myAdjust;
            }

            /* else if we need to expand fractional product */
        } else if (iAdjust > 0) {
            myValue *= getFactor(iAdjust);
        }

        /* Return the adjusted value */
        return myValue;
    }
}