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 }