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 }