ParsedDecimal.java
- /*
- * 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.
- */
- package org.apache.commons.text.numbers;
- /**
- * Internal class representing a decimal value parsed into separate components. Each number
- * is represented with
- * <ul>
- * <li>a boolean flag for the sign,</li>
- * <li> a sequence of the digits {@code 0 - 10} representing an unsigned integer with leading and trailing zeros
- * removed, and</li>
- * <li>an exponent value that when applied to the base 10 digits produces a floating point value with the
- * correct magnitude.</li>
- * </ul>
- * <p><strong>Examples</strong></p>
- * <table>
- * <tr><th>Double</th><th>Negative</th><th>Digits</th><th>Exponent</th></tr>
- * <tr><td>0.0</td><td>false</td><td>[0]</td><td>0</td></tr>
- * <tr><td>1.2</td><td>false</td><td>[1, 2]</td><td>-1</td></tr>
- * <tr><td>-0.00971</td><td>true</td><td>[9, 7, 1]</td><td>-5</td></tr>
- * <tr><td>56300</td><td>true</td><td>[5, 6, 3]</td><td>2</td></tr>
- * </table>
- */
- final class ParsedDecimal {
- /**
- * Interface containing values used during string formatting.
- */
- interface FormatOptions {
- /**
- * Gets the decimal separator character.
- * @return decimal separator character
- */
- char getDecimalSeparator();
- /**
- * 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.
- * @return array containing the digit characters 0-9
- */
- char[] getDigits();
- /**
- * Gets the exponent separator as an array of characters.
- * @return exponent separator as an array of characters
- */
- char[] getExponentSeparatorChars();
- /**
- * Gets the character used to separate thousands groupings.
- * @return character used to separate thousands groupings
- */
- char getGroupingSeparator();
- /**
- * Gets the minus sign character.
- * @return minus sign character
- */
- char getMinusSign();
- /**
- * Return {@code true} if exponent values should always be included in
- * formatted output, even if the value is zero.
- * @return {@code true} if exponent values should always be included
- */
- boolean isAlwaysIncludeExponent();
- /**
- * Return {@code true} if thousands should be grouped.
- * @return {@code true} if thousand should be grouped
- */
- boolean isGroupThousands();
- /**
- * Return {@code true} if fraction placeholders (e.g., {@code ".0"} in {@code "1.0"})
- * should be included.
- * @return {@code true} if fraction placeholders should be included
- */
- boolean isIncludeFractionPlaceholder();
- /**
- * Return {@code true} if the string zero should be prefixed with the minus sign
- * for negative zero values.
- * @return {@code true} if the minus zero string should be allowed
- */
- boolean isSignedZero();
- }
- /** Minus sign character. */
- private static final char MINUS_CHAR = '-';
- /** Decimal separator character. */
- private static final char DECIMAL_SEP_CHAR = '.';
- /** Exponent character. */
- private static final char EXPONENT_CHAR = 'E';
- /** Zero digit character. */
- private static final char ZERO_CHAR = '0';
- /** Number of characters in thousands groupings. */
- private static final int THOUSANDS_GROUP_SIZE = 3;
- /** Radix for decimal numbers. */
- private static final int DECIMAL_RADIX = 10;
- /** Center value used when rounding. */
- private static final int ROUND_CENTER = DECIMAL_RADIX / 2;
- /** Number that exponents in engineering format must be a multiple of. */
- private static final int ENG_EXPONENT_MOD = 3;
- /**
- * Gets the numeric value of the given digit character. No validation of the
- * character type is performed.
- * @param ch digit character
- * @return numeric value of the digit character, ex: '1' = 1
- */
- private static int digitValue(final char ch) {
- return ch - ZERO_CHAR;
- }
- /**
- * Constructs a new instance from the given double value.
- * @param d double value
- * @return a new instance containing the parsed components of the given double value
- * @throws IllegalArgumentException if {@code d} is {@code NaN} or infinite
- */
- public static ParsedDecimal from(final double d) {
- if (!Double.isFinite(d)) {
- throw new IllegalArgumentException("Double is not finite");
- }
- // Get the canonical string representation of the double value and parse
- // it to extract the components of the decimal value. From the documentation
- // of Double.toString() and the fact that d is finite, we are guaranteed the
- // following:
- // - the string will not be empty
- // - it will contain exactly one decimal point character
- // - all digit characters are in the ASCII range
- final char[] strChars = Double.toString(d).toCharArray();
- final boolean negative = strChars[0] == MINUS_CHAR;
- final int digitStartIdx = negative ? 1 : 0;
- final int[] digits = new int[strChars.length - digitStartIdx - 1];
- boolean foundDecimalPoint = false;
- int digitCount = 0;
- int significantDigitCount = 0;
- int decimalPos = 0;
- int i;
- for (i = digitStartIdx; i < strChars.length; ++i) {
- final char ch = strChars[i];
- if (ch == DECIMAL_SEP_CHAR) {
- foundDecimalPoint = true;
- decimalPos = digitCount;
- } else if (ch == EXPONENT_CHAR) {
- // no more mantissa digits
- break;
- } else if (ch != ZERO_CHAR || digitCount > 0) {
- // this is either the first non-zero digit or one after it
- final int val = digitValue(ch);
- digits[digitCount++] = val;
- if (val > 0) {
- significantDigitCount = digitCount;
- }
- } else if (foundDecimalPoint) {
- // leading zero in a fraction; adjust the decimal position
- --decimalPos;
- }
- }
- if (digitCount > 0) {
- // determine the exponent
- final int explicitExponent = i < strChars.length
- ? parseExponent(strChars, i + 1)
- : 0;
- final int exponent = explicitExponent + decimalPos - significantDigitCount;
- return new ParsedDecimal(negative, digits, significantDigitCount, exponent);
- }
- // no non-zero digits, so value is zero
- return new ParsedDecimal(negative, new int[] {0}, 1, 0);
- }
- /**
- * Parses a double exponent value from {@code chars}, starting at the {@code start}
- * index and continuing through the end of the array.
- * @param chars character array to parse a double exponent value from
- * @param start start index
- * @return parsed exponent value
- */
- private static int parseExponent(final char[] chars, final int start) {
- int i = start;
- final boolean neg = chars[i] == MINUS_CHAR;
- if (neg) {
- ++i;
- }
- int exp = 0;
- for (; i < chars.length; ++i) {
- exp = exp * DECIMAL_RADIX + digitValue(chars[i]);
- }
- return neg ? -exp : exp;
- }
- /** True if the value is negative. */
- final boolean negative;
- /** Array containing the significant decimal digits for the value. */
- final int[] digits;
- /** Number of digits used in the digits array; not necessarily equal to the length. */
- int digitCount;
- /** Exponent for the value. */
- int exponent;
- /** Output buffer for use in creating string representations. */
- private char[] outputChars;
- /** Output buffer index. */
- private int outputIdx;
- /**
- * Constructs a new instance from its parts.
- * @param negative {@code true} if the value is negative
- * @param digits array containing significant digits
- * @param digitCount number of digits used from the {@code digits} array
- * @param exponent exponent value
- */
- private ParsedDecimal(final boolean negative, final int[] digits, final int digitCount,
- final int exponent) {
- this.negative = negative;
- this.digits = digits;
- this.digitCount = digitCount;
- this.exponent = exponent;
- }
- /**
- * Appends the given character to the output buffer.
- * @param ch character to append
- */
- private void append(final char ch) {
- outputChars[outputIdx++] = ch;
- }
- /**
- * Appends the given character array directly to the output buffer.
- * @param chars characters to append
- */
- private void append(final char[] chars) {
- for (final char c : chars) {
- append(c);
- }
- }
- /**
- * Appends the fractional component of the number to the current output buffer.
- * @param zeroCount number of zeros to add after the decimal point and before the
- * first significant digit
- * @param startIdx significant digit start index
- * @param opts format options
- */
- private void appendFraction(final int zeroCount, final int startIdx, final FormatOptions opts) {
- final char[] localizedDigits = opts.getDigits();
- final char localizedZero = localizedDigits[0];
- if (startIdx < digitCount) {
- append(opts.getDecimalSeparator());
- // add the zero prefix
- for (int i = 0; i < zeroCount; ++i) {
- append(localizedZero);
- }
- // add the fraction digits
- for (int i = startIdx; i < digitCount; ++i) {
- appendLocalizedDigit(digits[i], localizedDigits);
- }
- } else if (opts.isIncludeFractionPlaceholder()) {
- append(opts.getDecimalSeparator());
- append(localizedZero);
- }
- }
- /**
- * Appends the localized representation of the digit {@code n} to the output buffer.
- * @param n digit to append
- * @param digitChars character array containing localized versions of the digits {@code 0-9}
- * in that order
- */
- private void appendLocalizedDigit(final int n, final char[] digitChars) {
- append(digitChars[n]);
- }
- /**
- * Appends the whole number portion of this value to the output buffer. No thousands
- * separators are added.
- * @param wholeCount total number of digits required to the left of the decimal point
- * @param opts format options
- * @return number of digits from {@code digits} appended to the output buffer
- * @see #appendWholeGrouped(int, FormatOptions)
- */
- private int appendWhole(final int wholeCount, final FormatOptions opts) {
- if (shouldIncludeMinus(opts)) {
- append(opts.getMinusSign());
- }
- final char[] localizedDigits = opts.getDigits();
- final char localizedZero = localizedDigits[0];
- final int significantDigitCount = Math.max(0, Math.min(wholeCount, digitCount));
- if (significantDigitCount > 0) {
- int i;
- for (i = 0; i < significantDigitCount; ++i) {
- appendLocalizedDigit(digits[i], localizedDigits);
- }
- for (; i < wholeCount; ++i) {
- append(localizedZero);
- }
- } else {
- append(localizedZero);
- }
- return significantDigitCount;
- }
- /**
- * Appends the whole number portion of this value to the output buffer, adding thousands
- * separators as needed.
- * @param wholeCount total number of digits required to the right of the decimal point
- * @param opts format options
- * @return number of digits from {@code digits} appended to the output buffer
- * @see #appendWhole(int, FormatOptions)
- */
- private int appendWholeGrouped(final int wholeCount, final FormatOptions opts) {
- if (shouldIncludeMinus(opts)) {
- append(opts.getMinusSign());
- }
- final char[] localizedDigits = opts.getDigits();
- final char localizedZero = localizedDigits[0];
- final char groupingChar = opts.getGroupingSeparator();
- final int appendCount = Math.max(0, Math.min(wholeCount, digitCount));
- if (appendCount > 0) {
- int i;
- int pos = wholeCount;
- for (i = 0; i < appendCount; ++i, --pos) {
- appendLocalizedDigit(digits[i], localizedDigits);
- if (requiresGroupingSeparatorAfterPosition(pos)) {
- append(groupingChar);
- }
- }
- for (; i < wholeCount; ++i, --pos) {
- append(localizedZero);
- if (requiresGroupingSeparatorAfterPosition(pos)) {
- append(groupingChar);
- }
- }
- } else {
- append(localizedZero);
- }
- return appendCount;
- }
- /**
- * Gets the number of characters required for the digit portion of a string representation of
- * this value. This excludes any exponent or thousands groupings characters.
- * @param decimalPos decimal point position relative to the {@code digits} array
- * @param opts format options
- * @return number of characters required for the digit portion of a string representation of
- * this value
- */
- private int getDigitStringSize(final int decimalPos, final FormatOptions opts) {
- int size = digitCount;
- if (shouldIncludeMinus(opts)) {
- ++size;
- }
- if (decimalPos < 1) {
- // no whole component;
- // add decimal point and leading zeros
- size += 2 + Math.abs(decimalPos);
- } else if (decimalPos >= digitCount) {
- // no fraction component;
- // add trailing zeros
- size += decimalPos - digitCount;
- if (opts.isIncludeFractionPlaceholder()) {
- size += 2;
- }
- } else {
- // whole and fraction components;
- // add decimal point
- size += 1;
- }
- return size;
- }
- /**
- * Gets the exponent value. This exponent produces a floating point value with the
- * correct magnitude when applied to the internal unsigned integer.
- * @return exponent value
- */
- public int getExponent() {
- return exponent;
- }
- /**
- * Gets the number of characters required to create a plain format representation
- * of this value.
- * @param decimalPos decimal position relative to the {@code digits} array
- * @param opts format options
- * @return number of characters in the plain string representation of this value,
- * created using the given parameters
- */
- private int getPlainStringSize(final int decimalPos, final FormatOptions opts) {
- int size = getDigitStringSize(decimalPos, opts);
- // adjust for groupings if needed
- if (opts.isGroupThousands() && decimalPos > 0) {
- size += (decimalPos - 1) / THOUSANDS_GROUP_SIZE;
- }
- return size;
- }
- /**
- * 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 point).
- * @return the exponent that would be used when representing this number in scientific
- * notation
- */
- public int getScientificExponent() {
- return digitCount + exponent - 1;
- }
- /**
- * 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 {@code -0}.
- * @return {@code true} if the value is equal to zero
- */
- boolean isZero() {
- return digits[0] == 0;
- }
- /**
- * 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 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 {@link java.math.RoundingMode#HALF_EVEN half-even rounding}.
- * @param precision maximum number of significant digits to include
- */
- public void maxPrecision(final int precision) {
- if (precision > 0 && precision < digitCount) {
- if (shouldRoundUp(precision)) {
- roundUp(precision);
- } else {
- truncate(precision);
- }
- }
- }
- /**
- * Gets the output buffer as a string.
- * @return output buffer as a string
- */
- private String outputString() {
- final String str = String.valueOf(outputChars);
- outputChars = null;
- return str;
- }
- /**
- * Prepares the output buffer for a string of the given size.
- * @param size buffer size
- */
- private void prepareOutput(final int size) {
- outputChars = new char[size];
- outputIdx = 0;
- }
- /**
- * Returns {@code true} if a grouping separator should be added after the whole digit
- * character at the given position.
- * @param pos whole digit character position, with values starting at 1 and increasing
- * from right to left.
- * @return {@code true} if a grouping separator should be added
- */
- private boolean requiresGroupingSeparatorAfterPosition(final int pos) {
- return pos > 1 && pos % THOUSANDS_GROUP_SIZE == 1;
- }
- /**
- * Rounds the instance to the given decimal exponent position using
- * {@link java.math.RoundingMode#HALF_EVEN half-even rounding}. For example, a value of {@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).
- * @param roundExponent exponent defining the decimal place to round to
- */
- public void round(final int roundExponent) {
- if (roundExponent > exponent) {
- final int max = digitCount + exponent;
- if (roundExponent < max) {
- // rounding to a decimal place less than the max; set max precision
- maxPrecision(max - roundExponent);
- } else if (roundExponent == max && shouldRoundUp(0)) {
- // rounding up directly on the max decimal place
- setSingleDigitValue(1, roundExponent);
- } else {
- // change to zero
- setSingleDigitValue(0, 0);
- }
- }
- }
- /**
- * Rounds the value up to the given number of digits.
- * @param count target number of digits; must be greater than zero and
- * less than the current number of digits
- */
- private void roundUp(final int count) {
- int removedDigits = digitCount - count;
- int i;
- for (i = count - 1; i >= 0; --i) {
- final int d = digits[i] + 1;
- if (d < DECIMAL_RADIX) {
- // value did not carry over; done adding
- digits[i] = d;
- break;
- }
- // value carried over; the current position is 0
- // which we will ignore by shortening the digit count
- ++removedDigits;
- }
- if (i < 0) {
- // all values carried over
- setSingleDigitValue(1, exponent + removedDigits);
- } else {
- // values were updated in-place; just need to update the length
- truncate(digitCount - removedDigits);
- }
- }
- /**
- * Sets the value of this instance to a single digit with the given exponent.
- * The sign of the value is retained.
- * @param digit digit value
- * @param newExponent new exponent value
- */
- private void setSingleDigitValue(final int digit, final int newExponent) {
- digits[0] = digit;
- digitCount = 1;
- exponent = newExponent;
- }
- /**
- * Returns {@code true} if a formatted string with the given target exponent should include
- * the exponent field.
- * @param targetExponent exponent of the formatted result
- * @param opts format options
- * @return {@code true} if the formatted string should include the exponent field
- */
- private boolean shouldIncludeExponent(final int targetExponent, final FormatOptions opts) {
- return targetExponent != 0 || opts.isAlwaysIncludeExponent();
- }
- /**
- * Returns {@code true} if formatted strings should include the minus sign, considering
- * the value of this instance and the given format options.
- * @param opts format options
- * @return {@code true} if a minus sign should be included in the output
- */
- private boolean shouldIncludeMinus(final FormatOptions opts) {
- return negative && (opts.isSignedZero() || !isZero());
- }
- /**
- * Returns {@code true} if a rounding operation for the given number of digits should
- * round up.
- * @param count number of digits to round to; must be greater than zero and less
- * than the current number of digits
- * @return {@code true} if a rounding operation for the given number of digits should
- * round up
- */
- private boolean shouldRoundUp(final int count) {
- // Round up in the following cases:
- // 1. The digit after the last digit is greater than 5.
- // 2. The digit after the last digit is 5 and there are additional (non-zero)
- // digits after it.
- // 3. The digit after the last digit is 5, there are no additional digits afterward,
- // and the last digit is odd (half-even rounding).
- final int digitAfterLast = digits[count];
- return digitAfterLast > ROUND_CENTER || digitAfterLast == ROUND_CENTER
- && (count < digitCount - 1 || digits[count - 1] % 2 != 0);
- }
- /**
- * Returns a string representation of this value in engineering notation. This is similar to {@link #toScientificString(FormatOptions) scientific notation}
- * but with the exponent forced to be a multiple of 3, allowing easier alignment with SI prefixes.
- * <p>
- * For example:
- * </p>
- *
- * <pre>
- * 0 = "0.0"
- * 10 = "10.0"
- * 1e-6 = "1.0E-6"
- * 1e11 = "100.0E9"
- * </pre>
- *
- * @param opts format options
- * @return value in engineering format
- */
- public String toEngineeringString(final FormatOptions opts) {
- final int decimalPos = 1 + Math.floorMod(getScientificExponent(), ENG_EXPONENT_MOD);
- return toScientificString(decimalPos, opts);
- }
- /**
- * Returns a string representation of this value with no exponent field.
- * <p>
- * For example:
- * </p>
- *
- * <pre>
- * 10 = "10.0"
- * 1e-6 = "0.000001"
- * 1e11 = "100000000000.0"
- * </pre>
- *
- * @param opts format options
- * @return value in plain format
- */
- public String toPlainString(final FormatOptions opts) {
- final int decimalPos = digitCount + exponent;
- final int fractionZeroCount = decimalPos < 1
- ? Math.abs(decimalPos)
- : 0;
- prepareOutput(getPlainStringSize(decimalPos, opts));
- final int fractionStartIdx = opts.isGroupThousands()
- ? appendWholeGrouped(decimalPos, opts)
- : appendWhole(decimalPos, opts);
- appendFraction(fractionZeroCount, fractionStartIdx, opts);
- return outputString();
- }
- /**
- * Returns a string representation of this value in scientific notation.
- * <p>
- * For example:
- * </p>
- *
- * <pre>
- * 0 = "0.0"
- * 10 = "1.0E1"
- * 1e-6 = "1.0E-6"
- * 1e11 = "1.0E11"
- * </pre>
- *
- * @param opts format options
- * @return value in scientific format
- */
- public String toScientificString(final FormatOptions opts) {
- return toScientificString(1, opts);
- }
- /**
- * Returns a string representation of the value in scientific notation using the
- * given decimal point position.
- * @param decimalPos decimal position relative to the {@code digits} array; this value
- * is expected to be greater than 0
- * @param opts format options
- * @return value in scientific format
- */
- private String toScientificString(final int decimalPos, final FormatOptions opts) {
- final int targetExponent = digitCount + exponent - decimalPos;
- final int absTargetExponent = Math.abs(targetExponent);
- final boolean includeExponent = shouldIncludeExponent(targetExponent, opts);
- final boolean negativeExponent = targetExponent < 0;
- // determine the size of the full formatted string, including the number of
- // characters needed for the exponent digits
- int size = getDigitStringSize(decimalPos, opts);
- int exponentDigitCount = 0;
- if (includeExponent) {
- exponentDigitCount = absTargetExponent > 0
- ? (int) Math.floor(Math.log10(absTargetExponent)) + 1
- : 1;
- size += opts.getExponentSeparatorChars().length + exponentDigitCount;
- if (negativeExponent) {
- ++size;
- }
- }
- prepareOutput(size);
- // append the portion before the exponent field
- final int fractionStartIdx = appendWhole(decimalPos, opts);
- appendFraction(0, fractionStartIdx, opts);
- if (includeExponent) {
- // append the exponent field
- append(opts.getExponentSeparatorChars());
- if (negativeExponent) {
- append(opts.getMinusSign());
- }
- // append the exponent digits themselves; compute the
- // string representation directly and add it to the output
- // buffer to avoid the overhead of Integer.toString()
- final char[] localizedDigits = opts.getDigits();
- int rem = absTargetExponent;
- for (int i = size - 1; i >= outputIdx; --i) {
- outputChars[i] = localizedDigits[rem % DECIMAL_RADIX];
- rem /= DECIMAL_RADIX;
- }
- outputIdx = size;
- }
- return outputString();
- }
- /**
- * Truncates the value to the given number of digits.
- * @param count number of digits; must be greater than zero and less than
- * the current number of digits
- */
- private void truncate(final int count) {
- // trim all trailing zero digits, making sure to leave
- // at least one digit left
- int nonZeroCount = count;
- for (int i = count - 1;
- i > 0 && digits[i] == 0;
- --i) {
- --nonZeroCount;
- }
- exponent += digitCount - nonZeroCount;
- digitCount = nonZeroCount;
- }
- }