1   /* Copyright 2024-2025 The Johns Hopkins University Applied Physics Laboratory
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.files.iirv.terms.base;
18  
19  import org.hipparchus.util.ArithmeticUtils;
20  import org.hipparchus.util.FastMath;
21  import org.orekit.errors.OrekitIllegalArgumentException;
22  import org.orekit.errors.OrekitMessages;
23  import org.orekit.files.iirv.terms.IIRVTermUtils;
24  
25  import java.util.Locale;
26  import java.util.regex.Pattern;
27  
28  /**
29   * Term in an IIRV Vector representing a double value.
30   *
31   * @author Nick LaFarge
32   * @since 13.0
33   */
34  public class DoubleValuedIIRVTerm extends IIRVVectorTerm<Double> {
35  
36      /** Space pattern. */
37      private static final Pattern SPACE_PATTERN = Pattern.compile(" ");
38  
39      /**
40       * Number of characters before the end of the encoded String the decimal place is assumed to occur.
41       * <p>
42       * For example, for {@code nCharsAfterDecimalPlace=2} and {@code length=4}, then 12.34
43       * is encoded to "1234".
44       */
45      private final int nCharsAfterDecimalPlace;
46  
47      /** True if negative values are permitted, false if the value is positive. */
48      private final boolean isSigned;
49  
50      /**
51       * Constructs an IIRV Vector Term represented by a double. This representation is used for any numeric terms
52       * in the IIRV Vector that contain a decimal point.
53       *
54       * @param pattern                 Regular expression pattern that validates the term
55       * @param value                   Value of the term, expressed as a double
56       * @param length                  Length of the term, measured in number of characters in the String representation
57       * @param nCharsAfterDecimalPlace Number of characters before the end of the encoded String the decimal place is
58       *                                assumed to occur.
59       * @param isSigned                True if negative values are permitted, false if the value is positive
60       */
61      public DoubleValuedIIRVTerm(final String pattern, final double value, final int length, final int nCharsAfterDecimalPlace, final boolean isSigned) {
62          super(pattern, value, length);
63          this.nCharsAfterDecimalPlace = nCharsAfterDecimalPlace;
64          this.isSigned = isSigned;
65  
66          // Validate input data
67          validateString(toEncodedString(value));
68          validateOverflow(this.value());
69      }
70  
71      /**
72       * Constructs an IIRV Vector Term represented by a double from a given String. This representation is used for any
73       * numeric terms in the IIRV Vector that contain a decimal point.
74       *
75       * @param pattern                 Regular expression pattern that validates the term
76       * @param value                   Value of the term, expressed as a String
77       * @param length                  Length of the term, measured in number of characters in the String representation
78       * @param nCharsAfterDecimalPlace Number of characters before the end of {@code value} the decimal place is
79       *                                assumed to occur.
80       * @param isSigned                True if negative values are permitted, false if the value is positive
81       */
82      public DoubleValuedIIRVTerm(final String pattern, final String value, final int length, final int nCharsAfterDecimalPlace, final boolean isSigned) {
83          super(pattern, DoubleValuedIIRVTerm.computeValueFromString(value, nCharsAfterDecimalPlace), length);
84          this.nCharsAfterDecimalPlace = nCharsAfterDecimalPlace;
85          this.isSigned = isSigned;
86  
87          // Validate input data
88          validateString(value);
89          validateOverflow(this.value());
90      }
91  
92      /**
93       * Compute the double value of the term from a given String.
94       *
95       * @param value                   String value to convert to a double
96       * @param nCharsAfterDecimalPlace Number of characters before the end of {@code value} the decimal place is
97       *                                assumed to occur.
98       * @return Double value corresponding to the {@code value} String argument
99       */
100     public static double computeValueFromString(final String value, final int nCharsAfterDecimalPlace) {
101         try {
102             String intStr = value;
103 
104             // Remove spaces (for positive values)
105             intStr = SPACE_PATTERN.matcher(intStr).replaceAll("");
106 
107             // Return if there are no characters after the decimal place
108             if (nCharsAfterDecimalPlace == 0) {
109                 return Integer.parseInt(intStr);
110             }
111 
112             // Get the sign: negative if the first character is '-'
113             final int sign = intStr.charAt(0) == '-' ? -1 : 1;
114 
115             // Get value before/after the decimal place
116             final int beforeDecimalPlace = Integer.parseInt(intStr.substring(0, intStr.length() - nCharsAfterDecimalPlace));
117             final int afterDecimalPlace = Integer.parseInt(intStr.substring(intStr.length() - nCharsAfterDecimalPlace));
118 
119             // Turn into a double by dividing the n numbers that appear after the decimal places by 10^n
120             final double unsignedValue = FastMath.abs(beforeDecimalPlace) +
121                                          afterDecimalPlace / (double) ArithmeticUtils.pow(10, nCharsAfterDecimalPlace);
122 
123             // Return the resulting double with the correct sign
124             return sign * unsignedValue;
125         } catch (NumberFormatException e) {
126             throw new OrekitIllegalArgumentException(OrekitMessages.IIRV_INVALID_TERM_VALUE, value);
127         }
128     }
129 
130     /** {@inheritDoc} */
131     @Override
132     public String toEncodedString(final Double termValue) {
133         // Reserve one character for the sign (if applicable)
134         final int signAdjustedStringLength = isSigned ? length() - 1 : length();
135 
136         // Round the number to the specified number of decimal places
137         final double p = ArithmeticUtils.pow(10, nCharsAfterDecimalPlace); // beware, this *must* be a double
138         final double roundedNum = FastMath.round(termValue * p) / p;
139 
140         // Format the absolute value of the rounded number with specified integer and decimal lengths
141         String formattedStr = String.format(Locale.US,
142                                             "%0" + signAdjustedStringLength + "." + nCharsAfterDecimalPlace + "f",
143                                             FastMath.abs(roundedNum));
144 
145         // Remove the decimal point
146         formattedStr = formattedStr.replace(".", "");
147 
148         // Add leading zeros for cases where the number has less than the max number of integer digits
149         if (formattedStr.length() < signAdjustedStringLength) {
150             formattedStr = IIRVTermUtils.addPadding(formattedStr, '0', signAdjustedStringLength, true);
151         }
152 
153         // If the resulting String is all zero, then always use a positive sign to avoid encoding negative zero
154         final int numNonzeroCharacters = formattedStr.replace("0", "").length();
155 
156         // Sign character ("" for unsigned number)
157         String signCharacter = "";
158         if (isSigned) {
159             if (termValue >= 0 || numNonzeroCharacters == 0) {
160                 signCharacter = " ";
161             } else {
162                 signCharacter = "-";
163             }
164         }
165 
166         return signCharacter + formattedStr;
167     }
168 
169     /**
170      * Validate that there is a sufficient number of characters available in the encoded String representation to
171      * represent the value of the given double value.
172      *
173      * @param value Double value to check against the String encoding parameters
174      */
175     void validateOverflow(final double value) {
176         // Compute the number of characters, excluding the sign character and all characters after the decimal place
177         int n = length() - nCharsAfterDecimalPlace;
178         if (isSigned) {
179             n--;
180         }
181 
182         // If the value is greater than the maximum possible value, throw an error
183         final double maxPossibleValue = ArithmeticUtils.pow(10, n);
184         if (FastMath.abs(value) >= maxPossibleValue) {
185             throw new OrekitIllegalArgumentException(OrekitMessages.IIRV_VALUE_TOO_LARGE, FastMath.abs(value), maxPossibleValue);
186         }
187     }
188 }