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