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          adjustToScale(theCurrency.getDefaultFractionDigits());
82      }
83  
84      /**
85       * Construct a new OceanusMoney by combining money and rate.
86       *
87       * @param pMoney the Money to apply rate to
88       * @param pRate  the Rate to apply
89       */
90      private OceanusMoney(final OceanusMoney pMoney,
91                           final OceanusRate pRate) {
92          this(pMoney.getCurrency());
93          calculateProduct(pMoney, pRate);
94      }
95  
96      /**
97       * Construct a new Money by combining money and ratio.
98       *
99       * @param pMoney the Money to apply ratio to
100      * @param pRatio the Ratio to apply
101      */
102     private OceanusMoney(final OceanusMoney pMoney,
103                          final OceanusRatio pRatio) {
104         this(pMoney.getCurrency());
105         calculateProduct(pMoney, pRatio);
106     }
107 
108     /**
109      * Create the decimal from a byte array.
110      *
111      * @param pBuffer the buffer
112      */
113     public OceanusMoney(final byte[] pBuffer) {
114         super(pBuffer);
115         if (pBuffer.length < Long.BYTES + 1 + CURRCODE_LEN) {
116             throw new IllegalArgumentException();
117         }
118         final byte[] myCurr = Arrays.copyOfRange(pBuffer, Long.BYTES + 1, pBuffer.length);
119         final String myCurrCode = new String(myCurr);
120         theCurrency = Currency.getInstance(myCurrCode);
121     }
122 
123     /**
124      * Access the currency.
125      *
126      * @return the currency
127      */
128     public Currency getCurrency() {
129         return theCurrency;
130     }
131 
132     /**
133      * Factory method for generating whole monetary units for a currency (e.g. £)
134      *
135      * @param pUnits    the number of whole monetary units
136      * @param pCurrency the currency
137      * @return the allocated money
138      */
139     public static OceanusMoney getWholeUnits(final long pUnits,
140                                              final Currency pCurrency) {
141         /* Allocate the money */
142         final OceanusMoney myResult = new OceanusMoney(pCurrency);
143         final int myScale = myResult.scale();
144         myResult.setValue(adjustDecimals(pUnits, myScale), myScale);
145         return myResult;
146     }
147 
148     /**
149      * Factory method for generating whole monetary units (e.g. £)
150      *
151      * @param pUnits the number of whole monetary units
152      * @return the allocated money
153      */
154     public static OceanusMoney getWholeUnits(final long pUnits) {
155         /* Allocate the money */
156         final OceanusMoney myResult = new OceanusMoney();
157         final int myScale = myResult.scale();
158         myResult.setValue(adjustDecimals(pUnits, myScale), myScale);
159         return myResult;
160     }
161 
162     /**
163      * Add a monetary amount to the value.
164      *
165      * @param pValue The money to add to this one.
166      */
167     public void addAmount(final OceanusMoney pValue) {
168         /* Currency must be identical */
169         if (!theCurrency.equals(pValue.getCurrency())) {
170             throw new IllegalArgumentException(ERROR_DIFFER);
171         }
172 
173         /* Add the value */
174         super.addValue(pValue);
175     }
176 
177     /**
178      * Subtract a monetary amount from the value.
179      *
180      * @param pValue The money to subtract from this one.
181      */
182     public void subtractAmount(final OceanusMoney pValue) {
183         /* Currency must be identical */
184         if (!theCurrency.equals(pValue.getCurrency())) {
185             throw new IllegalArgumentException(ERROR_DIFFER);
186         }
187 
188         /* Subtract the value */
189         super.subtractValue(pValue);
190     }
191 
192     @Override
193     public void addValue(final OceanusDecimal pValue) {
194         throw new UnsupportedOperationException();
195     }
196 
197     @Override
198     public void subtractValue(final OceanusDecimal pValue) {
199         throw new UnsupportedOperationException();
200     }
201 
202     /**
203      * Obtain value in different currency.
204      *
205      * @param pCurrency the currency to convert to
206      * @return the converted money in the new currency
207      */
208     public OceanusMoney changeCurrency(final Currency pCurrency) {
209         /* Convert currency with an exchange rate of one */
210         return convertCurrency(pCurrency, OceanusRatio.ONE);
211     }
212 
213     /**
214      * Obtain converted money.
215      *
216      * @param pCurrency the currency to convert to
217      * @param pRate     the conversion rate
218      * @return the converted money in the new currency
219      */
220     public OceanusMoney convertCurrency(final Currency pCurrency,
221                                         final OceanusRatio pRate) {
222         /* If this is the same currency then no conversion */
223         if (theCurrency.equals(pCurrency)) {
224             return new OceanusMoney(this);
225         }
226 
227         /* Create the new Money */
228         final OceanusMoney myResult = new OceanusMoney(pCurrency);
229         myResult.calculateProduct(this, pRate);
230         return myResult;
231     }
232 
233     /**
234      * obtain a Diluted money.
235      *
236      * @param pDilution the dilution factor
237      * @return the calculated value
238      */
239     public OceanusMoney getDilutedMoney(final OceanusRatio pDilution) {
240         /* Calculate diluted value */
241         return new OceanusMoney(this, pDilution);
242     }
243 
244     /**
245      * calculate the value of this money at a given rate.
246      *
247      * @param pRate the rate to calculate at
248      * @return the calculated value
249      */
250     public OceanusMoney valueAtRate(final OceanusRate pRate) {
251         /* Calculate the money at this rate */
252         return new OceanusMoney(this, pRate);
253     }
254 
255     /**
256      * calculate the value of this money at a given ratio.
257      *
258      * @param pRatio the ratio to multiply by
259      * @return the calculated value
260      */
261     public OceanusMoney valueAtRatio(final OceanusRatio pRatio) {
262         /* Calculate the money at this rate */
263         return new OceanusMoney(this, pRatio);
264     }
265 
266     /**
267      * calculate the gross value of this money at a given rate used to convert from net to gross
268      * values form interest and dividends.
269      *
270      * @param pRate the rate to calculate at
271      * @return the calculated value
272      */
273     public OceanusMoney grossValueAtRate(final OceanusRate pRate) {
274         /* Calculate the Gross corresponding to this net value at the rate */
275         final OceanusRatio myRatio = pRate.getRemainingRate().getInverseRatio();
276         return new OceanusMoney(this, myRatio);
277     }
278 
279     /**
280      * calculate the TaxCredit value of this money at a given rate used to convert from net to
281      * gross. values form interest and dividends
282      *
283      * @param pRate the rate to calculate at
284      * @return the calculated value
285      */
286     public OceanusMoney taxCreditAtRate(final OceanusRate pRate) {
287         /* Calculate the Tax Credit corresponding to this net value at the rate */
288         final OceanusRatio myRatio = new OceanusRatio(pRate, pRate.getRemainingRate());
289         return new OceanusMoney(this, myRatio);
290     }
291 
292     /**
293      * calculate the value of this money at a given proportion (i.e. weight/total).
294      *
295      * @param pWeight the weight of this item
296      * @param pTotal  the total weight of all the items
297      * @return the calculated value
298      */
299     public OceanusMoney valueAtWeight(final OceanusMoney pWeight,
300                                       final OceanusMoney pTotal) {
301         /* Handle zero total */
302         if (!pTotal.isNonZero()) {
303             return new OceanusMoney(theCurrency);
304         }
305 
306         /* Calculate the defined ratio of this value */
307         final OceanusRatio myRatio = new OceanusRatio(pWeight, pTotal);
308         return new OceanusMoney(this, myRatio);
309     }
310 
311     /**
312      * calculate the value of this money at a given proportion (i.e. weight/total).
313      *
314      * @param pWeight the weight of this item
315      * @param pTotal  the total weight of all the items
316      * @return the calculated value
317      */
318     public OceanusMoney valueAtWeight(final OceanusUnits pWeight,
319                                       final OceanusUnits pTotal) {
320         /* Handle zero total */
321         if (!pTotal.isNonZero()) {
322             return new OceanusMoney(theCurrency);
323         }
324 
325         /* Calculate the defined ratio of this value */
326         final OceanusRatio myRatio = new OceanusRatio(pWeight, pTotal);
327         return new OceanusMoney(this, myRatio);
328     }
329 
330     /**
331      * Determine default currency.
332      *
333      * @return the default currency
334      */
335     private static Currency determineDefaultCurrency() {
336         /* Obtain the default currency */
337         final Currency myCurrency = DecimalFormatSymbols.getInstance().getCurrency();
338 
339         /* If the default is a pseudo-currency then default to GBP */
340         return myCurrency.getDefaultFractionDigits() < 0
341                 ? Currency.getInstance(Locale.UK)
342                 : myCurrency;
343     }
344 
345     /**
346      * Obtain default currency.
347      *
348      * @return the default currency
349      */
350     public static Currency getDefaultCurrency() {
351         return DEFAULT_CURRENCY;
352     }
353 
354     @Override
355     public boolean equals(final Object pThat) {
356         /* Handle trivial cases */
357         if (this == pThat) {
358             return true;
359         }
360         if (pThat == null) {
361             return false;
362         }
363 
364         /* Make sure that the object is the same class */
365         if (getClass() != pThat.getClass()) {
366             return false;
367         }
368 
369         /* Cast as money */
370         final OceanusMoney myThat = (OceanusMoney) pThat;
371 
372         /* Check currency */
373         if (!theCurrency.equals(myThat.getCurrency())) {
374             return false;
375         }
376 
377         /* Check value and scale */
378         return super.equals(pThat);
379     }
380 
381     @Override
382     public int hashCode() {
383         return Objects.hash(theCurrency, super.hashCode());
384     }
385 
386     @Override
387     public byte[] toBytes() {
388         final byte[] myBase = super.toBytes();
389         final byte[] myCurr = theCurrency.getCurrencyCode().getBytes(StandardCharsets.UTF_8);
390         final byte[] myResult = Arrays.copyOf(myBase, myBase.length + myCurr.length);
391         System.arraycopy(myCurr, 0, myResult, myBase.length, myCurr.length);
392         return myResult;
393     }
394 }