PolynomialParser.java

/* Copyright 2002-2022 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;

        }

    }

}