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