/*
* 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.
*/
/* $Id$ */
package com.revolsys.util;
/**
* This class implements fast, thread-safe format of a double value
* with a given number of decimal digits.
* <p>
* The contract for the format methods is this one:
* if the source is greater than or equal to 1 (in absolute value),
* use the decimals parameter to define the number of decimal digits; else,
* use the precision parameter to define the number of decimal digits.
* <p>
* A few examples (consider decimals being 4 and precision being 8):
* <ul>
* <li>0.0 should be rendered as "0"
* <li>0.1 should be rendered as "0.1"
* <li>1234.1 should be rendered as "1234.1"
* <li>1234.1234567 should be rendered as "1234.1235" (note the trailing 5! Rounding!)
* <li>1234.00001 should be rendered as "1234"
* <li>0.00001 should be rendered as "0.00001" (here you see the effect of the "precision" parameter)
* <li>0.00000001 should be rendered as "0.00000001"
* <li>0.000000001 should be rendered as "0"
* </ul>
*
* Originally authored by Julien Aymé.
*/
public final class DoubleFormatUtil {
private static final double[] POWERS_OF_TEN_DOUBLE = new double[30];
/**
* Most used power of ten (to avoid the cost of Math.pow(10, n)
*/
private static final long[] POWERS_OF_TEN_LONG = new long[19];
static {
POWERS_OF_TEN_LONG[0] = 1L;
for (int i = 1; i < POWERS_OF_TEN_LONG.length; i++) {
POWERS_OF_TEN_LONG[i] = POWERS_OF_TEN_LONG[i - 1] * 10L;
}
for (int i = 0; i < POWERS_OF_TEN_DOUBLE.length; i++) {
POWERS_OF_TEN_DOUBLE[i] = Double.parseDouble("1e" + i);
}
}
/**
* Helper method to do the custom rounding used within formatDoublePrecise
*
* @param target the buffer to write to
* @param scale the expected rounding scale
* @param intP the source integer part
* @param decP the source decimal part, truncated to scale + 1 digit
*/
private static void format(final StringBuilder target, int scale, long intP, long decP) {
if (decP != 0L) {
// decP is the decimal part of source, truncated to scale + 1 digit.
// Custom rounding: add 5
decP += 5L;
decP /= 10L;
if (decP >= tenPowDouble(scale)) {
intP++;
decP -= tenPow(scale);
}
if (decP != 0L) {
// Remove trailing zeroes
while (decP % 10L == 0L) {
decP = decP / 10L;
scale--;
}
}
}
target.append(intP);
if (decP != 0L) {
target.append('.');
// Use tenPow instead of tenPowDouble for scale below 18,
// since the casting of decP to double may cause some imprecisions:
// E.g. for decP = 9999999999999999L and scale = 17,
// decP < tenPow(16) while (double) decP == tenPowDouble(16)
while (scale > 0 && (scale > 18 ? decP < tenPowDouble(--scale) : decP < tenPow(--scale))) {
// Insert leading zeroes
target.append('0');
}
target.append(decP);
}
}
/**
* Rounds the given source value at the given precision
* and writes the rounded value into the given target
*
* @param source the source value to round
* @param decimals the decimals to round at (use if abs(source) ≥ 1.0)
* @param precision the precision to round at (use if abs(source) < 1.0)
* @param target the buffer to write to
*/
public static void formatDouble(final double source, final int decimals, final int precision,
final StringBuilder target) {
final int scale = Math.abs(source) >= 1.0 ? decimals : precision;
if (tooManyDigitsUsed(source, scale) || tooCloseToRound(source, scale)) {
formatDoublePrecise(source, decimals, precision, target);
} else {
formatDoubleFast(source, decimals, precision, target);
}
}
/**
* Rounds the given source value at the given precision
* and writes the rounded value into the given target
* <p>
* This method internally uses double precision computation and rounding,
* so the result may not be accurate (see formatDouble method for conditions).
*
* @param source the source value to round
* @param decimals the decimals to round at (use if abs(source) ≥ 1.0)
* @param precision the precision to round at (use if abs(source) < 1.0)
* @param target the buffer to write to
*/
public static void formatDoubleFast(double source, final int decimals, final int precision,
final StringBuilder target) {
if (isRoundedToZero(source, decimals, precision)) {
// Will always be rounded to 0
target.append('0');
return;
} else if (Double.isNaN(source) || Double.isInfinite(source)) {
// Cannot be formated
target.append(Double.toString(source));
return;
}
final boolean isPositive = source >= 0.0;
source = Math.abs(source);
int scale = source >= 1.0 ? decimals : precision;
long intPart = (long)Math.floor(source);
final double tenScale = tenPowDouble(scale);
final double fracUnroundedPart = (source - intPart) * tenScale;
long fracPart = Math.round(fracUnroundedPart);
if (fracPart >= tenScale) {
intPart++;
fracPart = Math.round(fracPart - tenScale);
}
if (fracPart != 0L) {
// Remove trailing zeroes
while (fracPart % 10L == 0L) {
fracPart = fracPart / 10L;
scale--;
}
}
if (intPart != 0L || fracPart != 0L) {
// non-zero value
if (!isPositive) {
// negative value, insert sign
target.append('-');
}
// append integer part
target.append(intPart);
if (fracPart != 0L) {
// append fractional part
target.append('.');
// insert leading zeroes
while (scale > 0 && fracPart < tenPowDouble(--scale)) {
target.append('0');
}
target.append(fracPart);
}
} else {
target.append('0');
}
}
/**
* Rounds the given source value at the given precision
* and writes the rounded value into the given target
* <p>
* This method internally uses the String representation of the source value,
* in order to avoid any double precision computation error.
*
* @param source the source value to round
* @param decimals the decimals to round at (use if abs(source) ≥ 1.0)
* @param precision the precision to round at (use if abs(source) < 1.0)
* @param target the buffer to write to
*/
public static void formatDoublePrecise(double source, final int decimals, final int precision,
final StringBuilder target) {
if (isRoundedToZero(source, decimals, precision)) {
// Will always be rounded to 0
target.append('0');
return;
} else if (!Double.isFinite(source)) {
// Cannot be formated
target.append(source);
return;
}
final boolean negative = source < 0.0;
if (negative) {
source = -source;
// Done once and for all
target.append('-');
}
final int scale = source >= 1.0 ? decimals : precision;
// The only way to format precisely the double is to use the String
// representation of the double, and then to do mathematical integer
// operation on it.
final String s = Double.toString(source);
if (source >= 1e-3 && source < 1e7) {
// Plain representation of double: "intPart.decimalPart"
final int dot = s.indexOf('.');
String decS = s.substring(dot + 1);
int decLength = decS.length();
if (scale >= decLength) {
if ("0".equals(decS)) {
// source is a mathematical integer
target.append(s.substring(0, dot));
} else {
target.append(s);
// Remove trailing zeroes
for (int l = target.length() - 1; l >= 0 && target.charAt(l) == '0'; l--) {
target.setLength(l);
}
}
return;
} else if (scale + 1 < decLength) {
// ignore unnecessary digits
decLength = scale + 1;
decS = decS.substring(0, decLength);
}
final long intP = Long.parseLong(s.substring(0, dot));
final long decP = Long.parseLong(decS);
format(target, scale, intP, decP);
} else {
// Scientific representation of double: "x.xxxxxEyyy"
final int dot = s.indexOf('.');
assert dot >= 0;
final int exp = s.indexOf('E');
assert exp >= 0;
int exposant = Integer.parseInt(s.substring(exp + 1));
final String intS = s.substring(0, dot);
final String decS = s.substring(dot + 1, exp);
final int decLength = decS.length();
if (exposant >= 0) {
final int digits = decLength - exposant;
if (digits <= 0) {
// no decimal part,
// no rounding involved
target.append(intS);
target.append(decS);
for (int i = -digits; i > 0; i--) {
target.append('0');
}
} else if (digits <= scale) {
// decimal part precision is lower than scale,
// no rounding involved
target.append(intS);
target.append(decS.substring(0, exposant));
target.append('.');
target.append(decS.substring(exposant));
} else {
// decimalDigits > scale,
// Rounding involved
final long intP = Long.parseLong(intS) * tenPow(exposant)
+ Long.parseLong(decS.substring(0, exposant));
final long decP = Long.parseLong(decS.substring(exposant, exposant + scale + 1));
format(target, scale, intP, decP);
}
} else {
// Only a decimal part is supplied
exposant = -exposant;
final int digits = scale - exposant + 1;
if (digits < 0) {
target.append('0');
} else if (digits == 0) {
final long decP = Long.parseLong(intS);
format(target, scale, 0L, decP);
} else if (decLength < digits) {
final long decP = Long.parseLong(intS) * tenPow(decLength + 1)
+ Long.parseLong(decS) * 10;
format(target, exposant + decLength, 0L, decP);
} else {
final long subDecP = Long.parseLong(decS.substring(0, digits));
final long decP = Long.parseLong(intS) * tenPow(digits) + subDecP;
format(target, scale, 0L, decP);
}
}
}
}
/**
* Returns the exponent of the given value
*
* @param value the value to get the exponent from
* @return the value's exponent
*/
public static int getExponant(final double value) {
// See Double.doubleToRawLongBits javadoc or IEEE-754 spec
// to have this algorithm
long exp = Double.doubleToRawLongBits(value) & 0x7ff0000000000000L;
exp = exp >> 52;
return (int)(exp - 1023L);
}
/**
* Returns true if the given source value will be rounded to zero
*
* @param source the source value to round
* @param decimals the decimals to round at (use if abs(source) ≥ 1.0)
* @param precision the precision to round at (use if abs(source) < 1.0)
* @return true if the source value will be rounded to zero
*/
private static boolean isRoundedToZero(final double source, final int decimals,
final int precision) {
// Use 4.999999999999999 instead of 5 since in some cases, 5.0 / 1eN > 5e-N
// (e.g. for N = 37, 42, 45, 66, ...)
return source == 0.0
|| Math.abs(source) < 4.999999999999999 / tenPowDouble(Math.max(decimals, precision) + 1);
}
/**
* Returns ten to the power of n
*
* @param n the nth power of ten to get
* @return ten to the power of n
*/
public static long tenPow(final int n) {
assert n >= 0;
return n < POWERS_OF_TEN_LONG.length ? POWERS_OF_TEN_LONG[n] : (long)Math.pow(10, n);
}
private static double tenPowDouble(final int n) {
assert n >= 0;
return n < POWERS_OF_TEN_DOUBLE.length ? POWERS_OF_TEN_DOUBLE[n] : Math.pow(10, n);
}
/**
* Returns true if the given source is considered to be too close
* of a rounding value for the given scale.
*
* @param source the source to round
* @param scale the scale to round at
* @return true if the source will be potentially rounded at the scale
*/
private static boolean tooCloseToRound(double source, final int scale) {
source = Math.abs(source);
final long intPart = (long)Math.floor(source);
final double fracPart = (source - intPart) * tenPowDouble(scale);
final double decExp = Math.log10(source);
final double range = decExp + scale >= 12 ? .1 : .001;
final double distanceToRound1 = Math.abs(fracPart - Math.floor(fracPart));
final double distanceToRound2 = Math.abs(fracPart - Math.floor(fracPart) - 0.5);
return distanceToRound1 <= range || distanceToRound2 <= range;
// .001 range: Totally arbitrary range,
// I never had a failure in 10e7 random tests with this value
// May be JVM dependent or architecture dependent
}
/**
* Returns true if the rounding is considered to use too many digits
* of the double for a fast rounding
*
* @param source the source to round
* @param scale the scale to round at
* @return true if the rounding will potentially use too many digits
*/
private static boolean tooManyDigitsUsed(final double source, final int scale) {
// if scale >= 308, 10^308 ~= Infinity
final double decExp = Math.log10(source);
return scale >= 308 || decExp + scale >= 14.5;
}
private DoubleFormatUtil() {
}
}