Parser.java
/* Copyright 2002-2024 CS GROUP
* Licensed to CS GROUP (CS) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* CS licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.orekit.utils.units;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.hipparchus.fraction.Fraction;
import org.hipparchus.util.FastMath;
/** Parser for units.
* <p>
* This fairly basic parser uses recursive descent with the following grammar,
* where '*' can in fact be either '*', '×', '.', or '·', '/' can be either
* '/' or '⁄' and '^' can be either '^', "**" or implicit with switch to superscripts,
* and fraction are either unicode fractions like ½ or ⅞ or the decimal value 0.5.
* The special cases "n/a" returns a null list. It is intended to manage the
* special unit {@link Unit#NONE}.
* </p>
* <pre>
* unit ::= "n/a" | chain
* chain ::= operand { ('*' | '/') operand }
* operand ::= integer | integer term | term
* term ::= '√' base | base power
* power ::= '^' exponent | ε
* exponent ::= 'fraction' | integer | '(' integer denominator ')'
* denominator ::= '/' integer | ε
* base ::= identifier | '(' chain ')'
* </pre>
* <p>
* This parses correctly units like MHz, km/√d, kg.m.s⁻¹, µas^⅖/(h**(2)×m)³, km/√(kg.s),
* √kg*km** (3/2) /(µs^2*Ω⁻⁷), km**0.5/s, #/y, 2rev/d², 1/s.
* </p>
* <p>
* Note that we don't accept combining square roots and power on the same operand; km/√d³
* is refused (but km/√(d³) is accepted). We also accept a single integer prefix and
* only at the start of the specification.
* </p>
* @author Luc Maisonobe
* @since 11.0
*/
public class Parser {
/** Private constructor for a utility class.
*/
private Parser() {
}
/** Build the list of terms corresponding to a units specification.
* @param unitsSpecification units specification to parse
* @return parse tree
*/
public static List<PowerTerm> buildTermsList(final String unitsSpecification) {
if (Unit.NONE.getName().equals(unitsSpecification)) {
// special case for no units
return null;
} else {
final Lexer lexer = new Lexer(unitsSpecification);
final List<PowerTerm> chain = chain(lexer);
if (lexer.next() != null) {
throw lexer.generateException();
}
return chain;
}
}
/** Parse a units chain.
* @param lexer lexer providing tokens
* @return parsed units chain
*/
private static List<PowerTerm> chain(final Lexer lexer) {
final List<PowerTerm> chain = new ArrayList<>();
chain.addAll(operand(lexer));
for (Token token = lexer.next(); token != null; token = lexer.next()) {
if (checkType(token, TokenType.MULTIPLICATION)) {
chain.addAll(operand(lexer));
} else if (checkType(token, TokenType.DIVISION)) {
chain.addAll(reciprocate(operand(lexer)));
} else {
lexer.pushBack();
break;
}
}
return chain;
}
/** Parse an operand.
* @param lexer lexer providing tokens
* @return parsed operand
*/
private static List<PowerTerm> operand(final Lexer lexer) {
final Token token1 = lexer.next();
if (token1 == null) {
throw lexer.generateException();
}
if (checkType(token1, TokenType.INTEGER)) {
final int scale = token1.getInt();
final Token token2 = lexer.next();
lexer.pushBack();
if (token2 == null ||
checkType(token2, TokenType.MULTIPLICATION) ||
checkType(token2, TokenType.DIVISION)) {
return Collections.singletonList(new PowerTerm(scale, "1", Fraction.ONE));
} else {
return applyScale(term(lexer), scale);
}
} else {
lexer.pushBack();
return term(lexer);
}
}
/** Parse a term.
* @param lexer lexer providing tokens
* @return parsed term
*/
private static List<PowerTerm> term(final Lexer lexer) {
final Token token = lexer.next();
if (token.getType() == TokenType.SQUARE_ROOT) {
return applyExponent(base(lexer), Fraction.ONE_HALF);
} else {
lexer.pushBack();
return applyExponent(base(lexer), power(lexer));
}
}
/** Parse a power operation.
* @param lexer lexer providing tokens
* @return exponent, or null if no exponent
*/
private static Fraction power(final Lexer lexer) {
final Token token = lexer.next();
if (checkType(token, TokenType.POWER)) {
return exponent(lexer);
} else {
lexer.pushBack();
return null;
}
}
/** Parse an exponent.
* @param lexer lexer providing tokens
* @return exponent
*/
private static Fraction exponent(final Lexer lexer) {
final Token token = lexer.next();
if (checkType(token, TokenType.FRACTION)) {
return token.getFraction();
} else if (checkType(token, TokenType.INTEGER)) {
return new Fraction(token.getInt());
} else {
lexer.pushBack();
accept(lexer, TokenType.OPEN);
final int num = accept(lexer, TokenType.INTEGER).getInt();
final int den = denominator(lexer);
accept(lexer, TokenType.CLOSE);
return new Fraction(num, den);
}
}
/** Parse a denominator.
* @param lexer lexer providing tokens
* @return denominatior
*/
private static int denominator(final Lexer lexer) {
final Token token = lexer.next();
if (checkType(token, TokenType.DIVISION)) {
return accept(lexer, TokenType.INTEGER).getInt();
} else {
lexer.pushBack();
return 1;
}
}
/** Parse a base term.
* @param lexer lexer providing tokens
* @return base term
*/
private static List<PowerTerm> base(final Lexer lexer) {
final Token token = lexer.next();
if (checkType(token, TokenType.IDENTIFIER)) {
return Collections.singletonList(new PowerTerm(1.0, token.getSubString(), Fraction.ONE));
} else {
lexer.pushBack();
accept(lexer, TokenType.OPEN);
final List<PowerTerm> chain = chain(lexer);
accept(lexer, TokenType.CLOSE);
return chain;
}
}
/** Compute the reciprocal a base term.
* @param base base term
* @return reciprocal of base term
*/
private static List<PowerTerm> reciprocate(final List<PowerTerm> base) {
// reciprocate individual terms
final List<PowerTerm> reciprocal = new ArrayList<>(base.size());
for (final PowerTerm term : base) {
reciprocal.add(new PowerTerm(1.0 / term.getScale(), term.getBase(), term.getExponent().negate()));
}
return reciprocal;
}
/** Apply a scaling factor to a base term.
* @param base base term
* @param scale scaling factor
* @return term with scaling factor applied (same as {@code base} if {@code scale} is 1)
*/
private static List<PowerTerm> applyScale(final List<PowerTerm> base, final int scale) {
if (scale == 1) {
// no scaling at all, return the base term itself
return base;
}
// combine scaling factor with first term
final List<PowerTerm> powered = new ArrayList<>(base.size());
boolean first = true;
for (final PowerTerm term : base) {
if (first) {
powered.add(new PowerTerm(scale * term.getScale(), term.getBase(), term.getExponent()));
first = false;
} else {
powered.add(term);
}
}
return powered;
}
/** Apply an exponent to a base term.
* @param base base term
* @param exponent exponent (may be null)
* @return term with exponent applied (same as {@code base} if exponent is null)
*/
private static List<PowerTerm> applyExponent(final List<PowerTerm> base, final Fraction exponent) {
if (exponent == null || exponent.equals(Fraction.ONE)) {
// return the base term itself
return base;
}
// combine exponent with existing ones, for example to handles compounds units like m/(kg.s²)³
final List<PowerTerm> powered = new ArrayList<>(base.size());
for (final PowerTerm term : base) {
final double poweredScale;
if (exponent.isInteger()) {
poweredScale = FastMath.pow(term.getScale(), exponent.getNumerator());
} else if (Fraction.ONE_HALF.equals(exponent)) {
poweredScale = FastMath.sqrt(term.getScale());
} else {
poweredScale = FastMath.pow(term.getScale(), exponent.doubleValue());
}
powered.add(new PowerTerm(poweredScale, term.getBase(), exponent.multiply(term.getExponent())));
}
return powered;
}
/** Accept a token.
* @param lexer lexer providing tokens
* @param expected expected token type
* @return accepted token
*/
private static Token accept(final Lexer lexer, final TokenType expected) {
final Token token = lexer.next();
if (!checkType(token, expected)) {
throw lexer.generateException();
}
return token;
}
/** Check a token exists and has proper type.
* @param token token to check
* @param expected expected token type
* @return true if token exists and has proper type
*/
private static boolean checkType(final Token token, final TokenType expected) {
return token != null && token.getType() == expected;
}
}