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 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 }