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 io.github.tonywasher.joceanus.oceanus.base.OceanusLocale;
20
21 import java.util.Currency;
22 import java.util.Locale;
23
24 /**
25 * Presentation methods for decimals in a particular locale.
26 *
27 * @author Tony Washer
28 */
29 public class OceanusDecimalFormatter {
30 /**
31 * The Buffer length for building decimal strings.
32 */
33 protected static final int INITIAL_BUFLEN = 20;
34
35 /**
36 * The Blank character.
37 */
38 public static final char CHAR_BLANK = ' ';
39
40 /**
41 * The Zero character.
42 */
43 public static final char CHAR_ZERO = '0';
44
45 /**
46 * The Minus character.
47 */
48 public static final char CHAR_MINUS = '-';
49
50 /**
51 * The Group character.
52 */
53 public static final char CHAR_GROUP = ',';
54
55 /**
56 * The Decimal character.
57 */
58 public static final String STR_DEC = ".";
59
60 /**
61 * The Currency separator.
62 */
63 protected static final String STR_CURRSEP = ":";
64
65 /**
66 * The locale.
67 */
68 static final OceanusDecimalLocale LOCALE_DEFAULT = new OceanusDecimalLocale();
69
70 /**
71 * The locale.
72 */
73 private OceanusDecimalLocale theLocale;
74
75 /**
76 * Do we use accounting format for monetary values?
77 */
78 private boolean useAccounting;
79
80 /**
81 * Width for accounting format.
82 */
83 private int theAccountingWidth;
84
85 /**
86 * Constructor.
87 */
88 public OceanusDecimalFormatter() {
89 /* Use default locale */
90 this(OceanusLocale.getDefaultLocale());
91 }
92
93 /**
94 * Constructor.
95 *
96 * @param pLocale the locale
97 */
98 public OceanusDecimalFormatter(final Locale pLocale) {
99 /* Set the locale */
100 setLocale(pLocale);
101 }
102
103 /**
104 * Set the locale.
105 *
106 * @param pLocale the locale
107 */
108 public final void setLocale(final Locale pLocale) {
109 /* Store the locale */
110 theLocale = new OceanusDecimalLocale(pLocale);
111 }
112
113 /**
114 * Set accounting width.
115 *
116 * @param pWidth the accounting width to use
117 */
118 public void setAccountingWidth(final int pWidth) {
119 /* Set accounting mode on and set the width */
120 useAccounting = true;
121 theAccountingWidth = pWidth;
122 }
123
124 /**
125 * Clear accounting mode.
126 */
127 public void clearAccounting() {
128 /* Clear the accounting mode flag */
129 useAccounting = false;
130 }
131
132 /**
133 * Format a decimal value without reference to locale.
134 *
135 * @param pValue the value to format
136 * @return the formatted value
137 */
138 protected static String toString(final OceanusDecimal pValue) {
139 /* Access the value and scale */
140 long myValue = pValue.unscaledValue();
141 final int myScale = pValue.scale();
142
143 /* handle negative values */
144 final boolean isNegative = myValue < 0;
145 if (isNegative) {
146 myValue = -myValue;
147 }
148
149 /* Format the string */
150 final StringBuilder myString = new StringBuilder(INITIAL_BUFLEN);
151 myString.append(myValue);
152
153 /* Add leading zeroes */
154 int myLen = myString.length();
155 while (myLen < (myScale + 1)) {
156 myString.insert(0, CHAR_ZERO);
157 myLen++;
158 }
159
160 /* Insert the decimal into correct position if needed */
161 if (myScale > 0) {
162 myString.insert(myLen
163 - myScale, STR_DEC);
164 }
165
166 /* Add minus sign if required */
167 if (isNegative) {
168 myString.insert(0, CHAR_MINUS);
169 }
170
171 /* Return the string */
172 return myString.toString();
173 }
174
175 /**
176 * Format a money value with currency code, into a locale independent format.
177 *
178 * @param pValue the value to format
179 * @return the formatted value
180 */
181 public String toCurrencyString(final OceanusMoney pValue) {
182 /* Format the basic value */
183 final StringBuilder myWork = new StringBuilder(toString(pValue));
184
185 /* Add the currency symbol */
186 final Currency myCurrency = pValue.getCurrency();
187 myWork.insert(0, STR_CURRSEP);
188 myWork.insert(0, myCurrency.getCurrencyCode());
189
190 /* Return the string */
191 return myWork.toString();
192 }
193
194 /**
195 * Format a numeric decimal value.
196 *
197 * @param pValue the value to format
198 * @param pScale the scale of the decimal
199 * @param pDecSeparator the decimal separator
200 * @return the formatted value.
201 */
202 private StringBuilder formatDecimal(final long pValue,
203 final int pScale,
204 final String pDecSeparator) {
205 /* Access the value */
206 long myValue = pValue;
207
208 /* Reject negative scales */
209 if (pScale < 0) {
210 throw new IllegalArgumentException("Decimals cannot be negative");
211 }
212
213 /* handle negative values */
214 final boolean isNegative = myValue < 0;
215 if (isNegative) {
216 myValue = -myValue;
217 }
218
219 /* Format the string */
220 final StringBuilder myString = new StringBuilder(INITIAL_BUFLEN);
221 myString.append(myValue);
222
223 /* Add leading zeroes */
224 int myLen = myString.length();
225 while (myLen < (pScale + 1)) {
226 myString.insert(0, CHAR_ZERO);
227 myLen++;
228 }
229
230 /* If we have decimals */
231 if (pScale > 0) {
232 /* Insert decimal point and remove decimals from length */
233 myString.insert(myLen
234 - pScale, pDecSeparator);
235 myLen -= pScale;
236 }
237
238 /* Loop while we need to add grouping */
239 final int myGroupingSize = theLocale.getGroupingSize();
240 final String myGrouping = theLocale.getGrouping();
241 while (myLen > myGroupingSize) {
242 /* Insert grouping character and remove grouping size from length */
243 myString.insert(myLen
244 - myGroupingSize, myGrouping);
245 myLen -= myGroupingSize;
246 }
247
248 /* Add minus sign if required */
249 if (isNegative) {
250 myString.insert(0, theLocale.getMinusSign());
251 }
252
253 /* Return the string */
254 return myString;
255 }
256
257 /**
258 * Format a long value.
259 *
260 * @param pValue the value to format
261 * @return the formatted value.
262 */
263 private StringBuilder formatLong(final long pValue) {
264 /* Access the value */
265 long myValue = pValue;
266
267 /* handle negative values */
268 final boolean isNegative = myValue < 0;
269 if (isNegative) {
270 myValue = -myValue;
271 }
272
273 /* Format the string */
274 final StringBuilder myString = new StringBuilder(INITIAL_BUFLEN);
275 myString.append(myValue);
276
277 /* Loop while we need to add grouping */
278 int myLen = myString.length();
279 final int myGroupingSize = theLocale.getGroupingSize();
280 final String myGrouping = theLocale.getGrouping();
281 while (myLen > myGroupingSize) {
282 /* Insert grouping character and remove grouping size from length */
283 myString.insert(myLen
284 - myGroupingSize, myGrouping);
285 myLen -= myGroupingSize;
286 }
287
288 /* Add minus sign if required */
289 if (isNegative) {
290 myString.insert(0, theLocale.getMinusSign());
291 }
292
293 /* Return the string */
294 return myString;
295 }
296
297 /**
298 * Format Money value.
299 *
300 * @param pMoney the value to format
301 * @return the formatted value
302 */
303 public String formatMoney(final OceanusMoney pMoney) {
304 /* If we are using accounting and have zero */
305 if (useAccounting
306 && pMoney.isZero()) {
307 /* Format the zero */
308 return formatZeroAccounting(pMoney.getCurrency());
309 }
310
311 /* Format the basic value */
312 final StringBuilder myWork = formatDecimal(pMoney.unscaledValue(), pMoney.scale(), theLocale.getMoneyDecimal());
313
314 /* If we have a leading minus sign */
315 final char myMinus = theLocale.getMinusSign();
316 final boolean isNegative = myWork.charAt(0) == myMinus;
317 if (isNegative) {
318 /* Remove the minus sign */
319 myWork.deleteCharAt(0);
320 }
321
322 /* If we are using accounting mode */
323 if (useAccounting) {
324 /* Format for accounting */
325 formatForAccounting(myWork);
326 }
327
328 /* Add the currency symbol */
329 final Currency myCurrency = pMoney.getCurrency();
330 myWork.insert(0, theLocale.getSymbol(myCurrency));
331
332 /* Re-Add the minus sign */
333 if (isNegative) {
334 myWork.insert(0, myMinus);
335 }
336
337 /* return the formatted value */
338 return myWork.toString();
339 }
340
341 /**
342 * Format Price value.
343 *
344 * @param pPrice the value to format
345 * @return the formatted value
346 */
347 public String formatPrice(final OceanusPrice pPrice) {
348 /* return the formatted value */
349 return formatMoney(pPrice);
350 }
351
352 /**
353 * Format Rate value.
354 *
355 * @param pRate the value to format
356 * @return the formatted value
357 */
358 public String formatRate(final OceanusRate pRate) {
359 /* Format the basic value */
360 final StringBuilder myWork = formatDecimal(pRate.unscaledValue(), pRate.scale()
361 - OceanusDecimalParser.ADJUST_PERCENT, theLocale.getDecimal());
362
363 /* Append the perCent sign */
364 myWork.append(theLocale.getPerCent());
365
366 /* return the formatted value */
367 return myWork.toString();
368 }
369
370 /**
371 * Format Rate value.
372 *
373 * @param pRate the value to format
374 * @return the formatted value
375 */
376 public String formatRatePerMille(final OceanusRate pRate) {
377 /* Format the basic value */
378 final StringBuilder myWork = formatDecimal(pRate.unscaledValue(), pRate.scale()
379 - OceanusDecimalParser.ADJUST_PERMILLE, theLocale.getDecimal());
380
381 /* Append the perMille sign */
382 myWork.append(theLocale.getPerMille());
383
384 /* return the formatted value */
385 return myWork.toString();
386 }
387
388 /**
389 * Format Units value.
390 *
391 * @param pUnits the value to format
392 * @return the formatted value
393 */
394 public String formatUnits(final OceanusUnits pUnits) {
395 /* Format the basic value */
396 return formatBasicDecimal(pUnits);
397 }
398
399 /**
400 * Format Ratio value.
401 *
402 * @param pRatio the value to format
403 * @return the formatted value
404 */
405 public String formatRatio(final OceanusRatio pRatio) {
406 /* Format the basic value */
407 return formatBasicDecimal(pRatio);
408 }
409
410 /**
411 * Format Decimal value.
412 *
413 * @param pDecimal the value to format
414 * @return the formatted value
415 */
416 public String formatDecimal(final OceanusDecimal pDecimal) {
417 /* Split out special cases */
418 if (pDecimal instanceof OceanusMoney myMoney) {
419 return formatMoney(myMoney);
420 } else if (pDecimal instanceof OceanusRate myRate) {
421 return formatRate(myRate);
422 }
423
424 /* return the formatted value */
425 return formatBasicDecimal(pDecimal);
426 }
427
428 /**
429 * Format Decimal value.
430 *
431 * @param pDecimal the value to format
432 * @return the formatted value
433 */
434 private String formatBasicDecimal(final OceanusDecimal pDecimal) {
435 /* Format the basic value */
436 final StringBuilder myWork = formatDecimal(pDecimal.unscaledValue(), pDecimal.scale(), theLocale.getDecimal());
437
438 /* return the formatted value */
439 return myWork.toString();
440 }
441
442 /**
443 * Format for accounting.
444 *
445 * @param pWork the working buffer
446 */
447 private void formatForAccounting(final StringBuilder pWork) {
448 /* If we are short of the width */
449 int myLen = pWork.length();
450 while (myLen < theAccountingWidth) {
451 /* Prefix with blank */
452 pWork.insert(0, CHAR_BLANK);
453 myLen++;
454 }
455 }
456
457 /**
458 * Format a Zero for accounting.
459 *
460 * @param pCurrency the currency
461 * @return the formatted string
462 */
463 private String formatZeroAccounting(final Currency pCurrency) {
464 /* Determine the scale */
465 final int myScale = pCurrency.getDefaultFractionDigits();
466
467 /* Create a buffer build */
468 final StringBuilder myWork = new StringBuilder(Character.toString(CHAR_MINUS));
469
470 /* If we have decimals */
471 /* Add a blank in place of the decimal digit */
472 myWork.append(String.valueOf(CHAR_BLANK).repeat(Math.max(0, myScale)));
473
474 /* If we are short of the width */
475 int myLen = myWork.length();
476 while (myLen < theAccountingWidth) {
477 /* Prefix with blank */
478 myWork.insert(0, CHAR_BLANK);
479 myLen++;
480 }
481
482 /* Add the currency symbol */
483 myWork.insert(0, theLocale.getSymbol(pCurrency));
484
485 /* Return the string */
486 return myWork.toString();
487 }
488
489 /**
490 * Format Long value.
491 *
492 * @param pValue the value to format
493 * @return the formatted value
494 */
495 public String formatLong(final Long pValue) {
496 /* Format the basic value */
497 final StringBuilder myWork = formatLong(pValue.longValue());
498
499 /* return the formatted value */
500 return myWork.toString();
501 }
502
503 /**
504 * Format Integer value.
505 *
506 * @param pValue the value to format
507 * @return the formatted value
508 */
509 public String formatInteger(final Integer pValue) {
510 /* Format the basic value */
511 final StringBuilder myWork = formatLong(pValue.longValue());
512
513 /* return the formatted value */
514 return myWork.toString();
515 }
516
517 /**
518 * Format Short value.
519 *
520 * @param pValue the value to format
521 * @return the formatted value
522 */
523 public String formatShort(final Short pValue) {
524 /* Format the basic value */
525 final StringBuilder myWork = formatLong(pValue.longValue());
526
527 /* return the formatted value */
528 return myWork.toString();
529 }
530 }