org.apache.xmlgraphics.util.DoubleFormatUtil Maven / Gradle / Ivy
Show all versions of xmlgraphics-commons Show documentation
/*
* 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: DoubleFormatUtil.java 1732019 2016-02-24 05:01:10Z gadams $ */
package org.apache.xmlgraphics.util;
/**
* This class implements fast, thread-safe format of a double value
* with a given number of decimal digits.
*
* 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.
*
* A few examples (consider decimals being 4 and precision being 8):
*
* - 0.0 should be rendered as "0"
*
- 0.1 should be rendered as "0.1"
*
- 1234.1 should be rendered as "1234.1"
*
- 1234.1234567 should be rendered as "1234.1235" (note the trailing 5! Rounding!)
*
- 1234.00001 should be rendered as "1234"
*
- 0.00001 should be rendered as "0.00001" (here you see the effect of the "precision" parameter)
*
- 0.00000001 should be rendered as "0.00000001"
*
- 0.000000001 should be rendered as "0"
*
*
* Originally authored by Julien Aymé.
*/
public final class DoubleFormatUtil {
private DoubleFormatUtil() {
}
/**
* 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, StringBuffer 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, StringBuffer 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);
// 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);
}
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 (digits < 0) {
target.append('0');
} else 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);
}
}
}
}
/**
* 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);
}
/**
* 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);
}
}
/**
* 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(StringBuffer 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, StringBuffer 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 (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');
}
}
/**
* 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(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 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
}
}