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