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 }