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