View Javadoc
1   package org.djunits.unit;
2   
3   import java.util.Objects;
4   
5   import org.djunits.quantity.def.Quantity;
6   import org.djunits.unit.scale.Scale;
7   import org.djunits.unit.si.SIPrefix;
8   import org.djunits.unit.si.SIPrefixes;
9   import org.djunits.unit.system.UnitSystem;
10  import org.djutils.exceptions.Throw;
11  
12  /**
13   * The AbstractUnit is the parent class of all units, and encodes the common behavior of the units. All units are internally
14   * <i>stored</i> relative to a standard unit with conversion factor. This means that e.g., a meter is stored with conversion
15   * factor 1.0, and kilometer is stored with a conversion factor 1000.0. This means that if we want to express a length meter in
16   * kilometers, we have to <i>divide</i> by the conversion factor.
17   * <p>
18   * The conversion to and from the base unit is left to a Scale. Many scales are linear (e.g., converting dm, cm, and mm to
19   * meters), but there are also non-linear scales such as the percentage for an angle, where 90 degrees equals an infinite
20   * percentage. Copyright (c) 2025-2026 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights
21   * reserved. See for project information <a href="https://djunits.org" target="_blank">https://djunits.org</a>. The DJUNITS
22   * project is distributed under a <a href="https://djunits.org/docs/license.html" target="_blank">three-clause BSD-style
23   * license</a>.
24   * @author Alexander Verbraeck
25   * @param <U> the unit type
26   * @param <Q> the quantity type belonging to this unit
27   */
28  public abstract class AbstractUnit<U extends Unit<U, Q>, Q extends Quantity<Q>> implements Unit<U, Q>
29  {
30      /** The textual abbreviation of the unit, which is also the id. */
31      private final String textualAbbreviation;
32  
33      /** The symbolic representation of the unit, which is the default for display. */
34      private final String displayAbbreviation;
35  
36      /** The full name of the unit. */
37      private final String name;
38  
39      /** The scale to use to convert between this unit and the standard (e.g., SI, BASE) unit. */
40      private final Scale scale;
41  
42      /** The unit system, e.g. SI or Imperial. */
43      private final UnitSystem unitSystem;
44  
45      /** The SI-prefix, if any, to allow localization of the SI-prefix. */
46      private SIPrefix siPrefix = null;
47  
48      /**
49       * Create a new unit, where the textual abbreviation is the same as the display abbreviation.
50       * @param textualAbbreviation the textual abbreviation of the unit, which also serves as the id
51       * @param name the full name of the unit
52       * @param scale the scale to use to convert between this unit and the standard (e.g., SI, BASE) unit
53       * @param unitSystem unit system, e.g. SI or Imperial
54       */
55      public AbstractUnit(final String textualAbbreviation, final String name, final Scale scale, final UnitSystem unitSystem)
56      {
57          this(textualAbbreviation, textualAbbreviation, name, scale, unitSystem);
58      }
59  
60      /**
61       * Create a new unit, with textual abbreviation(s) and a display abbreviation.
62       * @param textualAbbreviation the textual abbreviation of the unit, which also serves as the id
63       * @param displayAbbreviation the display abbreviation of the unit
64       * @param name the full name of the unit
65       * @param scale the scale to use to convert between this unit and the standard (e.g., SI, BASE) unit
66       * @param unitSystem unit system, e.g. SI or Imperial
67       */
68      public AbstractUnit(final String textualAbbreviation, final String displayAbbreviation, final String name,
69              final Scale scale, final UnitSystem unitSystem)
70      {
71          // Check the validity
72          String cName = Units.unitClassName(getClass());
73          Throw.whenNull(textualAbbreviation, "Constructing unit %s: textualAbbreviation cannot be null", cName);
74          Throw.whenNull(displayAbbreviation, "Constructing unit %s: displayAbbreviation cannot be null", cName);
75          Throw.when(textualAbbreviation.length() == 0, UnitRuntimeException.class,
76                  "Constructing unit %s: textualAbbreviation string cannot be empty", cName);
77          Throw.whenNull(name, "Constructing unit %s.%s: name cannot be null", cName, textualAbbreviation);
78          Throw.when(name.length() == 0, UnitRuntimeException.class, "Constructing unit %s.%s: name.length cannot be 0", cName,
79                  textualAbbreviation);
80          Throw.whenNull(scale, "Constructing unit %s.%s: scale cannot be null", cName, textualAbbreviation);
81          Throw.whenNull(unitSystem, "Constructing unit %s.%s: unitSystem cannot be null", cName, textualAbbreviation);
82  
83          // Build the unit
84          this.displayAbbreviation = displayAbbreviation;
85          this.textualAbbreviation = textualAbbreviation;
86          this.name = name;
87          this.scale = scale;
88          this.unitSystem = unitSystem;
89  
90          // Register the unit
91          Units.register(this);
92      }
93  
94      /**
95       * Generate and register the units with all SI-prefixes.
96       * @param kilo whether the base unit already has a "kilo" in its abbreviation/name, such as the kilogram
97       * @param perUnit whether it is a "per unit" such as "per meter"
98       * @return the unit for method chaining
99       */
100     @SuppressWarnings("unchecked")
101     public U generateSiPrefixes(final boolean kilo, final boolean perUnit)
102     {
103         String cName = getClass().getSimpleName();
104         Throw.when(!getScale().isIdentityScale(), UnitRuntimeException.class,
105                 "SI prefixes generation for unit %s only applicable to unit with base scale", cName);
106         Throw.when(kilo && !perUnit && !getId().startsWith("k"), UnitRuntimeException.class,
107                 "SI prefixes generated for kilo class for unit %s, but abbreviation %s does not start with a 'k'", cName,
108                 getId());
109         Throw.when(kilo && perUnit && !getId().startsWith("/k"), UnitRuntimeException.class,
110                 "SI prefixes generated for per-kilo class for unit %s, but abbreviation %s does not start with '/k'", cName,
111                 getId());
112         Throw.when(kilo && !perUnit && !getStoredName().startsWith("kilo"), UnitRuntimeException.class,
113                 "SI prefixes generated for kilo class for unit %s, but name %s does not start with 'kilo'", cName,
114                 getStoredName());
115         Throw.when(kilo && perUnit && !getStoredName().startsWith("per kilo"), UnitRuntimeException.class,
116                 "SI prefixes generated for kilo class for unit %s, but name %s does not start with 'per kilo'", cName,
117                 getStoredName());
118         Throw.when(perUnit && !getId().startsWith("/"), UnitRuntimeException.class,
119                 "SI prefixes generated for 'per' class for unit %s, but abbreviation %s does not start with '/'", cName,
120                 getId());
121         Throw.when(perUnit && !getStoredName().startsWith("per "), UnitRuntimeException.class,
122                 "SI prefixes generated for 'per' class for unit %s, but name %s does not start with 'per '", cName,
123                 getStoredName());
124 
125         if (!kilo)
126         {
127             if (!perUnit)
128             {
129                 this.siPrefix = SIPrefixes.UNIT_PREFIXES.get("");
130                 for (SIPrefix sip : SIPrefixes.UNIT_PREFIXES.values())
131                 {
132                     if (sip.getFactor() != 1.0)
133                     {
134                         U unit = deriveUnit(sip.getDefaultTextualPrefix() + getStoredTextualAbbreviation(),
135                                 sip.getDefaultDisplayPrefix() + getStoredDisplayAbbreviation(),
136                                 sip.getPrefixName() + getStoredName(), sip.getFactor(), getUnitSystem());
137                         unit.setSiPrefix(sip);
138                     }
139                 }
140             }
141             else
142             {
143                 this.siPrefix = SIPrefixes.PER_UNIT_PREFIXES.get("/");
144                 for (SIPrefix sip : SIPrefixes.PER_UNIT_PREFIXES.values())
145                 {
146                     if (sip.getFactor() != 1.0)
147                     {
148                         U unit = deriveUnit(sip.getDefaultTextualPrefix() + getStoredTextualAbbreviation().substring(1),
149                                 sip.getDefaultDisplayPrefix() + getStoredDisplayAbbreviation().substring(1),
150                                 sip.getPrefixName() + getStoredName().substring(4), sip.getFactor(), getUnitSystem());
151                         unit.setSiPrefix(sip);
152                     }
153                 }
154             }
155         }
156         else
157         {
158             if (!perUnit)
159             {
160                 this.siPrefix = SIPrefixes.KILO_PREFIXES.get("k");
161                 for (SIPrefix sip : SIPrefixes.KILO_PREFIXES.values())
162                 {
163                     if (sip.getFactor() != 1.0)
164                     {
165                         U unit = deriveUnit(sip.getDefaultTextualPrefix() + getStoredTextualAbbreviation().substring(1),
166                                 sip.getDefaultDisplayPrefix() + getStoredDisplayAbbreviation().substring(1),
167                                 sip.getPrefixName() + getStoredName().substring(4), sip.getFactor(), getUnitSystem());
168                         unit.setSiPrefix(sip);
169                     }
170                 }
171             }
172             else
173             {
174                 this.siPrefix = SIPrefixes.PER_KILO_PREFIXES.get("/k");
175                 for (SIPrefix sip : SIPrefixes.PER_KILO_PREFIXES.values())
176                 {
177                     if (sip.getFactor() != 1.0)
178                     {
179 
180                         U unit = deriveUnit(sip.getDefaultTextualPrefix() + getStoredTextualAbbreviation().substring(2),
181                                 sip.getDefaultDisplayPrefix() + getStoredDisplayAbbreviation().substring(2),
182                                 sip.getPrefixName() + getStoredName().substring(8), sip.getFactor(), getUnitSystem());
183                         unit.setSiPrefix(sip);
184                     }
185                 }
186             }
187         }
188         return (U) this;
189     }
190 
191     /**
192      * Return a linearly scaled derived unit for this unit, where the textual abbreviation is the same as the display
193      * abbreviation.
194      * @param textualAbbreviation the textual abbreviation of the unit, which doubles as the id
195      * @param name the full name of the unit
196      * @param scaleFactor the (linear) scale factor to multiply with the current (linear) scale factor
197      * @param unitSystem unit system, e.g. SI or Imperial
198      * @return a derived unit for this unit
199      */
200     @SuppressWarnings("checkstyle:hiddenfield")
201     public U deriveUnit(final String textualAbbreviation, final String name, final double scaleFactor,
202             final UnitSystem unitSystem)
203     {
204         return deriveUnit(textualAbbreviation, textualAbbreviation, name, scaleFactor, unitSystem);
205     }
206 
207     /**
208      * Return a linearly scaled derived unit for this unit, with textual abbreviation(s) and a display abbreviation.
209      * @param textualAbbreviation the textual abbreviation of the unit, which doubles as the id
210      * @param displayAbbreviation the display abbreviation of the unit
211      * @param name the full name of the unit
212      * @param scaleFactor the (linear) scale factor to multiply with the current (linear) scale factor
213      * @param unitSystem unit system, e.g. SI or Imperial
214      * @return a derived unit for this unit
215      */
216     @SuppressWarnings("checkstyle:hiddenfield")
217     public abstract U deriveUnit(String textualAbbreviation, String displayAbbreviation, String name, double scaleFactor,
218             UnitSystem unitSystem);
219 
220     @Override
221     public String getId()
222     {
223         return this.textualAbbreviation;
224     }
225 
226     @Override
227     public String getStoredTextualAbbreviation()
228     {
229         return this.textualAbbreviation;
230     }
231 
232     @Override
233     public String getTextualAbbreviation()
234     {
235         return Units.localizedUnitTextualAbbr(getClass(), getId());
236     }
237 
238     @Override
239     public String getStoredDisplayAbbreviation()
240     {
241         return this.displayAbbreviation;
242     }
243 
244     @Override
245     public String getDisplayAbbreviation()
246     {
247         return Units.localizedUnitDisplayAbbr(getClass(), getId());
248     }
249 
250     @Override
251     public String getStoredName()
252     {
253         return this.name;
254     }
255 
256     @Override
257     public String getName()
258     {
259         return Units.localizedUnitName(getClass(), getId());
260     }
261 
262     @Override
263     public Scale getScale()
264     {
265         return this.scale;
266     }
267 
268     @Override
269     public UnitSystem getUnitSystem()
270     {
271         return this.unitSystem;
272     }
273 
274     @SuppressWarnings({"unchecked", "hiddenfield"})
275     @Override
276     public U setSiPrefix(final SIPrefix siPrefix)
277     {
278         this.siPrefix = siPrefix;
279         return (U) this;
280     }
281 
282     @Override
283     public U setSiPrefix(final String prefix)
284     {
285         SIPrefix sip = SIPrefixes.getSiPrefix(prefix);
286         return setSiPrefix(sip);
287     }
288 
289     @Override
290     public U setSiPrefixKilo(final String prefix)
291     {
292         SIPrefix sip = SIPrefixes.getSiPrefixKilo(prefix);
293         return setSiPrefix(sip);
294     }
295 
296     @Override
297     public U setSiPrefixPer(final String prefix)
298     {
299         SIPrefix sip = SIPrefixes.getSiPrefixPer(prefix);
300         return setSiPrefix(sip);
301     }
302 
303     @Override
304     public SIPrefix getSiPrefix()
305     {
306         return this.siPrefix;
307     }
308 
309     @Override
310     public int hashCode()
311     {
312         return Objects.hash(this.displayAbbreviation, this.name, this.scale, this.textualAbbreviation, this.unitSystem);
313     }
314 
315     @Override
316     @SuppressWarnings("checkstyle:needbraces")
317     public boolean equals(final Object obj)
318     {
319         if (this == obj)
320             return true;
321         if (obj == null)
322             return false;
323         if (getClass() != obj.getClass())
324             return false;
325         AbstractUnit<?, ?> other = (AbstractUnit<?, ?>) obj;
326         return Objects.equals(this.displayAbbreviation, other.displayAbbreviation) && Objects.equals(this.name, other.name)
327                 && Objects.equals(this.scale, other.scale)
328                 && Objects.equals(this.textualAbbreviation, other.textualAbbreviation)
329                 && Objects.equals(this.unitSystem, other.unitSystem);
330     }
331 
332     @Override
333     public String toString()
334     {
335         return this.displayAbbreviation;
336     }
337 }