View Javadoc
1   package org.djunits.formatter;
2   
3   import java.util.Locale;
4   
5   import org.djunits.quantity.def.AbsQuantity;
6   import org.djunits.quantity.def.Quantity;
7   import org.djunits.unit.Unit;
8   import org.djunits.unit.Units;
9   import org.djunits.unit.si.PrefixType;
10  import org.djunits.unit.si.SIPrefix;
11  import org.djunits.unit.si.SIPrefixes;
12  
13  /**
14   * QuantityFormatter formats a quantity as a String, using the settings of the {@link QuantityFormatContext}. The
15   * {@link QuantityFormatContext} is filled by using the setters in the {@link QuantityFormat} class. Note that there is no
16   * guarantee that the format settings can always be satisfied. As an example, when the required width is too small to fit the
17   * answer, the output will show the correct result, but violate the width format setting.
18   * <p>
19   * Copyright (c) 2026-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   */
24  public class QuantityFormatter extends Formatter<QuantityFormatContext>
25  {
26      /**
27       * @param quantity the quantity to format
28       * @param ctx the quantity format context
29       */
30      QuantityFormatter(final Quantity<?> quantity, final QuantityFormatContext ctx)
31      {
32          super(quantity, ctx);
33      }
34  
35      /**
36       * Format a quantity according to a given {@link QuantityFormat}. Note that this method might not be thread-safe for setting
37       * the Locale. If another thread changes the Locale while formatting, outcomes could vary.
38       * @param quantity the quantity to format
39       * @param quantityFormat the format to apply to the quantity
40       * @return a String with a formatted quantity, matching the given format as closely as possible
41       */
42      public static String format(final Quantity<?> quantity, final QuantityFormat quantityFormat)
43      {
44          QuantityFormatContext ctx = quantityFormat.ctx;
45          Locale savedLocale = Locale.getDefault();
46          try
47          {
48              savedLocale = saveLocale(ctx.locale);
49              return new QuantityFormatter(quantity, ctx).format();
50          }
51          finally
52          {
53              restoreLocale(savedLocale);
54          }
55      }
56  
57      /**
58       * Format an absolute quantity according to a given {@link QuantityFormat}. Note that this method might not be thread-safe
59       * for setting the Locale. If another thread changes the Locale while formatting, outcomes could vary.
60       * @param absQuantity the absolute quantity to format
61       * @param quantityFormat the format to apply to the absolute quantity
62       * @return a String with a formatted absolute quantity, matching the given format as closely as possible
63       */
64      public static String format(final AbsQuantity<?, ?, ?> absQuantity, final QuantityFormat quantityFormat)
65      {
66          QuantityFormatContext ctx = quantityFormat.ctx;
67          Locale savedLocale = Locale.getDefault();
68          try
69          {
70              savedLocale = saveLocale(ctx.locale);
71              return new QuantityFormatter(absQuantity.getQuantity(), ctx).format()
72                      + formatReference(ctx, absQuantity.getReference());
73          }
74          finally
75          {
76              restoreLocale(savedLocale);
77          }
78      }
79  
80      /**
81       * Return the value as a quantity.
82       * @return the value as a quantity
83       */
84      Quantity<?> quantity()
85      {
86          return (Quantity<?>) this.value;
87      }
88  
89      /**
90       * Return the quantity, formatted according to the context settings.
91       * @return the formatted quantity
92       */
93      @Override
94      String format()
95      {
96          formatUnit();
97          double value = this.useSi ? quantity().si : this.unit.getScale().fromIdentityScale(quantity().si());
98          return formatValue(value) + this.ctx.unitPrefix + this.unitStr + this.ctx.unitPostfix;
99      }
100 
101     /**
102      * Format the unit according to the context settings.
103      */
104     @SuppressWarnings("checkstyle:needbraces")
105     @Override
106     void formatUnit()
107     {
108         boolean formatted = checkSiUnits();
109         if (!formatted)
110             formatted = checkUnitString();
111         if (!formatted)
112             formatted = checkDisplayUnit();
113         checkScaleSiPrefixes();
114         if (this.unitStr == null)
115             this.unitStr = this.ctx.textual ? this.unit.getTextualAbbreviation() : this.unit.getDisplayAbbreviation();
116     }
117 
118     /**
119      * Apply the display unit if present.
120      * @return whether display unit formatting was applied
121      * @param <Q> the quantity type
122      */
123     @SuppressWarnings("unchecked")
124     private <Q extends Quantity<Q>> boolean checkScaleSiPrefixes()
125     {
126         Q q = (Q) quantity();
127         if (this.ctx.scaleSiPrefixes && this.unit.getSiPrefix() != null)
128         {
129             PrefixType type = this.unit.getSiPrefix().getType();
130             double si = q.si();
131             double log10si = Math.log10(si);
132             int power = log10si > 0 ? 3 * (int) (Math.log10(si) / 3.0) : 3 * (int) (Math.log10(si) / 3.0 - 1);
133             if (power >= this.ctx.minimumPrefixPower && power <= this.ctx.maximumPrefixPower)
134             {
135                 switch (type)
136                 {
137                     case UNIT:
138                     {
139                         SIPrefix prefix = SIPrefixes.FACTORS.getOrDefault(power, SIPrefixes.getSiPrefix(""));
140                         String key = prefix.getDefaultTextualPrefix() + q.getDisplayUnit().getBaseUnit().getId();
141                         this.unit = (Unit<?, Q>) Units.resolve(q.getDisplayUnit().getClass(), key);
142                         return true;
143                     }
144                     case KILO:
145                     {
146                         power += 3;
147                         SIPrefix prefix = SIPrefixes.FACTORS.getOrDefault(power, SIPrefixes.getSiPrefix(""));
148                         String key = prefix.getDefaultTextualPrefix() + q.getDisplayUnit().getBaseUnit().getId().substring(1);
149                         this.unit = (Unit<?, Q>) Units.resolve(q.getDisplayUnit().getClass(), key);
150                         return true;
151                     }
152                     case PER_UNIT:
153                     {
154                         SIPrefix prefix = SIPrefixes.FACTORS.getOrDefault(-power, SIPrefixes.getSiPrefix(""));
155                         String key =
156                                 "/" + prefix.getDefaultTextualPrefix() + q.getDisplayUnit().getBaseUnit().getId().substring(1);
157                         this.unit = (Unit<?, Q>) Units.resolve(q.getDisplayUnit().getClass(), key);
158                         return true;
159                     }
160                     case PER_KILO:
161                     {
162                         power -= 3;
163                         SIPrefix prefix = SIPrefixes.FACTORS.getOrDefault(-power, SIPrefixes.getSiPrefix(""));
164                         String key =
165                                 "/" + prefix.getDefaultTextualPrefix() + q.getDisplayUnit().getBaseUnit().getId().substring(2);
166                         this.unit = (Unit<?, Q>) Units.resolve(q.getDisplayUnit().getClass(), key);
167                         return true;
168                     }
169                     default:
170                         // silently ignore
171                 }
172             }
173         }
174         return false;
175     }
176 
177 }