Monday, November 20, 2017

Simple String Representation of Java Decimal Numbers without Scientific Notation

The primary types/objects used for decimal numbers in Java are float/Float, double/Double, and BigDecimal. Each of these has cases in which its "default" string representation is "computerized scientific notation." This post demonstrates some simple approaches to provide a string representation of the decimal number in these cases without scientific notation.

Examples in this post will demonstrate the "default" scientific notation String representations of these Java numeric types using a range of numbers for each type that demonstrate approximately where the "default" representation for each type becomes scientific notation. The next three code listings show the code for constructing general ranges for floats, doubles, and BigDecimals. The full source code listing for these examples is available on GitHub.

Constructing the Example Range of Floats

/**
 * Writes floats in the provided format and in the
 * provided range to standard output.
 *
 * @param start Float to start writing.
 * @param threshold Float past which to not write anymore.
 * @param delta Delta for each increment of floats to be written.
 * @param label Label for header.
 * @param format Format for print out.
 */
private static void writeFloatsToOutput(
   final float start,
   final float threshold,
   final float delta,
   final String label,
   final Format format)
{
   out.println(generateHeader(label));
   float floatValue = start;
   do
   {
      out.println("= " + format.fromFloat(floatValue));
      floatValue += delta;
   }
   while (floatValue < threshold);
}

Constructing the Example Range of Doubles

/**
 * Writes doubles in the provided format and in the
 * provided range to standard output.
 *
 * @param start Double to start writing.
 * @param threshold Double past which to not write anymore.
 * @param delta Delta for each increment of doubles to be written.
 * @param label Label for header.
 * @param format Format for print out.
 */
private static void writeDoublesToOutput(
   final double start,
   final double threshold,
   final double delta,
   final String label,
   final Format format)
{
   out.println(generateHeader(label));
   double doubleValue = start;
   do
   {
      out.println("= " + format.fromDouble(doubleValue));
      doubleValue += delta;
   }
   while (doubleValue < threshold);
}

Constructing the Example Range of BigDecimals

/**
 * Writes BigDecimals in the provided format and in the
 * provided range to standard output.
 *
 * @param start BigDecimal to start writing.
 * @param threshold BigDecimal past which to not write anymore.
 * @param delta Delta for each increment of BigDecimals to be written.
 * @param label Label for header.
 * @param format Format for print out.
 */
private static void writeBigDecimalsToOutput(
   final BigDecimal start,
   final BigDecimal threshold,
   final BigDecimal delta,
   final String label,
   final Format format)
{
   out.println(generateHeader(label));
   BigDecimal decimal = start;
   do
   {
      out.println("= " + format.fromBigDecimal(decimal));
      decimal = decimal.add(delta);
   }
   while (decimal.compareTo(threshold) < 0);
}

The three methods shown above can be called with ranges specified to demonstrate when scientific notation is automatically employed for String representations of the Java decimal types. The output from running the above with "default" format for each numeric type is shown in the next three output listings.

The default representation of very small and very large floats does include scientific notation for the smallest numbers shown and for the largest numbers shown. These numbers demonstrate what is discussed in the Float.toString(Float) documentation: numbers "less than 10-3 or greater than or equal to 107" are "represented in so-called 'computerized scientific notation.'"

==========================
= Small Floats (DEFAULT) =
==========================
= 8.5E-4
= 9.5E-4
= 0.00105
= 0.0011499999
= 0.0012499999
= 0.0013499998
= 0.0014499997
= 0.0015499997
= 0.0016499996
= 0.0017499996
= 0.0018499995
= 0.0019499995
==========================
= Large Floats (DEFAULT) =
==========================
= 9999995.0
= 9999996.0
= 9999997.0
= 9999998.0
= 9999999.0
= 1.0E7
= 1.0000001E7
= 1.0000002E7
= 1.0000003E7
= 1.0000004E7

The default representation of very small and very large doubles does include scientific notation for the smallest numbers shown and for the largest numbers shown. These numbers demonstrate what is discussed in the Javadoc documentation for Double.toString(double): numbers "less than 10-3 or greater than or equal to 107" are "represented in so-called 'computerized scientific notation.'"

===========================
= Small Doubles (DEFAULT) =
===========================
= 8.5E-4
= 9.5E-4
= 0.00105
= 0.00115
= 0.00125
= 0.00135
= 0.0014500000000000001
= 0.0015500000000000002
= 0.0016500000000000002
= 0.0017500000000000003
= 0.0018500000000000003
= 0.0019500000000000003
===========================
= Large Doubles (DEFAULT) =
===========================
= 9999995.0
= 9999996.0
= 9999997.0
= 9999998.0
= 9999999.0
= 1.0E7
= 1.0000001E7
= 1.0000002E7
= 1.0000003E7
= 1.0000004E7

While float and double had their smallest and largest numbers expressed in scientific notation, BigDecimal only does this by default for smaller numbers. This is described in the BigDecimal.toString() Javadoc documentation: "If the scale is greater than or equal to zero and the adjusted exponent is greater than or equal to -6, the number will be converted to a character form without using exponential notation. ... if ... the adjusted exponent is less than -6, the number will be converted to a character form using exponential notation."

===============================
= Small BigDecimals (DEFAULT) =
===============================
= 8.5E-7
= 9.5E-7
= 0.00000105
= 0.00000115
= 0.00000125
= 0.00000135
= 0.00000145
= 0.00000155
= 0.00000165
= 0.00000175
= 0.00000185
= 0.00000195
===============================
= Large BigDecimals (DEFAULT) =
===============================
= 99999950000000000000000000000000000000000000000000
= 99999960000000000000000000000000000000000000000000
= 99999970000000000000000000000000000000000000000000
= 99999980000000000000000000000000000000000000000000
= 99999990000000000000000000000000000000000000000000
= 100000000000000000000000000000000000000000000000000
= 100000010000000000000000000000000000000000000000000
= 100000020000000000000000000000000000000000000000000
= 100000030000000000000000000000000000000000000000000
= 100000040000000000000000000000000000000000000000000
private static void writeFormattedValues(final Format format)
{
   writeFloatsToOutput(
      0.00085f, 0.002f, 0.0001f, "Small Floats (" + format + ")", format);
   writeFloatsToOutput(
      9_999_995f, 10_000_005f, 1f, "Large Floats (" + format + ")", format);

   writeDoublesToOutput(
      0.00085d, 0.002d, 0.0001d, "Small Doubles (" + format + ")", format);
   writeDoublesToOutput(
      9_999_995d, 10_000_005d, 1d, "Large Doubles (" + format + ")", format);

   writeBigDecimalsToOutput(
      new BigDecimal("0.00000085"),
      new BigDecimal("0.000002"),
      new BigDecimal("0.0000001"),
      "Small BigDecimals (" + format + ")",
      format);
   writeBigDecimalsToOutput(
      new BigDecimal("99999950000000000000000000000000000000000000000000"),
      new BigDecimal("100000050000000000000000000000000000000000000000000"),
      new BigDecimal("10000000000000000000000000000000000000000000"),
      "Large BigDecimals (" + format + ")",
      format);
}

The representation of very small and very large numbers in the code above can be presented in default format or in a format the precludes use of scientific notation. The code listing for the Format enum is shown next and this enum demonstrates approaches that can be used with float, double, and BigDecimal to render them without scientific notation.

Format.java

/**
 * Supports rendering of Java numeric types float, double,
 * and BigDecimal in "default" format and in format that
 * avoids use of scientific notation.
 */
public enum Format
{
   DEFAULT
   {
      @Override
      public String fromFloat(final float floatValue)
      {
         return String.valueOf(floatValue);
      }
      @Override
      public String fromDouble(final double doubleValue)
      {
         return String.valueOf(doubleValue);
      }
      @Override
      public String fromBigDecimal(final BigDecimal bigDecimalValue)
      {
         return bigDecimalValue.toString();
      }
   },
   NO_EXPONENT
   {
      @Override
      public String fromFloat(final float floatValue)
      {
         return getFormatter().format(floatValue);
      }
      @Override
      public String fromDouble(final double doubleValue)
      {
         return getFormatter().format(doubleValue);
      }
      @Override
      public String fromBigDecimal(final BigDecimal bigDecimalValue)
      {
         return bigDecimalValue.toPlainString();
      }
   };

   /**
    * Present provided float as a String formatted per my format.
    *
    * @param floatValue Float-typed number to be rendered.
    */
   public abstract String fromFloat(final float floatValue);
   /**
    * Present provided double as a String formatted per my format.
    *
    * @param doubleValue Double-typed number to be rendered.
    */
   public abstract String fromDouble(final double doubleValue);
   /**
    * Present provided BigDecimal as a String formatted per my format.
    *
    * @param bigDecimalValue BigDecimal object to be rendered.
    */
   public abstract String fromBigDecimal(final BigDecimal bigDecimalValue);

   /**
    * Provide an instance of {@code NumberFormat} configured to
    * display the maximum fractional digits and to not group.
    *
    * @return Instance of {@code NumberFormat} to be used to provide
    *    a representation of the decimal number without scientific notation.
    */
   private static NumberFormat getFormatter()
   {
      // Used to prevent numbers from being represented in scientific notation.
      final NumberFormat numberFormat = NumberFormat.getInstance();
      numberFormat.setMaximumFractionDigits(Integer.MAX_VALUE);
      numberFormat.setGroupingUsed(false);
      return numberFormat;
   }
}

The Format enum uses an instance of NumberFormat with grouping disabled and with the maximum fraction digits set to Integer.MAX_VALUE to ensure that floats and doubles are rendered without scientific notation. It's even easier to accomplish this with BigDecimal using its toPlainString() method.

The output from running the code with the Format.NO_EXPONENT is shown next (and there's no exponents or scientific notation in sight).

==============================
= Small Floats (NO_EXPONENT) =
==============================
= 0.0008500000112690032
= 0.0009500000160187483
= 0.0010499999625608325
= 0.0011499999091029167
= 0.001249999855645001
= 0.0013499998021870852
= 0.0014499997487291694
= 0.0015499996952712536
= 0.0016499996418133378
= 0.001749999588355422
= 0.0018499995348975062
= 0.0019499994814395905
==============================
= Large Floats (NO_EXPONENT) =
==============================
= 9999995
= 9999996
= 9999997
= 9999998
= 9999999
= 10000000
= 10000001
= 10000002
= 10000003
= 10000004
===============================
= Small Doubles (NO_EXPONENT) =
===============================
= 0.00085
= 0.00095
= 0.00105
= 0.00115
= 0.00125
= 0.00135
= 0.0014500000000000001
= 0.0015500000000000002
= 0.0016500000000000002
= 0.0017500000000000003
= 0.0018500000000000003
= 0.0019500000000000003
===============================
= Large Doubles (NO_EXPONENT) =
===============================
= 9999995
= 9999996
= 9999997
= 9999998
= 9999999
= 10000000
= 10000001
= 10000002
= 10000003
= 10000004
===================================
= Small BigDecimals (NO_EXPONENT) =
===================================
= 0.00000085
= 0.00000095
= 0.00000105
= 0.00000115
= 0.00000125
= 0.00000135
= 0.00000145
= 0.00000155
= 0.00000165
= 0.00000175
= 0.00000185
= 0.00000195
===================================
= Large BigDecimals (NO_EXPONENT) =
===================================
= 99999950000000000000000000000000000000000000000000
= 99999960000000000000000000000000000000000000000000
= 99999970000000000000000000000000000000000000000000
= 99999980000000000000000000000000000000000000000000
= 99999990000000000000000000000000000000000000000000
= 100000000000000000000000000000000000000000000000000
= 100000010000000000000000000000000000000000000000000
= 100000020000000000000000000000000000000000000000000
= 100000030000000000000000000000000000000000000000000
= 100000040000000000000000000000000000000000000000000

The standard Java floating types and BigDecimal class render some numbers in scientific notation, but it's easy to ensure that this default presentation of scientific notation is not used when it is not desired.

No comments: