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-2024 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 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 byte[]; The (currently) 9 dimensions of the SI unit we distinguish: 0: angle (rad), 1: solid angle
55       *            (sr), 2: mass (kg), 3: length (m), 4: time (s), 5: current (A), 6: temperature (K), 7: amount of substance
56       *            (mol), 8: 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 byte[]; The (currently) 9 dimensions of the SI unit we distinguish: 0: angle (rad), 1: solid angle (sr),
71       *            2: mass (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 byte[]; The (currently) 9 dimensions of the SI unit we distinguish: 0: angle (rad), 1: solid angle
74       *            (sr), 2: mass (kg), 3: length (m), 4: time (s), 5: current (A), 6: temperature (K), 7: amount of substance
75       *            (mol), 8: 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 int; dimension of the angle (rad)
92       * @param solidAngle int; dimension of the solidAngle (sr)
93       * @param mass int; dimension of the mass (kg)
94       * @param length int; dimension of the length (m)
95       * @param time int; dimension of the time (s)
96       * @param current int; dimension of the current (A)
97       * @param temperature int; dimension of the temperature (K)
98       * @param amountOfSubstance int; dimension of the amount of substance (mol)
99       * @param luminousIntensity int; 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 String; the string to parse
127      * @return SIDimension; 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 String; concatenation of SI units with positive or negative dimensions. No divisions sign is allowed.
159      * @return byte[]; 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 String[]; a safe copy of the SI abbreviations
236      */
237     public String[] siAbbreviations()
238     {
239         return SI_ABBREVIATIONS.clone();
240     }
241 
242     /**
243      * Add a set of SI dimensions to this SIDimensions. Note: as dimensions are considered to be immutable, a new dimension is
244      * returned. The original dimension (<code>this</code>) remains unaltered.
245      * @param other SIDimensions; the dimensions to add (usually as a result of multiplication of scalars)
246      * @return SIDimensions; the new dimensions with the dimensions of this object plus the dimensions in the parameter
247      */
248     public SIDimensions plus(final SIDimensions other)
249     {
250         byte[] result = new byte[NUMBER_DIMENSIONS];
251         for (int i = 0; i < NUMBER_DIMENSIONS; i++)
252         {
253             result[i] = (byte) (this.dimensions[i] + other.dimensions[i]);
254         }
255         return new SIDimensions(result);
256     }
257 
258     /**
259      * Subtract a set of SI dimensions from this SIDimensions. Note: as dimensions are considered to be immutable, a new
260      * dimension is returned. The original dimension (<code>this</code>) remains unaltered.
261      * @param other SIDimensions; the dimensions to subtract (usually as a result of division of scalars)
262      * @return SIDimensions; the new dimensions with the dimensions of this object minus the dimensions in the parameter
263      */
264     public SIDimensions minus(final SIDimensions other)
265     {
266         byte[] result = new byte[NUMBER_DIMENSIONS];
267         for (int i = 0; i < NUMBER_DIMENSIONS; i++)
268         {
269             result[i] = (byte) (this.dimensions[i] - other.dimensions[i]);
270         }
271         return new SIDimensions(result);
272     }
273 
274     /**
275      * Invert a set of SI dimensions; instead of m/s we get s/m. Note: as dimensions are considered to be immutable, a new
276      * dimension is returned. The original dimension (<code>this</code>) remains unaltered.
277      * @return SIDimensions; the new dimensions that are the inverse of the dimensions in this object
278      */
279     public SIDimensions invert()
280     {
281         byte[] result = new byte[NUMBER_DIMENSIONS];
282         for (int i = 0; i < NUMBER_DIMENSIONS; i++)
283         {
284             result[i] = (byte) (-this.dimensions[i]);
285         }
286         return new SIDimensions(result);
287     }
288 
289     /**
290      * Add two SIDimensions and return the new SIDimensions. Usually, dimensions are added as a result of multiplication of
291      * scalars.
292      * @param dim1 SIDimensions; the first set of dimensions
293      * @param dim2 SIDimensions; the second set of dimensions
294      * @return the new dimensions with the sum of the dimensions in the parameters
295      */
296     public static SIDimensions add(final SIDimensions dim1, final SIDimensions dim2)
297     {
298         byte[] dim = new byte[NUMBER_DIMENSIONS];
299         for (int i = 0; i < NUMBER_DIMENSIONS; i++)
300         {
301             dim[i] = (byte) (dim1.dimensions[i] + dim2.dimensions[i]);
302         }
303         return new SIDimensions(dim);
304     }
305 
306     /**
307      * Subtract an SIDimensions (dim2) from another SIDimensions (dim1) and return the new SIDimensions. Usually, dimensions are
308      * added as a result of division of scalars.
309      * @param dim1 SIDimensions; the first set of dimensions
310      * @param dim2 SIDimensions; the second set of dimensions that will be subtracted from dim1
311      * @return the new dimensions with the difference of the dimensions in the parameters
312      */
313     public static SIDimensions subtract(final SIDimensions dim1, final SIDimensions dim2)
314     {
315         byte[] dim = new byte[NUMBER_DIMENSIONS];
316         for (int i = 0; i < NUMBER_DIMENSIONS; i++)
317         {
318             dim[i] = (byte) (dim1.dimensions[i] - dim2.dimensions[i]);
319         }
320         return new SIDimensions(dim);
321     }
322 
323     /**
324      * Indicate whether this SIDImensions contains one or more fractional dimensions.
325      * @return boolean; whether this SIDImensions contains one or more fractional dimensions
326      */
327     public boolean isFractional()
328     {
329         return this.fractional;
330     }
331 
332     /** {@inheritDoc} */
333     @Override
334     public int hashCode()
335     {
336         final int prime = 31;
337         int result = 1;
338         result = prime * result + Arrays.hashCode(this.denominator);
339         result = prime * result + Arrays.hashCode(this.dimensions);
340         return result;
341     }
342 
343     /** {@inheritDoc} */
344     @Override
345     @SuppressWarnings("checkstyle:needbraces")
346     public boolean equals(final Object obj)
347     {
348         if (this == obj)
349             return true;
350         if (obj == null)
351             return false;
352         if (getClass() != obj.getClass())
353             return false;
354         SIDimensions other = (SIDimensions) obj;
355         if (!Arrays.equals(this.denominator, other.denominator))
356             return false;
357         if (!Arrays.equals(this.dimensions, other.dimensions))
358             return false;
359         return true;
360     }
361 
362     /**
363      * Return a string such as "kgm/s2" or "kg.m/s^2" or "kg.m.s^-2" from this SIDimensions.
364      * @param divided boolean; if true, return m/s2 for acceleration; if false return ms-2
365      * @param separator String; add this string between successive units, e.g. kg.m.s-2 instead of kgms-2
366      * @param powerPrefix String; the prefix for the power, e.g., "^" or "&lt;sup&gt;"
367      * @param powerPostfix String; the postfix for the power, e.g., "&lt;/sup&gt;"
368      * @return String; a formatted string for this SIDimensions
369      */
370     public String toString(final boolean divided, final String separator, final String powerPrefix, final String powerPostfix)
371     {
372         StringBuffer s = new StringBuffer();
373         boolean first = true;
374         boolean negative = false;
375         for (int i = 0; i < NUMBER_DIMENSIONS; i++)
376         {
377             if (this.dimensions[i] < 0)
378             {
379                 negative = true;
380             }
381             if ((!divided && this.dimensions[i] != 0) || (divided && this.dimensions[i] > 0))
382             {
383                 if (!first)
384                 {
385                     s.append(separator);
386                 }
387                 else
388                 {
389                     first = false;
390                 }
391                 s.append(SI_ABBREVIATIONS[i]);
392                 if (this.dimensions[i] != 1)
393                 {
394                     s.append(powerPrefix);
395                     s.append(this.dimensions[i]);
396                     s.append(powerPostfix);
397                 }
398             }
399         }
400         if (s.length() == 0)
401         {
402             s.append("1");
403         }
404         if (divided && negative)
405         {
406             s.append("/");
407         }
408         if (divided)
409         {
410             first = true;
411             for (int i = 0; i < NUMBER_DIMENSIONS; i++)
412             {
413                 if (this.dimensions[i] < 0)
414                 {
415                     if (!first)
416                     {
417                         s.append(separator);
418                     }
419                     else
420                     {
421                         first = false;
422                     }
423                     s.append(SI_ABBREVIATIONS[i]);
424                     if (this.dimensions[i] < -1)
425                     {
426                         s.append(powerPrefix);
427                         s.append(-this.dimensions[i]);
428                         s.append(powerPostfix);
429                     }
430                 }
431             }
432         }
433         if (s.toString().equals("1"))
434         {
435             return "";
436         }
437         return s.toString();
438     }
439 
440     /**
441      * Return a string such as "kgm/s2" or "kg.m/s2" or "kg.m.s-2" from this SIDimensions.
442      * @param divided boolean; if true, return m/s2 for acceleration; if false return ms-2
443      * @param separator boolean; if true, add a period between successive units, e.g. kg.m.s-2 instead of kgms-2
444      * @return String; a formatted string describing this SIDimensions
445      */
446     public String toString(final boolean divided, final boolean separator)
447     {
448         return toString(divided, separator ? "." : "", "", "");
449     }
450 
451     /**
452      * Return a string such as "kgm/s2" or "kg.m/s^2" or "kg.m.s^-2" from this SIDimensions.
453      * @param divided boolean; if true, return m/s2 for acceleration; if false return ms-2
454      * @param separator boolean; if true, add a period between successive units, e.g. kg.m.s-2 instead of kgms-2
455      * @param power boolean; if true, add a ^ sign before the power, e.g., "kg.m^2/s^3" instead of "kg.m2/s3"
456      * @return String; a formatted string describing this SIDimensions
457      */
458     public String toString(final boolean divided, final boolean separator, final boolean power)
459     {
460         return toString(divided, separator ? "." : "", power ? "^" : "", "");
461     }
462 
463     /**
464      * Return a string such as "kgm/s<sup>2</sup>" or or "kg.m.s<sup>-2</sup>" from this SIDimensions.
465      * @param divided boolean; if true, return "m/s<sup>2</sup>" for acceleration; if false return "ms<sup>-2</sup>"
466      * @param separator boolean; if true, add a period between successive units, e.g. kg.m.s<sup>-2</sup>
467      * @return String; a formatted string describing this SIDimensions
468      */
469     public String toHTMLString(final boolean divided, final boolean separator)
470     {
471         return toString(divided, separator ? "." : "", "<sup>", "</sup>");
472     }
473 
474     /** {@inheritDoc} */
475     @Override
476     public String toString()
477     {
478         if (this.fractional)
479         {
480             StringBuffer sb = new StringBuffer();
481             sb.append("[");
482             for (int i = 0; i < NUMBER_DIMENSIONS; i++)
483             {
484                 if (i > 0)
485                 {
486                     sb.append(", ");
487                 }
488                 if (this.denominator[i] != 1 && this.dimensions[i] != 0)
489                 {
490                     sb.append(this.dimensions[i] + "/" + this.denominator[i]);
491                 }
492                 else
493                 {
494                     sb.append(this.dimensions[i]);
495                 }
496             }
497             sb.append("]");
498             return sb.toString();
499         }
500         else
501         {
502             return Arrays.toString(this.dimensions);
503         }
504     }
505 
506 }