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