1 package org.djunits.unit.si;
2
3 import java.util.Arrays;
4
5 import org.djunits.quantity.SIQuantity;
6 import org.djunits.unit.UnitInterface;
7 import org.djunits.unit.UnitRuntimeException;
8 import org.djunits.unit.scale.IdentityScale;
9 import org.djunits.unit.scale.Scale;
10 import org.djunits.unit.system.UnitSystem;
11 import org.djutils.exceptions.Throw;
12
13 /**
14 * SIUnit stores the dimensionality of a unit using the SI standards. Angle (rad) and solid angle (sr) have been added to be
15 * able to specify often used units regarding rotation.
16 * <p>
17 * Copyright (c) 2019-2026 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
18 * BSD-style license. See <a href="https://djunits.org/docs/license.html">DJUNITS License</a>
19 * @author Alexander Verbraeck
20 */
21 public class SIUnit implements UnitInterface<SIUnit, SIQuantity>
22 {
23 /** The (currently) 9 dimensions we take into account: rad, sr, kg, m, s, A, K, mol, cd. */
24 public static final int NUMBER_DIMENSIONS = 9;
25
26 /** The abbreviations of the SI units we use in SIUnit. */
27 private static final String[] SI_ABBREVIATIONS = new String[] {"rad", "sr", "kg", "m", "s", "A", "K", "mol", "cd"};
28
29 /** For parsing, the mol has to be parsed before the m, otherwise the "m" from "mol" is eaten; same for "s" and "sr". */
30 private static final int[] PARSE_ORDER = new int[] {0, 1, 2, 7, 3, 4, 5, 6, 8};
31
32 /** the dimensionless SIUnit. */
33 public static final SIUnit DIMLESS = new SIUnit(0, 0, 0, 0, 0, 0, 0, 0, 0);
34
35 /**
36 * The (currently) 9 dimensions of the SI unit we distinguish: 0: angle (rad), 1: solid angle (sr), 2: mass (kg), 3: length
37 * (m), 4: time (s), 5: current (A), 6: temperature (K), 7: amount of substance (mol), 8: luminous intensity (cd). As an
38 * example, speed is indicated as length = 1; time = -1.
39 */
40 private final int[] dimensions;
41
42 /**
43 * Create an immutable SIUnit instance based on a safe copy of a given dimensions specification. As an example, speed is
44 * indicated as length = 1; time = -1 with the other dimensions equal to zero.
45 * @param dimensions The (currently) 9 dimensions of the SI unit we distinguish: 0: angle (rad), 1: solid angle (sr), 2:
46 * mass (kg), 3: length (m), 4: time (s), 5: current (A), 6: temperature (K), 7: amount of substance (mol), 8:
47 * luminous intensity (cd).
48 */
49 public SIUnit(final int[] dimensions)
50 {
51 Throw.whenNull(dimensions, "dimensions cannot be null");
52 Throw.when(dimensions.length != NUMBER_DIMENSIONS, IllegalArgumentException.class,
53 "SIUnit has the wrong dimensionality: %s instead of %s", dimensions.length, NUMBER_DIMENSIONS);
54 this.dimensions = dimensions.clone(); // safe copy
55 }
56
57 /**
58 * Create an immutable SIUnit instance based on a safe copy of a given dimensions specification.
59 * @param angle dimension of the angle (rad)
60 * @param solidAngle dimension of the solidAngle (sr)
61 * @param mass dimension of the mass (kg)
62 * @param length dimension of the length (m)
63 * @param time dimension of the time (s)
64 * @param current dimension of the current (A)
65 * @param temperature dimension of the temperature (K)
66 * @param amountOfSubstance dimension of the amount of substance (mol)
67 * @param luminousIntensity dimension of the luminous intensity (cd)
68 */
69 @SuppressWarnings("checkstyle:parameternumber")
70 public SIUnit(final int angle, final int solidAngle, final int mass, final int length, final int time, final int current,
71 final int temperature, final int amountOfSubstance, final int luminousIntensity)
72 {
73 this.dimensions = new int[NUMBER_DIMENSIONS];
74 this.dimensions[0] = angle;
75 this.dimensions[1] = solidAngle;
76 this.dimensions[2] = mass;
77 this.dimensions[3] = length;
78 this.dimensions[4] = time;
79 this.dimensions[5] = current;
80 this.dimensions[6] = temperature;
81 this.dimensions[7] = amountOfSubstance;
82 this.dimensions[8] = luminousIntensity;
83 }
84
85 /**
86 * Parse a string representing SI dimensions to an SIUnit object. Example: SIUnit.of("kgm/s2") and SIUnit.of("kgms-2") will
87 * both be translated to a dimensions object with vector {0,0,1,1,-2,0,0,0,0}. It is allowed to use 0 or 1 for the
88 * dimensions. Having the same unit in the numerator and the denominator is not seen as a problem: the values are subtracted
89 * from each other, so m/m will have a length dimensionality of 0. Dimensions between -9 and 9 are allowed. Spaces, periods
90 * and ^ are taken out, but other characters are not allowed and will lead to a UnitException. The order of allowed units is
91 * arbitrary, so "kg/ms2" is accepted as well as "kg/s^2.m".
92 * @param siString the string to parse
93 * @return the corresponding SI dimensions
94 * @throws UnitRuntimeException when the string could not be parsed into dimensions
95 */
96 public static SIUnit of(final String siString) throws UnitRuntimeException
97 {
98 Throw.whenNull(siString, "siString cannot be null");
99 String dimString = siString.replaceAll("[ .^]", "");
100 if (dimString.contains("/"))
101 {
102 String[] parts = dimString.split("\\/");
103 if (parts.length != 2)
104 {
105 throw new UnitRuntimeException("SI String " + dimString + " contains more than one division sign");
106 }
107 int[] numerator = parse(parts[0]);
108 int[] denominator = parse(parts[1]);
109 for (int i = 0; i < NUMBER_DIMENSIONS; i++)
110 {
111 numerator[i] -= denominator[i];
112 }
113 return new SIUnit(numerator);
114 }
115 return new SIUnit(parse(dimString));
116 }
117
118 /**
119 * Translate a string representing SI dimensions to an SIUnit object. Example: SIUnit.of("kgm2") is translated to a vector
120 * {0,0,1,2,0,0,0,0,0}. It is allowed to use 0 or 1 for the dimensions. Dimensions between -9 and 9 are allowed. The parsing
121 * is quite lenient: periods and carets (^) are taken out, and the order can be arbitrary, so "kgms-2" is accepted as well
122 * as "m.s^-2.kg". Note that the empty string parses to the dimensionless unit.
123 * @param siString concatenation of SI units with positive or negative dimensions. No divisions sign is allowed.
124 * @return a vector of length <code>NUMBER_DIMENSIONS</code> with the dimensions for the SI units
125 * @throws UnitRuntimeException when the String cannot be parsed, e.g. due to units not being recognized
126 */
127 private static int[] parse(final String siString) throws UnitRuntimeException
128 {
129 Throw.whenNull(siString, "siString cannot be null");
130 int[] result = new int[NUMBER_DIMENSIONS];
131 if (siString.equals("1") || siString.length() == 0)
132 {
133 return result;
134 }
135 String copy = siString;
136 int copyLength = copy.length();
137 while (copyLength > 0)
138 {
139 // find the next unit
140 for (int j = 0; j < SI_ABBREVIATIONS.length; j++)
141 {
142 int i = PARSE_ORDER[j];
143 String si = SI_ABBREVIATIONS[i];
144 if (copy.startsWith(si))
145 {
146 if (result[i] != 0)
147 {
148 throw new UnitRuntimeException("SI string " + siString + " has a double entry for unit " + si);
149 }
150 copy = copy.substring(si.length());
151 if (copy.length() == 0)
152 {
153 result[i] = 1;
154 break;
155 }
156 else if (copy.startsWith("-"))
157 {
158 if (copy.length() == 1)
159 {
160 throw new UnitRuntimeException("SI string " + siString + " ends with a minus sign");
161 }
162 if (Character.isDigit(copy.charAt(1)))
163 {
164 result[i] = (-1 * (copy.charAt(1) - '0'));
165 copy = copy.substring(2);
166 break;
167 }
168 throw new UnitRuntimeException(
169 "SI string " + siString + " has a minus sign for unit " + si + " but no dimension");
170 }
171 else if (Character.isDigit(copy.charAt(0)))
172 {
173 result[i] = (copy.charAt(0) - '0');
174 copy = copy.substring(1);
175 break;
176 }
177 else
178 {
179 result[i] = 1;
180 break;
181 }
182 }
183 }
184 if (copy.length() == copyLength)
185 {
186 // we did not parse anything... wrong character
187 break;
188 }
189 copyLength = copy.length();
190 }
191 if (copy.length() != 0)
192 {
193 throw new UnitRuntimeException("Trailing information in SI string " + siString);
194 }
195 return result;
196 }
197
198 /**
199 * Returns a safe copy of the SI abbreviations (a public static final String[] is mutable).
200 * @return a safe copy of the SI abbreviations
201 */
202 public String[] siAbbreviations()
203 {
204 return SI_ABBREVIATIONS.clone();
205 }
206
207 /**
208 * Return a safe copy of the exponents of the SI dimensions in the order rad, sr, kg, m, s, A, K, mol, cd. Since it is a
209 * safe copy, calculations can be carried out on the int[] return value.
210 * @return a safe copy of the exponents of the SI dimensions in the order rad, sr, kg, m, s, A, K, mol, cd
211 */
212 public int[] siDimensions()
213 {
214 return this.dimensions.clone();
215 }
216
217 /**
218 * Add a set of SI dimensions to this SIUnit. Note: as dimensions are considered to be immutable, a new dimension is
219 * returned. The original dimension (<code>this</code>) remains unaltered.
220 * @param other the dimensions to add (usually as a result of multiplication of scalars)
221 * @return the new dimensions with the dimensions of this object plus the dimensions in the parameter
222 */
223 public SIUnit plus(final SIUnit other)
224 {
225 int[] result = new int[NUMBER_DIMENSIONS];
226 for (int i = 0; i < NUMBER_DIMENSIONS; i++)
227 {
228 result[i] = this.dimensions[i] + other.dimensions[i];
229 }
230 return new SIUnit(result);
231 }
232
233 /**
234 * Subtract a set of SI dimensions from this SIUnit. Note: as dimensions are considered to be immutable, a new dimension is
235 * returned. The original dimension (<code>this</code>) remains unaltered.
236 * @param other the dimensions to subtract (usually as a result of division of scalars)
237 * @return the new dimensions with the dimensions of this object minus the dimensions in the parameter
238 */
239 public SIUnit minus(final SIUnit other)
240 {
241 int[] result = new int[NUMBER_DIMENSIONS];
242 for (int i = 0; i < NUMBER_DIMENSIONS; i++)
243 {
244 result[i] = this.dimensions[i] - other.dimensions[i];
245 }
246 return new SIUnit(result);
247 }
248
249 /**
250 * Invert a set of SI dimensions; instead of m/s we get s/m. Note: as dimensions are considered to be immutable, a new
251 * dimension is returned. The original dimension (<code>this</code>) remains unaltered.
252 * @return the new dimensions that are the inverse of the dimensions in this object
253 */
254 public SIUnit invert()
255 {
256 int[] result = new int[NUMBER_DIMENSIONS];
257 for (int i = 0; i < NUMBER_DIMENSIONS; i++)
258 {
259 result[i] = -this.dimensions[i];
260 }
261 return new SIUnit(result);
262 }
263
264 /**
265 * Raise a set of SI dimensions to the n-th power. Note: as dimensions are considered to be immutable, a new dimension is
266 * returned. The original dimension (<code>this</code>) remains unaltered.
267 * @param n the power to which to raise this set of dimensions
268 * @return the new dimensions with the dimensions of this object raised to the n-th power
269 */
270 public SIUnit pow(final int n)
271 {
272 int[] result = new int[NUMBER_DIMENSIONS];
273 for (int i = 0; i < NUMBER_DIMENSIONS; i++)
274 {
275 result[i] = this.dimensions[i] * n;
276 }
277 return new SIUnit(result);
278 }
279
280 /**
281 * Add two SIUnit and return the new SIUnit. Usually, dimensions are added as a result of multiplication of scalars.
282 * @param dim1 the first set of dimensions
283 * @param dim2 the second set of dimensions
284 * @return the new dimensions with the sum of the dimensions in the parameters
285 */
286 public static SIUnit add(final SIUnit dim1, final SIUnit dim2)
287 {
288 int[] dim = new int[NUMBER_DIMENSIONS];
289 for (int i = 0; i < NUMBER_DIMENSIONS; i++)
290 {
291 dim[i] = dim1.dimensions[i] + dim2.dimensions[i];
292 }
293 return new SIUnit(dim);
294 }
295
296 /**
297 * Subtract an SIUnit (dim2) from another SIUnit (dim1) and return the new SIUnit. Usually, dimensions are added as a result
298 * of division of scalars.
299 * @param dim1 the first set of dimensions
300 * @param dim2 the second set of dimensions that will be subtracted from dim1
301 * @return the new dimensions with the difference of the dimensions in the parameters
302 */
303 public static SIUnit subtract(final SIUnit dim1, final SIUnit dim2)
304 {
305 int[] dim = new int[NUMBER_DIMENSIONS];
306 for (int i = 0; i < NUMBER_DIMENSIONS; i++)
307 {
308 dim[i] = dim1.dimensions[i] - dim2.dimensions[i];
309 }
310 return new SIUnit(dim);
311 }
312
313 @Override
314 public SIQuantity ofSi(final double si)
315 {
316 return new SIQuantity(si, this);
317 }
318
319 /**
320 * Return a string such as "kgm/s2" or "kg.m/s^2" or "kg.m.s^-2" from this SIUnit.
321 * @param divided if true, return m/s2 for acceleration; if false return ms-2
322 * @param separator add this string between successive units, e.g. kg.m.s-2 instead of kgms-2
323 * @param powerPrefix the prefix for the power, e.g., "^" or "<sup>"
324 * @param powerPostfix the postfix for the power, e.g., "</sup>"
325 * @return a formatted string for this SIUnit
326 */
327 public String toString(final boolean divided, final String separator, final String powerPrefix, final String powerPostfix)
328 {
329 StringBuffer s = new StringBuffer();
330 boolean first = true;
331 boolean negative = false;
332 for (int i = 0; i < NUMBER_DIMENSIONS; i++)
333 {
334 if (this.dimensions[i] < 0)
335 {
336 negative = true;
337 }
338 if ((!divided && this.dimensions[i] != 0) || (divided && this.dimensions[i] > 0))
339 {
340 if (!first)
341 {
342 s.append(separator);
343 }
344 else
345 {
346 first = false;
347 }
348 s.append(SI_ABBREVIATIONS[i]);
349 if (this.dimensions[i] != 1)
350 {
351 s.append(powerPrefix);
352 s.append(this.dimensions[i]);
353 s.append(powerPostfix);
354 }
355 }
356 }
357 if (s.length() == 0)
358 {
359 s.append("1");
360 }
361 if (divided && negative)
362 {
363 s.append("/");
364 }
365 if (divided)
366 {
367 first = true;
368 for (int i = 0; i < NUMBER_DIMENSIONS; i++)
369 {
370 if (this.dimensions[i] < 0)
371 {
372 if (!first)
373 {
374 s.append(separator);
375 }
376 else
377 {
378 first = false;
379 }
380 s.append(SI_ABBREVIATIONS[i]);
381 if (this.dimensions[i] < -1)
382 {
383 s.append(powerPrefix);
384 s.append(-this.dimensions[i]);
385 s.append(powerPostfix);
386 }
387 }
388 }
389 }
390 if (s.toString().equals("1"))
391 {
392 return "";
393 }
394 return s.toString();
395 }
396
397 /**
398 * Return a string such as "kgm/s2" or "kg.m/s2" or "kg.m.s-2" from this SIUnit.
399 * @param divided if true, return m/s2 for acceleration; if false return ms-2
400 * @param separator if true, add a period between successive units, e.g. kg.m.s-2 instead of kgms-2
401 * @return a formatted string describing this SIUnit
402 */
403 public String toString(final boolean divided, final boolean separator)
404 {
405 return toString(divided, separator ? "." : "", "", "");
406 }
407
408 /**
409 * Return a string such as "kgm/s2" or "kg.m/s^2" or "kg.m.s^-2" from this SIUnit.
410 * @param divided if true, return m/s2 for acceleration; if false return ms-2
411 * @param separator if true, add a period between successive units, e.g. kg.m.s-2 instead of kgms-2
412 * @param power if true, add a ^ sign before the power, e.g., "kg.m^2/s^3" instead of "kg.m2/s3"
413 * @return a formatted string describing this SIUnit
414 */
415 public String toString(final boolean divided, final boolean separator, final boolean power)
416 {
417 return toString(divided, separator ? "." : "", power ? "^" : "", "");
418 }
419
420 /**
421 * Return a string such as "kgm/s<sup>2</sup>" or or "kg.m.s<sup>-2</sup>" from this SIUnit.
422 * @param divided if true, return "m/s<sup>2</sup>" for acceleration; if false return "ms<sup>-2</sup>"
423 * @param separator if true, add a period between successive units, e.g. kg.m.s<sup>-2</sup>
424 * @return a formatted string describing this SIUnit
425 */
426 public String toHTMLString(final boolean divided, final boolean separator)
427 {
428 return toString(divided, separator ? "." : "", "<sup>", "</sup>");
429 }
430
431 @Override
432 public String toString()
433 {
434 return Arrays.toString(this.dimensions);
435 }
436
437 @Override
438 public String getId()
439 {
440 return toString(true, false);
441 }
442
443 @Override
444 public Scale getScale()
445 {
446 return IdentityScale.SCALE;
447 }
448
449 @Override
450 public UnitSystem getUnitSystem()
451 {
452 return UnitSystem.SI_BASE;
453 }
454
455 @Override
456 public SIUnit siUnit()
457 {
458 return this;
459 }
460
461 @Override
462 public SIUnit getBaseUnit()
463 {
464 return this;
465 }
466
467 @Override
468 public String getTextualAbbreviation()
469 {
470 return toString(true, false);
471 }
472
473 @Override
474 public String getDisplayAbbreviation()
475 {
476 return toString(true, false);
477 }
478
479 @Override
480 public String getName()
481 {
482 return toString(true, false);
483 }
484
485 @Override
486 public String getStoredTextualAbbreviation()
487 {
488 return toString(true, false);
489 }
490
491 @Override
492 public String getStoredDisplayAbbreviation()
493 {
494 return toString(true, false);
495 }
496
497 @Override
498 public String getStoredName()
499 {
500 return toString(true, false);
501 }
502
503 @Override
504 public SIUnit setSiPrefix(final SIPrefix siPrefix)
505 {
506 return this;
507 }
508
509 @Override
510 public SIUnit setSiPrefix(final String prefix)
511 {
512 return this;
513 }
514
515 @Override
516 public SIUnit setSiPrefixKilo(final String prefix)
517 {
518 return this;
519 }
520
521 @Override
522 public SIUnit setSiPrefixPer(final String prefix)
523 {
524 return this;
525 }
526
527 @Override
528 public SIPrefix getSiPrefix()
529 {
530 return null;
531 }
532
533 @Override
534 public int hashCode()
535 {
536 final int prime = 31;
537 int result = 1;
538 result = prime * result + Arrays.hashCode(this.dimensions);
539 return result;
540 }
541
542 @Override
543 @SuppressWarnings("checkstyle:needbraces")
544 public boolean equals(final Object obj)
545 {
546 if (this == obj)
547 return true;
548 if (obj == null)
549 return false;
550 if (getClass() != obj.getClass())
551 return false;
552 SIUnit other = (SIUnit) obj;
553 if (!Arrays.equals(this.dimensions, other.dimensions))
554 return false;
555 return true;
556 }
557
558 }