This is an automated email from the ASF dual-hosted git repository. ggregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-text.git
commit 04a79525d8cd7d7e00c78d3bd8c03c9230763d2a Author: Gary Gregory <garydgreg...@gmail.com> AuthorDate: Sat Jul 24 11:19:19 2021 -0400 Sort members. --- .../apache/commons/text/numbers/DoubleFormat.java | 722 ++++++++--------- .../apache/commons/text/numbers/ParsedDecimal.java | 854 ++++++++++----------- 2 files changed, 788 insertions(+), 788 deletions(-) diff --git a/src/main/java/org/apache/commons/text/numbers/DoubleFormat.java b/src/main/java/org/apache/commons/text/numbers/DoubleFormat.java index 5269a90..1522330 100644 --- a/src/main/java/org/apache/commons/text/numbers/DoubleFormat.java +++ b/src/main/java/org/apache/commons/text/numbers/DoubleFormat.java @@ -145,23 +145,166 @@ public enum DoubleFormat { */ MIXED(MixedDoubleFormat::new); - /** Function used to construct instances for this format type. */ - private final Function<Builder, DoubleFunction<String>> factory; - /** - * Constructs a new instance. - * @param factory function used to construct format instances + * Base class for standard double formatting classes. */ - DoubleFormat(final Function<Builder, DoubleFunction<String>> factory) { - this.factory = factory; - } + private abstract static class AbstractDoubleFormat + implements DoubleFunction<String>, ParsedDecimal.FormatOptions { - /** - * Creates a {@link Builder} for building formatter functions for this format type. - * @return builder instance - */ - public Builder builder() { - return new Builder(factory); + /** Maximum precision; 0 indicates no limit. */ + private final int maxPrecision; + + /** Minimum decimal exponent. */ + private final int minDecimalExponent; + + /** String representing positive infinity. */ + private final String positiveInfinity; + + /** String representing negative infinity. */ + private final String negativeInfinity; + + /** String representing NaN. */ + private final String nan; + + /** Flag determining if fraction placeholders should be used. */ + private final boolean fractionPlaceholder; + + /** Flag determining if signed zero strings are allowed. */ + private final boolean signedZero; + + /** String containing the digits 0-9. */ + private final char[] digits; + + /** Decimal separator character. */ + private final char decimalSeparator; + + /** Thousands grouping separator. */ + private final char groupingSeparator; + + /** Flag indicating if thousands should be grouped. */ + private final boolean groupThousands; + + /** Minus sign character. */ + private final char minusSign; + + /** Exponent separator character. */ + private final char[] exponentSeparatorChars; + + /** Flag indicating if exponent values should always be included, even if zero. */ + private final boolean alwaysIncludeExponent; + + /** + * Constructs a new instance. + * @param builder builder instance containing configuration values + */ + AbstractDoubleFormat(final Builder builder) { + this.maxPrecision = builder.maxPrecision; + this.minDecimalExponent = builder.minDecimalExponent; + + this.positiveInfinity = builder.infinity; + this.negativeInfinity = builder.minusSign + builder.infinity; + this.nan = builder.nan; + + this.fractionPlaceholder = builder.fractionPlaceholder; + this.signedZero = builder.signedZero; + this.digits = builder.digits.toCharArray(); + this.decimalSeparator = builder.decimalSeparator; + this.groupingSeparator = builder.groupingSeparator; + this.groupThousands = builder.groupThousands; + this.minusSign = builder.minusSign; + this.exponentSeparatorChars = builder.exponentSeparator.toCharArray(); + this.alwaysIncludeExponent = builder.alwaysIncludeExponent; + } + + /** {@inheritDoc} */ + @Override + public String apply(final double d) { + if (Double.isFinite(d)) { + return applyFinite(d); + } else if (Double.isInfinite(d)) { + return d > 0.0 + ? positiveInfinity + : negativeInfinity; + } + return nan; + } + + /** + * Returns a formatted string representation of the given finite value. + * @param d double value + */ + private String applyFinite(final double d) { + final ParsedDecimal n = ParsedDecimal.from(d); + + int roundExponent = Math.max(n.getExponent(), minDecimalExponent); + if (maxPrecision > 0) { + roundExponent = Math.max(n.getScientificExponent() - maxPrecision + 1, roundExponent); + } + n.round(roundExponent); + + return applyFiniteInternal(n); + } + + /** + * Returns a formatted representation of the given rounded decimal value to {@code dst}. + * @param val value to format + * @return a formatted representation of the given rounded decimal value to {@code dst}. + */ + protected abstract String applyFiniteInternal(ParsedDecimal val); + + /** {@inheritDoc} */ + @Override + public char getDecimalSeparator() { + return decimalSeparator; + } + + /** {@inheritDoc} */ + @Override + public char[] getDigits() { + return digits; + } + + /** {@inheritDoc} */ + @Override + public char[] getExponentSeparatorChars() { + return exponentSeparatorChars; + } + + /** {@inheritDoc} */ + @Override + public char getGroupingSeparator() { + return groupingSeparator; + } + + /** {@inheritDoc} */ + @Override + public char getMinusSign() { + return minusSign; + } + + /** {@inheritDoc} */ + @Override + public boolean isAlwaysIncludeExponent() { + return alwaysIncludeExponent; + } + + /** {@inheritDoc} */ + @Override + public boolean isGroupThousands() { + return groupThousands; + } + + /** {@inheritDoc} */ + @Override + public boolean isIncludeFractionPlaceholder() { + return fractionPlaceholder; + } + + /** {@inheritDoc} */ + @Override + public boolean isSignedZero() { + return signedZero; + } } /** @@ -236,79 +379,6 @@ public enum DoubleFormat { } /** - * Sets the maximum number of significant decimal digits used in format - * results. A value of {@code 0} indicates no limit. The default value is {@code 0}. - * @param maxPrecision maximum precision - * @return this instance - */ - public Builder maxPrecision(final int maxPrecision) { - this.maxPrecision = maxPrecision; - return this; - } - - /** - * Sets the minimum decimal exponent for formatted strings. No digits with an - * absolute value of less than <code>10<sup>minDecimalExponent</sup></code> will - * be included in format results. If the number being formatted does not contain - * any such digits, then zero is returned. For example, if {@code minDecimalExponent} - * is set to {@code -2} and the number {@code 3.14159} is formatted, the plain - * format result will be {@code "3.14"}. If {@code 0.001} is formatted, then the - * result is the zero string. - * @param minDecimalExponent minimum decimal exponent - * @return this instance - */ - public Builder minDecimalExponent(final int minDecimalExponent) { - this.minDecimalExponent = minDecimalExponent; - return this; - } - - /** - * Sets the maximum decimal exponent for numbers formatted as plain decimal strings when - * using the {@link DoubleFormat#MIXED MIXED} format type. If the number being formatted - * has an absolute value less than <code>10<sup>plainFormatMaxDecimalExponent + 1</sup></code> and - * greater than or equal to <code>10<sup>plainFormatMinDecimalExponent</sup></code> after any - * necessary rounding, then the formatted result will use the {@link DoubleFormat#PLAIN PLAIN} format type. - * Otherwise, {@link DoubleFormat#SCIENTIFIC SCIENTIFIC} format will be used. For example, - * if this value is set to {@code 2}, the number {@code 999} will be formatted as {@code "999.0"} - * while {@code 1000} will be formatted as {@code "1.0E3"}. - * - * <p>The default value is {@value #DEFAULT_PLAIN_FORMAT_MAX_DECIMAL_EXPONENT}. - * - * <p>This value is ignored for formats other than {@link DoubleFormat#MIXED}. - * @param plainFormatMaxDecimalExponent maximum decimal exponent for values formatted as plain - * strings when using the {@link DoubleFormat#MIXED MIXED} format type. - * @return this instance - * @see #plainFormatMinDecimalExponent(int) - */ - public Builder plainFormatMaxDecimalExponent(final int plainFormatMaxDecimalExponent) { - this.plainFormatMaxDecimalExponent = plainFormatMaxDecimalExponent; - return this; - } - - /** - * Sets the minimum decimal exponent for numbers formatted as plain decimal strings when - * using the {@link DoubleFormat#MIXED MIXED} format type. If the number being formatted - * has an absolute value less than <code>10<sup>plainFormatMaxDecimalExponent + 1</sup></code> and - * greater than or equal to <code>10<sup>plainFormatMinDecimalExponent</sup></code> after any - * necessary rounding, then the formatted result will use the {@link DoubleFormat#PLAIN PLAIN} format type. - * Otherwise, {@link DoubleFormat#SCIENTIFIC SCIENTIFIC} format will be used. For example, - * if this value is set to {@code -2}, the number {@code 0.01} will be formatted as {@code "0.01"} - * while {@code 0.0099} will be formatted as {@code "9.9E-3"}. - * - * <p>The default value is {@value #DEFAULT_PLAIN_FORMAT_MIN_DECIMAL_EXPONENT}. - * - * <p>This value is ignored for formats other than {@link DoubleFormat#MIXED}. - * @param plainFormatMinDecimalExponent maximum decimal exponent for values formatted as plain - * strings when using the {@link DoubleFormat#MIXED MIXED} format type. - * @return this instance - * @see #plainFormatMinDecimalExponent(int) - */ - public Builder plainFormatMinDecimalExponent(final int plainFormatMinDecimalExponent) { - this.plainFormatMinDecimalExponent = plainFormatMinDecimalExponent; - return this; - } - - /** * Sets the flag determining whether or not the zero string may be returned with the minus * sign or if it will always be returned in the positive form. For example, if set to {@code true}, * the string {@code "-0.0"} may be returned for some input numbers. If {@code false}, only {@code "0.0"} @@ -323,46 +393,27 @@ public enum DoubleFormat { } /** - * Sets the string containing the digit characters 0-9, in that order. The - * default value is the string {@code "0123456789"}. - * @param digits string containing the digit characters 0-9 - * @return this instance - * @throws NullPointerException if the argument is {@code null} - * @throws IllegalArgumentException if the argument does not have a length of exactly 10 - */ - public Builder digits(final String digits) { - Objects.requireNonNull(digits, "Digits string cannot be null"); - if (digits.length() != DEFAULT_DECIMAL_DIGITS.length()) { - throw new IllegalArgumentException("Digits string must contain exactly " - + DEFAULT_DECIMAL_DIGITS.length() + " characters."); - } - - this.digits = digits; - return this; - } - - /** - * Sets the flag determining whether or not a zero character is added in the fraction position - * when no fractional value is present. For example, if set to {@code true}, the number {@code 1} would - * be formatted as {@code "1.0"}. If {@code false}, it would be formatted as {@code "1"}. The default - * value is {@code true}. - * @param fractionPlaceholder if {@code true}, a zero character is placed in the fraction position when - * no fractional value is present; if {@code false}, fractional digits are only included when needed + * Sets the flag indicating if an exponent value should always be included in the + * formatted value, even if the exponent value is zero. This property only applies + * to formats that use scientific notation, namely + * {@link DoubleFormat#SCIENTIFIC SCIENTIFIC}, + * {@link DoubleFormat#ENGINEERING ENGINEERING}, and + * {@link DoubleFormat#MIXED MIXED}. The default value is {@code false}. + * @param alwaysIncludeExponent if {@code true}, exponents will always be included in formatted + * output even if the exponent value is zero * @return this instance */ - public Builder includeFractionPlaceholder(final boolean fractionPlaceholder) { - this.fractionPlaceholder = fractionPlaceholder; + public Builder alwaysIncludeExponent(final boolean alwaysIncludeExponent) { + this.alwaysIncludeExponent = alwaysIncludeExponent; return this; } /** - * Sets the character used as the minus sign. - * @param minusSign character to use as the minus sign - * @return this instance + * Builds a new double format function. + * @return format function */ - public Builder minusSign(final char minusSign) { - this.minusSign = minusSign; - return this; + public DoubleFunction<String> build() { + return factory.apply(this); } /** @@ -378,27 +429,21 @@ public enum DoubleFormat { } /** - * Sets the character used to separate groups of thousands. Default value is {@code ','}. - * @param groupingSeparator character used to separate groups of thousands + * Sets the string containing the digit characters 0-9, in that order. The + * default value is the string {@code "0123456789"}. + * @param digits string containing the digit characters 0-9 * @return this instance - * @see #groupThousands(boolean) + * @throws NullPointerException if the argument is {@code null} + * @throws IllegalArgumentException if the argument does not have a length of exactly 10 */ - public Builder groupingSeparator(final char groupingSeparator) { - this.groupingSeparator = groupingSeparator; - return this; - } + public Builder digits(final String digits) { + Objects.requireNonNull(digits, "Digits string cannot be null"); + if (digits.length() != DEFAULT_DECIMAL_DIGITS.length()) { + throw new IllegalArgumentException("Digits string must contain exactly " + + DEFAULT_DECIMAL_DIGITS.length() + " characters."); + } - /** - * If set to {@code true}, thousands will be grouped with the - * {@link #groupingSeparator(char) grouping separator}. For example, if set to {@code true}, - * the number {@code 1000} could be formatted as {@code "1,000"}. This property only applies - * to the {@link DoubleFormat#PLAIN PLAIN} format. Default value is {@code false}. - * @param groupThousands if {@code true}, thousands will be grouped - * @return this instance - * @see #groupingSeparator(char) - */ - public Builder groupThousands(final boolean groupThousands) { - this.groupThousands = groupThousands; + this.digits = digits; return this; } @@ -416,45 +461,6 @@ public enum DoubleFormat { } /** - * Sets the flag indicating if an exponent value should always be included in the - * formatted value, even if the exponent value is zero. This property only applies - * to formats that use scientific notation, namely - * {@link DoubleFormat#SCIENTIFIC SCIENTIFIC}, - * {@link DoubleFormat#ENGINEERING ENGINEERING}, and - * {@link DoubleFormat#MIXED MIXED}. The default value is {@code false}. - * @param alwaysIncludeExponent if {@code true}, exponents will always be included in formatted - * output even if the exponent value is zero - * @return this instance - */ - public Builder alwaysIncludeExponent(final boolean alwaysIncludeExponent) { - this.alwaysIncludeExponent = alwaysIncludeExponent; - return this; - } - - /** - * Sets the string used to represent infinity. For negative infinity, this string - * is prefixed with the {@link #minusSign(char) minus sign}. - * @param infinity string used to represent infinity - * @return this instance - * @throws NullPointerException if the argument is {@code null} - */ - public Builder infinity(final String infinity) { - this.infinity = Objects.requireNonNull(infinity, "Infinity string cannot be null"); - return this; - } - - /** - * Sets the string used to represent {@link Double#NaN}. - * @param nan string used to represent {@link Double#NaN} - * @return this instance - * @throws NullPointerException if the argument is {@code null} - */ - public Builder nan(final String nan) { - this.nan = Objects.requireNonNull(nan, "NaN string cannot be null"); - return this; - } - - /** * Configures this instance with the given format symbols. The following values * are set: * <ul> @@ -481,219 +487,191 @@ public enum DoubleFormat { .groupingSeparator(symbols.getGroupingSeparator()) .minusSign(symbols.getMinusSign()) .exponentSeparator(symbols.getExponentSeparator()) - .infinity(symbols.getInfinity()) - .nan(symbols.getNaN()); - } - - /** - * Gets a string containing the localized digits 0-9 for the given symbols object. The - * string is constructed by starting at the {@link DecimalFormatSymbols#getZeroDigit() zero digit} - * and adding the next 9 consecutive characters. - * @param symbols symbols object - * @return string containing the localized digits 0-9 - */ - private String getDigitString(final DecimalFormatSymbols symbols) { - final int zeroDelta = symbols.getZeroDigit() - DEFAULT_DECIMAL_DIGITS.charAt(0); - - final char[] digitChars = new char[DEFAULT_DECIMAL_DIGITS.length()]; - for (int i = 0; i < DEFAULT_DECIMAL_DIGITS.length(); ++i) { - digitChars[i] = (char) (DEFAULT_DECIMAL_DIGITS.charAt(i) + zeroDelta); - } - - return String.valueOf(digitChars); - } - - /** - * Builds a new double format function. - * @return format function - */ - public DoubleFunction<String> build() { - return factory.apply(this); - } - } - - /** - * Base class for standard double formatting classes. - */ - private abstract static class AbstractDoubleFormat - implements DoubleFunction<String>, ParsedDecimal.FormatOptions { - - /** Maximum precision; 0 indicates no limit. */ - private final int maxPrecision; - - /** Minimum decimal exponent. */ - private final int minDecimalExponent; - - /** String representing positive infinity. */ - private final String positiveInfinity; - - /** String representing negative infinity. */ - private final String negativeInfinity; - - /** String representing NaN. */ - private final String nan; - - /** Flag determining if fraction placeholders should be used. */ - private final boolean fractionPlaceholder; - - /** Flag determining if signed zero strings are allowed. */ - private final boolean signedZero; - - /** String containing the digits 0-9. */ - private final char[] digits; - - /** Decimal separator character. */ - private final char decimalSeparator; - - /** Thousands grouping separator. */ - private final char groupingSeparator; - - /** Flag indicating if thousands should be grouped. */ - private final boolean groupThousands; - - /** Minus sign character. */ - private final char minusSign; - - /** Exponent separator character. */ - private final char[] exponentSeparatorChars; - - /** Flag indicating if exponent values should always be included, even if zero. */ - private final boolean alwaysIncludeExponent; - - /** - * Constructs a new instance. - * @param builder builder instance containing configuration values - */ - AbstractDoubleFormat(final Builder builder) { - this.maxPrecision = builder.maxPrecision; - this.minDecimalExponent = builder.minDecimalExponent; - - this.positiveInfinity = builder.infinity; - this.negativeInfinity = builder.minusSign + builder.infinity; - this.nan = builder.nan; - - this.fractionPlaceholder = builder.fractionPlaceholder; - this.signedZero = builder.signedZero; - this.digits = builder.digits.toCharArray(); - this.decimalSeparator = builder.decimalSeparator; - this.groupingSeparator = builder.groupingSeparator; - this.groupThousands = builder.groupThousands; - this.minusSign = builder.minusSign; - this.exponentSeparatorChars = builder.exponentSeparator.toCharArray(); - this.alwaysIncludeExponent = builder.alwaysIncludeExponent; - } - - /** {@inheritDoc} */ - @Override - public boolean isIncludeFractionPlaceholder() { - return fractionPlaceholder; + .infinity(symbols.getInfinity()) + .nan(symbols.getNaN()); } - /** {@inheritDoc} */ - @Override - public boolean isSignedZero() { - return signedZero; - } + /** + * Gets a string containing the localized digits 0-9 for the given symbols object. The + * string is constructed by starting at the {@link DecimalFormatSymbols#getZeroDigit() zero digit} + * and adding the next 9 consecutive characters. + * @param symbols symbols object + * @return string containing the localized digits 0-9 + */ + private String getDigitString(final DecimalFormatSymbols symbols) { + final int zeroDelta = symbols.getZeroDigit() - DEFAULT_DECIMAL_DIGITS.charAt(0); - /** {@inheritDoc} */ - @Override - public char[] getDigits() { - return digits; - } + final char[] digitChars = new char[DEFAULT_DECIMAL_DIGITS.length()]; + for (int i = 0; i < DEFAULT_DECIMAL_DIGITS.length(); ++i) { + digitChars[i] = (char) (DEFAULT_DECIMAL_DIGITS.charAt(i) + zeroDelta); + } - /** {@inheritDoc} */ - @Override - public char getDecimalSeparator() { - return decimalSeparator; + return String.valueOf(digitChars); } - /** {@inheritDoc} */ - @Override - public char getGroupingSeparator() { - return groupingSeparator; + /** + * Sets the character used to separate groups of thousands. Default value is {@code ','}. + * @param groupingSeparator character used to separate groups of thousands + * @return this instance + * @see #groupThousands(boolean) + */ + public Builder groupingSeparator(final char groupingSeparator) { + this.groupingSeparator = groupingSeparator; + return this; } - /** {@inheritDoc} */ - @Override - public boolean isGroupThousands() { - return groupThousands; + /** + * If set to {@code true}, thousands will be grouped with the + * {@link #groupingSeparator(char) grouping separator}. For example, if set to {@code true}, + * the number {@code 1000} could be formatted as {@code "1,000"}. This property only applies + * to the {@link DoubleFormat#PLAIN PLAIN} format. Default value is {@code false}. + * @param groupThousands if {@code true}, thousands will be grouped + * @return this instance + * @see #groupingSeparator(char) + */ + public Builder groupThousands(final boolean groupThousands) { + this.groupThousands = groupThousands; + return this; } - /** {@inheritDoc} */ - @Override - public char getMinusSign() { - return minusSign; + /** + * Sets the flag determining whether or not a zero character is added in the fraction position + * when no fractional value is present. For example, if set to {@code true}, the number {@code 1} would + * be formatted as {@code "1.0"}. If {@code false}, it would be formatted as {@code "1"}. The default + * value is {@code true}. + * @param fractionPlaceholder if {@code true}, a zero character is placed in the fraction position when + * no fractional value is present; if {@code false}, fractional digits are only included when needed + * @return this instance + */ + public Builder includeFractionPlaceholder(final boolean fractionPlaceholder) { + this.fractionPlaceholder = fractionPlaceholder; + return this; } - /** {@inheritDoc} */ - @Override - public char[] getExponentSeparatorChars() { - return exponentSeparatorChars; + /** + * Sets the string used to represent infinity. For negative infinity, this string + * is prefixed with the {@link #minusSign(char) minus sign}. + * @param infinity string used to represent infinity + * @return this instance + * @throws NullPointerException if the argument is {@code null} + */ + public Builder infinity(final String infinity) { + this.infinity = Objects.requireNonNull(infinity, "Infinity string cannot be null"); + return this; } - /** {@inheritDoc} */ - @Override - public boolean isAlwaysIncludeExponent() { - return alwaysIncludeExponent; + /** + * Sets the maximum number of significant decimal digits used in format + * results. A value of {@code 0} indicates no limit. The default value is {@code 0}. + * @param maxPrecision maximum precision + * @return this instance + */ + public Builder maxPrecision(final int maxPrecision) { + this.maxPrecision = maxPrecision; + return this; } - /** {@inheritDoc} */ - @Override - public String apply(final double d) { - if (Double.isFinite(d)) { - return applyFinite(d); - } else if (Double.isInfinite(d)) { - return d > 0.0 - ? positiveInfinity - : negativeInfinity; - } - return nan; + /** + * Sets the minimum decimal exponent for formatted strings. No digits with an + * absolute value of less than <code>10<sup>minDecimalExponent</sup></code> will + * be included in format results. If the number being formatted does not contain + * any such digits, then zero is returned. For example, if {@code minDecimalExponent} + * is set to {@code -2} and the number {@code 3.14159} is formatted, the plain + * format result will be {@code "3.14"}. If {@code 0.001} is formatted, then the + * result is the zero string. + * @param minDecimalExponent minimum decimal exponent + * @return this instance + */ + public Builder minDecimalExponent(final int minDecimalExponent) { + this.minDecimalExponent = minDecimalExponent; + return this; } /** - * Returns a formatted string representation of the given finite value. - * @param d double value + * Sets the character used as the minus sign. + * @param minusSign character to use as the minus sign + * @return this instance */ - private String applyFinite(final double d) { - final ParsedDecimal n = ParsedDecimal.from(d); + public Builder minusSign(final char minusSign) { + this.minusSign = minusSign; + return this; + } - int roundExponent = Math.max(n.getExponent(), minDecimalExponent); - if (maxPrecision > 0) { - roundExponent = Math.max(n.getScientificExponent() - maxPrecision + 1, roundExponent); - } - n.round(roundExponent); + /** + * Sets the string used to represent {@link Double#NaN}. + * @param nan string used to represent {@link Double#NaN} + * @return this instance + * @throws NullPointerException if the argument is {@code null} + */ + public Builder nan(final String nan) { + this.nan = Objects.requireNonNull(nan, "NaN string cannot be null"); + return this; + } - return applyFiniteInternal(n); + /** + * Sets the maximum decimal exponent for numbers formatted as plain decimal strings when + * using the {@link DoubleFormat#MIXED MIXED} format type. If the number being formatted + * has an absolute value less than <code>10<sup>plainFormatMaxDecimalExponent + 1</sup></code> and + * greater than or equal to <code>10<sup>plainFormatMinDecimalExponent</sup></code> after any + * necessary rounding, then the formatted result will use the {@link DoubleFormat#PLAIN PLAIN} format type. + * Otherwise, {@link DoubleFormat#SCIENTIFIC SCIENTIFIC} format will be used. For example, + * if this value is set to {@code 2}, the number {@code 999} will be formatted as {@code "999.0"} + * while {@code 1000} will be formatted as {@code "1.0E3"}. + * + * <p>The default value is {@value #DEFAULT_PLAIN_FORMAT_MAX_DECIMAL_EXPONENT}. + * + * <p>This value is ignored for formats other than {@link DoubleFormat#MIXED}. + * @param plainFormatMaxDecimalExponent maximum decimal exponent for values formatted as plain + * strings when using the {@link DoubleFormat#MIXED MIXED} format type. + * @return this instance + * @see #plainFormatMinDecimalExponent(int) + */ + public Builder plainFormatMaxDecimalExponent(final int plainFormatMaxDecimalExponent) { + this.plainFormatMaxDecimalExponent = plainFormatMaxDecimalExponent; + return this; } /** - * Returns a formatted representation of the given rounded decimal value to {@code dst}. - * @param val value to format - * @return a formatted representation of the given rounded decimal value to {@code dst}. + * Sets the minimum decimal exponent for numbers formatted as plain decimal strings when + * using the {@link DoubleFormat#MIXED MIXED} format type. If the number being formatted + * has an absolute value less than <code>10<sup>plainFormatMaxDecimalExponent + 1</sup></code> and + * greater than or equal to <code>10<sup>plainFormatMinDecimalExponent</sup></code> after any + * necessary rounding, then the formatted result will use the {@link DoubleFormat#PLAIN PLAIN} format type. + * Otherwise, {@link DoubleFormat#SCIENTIFIC SCIENTIFIC} format will be used. For example, + * if this value is set to {@code -2}, the number {@code 0.01} will be formatted as {@code "0.01"} + * while {@code 0.0099} will be formatted as {@code "9.9E-3"}. + * + * <p>The default value is {@value #DEFAULT_PLAIN_FORMAT_MIN_DECIMAL_EXPONENT}. + * + * <p>This value is ignored for formats other than {@link DoubleFormat#MIXED}. + * @param plainFormatMinDecimalExponent maximum decimal exponent for values formatted as plain + * strings when using the {@link DoubleFormat#MIXED MIXED} format type. + * @return this instance + * @see #plainFormatMinDecimalExponent(int) */ - protected abstract String applyFiniteInternal(ParsedDecimal val); + public Builder plainFormatMinDecimalExponent(final int plainFormatMinDecimalExponent) { + this.plainFormatMinDecimalExponent = plainFormatMinDecimalExponent; + return this; + } } /** - * Format class that produces plain decimal strings that do not use - * scientific notation. + * Format class that uses engineering notation for all values. */ - private static class PlainDoubleFormat extends AbstractDoubleFormat { + private static class EngineeringDoubleFormat extends AbstractDoubleFormat { /** * Constructs a new instance. * @param builder builder instance containing configuration values */ - PlainDoubleFormat(final Builder builder) { + EngineeringDoubleFormat(final Builder builder) { super(builder); } - /** - * {@inheritDoc} - */ + /** {@inheritDoc} */ @Override - protected String applyFiniteInternal(final ParsedDecimal val) { - return val.toPlainString(this); + public String applyFiniteInternal(final ParsedDecimal val) { + return val.toEngineeringString(this); } } @@ -733,42 +711,64 @@ public enum DoubleFormat { } /** - * Format class that uses scientific notation for all values. + * Format class that produces plain decimal strings that do not use + * scientific notation. */ - private static class ScientificDoubleFormat extends AbstractDoubleFormat { + private static class PlainDoubleFormat extends AbstractDoubleFormat { /** * Constructs a new instance. * @param builder builder instance containing configuration values */ - ScientificDoubleFormat(final Builder builder) { + PlainDoubleFormat(final Builder builder) { super(builder); } - /** {@inheritDoc} */ + /** + * {@inheritDoc} + */ @Override - public String applyFiniteInternal(final ParsedDecimal val) { - return val.toScientificString(this); + protected String applyFiniteInternal(final ParsedDecimal val) { + return val.toPlainString(this); } } /** - * Format class that uses engineering notation for all values. + * Format class that uses scientific notation for all values. */ - private static class EngineeringDoubleFormat extends AbstractDoubleFormat { + private static class ScientificDoubleFormat extends AbstractDoubleFormat { /** * Constructs a new instance. * @param builder builder instance containing configuration values */ - EngineeringDoubleFormat(final Builder builder) { + ScientificDoubleFormat(final Builder builder) { super(builder); } /** {@inheritDoc} */ @Override public String applyFiniteInternal(final ParsedDecimal val) { - return val.toEngineeringString(this); + return val.toScientificString(this); } } + + /** Function used to construct instances for this format type. */ + private final Function<Builder, DoubleFunction<String>> factory; + + /** + * Constructs a new instance. + * @param factory function used to construct format instances + */ + DoubleFormat(final Function<Builder, DoubleFunction<String>> factory) { + this.factory = factory; + } + + /** + * Creates a {@link Builder} for building formatter functions for this format type. + * @return builder instance + */ + public Builder builder() { + return new Builder(factory); + } } diff --git a/src/main/java/org/apache/commons/text/numbers/ParsedDecimal.java b/src/main/java/org/apache/commons/text/numbers/ParsedDecimal.java index 05060e6..7581c0d 100644 --- a/src/main/java/org/apache/commons/text/numbers/ParsedDecimal.java +++ b/src/main/java/org/apache/commons/text/numbers/ParsedDecimal.java @@ -43,18 +43,10 @@ final class ParsedDecimal { interface FormatOptions { /** - * 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 + * Get the decimal separator character. + * @return decimal separator character */ - boolean isSignedZero(); + char getDecimalSeparator(); /** * Get an array containing the localized digit characters 0-9 in that order. @@ -64,10 +56,10 @@ final class ParsedDecimal { char[] getDigits(); /** - * Get the decimal separator character. - * @return decimal separator character + * Get the exponent separator as an array of characters. + * @return exponent separator as an array of characters */ - char getDecimalSeparator(); + char[] getExponentSeparatorChars(); /** * Get the character used to separate thousands groupings. @@ -76,29 +68,37 @@ final class ParsedDecimal { char getGroupingSeparator(); /** - * Return {@code true} if thousands should be grouped. - * @return {@code true} if thousand should be grouped - */ - boolean isGroupThousands(); - - /** * Get the minus sign character. * @return minus sign character */ char getMinusSign(); /** - * Get the exponent separator as an array of characters. - * @return exponent separator as an array of characters - */ - char[] getExponentSeparatorChars(); - - /** * 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. */ @@ -125,6 +125,106 @@ final class ParsedDecimal { /** 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; @@ -159,244 +259,77 @@ final class ParsedDecimal { } /** - * 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 + * Appends the given character to the output buffer. + * @param ch character to append */ - public int getExponent() { - return exponent; + private void append(final char ch) { + outputChars[outputIdx++] = ch; } /** - * Get sthe 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 + * Appends the given character array directly to the output buffer. + * @param chars characters to append */ - public int getScientificExponent() { - return digitCount + exponent - 1; + private void append(final char[] chars) { + for (final char c : chars) { + append(c); + } } /** - * 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 + * 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 */ - public void round(final int roundExponent) { - if (roundExponent > exponent) { - final int max = digitCount + exponent; + private void appendFraction(final int zeroCount, final int startIdx, final FormatOptions opts) { + final char[] localizedDigits = opts.getDigits(); + final char localizedZero = localizedDigits[0]; - 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); + 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); } } /** - * 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 + * 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 */ - public void maxPrecision(final int precision) { - if (precision > 0 && precision < digitCount) { - if (shouldRoundUp(precision)) { - roundUp(precision); - } else { - truncate(precision); - } - } + private void appendLocalizedDigit(final int n, final char[] digitChars) { + append(digitChars[n]); } /** - * Returns a string representation of this value with no exponent field. Ex: - * <pre> - * 10 = "10.0" - * 1e-6 = "0.000001" - * 1e11 = "100000000000.0" - * </pre> + * 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 value in plain format + * @return number of digits from {@code digits} appended to the output buffer + * @see #appendWholeGrouped(int, FormatOptions) */ - public String toPlainString(final FormatOptions opts) { - final int decimalPos = digitCount + exponent; - final int fractionZeroCount = decimalPos < 1 - ? Math.abs(decimalPos) - : 0; + private int appendWhole(final int wholeCount, final FormatOptions opts) { + if (shouldIncludeMinus(opts)) { + append(opts.getMinusSign()); + } - 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. Ex: - * <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 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. - * <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 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(); - } - - /** - * 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; - } - - /** - * 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; - } - - /** - * 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 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 char[] localizedDigits = opts.getDigits(); + final char localizedZero = localizedDigits[0]; final int significantDigitCount = Math.max(0, Math.min(wholeCount, digitCount)); @@ -459,65 +392,6 @@ final class ParsedDecimal { } /** - * 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; - } - - /** - * 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); - } - } - - /** - * 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 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 @@ -551,45 +425,120 @@ final class ParsedDecimal { } /** - * 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 + * 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 */ - private boolean shouldIncludeMinus(final FormatOptions opts) { - return negative && (opts.isSignedZero() || !isZero()); + public int getExponent() { + return exponent; } /** - * Returns {@code true} if a formatted string with the given target exponent should include - * the exponent field. - * @param targetExponent exponent of the formatted result + * 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 {@code true} if the formatted string should include the exponent field + * @return number of characters in the plain string representation of this value, + * created using the given parameters */ - private boolean shouldIncludeExponent(final int targetExponent, final FormatOptions opts) { - return targetExponent != 0 || opts.isAlwaysIncludeExponent(); + 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; } /** - * 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 + * Get sthe 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 */ - 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]; + public int getScientificExponent() { + return digitCount + exponent - 1; + } - return digitAfterLast > ROUND_CENTER || (digitAfterLast == ROUND_CENTER - && (count < digitCount - 1 || (digits[count - 1] % 2) != 0)); + /** + * Returns {@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); + } + } } /** @@ -623,15 +572,6 @@ final class ParsedDecimal { } /** - * Returns {@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; - } - - /** * 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 @@ -644,120 +584,180 @@ final class ParsedDecimal { } /** - * 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 + * 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 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; + private boolean shouldIncludeExponent(final int targetExponent, final FormatOptions opts) { + return targetExponent != 0 || opts.isAlwaysIncludeExponent(); } /** - * 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 + * 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 */ - 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(); + private boolean shouldIncludeMinus(final FormatOptions opts) { + return negative && (opts.isSignedZero() || !isZero()); + } - final boolean negative = strChars[0] == MINUS_CHAR; - final int digitStartIdx = negative ? 1 : 0; + /** + * 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]; - final int[] digits = new int[strChars.length - digitStartIdx - 1]; + return digitAfterLast > ROUND_CENTER || (digitAfterLast == ROUND_CENTER + && (count < digitCount - 1 || (digits[count - 1] % 2) != 0)); + } - boolean foundDecimalPoint = false; - int digitCount = 0; - int significantDigitCount = 0; - int decimalPos = 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. + * <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); + } - int i; - for (i = digitStartIdx; i < strChars.length; ++i) { - final char ch = strChars[i]; + /** + * Returns a string representation of this value with no exponent field. Ex: + * <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; - 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; + prepareOutput(getPlainStringSize(decimalPos, opts)); - if (val > 0) { - significantDigitCount = digitCount; - } - } else if (foundDecimalPoint) { - // leading zero in a fraction; adjust the decimal position - --decimalPos; - } - } + final int fractionStartIdx = opts.isGroupThousands() + ? appendWholeGrouped(decimalPos, opts) + : appendWhole(decimalPos, opts); - if (digitCount > 0) { - // determine the exponent - final int explicitExponent = i < strChars.length - ? parseExponent(strChars, i + 1) - : 0; - final int exponent = explicitExponent + decimalPos - significantDigitCount; + appendFraction(fractionZeroCount, fractionStartIdx, opts); - return new ParsedDecimal(negative, digits, significantDigitCount, exponent); - } + return outputString(); + } - // no non-zero digits, so value is zero - return new ParsedDecimal(negative, new int[] {0}, 1, 0); + /** + * Returns a string representation of this value in scientific notation. Ex: + * <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); } /** - * 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 + * 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 static int parseExponent(final char[] chars, final int start) { - int i = start; - final boolean neg = chars[i] == MINUS_CHAR; - if (neg) { - ++i; + 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; + } } - int exp = 0; - for (; i < chars.length; ++i) { - exp = (exp * DECIMAL_RADIX) + digitValue(chars[i]); + 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 neg ? -exp : exp; + return outputString(); } /** - * 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 + * 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 static int digitValue(final char ch) { - return ch - ZERO_CHAR; + 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; } }