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-2025 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 the quantity name (CamelCase)
64 * @param 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 the quantity name (CamelCase)
79 * @param siString 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 the quantity name (CamelCase)
102 * @param siSignature 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 quetta to quecto, 24 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 24 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 the unit to register in the map.
118 * @param siPrefixes indicates whether and which SI prefixes should be generated.
119 * @param siPrefixPower 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 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 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 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 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 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 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 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 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 the unit for which to retrieve the abbreviations
367 * @return 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 the unit for which to retrieve the display abbreviation
383 * @return 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 the unit for which to retrieve the textual abbreviation
394 * @return 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 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 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 }