DecimalFormatter.java

package org.mozilla.javascript.dtoa;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;

public class DecimalFormatter {
    private static final double MAX_FIXED = 1E21;

    /**
     * The algorithm of Number.prototype.toExponential. If fractionDigits is < 0, then it indicates
     * the special case that the value was previously undefined, which calls for a different
     * precision for the calculation.
     */
    public static String toExponential(double v, int fractionDigits) {
        assert Double.isFinite(v);

        if (fractionDigits < 0) {
            // In this case, we are supposed to use the "shortest possible representation."
            // This is what our usual toString implementation does
            return DoubleFormatter.toDecimal(v).toString(Decimal.Mode.TO_EXPONENTIAL);
        }

        boolean negative = Math.signum(v) < 0;
        double val = v;
        if (negative) {
            val = Math.abs(v);
        }
        var bd = new BigDecimal(val, new MathContext(fractionDigits + 1, RoundingMode.HALF_UP));

        int exponent;
        if (bd.scale() >= 0) {
            exponent = bd.precision() - bd.scale() - 1;
        } else {
            // digits000
            exponent = bd.precision() + -bd.scale() - 1;
        }

        return toExponentialString(bd, exponent, fractionDigits, negative);
    }

    /** The algorithm of Number.prototype.toFixed(fractionDigits). */
    public static String toFixed(double v, int fractionDigits) {
        assert Double.isFinite(v);
        assert fractionDigits >= 0;
        boolean negative = Math.signum(v) < 0;
        double val = v;
        if (negative) {
            val = Math.abs(v);
        }
        if (val >= MAX_FIXED) {
            return DoubleFormatter.toString(v);
        }
        var bd = new BigDecimal(val, MathContext.UNLIMITED);
        if (bd.scale() > fractionDigits) {
            bd = bd.setScale(fractionDigits, RoundingMode.HALF_UP);
        }
        return toFixedString(bd, fractionDigits, negative);
    }

    /** The algorithm of Number.prototype.toPrecision() */
    public static String toPrecision(double v, int precision) {
        assert Double.isFinite(v);
        assert precision >= 1;
        boolean negative = Math.signum(v) < 0;
        double val;
        if (negative) {
            val = -v;
        } else {
            val = v;
        }
        var bd = new BigDecimal(val, new MathContext(precision, RoundingMode.HALF_UP));

        int scale = bd.scale();
        int numDigits = bd.precision();
        int exponent;
        int fractionDigits;
        if (scale >= 0) {
            if (scale >= numDigits) {
                // 0.digits000
                fractionDigits = precision;
            } else {
                // dig.its
                fractionDigits = precision - (numDigits - scale);
            }
            exponent = numDigits - scale - 1;
        } else {
            // digits000
            fractionDigits = 0;
            exponent = numDigits + -scale - 1;
        }

        if (exponent < -6 || exponent >= precision) {
            return toExponentialString(bd, exponent, precision - 1, negative);
        }
        return toFixedString(bd, fractionDigits, negative);
    }

    private static String toFixedString(BigDecimal d, int fractionDigits, boolean negative) {
        int scale = d.scale();
        // Turns out that, in UNLIMITED scale mode, BigDecimal will not
        // produce a negative scale.
        assert (scale >= 0);
        String digits = d.unscaledValue().toString();
        int numDigits = digits.length();
        if (scale == 0 && fractionDigits == 0) {
            if (negative) {
                return "-" + digits;
            }
            return digits;
        }

        // Room for digits, -, ., extra 0
        var b = new StringBuilder(numDigits * 2 + 3);
        if (negative) {
            b.append('-');
        }
        if (scale >= numDigits) {
            // 0.000digits000
            b.append("0.");
            fillZeroes(b, scale - numDigits);
            b.append(digits);
        } else {
            // dig.its000
            b.append(digits.substring(0, numDigits - scale));
            b.append('.');
            b.append(digits.substring(numDigits - scale));
        }
        fillZeroes(b, fractionDigits - scale);
        return b.toString();
    }

    private static String toExponentialString(
            BigDecimal d, int exponent, int fractionDigits, boolean negative) {
        String digits = d.unscaledValue().toString();
        int numDigits = digits.length();
        // Room for digits, ., -, e+000
        StringBuilder b = new StringBuilder(numDigits + fractionDigits + 7);

        if (negative) {
            b.append('-');
        }
        b.append(digits.charAt(0));
        if (numDigits > 1 || fractionDigits >= 1) {
            b.append('.');
            b.append(digits.substring(1));
            fillZeroes(b, fractionDigits - (numDigits - 1));
        }
        b.append('e');
        if (exponent >= 0) {
            b.append('+');
        }
        b.append(exponent);
        return b.toString();
    }

    private static void fillZeroes(StringBuilder b, int count) {
        for (int i = 0; i < count; i++) {
            b.append('0');
        }
    }
}