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.util.ArrayList;
20 import java.util.Collections;
21 import java.util.List;
22
23 import org.hipparchus.fraction.Fraction;
24 import org.hipparchus.util.FastMath;
25
26 /** Parser for units.
27 * <p>
28 * This fairly basic parser uses recursive descent with the following grammar,
29 * where '*' can in fact be either '*', '×', '.', or '·', '/' can be either
30 * '/' or '⁄' and '^' can be either '^', "**" or implicit with switch to superscripts,
31 * and fraction are either unicode fractions like ½ or ⅞ or the decimal value 0.5.
32 * The special cases "n/a" returns a null list. It is intended to manage the
33 * special unit {@link Unit#NONE}.
34 * </p>
35 * <pre>
36 * unit ::= "n/a" | chain
37 * chain ::= operand { ('*' | '/') operand }
38 * operand ::= integer | integer term | term
39 * term ::= '√' base | base power
40 * power ::= '^' exponent | ε
41 * exponent ::= 'fraction' | integer | '(' integer denominator ')'
42 * denominator ::= '/' integer | ε
43 * base ::= identifier | '(' chain ')'
44 * </pre>
45 * <p>
46 * This parses correctly units like MHz, km/√d, kg.m.s⁻¹, µas^⅖/(h**(2)×m)³, km/√(kg.s),
47 * √kg*km** (3/2) /(µs^2*Ω⁻⁷), km**0.5/s, #/y, 2rev/d², 1/s.
48 * </p>
49 * <p>
50 * Note that we don't accept combining square roots and power on the same operand; km/√d³
51 * is refused (but km/√(d³) is accepted). We also accept a single integer prefix and
52 * only at the start of the specification.
53 * </p>
54 * @author Luc Maisonobe
55 * @since 11.0
56 */
57 public class Parser {
58
59 /** Private constructor for a utility class.
60 */
61 private Parser() {
62 }
63
64 /** Build the list of terms corresponding to a units specification.
65 * @param unitsSpecification units specification to parse
66 * @return parse tree
67 */
68 public static List<PowerTerm> buildTermsList(final String unitsSpecification) {
69 if (Unit.NONE.getName().equals(unitsSpecification)) {
70 // special case for no units
71 return null;
72 } else {
73 final Lexer lexer = new Lexer(unitsSpecification);
74 final List<PowerTerm> chain = chain(lexer);
75 if (lexer.next() != null) {
76 throw lexer.generateException();
77 }
78 return chain;
79 }
80 }
81
82 /** Parse a units chain.
83 * @param lexer lexer providing tokens
84 * @return parsed units chain
85 */
86 private static List<PowerTerm> chain(final Lexer lexer) {
87 final List<PowerTerm> chain = new ArrayList<>();
88 chain.addAll(operand(lexer));
89 for (Token token = lexer.next(); token != null; token = lexer.next()) {
90 if (checkType(token, TokenType.MULTIPLICATION)) {
91 chain.addAll(operand(lexer));
92 } else if (checkType(token, TokenType.DIVISION)) {
93 chain.addAll(reciprocate(operand(lexer)));
94 } else {
95 lexer.pushBack();
96 break;
97 }
98 }
99 return chain;
100 }
101
102 /** Parse an operand.
103 * @param lexer lexer providing tokens
104 * @return parsed operand
105 */
106 private static List<PowerTerm> operand(final Lexer lexer) {
107 final Token token1 = lexer.next();
108 if (token1 == null) {
109 throw lexer.generateException();
110 }
111 if (checkType(token1, TokenType.INTEGER)) {
112 final int scale = token1.getInt();
113 final Token token2 = lexer.next();
114 lexer.pushBack();
115 if (token2 == null ||
116 checkType(token2, TokenType.MULTIPLICATION) ||
117 checkType(token2, TokenType.DIVISION)) {
118 return Collections.singletonList(new PowerTerm(scale, "1", Fraction.ONE));
119 } else {
120 return applyScale(term(lexer), scale);
121 }
122 } else {
123 lexer.pushBack();
124 return term(lexer);
125 }
126 }
127
128 /** Parse a term.
129 * @param lexer lexer providing tokens
130 * @return parsed term
131 */
132 private static List<PowerTerm> term(final Lexer lexer) {
133 final Token token = lexer.next();
134 if (token.getType() == TokenType.SQUARE_ROOT) {
135 return applyExponent(base(lexer), Fraction.ONE_HALF);
136 } else {
137 lexer.pushBack();
138 return applyExponent(base(lexer), power(lexer));
139 }
140 }
141
142 /** Parse a power operation.
143 * @param lexer lexer providing tokens
144 * @return exponent, or null if no exponent
145 */
146 private static Fraction power(final Lexer lexer) {
147 final Token token = lexer.next();
148 if (checkType(token, TokenType.POWER)) {
149 return exponent(lexer);
150 } else {
151 lexer.pushBack();
152 return null;
153 }
154 }
155
156 /** Parse an exponent.
157 * @param lexer lexer providing tokens
158 * @return exponent
159 */
160 private static Fraction exponent(final Lexer lexer) {
161 final Token token = lexer.next();
162 if (checkType(token, TokenType.FRACTION)) {
163 return token.getFraction();
164 } else if (checkType(token, TokenType.INTEGER)) {
165 return new Fraction(token.getInt());
166 } else {
167 lexer.pushBack();
168 accept(lexer, TokenType.OPEN);
169 final int num = accept(lexer, TokenType.INTEGER).getInt();
170 final int den = denominator(lexer);
171 accept(lexer, TokenType.CLOSE);
172 return new Fraction(num, den);
173 }
174 }
175
176 /** Parse a denominator.
177 * @param lexer lexer providing tokens
178 * @return denominatior
179 */
180 private static int denominator(final Lexer lexer) {
181 final Token token = lexer.next();
182 if (checkType(token, TokenType.DIVISION)) {
183 return accept(lexer, TokenType.INTEGER).getInt();
184 } else {
185 lexer.pushBack();
186 return 1;
187 }
188 }
189
190 /** Parse a base term.
191 * @param lexer lexer providing tokens
192 * @return base term
193 */
194 private static List<PowerTerm> base(final Lexer lexer) {
195 final Token token = lexer.next();
196 if (checkType(token, TokenType.IDENTIFIER)) {
197 return Collections.singletonList(new PowerTerm(1.0, token.getSubString(), Fraction.ONE));
198 } else {
199 lexer.pushBack();
200 accept(lexer, TokenType.OPEN);
201 final List<PowerTerm> chain = chain(lexer);
202 accept(lexer, TokenType.CLOSE);
203 return chain;
204 }
205 }
206
207 /** Compute the reciprocal a base term.
208 * @param base base term
209 * @return reciprocal of base term
210 */
211 private static List<PowerTerm> reciprocate(final List<PowerTerm> base) {
212
213 // reciprocate individual terms
214 final List<PowerTerm> reciprocal = new ArrayList<>(base.size());
215 for (final PowerTerm term : base) {
216 reciprocal.add(new PowerTerm(1.0 / term.getScale(), term.getBase(), term.getExponent().negate()));
217 }
218
219 return reciprocal;
220
221 }
222
223 /** Apply a scaling factor to a base term.
224 * @param base base term
225 * @param scale scaling factor
226 * @return term with scaling factor applied (same as {@code base} if {@code scale} is 1)
227 */
228 private static List<PowerTerm> applyScale(final List<PowerTerm> base, final int scale) {
229
230 if (scale == 1) {
231 // no scaling at all, return the base term itself
232 return base;
233 }
234
235 // combine scaling factor with first term
236 final List<PowerTerm> powered = new ArrayList<>(base.size());
237 boolean first = true;
238 for (final PowerTerm term : base) {
239 if (first) {
240 powered.add(new PowerTerm(scale * term.getScale(), term.getBase(), term.getExponent()));
241 first = false;
242 } else {
243 powered.add(term);
244 }
245 }
246
247 return powered;
248
249 }
250
251 /** Apply an exponent to a base term.
252 * @param base base term
253 * @param exponent exponent (may be null)
254 * @return term with exponent applied (same as {@code base} if exponent is null)
255 */
256 private static List<PowerTerm> applyExponent(final List<PowerTerm> base, final Fraction exponent) {
257
258 if (exponent == null || exponent.equals(Fraction.ONE)) {
259 // return the base term itself
260 return base;
261 }
262
263 // combine exponent with existing ones, for example to handles compounds units like m/(kg.s²)³
264 final List<PowerTerm> powered = new ArrayList<>(base.size());
265 for (final PowerTerm term : base) {
266 final double poweredScale;
267 if (exponent.isInteger()) {
268 poweredScale = FastMath.pow(term.getScale(), exponent.getNumerator());
269 } else if (Fraction.ONE_HALF.equals(exponent)) {
270 poweredScale = FastMath.sqrt(term.getScale());
271 } else {
272 poweredScale = FastMath.pow(term.getScale(), exponent.doubleValue());
273 }
274 powered.add(new PowerTerm(poweredScale, term.getBase(), exponent.multiply(term.getExponent())));
275 }
276
277 return powered;
278
279 }
280
281 /** Accept a token.
282 * @param lexer lexer providing tokens
283 * @param expected expected token type
284 * @return accepted token
285 */
286 private static Token accept(final Lexer lexer, final TokenType expected) {
287 final Token token = lexer.next();
288 if (!checkType(token, expected)) {
289 throw lexer.generateException();
290 }
291 return token;
292 }
293
294 /** Check a token exists and has proper type.
295 * @param token token to check
296 * @param expected expected token type
297 * @return true if token exists and has proper type
298 */
299 private static boolean checkType(final Token token, final TokenType expected) {
300 return token != null && token.getType() == expected;
301 }
302
303 }