All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.commons.text.numbers.ParsedDecimal Maven / Gradle / Ivy

The newest version!
/*
 * 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
 * 
    *
  • a boolean flag for the sign,
  • *
  • a sequence of the digits {@code 0 - 10} representing an unsigned integer with leading and trailing zeros * removed, and
  • *
  • an exponent value that when applied to the base 10 digits produces a floating point value with the * correct magnitude.
  • *
*

Examples

* * * * * * *
DoubleNegativeDigitsExponent
0.0false[0]0
1.2false[1, 2]-1
-0.00971true[9, 7, 1]-5
56300true[5, 6, 3]2
*/ 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 must 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 at most 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-2 (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. *

* For example: *

* *
     * 0 = "0.0"
     * 10 = "10.0"
     * 1e-6 = "1.0E-6"
     * 1e11 = "100.0E9"
     * 
* * @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. *

* For example: *

* *
     * 10 = "10.0"
     * 1e-6 = "0.000001"
     * 1e11 = "100000000000.0"
     * 
* * @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. *

* For example: *

* *
     * 0 = "0.0"
     * 10 = "1.0E1"
     * 1e-6 = "1.0E-6"
     * 1e11 = "1.0E11"
     * 
* * @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; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy