View Javadoc
1   /*
2    * Oceanus: Java Utilities
3    * Copyright 2026. Tony Washer
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
6    * use this file except in compliance with the License.  You may obtain a copy
7    * of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
14   * License for the specific language governing permissions and limitations under
15   * the License.
16   */
17  
18  /**
19   * Provides classes to represent decimal numbers with fixed numbers of decimal digits
20   * {@link #theScale} as Long integers. The decimal value is multiplied by 10 to the power of the
21   * number of decimals for the number ({@link #theFactor}). The integral part of the number can be
22   * expressed as (Value / Factor) and the fractional part as (Value % Factor). Arithmetic is then
23   * performed as whole number arithmetic on these values, with due care taken on multiplication and
24   * division to express the result to the correct number of decimals without losing any part of the
25   * answer to overflow.
26   */
27  package io.github.tonywasher.joceanus.oceanus.decimal;
28  
29  import java.math.BigDecimal;
30  import java.math.RoundingMode;
31  
32  /**
33   * Decimal class performing integer arithmetic on large decimals.
34   */
35  public class OceanusNewDecimal {
36      /**
37       * The Decimal radix.
38       */
39      private static final int RADIX_TEN = 10;
40  
41      /**
42       * The Maximum # of Decimals.
43       */
44      public static final int MAX_DECIMALS = 9;
45  
46      /**
47       * The Integer boost.
48       */
49      private static final long INTEGER_BOOST = 0x100000000L;
50  
51      /**
52       * The Integer mask.
53       */
54      private static final long INTEGER_MASK = 0xFFFFFFFFL;
55  
56      /**
57       * Powers of Ten.
58       */
59      private static final int[] POWERS_OF_TEN = getPowersOfTen(MAX_DECIMALS);
60  
61      /**
62       * The number of decimal digits.
63       */
64      private final int theScale;
65  
66      /**
67       * The Decimal factor, used for rounding fractional parts.
68       */
69      private final int theFactor;
70  
71      /**
72       * sign.
73       */
74      private int theSign;
75  
76      /**
77       * Positive Integral part.
78       */
79      private long theIntegral;
80  
81      /**
82       * Positive Fractional part.
83       */
84      private int theFractional;
85  
86      /**
87       * Constructor.
88       *
89       * @param pScale the number of decimal digits
90       */
91      public OceanusNewDecimal(final int pScale) {
92          this(0, 0, 0, pScale);
93      }
94  
95      /**
96       * Constructor.
97       *
98       * @param pSource the source BigDecimal
99       */
100     public OceanusNewDecimal(final BigDecimal pSource) {
101         /* Store sign and scale */
102         theSign = pSource.signum();
103         theScale = pSource.scale();
104         checkValidScale(theScale);
105         theFactor = getFactor(theScale);
106 
107         /* Extract the integral and fractional parts */
108         theIntegral = pSource.longValue();
109         theIntegral *= theSign;
110         long myFractional = pSource.movePointRight(theScale).longValue() * theSign;
111         myFractional %= theFactor;
112         theFractional = (int) myFractional;
113     }
114 
115     /**
116      * Constructor.
117      *
118      * @param pIntegral   the integral part of the decimal.
119      * @param pFractional the fractional part of the decimal
120      * @param pSign       the sign of the decimal
121      * @param pScale      the number of decimal digits
122      */
123     public OceanusNewDecimal(final long pIntegral,
124                              final int pFractional,
125                              final int pSign,
126                              final int pScale) {
127         /* Check that scale if valid */
128         checkValidScale(pScale);
129 
130         /* Store details */
131         theIntegral = pIntegral;
132         theFractional = pFractional;
133         theSign = pSign;
134         theScale = pScale;
135         theFactor = getFactor(theScale);
136     }
137 
138     /**
139      * Check that the scale is valid.
140      *
141      * @param pScale the scale
142      */
143     private static void checkValidScale(final int pScale) {
144         if (pScale < 0 || pScale > MAX_DECIMALS) {
145             throw new IllegalArgumentException("Invalid scale - " + pScale);
146         }
147     }
148 
149     /**
150      * Obtain the integral part of the decimal.
151      *
152      * @return the integral part of the decimal
153      */
154     public long integralValue() {
155         return theIntegral * theSign;
156     }
157 
158     /**
159      * Obtain the sign of the decimal.
160      *
161      * @return -1, 0, or 1 as the value of this Decimal is negative, zero, or positive
162      */
163     public int signum() {
164         return theSign;
165     }
166 
167     /**
168      * Obtain the fractional part of the decimal.
169      *
170      * @return the fractional part of the decimal
171      */
172     public int fractionalValue() {
173         return theFractional * theSign;
174     }
175 
176     /**
177      * Obtain the scale of the decimal.
178      *
179      * @return the scale of the decimal
180      */
181     public int scale() {
182         return theScale;
183     }
184 
185     /**
186      * Add a decimal to value.
187      *
188      * @param pDecimal the decimal to add to this value
189      */
190     public void add(final OceanusNewDecimal pDecimal) {
191         /* Access the integral/fractional part of the second decimal at the same scale */
192         long myIntegral = pDecimal.theIntegral;
193         long myFractional = adjustDecimals(pDecimal.theFractional, theScale - pDecimal.theScale);
194         if (myFractional >= theFactor) {
195             myFractional %= theFactor;
196             myIntegral++;
197         }
198         add(myIntegral, (int) myFractional);
199     }
200 
201     /**
202      * Subtract a decimal from value.
203      *
204      * @param pDecimal the decimal to subtract from this value
205      */
206     public void subtract(final OceanusNewDecimal pDecimal) {
207         /* Access the integral/fractional part of the second decimal at the same scale */
208         long myIntegral = pDecimal.theIntegral;
209         long myFractional = adjustDecimals(pDecimal.theFractional, theScale - pDecimal.theScale);
210         if (myFractional >= theFactor) {
211             myFractional %= theFactor;
212             myIntegral++;
213         }
214         add(-myIntegral, (int) -myFractional);
215     }
216 
217     /**
218      * Add a decimal to this value.
219      *
220      * @param pIntegral   the integral part of the decimal.
221      * @param pFractional the fractional part of the decimal
222      */
223     private void add(final long pIntegral,
224                      final int pFractional) {
225         /* Add the fractional and non-fractional parts of the sum */
226         theFractional = pFractional + fractionalValue();
227         theIntegral = pIntegral + integralValue();
228 
229         /* If we have a positive integral # */
230         if (theIntegral > 0) {
231             /* Handle fractional too small */
232             if (theFractional < 0) {
233                 theFractional += theFactor;
234                 theIntegral--;
235 
236                 /* Handle fractional too large */
237             } else if (theFractional >= theFactor) {
238                 theFractional -= theFactor;
239                 theIntegral++;
240             }
241 
242             /* Set sign */
243             theSign = 1;
244 
245             /* If we have a negative integral # */
246         } else if (theIntegral < 0) {
247             /* Handle fractional too large */
248             if (theFractional > 0) {
249                 theFractional -= theFactor;
250                 theIntegral++;
251 
252                 /* Handle fractional too small */
253             } else if (theFractional <= -theFactor) {
254                 theFractional += theFactor;
255                 theIntegral--;
256             }
257 
258             /* Set sign */
259             theSign = -1;
260             theFractional = -theFractional;
261             theIntegral = -theIntegral;
262 
263             /* else we have a zero integral */
264         } else {
265             /* Handle fractional too large */
266             if (theFractional >= theFactor) {
267                 theFractional -= theFactor;
268                 theIntegral = 1;
269                 theSign = 1;
270 
271                 /* Handle Fractional too small */
272             } else if (theFractional <= -theFactor) {
273                 theFractional += theFactor;
274                 theSign = -1;
275                 theIntegral = 1;
276                 theFractional = -theFractional;
277 
278                 /* Handle positive fractional */
279             } else if (theFractional > 0) {
280                 theSign = 1;
281 
282                 /* Handle negative fractional */
283             } else if (theFractional < 0) {
284                 theSign = -1;
285                 theFractional = -theFractional;
286 
287                 /* Handle zero fractional */
288             } else {
289                 theSign = 0;
290             }
291         }
292     }
293 
294     /**
295      * Multiply by another decimal
296      * <p>
297      * This function splits the values into three separate integers and then performs long arithmetic to
298      * prevent loss of precision. The value is represented as (x,y,z,s) where the decimal may be written as
299      * x*2<sup>32</sup> + y + z*10<sup>-s</sup> and x,y,z,s are all integers.
300      * <p>
301      * The product of (x<sub>1</sub>, y<sub>1</sub>, z<sub>1</sub>, s) by
302      * (x<sub>2</sub>, y<sub>2</sub>, z<sub>2</sub>, t)
303      * is therefore x<sub>1</sub>*x<sub>2</sub>*2<sup>64</sup> (discardable)
304      * + (x<sub>1</sub>*y<sub>2</sub> + x<sub>2</sub>*y<sub>1</sub>)*2<sup>32</sup>
305      * + x<sub>2</sub>*y<sub>2</sub>
306      * + x<sub>1</sub>*z<sub>2</sub>*2<sup>32</sup>*10<sup>-t</sup>
307      * + x<sub>2</sub>*z<sub>1</sub>*2<sup>32</sup>*10<sup>-s</sup>
308      * + y<sub>1</sub>*z<sub>2</sub>*10<sup>-t</sup>
309      * + y<sub>2</sub>*z<sub>1</sub>*10<sup>-s</sup>
310      * + z<sub>1</sub>*z<sub>2</sub>*10<sup>-s-t</sup>
311      *
312      * @param pMultiplicand the decimal to multiply by
313      */
314     public void multiply(final OceanusNewDecimal pMultiplicand) {
315         /* Access the parts of this value */
316         final long myX1 = theIntegral >>> Integer.SIZE;
317         final long myY1 = theIntegral & INTEGER_MASK;
318         final long myZ1 = theFractional;
319         final int myS = theScale;
320         final int mySFactor = theFactor;
321 
322         /* Access the parts of the multiplicand */
323         final long myX2 = pMultiplicand.theIntegral >>> Integer.SIZE;
324         final long myY2 = pMultiplicand.theIntegral & INTEGER_MASK;
325         final long myZ2 = pMultiplicand.theFractional;
326         final int myT = pMultiplicand.theScale;
327         final int myTFactor = pMultiplicand.theFactor;
328         final long mySTFactor = mySFactor * (long) myTFactor;
329 
330         /* Calculate integral products */
331         long myIntegral = ((myX1 * myY2) + (myY1 * myX2)) << Integer.SIZE;
332         myIntegral += myY2 * myX2;
333 
334         /* Calculate product of X2 and Z1 and multiply by 2^32 */
335         long myProduct = myX2 * myZ1;
336         long myIntPart = myProduct / mySFactor;
337         long myFracPart = myProduct % mySFactor;
338         myIntegral += myIntPart << Integer.SIZE;
339         myFracPart *= INTEGER_BOOST;
340         myIntegral += myFracPart / mySFactor;
341         long myFractional = adjustDecimals(myFracPart % mySFactor, myT);
342 
343         /* Calculate products of Y2 and Z1 */
344         myProduct = myY2 * myZ1;
345         myIntegral += myProduct / mySFactor;
346         myFractional += adjustDecimals(myProduct % mySFactor, myT);
347 
348         /* Calculate product of X1 and Z2 and multiply by 2^32 */
349         myProduct = myX1 * myZ2;
350         myIntPart = myProduct / myTFactor;
351         myFracPart = myProduct % myTFactor;
352         myIntegral += myIntPart << Integer.SIZE;
353         myFracPart *= INTEGER_BOOST;
354         myIntegral += myFracPart / myTFactor;
355         myFractional += adjustDecimals(myFracPart % myTFactor, myS);
356 
357         /* Calculate products of Y1 and Z2 */
358         myProduct = myY1 * myZ2;
359         myIntegral += myProduct / myTFactor;
360         myFractional += adjustDecimals(myProduct % myTFactor, myS);
361 
362         /* Calculate products of Z1 and Z2 */
363         myFractional += myZ1 * myZ2;
364 
365         /* Handle wrap of fractional */
366         myIntegral += myFracPart / mySTFactor;
367         myFractional %= mySTFactor;
368 
369         /* Adjust decimals */
370         myFractional = adjustDecimals(myFractional, -myT);
371         if (myFractional >= mySFactor) {
372             myFractional %= mySFactor;
373             myIntegral++;
374         }
375 
376         /* Store the result */
377         theIntegral = myIntegral;
378         theFractional = (int) myFractional;
379         theSign *= pMultiplicand.theSign;
380     }
381 
382     /**
383      * Divide by another decimal.
384      * <p>
385      * This function uses BigDecimal to perform the calculation
386      *
387      * @param pDivisor the decimal to divide by
388      */
389     public void divide(final OceanusNewDecimal pDivisor) {
390         /* Calculate the result */
391         final BigDecimal myNumerator = toBigDecimal();
392         final BigDecimal myDenominator = pDivisor.toBigDecimal();
393         final BigDecimal myResult = myNumerator.divide(myDenominator, theScale, RoundingMode.HALF_UP);
394 
395         /* Extract the integral and fractional parts */
396         theSign = myResult.signum();
397         theIntegral = myResult.longValue();
398         theIntegral *= theSign;
399         long myFractional = myResult.movePointRight(theScale).longValue() * theSign;
400         myFractional %= theFactor;
401         theFractional = (int) myFractional;
402     }
403 
404     /**
405      * Convert to BigDecimal.
406      *
407      * @return the BigDecimal equivalent
408      */
409     public BigDecimal toBigDecimal() {
410         final BigDecimal myIntegral = new BigDecimal(theIntegral);
411         final BigDecimal myFractional = new BigDecimal(theFractional).movePointLeft(theScale);
412         final BigDecimal myResult = myIntegral.add(myFractional);
413         return theSign == -1 ? myResult.negate() : myResult;
414     }
415 
416     @Override
417     public String toString() {
418         /* Format the string */
419         final StringBuilder myString = new StringBuilder(100);
420         if (theSign == -1) {
421             myString.append('-');
422         }
423         myString.append(theIntegral);
424         if (theScale > 0) {
425             final int myLen = myString.length();
426             myString.append(theFractional + theFactor);
427             myString.setCharAt(myLen, '.');
428         }
429 
430         /* Return the string */
431         return myString.toString();
432     }
433 
434     /**
435      * Build powers of ten.
436      *
437      * @param pMax maximum power of ten
438      * @return array of powers of ten
439      */
440     private static int[] getPowersOfTen(final int pMax) {
441         /* Allocate the array */
442         final int[] myArray = new int[pMax + 1];
443 
444         /* Initialise array */
445         int myValue = 1;
446         myArray[0] = myValue;
447 
448         /* Loop through array */
449         for (int i = 1; i < pMax + 1; i++) {
450             /* Adjust value and record it */
451             myValue *= RADIX_TEN;
452             myArray[i] = myValue;
453         }
454 
455         /* Return the array */
456         return myArray;
457     }
458 
459     /**
460      * Obtain factor.
461      *
462      * @param pDecimals the number of decimals
463      * @return the decimal part of the number
464      */
465     private static int getFactor(final int pDecimals) {
466         return POWERS_OF_TEN[pDecimals];
467     }
468 
469     /**
470      * Adjust a value to a different number of decimals.
471      * <p>
472      * If the adjustment is to reduce the number of decimals, the most significant digit of the
473      * discarded digits is examined to determine whether to round up. If the number of decimals is
474      * to be increased, zeros are simply added to the end.
475      *
476      * @param pValue  the value to adjust
477      * @param iAdjust the adjustment (positive if # of decimals are to increase, negative if they
478      *                are to decrease)
479      * @return the adjusted value
480      */
481     private static long adjustDecimals(final long pValue,
482                                        final int iAdjust) {
483         /* Take a copy of the value */
484         long myValue = pValue;
485 
486         /* If we need to reduce decimals */
487         if (iAdjust < 0) {
488             /* If we have more than one decimal to remove */
489             if (iAdjust + 1 < 0) {
490                 /* Calculate division factor (minus one) */
491                 final long myFactor = getFactor(-(iAdjust + 1));
492 
493                 /* Reduce to 10 times required value */
494                 myValue /= myFactor;
495             }
496 
497             /* Access last digit */
498             long myDigit = myValue
499                     % RADIX_TEN;
500 
501             /* Handle negative values */
502             int myAdjust = 1;
503             if (myDigit < 0) {
504                 myAdjust = -1;
505                 myDigit = -myDigit;
506             }
507 
508             /* Reduce final decimal and round up if required */
509             myValue /= RADIX_TEN;
510             if (myDigit >= (RADIX_TEN >> 1)) {
511                 myValue += myAdjust;
512             }
513 
514             /* else if we need to expand fractional product */
515         } else if (iAdjust > 0) {
516             myValue *= getFactor(iAdjust);
517         }
518 
519         /* Return the adjusted value */
520         return myValue;
521     }
522 }