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 /** Earth Radii used as Bstar unit in CCSDS OMM. */
137 public static final Unit EARTH_RADII = new Unit("ER", 1.0, Fraction.ZERO, Fraction.ZERO, Fraction.ZERO, Fraction.ONE, Fraction.ZERO);
138
139 /** Serializable UID. */
140 private static final long serialVersionUID = 20210402L;
141
142 /** Name name of the unit. */
143 private final String name;
144
145 /** Scaling factor to SI units. */
146 private final double scale;
147
148 /** Mass exponent. */
149 private final Fraction mass;
150
151 /** Length exponent. */
152 private final Fraction length;
153
154 /** Time exponent. */
155 private final Fraction time;
156
157 /** Current exponent. */
158 private final Fraction current;
159
160 /** Angle exponent. */
161 private final Fraction angle;
162
163 /** Simple constructor.
164 * @param name name of the unit
165 * @param scale scaling factor to SI units
166 * @param mass mass exponent
167 * @param length length exponent
168 * @param time time exponent
169 * @param current current exponent
170 * @param angle angle exponent
171 */
172 public Unit(final String name, final double scale,
173 final Fraction mass, final Fraction length,
174 final Fraction time, final Fraction current,
175 final Fraction angle) {
176 this.name = name;
177 this.scale = scale;
178 this.mass = mass;
179 this.length = length;
180 this.time = time;
181 this.current = current;
182 this.angle = angle;
183 }
184
185 /** Get the name of the unit.
186 * @return name of the unit
187 */
188 public String getName() {
189 return name;
190 }
191
192 /** Get the scaling factor to SI units.
193 * @return scaling factor to SI units
194 */
195 public double getScale() {
196 return scale;
197 }
198
199 /** Get the mass exponent.
200 * @return mass exponent
201 */
202 public Fraction getMass() {
203 return mass;
204 }
205
206 /** Get the length exponent.
207 * @return length exponent
208 */
209 public Fraction getLength() {
210 return length;
211 }
212
213 /** Get the time exponent.
214 * @return time exponent
215 */
216 public Fraction getTime() {
217 return time;
218 }
219
220 /** Get the current exponent.
221 * @return current exponent
222 */
223 public Fraction getCurrent() {
224 return current;
225 }
226
227 /** Get the angle exponent.
228 * @return angle exponent
229 */
230 public Fraction getAngle() {
231 return angle;
232 }
233
234 /** Check if a unit has the same dimension as another unit.
235 * @param other other unit to check against
236 * @return true if unit has the same dimension as the other unit
237 */
238 public boolean sameDimension(final Unit other) {
239 return time.equals(other.time) && length.equals(other.length) &&
240 mass.equals(other.mass) && current.equals(other.current) &&
241 angle.equals(other.angle);
242 }
243
244 /** Create the SI unit with same dimension.
245 * @return a new unit, with same dimension as instance and scaling factor set to 1.0
246 */
247 public Unit sameDimensionSI() {
248 final StringBuilder builder = new StringBuilder();
249 append(builder, KILOGRAM.name, mass);
250 append(builder, METRE.name, length);
251 append(builder, SECOND.name, time);
252 append(builder, AMPERE.name, current);
253 append(builder, RADIAN.name, angle);
254 if (builder.length() == 0) {
255 builder.append('1');
256 }
257 return new Unit(builder.toString(), 1.0, mass, length, time, current, angle);
258 }
259
260 /** Append a dimension contribution to a unit name.
261 * @param builder builder for unit name
262 * @param dim name of the dimension
263 * @param exp exponent of the dimension
264 */
265 private void append(final StringBuilder builder, final String dim, final Fraction exp) {
266 if (!exp.isZero()) {
267 if (builder.length() > 0) {
268 builder.append('.');
269 }
270 builder.append(dim);
271 if (exp.getDenominator() == 1) {
272 if (exp.getNumerator() != 1) {
273 builder.append(Integer.toString(exp.getNumerator()).
274 replace('-', '⁻').
275 replace('0', '⁰').
276 replace('1', '¹').
277 replace('2', '²').
278 replace('3', '³').
279 replace('4', '⁴').
280 replace('5', '⁵').
281 replace('6', '⁶').
282 replace('7', '⁷').
283 replace('8', '⁸').
284 replace('9', '⁹'));
285 }
286 } else {
287 builder.
288 append("^(").
289 append(exp.getNumerator()).
290 append('/').
291 append(exp.getDenominator()).
292 append(')');
293 }
294 }
295 }
296
297 /** Create an alias for a unit.
298 * @param newName name of the new unit
299 * @return a new unit representing same unit as the instance but with a different name
300 */
301 public Unit alias(final String newName) {
302 return new Unit(newName, scale, mass, length, time, current, angle);
303 }
304
305 /** Scale a unit.
306 * @param newName name of the new unit
307 * @param factor scaling factor
308 * @return a new unit representing scale times the instance
309 */
310 public Unit scale(final String newName, final double factor) {
311 return new Unit(newName, factor * scale, mass, length, time, current, angle);
312 }
313
314 /** Create power of unit.
315 * @param newName name of the new unit
316 * @param exponent exponent to apply
317 * @return a new unit representing the power of the instance
318 */
319 public Unit power(final String newName, final Fraction exponent) {
320
321 final int num = exponent.getNumerator();
322 final int den = exponent.getDenominator();
323 double s = (num == 1) ? scale : FastMath.pow(scale, num);
324 if (den > 1) {
325 if (den == 2) {
326 s = FastMath.sqrt(s);
327 } else if (den == 3) {
328 s = FastMath.cbrt(s);
329 } else {
330 s = FastMath.pow(s, 1.0 / den);
331 }
332 }
333
334 return new Unit(newName, s,
335 mass.multiply(exponent), length.multiply(exponent),
336 time.multiply(exponent), current.multiply(current),
337 angle.multiply(exponent));
338 }
339
340 /** Create root of unit.
341 * @param newName name of the new unit
342 * @return a new unit representing the square root of the instance
343 */
344 public Unit sqrt(final String newName) {
345 return new Unit(newName, FastMath.sqrt(scale),
346 mass.divide(2), length.divide(2),
347 time.divide(2), current.divide(2),
348 angle.divide(2));
349 }
350
351 /** Create product of units.
352 * @param newName name of the new unit
353 * @param other unit to multiply with
354 * @return a new unit representing the this times the other unit
355 */
356 public Unit multiply(final String newName, final Unit other) {
357 return new Unit(newName, scale * other.scale,
358 mass.add(other.mass), length.add(other.length),
359 time.add(other.time), current.add(other.current),
360 angle.add(other.angle));
361 }
362
363 /** Create quotient of units.
364 * @param newName name of the new unit
365 * @param other unit to divide with
366 * @return a new unit representing the this divided by the other unit
367 */
368 public Unit divide(final String newName, final Unit other) {
369 return new Unit(newName, scale / other.scale,
370 mass.subtract(other.mass), length.subtract(other.length),
371 time.subtract(other.time), current.subtract(other.current),
372 angle.subtract(other.angle));
373 }
374
375 /** Convert a value to SI units.
376 * @param value value instance unit
377 * @return value in SI units
378 */
379 public double toSI(final double value) {
380 return value * scale;
381 }
382
383 /** Convert a value to SI units.
384 * @param value value instance unit
385 * @return value in SI units
386 */
387 public double toSI(final Double value) {
388 return value == null ? Double.NaN : value.doubleValue() * scale;
389 }
390
391 /** Convert a value from SI units.
392 * @param value value SI unit
393 * @return value in instance units
394 */
395 public double fromSI(final double value) {
396 return value / scale;
397 }
398
399 /** Convert a value from SI units.
400 * @param value value SI unit
401 * @return value in instance units
402 */
403 public double fromSI(final Double value) {
404 return value == null ? Double.NaN : value.doubleValue() / scale;
405 }
406
407 /** Parse a unit.
408 * <p>
409 * The grammar for unit specification allows chains units multiplication and
410 * division, as well as putting powers on units.
411 * </p>
412 * <p>The symbols used for units are the SI units with some extensions.
413 * </p>
414 * <dl>
415 * <dt>year</dt>
416 * <dd>the accepted non-SI unit for Julian year is "a" but we also accept "yr"</dd>
417 * <dt>day</dt>
418 * <dd>the accepted non-SI unit for day is "d" but we also accept "day"</dd>
419 * <dt>dimensionless</dt>
420 * <dd>both "1" and "#" (U+0023, NUMBER SIGN) are accepted</dd>
421 * <dt>mass</dt>
422 * <dd>"g" is the standard symbol, despite the unit is "kg" (it is the only
423 * unit that has a prefix in its name, so all multiples must be based on "g")</dd>
424 * <dt>degrees</dt>
425 * <dd>the base symbol for degrees is "°" (U+00B0, DEGREE SIGN), but we also accept
426 * "◦" (U+25E6, WHITE BULLET) and "deg"</dd>
427 * <dt>arcminute</dt>
428 * <dd>The base symbol for arcminute is "′" (U+2032, PRIME) but we also accept "'" (U+0027, APOSTROPHE)</dd>
429 * <dt>arcsecond</dt>
430 * <dd>The base symbol for arcsecond is "″" (U+2033, DOUBLE PRIME) but we also accept
431 * "''" (two occurrences of U+0027, APOSTROPHE), "\"" (U+0022, QUOTATION MARK) and "as"</dd>
432 * </dl>
433 * <p>
434 * All the SI prefix (from "y", yocto, to "Y", Yotta) are accepted, as well
435 * as integer prefixes. The standard symbol for micro 10⁻⁶ is "µ" (U+00B5, MICRO SIGN),
436 * but we also accept "μ" (U+03BC, GREEK SMALL LETTER MU). Beware that some combinations
437 * are forbidden, for example "Pa" is Pascal, not peta-years, and "as" is arcsecond for
438 * this parser, not atto-seconds, because many people in the space field use mas for
439 * milliarcseconds and µas for microarcseconds. Beware that prefixes are case-sensitive!
440 * Integer prefixes can be used to specify units like "30s", but only once at the beginning
441 * of the specification (i.e. "2rev/d²" is accepted, but "rev/(2d)²" is refused). Conforming
442 * with SI brochure "The International System of Units" (9th edition, 2019), each SI
443 * prefix is part of the unit and precedes the unit symbol without a separator
444 * (i.e. MHz is seen as one identifier).
445 * </p>
446 * <dl>
447 * <dt>multiplication</dt>
448 * <dd>can specified with either "*" (U+002A, ASTERISK), "×" (U+00D7, MULTIPLICATION SIGN),
449 * "." (U+002E, FULL STOP) or "·" (U+00B7, MIDDLE DOT) as the operator</dd>
450 * <dt>division</dt>
451 * <dd>can be specified with either "/" (U+002F, SOLIDUS) or "⁄" (U+2044, FRACTION SLASH)
452 * as the operator</dd>
453 * <dt>powers</dt>
454 * <dd>can be specified either by
455 * <ul>
456 * <li>prefixing with the unicode "√" (U+221A, SQUARE ROOT) character</li>
457 * <li>postfixing with "**", "^" or implicitly using unicode superscripts</li>
458 * </ul>
459 * </dd>
460 * </dl>
461 * <p>
462 * Exponents can be specified in different ways:
463 * <ul>
464 * <li>as an integer, as in "m^-2" or "m⁻²"</li>
465 * <li>directly as unicode characters for the few fractions that unicode supports, as in "Ω^⅞"</li>
466 * <li>as the special decimal value 0.5 which is used by CCSDS, as in "km**0.5"</li>
467 * <li>as a pair of parentheses surrounding two integers separated by a solidus or fraction slash,
468 * as in "Pa^(11/12)"</li>
469 * </ul>
470 * For integer exponents, the digits must be ASCII digits from the Basic Latin block from
471 * unicode if explicit exponent marker "**" or "^" is used, or using unicode superscript
472 * digits if implicit exponentiation (i.e. no markers at all) is used. Unicode superscripts
473 * are not allowed for fractional exponents because unicode does not provide a superscript solidus.
474 * Negative exponents can be used too.
475 * <p>
476 * These rules mean all the following (silly) examples are parsed properly:
477 * MHz, km/√d, kg.m.s⁻¹, µas^⅖/(h**(2)×m)³, km/√(kg.s), km**0.5, 2rev/d²
478 * </p>
479 * @param unitSpecification unit specification to parse
480 * @return parsed unit
481 */
482 public static Unit parse(final String unitSpecification) {
483
484 // parse the specification
485 final List<PowerTerm> terms = Parser.buildTermsList(unitSpecification);
486
487 if (terms == null) {
488 // special handling of "n/a"
489 return Unit.NONE;
490 }
491
492 // build compound unit
493 Unit unit = Unit.ONE;
494 for (final PowerTerm term : terms) {
495 try {
496 Unit u = PrefixedUnit.valueOf(term.getBase().toString());
497 if (!Fraction.ONE.equals(term.getExponent())) {
498 u = u.power(null, term.getExponent());
499 }
500 u = u.scale(null, term.getScale());
501 unit = unit.multiply(null, u);
502 } catch (IllegalArgumentException iae) {
503 throw new OrekitException(OrekitMessages.UNKNOWN_UNIT, term.getBase());
504 }
505 }
506
507 // give final name to unit
508 return unit.alias(unitSpecification);
509
510 }
511
512 /** Check if the instance represents the same unit as another instance.
513 * <p>
514 * The name is not considered so aliases are considered equal.
515 * </p>
516 * @param unit other unit
517 * @return true if the instance and the other unit refer to the same unit
518 */
519 public boolean equals(final Object unit) {
520
521 if (unit == this) {
522 // first fast check
523 return true;
524 }
525
526 if (unit instanceof Unit) {
527 final Unit u = (Unit) unit;
528 return Precision.equals(scale, u.scale, 1) &&
529 mass.equals(u.mass) && length.equals(u.length) && time.equals(u.time) &&
530 current.equals(u.current) && angle.equals(u.angle);
531 }
532
533 return false;
534
535 }
536
537 /** Get a hashcode for this unit.
538 * @return hashcode
539 */
540 public int hashCode() {
541 return 0x67e7 ^
542 (Double.hashCode(scale) << 12) ^
543 (mass.hashCode() << 10) ^
544 (length.hashCode() << 8) ^
545 (time.hashCode() << 6) ^
546 (current.hashCode() << 4) ^
547 (angle.hashCode() << 2);
548 }
549
550 /** {@inheritDoc} */
551 @Override
552 public String toString() {
553 return getName();
554 }
555
556 }