1 /* Copyright 2022-2025 Thales Alenia Space
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.utils.formatting;
18
19 import org.hipparchus.util.FastMath;
20 import org.orekit.errors.OrekitInternalError;
21
22 import java.io.IOException;
23
24 /** Formatter for double numbers with low overhead.
25 * <p>
26 * This class is intended to be used when formatting large amounts of data with
27 * fixed formats like, for example, large ephemeris or measurement files.
28 * </p>
29 * <p>
30 * Building the formatter is done once, and the formatter
31 * {@link #appendTo(Appendable, double)} or {@link #toString(double)} methods can
32 * be called hundreds of thousands of times, without incurring the overhead that
33 * would occur with {@code String.format()}. Some tests showed this formatter is
34 * about 5 times faster than {@code String.format()} with
35 * {@code %{width}.{%precision}f} format.
36 * </p>
37 * <p>
38 * Instances of this class are immutable
39 * </p>
40 * @author Luc Maisonobe
41 * @since 13.0.3
42 */
43 public class FastDoubleFormatter {
44
45 /** Scaling array for fractional part. */
46 private static final long[] SCALING = new long[19];
47
48 static {
49 SCALING[0] = 1L;
50 for (int i = 1; i < SCALING.length; ++i) {
51 SCALING[i] = 10L * SCALING[i - 1];
52 }
53 }
54
55 /** Number of characters to output. */
56 private final int width;
57
58 /** Precision. */
59 private final int precision;
60
61 /** Scaling. */
62 private final long scaling;
63
64 /** Formatter for integer part. */
65 private final FastLongFormatter beforeFormatter;
66
67 /** Formatter for fractional part. */
68 private final FastLongFormatter afterFormatter;
69
70 /** Simple constructor.
71 * <p>
72 * This constructor is equivalent to {@link java.util.Formatter Formatter}
73 * float format {@code %{width}.{precision}f}
74 * </p>
75 * @param width number of characters to output
76 * @param precision number of decimal precision
77 */
78 public FastDoubleFormatter(final int width, final int precision) {
79 this.width = width;
80 this.precision = precision;
81 this.scaling = SCALING[precision];
82 this.beforeFormatter = precision == 0 ?
83 new FastLongFormatter(width, false) :
84 new FastLongFormatter(width - precision - 1, false);
85 this.afterFormatter = new FastLongFormatter(precision, true);
86 }
87
88 /** Get the width.
89 * @return width
90 */
91 public int getWidth() {
92 return width;
93 }
94
95 /** Get the precision.
96 * @return precision
97 */
98 public int getPrecision() {
99 return precision;
100 }
101
102 /** Append one formatted value to an {@code Appendable}.
103 * @param appendable to append value to
104 * @param value value to format
105 * @exception IOException if an I/O error occurs
106 */
107 public void appendTo(final Appendable appendable, final double value) throws IOException {
108
109 if (Double.isNaN(value)) {
110 // special case for NaN
111 for (int i = 0; i < width - 3; ++i) {
112 appendable.append(' ');
113 }
114 appendable.append("NaN");
115 } else {
116
117 if (Double.isInfinite(value)) {
118 // special case for infinities
119 if (FastMath.copySign(1.0, value) < 0) {
120 for (int i = 0; i < width - 9; ++i) {
121 appendable.append(' ');
122 }
123 appendable.append("-Infinity");
124 } else {
125 for (int i = 0; i < width - 8; ++i) {
126 appendable.append(' ');
127 }
128 appendable.append("Infinity");
129 }
130 } else {
131
132 // regular number
133 final double abs = FastMath.abs(value);
134 double before = FastMath.floor(abs);
135 long after = FastMath.round((abs - before) * scaling);
136
137 if (after >= scaling) {
138 // we have to round up to the next integer
139 before += 1;
140 after = 0L;
141 }
142
143 // convert to string
144 final double sign = FastMath.copySign(1.0, value);
145 if (sign < 0 && before == 0.0) {
146 // special case for negative values between -0.0 and -1.0
147 for (int i = 0; i < beforeFormatter.getWidth() - 2; ++i) {
148 appendable.append(' ');
149 }
150 appendable.append("-0");
151 } else {
152 // regular case
153 beforeFormatter.appendTo(appendable, FastMath.round(sign * before));
154 }
155
156 // fractional part
157 if (scaling > 1) {
158 appendable.append('.');
159 afterFormatter.appendTo(appendable, after);
160 }
161
162 }
163 }
164
165 }
166
167 /** Format one value.
168 * @param value value to format
169 * @return formatted string
170 */
171 public String toString(final double value) {
172 try {
173 final StringBuilder builder = new StringBuilder();
174 appendTo(builder, value);
175 return builder.toString();
176 } catch (IOException ioe) {
177 // this should never happen
178 throw new OrekitInternalError(ioe);
179 }
180 }
181
182 }