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 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 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     @Override
333     public int hashCode()
334     {
335         final int prime = 31;
336         int result = 1;
337         result = prime * result + Arrays.hashCode(this.denominator);
338         result = prime * result + Arrays.hashCode(this.dimensions);
339         return result;
340     }
341 
342     @Override
343     @SuppressWarnings("checkstyle:needbraces")
344     public boolean equals(final Object obj)
345     {
346         if (this == obj)
347             return true;
348         if (obj == null)
349             return false;
350         if (getClass() != obj.getClass())
351             return false;
352         SIDimensions other = (SIDimensions) obj;
353         if (!Arrays.equals(this.denominator, other.denominator))
354             return false;
355         if (!Arrays.equals(this.dimensions, other.dimensions))
356             return false;
357         return true;
358     }
359 
360     /**
361      * Return a string such as "kgm/s2" or "kg.m/s^2" or "kg.m.s^-2" from this SIDimensions.
362      * @param divided boolean; if true, return m/s2 for acceleration; if false return ms-2
363      * @param separator String; add this string between successive units, e.g. kg.m.s-2 instead of kgms-2
364      * @param powerPrefix String; the prefix for the power, e.g., "^" or "&lt;sup&gt;"
365      * @param powerPostfix String; the postfix for the power, e.g., "&lt;/sup&gt;"
366      * @return String; a formatted string for this SIDimensions
367      */
368     public String toString(final boolean divided, final String separator, final String powerPrefix, final String powerPostfix)
369     {
370         StringBuffer s = new StringBuffer();
371         boolean first = true;
372         boolean negative = false;
373         for (int i = 0; i < NUMBER_DIMENSIONS; i++)
374         {
375             if (this.dimensions[i] < 0)
376             {
377                 negative = true;
378             }
379             if ((!divided && this.dimensions[i] != 0) || (divided && this.dimensions[i] > 0))
380             {
381                 if (!first)
382                 {
383                     s.append(separator);
384                 }
385                 else
386                 {
387                     first = false;
388                 }
389                 s.append(SI_ABBREVIATIONS[i]);
390                 if (this.dimensions[i] != 1)
391                 {
392                     s.append(powerPrefix);
393                     s.append(this.dimensions[i]);
394                     s.append(powerPostfix);
395                 }
396             }
397         }
398         if (s.length() == 0)
399         {
400             s.append("1");
401         }
402         if (divided && negative)
403         {
404             s.append("/");
405         }
406         if (divided)
407         {
408             first = true;
409             for (int i = 0; i < NUMBER_DIMENSIONS; i++)
410             {
411                 if (this.dimensions[i] < 0)
412                 {
413                     if (!first)
414                     {
415                         s.append(separator);
416                     }
417                     else
418                     {
419                         first = false;
420                     }
421                     s.append(SI_ABBREVIATIONS[i]);
422                     if (this.dimensions[i] < -1)
423                     {
424                         s.append(powerPrefix);
425                         s.append(-this.dimensions[i]);
426                         s.append(powerPostfix);
427                     }
428                 }
429             }
430         }
431         if (s.toString().equals("1"))
432         {
433             return "";
434         }
435         return s.toString();
436     }
437 
438     /**
439      * Return a string such as "kgm/s2" or "kg.m/s2" or "kg.m.s-2" from this SIDimensions.
440      * @param divided boolean; if true, return m/s2 for acceleration; if false return ms-2
441      * @param separator boolean; if true, add a period between successive units, e.g. kg.m.s-2 instead of kgms-2
442      * @return String; a formatted string describing this SIDimensions
443      */
444     public String toString(final boolean divided, final boolean separator)
445     {
446         return toString(divided, separator ? "." : "", "", "");
447     }
448 
449     /**
450      * Return a string such as "kgm/s2" or "kg.m/s^2" or "kg.m.s^-2" from this SIDimensions.
451      * @param divided boolean; if true, return m/s2 for acceleration; if false return ms-2
452      * @param separator boolean; if true, add a period between successive units, e.g. kg.m.s-2 instead of kgms-2
453      * @param power boolean; if true, add a ^ sign before the power, e.g., "kg.m^2/s^3" instead of "kg.m2/s3"
454      * @return String; a formatted string describing this SIDimensions
455      */
456     public String toString(final boolean divided, final boolean separator, final boolean power)
457     {
458         return toString(divided, separator ? "." : "", power ? "^" : "", "");
459     }
460 
461     /**
462      * Return a string such as "kgm/s<sup>2</sup>" or or "kg.m.s<sup>-2</sup>" from this SIDimensions.
463      * @param divided boolean; if true, return "m/s<sup>2</sup>" for acceleration; if false return "ms<sup>-2</sup>"
464      * @param separator boolean; if true, add a period between successive units, e.g. kg.m.s<sup>-2</sup>
465      * @return String; a formatted string describing this SIDimensions
466      */
467     public String toHTMLString(final boolean divided, final boolean separator)
468     {
469         return toString(divided, separator ? "." : "", "<sup>", "</sup>");
470     }
471 
472     @Override
473     public String toString()
474     {
475         if (this.fractional)
476         {
477             StringBuffer sb = new StringBuffer();
478             sb.append("[");
479             for (int i = 0; i < NUMBER_DIMENSIONS; i++)
480             {
481                 if (i > 0)
482                 {
483                     sb.append(", ");
484                 }
485                 if (this.denominator[i] != 1 && this.dimensions[i] != 0)
486                 {
487                     sb.append(this.dimensions[i] + "/" + this.denominator[i]);
488                 }
489                 else
490                 {
491                     sb.append(this.dimensions[i]);
492                 }
493             }
494             sb.append("]");
495             return sb.toString();
496         }
497         else
498         {
499             return Arrays.toString(this.dimensions);
500         }
501     }
502 
503 }