View Javadoc
1   package org.djunits.formatter;
2   
3   import java.math.BigDecimal;
4   import java.text.DecimalFormat;
5   import java.text.DecimalFormatSymbols;
6   import java.util.Locale;
7   
8   import org.djunits.quantity.def.Reference;
9   import org.djunits.unit.Unit;
10  import org.djunits.unit.Units;
11  import org.djunits.value.Value;
12  
13  /**
14   * Formatter of quantities, vectors, matrices and tables according to the format options that are stored in the
15   * {@link FormatContext} or one of its extensions.
16   * <p>
17   * Copyright (c) 2025-2026 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
18   * for project information <a href="https://djunits.org" target="_blank">https://djunits.org</a>. The DJUNITS project is
19   * distributed under a <a href="https://djunits.org/docs/license.html" target="_blank">three-clause BSD-style license</a>.
20   * @author Alexander Verbraeck
21   * @author Peter Knoppers
22   * @param <C> the specific {@link FormatContext} that is used
23   */
24  @SuppressWarnings({"checkstyle:needbraces", "checkstyle:visibilitymodifier"})
25  public abstract class Formatter<C extends FormatContext>
26  {
27      /** the format context. */
28      final C ctx;
29  
30      /** the value (quantity, vector, matrix) with a display unit. */
31      final Value<?, ?> value;
32  
33      /** the unit to express the value in. */
34      Unit<?, ?> unit;
35  
36      /** the formatted unit. */
37      String unitStr = null;
38  
39      /** using SI value or valueInUnit for the calculated unit. */
40      boolean useSi = false;
41  
42      /**
43       * @param value the value to format
44       * @param ctx the format context
45       */
46      Formatter(final Value<?, ?> value, final C ctx)
47      {
48          this.ctx = ctx;
49          this.value = value;
50          this.unit = value.getDisplayUnit();
51      }
52  
53      /**
54       * Format the value(s) and unit and return the String representation.
55       * @return the formatted value and unit
56       */
57      abstract String format();
58  
59      /**
60       * Check if SI formatting needs to be applied.
61       * @return whether SI formatting was applied
62       */
63      boolean checkSiUnits()
64      {
65          if (!this.ctx.siUnits)
66              return false;
67          this.unit = this.unit.siUnit();
68          this.useSi = true;
69          this.unitStr = this.unit.siUnit().format(this.ctx.siDivisionSymbol, this.ctx.siDotSeparator, this.ctx.siPowerPrefix,
70                  this.ctx.siPowerPostfix);
71          return true;
72      }
73  
74      /**
75       * Apply the unit string if present.
76       * @return whether unit string formatting was applied
77       */
78      boolean checkUnitString()
79      {
80          if (this.ctx.unitString != null)
81          {
82              try
83              {
84                  this.unit = Units.resolve(this.unit.getClass(), this.ctx.unitString);
85                  this.useSi = false;
86                  return true;
87              }
88              catch (Exception e)
89              {
90                  // silently ignore
91              }
92          }
93          return false;
94      }
95  
96      /**
97       * Apply the display unit if present.
98       * @return whether display unit formatting was applied
99       */
100     boolean checkDisplayUnit()
101     {
102         if (this.ctx.displayUnit != null)
103         {
104             try
105             {
106                 this.unit = Units.resolve(this.unit.getClass(), this.ctx.displayUnit.getId());
107                 this.useSi = false;
108                 return true;
109             }
110             catch (Exception e)
111             {
112                 // silently ignore
113             }
114         }
115         return false;
116     }
117 
118     /**
119      * Format the unit according to the context settings.
120      */
121     @SuppressWarnings("checkstyle:needbraces")
122     void formatUnit()
123     {
124         boolean formatted = checkSiUnits();
125         if (!formatted)
126             formatted = checkUnitString();
127         if (!formatted)
128             formatted = checkDisplayUnit();
129         if (this.unitStr == null)
130             this.unitStr = this.ctx.textual ? this.unit.getTextualAbbreviation() : this.unit.getDisplayAbbreviation();
131     }
132 
133     /**
134      * Format the reference of an absolute value according to the context settings.
135      * @param ctx the format context with the settings for formatting a reference
136      * @param reference the reference to format
137      * @return the formatted reference, or an empty string when it is not displayed
138      */
139     static String formatReference(final FormatContext ctx, final Reference<?, ?, ?> reference)
140     {
141         if (!ctx.printReference)
142         {
143             return "";
144         }
145         return ctx.referencePrefix + reference.getId() + ctx.referencePostfix;
146     }
147 
148     /**
149      * Format a value according to the context settings.
150      * @param val the value to format
151      * @return the formatted value according to the context settings
152      */
153     String formatValue(final double val)
154     {
155         return switch (this.ctx.formatMode)
156         {
157             case VARIABLE_LENGTH -> formatVariableLength(val);
158             case FIXED_FLOAT -> formatFixedFloat(val);
159             case SCIENTIFIC_ALWAYS -> formatScientific(val);
160             case ENGINEERING_ALWAYS -> formatEngineering(val);
161             case FIXED_WITH_SCI_FALLBACK -> formatFixedSciFallback(val);
162             case FIXED_WITH_ENG_FALLBACK -> formatFixedEngFallback(val);
163             case FORMAT_STRING -> String.format(this.ctx.formatString, val);
164         };
165     }
166 
167     /**
168      * Format a value with variable length.
169      * @param val the value to format
170      * @return a formatted value with variable length
171      */
172     String formatVariableLength(final double val)
173     {
174         BigDecimal bd = BigDecimal.valueOf(val).stripTrailingZeros();
175         int left = bd.precision() - bd.scale(); // # digits left of decimal point
176         DecimalFormatSymbols sym = DecimalFormatSymbols.getInstance(Locale.getDefault());
177         DecimalFormat plain = this.ctx.groupingSeparator ? new DecimalFormat("#,##0.############################", sym)
178                 : new DecimalFormat("###0.############################", sym);
179         DecimalFormat sci = new DecimalFormat("0.################E0", sym);
180 
181         // Heuristic similar to %g
182         if (left > 10 || left < -6)
183         {
184             return sci.format(bd);
185         }
186         else
187         {
188             return plain.format(bd);
189         }
190     }
191 
192     /**
193      * Format a value with a fixed floating point length and a given number of decimals.
194      * @param val the value to format
195      * @return a formatted value with a fixed floating point length and a given number of decimals
196      */
197     String formatFixedFloat(final double val)
198     {
199         String gs = this.ctx.groupingSeparator ? "%," : "%";
200         String fmt = gs + this.ctx.width + "." + this.ctx.decimals + "f";
201         return String.format(fmt, val);
202     }
203 
204     /**
205      * Format a value using scientific notation with a fixed length and a given number of decimals.
206      * @param val the value to format
207      * @return a formatted value using scientific notation with a fixed length and a given number of decimals
208      */
209     String formatScientific(final double val)
210     {
211         String fmt = "%" + this.ctx.width + "." + this.ctx.decimals + (this.ctx.upperE ? "E" : "e");
212         return String.format(fmt, val);
213     }
214 
215     /**
216      * Format a value using engineering notation with a fixed length and a given number of decimals.
217      * @param val the value to format
218      * @return a formatted value using engineering notation with a fixed length and a given number of decimals
219      */
220     String formatEngineering(final double val)
221     {
222         if (val == 0.0)
223         {
224             return formatFixedFloat(0.0);
225         }
226 
227         double abs = Math.abs(val);
228         int exp = (int) Math.floor(Math.log10(abs));
229         int engExp = exp - (exp % 3);
230 
231         double mantissa = val / Math.pow(10, engExp);
232 
233         // Mantissa formatted as fixed
234         String gs = this.ctx.groupingSeparator ? "%,." : "%.";
235         String mantFmt = gs + this.ctx.decimals + "f";
236         String mant = String.format(mantFmt, mantissa);
237         String result = mant + (this.ctx.upperE ? "E" : "e") + String.format("%+d", engExp);
238         return pad(result, this.ctx.width);
239     }
240 
241     /**
242      * Format a value using fixed length, but when it does not fit, fall back to scientific notation.
243      * @param val the value to format
244      * @return a formatted value using fixed length, but when it does not fit, fall back to scientific notation.
245      */
246     String formatFixedSciFallback(final double val)
247     {
248         String s = formatFixedFloat(val);
249         if (s.length() <= this.ctx.width)
250             return s;
251         return formatScientific(val);
252     }
253 
254     /**
255      * Format a value using fixed length, but when it does not fit, fall back to engineering notation.
256      * @param val the value to format
257      * @return a formatted value using fixed length, but when it does not fit, fall back to engineering notation.
258      */
259     String formatFixedEngFallback(final double val)
260     {
261         String s = formatFixedFloat(val);
262         if (s.length() <= this.ctx.width)
263             return s;
264         return formatEngineering(val);
265     }
266 
267     /**
268      * Pad a string with spaces.
269      * @param s the string to pad
270      * @param width the width
271      * @return a padded string
272      */
273     static String pad(final String s, final int width)
274     {
275         if (s.length() >= width)
276             return s;
277         return String.format("%" + width + "s", s);
278     }
279 
280     /**
281      * Save the current locale, and change the locale.
282      * @param newLocale the new locale (can be null if the locale does not change)
283      * @return the old locale, or null when the locale was not changed
284      */
285     static Locale saveLocale(final Locale newLocale)
286     {
287         if (newLocale != null)
288         {
289             Locale oldLocale = Locale.getDefault();
290             Locale.setDefault(newLocale);
291             return oldLocale;
292         }
293         return null;
294     }
295 
296     /**
297      * Restore the locale to the old locale.
298      * @param oldLocale the old locale (can be null if the locale was not changed earlier)
299      */
300     static void restoreLocale(final Locale oldLocale)
301     {
302         if (oldLocale != null)
303         {
304             Locale.setDefault(oldLocale);
305         }
306     }
307 
308 }