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