DoubleValuedIIRVTerm.java

/* Copyright 2024-2025 The Johns Hopkins University Applied Physics Laboratory
 * 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.files.iirv.terms.base;

import org.hipparchus.util.ArithmeticUtils;
import org.hipparchus.util.FastMath;
import org.orekit.errors.OrekitIllegalArgumentException;
import org.orekit.errors.OrekitMessages;
import org.orekit.files.iirv.terms.IIRVTermUtils;

import java.util.Locale;
import java.util.regex.Pattern;

/**
 * Term in an IIRV Vector representing a double value.
 *
 * @author Nick LaFarge
 * @since 13.0
 */
public class DoubleValuedIIRVTerm extends IIRVVectorTerm<Double> {

    /** Space pattern. */
    private static final Pattern SPACE_PATTERN = Pattern.compile(" ");

    /**
     * Number of characters before the end of the encoded String the decimal place is assumed to occur.
     * <p>
     * For example, for {@code nCharsAfterDecimalPlace=2} and {@code length=4}, then 12.34
     * is encoded to "1234".
     */
    private final int nCharsAfterDecimalPlace;

    /** True if negative values are permitted, false if the value is positive. */
    private final boolean isSigned;

    /**
     * Constructs an IIRV Vector Term represented by a double. This representation is used for any numeric terms
     * in the IIRV Vector that contain a decimal point.
     *
     * @param pattern                 Regular expression pattern that validates the term
     * @param value                   Value of the term, expressed as a double
     * @param length                  Length of the term, measured in number of characters in the String representation
     * @param nCharsAfterDecimalPlace Number of characters before the end of the encoded String the decimal place is
     *                                assumed to occur.
     * @param isSigned                True if negative values are permitted, false if the value is positive
     */
    public DoubleValuedIIRVTerm(final String pattern, final double value, final int length, final int nCharsAfterDecimalPlace, final boolean isSigned) {
        super(pattern, value, length);
        this.nCharsAfterDecimalPlace = nCharsAfterDecimalPlace;
        this.isSigned = isSigned;

        // Validate input data
        validateString(toEncodedString(value));
        validateOverflow(this.value());
    }

    /**
     * Constructs an IIRV Vector Term represented by a double from a given String. This representation is used for any
     * numeric terms in the IIRV Vector that contain a decimal point.
     *
     * @param pattern                 Regular expression pattern that validates the term
     * @param value                   Value of the term, expressed as a String
     * @param length                  Length of the term, measured in number of characters in the String representation
     * @param nCharsAfterDecimalPlace Number of characters before the end of {@code value} the decimal place is
     *                                assumed to occur.
     * @param isSigned                True if negative values are permitted, false if the value is positive
     */
    public DoubleValuedIIRVTerm(final String pattern, final String value, final int length, final int nCharsAfterDecimalPlace, final boolean isSigned) {
        super(pattern, DoubleValuedIIRVTerm.computeValueFromString(value, nCharsAfterDecimalPlace), length);
        this.nCharsAfterDecimalPlace = nCharsAfterDecimalPlace;
        this.isSigned = isSigned;

        // Validate input data
        validateString(value);
        validateOverflow(this.value());
    }

    /**
     * Compute the double value of the term from a given String.
     *
     * @param value                   String value to convert to a double
     * @param nCharsAfterDecimalPlace Number of characters before the end of {@code value} the decimal place is
     *                                assumed to occur.
     * @return Double value corresponding to the {@code value} String argument
     */
    public static double computeValueFromString(final String value, final int nCharsAfterDecimalPlace) {
        try {
            String intStr = value;

            // Remove spaces (for positive values)
            intStr = SPACE_PATTERN.matcher(intStr).replaceAll("");

            // Return if there are no characters after the decimal place
            if (nCharsAfterDecimalPlace == 0) {
                return Integer.parseInt(intStr);
            }

            // Get the sign: negative if the first character is '-'
            final int sign = intStr.charAt(0) == '-' ? -1 : 1;

            // Get value before/after the decimal place
            final int beforeDecimalPlace = Integer.parseInt(intStr.substring(0, intStr.length() - nCharsAfterDecimalPlace));
            final int afterDecimalPlace = Integer.parseInt(intStr.substring(intStr.length() - nCharsAfterDecimalPlace));

            // Turn into a double by dividing the n numbers that appear after the decimal places by 10^n
            final double unsignedValue = FastMath.abs(beforeDecimalPlace) +
                                         afterDecimalPlace / (double) ArithmeticUtils.pow(10, nCharsAfterDecimalPlace);

            // Return the resulting double with the correct sign
            return sign * unsignedValue;
        } catch (NumberFormatException e) {
            throw new OrekitIllegalArgumentException(OrekitMessages.IIRV_INVALID_TERM_VALUE, value);
        }
    }

    /** {@inheritDoc} */
    @Override
    public String toEncodedString(final Double termValue) {
        // Reserve one character for the sign (if applicable)
        final int signAdjustedStringLength = isSigned ? length() - 1 : length();

        // Round the number to the specified number of decimal places
        final double p = ArithmeticUtils.pow(10, nCharsAfterDecimalPlace); // beware, this *must* be a double
        final double roundedNum = FastMath.round(termValue * p) / p;

        // Format the absolute value of the rounded number with specified integer and decimal lengths
        String formattedStr = String.format(Locale.US,
                                            "%0" + signAdjustedStringLength + "." + nCharsAfterDecimalPlace + "f",
                                            FastMath.abs(roundedNum));

        // Remove the decimal point
        formattedStr = formattedStr.replace(".", "");

        // Add leading zeros for cases where the number has less than the max number of integer digits
        if (formattedStr.length() < signAdjustedStringLength) {
            formattedStr = IIRVTermUtils.addPadding(formattedStr, '0', signAdjustedStringLength, true);
        }

        // If the resulting String is all zero, then always use a positive sign to avoid encoding negative zero
        final int numNonzeroCharacters = formattedStr.replace("0", "").length();

        // Sign character ("" for unsigned number)
        String signCharacter = "";
        if (isSigned) {
            if (termValue >= 0 || numNonzeroCharacters == 0) {
                signCharacter = " ";
            } else {
                signCharacter = "-";
            }
        }

        return signCharacter + formattedStr;
    }

    /**
     * Validate that there is a sufficient number of characters available in the encoded String representation to
     * represent the value of the given double value.
     *
     * @param value Double value to check against the String encoding parameters
     */
    void validateOverflow(final double value) {
        // Compute the number of characters, excluding the sign character and all characters after the decimal place
        int n = length() - nCharsAfterDecimalPlace;
        if (isSigned) {
            n--;
        }

        // If the value is greater than the maximum possible value, throw an error
        final double maxPossibleValue = ArithmeticUtils.pow(10, n);
        if (FastMath.abs(value) >= maxPossibleValue) {
            throw new OrekitIllegalArgumentException(OrekitMessages.IIRV_VALUE_TOO_LARGE, FastMath.abs(value), maxPossibleValue);
        }
    }
}