blob: 7bfe63d0507f01749142abd7a74cd2852ec47eab [file]
/*
* Copyright (c) 2023, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.internal.math;
/*
* This class provides support for the 'e', 'f' and 'g' conversions on double
* values with sign bit 0.
* It is worth noting that float values are converted to double values _before_
* control reaches code in this class.
*
* It delegates the conversion to decimal to class DoubleToDecimal to get
* the decimal d selected by Double.toString(double) as a pair of integers
* f and e meeting d = f 10^e.
* It then rounds d to the appropriate number of digits, as per specification,
* and extracts the digits of both the significand and, where required, the
* exponent of the rounded value.
*
* Further processing like padding, sign, grouping, localization, etc., is the
* responsibility of the caller.
*/
public final class FormattedFPDecimal {
public static final char SCIENTIFIC = 'e';
public static final char PLAIN = 'f';
public static final char GENERAL = 'g';
private long f;
private int e; // normalized to 0 when f = 0
private int n;
private char[] digits; // ... and often the decimal separator as well
private char[] exp; // [+-][e]ee, that is, sign and minimum 2 digits
private FormattedFPDecimal() {
}
public static FormattedFPDecimal valueOf(double v, int prec, char form) {
FormattedFPDecimal fd = split(v);
return switch (form) {
case SCIENTIFIC -> fd.scientific(prec);
case PLAIN -> fd.plain(prec);
case GENERAL -> fd.general(prec);
default -> throw new IllegalArgumentException(
String.format("unsupported form '%c'", form)
);
};
}
private static FormattedFPDecimal split(double v) {
FormattedFPDecimal fd = new FormattedFPDecimal();
DoubleToDecimal.split(v, fd);
return fd;
}
/**
* Returns a FormattedFPDecimal with the appropriate precision for
* {@link Double#toString(double)}.
*
* @see java.math.BigDecimal#valueOf(double)
*/
public static FormattedFPDecimal valueForDoubleToString(double v) {
final FormattedFPDecimal fd = split(v);
final int expR = fd.getExponentRounded();
// Adjust precision, following rules for Double.toString. There is
// always at least one digit and some cases require an extra one to
// force a digit after the decimal. No additional rounding is performed;
// no significant trailing digits are removed.
final int targetPrec =
// No extra trailing digit needed
(-3 <= expR && expR < 0) ? 1
// Keep digits to left of decimal, plus leave a trailing zero
: (0 <= expR && expR < 7) ? expR + 2 :
// Otherwise, require at least 2 digits, to include trailing
// digit when there is a single digit
2;
long s = fd.f;
int prec = fd.n;
if (prec < targetPrec) {
// Add zeros needed to reach target precision
final int addZeros = targetPrec - prec;
s *= MathUtils.pow10(addZeros); // addZeros will be at most 8
prec = targetPrec;
} else {
// Remove trailing zeros to try to reach target precision
while (prec > targetPrec && s % 10 == 0) {
s = s / 10;
prec--;
}
}
// Calculate new e based on updated precision
final int eNew = expR - prec + 1; // expR is defined as prec + e - 1
fd.set(s, eNew, prec);
return fd;
}
public long getSignificand() {
return f;
}
public int getPrecision() {
return n;
}
public int getExp() {
return e;
}
public void set(long f, int e, int n) {
/* Initially, n = 0 if f = 0, and 10^{n-1} <= f < 10^n if f != 0 */
this.f = f;
this.e = e;
this.n = n;
}
public char[] getExponent() {
return exp;
}
public char[] getMantissa() {
return digits;
}
public int getExponentRounded() {
return n + e - 1;
}
private FormattedFPDecimal plain(int prec) {
/*
* Rounding d = f 10^e to prec digits in plain mode means the same
* as rounding it to the p = n + e + prec most significand digits of d,
* with the understanding that p < 0 cuts off all its digits.
*/
round(n + e + (long) prec); // n + e is well inside the int range
return plainChars();
}
private FormattedFPDecimal plainChars() {
if (e >= 0) {
plainCharsPureInteger();
} else if (n + e > 0) {
plainCharsMixed();
} else {
plainCharsPureFraction();
}
return this;
}
private void plainCharsPureInteger() {
digits = new char[n + e];
fillWithZeros(n, n + e);
fillWithDigits(f, 0, n);
}
private void plainCharsMixed() {
digits = new char[n + 1];
long x = fillWithDigits(f, n + 1 + e, n + 1);
digits[n + e] = '.';
fillWithDigits(x, 0, n + e);
}
private void plainCharsPureFraction() {
digits = new char[2 - e];
long x = f;
fillWithDigits(x, 2 - e - n, 2 - e);
fillWithZeros(0, 2 - e - n);
digits[1] = '.';
}
private FormattedFPDecimal scientific(int prec) {
/*
* Rounding d = f 10^e to prec digits in scientific mode means the same
* as rounding it to the p = prec + 1 most significand digits of d.
*/
round(prec + 1L);
return scientificChars(prec);
}
private FormattedFPDecimal scientificChars(int prec) {
if (prec != 0) {
scientificCharsWithFraction();
} else {
scientificCharsNoFraction();
}
expChars();
return this;
}
private void scientificCharsWithFraction() {
digits = new char[1 + n]; // room for leading digit and for '.'
long x = fillWithDigits(f, 2, 1 + n);
digits[1] = '.';
digits[0] = toDigit(x);
}
private void scientificCharsNoFraction() {
digits = new char[1];
digits[0] = toDigit(f);
}
private FormattedFPDecimal general(int prec) {
/*
* Rounding d = f 10^e to prec digits in general mode means the same
* as rounding it to the p = prec most significand digits of d, and then
* deciding whether to format it in plain or scientific mode, depending
* on the rounded value.
*/
round(prec);
int er = getExponentRounded();
if (-4 <= er && er < prec) {
plainChars();
} else {
scientificChars(prec - 1);
}
return this;
}
private void expChars() {
int er = getExponentRounded();
int aer = Math.abs(er);
exp = new char[aer >= 100 ? 4 : 3];
int q;
if (aer >= 100) {
q = aer / 10;
exp[3] = toDigit(aer - 10 * q);
aer = q;
}
q = aer / 10;
exp[2] = toDigit(aer - 10 * q);
exp[1] = toDigit(q);
exp[0] = er >= 0 ? '+' : '-';
}
private void round(long pp) {
/*
* Let d = f 10^e, and let p shorten pp.
* This method rounds d to the p most significant digits.
* It does so by possibly modifying f, e and n.
* When f becomes 0, e and n are normalized to 0 and 1, resp.
*
* For any real x let
* r(x) = floor(x + 1/2)
* which is rounding to the closest integer, with ties rounded toward
* positive infinity.
*
* When f = 0 there's not much to say, except that this holds iff n = 0.
*
* Otherwise, since
* 10^{n-1} <= f < 10^n
* it follows that
* 10^{e+n-1} <= d < 10^{e+n}
* To round d to the most significant p digits, first scale d to the
* range [10^{p-1}, 10^p), cutoff the fractional digits by applying r,
* and finally scale back.
* To this end, first define
* ds = d 10^{p-e-n}
* which ensures
* 10^{p-1} <= ds < 10^p
*
* Now, if p < 0 (that is, if p <= -1) then
* ds < 10^p <= 10^{-1} < 1/2
* so that
* r(ds) = 0
* Thus, rounding d to p < 0 digits leads to 0.
*/
if (n == 0 || pp < 0) {
f = 0;
e = 0;
n = 1;
return;
}
/*
* Further, if p >= n then
* ds = f 10^e 10^{p-e-n} = f 10^{p-n}
* which shows that ds is an integer, so r(ds) = ds. That is,
* rounding to p >= n digits leads to a result equal to d.
*/
if (pp >= n) { // no rounding needed
return;
}
/*
* Finally, 0 <= p < n. When p = 0 it follows that
* 10^{-1} <= ds < 1
* 0 <= f' = r(ds) <= 1
* that is, f' is either 0 or 1.
*
* Otherwise
* 10^{p-1} <= ds < 10^p
* 1 <= 10^{p-1} <= f' = r(ds) <= 10^p
* Note that f' = 10^p is a possible outcome.
*
* Scale back, where e' = e + n - p
* d' = f' 10^{e+n-p} = f' 10^e', with 10^{e+n-1} <= d' <= 10^{e+n}
*
* Since n > p, f' can be computed in integer arithmetic as follows,
* where / denotes division in the real numbers:
* f' = r(ds) = r(f 10^{p-n}) = r(f / 10^{n-p})
* = floor(f / 10^{n-p} + 1/2)
* = floor((f + 10^{n-p}/2) / 10^{n-p})
*/
int p = (int) pp; // 0 <= pp < n, safe cast
e += n - p; // new e is well inside the int range
long pow10 = MathUtils.pow10(n - p);
f = (f + (pow10 >> 1)) / pow10;
if (p == 0) {
n = 1;
if (f == 0) {
e = 0;
}
return;
}
n = p;
if (f == MathUtils.pow10(p)) {
/*
* f is n + 1 digits long.
* Absorb one trailing zero into e and reduce f accordingly.
*/
f /= 10;
e += 1;
}
}
/*
* Fills the digits section with indices in [from, to) with the lower
* to - from digits of x (as chars), while stripping them away from x.
* Returns the stripped x.
*/
private long fillWithDigits(long x, int from, int to) {
while (to > from) {
long q = x / 10;
digits[--to] = toDigit(x - q * 10);
x = q;
}
return x;
}
/*
* Fills the digits section with indices in [from, to) with '0'.
*/
private void fillWithZeros(int from, int to) {
while (to > from) {
digits[--to] = '0';
}
}
private static char toDigit(long d) {
return toDigit((int) d);
}
private static char toDigit(int d) {
return (char) (d + '0');
}
}