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-2022 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<String, U>; 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<String, U>; 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 }