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.format;
18  
19  import io.github.tonywasher.joceanus.oceanus.base.OceanusException;
20  import io.github.tonywasher.joceanus.oceanus.base.OceanusLocale;
21  import io.github.tonywasher.joceanus.oceanus.convert.OceanusDataConverter;
22  import io.github.tonywasher.joceanus.oceanus.date.OceanusDate;
23  import io.github.tonywasher.joceanus.oceanus.date.OceanusDateFormatter;
24  import io.github.tonywasher.joceanus.oceanus.date.OceanusDateRange;
25  import io.github.tonywasher.joceanus.oceanus.decimal.OceanusDecimal;
26  import io.github.tonywasher.joceanus.oceanus.decimal.OceanusDecimalFormatter;
27  import io.github.tonywasher.joceanus.oceanus.decimal.OceanusDecimalParser;
28  import io.github.tonywasher.joceanus.oceanus.decimal.OceanusMoney;
29  import io.github.tonywasher.joceanus.oceanus.decimal.OceanusPrice;
30  import io.github.tonywasher.joceanus.oceanus.decimal.OceanusRate;
31  import io.github.tonywasher.joceanus.oceanus.decimal.OceanusRatio;
32  import io.github.tonywasher.joceanus.oceanus.decimal.OceanusUnits;
33  import io.github.tonywasher.joceanus.oceanus.profile.OceanusProfile;
34  
35  import java.math.BigDecimal;
36  import java.math.BigInteger;
37  import java.time.LocalDate;
38  import java.util.ArrayList;
39  import java.util.Calendar;
40  import java.util.Date;
41  import java.util.List;
42  import java.util.Locale;
43  import java.util.Map;
44  
45  /**
46   * Data Formatter.
47   */
48  public class OceanusDataFormatter {
49      /**
50       * Formatter extension.
51       */
52      public interface OceanusDataFormatterExtension {
53          /**
54           * Format an object value.
55           *
56           * @param pValue the object to format
57           * @return the formatted value (or null if not recognised)
58           */
59          String formatObject(Object pValue);
60      }
61  
62      /**
63       * Invalid class error.
64       */
65      private static final String ERROR_CLASS = "Invalid Class: ";
66  
67      /**
68       * Date Formatter.
69       */
70      private final OceanusDateFormatter theDateFormatter;
71  
72      /**
73       * Decimal Formatter.
74       */
75      private final OceanusDecimalFormatter theDecimalFormatter;
76  
77      /**
78       * Date Formatter.
79       */
80      private final OceanusDecimalParser theDecimalParser;
81  
82      /**
83       * Extensions.
84       */
85      private final List<OceanusDataFormatterExtension> theExtensions;
86  
87      /**
88       * Constructor.
89       */
90      public OceanusDataFormatter() {
91          this(OceanusLocale.getDefaultLocale());
92      }
93  
94      /**
95       * Constructor.
96       *
97       * @param pLocale the locale
98       */
99      public OceanusDataFormatter(final Locale pLocale) {
100         theDateFormatter = new OceanusDateFormatter(pLocale);
101         theDecimalFormatter = new OceanusDecimalFormatter(pLocale);
102         theDecimalParser = new OceanusDecimalParser(pLocale);
103         theExtensions = new ArrayList<>();
104     }
105 
106     /**
107      * Obtain the date formatter.
108      *
109      * @return the formatter
110      */
111     public OceanusDateFormatter getDateFormatter() {
112         return theDateFormatter;
113     }
114 
115     /**
116      * Obtain the decimal formatter.
117      *
118      * @return the formatter
119      */
120     public OceanusDecimalFormatter getDecimalFormatter() {
121         return theDecimalFormatter;
122     }
123 
124     /**
125      * Obtain the decimal parser.
126      *
127      * @return the parser
128      */
129     public OceanusDecimalParser getDecimalParser() {
130         return theDecimalParser;
131     }
132 
133     /**
134      * Extend the formatter.
135      *
136      * @param pExtension the extension
137      */
138     public void extendFormatter(final OceanusDataFormatterExtension pExtension) {
139         theExtensions.add(pExtension);
140     }
141 
142     /**
143      * Set accounting width.
144      *
145      * @param pWidth the accounting width to use
146      */
147     public void setAccountingWidth(final int pWidth) {
148         /* Set accounting width on decimal formatter */
149         theDecimalFormatter.setAccountingWidth(pWidth);
150     }
151 
152     /**
153      * Clear accounting mode.
154      */
155     public void clearAccounting() {
156         /* Clear the accounting mode flag */
157         theDecimalFormatter.clearAccounting();
158     }
159 
160     /**
161      * Set the date format.
162      *
163      * @param pFormat the format string
164      */
165     public final void setFormat(final String pFormat) {
166         /* Tell the formatters about the format */
167         theDateFormatter.setFormat(pFormat);
168     }
169 
170     /**
171      * Set the locale.
172      *
173      * @param pLocale the locale
174      */
175     public final void setLocale(final Locale pLocale) {
176         /* Tell the formatters about the locale */
177         theDateFormatter.setLocale(pLocale);
178         theDecimalFormatter.setLocale(pLocale);
179         theDecimalParser.setLocale(pLocale);
180     }
181 
182     /**
183      * Obtain the locale.
184      *
185      * @return the locale
186      */
187     public Locale getLocale() {
188         /* Obtain locale from date formatter */
189         return theDateFormatter.getLocale();
190     }
191 
192     /**
193      * Format an object value.
194      *
195      * @param pValue the object to format
196      * @return the formatted value
197      */
198     public String formatObject(final Object pValue) {
199         /* Handle null value */
200         if (pValue == null) {
201             return null;
202         }
203 
204         /* Loop through extensions */
205         for (OceanusDataFormatterExtension myExtension : theExtensions) {
206             final String myResult = myExtension.formatObject(pValue);
207             if (myResult != null) {
208                 return myResult;
209             }
210         }
211 
212         /* Access the class */
213         final Class<?> myClass = pValue.getClass();
214 
215         /* Handle Native classes */
216         if (pValue instanceof String s) {
217             return s;
218         }
219         if (pValue instanceof Boolean) {
220             return Boolean.TRUE.equals(pValue)
221                     ? "true"
222                     : "false";
223         }
224         if (pValue instanceof Short
225                 || pValue instanceof Integer
226                 || pValue instanceof Long) {
227             return pValue.toString();
228         }
229         if (pValue instanceof Float
230                 || pValue instanceof Double) {
231             return pValue.toString();
232         }
233         if (pValue instanceof BigInteger
234                 || pValue instanceof BigDecimal) {
235             return pValue.toString();
236         }
237 
238         /* Handle Enumerated classes */
239         if (pValue instanceof Enum) {
240             return pValue.toString();
241         }
242 
243         /* Handle Class */
244         if (pValue instanceof Class<?> myClazz) {
245             return myClazz.getCanonicalName();
246         }
247 
248         /* Handle Native array classes */
249         if (pValue instanceof byte[] ba) {
250             return OceanusDataConverter.bytesToHexString(ba);
251         }
252         if (pValue instanceof char[] ca) {
253             return new String(ca);
254         }
255 
256         /* Handle date classes */
257         if (pValue instanceof Calendar myCal) {
258             return theDateFormatter.formatCalendarDay(myCal);
259         }
260         if (pValue instanceof Date myDate) {
261             return theDateFormatter.formatJavaDate(myDate);
262         }
263         if (pValue instanceof LocalDate myDate) {
264             return theDateFormatter.formatLocalDate(myDate);
265         }
266         if (pValue instanceof OceanusDate myDate) {
267             return theDateFormatter.formatDate(myDate);
268         }
269         if (pValue instanceof OceanusDateRange myRange) {
270             return theDateFormatter.formatDateRange(myRange);
271         }
272 
273         /* Handle decimal classes */
274         if (pValue instanceof OceanusDecimal myDecimal) {
275             return theDecimalFormatter.formatDecimal(myDecimal);
276         }
277 
278         /* Handle TethysProfile */
279         if (pValue instanceof OceanusProfile myProfile) {
280             /* Format the profile */
281             return myProfile.getName()
282                     + ": "
283                     + (myProfile.isRunning()
284                     ? myProfile.getStatus()
285                     : myProfile.getElapsed());
286         }
287 
288         /* Handle OceanusExceptions */
289         if (pValue instanceof OceanusException) {
290             return myClass.getSimpleName();
291         }
292 
293         /* Standard format option */
294         return formatBasicValue(pValue);
295     }
296 
297     /**
298      * Parse object value.
299      *
300      * @param <T>     the value type
301      * @param pSource the source value
302      * @param pClazz  the value type class
303      * @return the formatted value
304      * @throws IllegalArgumentException on bad Date/Decimal format
305      * @throws NumberFormatException    on bad Integer format
306      */
307     public <T> T parseValue(final String pSource,
308                             final Class<T> pClazz) {
309         if (Boolean.class.equals(pClazz)) {
310             return pClazz.cast(Boolean.parseBoolean(pSource));
311         }
312         if (Short.class.equals(pClazz)) {
313             return pClazz.cast(Short.parseShort(pSource));
314         }
315         if (Integer.class.equals(pClazz)) {
316             return pClazz.cast(Integer.parseInt(pSource));
317         }
318         if (Long.class.equals(pClazz)) {
319             return pClazz.cast(Long.parseLong(pSource));
320         }
321         if (Float.class.equals(pClazz)) {
322             return pClazz.cast(Float.parseFloat(pSource));
323         }
324         if (Double.class.equals(pClazz)) {
325             return pClazz.cast(Double.parseDouble(pSource));
326         }
327         if (BigInteger.class.equals(pClazz)) {
328             return pClazz.cast(new BigInteger(pSource));
329         }
330         if (BigDecimal.class.equals(pClazz)) {
331             return pClazz.cast(new BigDecimal(pSource));
332         }
333         if (Date.class.equals(pClazz)) {
334             /* Parse the date */
335             return pClazz.cast(theDateFormatter.parseJavaDate(pSource));
336         }
337         if (OceanusDate.class.equals(pClazz)) {
338             /* Parse the date */
339             return pClazz.cast(theDateFormatter.parseDate(pSource));
340         }
341         if (Calendar.class.equals(pClazz)) {
342             /* Parse the date */
343             return pClazz.cast(theDateFormatter.parseCalendarDay(pSource));
344         }
345         if (LocalDate.class.equals(pClazz)) {
346             /* Parse the date */
347             return pClazz.cast(theDateFormatter.parseLocalDate(pSource));
348         }
349         if (OceanusPrice.class.equals(pClazz)) {
350             /* Parse the price */
351             return pClazz.cast(theDecimalParser.parsePriceValue(pSource));
352         }
353         if (OceanusMoney.class.equals(pClazz)) {
354             /* Parse the money */
355             return pClazz.cast(theDecimalParser.parseMoneyValue(pSource));
356         }
357         if (OceanusRate.class.equals(pClazz)) {
358             /* Parse the rate */
359             return pClazz.cast(theDecimalParser.parseRateValue(pSource));
360         }
361         if (OceanusUnits.class.equals(pClazz)) {
362             /* Parse the units */
363             return pClazz.cast(theDecimalParser.parseUnitsValue(pSource));
364         }
365         if (OceanusRatio.class.equals(pClazz)) {
366             /* Parse the dilution */
367             return pClazz.cast(theDecimalParser.parseRatioValue(pSource));
368         }
369         throw new IllegalArgumentException(ERROR_CLASS + pClazz.getSimpleName());
370     }
371 
372     /**
373      * Parse object value.
374      *
375      * @param <T>     the value type
376      * @param pSource the source value
377      * @param pClazz  the value type class
378      * @return the formatted value
379      * @throws IllegalArgumentException on bad TethysDecimal format
380      */
381     public <T> T parseValue(final Double pSource,
382                             final Class<T> pClazz) {
383         if (OceanusPrice.class.equals(pClazz)) {
384             /* Parse the price */
385             return pClazz.cast(theDecimalParser.createPriceFromDouble(pSource));
386         }
387         if (OceanusMoney.class.equals(pClazz)) {
388             /* Parse the money */
389             return pClazz.cast(theDecimalParser.createMoneyFromDouble(pSource));
390         }
391         if (OceanusRate.class.equals(pClazz)) {
392             /* Parse the rate */
393             return pClazz.cast(theDecimalParser.createRateFromDouble(pSource));
394         }
395         if (OceanusUnits.class.equals(pClazz)) {
396             /* Parse the units */
397             return pClazz.cast(theDecimalParser.createUnitsFromDouble(pSource));
398         }
399         if (OceanusRatio.class.equals(pClazz)) {
400             /* Parse the dilution */
401             return pClazz.cast(theDecimalParser.createRatioFromDouble(pSource));
402         }
403         throw new IllegalArgumentException(ERROR_CLASS + pClazz.getSimpleName());
404     }
405 
406     /**
407      * Parse object value.
408      *
409      * @param <T>       the value type
410      * @param pSource   the source value
411      * @param pCurrCode the currency code
412      * @param pClazz    the value type class
413      * @return the formatted value
414      * @throws IllegalArgumentException on bad TethysDecimal format
415      */
416     public <T> T parseValue(final Double pSource,
417                             final String pCurrCode,
418                             final Class<T> pClazz) {
419         if (OceanusPrice.class.equals(pClazz)) {
420             /* Parse the price */
421             return pClazz.cast(theDecimalParser.createPriceFromDouble(pSource, pCurrCode));
422         }
423         if (OceanusMoney.class.equals(pClazz)) {
424             /* Parse the money */
425             return pClazz.cast(theDecimalParser.createMoneyFromDouble(pSource, pCurrCode));
426         }
427         throw new IllegalArgumentException(ERROR_CLASS + pClazz.getSimpleName());
428     }
429 
430     /**
431      * Format basic object.
432      *
433      * @param pValue the object
434      * @return the formatted value
435      */
436     private static String formatBasicValue(final Object pValue) {
437         /* Access the class */
438         final Class<?> myClass = pValue.getClass();
439 
440         /* Create basic result */
441         final StringBuilder myBuilder = new StringBuilder();
442         myBuilder.append(myClass.getCanonicalName());
443 
444         /* Handle list/map instances */
445         if (pValue instanceof List<?> myList) {
446             formatSize(myBuilder, myList.size());
447         } else if (pValue instanceof Map<?, ?> myMap) {
448             formatSize(myBuilder, myMap.size());
449         }
450 
451         /* Return the value */
452         return myBuilder.toString();
453     }
454 
455     /**
456      * Format size.
457      *
458      * @param pBuilder the string builder
459      * @param pSize    the size
460      */
461     private static void formatSize(final StringBuilder pBuilder,
462                                    final Object pSize) {
463         /* Append the size */
464         pBuilder.append('(');
465         pBuilder.append(pSize);
466         pBuilder.append(')');
467     }
468 }