net.intelie.pipes.util.DoubleUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of pipes-api Show documentation
Show all versions of pipes-api Show documentation
Intelie Pipes' API classes and interfaces
package net.intelie.pipes.util;
import java.util.List;
public abstract class DoubleUtils {
/**
* 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];
private static final double[] POWERS_OF_TEN_DOUBLE = new double[30];
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);
}
}
public static String format(double value) {
StringBuilder builder = new StringBuilder();
formatDouble(value, 10, 10, builder);
return builder.toString();
}
/**
* 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(double source, int decimals, int precision, StringBuilder target) {
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
*
* 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, int decimals, int precision, 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;
}
boolean negative = source < 0.0;
if (negative) {
source = -source;
// Done once and for all
target.append('-');
}
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.
String s = Double.toString(source);
if (source >= 1e-3 && source < 1e7) {
// Plain representation of double: "intPart.decimalPart"
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);
removeTrailingZeroes(target);
}
return;
} else if (scale + 1 < decLength) {
// ignore unnecessary digits
decLength = scale + 1;
decS = decS.substring(0, decLength);
}
long intP = Long.parseLong(s.substring(0, dot));
long decP = Long.parseLong(decS);
format(target, scale, intP, decP);
} else {
// Scientific representation of double: "x.xxxxxEyyy"
int dot = s.indexOf('.');
assert dot >= 0;
int exp = s.indexOf('E');
assert exp >= 0;
int exposant = Integer.parseInt(s.substring(exp + 1));
String intS = s.substring(0, dot);
String decS = s.substring(dot + 1, exp);
int decLength = decS.length();
if (exposant >= 0) {
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
long intP = Long.parseLong(intS) * tenPow(exposant) + Long.parseLong(decS.substring(0, exposant));
long decP = Long.parseLong(decS.substring(exposant, exposant + scale + 1));
format(target, scale, intP, decP);
}
} else {
// Only a decimal part is supplied
exposant = -exposant;
int digits = scale - exposant + 1;
if (!appendZero(digits < 0, target)) {
if (digits == 0) {
long decP = Long.parseLong(intS);
format(target, scale, 0L, decP);
} else if (decLength < digits) {
long decP = Long.parseLong(intS) * tenPow(decLength + 1) + Long.parseLong(decS) * 10;
format(target, exposant + decLength, 0L, decP);
} else {
long subDecP = Long.parseLong(decS.substring(0, digits));
long decP = Long.parseLong(intS) * tenPow(digits) + subDecP;
format(target, scale, 0L, decP);
}
}
}
}
}
public static boolean appendZero(boolean condition, StringBuilder target) {
if (condition)
target.append('0');
return condition;
}
public static void removeTrailingZeroes(StringBuilder target) {
for (int l = target.length() - 1; l >= 0 && target.charAt(l) == '0'; l--) {
target.setLength(l);
}
}
/**
* 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(double source, int decimals, 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(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(int n) {
assert n >= 0;
return n < POWERS_OF_TEN_DOUBLE.length ? POWERS_OF_TEN_DOUBLE[n] : Math.pow(10, n);
}
/**
* 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(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
*
* 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, int decimals, int precision, 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;
}
boolean isPositive = source >= 0.0;
source = Math.abs(source);
int scale = (source >= 1.0) ? decimals : precision;
long intPart = (long) Math.floor(source);
double tenScale = tenPowDouble(scale);
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 (!appendZero(intPart == 0L && fracPart == 0L, target)) {
// 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);
}
}
}
/**
* 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(double source, int scale) {
// if scale >= 308, 10^308 ~= Infinity
double decExp = Math.log10(source);
return scale >= 308 || decExp + scale >= 14.5;
}
/**
* 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, int scale) {
source = Math.abs(source);
long intPart = (long) Math.floor(source);
double fracPart = (source - intPart) * tenPowDouble(scale);
double decExp = Math.log10(source);
double range = decExp + scale >= 12 ? .1 : .001;
double distanceToRound1 = Math.abs(fracPart - Math.floor(fracPart));
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
}
}