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