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  import java.util.Arrays;
24  
25  /** Formatter for long integers with low overhead.
26   * <p>
27   * This class is intended to be used when formatting large amounts of data with
28   * fixed formats like, for example, large ephemeris or measurement files.
29   * </p>
30   * <p>
31   * Building the formatter is done once, and the formatter
32   * {@link #appendTo(Appendable, long)} or {@link #toString(long)} methods can be
33   * called hundreds of thousands of times, without incurring the overhead that
34   * would occur with {@code String.format()}. Some tests showed this formatter is
35   * about 10 times faster than {@code String.format()} with {@code %{width}d} 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 FastLongFormatter {
44  
45      /** Digits. */
46      private static final char[] DIGITS = {
47          '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
48      };
49  
50      /** Number of characters to output. */
51      private final int width;
52  
53      /** Zero padding indicator. */
54      private final boolean zeroPadding;
55  
56      /** Size of the conversion array. */
57      private final int size;
58  
59      /** Simple constructor.
60       * <p>
61       * This constructor is equivalent to either {@link java.util.Formatter Formatter}
62       * integer format {@code %{width}d} or {@code %0{width}d}
63       * </p>
64       * @param width       number of characters to output
65       * @param zeroPadding if true, the result is left padded with '0' until it matches width
66       */
67      public FastLongFormatter(final int width, final boolean zeroPadding) {
68          this.width       = width;
69          this.zeroPadding = zeroPadding;
70          this.size        = FastMath.max(width, 20);
71      }
72  
73      /** Get the width.
74       * @return width
75       */
76      public int getWidth() {
77          return width;
78      }
79  
80      /** Check if left padding uses '0' characters.
81       * @return true if left padding uses '0' characters
82       */
83      public boolean hasZeroPadding() {
84          return zeroPadding;
85      }
86  
87      /** Append one formatted value to an {@code Appendable}.
88       * @param appendable to append value to
89       * @param value value to format
90       * @exception IOException if an I/O error occurs
91       */
92      public void appendTo(final Appendable appendable, final long value) throws IOException {
93  
94          // initialize conversion loop
95          final char[] digits = new char[size];
96          int index = 0;
97          long remaining;
98          if (value == Long.MIN_VALUE) {
99              // special case for value -9223372036854775808L that has no representable opposite
100             digits[0] = '8';
101             index     = 1;
102             remaining = 922337203685477580L;
103         } else {
104             remaining = FastMath.abs(value);
105         }
106 
107         // convert to decimal string
108         do {
109             digits[index++] = DIGITS[(int) (remaining % 10L)];
110             remaining /= 10L;
111         } while (remaining > 0L);
112 
113         // manage sign and padding
114         if (zeroPadding) {
115             if (value < 0L) {
116                 // zero padding a negative value occurs between the minus sign and the most significant digit
117                 if (index < width - 1) {
118                     Arrays.fill(digits, index, width - 1, '0');
119                     index = width - 1;
120                 }
121                 digits[index++] = '-';
122             }
123             else {
124                 if (index < width) {
125                     Arrays.fill(digits, index, width, '0');
126                     index = width;
127                 }
128             }
129         } else {
130             if (value < 0L) {
131                 // space padding a negative value is before minus sign
132                 digits[index++] = '-';
133             }
134             if (index < width) {
135                 Arrays.fill(digits, index, width, ' ');
136                 index = width;
137             }
138         }
139 
140         // fill up string
141         while (index > 0) {
142             appendable.append(digits[--index]);
143         }
144 
145     }
146 
147     /** Format one value.
148      * @param value value to format
149      * @return formatted string
150      */
151     public String toString(final long value) {
152         try {
153             final StringBuilder builder = new StringBuilder();
154             appendTo(builder, value);
155             return builder.toString();
156         } catch (IOException ioe) {
157             // this should never happen
158             throw new OrekitInternalError(ioe);
159         }
160     }
161 
162 }