View Javadoc
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 "&lt;sup&gt;"
324      * @param powerPostfix the postfix for the power, e.g., "&lt;/sup&gt;"
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 }