View Javadoc
1   package org.djunits.quantity;
2   
3   import java.io.Serializable;
4   import java.util.LinkedHashMap;
5   import java.util.LinkedHashSet;
6   import java.util.Locale;
7   import java.util.Map;
8   import java.util.Set;
9   
10  import org.djunits.locale.UnitLocale;
11  import org.djunits.unit.Unit;
12  import org.djunits.unit.si.SIDimensions;
13  import org.djunits.unit.si.SIPrefix;
14  import org.djunits.unit.si.SIPrefixes;
15  import org.djunits.unit.util.UnitException;
16  import org.djunits.unit.util.UnitRuntimeException;
17  import org.djutils.exceptions.Throw;
18  
19  /**
20   * Quantity contains a map of all registered units belonging to this base. It also contains the SI 'fingerprint' of the unit.
21   * The fingerprint is registered in the UnitTypes singleton where are unit types are registered.
22   * <p>
23   * Copyright (c) 2019-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
24   * BSD-style license. See <a href="https://djunits.org/docs/license.html">DJUNITS License</a>
25   * </p>
26   * @author <a href="https://www.tudelft.nl/averbraeck" target="_blank">Alexander Verbraeck</a>
27   * @param <U> the unit to reference the actual unit in return values
28   */
29  public class Quantity<U extends Unit<U>> implements Serializable
30  {
31      /** */
32      private static final long serialVersionUID = 20190818L;
33  
34      /**
35       * The SI dimensions of the unit. Also filled for e.g., imperial values with a conversion factor to an SIDimensions. When a
36       * value has no SI dimensions, all 9 dimensions can be set to zero.
37       */
38      private final SIDimensions siDimensions;
39  
40      /** Name of the quantity. */
41      private final String name;
42  
43      /** Derived units for this unit base, retrievable by id. The key is the unit id (e.g., "m"). */
44      private final Map<String, U> unitsById = new LinkedHashMap<String, U>();
45  
46      /** Derived units for this unit base, retrievable by abbreviation. The key is the unit abbreviation (e.g., "kWh"). */
47      private final Map<String, U> unitsByAbbreviation = new LinkedHashMap<String, U>();
48  
49      /** The standard unit belonging to this unit base. The first unit that gets registered is considered to be standard. */
50      private U standardUnit = null;
51  
52      /** Derived units for this unit base, retrievable by localized abbreviation. The key is the localized abbreviation. */
53      private final Map<String, U> unitsByLocalizedAbbreviation = new LinkedHashMap<String, U>();
54  
55      /** Last loaded Locale for the localized abbreviations. */
56      private static Locale currentLocale = null;
57  
58      /** Localization information. */
59      private static UnitLocale localization = new UnitLocale("unit");
60  
61      /**
62       * Create a unit base with the SI dimensions.
63       * @param name String; the quantity name (CamelCase)
64       * @param siDimensions SIDimensions; the 9 dimensions of the unit, wrapped in an SIDimensions object
65       * @throws NullPointerException when one of the arguments is null
66       */
67      public Quantity(final String name, final SIDimensions siDimensions)
68      {
69          Throw.whenNull(name, "name cannot be null");
70          Throw.when(name.length() == 0, UnitRuntimeException.class, "name of unit cannot be empty");
71          Throw.whenNull(siDimensions, "siDimensions cannot be null");
72          this.name = name;
73          this.siDimensions = siDimensions;
74      }
75  
76      /**
77       * Create a unit base with the SI dimensions as a String.
78       * @param name String; the quantity name (CamelCase)
79       * @param siString String; the 9 dimensions of the unit, represented as an SI string
80       * @throws UnitRuntimeException when the String cannot be translated into an SIDimensions object
81       * @throws NullPointerException when one of the arguments is null
82       */
83      public Quantity(final String name, final String siString) throws UnitRuntimeException
84      {
85          Throw.whenNull(name, "name cannot be null");
86          Throw.when(name.length() == 1, UnitRuntimeException.class, "name of unit cannot be empty");
87          Throw.whenNull(siString, "siString cannot be null");
88          this.name = name;
89          try
90          {
91              this.siDimensions = SIDimensions.of(siString);
92          }
93          catch (UnitException exception)
94          {
95              throw new UnitRuntimeException(exception);
96          }
97      }
98  
99      /**
100      * Create a unit base with the SI dimensions, provided as a byte array.
101      * @param name String; the quantity name (CamelCase)
102      * @param siSignature byte[]; the 9 dimensions of the unit
103      * @throws NullPointerException when one of the arguments is null
104      */
105     public Quantity(final String name, final byte[] siSignature)
106     {
107         this(name, new SIDimensions(siSignature));
108     }
109 
110     /**
111      * Register the unit in the map. If the unit supports SI prefixes from yocto to yotta, 20 additional abbreviations are
112      * registered. When there is both a unit with an "SI prefix" and a separately registered unit, the most specific
113      * specification will be registered in the map. As an example, when the LengthUnit "METER" is registered, all 20 units such
114      * as the millimeter and the kilometer are registered as well. When earlier or later the "KILOMETER" is created as a
115      * separate unit, the "km" lookup will result in the "KILOMETER" registration rather than in the "METER" registration with a
116      * factor of 1000.
117      * @param unit U; the unit to register in the map.
118      * @param siPrefixes SIPrefixes; indicates whether and which SI prefixes should be generated.
119      * @param siPrefixPower double; the power factor of the SI prefixes, e.g. 2.0 for square meters and 3.0 for cubic meters.
120      */
121     public void registerUnit(final U unit, final SIPrefixes siPrefixes, final double siPrefixPower)
122     {
123         Throw.whenNull(unit, "unit cannot be null");
124         if (this.standardUnit == null)
125         {
126             this.standardUnit = unit; // The first unit that gets registered is considered to be standard
127             Quantities.INSTANCE.register(this);
128         }
129         if (siPrefixes.equals(SIPrefixes.UNIT))
130         {
131             for (SIPrefix siPrefix : SIPrefixes.UNIT_PREFIXES.values())
132             {
133                 unit.deriveSI(siPrefix, siPrefixPower, true); // true = automatically generated
134                 // the unit will register itself as a generated unit
135             }
136         }
137         else if (siPrefixes.equals(SIPrefixes.UNIT_POS))
138         {
139             for (SIPrefix siPrefix : SIPrefixes.UNIT_POS_PREFIXES.values())
140             {
141                 unit.deriveSI(siPrefix, siPrefixPower, true); // true = automatically generated
142             }
143         }
144         else if (siPrefixes.equals(SIPrefixes.KILO))
145         {
146             for (SIPrefix siPrefix : SIPrefixes.KILO_PREFIXES.values())
147             {
148                 unit.deriveSIKilo(siPrefix, siPrefixPower, true); // true = automatically generated
149             }
150         }
151         else if (siPrefixes.equals(SIPrefixes.PER_UNIT))
152         {
153             for (SIPrefix siPrefix : SIPrefixes.PER_UNIT_PREFIXES.values())
154             {
155                 unit.derivePerSI(siPrefix, siPrefixPower, true); // true = automatically generated
156             }
157         }
158 
159         // register the (generated) unit
160         if (this.unitsById.containsKey(unit.getId()))
161         {
162             // if both are generated or both are not generated, give an error
163             if (this.unitsById.get(unit.getId()).isGenerated() == unit.isGenerated())
164             {
165                 throw new UnitRuntimeException("A unit with id " + unit.getId() + " has already been registered for unit type "
166                         + unit.getClass().getSimpleName());
167             }
168             else
169             {
170                 if (!unit.isGenerated())
171                 {
172                     // if the new unit is explicit, register and overwrite the existing one
173                     this.unitsById.put(unit.getId(), unit);
174                 }
175                 // otherwise, the new unit is generated, and the existing one was explicit: ignore the generated one
176             }
177         }
178         else
179         {
180             // not registered yet
181             this.unitsById.put(unit.getId(), unit);
182         }
183 
184         // register the abbreviation(s) of the (generated) unit
185         for (String abbreviation : unit.getDefaultAbbreviations())
186         {
187             if (this.unitsByAbbreviation.containsKey(abbreviation))
188             {
189                 // if both are generated or both are not generated, give an exception
190                 if (this.unitsByAbbreviation.get(abbreviation).isGenerated() == unit.isGenerated())
191                 {
192                     throw new UnitRuntimeException("A unit with abbreviation " + abbreviation
193                             + " has already been registered for unit type " + unit.getClass().getSimpleName());
194                 }
195                 else
196                 {
197                     if (!unit.isGenerated())
198                     {
199                         // overwrite the automatically generated unit with the explicit one
200                         this.unitsByAbbreviation.put(abbreviation, unit);
201                     }
202                     // otherwise, the new unit is generated, and the existing one was explicit: ignore the generated one
203                 }
204             }
205             else
206             {
207                 // not registered yet
208                 this.unitsByAbbreviation.put(abbreviation, unit);
209             }
210         }
211     }
212 
213     /**
214      * Unregister a unit from the registry, e.g. after a Unit test, or to insert a replacement for an already existing unit.
215      * @param unit U; the unit to unregister.
216      */
217     public void unregister(final U unit)
218     {
219         Throw.whenNull(unit, "null unit cannot be removed from the unit registry");
220         if (this.unitsById.containsValue(unit))
221         {
222             this.unitsById.remove(unit.getId(), unit);
223         }
224         for (String abbreviation : unit.getDefaultAbbreviations())
225         {
226             if (this.unitsByAbbreviation.containsKey(abbreviation))
227             {
228                 if (unit.equals(this.unitsByAbbreviation.get(abbreviation)))
229                 {
230                     this.unitsByAbbreviation.remove(abbreviation, unit);
231                 }
232             }
233         }
234     }
235 
236     /**
237      * Retrieve the name of the quantity.
238      * @return String; the name of the quantity
239      */
240     public final String getName()
241     {
242         return this.name;
243     }
244 
245     /**
246      * @return the siDimensions
247      */
248     public final SIDimensions getSiDimensions()
249     {
250         return this.siDimensions;
251     }
252 
253     /**
254      * Retrieve a unit by Id.
255      * @param id String; the id to look up
256      * @return the corresponding unit or null when it was not found
257      */
258     public U getUnitById(final String id)
259     {
260         return this.unitsById.get(id);
261     }
262 
263     /**
264      * Check whether the locale for which abbreviation maps have been loaded is still current. If not, (re)load.
265      */
266     protected void checkLocale()
267     {
268         if (currentLocale == null || !currentLocale.equals(Locale.getDefault(Locale.Category.DISPLAY)))
269         {
270             localization.checkReload();
271             this.unitsByLocalizedAbbreviation.clear();
272             for (String id : this.unitsById.keySet())
273             {
274                 String[] abbreviationArray = localization.getString(getName() + "." + id).split("\\|");
275                 for (String abb : abbreviationArray)
276                 {
277                     this.unitsByLocalizedAbbreviation.put(abb.strip(), this.unitsById.get(id));
278                 }
279             }
280             currentLocale = Locale.getDefault(Locale.Category.DISPLAY);
281         }
282     }
283 
284     /**
285      * Retrieve a unit by one of its abbreviations. First try whether the abbreviation itself is available. If not, look up the
286      * unit without spaces, "." and "^" to map e.g., "kg.m/s^2" to "kgm/s2". If that fails, see if the unit is an SIDimensions
287      * string. If not, return null.
288      * @param abbreviation String; the abbreviation to look up
289      * @return the corresponding unit or null when it was not found
290      */
291     public U getUnitByAbbreviation(final String abbreviation)
292     {
293         checkLocale();
294         U unit = this.unitsByLocalizedAbbreviation.get(abbreviation);
295         if (unit == null)
296         {
297             unit = this.unitsByLocalizedAbbreviation.get(abbreviation.replaceAll("[ .^]", ""));
298         }
299         if (unit == null)
300         {
301             unit = this.unitsByAbbreviation.get(abbreviation);
302         }
303         if (unit == null)
304         {
305             unit = this.unitsByAbbreviation.get(abbreviation.replaceAll("[ .^]", ""));
306         }
307         if (unit == null)
308         {
309             try
310             {
311                 SIDimensions dim = SIDimensions.of(abbreviation);
312                 if (dim != null && dim.equals(this.siDimensions))
313                 {
314                     unit = this.standardUnit;
315                 }
316             }
317             catch (UnitException exception)
318             {
319                 unit = null;
320             }
321         }
322         return unit;
323     }
324 
325     /**
326      * Retrieve a unit by one of its abbreviations. First try whether the abbreviation itself is available. If not, try without
327      * "." that might separate the units (e.g., "N.m"). If that fails, look up the unit without "." and "^" to map e.g.,
328      * "kg.m/s^2" to "kgm/s2". If that fails, see if the unit is an SIDimensions string. If not, return null.
329      * @param abbreviation String; the abbreviation to look up
330      * @return the corresponding unit or null when it was not found
331      */
332     public U of(final String abbreviation)
333     {
334         return this.getUnitByAbbreviation(abbreviation);
335     }
336 
337     /**
338      * Retrieve a safe copy of the unitsById.
339      * @return Map&lt;String, U&gt;; a safe copy of the unitsById
340      */
341     public Map<String, U> getUnitsById()
342     {
343         return new LinkedHashMap<>(this.unitsById);
344     }
345 
346     /**
347      * Return a safe copy of the unitsByAbbreviation.
348      * @return Map&lt;String, U&gt;; a safe copy of the unitsByAbbreviation
349      */
350     public Map<String, U> getUnitsByAbbreviation()
351     {
352         return new LinkedHashMap<>(this.unitsByAbbreviation);
353     }
354 
355     /**
356      * Return a safe copy of the unitsByLocalizedAbbreviation.
357      * @return Map&lt;String, U&gt;; a safe copy of the unitsByLocalizedAbbreviation
358      */
359     public Map<String, U> getUnitsByLocalizedAbbreviation()
360     {
361         return new LinkedHashMap<>(this.unitsByLocalizedAbbreviation);
362     }
363 
364     /**
365      * Retrieve a safe copy of the localized unit abbreviations.
366      * @param unit U; the unit for which to retrieve the abbreviations
367      * @return Set&lt;String&gt;; the localized unit abbreviations
368      */
369     public Set<String> getLocalizedAbbreviations(final U unit)
370     {
371         String[] abbreviationArray = localization.getString(getName() + "." + unit.getId()).split("\\|");
372         Set<String> set = new LinkedHashSet<>();
373         for (String abb : abbreviationArray)
374         {
375             set.add(abb.strip());
376         }
377         return set;
378     }
379 
380     /**
381      * Retrieve the localized display abbreviation.
382      * @param unit U; the unit for which to retrieve the display abbreviation
383      * @return String; the localized display abbreviation
384      */
385     public String getLocalizedDisplayAbbreviation(final U unit)
386     {
387         String[] abbreviationArray = localization.getString(getName() + "." + unit.getId()).split("\\|");
388         return abbreviationArray[0].strip();
389     }
390 
391     /**
392      * Retrieve the localized textual abbreviation.
393      * @param unit U; the unit for which to retrieve the textual abbreviation
394      * @return String; the localized textual abbreviation
395      */
396     public String getLocalizedTextualAbbreviation(final U unit)
397     {
398         String[] abbreviationArray = localization.getString(getName() + "." + unit.getId()).split("\\|");
399         return (abbreviationArray.length > 1) ? abbreviationArray[1].strip() : abbreviationArray[0].strip();
400     }
401 
402     /**
403      * Retrieve the localized name of this unit.
404      * @return String; the localized name of this unit
405      */
406     public String getLocalizedName()
407     {
408         return localization.getString(getName());
409     }
410 
411     /**
412      * Retrieve the standard unit for this unit base (usually the first registered unit).
413      * @return U; the standardUnit for this unit base (usually the first registered unit)
414      */
415     public U getStandardUnit()
416     {
417         return this.standardUnit;
418     }
419 
420     @Override
421     public int hashCode()
422     {
423         // the hashCode of the standardUnit is not evaluated because of a loop to Quantity
424         // the hashCode of the unitByAbbreviation.values() is not evaluated because of a loop to Quantity
425         // the hashCode of the unitById.values() is not evaluated because of a loop to Quantity
426         final int prime = 31;
427         int result = 1;
428         result = prime * result + ((this.siDimensions == null) ? 0 : this.siDimensions.hashCode());
429         result = prime * result + ((this.standardUnit == null) ? 0 : this.standardUnit.getId().hashCode());
430         result = prime * result + ((this.unitsByAbbreviation == null) ? 0 : this.unitsByAbbreviation.keySet().hashCode());
431         result = prime * result + ((this.unitsById == null) ? 0 : this.unitsById.keySet().hashCode());
432         return result;
433     }
434 
435     @Override
436     @SuppressWarnings("checkstyle:needbraces")
437     public boolean equals(final Object obj)
438     {
439         if (this == obj)
440             return true;
441         if (obj == null)
442             return false;
443         if (getClass() != obj.getClass())
444             return false;
445         Quantity<?> other = (Quantity<?>) obj;
446         if (this.siDimensions == null)
447         {
448             if (other.siDimensions != null)
449                 return false;
450         }
451         else if (!this.siDimensions.equals(other.siDimensions))
452             return false;
453         if (this.standardUnit == null)
454         {
455             if (other.standardUnit != null)
456                 return false;
457         }
458         // the standardUnit is not compared with equals() because of a loop to Quantity
459         else if (!this.standardUnit.getId().equals(other.standardUnit.getId()))
460             return false;
461         if (this.unitsByAbbreviation == null)
462         {
463             if (other.unitsByAbbreviation != null)
464                 return false;
465         }
466         // the unitByAbbreviation is not compared with equals() because of a loop to Quantity
467         else if (!this.unitsByAbbreviation.keySet().equals(other.unitsByAbbreviation.keySet()))
468             return false;
469         if (this.unitsById == null)
470         {
471             if (other.unitsById != null)
472                 return false;
473         }
474         // the unitById is not compared with equals() because of a loop to Quantity
475         else if (!this.unitsById.keySet().equals(other.unitsById.keySet()))
476             return false;
477         return true;
478     }
479 
480     @Override
481     public String toString()
482     {
483         return "Quantity [standardUnit=" + this.standardUnit + ", name=" + this.name + ", siDimensions=" + this.siDimensions
484                 + "]";
485     }
486 
487 }