View Javadoc
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    *      https://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  /**
20   * Internal class representing a decimal value parsed into separate components. Each number
21   * is represented with
22   * <ul>
23   *  <li>a boolean flag for the sign,</li>
24   *  <li> a sequence of the digits {@code 0 - 10} representing an unsigned integer with leading and trailing zeros
25   *      removed, and</li>
26   *  <li>an exponent value that when applied to the base 10 digits produces a floating point value with the
27   *      correct magnitude.</li>
28   * </ul>
29   * <p><strong>Examples</strong></p>
30   * <table>
31   *  <tr><th>Double</th><th>Negative</th><th>Digits</th><th>Exponent</th></tr>
32   *  <tr><td>0.0</td><td>false</td><td>[0]</td><td>0</td></tr>
33   *  <tr><td>1.2</td><td>false</td><td>[1, 2]</td><td>-1</td></tr>
34   *  <tr><td>-0.00971</td><td>true</td><td>[9, 7, 1]</td><td>-5</td></tr>
35   *  <tr><td>56300</td><td>true</td><td>[5, 6, 3]</td><td>2</td></tr>
36   * </table>
37   */
38  final class ParsedDecimal {
39  
40      /**
41       * Interface containing values used during string formatting.
42       */
43      interface FormatOptions {
44  
45          /**
46           * Gets the decimal separator character.
47           *
48           * @return decimal separator character
49           */
50          char getDecimalSeparator();
51  
52          /**
53           * Gets an array containing the localized digit characters 0-9 in that order. This string <em>must</em> be non-null and have a length of 10.
54           *
55           * @return array containing the digit characters 0-9.
56           */
57          char[] getDigits();
58  
59          /**
60           * Gets the exponent separator as an array of characters.
61           *
62           * @return exponent separator as an array of characters.
63           */
64          char[] getExponentSeparatorChars();
65  
66          /**
67           * Gets the character used to separate thousands groupings.
68           *
69           * @return character used to separate thousands groupings.
70           */
71          char getGroupingSeparator();
72  
73          /**
74           * Gets the minus sign character.
75           *
76           * @return minus sign character.
77           */
78          char getMinusSign();
79  
80          /**
81           * Return {@code true} if exponent values should always be included in formatted output, even if the value is zero.
82           *
83           * @return {@code true} if exponent values should always be included.
84           */
85          boolean isAlwaysIncludeExponent();
86  
87          /**
88           * Return {@code true} if thousands should be grouped.
89           *
90           * @return {@code true} if thousand should be grouped.
91           */
92          boolean isGroupThousands();
93  
94          /**
95           * Return {@code true} if fraction placeholders (e.g., {@code ".0"} in {@code "1.0"}) should be included.
96           *
97           * @return {@code true} if fraction placeholders should be included.
98           */
99          boolean isIncludeFractionPlaceholder();
100 
101         /**
102          * Return {@code true} if the string zero should be prefixed with the minus sign for negative zero values.
103          *
104          * @return {@code true} if the minus zero string should be allowed.
105          */
106         boolean isSignedZero();
107     }
108 
109     /** Minus sign character. */
110     private static final char MINUS_CHAR = '-';
111 
112     /** Decimal separator character. */
113     private static final char DECIMAL_SEP_CHAR = '.';
114 
115     /** Exponent character. */
116     private static final char EXPONENT_CHAR = 'E';
117 
118     /** Zero digit character. */
119     private static final char ZERO_CHAR = '0';
120 
121     /** Number of characters in thousands groupings. */
122     private static final int THOUSANDS_GROUP_SIZE = 3;
123 
124     /** Radix for decimal numbers. */
125     private static final int DECIMAL_RADIX = 10;
126 
127     /** Center value used when rounding. */
128     private static final int ROUND_CENTER = DECIMAL_RADIX / 2;
129 
130     /** Number that exponents in engineering format must be a multiple of. */
131     private static final int ENG_EXPONENT_MOD = 3;
132 
133     /**
134      * Gets the numeric value of the given digit character. No validation of the
135      * character type is performed.
136      *
137      * @param ch digit character.
138      * @return numeric value of the digit character, ex: '1' = 1.
139      */
140     private static int digitValue(final char ch) {
141         return ch - ZERO_CHAR;
142     }
143 
144     /**
145      * Constructs a new instance from the given double value.
146      *
147      * @param d double value.
148      * @return a new instance containing the parsed components of the given double value.
149      * @throws IllegalArgumentException if {@code d} is {@code NaN} or infinite.
150      */
151     public static ParsedDecimal from(final double d) {
152         if (!Double.isFinite(d)) {
153             throw new IllegalArgumentException("Double is not finite");
154         }
155 
156         // Get the canonical string representation of the double value and parse
157         // it to extract the components of the decimal value. From the documentation
158         // of Double.toString() and the fact that d is finite, we are guaranteed the
159         // following:
160         // - the string will not be empty
161         // - it will contain exactly one decimal point character
162         // - all digit characters are in the ASCII range
163         final char[] strChars = Double.toString(d).toCharArray();
164 
165         final boolean negative = strChars[0] == MINUS_CHAR;
166         final int digitStartIdx = negative ? 1 : 0;
167 
168         final int[] digits = new int[strChars.length - digitStartIdx - 1];
169 
170         boolean foundDecimalPoint = false;
171         int digitCount = 0;
172         int significantDigitCount = 0;
173         int decimalPos = 0;
174 
175         int i;
176         for (i = digitStartIdx; i < strChars.length; ++i) {
177             final char ch = strChars[i];
178 
179             if (ch == DECIMAL_SEP_CHAR) {
180                 foundDecimalPoint = true;
181                 decimalPos = digitCount;
182             } else if (ch == EXPONENT_CHAR) {
183                 // no more mantissa digits
184                 break;
185             } else if (ch != ZERO_CHAR || digitCount > 0) {
186                 // this is either the first non-zero digit or one after it
187                 final int val = digitValue(ch);
188                 digits[digitCount++] = val;
189 
190                 if (val > 0) {
191                     significantDigitCount = digitCount;
192                 }
193             } else if (foundDecimalPoint) {
194                 // leading zero in a fraction; adjust the decimal position
195                 --decimalPos;
196             }
197         }
198 
199         if (digitCount > 0) {
200             // determine the exponent
201             final int explicitExponent = i < strChars.length
202                     ? parseExponent(strChars, i + 1)
203                     : 0;
204             final int exponent = explicitExponent + decimalPos - significantDigitCount;
205 
206             return new ParsedDecimal(negative, digits, significantDigitCount, exponent);
207         }
208 
209         // no non-zero digits, so value is zero
210         return new ParsedDecimal(negative, new int[] {0}, 1, 0);
211     }
212 
213     /**
214      * Parses a double exponent value from {@code chars}, starting at the {@code start}
215      * index and continuing through the end of the array.
216      *
217      * @param chars character array to parse a double exponent value from.
218      * @param start start index.
219      * @return parsed exponent value.
220      */
221     private static int parseExponent(final char[] chars, final int start) {
222         int i = start;
223         final boolean neg = chars[i] == MINUS_CHAR;
224         if (neg) {
225             ++i;
226         }
227 
228         int exp = 0;
229         for (; i < chars.length; ++i) {
230             exp = exp * DECIMAL_RADIX + digitValue(chars[i]);
231         }
232 
233         return neg ? -exp : exp;
234     }
235 
236     /** True if the value is negative. */
237     final boolean negative;
238 
239     /** Array containing the significant decimal digits for the value. */
240     final int[] digits;
241 
242     /** Number of digits used in the digits array; not necessarily equal to the length. */
243     int digitCount;
244 
245     /** Exponent for the value. */
246     int exponent;
247 
248     /** Output buffer for use in creating string representations. */
249     private char[] outputChars;
250 
251     /** Output buffer index. */
252     private int outputIdx;
253 
254     /**
255      * Constructs a new instance from its parts.
256      *
257      * @param negative {@code true} if the value is negative.
258      * @param digits array containing significant digits.
259      * @param digitCount number of digits used from the {@code digits} array.
260      * @param exponent exponent value.
261      */
262     private ParsedDecimal(final boolean negative, final int[] digits, final int digitCount,
263             final int exponent) {
264         this.negative = negative;
265         this.digits = digits;
266         this.digitCount = digitCount;
267         this.exponent = exponent;
268     }
269 
270     /**
271      * Appends the given character to the output buffer.
272      *
273      * @param ch character to append.
274      */
275     private void append(final char ch) {
276         outputChars[outputIdx++] = ch;
277     }
278 
279     /**
280      * Appends the given character array directly to the output buffer.
281      *
282      * @param chars characters to append.
283      */
284     private void append(final char[] chars) {
285         for (final char c : chars) {
286             append(c);
287         }
288     }
289 
290     /**
291      * Appends the fractional component of the number to the current output buffer.
292      *
293      * @param zeroCount number of zeros to add after the decimal point and before the first significant digit.
294      * @param startIdx  significant digit start index.
295      * @param opts      format options.
296      */
297     private void appendFraction(final int zeroCount, final int startIdx, final FormatOptions opts) {
298         final char[] localizedDigits = opts.getDigits();
299         final char localizedZero = localizedDigits[0];
300 
301         if (startIdx < digitCount) {
302             append(opts.getDecimalSeparator());
303 
304             // add the zero prefix
305             for (int i = 0; i < zeroCount; ++i) {
306                 append(localizedZero);
307             }
308 
309             // add the fraction digits
310             for (int i = startIdx; i < digitCount; ++i) {
311                 appendLocalizedDigit(digits[i], localizedDigits);
312             }
313         } else if (opts.isIncludeFractionPlaceholder()) {
314             append(opts.getDecimalSeparator());
315             append(localizedZero);
316         }
317     }
318 
319     /**
320      * Appends the localized representation of the digit {@code n} to the output buffer.
321      *
322      * @param n          digit to append.
323      * @param digitChars character array containing localized versions of the digits {@code 0-9} in that order.
324      */
325     private void appendLocalizedDigit(final int n, final char[] digitChars) {
326         append(digitChars[n]);
327     }
328 
329     /**
330      * Appends the whole number portion of this value to the output buffer. No thousands separators are added.
331      *
332      * @param wholeCount total number of digits required to the left of the decimal point.
333      * @param opts       format options.
334      * @return number of digits from {@code digits} appended to the output buffer.
335      * @see #appendWholeGrouped(int, FormatOptions)
336      */
337     private int appendWhole(final int wholeCount, final FormatOptions opts) {
338         if (shouldIncludeMinus(opts)) {
339             append(opts.getMinusSign());
340         }
341 
342         final char[] localizedDigits = opts.getDigits();
343         final char localizedZero = localizedDigits[0];
344 
345         final int significantDigitCount = Math.max(0, Math.min(wholeCount, digitCount));
346 
347         if (significantDigitCount > 0) {
348             int i;
349             for (i = 0; i < significantDigitCount; ++i) {
350                 appendLocalizedDigit(digits[i], localizedDigits);
351             }
352 
353             for (; i < wholeCount; ++i) {
354                 append(localizedZero);
355             }
356         } else {
357             append(localizedZero);
358         }
359 
360         return significantDigitCount;
361     }
362 
363     /**
364      * Appends the whole number portion of this value to the output buffer, adding thousands separators as needed.
365      *
366      * @param wholeCount total number of digits required to the right of the decimal point
367      * @param opts       format options.
368      * @return number of digits from {@code digits} appended to the output buffer.
369      * @see #appendWhole(int, FormatOptions)
370      */
371     private int appendWholeGrouped(final int wholeCount, final FormatOptions opts) {
372         if (shouldIncludeMinus(opts)) {
373             append(opts.getMinusSign());
374         }
375 
376         final char[] localizedDigits = opts.getDigits();
377         final char localizedZero = localizedDigits[0];
378         final char groupingChar = opts.getGroupingSeparator();
379 
380         final int appendCount = Math.max(0, Math.min(wholeCount, digitCount));
381 
382         if (appendCount > 0) {
383             int i;
384             int pos = wholeCount;
385             for (i = 0; i < appendCount; ++i, --pos) {
386                 appendLocalizedDigit(digits[i], localizedDigits);
387                 if (requiresGroupingSeparatorAfterPosition(pos)) {
388                     append(groupingChar);
389                 }
390             }
391 
392             for (; i < wholeCount; ++i, --pos) {
393                 append(localizedZero);
394                 if (requiresGroupingSeparatorAfterPosition(pos)) {
395                     append(groupingChar);
396                 }
397             }
398         } else {
399             append(localizedZero);
400         }
401 
402         return appendCount;
403     }
404 
405     /**
406      * Gets the number of characters required for the digit portion of a string representation of this value. This excludes any exponent or thousands groupings
407      * characters.
408      *
409      * @param decimalPos decimal point position relative to the {@code digits} array.
410      * @param opts       format options.
411      * @return number of characters required for the digit portion of a string representation of this value.
412      */
413     private int getDigitStringSize(final int decimalPos, final FormatOptions opts) {
414         int size = digitCount;
415         if (shouldIncludeMinus(opts)) {
416             ++size;
417         }
418         if (decimalPos < 1) {
419             // no whole component;
420             // add decimal point and leading zeros
421             size += 2 + Math.abs(decimalPos);
422         } else if (decimalPos >= digitCount) {
423             // no fraction component;
424             // add trailing zeros
425             size += decimalPos - digitCount;
426             if (opts.isIncludeFractionPlaceholder()) {
427                 size += 2;
428             }
429         } else {
430             // whole and fraction components;
431             // add decimal point
432             size += 1;
433         }
434 
435         return size;
436     }
437 
438     /**
439      * Gets the exponent value. This exponent produces a floating point value with the correct magnitude when applied to the internal unsigned integer.
440      *
441      * @return exponent value.
442      */
443     public int getExponent() {
444         return exponent;
445     }
446 
447     /**
448      * Gets the number of characters required to create a plain format representation of this value.
449      *
450      * @param decimalPos decimal position relative to the {@code digits} array.
451      * @param opts       format options.
452      * @return number of characters in the plain string representation of this value, created using the given parameters.
453      */
454     private int getPlainStringSize(final int decimalPos, final FormatOptions opts) {
455         int size = getDigitStringSize(decimalPos, opts);
456 
457         // adjust for groupings if needed
458         if (opts.isGroupThousands() && decimalPos > 0) {
459             size += (decimalPos - 1) / THOUSANDS_GROUP_SIZE;
460         }
461 
462         return size;
463     }
464 
465     /**
466      * Gets the exponent that would be used when representing this number in scientific notation (i.e., with a single non-zero digit in front of the decimal
467      * point).
468      *
469      * @return the exponent that would be used when representing this number in scientific notation.
470      */
471     public int getScientificExponent() {
472         return digitCount + exponent - 1;
473     }
474 
475     /**
476      * Tests {@code true} if this value is equal to zero. The sign field is ignored, meaning that this method will return {@code true} for both {@code +0} and
477      * {@code -0}.
478      *
479      * @return {@code true} if the value is equal to zero.
480      */
481     boolean isZero() {
482         return digits[0] == 0;
483     }
484 
485     /**
486      * Ensures that this instance has <em>at most</em> the given number of significant digits (i.e. precision). If this instance already has a precision less
487      * than or equal to the argument, nothing is done. If the given precision requires a reduction in the number of digits, then the value is rounded using
488      * {@link java.math.RoundingMode#HALF_EVEN half-even rounding}.
489      *
490      * @param precision maximum number of significant digits to include.
491      */
492     public void maxPrecision(final int precision) {
493         if (precision > 0 && precision < digitCount) {
494             if (shouldRoundUp(precision)) {
495                 roundUp(precision);
496             } else {
497                 truncate(precision);
498             }
499         }
500     }
501 
502     /**
503      * Gets the output buffer as a string.
504      *
505      * @return output buffer as a string
506      */
507     private String outputString() {
508         final String str = String.valueOf(outputChars);
509         outputChars = null;
510         return str;
511     }
512 
513     /**
514      * Prepares the output buffer for a string of the given size.
515      *
516      * @param size buffer size.
517      */
518     private void prepareOutput(final int size) {
519         outputChars = new char[size];
520         outputIdx = 0;
521     }
522 
523     /**
524      * Returns {@code true} if a grouping separator should be added after the whole digit character at the given position.
525      *
526      * @param pos whole digit character position, with values starting at 1 and increasing from right to left.
527      * @return {@code true} if a grouping separator should be added.
528      */
529     private boolean requiresGroupingSeparatorAfterPosition(final int pos) {
530         return pos > 1 && pos % THOUSANDS_GROUP_SIZE == 1;
531     }
532 
533     /**
534      * Rounds the instance to the given decimal exponent position using {@link java.math.RoundingMode#HALF_EVEN half-even rounding}. For example, a value of
535      * {@code -2} will round the instance to the digit at the position 10<sup>-2</sup> (i.e. to the closest multiple of 0.01).
536      *
537      * @param roundExponent exponent defining the decimal place to round to.
538      */
539     public void round(final int roundExponent) {
540         if (roundExponent > exponent) {
541             final int max = digitCount + exponent;
542 
543             if (roundExponent < max) {
544                 // rounding to a decimal place less than the max; set max precision
545                 maxPrecision(max - roundExponent);
546             } else if (roundExponent == max && shouldRoundUp(0)) {
547                 // rounding up directly on the max decimal place
548                 setSingleDigitValue(1, roundExponent);
549             } else {
550                 // change to zero
551                 setSingleDigitValue(0, 0);
552             }
553         }
554     }
555 
556     /**
557      * Rounds the value up to the given number of digits.
558      *
559      * @param count target number of digits; must be greater than zero and less than the current number of digits.
560      */
561     private void roundUp(final int count) {
562         int removedDigits = digitCount - count;
563         int i;
564         for (i = count - 1; i >= 0; --i) {
565             final int d = digits[i] + 1;
566 
567             if (d < DECIMAL_RADIX) {
568                 // value did not carry over; done adding
569                 digits[i] = d;
570                 break;
571             }
572             // value carried over; the current position is 0
573             // which we will ignore by shortening the digit count
574             ++removedDigits;
575         }
576 
577         if (i < 0) {
578             // all values carried over
579             setSingleDigitValue(1, exponent + removedDigits);
580         } else {
581             // values were updated in-place; just need to update the length
582             truncate(digitCount - removedDigits);
583         }
584     }
585 
586     /**
587      * Sets the value of this instance to a single digit with the given exponent. The sign of the value is retained.
588      *
589      * @param digit       digit value.
590      * @param newExponent new exponent value.
591      */
592     private void setSingleDigitValue(final int digit, final int newExponent) {
593         digits[0] = digit;
594         digitCount = 1;
595         exponent = newExponent;
596     }
597 
598     /**
599      * Returns {@code true} if a formatted string with the given target exponent should include the exponent field.
600      *
601      * @param targetExponent exponent of the formatted result.
602      * @param opts           format options.
603      * @return {@code true} if the formatted string should include the exponent field.
604      */
605     private boolean shouldIncludeExponent(final int targetExponent, final FormatOptions opts) {
606         return targetExponent != 0 || opts.isAlwaysIncludeExponent();
607     }
608 
609     /**
610      * Returns {@code true} if formatted strings should include the minus sign, considering the value of this instance and the given format options.
611      *
612      * @param opts format options.
613      * @return {@code true} if a minus sign should be included in the output.
614      */
615     private boolean shouldIncludeMinus(final FormatOptions opts) {
616         return negative && (opts.isSignedZero() || !isZero());
617     }
618 
619     /**
620      * Returns {@code true} if a rounding operation for the given number of digits should round up.
621      *
622      * @param count number of digits to round to; must be greater than zero and less than the current number of digits.
623      * @return {@code true} if a rounding operation for the given number of digits should round up.
624      */
625     private boolean shouldRoundUp(final int count) {
626         // Round up in the following cases:
627         // 1. The digit after the last digit is greater than 5.
628         // 2. The digit after the last digit is 5 and there are additional (non-zero)
629         //      digits after it.
630         // 3. The digit after the last digit is 5, there are no additional digits afterward,
631         //      and the last digit is odd (half-even rounding).
632         final int digitAfterLast = digits[count];
633 
634         return digitAfterLast > ROUND_CENTER || digitAfterLast == ROUND_CENTER
635                 && (count < digitCount - 1 || digits[count - 1] % 2 != 0);
636     }
637 
638     /**
639      * Returns a string representation of this value in engineering notation. This is similar to {@link #toScientificString(FormatOptions) scientific notation}
640      * but with the exponent forced to be a multiple of 3, allowing easier alignment with SI prefixes.
641      * <p>
642      * For example:
643      * </p>
644      *
645      * <pre>
646      * 0 = "0.0"
647      * 10 = "10.0"
648      * 1e-6 = "1.0E-6"
649      * 1e11 = "100.0E9"
650      * </pre>
651      *
652      * @param opts format options.
653      * @return value in engineering format.
654      */
655     public String toEngineeringString(final FormatOptions opts) {
656         final int decimalPos = 1 + Math.floorMod(getScientificExponent(), ENG_EXPONENT_MOD);
657         return toScientificString(decimalPos, opts);
658     }
659 
660     /**
661      * Returns a string representation of this value with no exponent field.
662      * <p>
663      * For example:
664      * </p>
665      *
666      * <pre>
667      * 10 = "10.0"
668      * 1e-6 = "0.000001"
669      * 1e11 = "100000000000.0"
670      * </pre>
671      *
672      * @param opts format options.
673      * @return value in plain format.
674      */
675     public String toPlainString(final FormatOptions opts) {
676         final int decimalPos = digitCount + exponent;
677         final int fractionZeroCount = decimalPos < 1
678                 ? Math.abs(decimalPos)
679                 : 0;
680 
681         prepareOutput(getPlainStringSize(decimalPos, opts));
682 
683         final int fractionStartIdx = opts.isGroupThousands()
684                 ? appendWholeGrouped(decimalPos, opts)
685                 : appendWhole(decimalPos, opts);
686 
687         appendFraction(fractionZeroCount, fractionStartIdx, opts);
688 
689         return outputString();
690     }
691 
692     /**
693      * Returns a string representation of this value in scientific notation.
694      * <p>
695      * For example:
696      * </p>
697      *
698      * <pre>
699      * 0 = "0.0"
700      * 10 = "1.0E1"
701      * 1e-6 = "1.0E-6"
702      * 1e11 = "1.0E11"
703      * </pre>
704      *
705      * @param opts format options.
706      * @return value in scientific format.
707      */
708     public String toScientificString(final FormatOptions opts) {
709         return toScientificString(1, opts);
710     }
711 
712     /**
713      * Returns a string representation of the value in scientific notation using the given decimal point position.
714      *
715      * @param decimalPos decimal position relative to the {@code digits} array; this value is expected to be greater than 0.
716      * @param opts       format options.
717      * @return value in scientific format.
718      */
719     private String toScientificString(final int decimalPos, final FormatOptions opts) {
720         final int targetExponent = digitCount + exponent - decimalPos;
721         final int absTargetExponent = Math.abs(targetExponent);
722         final boolean includeExponent = shouldIncludeExponent(targetExponent, opts);
723         final boolean negativeExponent = targetExponent < 0;
724 
725         // determine the size of the full formatted string, including the number of
726         // characters needed for the exponent digits
727         int size = getDigitStringSize(decimalPos, opts);
728         int exponentDigitCount = 0;
729         if (includeExponent) {
730             exponentDigitCount = absTargetExponent > 0
731                     ? (int) Math.floor(Math.log10(absTargetExponent)) + 1
732                     : 1;
733 
734             size += opts.getExponentSeparatorChars().length + exponentDigitCount;
735             if (negativeExponent) {
736                 ++size;
737             }
738         }
739 
740         prepareOutput(size);
741 
742         // append the portion before the exponent field
743         final int fractionStartIdx = appendWhole(decimalPos, opts);
744         appendFraction(0, fractionStartIdx, opts);
745 
746         if (includeExponent) {
747             // append the exponent field
748             append(opts.getExponentSeparatorChars());
749 
750             if (negativeExponent) {
751                 append(opts.getMinusSign());
752             }
753 
754             // append the exponent digits themselves; compute the
755             // string representation directly and add it to the output
756             // buffer to avoid the overhead of Integer.toString()
757             final char[] localizedDigits = opts.getDigits();
758             int rem = absTargetExponent;
759             for (int i = size - 1; i >= outputIdx; --i) {
760                 outputChars[i] = localizedDigits[rem % DECIMAL_RADIX];
761                 rem /= DECIMAL_RADIX;
762             }
763             outputIdx = size;
764         }
765 
766         return outputString();
767     }
768 
769     /**
770      * Truncates the value to the given number of digits.
771      *
772      * @param count number of digits; must be greater than zero and less than the current number of digits.
773      */
774     private void truncate(final int count) {
775         // trim all trailing zero digits, making sure to leave
776         // at least one digit left
777         int nonZeroCount = count;
778         for (int i = count - 1;
779                 i > 0 && digits[i] == 0;
780                 --i) {
781             --nonZeroCount;
782         }
783         exponent += digitCount - nonZeroCount;
784         digitCount = nonZeroCount;
785     }
786 }