View Javadoc
1   /*
2    * Oceanus: Java Utilities
3    * Copyright 2012-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  package io.github.tonywasher.joceanus.oceanus.decimal;
18  
19  import java.nio.charset.StandardCharsets;
20  import java.text.DecimalFormatSymbols;
21  import java.util.Arrays;
22  import java.util.Currency;
23  import java.util.Locale;
24  import java.util.Objects;
25  
26  /**
27   * Represents a Money object.
28   */
29  public class OceanusMoney
30          extends OceanusDecimal {
31      /**
32       * Money Byte length.
33       */
34      public static final int BYTE_LEN = Long.BYTES + 4;
35  
36      /**
37       * Currency code length.
38       */
39      private static final int CURRCODE_LEN = 2;
40  
41      /**
42       * Invalid Currency error text.
43       */
44      static final String ERROR_DIFFER = "Cannot add together two different currencies";
45  
46      /**
47       * Default currency.
48       */
49      static final Currency DEFAULT_CURRENCY = determineDefaultCurrency();
50  
51      /**
52       * Currency for money.
53       */
54      private final Currency theCurrency;
55  
56      /**
57       * Constructor for money of value zero in the default currency.
58       */
59      public OceanusMoney() {
60          this(DEFAULT_CURRENCY);
61      }
62  
63      /**
64       * Constructor for money of value zero.
65       *
66       * @param pCurrency the currency
67       */
68      public OceanusMoney(final Currency pCurrency) {
69          theCurrency = pCurrency;
70          recordScale(theCurrency.getDefaultFractionDigits());
71      }
72  
73      /**
74       * Construct a new OceanusMoney by copying another money.
75       *
76       * @param pMoney the Money to copy
77       */
78      public OceanusMoney(final OceanusMoney pMoney) {
79          super(pMoney.unscaledValue(), pMoney.scale());
80          theCurrency = pMoney.getCurrency();
81      }
82  
83      /**
84       * Constructor for money from a decimal string.
85       *
86       * @param pSource The source decimal string
87       * @throws IllegalArgumentException on invalidly formatted argument
88       */
89      public OceanusMoney(final String pSource) {
90          /* Use default constructor */
91          this();
92  
93          /* Parse the string and correct the scale */
94          OceanusDecimalParser.parseDecimalValue(pSource, this);
95          adjustToScale(theCurrency.getDefaultFractionDigits());
96      }
97  
98      /**
99       * Constructor for money from a decimal string.
100      *
101      * @param pSource   The source decimal string
102      * @param pCurrency the currency
103      * @throws IllegalArgumentException on invalidly formatted argument
104      */
105     public OceanusMoney(final String pSource,
106                         final Currency pCurrency) {
107         /* Use currency constructor */
108         this(pCurrency);
109 
110         /* Parse the string and correct the scale */
111         OceanusDecimalParser.parseDecimalValue(pSource, this);
112         adjustToScale(theCurrency.getDefaultFractionDigits());
113     }
114 
115     /**
116      * Construct a new OceanusMoney by combining units and price.
117      *
118      * @param pUnits the number of units
119      * @param pPrice the price of each unit
120      */
121     protected OceanusMoney(final OceanusUnits pUnits,
122                            final OceanusPrice pPrice) {
123         this(pPrice.getCurrency());
124         calculateProduct(pUnits, pPrice);
125     }
126 
127     /**
128      * Construct a new OceanusMoney by combining money and rate.
129      *
130      * @param pMoney the Money to apply rate to
131      * @param pRate  the Rate to apply
132      */
133     private OceanusMoney(final OceanusMoney pMoney,
134                          final OceanusRate pRate) {
135         this(pMoney.getCurrency());
136         calculateProduct(pMoney, pRate);
137     }
138 
139     /**
140      * Construct a new Money by combining money and ratio.
141      *
142      * @param pMoney the Money to apply ratio to
143      * @param pRatio the Ratio to apply
144      */
145     private OceanusMoney(final OceanusMoney pMoney,
146                          final OceanusRatio pRatio) {
147         this(pMoney.getCurrency());
148         calculateProduct(pMoney, pRatio);
149     }
150 
151     /**
152      * Create the decimal from a byte array.
153      *
154      * @param pBuffer the buffer
155      */
156     public OceanusMoney(final byte[] pBuffer) {
157         super(pBuffer);
158         if (pBuffer.length < Long.BYTES + 1 + CURRCODE_LEN) {
159             throw new IllegalArgumentException();
160         }
161         final byte[] myCurr = Arrays.copyOfRange(pBuffer, Long.BYTES + 1, pBuffer.length);
162         final String myCurrCode = new String(myCurr);
163         theCurrency = Currency.getInstance(myCurrCode);
164     }
165 
166     /**
167      * Access the currency.
168      *
169      * @return the currency
170      */
171     public Currency getCurrency() {
172         return theCurrency;
173     }
174 
175     /**
176      * Factory method for generating whole monetary units for a currency (e.g. £)
177      *
178      * @param pUnits    the number of whole monetary units
179      * @param pCurrency the currency
180      * @return the allocated money
181      */
182     public static OceanusMoney getWholeUnits(final long pUnits,
183                                              final Currency pCurrency) {
184         /* Allocate the money */
185         final OceanusMoney myResult = new OceanusMoney(pCurrency);
186         final int myScale = myResult.scale();
187         myResult.setValue(adjustDecimals(pUnits, myScale), myScale);
188         return myResult;
189     }
190 
191     /**
192      * Factory method for generating whole monetary units (e.g. £)
193      *
194      * @param pUnits the number of whole monetary units
195      * @return the allocated money
196      */
197     public static OceanusMoney getWholeUnits(final long pUnits) {
198         /* Allocate the money */
199         final OceanusMoney myResult = new OceanusMoney();
200         final int myScale = myResult.scale();
201         myResult.setValue(adjustDecimals(pUnits, myScale), myScale);
202         return myResult;
203     }
204 
205     /**
206      * Add a monetary amount to the value.
207      *
208      * @param pValue The money to add to this one.
209      */
210     public void addAmount(final OceanusMoney pValue) {
211         /* Currency must be identical */
212         if (!theCurrency.equals(pValue.getCurrency())) {
213             throw new IllegalArgumentException(ERROR_DIFFER);
214         }
215 
216         /* Add the value */
217         super.addValue(pValue);
218     }
219 
220     /**
221      * Subtract a monetary amount from the value.
222      *
223      * @param pValue The money to subtract from this one.
224      */
225     public void subtractAmount(final OceanusMoney pValue) {
226         /* Currency must be identical */
227         if (!theCurrency.equals(pValue.getCurrency())) {
228             throw new IllegalArgumentException(ERROR_DIFFER);
229         }
230 
231         /* Subtract the value */
232         super.subtractValue(pValue);
233     }
234 
235     @Override
236     public void addValue(final OceanusDecimal pValue) {
237         throw new UnsupportedOperationException();
238     }
239 
240     @Override
241     public void subtractValue(final OceanusDecimal pValue) {
242         throw new UnsupportedOperationException();
243     }
244 
245     /**
246      * Obtain value in different currency.
247      *
248      * @param pCurrency the currency to convert to
249      * @return the converted money in the new currency
250      */
251     public OceanusMoney changeCurrency(final Currency pCurrency) {
252         /* Convert currency with an exchange rate of one */
253         return convertCurrency(pCurrency, OceanusRatio.ONE);
254     }
255 
256     /**
257      * Obtain converted money.
258      *
259      * @param pCurrency the currency to convert to
260      * @param pRate     the conversion rate
261      * @return the converted money in the new currency
262      */
263     public OceanusMoney convertCurrency(final Currency pCurrency,
264                                         final OceanusRatio pRate) {
265         /* If this is the same currency then no conversion */
266         if (theCurrency.equals(pCurrency)) {
267             return new OceanusMoney(this);
268         }
269 
270         /* Create the new Money */
271         final OceanusMoney myResult = new OceanusMoney(pCurrency);
272         myResult.calculateProduct(this, pRate);
273         return myResult;
274     }
275 
276     /**
277      * obtain a Diluted money.
278      *
279      * @param pDilution the dilution factor
280      * @return the calculated value
281      */
282     public OceanusMoney getDilutedMoney(final OceanusRatio pDilution) {
283         /* Calculate diluted value */
284         return new OceanusMoney(this, pDilution);
285     }
286 
287     /**
288      * calculate the value of this money at a given rate.
289      *
290      * @param pRate the rate to calculate at
291      * @return the calculated value
292      */
293     public OceanusMoney valueAtRate(final OceanusRate pRate) {
294         /* Calculate the money at this rate */
295         return new OceanusMoney(this, pRate);
296     }
297 
298     /**
299      * calculate the value of this money at a given ratio.
300      *
301      * @param pRatio the ratio to multiply by
302      * @return the calculated value
303      */
304     public OceanusMoney valueAtRatio(final OceanusRatio pRatio) {
305         /* Calculate the money at this rate */
306         return new OceanusMoney(this, pRatio);
307     }
308 
309     /**
310      * calculate the gross value of this money at a given rate used to convert from net to gross
311      * values form interest and dividends.
312      *
313      * @param pRate the rate to calculate at
314      * @return the calculated value
315      */
316     public OceanusMoney grossValueAtRate(final OceanusRate pRate) {
317         /* Calculate the Gross corresponding to this net value at the rate */
318         final OceanusRatio myRatio = pRate.getRemainingRate().getInverseRatio();
319         return new OceanusMoney(this, myRatio);
320     }
321 
322     /**
323      * calculate the TaxCredit value of this money at a given rate used to convert from net to
324      * gross. values form interest and dividends
325      *
326      * @param pRate the rate to calculate at
327      * @return the calculated value
328      */
329     public OceanusMoney taxCreditAtRate(final OceanusRate pRate) {
330         /* Calculate the Tax Credit corresponding to this net value at the rate */
331         final OceanusRatio myRatio = new OceanusRatio(pRate, pRate.getRemainingRate());
332         return new OceanusMoney(this, myRatio);
333     }
334 
335     /**
336      * calculate the value of this money at a given proportion (i.e. weight/total).
337      *
338      * @param pWeight the weight of this item
339      * @param pTotal  the total weight of all the items
340      * @return the calculated value
341      */
342     public OceanusMoney valueAtWeight(final OceanusMoney pWeight,
343                                       final OceanusMoney pTotal) {
344         /* Handle zero total */
345         if (!pTotal.isNonZero()) {
346             return new OceanusMoney(theCurrency);
347         }
348 
349         /* Calculate the defined ratio of this value */
350         final OceanusRatio myRatio = new OceanusRatio(pWeight, pTotal);
351         return new OceanusMoney(this, myRatio);
352     }
353 
354     /**
355      * calculate the value of this money at a given proportion (i.e. weight/total).
356      *
357      * @param pWeight the weight of this item
358      * @param pTotal  the total weight of all the items
359      * @return the calculated value
360      */
361     public OceanusMoney valueAtWeight(final OceanusUnits pWeight,
362                                       final OceanusUnits pTotal) {
363         /* Handle zero total */
364         if (!pTotal.isNonZero()) {
365             return new OceanusMoney(theCurrency);
366         }
367 
368         /* Calculate the defined ratio of this value */
369         final OceanusRatio myRatio = new OceanusRatio(pWeight, pTotal);
370         return new OceanusMoney(this, myRatio);
371     }
372 
373     /**
374      * Determine default currency.
375      *
376      * @return the default currency
377      */
378     private static Currency determineDefaultCurrency() {
379         /* Obtain the default currency */
380         final Currency myCurrency = DecimalFormatSymbols.getInstance().getCurrency();
381 
382         /* If the default is a pseudo-currency then default to GBP */
383         return myCurrency.getDefaultFractionDigits() < 0
384                 ? Currency.getInstance(Locale.UK)
385                 : myCurrency;
386     }
387 
388     /**
389      * Obtain default currency.
390      *
391      * @return the default currency
392      */
393     public static Currency getDefaultCurrency() {
394         return DEFAULT_CURRENCY;
395     }
396 
397     @Override
398     public boolean equals(final Object pThat) {
399         /* Handle trivial cases */
400         if (this == pThat) {
401             return true;
402         }
403         if (pThat == null) {
404             return false;
405         }
406 
407         /* Make sure that the object is the same class */
408         if (getClass() != pThat.getClass()) {
409             return false;
410         }
411 
412         /* Cast as money */
413         final OceanusMoney myThat = (OceanusMoney) pThat;
414 
415         /* Check currency */
416         if (!theCurrency.equals(myThat.getCurrency())) {
417             return false;
418         }
419 
420         /* Check value and scale */
421         return super.equals(pThat);
422     }
423 
424     @Override
425     public int hashCode() {
426         return Objects.hash(theCurrency, super.hashCode());
427     }
428 
429     @Override
430     public byte[] toBytes() {
431         final byte[] myBase = super.toBytes();
432         final byte[] myCurr = theCurrency.getCurrencyCode().getBytes(StandardCharsets.UTF_8);
433         final byte[] myResult = Arrays.copyOf(myBase, myBase.length + myCurr.length);
434         System.arraycopy(myCurr, 0, myResult, myBase.length, myCurr.length);
435         return myResult;
436     }
437 }