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 }