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