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