/*
 * 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é.
/** * 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&eacute;. */
public final class DoubleFormatUtil { private DoubleFormatUtil() { }
Rounds the given source value at the given precision and writes the rounded value into the given target
Params:
  • source – the source value to round
  • decimals – the decimals to round at (use if abs(source) ≥ 1.0)
  • precision – the precision to round at (use if abs(source) < 1.0)
  • target – the buffer to write to
/** * 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) &ge; 1.0) * @param precision the precision to round at (use if abs(source) &lt; 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.

Params:
  • source – the source value to round
  • decimals – the decimals to round at (use if abs(source) ≥ 1.0)
  • precision – the precision to round at (use if abs(source) < 1.0)
  • target – the buffer to write to
/** * 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) &ge; 1.0) * @param precision the precision to round at (use if abs(source) &lt; 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
Params:
  • source – the source value to round
  • decimals – the decimals to round at (use if abs(source) ≥ 1.0)
  • precision – the precision to round at (use if abs(source) < 1.0)
Returns:true if the source value will be rounded to zero
/** * 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) &ge; 1.0) * @param precision the precision to round at (use if abs(source) &lt; 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)
/** * 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
Params:
  • n – the nth power of ten to get
Returns:ten to the power of n
/** * 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
Params:
  • target – the buffer to write to
  • scale – the expected rounding scale
  • intP – the source integer part
  • decP – the source decimal part, truncated to scale + 1 digit
/** * 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).

Params:
  • source – the source value to round
  • decimals – the decimals to round at (use if abs(source) ≥ 1.0)
  • precision – the precision to round at (use if abs(source) < 1.0)
  • target – the buffer to write to
/** * 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) &ge; 1.0) * @param precision the precision to round at (use if abs(source) &lt; 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
Params:
  • value – the value to get the exponent from
Returns:the value's exponent
/** * 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
Params:
  • source – the source to round
  • scale – the scale to round at
Returns:true if the rounding will potentially use too many digits
/** * 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.
Params:
  • source – the source to round
  • scale – the scale to round at
Returns:true if the source will be potentially rounded at the scale
/** * 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 } }