PolynomialParser.java
/* Copyright 2002-2023 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.data;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.hipparchus.util.FastMath;
/**
* Parser for polynomials in IERS tables.
* <p>
* IERS conventions tables display polynomial parts using several different formats,
* like the following ones:
* </p>
* <ul>
* <li>125.04455501° − 6962890.5431″t + 7.4722″t² + 0.007702″t³ − 0.00005939″t⁴</li>
* <li>0.02438175 × t + 0.00000538691 × t²</li>
* <li>0''.014506 + 4612''.15739966t + 1''.39667721t^2 - 0''.00009344t^3 + 0''.00001882t^4</li>
* <li>-16616.99 + 2004191742.88 t - 427219.05 t^2 - 198620.54 t^3 - 46.05 t^4 + 5.98 t^5</li>
* </ul>
* <p>
* This class parses all these formats and returns the coefficients.
* </p>
*
* @author Luc Maisonobe
* @see SeriesTerm
* @see PoissonSeries
* @see BodiesElements
*/
public class PolynomialParser {
/** Unit for the coefficients. */
public enum Unit {
/** Radians angles. */
RADIANS(1.0),
/** Degrees angles. */
DEGREES(FastMath.toRadians(1.0)),
/** Arc-seconds angles. */
ARC_SECONDS(FastMath.toRadians(1.0 / 3600.0)),
/** Milli arc-seconds angles. */
MILLI_ARC_SECONDS(FastMath.toRadians(1.0 / 3600000.0)),
/** Micro arc-seconds angles. */
MICRO_ARC_SECONDS(FastMath.toRadians(1.0 / 3600000000.0)),
/** No units. */
NO_UNITS(1.0);
/** Multiplication factor to convert to corresponding SI unit. */
private final double factor;
/** Simple constructor.
* @param factor multiplication factor to convert to corresponding SI unit.
*/
Unit(final double factor) {
this.factor = factor;
}
/** Convert value from instance unit to corresponding SI unit.
* @param value value in instance unit
* @return value in SI unit
*/
public double toSI(final double value) {
return value * factor;
}
}
/** Constants for various characters that can be used as minus sign. */
private static final String[] MINUS = new String[] {
"-", // unicode HYPHEN-MINUS
"\u2212" // unicode MINUS SIGN
};
/** Constants for various characters that can be used as plus sign. */
private static final String[] PLUS = new String[] {
"+", // unicode PLUS SIGN
};
/** Constants for various characters that can be used as multiplication sign. */
private static final String[] MULTIPLICATION = new String[] {
"*", // unicode ASTERISK
"\u00d7" // unicode MULTIPLICATION SIGN
};
/** Constants for various characters that can be used as degree unit. */
private static final String[] DEGREES = new String[] {
"\u00b0", // unicode DEGREE SIGN
"\u25e6" // unicode WHITE BULLET
};
/** Constants for various characters that can be used as arc-seconds unit. */
private static final String[] ARC_SECONDS = new String[] {
"\u2033", // unicode DOUBLE_PRIME
"''", // doubled unicode APOSTROPHE
"\"" // unicode QUOTATION MARK
};
/** Constants for various characters that can be used as powers. */
private static final String[] SUPERSCRIPTS = new String[] {
"\u2070", // unicode SUPERSCRIPT ZERO
"\u00b9", // unicode SUPERSCRIPT ONE
"\u00b2", // unicode SUPERSCRIPT TWO
"\u00b3", // unicode SUPERSCRIPT THREE
"\u2074", // unicode SUPERSCRIPT FOUR
"\u2075", // unicode SUPERSCRIPT FIVE
"\u2076", // unicode SUPERSCRIPT SIX
"\u2077", // unicode SUPERSCRIPT SEVEN
"\u2078", // unicode SUPERSCRIPT EIGHT
"\u2079", // unicode SUPERSCRIPT NINE
};
/** Constants for various characters that can be used as powers. */
private static final String[] DIGITS = new String[] {
"0", // unicode DIGIT ZERO
"1", // unicode DIGIT ONE
"2", // unicode DIGIT TWO
"3", // unicode DIGIT THREE
"4", // unicode DIGIT FOUR
"5", // unicode DIGIT FIVE
"6", // unicode DIGIT SIX
"7", // unicode DIGIT SEVEN
"8", // unicode DIGIT EIGHT
"9", // unicode DIGIT NINE
};
/** Regular expression pattern for monomials. */
private final Pattern pattern;
/** Matcher for a definition. */
private Matcher matcher;
/** Start index for next search. */
private int next;
/** Last parsed coefficient. */
private double parsedCoefficient;
/** Last parsed power. */
private int parsedPower;
/** Unit to use if no unit found while parsing. */
private final Unit defaultUnit;
/** Simple constructor.
* @param freeVariable name of the free variable
* @param defaultUnit unit to use if no unit found while parsing
*/
public PolynomialParser(final char freeVariable, final Unit defaultUnit) {
this.defaultUnit = defaultUnit;
final String space = "\\p{Space}*";
final String unit = either(quote(merge(DEGREES, ARC_SECONDS)));
final String sign = either(quote(merge(MINUS, PLUS)));
final String integer = "\\p{Digit}+";
final String exp = "[eE]" + zeroOrOne(sign, false) + integer;
final String fractional = "\\.\\p{Digit}*" + zeroOrOne(exp, false);
final String embeddedUnit = group(integer, true) +
group(unit, true) +
group(fractional, true);
final String appendedUnit = group(either(group(integer + zeroOrOne(fractional, false), false),
group(fractional, false)),
true) +
zeroOrOne(unit, true);
final String caretPower = "\\^" + any(quote(DIGITS));
final String superscripts = any(quote(SUPERSCRIPTS));
final String power = zeroOrOne(either(quote(MULTIPLICATION)), false) +
space + freeVariable +
either(caretPower, superscripts);
// the capturing groups of the following pattern are:
// group 1: sign
//
// when unit is embedded within the coefficient, as in 1''.39667721:
// group 2: integer part of the coefficient
// group 3: unit
// group 4: fractional part of the coefficient
//
// when unit is appended after the coefficient, as in 125.04455501°
// group 5: complete coefficient
// group 6: unit
//
// group 7: complete power, including free variable, for example "× τ^4" or "× τ⁴"
//
// when caret and regular digits are used, for example τ^4
// group 8: only exponent part of the power
//
// when superscripts are used, for example τ⁴
// group 9: only exponent part of the power
pattern = Pattern.compile(space + zeroOrOne(sign, true) + space +
either(group(embeddedUnit, false), group(appendedUnit, false)) +
space + zeroOrOne(power, true));
}
/** Merge two lists of markers.
* @param markers1 first list
* @param markers2 second list
* @return merged list
*/
private String[] merge(final String[] markers1, final String[] markers2) {
final String[] merged = new String[markers1.length + markers2.length];
System.arraycopy(markers1, 0, merged, 0, markers1.length);
System.arraycopy(markers2, 0, merged, markers1.length, markers2.length);
return merged;
}
/** Quote a list of markers.
* @param markers markers to quote
* @return quoted markers
*/
private String[] quote(final String... markers) {
final String[] quoted = new String[markers.length];
for (int i = 0; i < markers.length; ++i) {
quoted[i] = "\\Q" + markers[i] + "\\E";
}
return quoted;
}
/** Create a regular expression for a group.
* @param r raw regular expression to group
* @param capturing if true, the group is a capturing group
* @return group expression
*/
private String group(final CharSequence r, final boolean capturing) {
return (capturing ? "(" : "(?:") + r + ")";
}
/** Create a regular expression for alternative markers.
* @param markers allowed markers
* @return regular expression recognizing one marker from the list
* (the result is a non-capturing group)
*/
private String either(final CharSequence... markers) {
final StringBuilder builder = new StringBuilder();
for (final CharSequence marker : markers) {
if (builder.length() > 0) {
builder.append('|');
}
builder.append(marker);
}
return group(builder, false);
}
/** Create a regular expression for a repeatable part.
* @param markers allowed markers
* @return regular expression recognizing any number of markers from the list
* (the result is a capturing group)
*/
private String any(final CharSequence... markers) {
return group(either(markers) + "*", true);
}
/** Create a regular expression for an optional part.
* @param r optional raw regular expression
* @param capturing if true, wrap the optional part in a capturing group
* @return group expression
*/
private String zeroOrOne(final CharSequence r, final boolean capturing) {
final String optional = group(r, false) + "?";
return capturing ? group(optional, true) : optional;
}
/** Check if a substring starts with one marker from an array.
* @param s string containing the substring to check
* @param offset offset at which substring starts
* @param markers markes to check for
* @return index of the start marker, or negative if string does not start
* with one of the markers
*/
private int startMarker(final String s, final int offset, final String[] markers) {
for (int i = 0; i < markers.length; ++i) {
if (s.startsWith(markers[i], offset)) {
return i;
}
}
return -1;
}
/** Parse a polynomial expression.
* @param expression polynomial expression to parse
* @return polynomial coefficients array in increasing degree order, or
* null if expression is not a recognized polynomial
*/
public double[] parse(final String expression) {
final Map<Integer, Double> coefficients = new HashMap<Integer, Double>();
int maxDegree = -1;
matcher = pattern.matcher(expression);
next = 0;
while (parseMonomial(expression)) {
maxDegree = FastMath.max(maxDegree, parsedPower);
coefficients.put(parsedPower, parsedCoefficient);
}
if (maxDegree < 0) {
return null;
}
final double[] parsedPolynomial = new double[maxDegree + 1];
for (Map.Entry<Integer, Double> entry : coefficients.entrySet()) {
parsedPolynomial[entry.getKey()] = entry.getValue();
}
return parsedPolynomial;
}
/** Parse next monomial.
* @param expression polynomial expression to parse
* @return true if a monomial has been parsed
*/
private boolean parseMonomial(final String expression) {
// groups indices
final int signGroup = 1;
final int coeffIntGroup = 2;
final int embeddedUnitGroup = 3;
final int coeffFracGroup = 4;
final int coeffGroup = 5;
final int appendedUnitGroup = 6;
final int powerGroup = 7;
final int caretGroup = 8;
final int superScriptGroup = 9;
// advance matcher
matcher.region(next, matcher.regionEnd());
if (matcher.lookingAt()) {
// parse coefficient, with proper sign and unit
final double sign = startMarker(expression, matcher.start(signGroup), MINUS) >= 0 ? -1 : 1;
final String coeff;
final Unit unit;
if (matcher.start(embeddedUnitGroup) >= 0) {
// the unit is embedded between coefficient integer and fractional parts
coeff = matcher.group(coeffIntGroup) + matcher.group(coeffFracGroup);
if (startMarker(expression, matcher.start(embeddedUnitGroup), DEGREES) >= 0) {
unit = Unit.DEGREES;
} else {
// as we recognize only degrees and arc-seconds as explicit settings in the expression
// and as we know here the unit as been set, it must be arc seconds
unit = Unit.ARC_SECONDS;
}
} else {
// the unit is either after the coefficient or not present at all
coeff = matcher.group(coeffGroup);
if (startMarker(expression, matcher.start(appendedUnitGroup), DEGREES) >= 0) {
unit = Unit.DEGREES;
} else if (startMarker(expression, matcher.start(appendedUnitGroup), ARC_SECONDS) >= 0) {
unit = Unit.ARC_SECONDS;
} else {
unit = defaultUnit;
}
}
parsedCoefficient = sign * unit.toSI(Double.parseDouble(coeff));
if (matcher.start(powerGroup) < matcher.end(powerGroup)) {
// this a power 1 or more term
if (matcher.start(caretGroup) < matcher.end(caretGroup)) {
// general case: x^1234
parsedPower = 0;
for (int index = matcher.start(caretGroup); index < matcher.end(caretGroup); ++index) {
parsedPower = parsedPower * 10 + startMarker(expression, index, DIGITS);
}
} else if (matcher.start(superScriptGroup) < matcher.end(superScriptGroup)) {
// general case: x¹²³⁴
parsedPower = 0;
for (int index = matcher.start(superScriptGroup); index < matcher.end(superScriptGroup); ++index) {
parsedPower = parsedPower * 10 + startMarker(expression, index, SUPERSCRIPTS);
}
} else {
// special case: x is the same term as either x^1 or x¹
parsedPower = 1;
}
} else {
// this is a constant term
parsedPower = 0;
}
next = matcher.end();
return true;
} else {
parsedCoefficient = Double.NaN;
parsedPower = -1;
return false;
}
}
}