ParsedDecimal.java

  1. /*
  2.  * Licensed to the Apache Software Foundation (ASF) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * The ASF licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *      http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.apache.commons.text.numbers;

  18. /**
  19.  * Internal class representing a decimal value parsed into separate components. Each number
  20.  * is represented with
  21.  * <ul>
  22.  *  <li>a boolean flag for the sign,</li>
  23.  *  <li> a sequence of the digits {@code 0 - 10} representing an unsigned integer with leading and trailing zeros
  24.  *      removed, and</li>
  25.  *  <li>an exponent value that when applied to the base 10 digits produces a floating point value with the
  26.  *      correct magnitude.</li>
  27.  * </ul>
  28.  * <p><strong>Examples</strong></p>
  29.  * <table>
  30.  *  <tr><th>Double</th><th>Negative</th><th>Digits</th><th>Exponent</th></tr>
  31.  *  <tr><td>0.0</td><td>false</td><td>[0]</td><td>0</td></tr>
  32.  *  <tr><td>1.2</td><td>false</td><td>[1, 2]</td><td>-1</td></tr>
  33.  *  <tr><td>-0.00971</td><td>true</td><td>[9, 7, 1]</td><td>-5</td></tr>
  34.  *  <tr><td>56300</td><td>true</td><td>[5, 6, 3]</td><td>2</td></tr>
  35.  * </table>
  36.  */
  37. final class ParsedDecimal {

  38.     /**
  39.      * Interface containing values used during string formatting.
  40.      */
  41.     interface FormatOptions {

  42.         /**
  43.          * Gets the decimal separator character.
  44.          * @return decimal separator character
  45.          */
  46.         char getDecimalSeparator();

  47.         /**
  48.          * Gets an array containing the localized digit characters 0-9 in that order.
  49.          * This string <em>must</em> be non-null and have a length of 10.
  50.          * @return array containing the digit characters 0-9
  51.          */
  52.         char[] getDigits();

  53.         /**
  54.          * Gets the exponent separator as an array of characters.
  55.          * @return exponent separator as an array of characters
  56.          */
  57.         char[] getExponentSeparatorChars();

  58.         /**
  59.          * Gets the character used to separate thousands groupings.
  60.          * @return character used to separate thousands groupings
  61.          */
  62.         char getGroupingSeparator();

  63.         /**
  64.          * Gets the minus sign character.
  65.          * @return minus sign character
  66.          */
  67.         char getMinusSign();

  68.         /**
  69.          * Return {@code true} if exponent values should always be included in
  70.          * formatted output, even if the value is zero.
  71.          * @return {@code true} if exponent values should always be included
  72.          */
  73.         boolean isAlwaysIncludeExponent();

  74.         /**
  75.          * Return {@code true} if thousands should be grouped.
  76.          * @return {@code true} if thousand should be grouped
  77.          */
  78.         boolean isGroupThousands();

  79.         /**
  80.          * Return {@code true} if fraction placeholders (e.g., {@code ".0"} in {@code "1.0"})
  81.          * should be included.
  82.          * @return {@code true} if fraction placeholders should be included
  83.          */
  84.         boolean isIncludeFractionPlaceholder();

  85.         /**
  86.          * Return {@code true} if the string zero should be prefixed with the minus sign
  87.          * for negative zero values.
  88.          * @return {@code true} if the minus zero string should be allowed
  89.          */
  90.         boolean isSignedZero();
  91.     }

  92.     /** Minus sign character. */
  93.     private static final char MINUS_CHAR = '-';

  94.     /** Decimal separator character. */
  95.     private static final char DECIMAL_SEP_CHAR = '.';

  96.     /** Exponent character. */
  97.     private static final char EXPONENT_CHAR = 'E';

  98.     /** Zero digit character. */
  99.     private static final char ZERO_CHAR = '0';

  100.     /** Number of characters in thousands groupings. */
  101.     private static final int THOUSANDS_GROUP_SIZE = 3;

  102.     /** Radix for decimal numbers. */
  103.     private static final int DECIMAL_RADIX = 10;

  104.     /** Center value used when rounding. */
  105.     private static final int ROUND_CENTER = DECIMAL_RADIX / 2;

  106.     /** Number that exponents in engineering format must be a multiple of. */
  107.     private static final int ENG_EXPONENT_MOD = 3;

  108.     /**
  109.      * Gets the numeric value of the given digit character. No validation of the
  110.      * character type is performed.
  111.      * @param ch digit character
  112.      * @return numeric value of the digit character, ex: '1' = 1
  113.      */
  114.     private static int digitValue(final char ch) {
  115.         return ch - ZERO_CHAR;
  116.     }

  117.     /**
  118.      * Constructs a new instance from the given double value.
  119.      * @param d double value
  120.      * @return a new instance containing the parsed components of the given double value
  121.      * @throws IllegalArgumentException if {@code d} is {@code NaN} or infinite
  122.      */
  123.     public static ParsedDecimal from(final double d) {
  124.         if (!Double.isFinite(d)) {
  125.             throw new IllegalArgumentException("Double is not finite");
  126.         }

  127.         // Get the canonical string representation of the double value and parse
  128.         // it to extract the components of the decimal value. From the documentation
  129.         // of Double.toString() and the fact that d is finite, we are guaranteed the
  130.         // following:
  131.         // - the string will not be empty
  132.         // - it will contain exactly one decimal point character
  133.         // - all digit characters are in the ASCII range
  134.         final char[] strChars = Double.toString(d).toCharArray();

  135.         final boolean negative = strChars[0] == MINUS_CHAR;
  136.         final int digitStartIdx = negative ? 1 : 0;

  137.         final int[] digits = new int[strChars.length - digitStartIdx - 1];

  138.         boolean foundDecimalPoint = false;
  139.         int digitCount = 0;
  140.         int significantDigitCount = 0;
  141.         int decimalPos = 0;

  142.         int i;
  143.         for (i = digitStartIdx; i < strChars.length; ++i) {
  144.             final char ch = strChars[i];

  145.             if (ch == DECIMAL_SEP_CHAR) {
  146.                 foundDecimalPoint = true;
  147.                 decimalPos = digitCount;
  148.             } else if (ch == EXPONENT_CHAR) {
  149.                 // no more mantissa digits
  150.                 break;
  151.             } else if (ch != ZERO_CHAR || digitCount > 0) {
  152.                 // this is either the first non-zero digit or one after it
  153.                 final int val = digitValue(ch);
  154.                 digits[digitCount++] = val;

  155.                 if (val > 0) {
  156.                     significantDigitCount = digitCount;
  157.                 }
  158.             } else if (foundDecimalPoint) {
  159.                 // leading zero in a fraction; adjust the decimal position
  160.                 --decimalPos;
  161.             }
  162.         }

  163.         if (digitCount > 0) {
  164.             // determine the exponent
  165.             final int explicitExponent = i < strChars.length
  166.                     ? parseExponent(strChars, i + 1)
  167.                     : 0;
  168.             final int exponent = explicitExponent + decimalPos - significantDigitCount;

  169.             return new ParsedDecimal(negative, digits, significantDigitCount, exponent);
  170.         }

  171.         // no non-zero digits, so value is zero
  172.         return new ParsedDecimal(negative, new int[] {0}, 1, 0);
  173.     }

  174.     /**
  175.      * Parses a double exponent value from {@code chars}, starting at the {@code start}
  176.      * index and continuing through the end of the array.
  177.      * @param chars character array to parse a double exponent value from
  178.      * @param start start index
  179.      * @return parsed exponent value
  180.      */
  181.     private static int parseExponent(final char[] chars, final int start) {
  182.         int i = start;
  183.         final boolean neg = chars[i] == MINUS_CHAR;
  184.         if (neg) {
  185.             ++i;
  186.         }

  187.         int exp = 0;
  188.         for (; i < chars.length; ++i) {
  189.             exp = exp * DECIMAL_RADIX + digitValue(chars[i]);
  190.         }

  191.         return neg ? -exp : exp;
  192.     }

  193.     /** True if the value is negative. */
  194.     final boolean negative;

  195.     /** Array containing the significant decimal digits for the value. */
  196.     final int[] digits;

  197.     /** Number of digits used in the digits array; not necessarily equal to the length. */
  198.     int digitCount;

  199.     /** Exponent for the value. */
  200.     int exponent;

  201.     /** Output buffer for use in creating string representations. */
  202.     private char[] outputChars;

  203.     /** Output buffer index. */
  204.     private int outputIdx;

  205.     /**
  206.      * Constructs a new instance from its parts.
  207.      * @param negative {@code true} if the value is negative
  208.      * @param digits array containing significant digits
  209.      * @param digitCount number of digits used from the {@code digits} array
  210.      * @param exponent exponent value
  211.      */
  212.     private ParsedDecimal(final boolean negative, final int[] digits, final int digitCount,
  213.             final int exponent) {
  214.         this.negative = negative;
  215.         this.digits = digits;
  216.         this.digitCount = digitCount;
  217.         this.exponent = exponent;
  218.     }

  219.     /**
  220.      * Appends the given character to the output buffer.
  221.      * @param ch character to append
  222.      */
  223.     private void append(final char ch) {
  224.         outputChars[outputIdx++] = ch;
  225.     }

  226.     /**
  227.      * Appends the given character array directly to the output buffer.
  228.      * @param chars characters to append
  229.      */
  230.     private void append(final char[] chars) {
  231.         for (final char c : chars) {
  232.             append(c);
  233.         }
  234.     }

  235.     /**
  236.      * Appends the fractional component of the number to the current output buffer.
  237.      * @param zeroCount number of zeros to add after the decimal point and before the
  238.      *      first significant digit
  239.      * @param startIdx significant digit start index
  240.      * @param opts format options
  241.      */
  242.     private void appendFraction(final int zeroCount, final int startIdx, final FormatOptions opts) {
  243.         final char[] localizedDigits = opts.getDigits();
  244.         final char localizedZero = localizedDigits[0];

  245.         if (startIdx < digitCount) {
  246.             append(opts.getDecimalSeparator());

  247.             // add the zero prefix
  248.             for (int i = 0; i < zeroCount; ++i) {
  249.                 append(localizedZero);
  250.             }

  251.             // add the fraction digits
  252.             for (int i = startIdx; i < digitCount; ++i) {
  253.                 appendLocalizedDigit(digits[i], localizedDigits);
  254.             }
  255.         } else if (opts.isIncludeFractionPlaceholder()) {
  256.             append(opts.getDecimalSeparator());
  257.             append(localizedZero);
  258.         }
  259.     }

  260.     /**
  261.      * Appends the localized representation of the digit {@code n} to the output buffer.
  262.      * @param n digit to append
  263.      * @param digitChars character array containing localized versions of the digits {@code 0-9}
  264.      *      in that order
  265.      */
  266.     private void appendLocalizedDigit(final int n, final char[] digitChars) {
  267.         append(digitChars[n]);
  268.     }

  269.     /**
  270.      * Appends the whole number portion of this value to the output buffer. No thousands
  271.      * separators are added.
  272.      * @param wholeCount total number of digits required to the left of the decimal point
  273.      * @param opts format options
  274.      * @return number of digits from {@code digits} appended to the output buffer
  275.      * @see #appendWholeGrouped(int, FormatOptions)
  276.      */
  277.     private int appendWhole(final int wholeCount, final FormatOptions opts) {
  278.         if (shouldIncludeMinus(opts)) {
  279.             append(opts.getMinusSign());
  280.         }

  281.         final char[] localizedDigits = opts.getDigits();
  282.         final char localizedZero = localizedDigits[0];

  283.         final int significantDigitCount = Math.max(0, Math.min(wholeCount, digitCount));

  284.         if (significantDigitCount > 0) {
  285.             int i;
  286.             for (i = 0; i < significantDigitCount; ++i) {
  287.                 appendLocalizedDigit(digits[i], localizedDigits);
  288.             }

  289.             for (; i < wholeCount; ++i) {
  290.                 append(localizedZero);
  291.             }
  292.         } else {
  293.             append(localizedZero);
  294.         }

  295.         return significantDigitCount;
  296.     }

  297.     /**
  298.      * Appends the whole number portion of this value to the output buffer, adding thousands
  299.      * separators as needed.
  300.      * @param wholeCount total number of digits required to the right of the decimal point
  301.      * @param opts format options
  302.      * @return number of digits from {@code digits} appended to the output buffer
  303.      * @see #appendWhole(int, FormatOptions)
  304.      */
  305.     private int appendWholeGrouped(final int wholeCount, final FormatOptions opts) {
  306.         if (shouldIncludeMinus(opts)) {
  307.             append(opts.getMinusSign());
  308.         }

  309.         final char[] localizedDigits = opts.getDigits();
  310.         final char localizedZero = localizedDigits[0];
  311.         final char groupingChar = opts.getGroupingSeparator();

  312.         final int appendCount = Math.max(0, Math.min(wholeCount, digitCount));

  313.         if (appendCount > 0) {
  314.             int i;
  315.             int pos = wholeCount;
  316.             for (i = 0; i < appendCount; ++i, --pos) {
  317.                 appendLocalizedDigit(digits[i], localizedDigits);
  318.                 if (requiresGroupingSeparatorAfterPosition(pos)) {
  319.                     append(groupingChar);
  320.                 }
  321.             }

  322.             for (; i < wholeCount; ++i, --pos) {
  323.                 append(localizedZero);
  324.                 if (requiresGroupingSeparatorAfterPosition(pos)) {
  325.                     append(groupingChar);
  326.                 }
  327.             }
  328.         } else {
  329.             append(localizedZero);
  330.         }

  331.         return appendCount;
  332.     }

  333.     /**
  334.      * Gets the number of characters required for the digit portion of a string representation of
  335.      * this value. This excludes any exponent or thousands groupings characters.
  336.      * @param decimalPos decimal point position relative to the {@code digits} array
  337.      * @param opts format options
  338.      * @return number of characters required for the digit portion of a string representation of
  339.      *      this value
  340.      */
  341.     private int getDigitStringSize(final int decimalPos, final FormatOptions opts) {
  342.         int size = digitCount;
  343.         if (shouldIncludeMinus(opts)) {
  344.             ++size;
  345.         }
  346.         if (decimalPos < 1) {
  347.             // no whole component;
  348.             // add decimal point and leading zeros
  349.             size += 2 + Math.abs(decimalPos);
  350.         } else if (decimalPos >= digitCount) {
  351.             // no fraction component;
  352.             // add trailing zeros
  353.             size += decimalPos - digitCount;
  354.             if (opts.isIncludeFractionPlaceholder()) {
  355.                 size += 2;
  356.             }
  357.         } else {
  358.             // whole and fraction components;
  359.             // add decimal point
  360.             size += 1;
  361.         }

  362.         return size;
  363.     }

  364.     /**
  365.      * Gets the exponent value. This exponent produces a floating point value with the
  366.      * correct magnitude when applied to the internal unsigned integer.
  367.      * @return exponent value
  368.      */
  369.     public int getExponent() {
  370.         return exponent;
  371.     }

  372.     /**
  373.      * Gets the number of characters required to create a plain format representation
  374.      * of this value.
  375.      * @param decimalPos decimal position relative to the {@code digits} array
  376.      * @param opts format options
  377.      * @return number of characters in the plain string representation of this value,
  378.      *      created using the given parameters
  379.      */
  380.     private int getPlainStringSize(final int decimalPos, final FormatOptions opts) {
  381.         int size = getDigitStringSize(decimalPos, opts);

  382.         // adjust for groupings if needed
  383.         if (opts.isGroupThousands() && decimalPos > 0) {
  384.             size += (decimalPos - 1) / THOUSANDS_GROUP_SIZE;
  385.         }

  386.         return size;
  387.     }

  388.     /**
  389.      * Gets the exponent that would be used when representing this number in scientific
  390.      * notation (i.e., with a single non-zero digit in front of the decimal point).
  391.      * @return the exponent that would be used when representing this number in scientific
  392.      *      notation
  393.      */
  394.     public int getScientificExponent() {
  395.         return digitCount + exponent - 1;
  396.     }

  397.     /**
  398.      * Tests {@code true} if this value is equal to zero. The sign field is ignored,
  399.      * meaning that this method will return {@code true} for both {@code +0} and {@code -0}.
  400.      * @return {@code true} if the value is equal to zero
  401.      */
  402.     boolean isZero() {
  403.         return digits[0] == 0;
  404.     }

  405.     /**
  406.      * Ensures that this instance has <em>at most</em> the given number of significant digits
  407.      * (i.e. precision). If this instance already has a precision less than or equal
  408.      * to the argument, nothing is done. If the given precision requires a reduction in the number
  409.      * of digits, then the value is rounded using {@link java.math.RoundingMode#HALF_EVEN half-even rounding}.
  410.      * @param precision maximum number of significant digits to include
  411.      */
  412.     public void maxPrecision(final int precision) {
  413.         if (precision > 0 && precision < digitCount) {
  414.             if (shouldRoundUp(precision)) {
  415.                 roundUp(precision);
  416.             } else {
  417.                 truncate(precision);
  418.             }
  419.         }
  420.     }

  421.     /**
  422.      * Gets the output buffer as a string.
  423.      * @return output buffer as a string
  424.      */
  425.     private String outputString() {
  426.         final String str = String.valueOf(outputChars);
  427.         outputChars = null;
  428.         return str;
  429.     }

  430.     /**
  431.      * Prepares the output buffer for a string of the given size.
  432.      * @param size buffer size
  433.      */
  434.     private void prepareOutput(final int size) {
  435.         outputChars = new char[size];
  436.         outputIdx = 0;
  437.     }

  438.     /**
  439.      * Returns {@code true} if a grouping separator should be added after the whole digit
  440.      * character at the given position.
  441.      * @param pos whole digit character position, with values starting at 1 and increasing
  442.      *      from right to left.
  443.      * @return {@code true} if a grouping separator should be added
  444.      */
  445.     private boolean requiresGroupingSeparatorAfterPosition(final int pos) {
  446.         return pos > 1 && pos % THOUSANDS_GROUP_SIZE == 1;
  447.     }

  448.     /**
  449.      * Rounds the instance to the given decimal exponent position using
  450.      * {@link java.math.RoundingMode#HALF_EVEN half-even rounding}. For example, a value of {@code -2}
  451.      * will round the instance to the digit at the position 10<sup>-2</sup> (i.e. to the closest multiple of 0.01).
  452.      * @param roundExponent exponent defining the decimal place to round to
  453.      */
  454.     public void round(final int roundExponent) {
  455.         if (roundExponent > exponent) {
  456.             final int max = digitCount + exponent;

  457.             if (roundExponent < max) {
  458.                 // rounding to a decimal place less than the max; set max precision
  459.                 maxPrecision(max - roundExponent);
  460.             } else if (roundExponent == max && shouldRoundUp(0)) {
  461.                 // rounding up directly on the max decimal place
  462.                 setSingleDigitValue(1, roundExponent);
  463.             } else {
  464.                 // change to zero
  465.                 setSingleDigitValue(0, 0);
  466.             }
  467.         }
  468.     }

  469.     /**
  470.      * Rounds the value up to the given number of digits.
  471.      * @param count target number of digits; must be greater than zero and
  472.      *      less than the current number of digits
  473.      */
  474.     private void roundUp(final int count) {
  475.         int removedDigits = digitCount - count;
  476.         int i;
  477.         for (i = count - 1; i >= 0; --i) {
  478.             final int d = digits[i] + 1;

  479.             if (d < DECIMAL_RADIX) {
  480.                 // value did not carry over; done adding
  481.                 digits[i] = d;
  482.                 break;
  483.             }
  484.             // value carried over; the current position is 0
  485.             // which we will ignore by shortening the digit count
  486.             ++removedDigits;
  487.         }

  488.         if (i < 0) {
  489.             // all values carried over
  490.             setSingleDigitValue(1, exponent + removedDigits);
  491.         } else {
  492.             // values were updated in-place; just need to update the length
  493.             truncate(digitCount - removedDigits);
  494.         }
  495.     }

  496.     /**
  497.      * Sets the value of this instance to a single digit with the given exponent.
  498.      * The sign of the value is retained.
  499.      * @param digit digit value
  500.      * @param newExponent new exponent value
  501.      */
  502.     private void setSingleDigitValue(final int digit, final int newExponent) {
  503.         digits[0] = digit;
  504.         digitCount = 1;
  505.         exponent = newExponent;
  506.     }

  507.     /**
  508.      * Returns {@code true} if a formatted string with the given target exponent should include
  509.      * the exponent field.
  510.      * @param targetExponent exponent of the formatted result
  511.      * @param opts format options
  512.      * @return {@code true} if the formatted string should include the exponent field
  513.      */
  514.     private boolean shouldIncludeExponent(final int targetExponent, final FormatOptions opts) {
  515.         return targetExponent != 0 || opts.isAlwaysIncludeExponent();
  516.     }

  517.     /**
  518.      * Returns {@code true} if formatted strings should include the minus sign, considering
  519.      * the value of this instance and the given format options.
  520.      * @param opts format options
  521.      * @return {@code true} if a minus sign should be included in the output
  522.      */
  523.     private boolean shouldIncludeMinus(final FormatOptions opts) {
  524.         return negative && (opts.isSignedZero() || !isZero());
  525.     }

  526.     /**
  527.      * Returns {@code true} if a rounding operation for the given number of digits should
  528.      * round up.
  529.      * @param count number of digits to round to; must be greater than zero and less
  530.      *      than the current number of digits
  531.      * @return {@code true} if a rounding operation for the given number of digits should
  532.      *      round up
  533.      */
  534.     private boolean shouldRoundUp(final int count) {
  535.         // Round up in the following cases:
  536.         // 1. The digit after the last digit is greater than 5.
  537.         // 2. The digit after the last digit is 5 and there are additional (non-zero)
  538.         //      digits after it.
  539.         // 3. The digit after the last digit is 5, there are no additional digits afterward,
  540.         //      and the last digit is odd (half-even rounding).
  541.         final int digitAfterLast = digits[count];

  542.         return digitAfterLast > ROUND_CENTER || digitAfterLast == ROUND_CENTER
  543.                 && (count < digitCount - 1 || digits[count - 1] % 2 != 0);
  544.     }

  545.     /**
  546.      * Returns a string representation of this value in engineering notation. This is similar to {@link #toScientificString(FormatOptions) scientific notation}
  547.      * but with the exponent forced to be a multiple of 3, allowing easier alignment with SI prefixes.
  548.      * <p>
  549.      * For example:
  550.      * </p>
  551.      *
  552.      * <pre>
  553.      * 0 = "0.0"
  554.      * 10 = "10.0"
  555.      * 1e-6 = "1.0E-6"
  556.      * 1e11 = "100.0E9"
  557.      * </pre>
  558.      *
  559.      * @param opts format options
  560.      * @return value in engineering format
  561.      */
  562.     public String toEngineeringString(final FormatOptions opts) {
  563.         final int decimalPos = 1 + Math.floorMod(getScientificExponent(), ENG_EXPONENT_MOD);
  564.         return toScientificString(decimalPos, opts);
  565.     }

  566.     /**
  567.      * Returns a string representation of this value with no exponent field.
  568.      * <p>
  569.      * For example:
  570.      * </p>
  571.      *
  572.      * <pre>
  573.      * 10 = "10.0"
  574.      * 1e-6 = "0.000001"
  575.      * 1e11 = "100000000000.0"
  576.      * </pre>
  577.      *
  578.      * @param opts format options
  579.      * @return value in plain format
  580.      */
  581.     public String toPlainString(final FormatOptions opts) {
  582.         final int decimalPos = digitCount + exponent;
  583.         final int fractionZeroCount = decimalPos < 1
  584.                 ? Math.abs(decimalPos)
  585.                 : 0;

  586.         prepareOutput(getPlainStringSize(decimalPos, opts));

  587.         final int fractionStartIdx = opts.isGroupThousands()
  588.                 ? appendWholeGrouped(decimalPos, opts)
  589.                 : appendWhole(decimalPos, opts);

  590.         appendFraction(fractionZeroCount, fractionStartIdx, opts);

  591.         return outputString();
  592.     }

  593.     /**
  594.      * Returns a string representation of this value in scientific notation.
  595.      * <p>
  596.      * For example:
  597.      * </p>
  598.      *
  599.      * <pre>
  600.      * 0 = "0.0"
  601.      * 10 = "1.0E1"
  602.      * 1e-6 = "1.0E-6"
  603.      * 1e11 = "1.0E11"
  604.      * </pre>
  605.      *
  606.      * @param opts format options
  607.      * @return value in scientific format
  608.      */
  609.     public String toScientificString(final FormatOptions opts) {
  610.         return toScientificString(1, opts);
  611.     }

  612.     /**
  613.      * Returns a string representation of the value in scientific notation using the
  614.      * given decimal point position.
  615.      * @param decimalPos decimal position relative to the {@code digits} array; this value
  616.      *      is expected to be greater than 0
  617.      * @param opts format options
  618.      * @return value in scientific format
  619.      */
  620.     private String toScientificString(final int decimalPos, final FormatOptions opts) {
  621.         final int targetExponent = digitCount + exponent - decimalPos;
  622.         final int absTargetExponent = Math.abs(targetExponent);
  623.         final boolean includeExponent = shouldIncludeExponent(targetExponent, opts);
  624.         final boolean negativeExponent = targetExponent < 0;

  625.         // determine the size of the full formatted string, including the number of
  626.         // characters needed for the exponent digits
  627.         int size = getDigitStringSize(decimalPos, opts);
  628.         int exponentDigitCount = 0;
  629.         if (includeExponent) {
  630.             exponentDigitCount = absTargetExponent > 0
  631.                     ? (int) Math.floor(Math.log10(absTargetExponent)) + 1
  632.                     : 1;

  633.             size += opts.getExponentSeparatorChars().length + exponentDigitCount;
  634.             if (negativeExponent) {
  635.                 ++size;
  636.             }
  637.         }

  638.         prepareOutput(size);

  639.         // append the portion before the exponent field
  640.         final int fractionStartIdx = appendWhole(decimalPos, opts);
  641.         appendFraction(0, fractionStartIdx, opts);

  642.         if (includeExponent) {
  643.             // append the exponent field
  644.             append(opts.getExponentSeparatorChars());

  645.             if (negativeExponent) {
  646.                 append(opts.getMinusSign());
  647.             }

  648.             // append the exponent digits themselves; compute the
  649.             // string representation directly and add it to the output
  650.             // buffer to avoid the overhead of Integer.toString()
  651.             final char[] localizedDigits = opts.getDigits();
  652.             int rem = absTargetExponent;
  653.             for (int i = size - 1; i >= outputIdx; --i) {
  654.                 outputChars[i] = localizedDigits[rem % DECIMAL_RADIX];
  655.                 rem /= DECIMAL_RADIX;
  656.             }
  657.             outputIdx = size;
  658.         }

  659.         return outputString();
  660.     }

  661.     /**
  662.      * Truncates the value to the given number of digits.
  663.      * @param count number of digits; must be greater than zero and less than
  664.      *      the current number of digits
  665.      */
  666.     private void truncate(final int count) {
  667.         // trim all trailing zero digits, making sure to leave
  668.         // at least one digit left
  669.         int nonZeroCount = count;
  670.         for (int i = count - 1;
  671.                 i > 0 && digits[i] == 0;
  672.                 --i) {
  673.             --nonZeroCount;
  674.         }
  675.         exponent += digitCount - nonZeroCount;
  676.         digitCount = nonZeroCount;
  677.     }
  678. }