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