DecimalFormatUtil.java

/*
 * Copyright (c) 2020, Stein Eldar Johnsen
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF 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 net.morimekta.strings.internal;

import java.text.DecimalFormat;

import static java.lang.Integer.min;
import static java.lang.Math.abs;
import static java.lang.Math.log10;
import static java.text.DecimalFormatSymbols.getInstance;
import static java.util.Locale.US;

/**
 * A utility (project - private) to handle decimal formatting
 * of floats and doubles to the maximum precision that keeps
 * consistent to string + parse values.
 */
public final class DecimalFormatUtil {
    private static final DecimalFormat[] FLOAT_DECIMALS = floatFormatters(7, 4);
    private static final DecimalFormat[] DOUBLE_DECIMALS = floatFormatters(16, 8);
    private static final DecimalFormat SCIENTIFIC_DOUBLE_FORMATTER = new DecimalFormat("0.###############E0", getInstance(US));
    private static final DecimalFormat SCIENTIFIC_FLOAT_FORMATTER  = new DecimalFormat("0.######E0", getInstance(US));

    public static String formatFloat(float f) {
        int lg = precision(f);
        if (lg < -4 || lg > 6) return SCIENTIFIC_FLOAT_FORMATTER.format(f);

        final long l = (long) f;
        if (f == (float) l) {
            // actually an integer or long value.
            return Long.toString(l);
        } else {
            return FLOAT_DECIMALS[6 - lg].format(f);
        }
    }

    public static String formatDouble(double d) {
        int lg = precision(d);
        if (lg < -8 || lg > 14) return SCIENTIFIC_DOUBLE_FORMATTER.format(d);

        long l = (long) d;
        if (d == (double) l) {
            // actually an integer or long value.
            return Long.toString(l);
        } else {
            return DOUBLE_DECIMALS[15 - lg].format(d);
        }
    }

    private static int precision(double d) {
        int lg = (int) log10(abs(d));
        if (-1 < d && d < 1) --lg;
        return lg;
    }

    private static DecimalFormat[] floatFormatters(int precision, int margin) {
        int num = precision + margin;
        DecimalFormat[] formats = new DecimalFormat[num];
        for (int i = 0; i < num; ++i) {
            int decimals = min(i, precision);
            int nulls = i - decimals;

            StringBuilder format = new StringBuilder();
            if (nulls > 0) {
                format.append("0.").append("0".repeat(nulls));
            } else {
                format.append("#.");
            }
            format.append("#".repeat(decimals));
            formats[i] = new DecimalFormat(format.toString(), getInstance(US));
        }
        return formats;
    }
}