1   /* Copyright 2002-2022 CS GROUP
2    * Licensed to CS GROUP (CS) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * CS licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *   http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.orekit.utils.units;
18  
19  import java.io.Serializable;
20  import java.util.List;
21  
22  import org.hipparchus.fraction.Fraction;
23  import org.hipparchus.util.FastMath;
24  import org.hipparchus.util.Precision;
25  import org.orekit.errors.OrekitException;
26  import org.orekit.errors.OrekitMessages;
27  
28  /** Basic handling of multiplicative units.
29   * <p>
30   * This class is by no means a complete handling of units. For complete
31   * support, look at libraries like {@code UOM}. This class handles only
32   * time, length, mass and current dimensions, as well as angles (which are
33   * dimensionless).
34   * </p>
35   * <p>
36   * Instances of this class are immutable.
37   * </p>
38   * @see <a href="https://github.com/netomi/uom">UOM</a>
39   * @author Luc Maisonobe
40   * @since 11.0
41   */
42  public class Unit implements Serializable {
43  
44      /** No unit. */
45      public static final Unit NONE = new Unit("n/a", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO);
46  
47      /** Dimensionless unit. */
48      public static final Unit ONE = new Unit("1", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO);
49  
50      /** Percentage unit. */
51      public static final Unit PERCENT = new Unit("%", 1.0e-2, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO);
52  
53      /** Second unit. */
54      public static final Unit SECOND = new Unit("s", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ONE, Fraction.ZERO, Fraction.ZERO);
55  
56      /** Minute unit. */
57      public static final Unit MINUTE = SECOND.scale("min", 60.0);
58  
59      /** Hour unit. */
60      public static final Unit HOUR = MINUTE.scale("h", 60);
61  
62      /** Day unit. */
63      public static final Unit DAY = HOUR.scale("d", 24.0);
64  
65      /** Julian year unit.
66       * @see <a href="https://www.iau.org/publications/proceedings_rules/units/">SI Units at IAU</a>
67       */
68      public static final Unit YEAR = DAY.scale("a", 365.25);
69  
70      /** Hertz unit. */
71      public static final Unit HERTZ = SECOND.power("Hz", Fraction.MINUS_ONE);
72  
73      /** Metre unit. */
74      public static final Unit METRE = new Unit("m", 1.0, Fraction.ZERO, Fraction.ONE, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO);
75  
76      /** Kilometre unit. */
77      public static final Unit KILOMETRE = METRE.scale("km", 1000.0);
78  
79      /** Kilogram unit. */
80      public static final Unit KILOGRAM = new Unit("kg", 1.0, Fraction.ONE, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO);
81  
82      /** Gram unit. */
83      public static final Unit GRAM = KILOGRAM.scale("g", 1.0e-3);
84  
85      /** Ampere unit. */
86      public static final Unit AMPERE = new Unit("A", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ONE, Fraction.ZERO);
87  
88      /** Radian unit. */
89      public static final Unit RADIAN = new Unit("rad", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ONE);
90  
91      /** Degree unit. */
92      public static final Unit DEGREE = RADIAN.scale("°", FastMath.toRadians(1.0));
93  
94      /** Arc minute unit. */
95      public static final Unit ARC_MINUTE = DEGREE.scale("′", 1.0 / 60.0);
96  
97      /** Arc second unit. */
98      public static final Unit ARC_SECOND = ARC_MINUTE.scale("″", 1.0 / 60.0);
99  
100     /** Revolution unit. */
101     public static final Unit REVOLUTION = RADIAN.scale("rev", 2.0 * FastMath.PI);
102 
103     /** Newton unit. */
104     public static final Unit NEWTON = KILOGRAM.multiply(null, METRE).divide("N", SECOND.power(null, Fraction.TWO));
105 
106     /** Pascal unit. */
107     public static final Unit PASCAL = NEWTON.divide("Pa", METRE.power(null, Fraction.TWO));
108 
109     /** Bar unit. */
110     public static final Unit BAR = PASCAL.scale("bar", 100000.0);
111 
112     /** Joule unit. */
113     public static final Unit JOULE = NEWTON.multiply("J", METRE);
114 
115     /** Watt unit. */
116     public static final Unit WATT = JOULE.divide("W", SECOND);
117 
118     /** Coulomb unit. */
119     public static final Unit COULOMB = SECOND.multiply("C", AMPERE);
120 
121     /** Volt unit. */
122     public static final Unit VOLT = WATT.divide("V", AMPERE);
123 
124     /** Ohm unit. */
125     public static final Unit OHM = VOLT.divide("Ω", AMPERE);
126 
127     /** tesla unit. */
128     public static final Unit TESLA = VOLT.multiply(null, SECOND).divide("T", METRE.power(null, Fraction.TWO));
129 
130     /** Solar Flux Unit. */
131     public static final Unit SOLAR_FLUX_UNIT = WATT.divide(null, METRE.power(null, Fraction.TWO).multiply(null, HERTZ)).scale("sfu", 1.0e-22);
132 
133     /** Total Electron Content Unit. */
134     public static final Unit TOTAL_ELECTRON_CONTENT_UNIT = METRE.power(null, new Fraction(-2)).scale("TECU", 1.0e+16);
135 
136     /** Serializable UID. */
137     private static final long serialVersionUID = 20210402L;
138 
139     /** Name name of the unit. */
140     private final String name;
141 
142     /** Scaling factor to SI units. */
143     private final double scale;
144 
145     /** Mass exponent. */
146     private final Fraction mass;
147 
148     /** Length exponent. */
149     private final Fraction length;
150 
151     /** Time exponent. */
152     private final Fraction time;
153 
154     /** Current exponent. */
155     private final Fraction current;
156 
157     /** Angle exponent. */
158     private final Fraction angle;
159 
160     /** Simple constructor.
161      * @param name name of the unit
162      * @param scale scaling factor to SI units
163      * @param mass mass exponent
164      * @param length length exponent
165      * @param time time exponent
166      * @param current current exponent
167      * @param angle angle exponent
168      */
169     public Unit(final String name, final double scale,
170                 final Fraction mass, final Fraction length,
171                 final Fraction time, final Fraction current,
172                 final Fraction angle) {
173         this.name    = name;
174         this.scale   = scale;
175         this.mass    = mass;
176         this.length  = length;
177         this.time    = time;
178         this.current = current;
179         this.angle   = angle;
180     }
181 
182     /** Get the name of the unit.
183      * @return name of the unit
184      */
185     public String getName() {
186         return name;
187     }
188 
189     /** Get the scaling factor to SI units.
190      * @return scaling factor to SI units
191      */
192     public double getScale() {
193         return scale;
194     }
195 
196     /** Get the mass exponent.
197      * @return mass exponent
198      */
199     public Fraction getMass() {
200         return mass;
201     }
202 
203     /** Get the length exponent.
204      * @return length exponent
205      */
206     public Fraction getLength() {
207         return length;
208     }
209 
210     /** Get the time exponent.
211      * @return time exponent
212      */
213     public Fraction getTime() {
214         return time;
215     }
216 
217     /** Get the current exponent.
218      * @return current exponent
219      */
220     public Fraction getCurrent() {
221         return current;
222     }
223 
224     /** Get the angle exponent.
225      * @return angle exponent
226      */
227     public Fraction getAngle() {
228         return angle;
229     }
230 
231     /** Check if a unit has the same dimension as another unit.
232      * @param other other unit to check against
233      * @return true if unit has the same dimension as the other unit
234      */
235     public boolean sameDimension(final Unit other) {
236         return time.equals(other.time) && length.equals(other.length)   &&
237                mass.equals(other.mass) && current.equals(other.current) &&
238                angle.equals(other.angle);
239     }
240 
241     /** Create the SI unit with same dimension.
242      * @return a new unit, with same dimension as instance and scaling factor set to 1.0
243      */
244     public Unit sameDimensionSI() {
245         final StringBuilder builder = new StringBuilder();
246         append(builder, KILOGRAM.name, mass);
247         append(builder, METRE.name,    length);
248         append(builder, SECOND.name,   time);
249         append(builder, AMPERE.name,   current);
250         append(builder, RADIAN.name,   angle);
251         if (builder.length() == 0) {
252             builder.append('1');
253         }
254         return new Unit(builder.toString(), 1.0, mass, length, time, current, angle);
255     }
256 
257     /** Append a dimension contribution to a unit name.
258      * @param builder builder for unit name
259      * @param dim name of the dimension
260      * @param exp exponent of the dimension
261      */
262     private void append(final StringBuilder builder, final String dim, final Fraction exp) {
263         if (!exp.isZero()) {
264             if (builder.length() > 0) {
265                 builder.append('.');
266             }
267             builder.append(dim);
268             if (exp.getDenominator() == 1) {
269                 if (exp.getNumerator() != 1) {
270                     builder.append(Integer.toString(exp.getNumerator()).
271                                    replace('-', '⁻').
272                                    replace('0', '⁰').
273                                    replace('1', '¹').
274                                    replace('2', '²').
275                                    replace('3', '³').
276                                    replace('4', '⁴').
277                                    replace('5', '⁵').
278                                    replace('6', '⁶').
279                                    replace('7', '⁷').
280                                    replace('8', '⁸').
281                                    replace('9', '⁹'));
282                 }
283             } else {
284                 builder.
285                     append("^(").
286                     append(exp.getNumerator()).
287                     append('/').
288                     append(exp.getDenominator()).
289                     append(')');
290             }
291         }
292     }
293 
294     /** Create an alias for a unit.
295      * @param newName name of the new unit
296      * @return a new unit representing same unit as the instance but with a different name
297      */
298     public Unit alias(final String newName) {
299         return new Unit(newName, scale, mass, length, time, current, angle);
300     }
301 
302     /** Scale a unit.
303      * @param newName name of the new unit
304      * @param factor scaling factor
305      * @return a new unit representing scale times the instance
306      */
307     public Unit scale(final String newName, final double factor) {
308         return new Unit(newName, factor * scale, mass, length, time, current, angle);
309     }
310 
311     /** Create power of unit.
312      * @param newName name of the new unit
313      * @param exponent exponent to apply
314      * @return a new unit representing the power of the instance
315      */
316     public Unit power(final String newName, final Fraction exponent) {
317 
318         final int num = exponent.getNumerator();
319         final int den = exponent.getDenominator();
320         double s = (num == 1) ? scale : FastMath.pow(scale, num);
321         if (den > 1) {
322             if (den == 2) {
323                 s = FastMath.sqrt(s);
324             } else if (den == 3) {
325                 s = FastMath.cbrt(s);
326             } else {
327                 s = FastMath.pow(s, 1.0 / den);
328             }
329         }
330 
331         return new Unit(newName, s,
332                         mass.multiply(exponent), length.multiply(exponent),
333                         time.multiply(exponent), current.multiply(current),
334                         angle.multiply(exponent));
335     }
336 
337     /** Create root of unit.
338      * @param newName name of the new unit
339      * @return a new unit representing the square root of the instance
340      */
341     public Unit sqrt(final String newName) {
342         return new Unit(newName, FastMath.sqrt(scale),
343                         mass.divide(2), length.divide(2),
344                         time.divide(2), current.divide(2),
345                         angle.divide(2));
346     }
347 
348     /** Create product of units.
349      * @param newName name of the new unit
350      * @param other unit to multiply with
351      * @return a new unit representing the this times the other unit
352      */
353     public Unit multiply(final String newName, final Unit other) {
354         return new Unit(newName, scale * other.scale,
355                         mass.add(other.mass), length.add(other.length),
356                         time.add(other.time), current.add(other.current),
357                         angle.add(other.angle));
358     }
359 
360     /** Create quotient of units.
361      * @param newName name of the new unit
362      * @param other unit to divide with
363      * @return a new unit representing the this divided by the other unit
364      */
365     public Unit divide(final String newName, final Unit other) {
366         return new Unit(newName, scale / other.scale,
367                         mass.subtract(other.mass), length.subtract(other.length),
368                         time.subtract(other.time), current.subtract(other.current),
369                         angle.subtract(other.angle));
370     }
371 
372     /** Convert a value to SI units.
373      * @param value value instance unit
374      * @return value in SI units
375      */
376     public double toSI(final double value) {
377         return value * scale;
378     }
379 
380     /** Convert a value to SI units.
381      * @param value value instance unit
382      * @return value in SI units
383      */
384     public double toSI(final Double value) {
385         return value == null ? Double.NaN : value.doubleValue() * scale;
386     }
387 
388     /** Convert a value from SI units.
389      * @param value value SI unit
390      * @return value in instance units
391      */
392     public double fromSI(final double value) {
393         return value / scale;
394     }
395 
396     /** Convert a value from SI units.
397      * @param value value SI unit
398      * @return value in instance units
399      */
400     public double fromSI(final Double value) {
401         return value == null ? Double.NaN : value.doubleValue() / scale;
402     }
403 
404     /** Parse a unit.
405      * <p>
406      * The grammar for unit specification allows chains units multiplication and
407      * division, as well as putting powers on units.
408      * </p>
409      * <p>The symbols used for units are the SI units with some extensions.
410      * </p>
411      * <dl>
412      *   <dt>year</dt>
413      *   <dd>the accepted non-SI unit for Julian year is "a" but we also accept "yr"</dd>
414      *   <dt>dimensionless</dt>
415      *   <dd>both "1" and "#" (U+0023, NUMBER SIGN) are accepted</dd>
416      *   <dt>mass</dt>
417      *   <dd>"g" is the standard symbol, despite the unit is "kg" (its the only
418      *       unit that has a prefix in its name, so all multiples must be based on "g")</dd>
419      *   <dt>degrees</dt>
420      *   <dd>the base symbol for degrees is "°" (U+00B0, DEGREE SIGN), but we also accept
421      *       "◦" (U+25E6, WHITE BULLET) and "deg"</dd>
422      *   <dt>arcminute</dt>
423      *   <dd>The base symbol for  is "′" (U+2032, PRIME) but we also accept "'" (U+0027, APOSTROPHE)</dd>
424      *   <dt>arcsecond</dt>
425      *   <dd>The base symbol for  is "″" (U+2033, DOUBLE PRIME) but we also accept
426      *   "''" (two occurrences of U+0027, APOSTROPHE), "\"" (U+0022, QUOTATION MARK) and "as"</dd>
427      * </dl>
428      * <p>
429      * All the SI prefix (from "y", yocto, to "Y", Yotta) are accepted, as well
430      * as integer prefixes. The standard symbol for micro 10⁻⁶ is "µ" (U+00B5, MICRO SIGN),
431      * but we also accept "μ" (U+03BC, GREEK SMALL LETTER MU). Beware that some combinations
432      * are forbidden, for example "Pa" is Pascal, not peta-years, and "as" is arcsecond for
433      * this parser, not atto-seconds, because many people in the space field use mas for
434      * milliarcseconds and µas for microarcseconds. Beware that prefixes are case-sensitive!
435      * Integer prefixes can be used to specify units like "30s", but only once at the beginning
436      * of the specification (i.e. "2rev/d²" is accepted, but "rev/(2d)²" is refused). Conforming
437      * with SI brochure "The International System of Units" (9th edition, 2019), each SI
438      * prefix is part of the unit and precedes the unit symbol without a separator
439      * (i.e. MHz is seen as one identifier).
440      * </p>
441      * <dl>
442      *   <dt>multiplication</dt>
443      *   <dd>can specified with either "*" (U+002A, ASTERISK), "×" (U+00D7, MULTIPLICATION SIGN),
444      *   "." (U+002E, FULL STOP) or "·" (U+00B7, MIDDLE DOT) as the operator</dd>
445      *   <dt>division</dt>
446      *   <dd>can be specified with either "/" (U+002F, SOLIDUS) or "⁄" (U+2044, FRACTION SLASH)
447      *   as the operator</dd>
448      *   <dt>powers</dt>
449      *   <dd>can be specified either by
450      *     <ul>
451      *       <li>prefixing with the unicode "√" (U+221A, SQUARE ROOT) character</li>
452      *       <li>postfixing with "**", "^" or implicitly using unicode superscripts</li>
453      *     </ul>
454      *   </dd>
455      * </dl>
456      * <p>
457      * Exponents can be specified in different ways:
458      * <ul>
459      *   <li>as an integer, as in "m^-2" or "m⁻²"</li>
460      *   <li>directly as unicode characters for the few fractions that unicode supports, as in "Ω^⅞"</li>
461      *   <li>as the special decimal value 0.5 which is used by CCSDS, as in "km**0.5"</li>
462      *   <li>as a pair of parentheses surrounding two integers separated by a solidus or fraction slash,
463      *   as in "Pa^(11/12)"</li>
464      * </ul>
465      * For integer exponents, the digits must be ASCII digits from the Basic Latin block from
466      * unicode if explicit exponent maker "**" or "^" was used, or using unicode superscript
467      * digits if implicit exponentiation (i.e. no markers at all) is used. Unicode superscripts
468      * are not allowed for fractional exponents because unicode does not provide a superscript solidus.
469      * Negative exponents can be used too.
470      * <p>
471      * These rules mean all the following (silly) examples are parsed properly:
472      * MHz, km/√d, kg.m.s⁻¹, µas^⅖/(h**(2)×m)³, km/√(kg.s), km**0.5, 2rev/d²
473      * </p>
474      * @param unitSpecification unit specification to parse
475      * @return parsed unit
476      */
477     public static Unit parse(final String unitSpecification) {
478 
479         // parse the specification
480         final List<PowerTerm> terms = Parser.buildTermsList(unitSpecification);
481 
482         if (terms == null) {
483             // special handling of "n/a"
484             return Unit.NONE;
485         }
486 
487         // build compound unit
488         Unit unit = Unit.ONE;
489         for (final PowerTerm term : terms) {
490             try {
491                 Unit u = PrefixedUnit.valueOf(term.getBase().toString());
492                 if (!Fraction.ONE.equals(term.getExponent())) {
493                     u = u.power(null, term.getExponent());
494                 }
495                 u = u.scale(null, term.getScale());
496                 unit = unit.multiply(null, u);
497             } catch (IllegalArgumentException iae) {
498                 throw new OrekitException(OrekitMessages.UNKNOWN_UNIT, term.getBase());
499             }
500         }
501 
502         // give final name to unit
503         return unit.alias(unitSpecification);
504 
505     }
506 
507     /** Check if the instance represents the same unit as another instance.
508      * <p>
509      * The name is not considered so aliases are considered equal.
510      * </p>
511      * @param unit other unit
512      * @return true if the instance and the other unit refer to the same unit
513      */
514     public boolean equals(final Object unit) {
515 
516         if (unit == this) {
517             // first fast check
518             return true;
519         }
520 
521         if (unit instanceof Unit) {
522             final Unit u = (Unit) unit;
523             return Precision.equals(scale, u.scale, 1) &&
524                    mass.equals(u.mass) && length.equals(u.length) && time.equals(u.time) &&
525                    current.equals(u.current) && angle.equals(u.angle);
526         }
527 
528         return false;
529 
530     }
531 
532     /** Get a hashcode for this unit.
533      * @return hashcode
534      */
535     public int hashCode() {
536         return 0x67e7 ^
537                (Double.hashCode(scale) << 12) ^
538                (mass.hashCode()        << 10) ^
539                (length.hashCode()      <<  8) ^
540                (time.hashCode()        <<  6) ^
541                (current.hashCode()     <<  4) ^
542                (angle.hashCode()       <<  2);
543     }
544 
545     /** {@inheritDoc} */
546     @Override
547     public String toString() {
548         return getName();
549     }
550 
551 }