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;
     }
 }

Reply via email to