FastLongFormatter.java

/* Copyright 2022-2025 Thales Alenia Space
 * Licensed to CS GROUP (CS) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * CS licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.orekit.utils.formatting;

import org.hipparchus.util.FastMath;
import org.orekit.errors.OrekitInternalError;

import java.io.IOException;
import java.util.Arrays;

/** Formatter for long integers with low overhead.
 * <p>
 * This class is intended to be used when formatting large amounts of data with
 * fixed formats like, for example, large ephemeris or measurement files.
 * </p>
 * <p>
 * Building the formatter is done once, and the formatter
 * {@link #appendTo(Appendable, long)} or {@link #toString(long)} methods can be
 * called hundreds of thousands of times, without incurring the overhead that
 * would occur with {@code String.format()}. Some tests showed this formatter is
 * about 10 times faster than {@code String.format()} with {@code %{width}d} format.
 * </p>
 * <p>
 * Instances of this class are immutable
 * </p>
 * @author Luc Maisonobe
 * @since 13.0.3
 */
public class FastLongFormatter {

    /** Digits. */
    private static final char[] DIGITS = {
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    };

    /** Number of characters to output. */
    private final int width;

    /** Zero padding indicator. */
    private final boolean zeroPadding;

    /** Size of the conversion array. */
    private final int size;

    /** Simple constructor.
     * <p>
     * This constructor is equivalent to either {@link java.util.Formatter Formatter}
     * integer format {@code %{width}d} or {@code %0{width}d}
     * </p>
     * @param width       number of characters to output
     * @param zeroPadding if true, the result is left padded with '0' until it matches width
     */
    public FastLongFormatter(final int width, final boolean zeroPadding) {
        this.width       = width;
        this.zeroPadding = zeroPadding;
        this.size        = FastMath.max(width, 20);
    }

    /** Get the width.
     * @return width
     */
    public int getWidth() {
        return width;
    }

    /** Check if left padding uses '0' characters.
     * @return true if left padding uses '0' characters
     */
    public boolean hasZeroPadding() {
        return zeroPadding;
    }

    /** Append one formatted value to an {@code Appendable}.
     * @param appendable to append value to
     * @param value value to format
     * @exception IOException if an I/O error occurs
     */
    public void appendTo(final Appendable appendable, final long value) throws IOException {

        // initialize conversion loop
        final char[] digits = new char[size];
        int index = 0;
        long remaining;
        if (value == Long.MIN_VALUE) {
            // special case for value -9223372036854775808L that has no representable opposite
            digits[0] = '8';
            index     = 1;
            remaining = 922337203685477580L;
        } else {
            remaining = FastMath.abs(value);
        }

        // convert to decimal string
        do {
            digits[index++] = DIGITS[(int) (remaining % 10L)];
            remaining /= 10L;
        } while (remaining > 0L);

        // manage sign and padding
        if (zeroPadding) {
            if (value < 0L) {
                // zero padding a negative value occurs between the minus sign and the most significant digit
                if (index < width - 1) {
                    Arrays.fill(digits, index, width - 1, '0');
                    index = width - 1;
                }
                digits[index++] = '-';
            }
            else {
                if (index < width) {
                    Arrays.fill(digits, index, width, '0');
                    index = width;
                }
            }
        } else {
            if (value < 0L) {
                // space padding a negative value is before minus sign
                digits[index++] = '-';
            }
            if (index < width) {
                Arrays.fill(digits, index, width, ' ');
                index = width;
            }
        }

        // fill up string
        while (index > 0) {
            appendable.append(digits[--index]);
        }

    }

    /** Format one value.
     * @param value value to format
     * @return formatted string
     */
    public String toString(final long value) {
        try {
            final StringBuilder builder = new StringBuilder();
            appendTo(builder, value);
            return builder.toString();
        } catch (IOException ioe) {
            // this should never happen
            throw new OrekitInternalError(ioe);
        }
    }

}