Sort members.
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 @@
      */
     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 @@
         }
 
         /**
-         * 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,6 +393,42 @@
         }
 
         /**
+         * 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;
+        }
+
+        /**
+         * Builds a new double format function.
+         * @return format function
+         */
+        public DoubleFunction<String> build() {
+            return factory.apply(this);
+        }
+
+        /**
+         * Sets the decimal separator character, i.e., the character placed between the
+         * whole number and fractional portions of the formatted strings. The default value
+         * is {@code '.'}.
+         * @param decimalSeparator decimal separator character
+         * @return this instance
+         */
+        public Builder decimalSeparator(final char decimalSeparator) {
+            this.decimalSeparator = decimalSeparator;
+            return this;
+        }
+
+        /**
          * 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
@@ -342,67 +448,6 @@
         }
 
         /**
-         * 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;
-        }
-
-        /**
-         * Sets the character used as the minus sign.
-         * @param minusSign character to use as the minus sign
-         * @return this instance
-         */
-        public Builder minusSign(final char minusSign) {
-            this.minusSign = minusSign;
-            return this;
-        }
-
-        /**
-         * Sets the decimal separator character, i.e., the character placed between the
-         * whole number and fractional portions of the formatted strings. The default value
-         * is {@code '.'}.
-         * @param decimalSeparator decimal separator character
-         * @return this instance
-         */
-        public Builder decimalSeparator(final char decimalSeparator) {
-            this.decimalSeparator = decimalSeparator;
-            return this;
-        }
-
-        /**
-         * 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;
-        }
-
-        /**
-         * 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;
-        }
-
-        /**
          * Sets the exponent separator character, i.e., the string placed between
          * the mantissa and the exponent. The default value is {@code "E"}, as in
          * {@code "1.2E6"}.
@@ -416,45 +461,6 @@
         }
 
         /**
-         * 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>
@@ -504,196 +510,168 @@
         }
 
         /**
-         * Builds a new double format function.
-         * @return format function
+         * 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 DoubleFunction<String> build() {
-            return factory.apply(this);
+        public Builder groupingSeparator(final char groupingSeparator) {
+            this.groupingSeparator = groupingSeparator;
+            return this;
+        }
+
+        /**
+         * 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;
+        }
+
+        /**
+         * 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;
+        }
+
+        /**
+         * 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 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 character used as the minus sign.
+         * @param minusSign character to use as the minus sign
+         * @return this instance
+         */
+        public Builder minusSign(final char minusSign) {
+            this.minusSign = minusSign;
+            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;
+        }
+
+        /**
+         * 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;
         }
     }
 
     /**
-     * Base class for standard double formatting classes.
+     * Format class that uses engineering notation for all values.
      */
-    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;
+    private static class EngineeringDoubleFormat extends AbstractDoubleFormat {
 
         /**
          * 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;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public boolean isSignedZero() {
-            return signedZero;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public char[] getDigits() {
-            return digits;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public char getDecimalSeparator() {
-            return decimalSeparator;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public char getGroupingSeparator() {
-            return groupingSeparator;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public boolean isGroupThousands() {
-            return groupThousands;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public char getMinusSign() {
-            return minusSign;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public char[] getExponentSeparatorChars() {
-            return exponentSeparatorChars;
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public boolean isAlwaysIncludeExponent() {
-            return 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);
-    }
-
-    /**
-     * Format class that produces plain decimal strings that do not use
-     * scientific notation.
-     */
-    private static class PlainDoubleFormat 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,6 +711,29 @@
     }
 
     /**
+     * Format class that produces plain decimal strings that do not use
+     * scientific notation.
+     */
+    private static class PlainDoubleFormat extends AbstractDoubleFormat {
+
+        /**
+         * Constructs a new instance.
+         * @param builder builder instance containing configuration values
+         */
+        PlainDoubleFormat(final Builder builder) {
+            super(builder);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        protected String applyFiniteInternal(final ParsedDecimal val) {
+            return val.toPlainString(this);
+        }
+    }
+
+    /**
      * Format class that uses scientific notation for all values.
      */
     private static class ScientificDoubleFormat extends AbstractDoubleFormat {
@@ -752,23 +753,22 @@
         }
     }
 
+    /** Function used to construct instances for this format type. */
+    private final Function<Builder, DoubleFunction<String>> factory;
+
     /**
-     * Format class that uses engineering notation for all values.
+     * Constructs a new instance.
+     * @param factory function used to construct format instances
      */
-    private static class EngineeringDoubleFormat extends AbstractDoubleFormat {
+    DoubleFormat(final Function<Builder, DoubleFunction<String>> factory) {
+        this.factory = factory;
+    }
 
-        /**
-         * Constructs a new instance.
-         * @param builder builder instance containing configuration values
-         */
-        EngineeringDoubleFormat(final Builder builder) {
-            super(builder);
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public String applyFiniteInternal(final ParsedDecimal val) {
-            return val.toEngineeringString(this);
-        }
+    /**
+     * 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,6 +43,50 @@
     interface FormatOptions {
 
         /**
+         * Get the decimal separator character.
+         * @return decimal separator character
+         */
+        char getDecimalSeparator();
+
+        /**
+         * Get an array containing the localized digit characters 0-9 in that order.
+         * This string <em>must</em> be non-null and have a length of 10.
+         * @return array containing the digit characters 0-9
+         */
+        char[] getDigits();
+
+        /**
+         * Get the exponent separator as an array of characters.
+         * @return exponent separator as an array of characters
+         */
+        char[] getExponentSeparatorChars();
+
+        /**
+         * Get the character used to separate thousands groupings.
+         * @return character used to separate thousands groupings
+         */
+        char getGroupingSeparator();
+
+        /**
+         * Get the minus sign character.
+         * @return minus sign character
+         */
+        char getMinusSign();
+
+        /**
+         * Return {@code true} if exponent values should always be included in
+         * formatted output, even if the value is zero.
+         * @return {@code true} if exponent values should always be included
+         */
+        boolean isAlwaysIncludeExponent();
+
+        /**
+         * Return {@code true} if thousands should be grouped.
+         * @return {@code true} if thousand should be grouped
+         */
+        boolean isGroupThousands();
+
+        /**
          * Return {@code true} if fraction placeholders (e.g., {@code ".0"} in {@code "1.0"})
          * should be included.
          * @return {@code true} if fraction placeholders should be included
@@ -55,50 +99,6 @@
          * @return {@code true} if the minus zero string should be allowed
          */
         boolean isSignedZero();
-
-        /**
-         * Get an array containing the localized digit characters 0-9 in that order.
-         * This string <em>must</em> be non-null and have a length of 10.
-         * @return array containing the digit characters 0-9
-         */
-        char[] getDigits();
-
-        /**
-         * Get the decimal separator character.
-         * @return decimal separator character
-         */
-        char getDecimalSeparator();
-
-        /**
-         * Get the character used to separate thousands groupings.
-         * @return character used to separate thousands groupings
-         */
-        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();
     }
 
     /** Minus sign character. */
@@ -125,540 +125,14 @@
     /** Number that exponents in engineering format must be a multiple of. */
     private static final int ENG_EXPONENT_MOD = 3;
 
-    /** True if the value is negative. */
-    final boolean negative;
-
-    /** Array containing the significant decimal digits for the value. */
-    final int[] digits;
-
-    /** Number of digits used in the digits array; not necessarily equal to the length. */
-    int digitCount;
-
-    /** Exponent for the value. */
-    int exponent;
-
-    /** Output buffer for use in creating string representations. */
-    private char[] outputChars;
-
-    /** Output buffer index. */
-    private int outputIdx;
-
     /**
-     * Constructs a new instance from its parts.
-     * @param negative {@code true} if the value is negative
-     * @param digits array containing significant digits
-     * @param digitCount number of digits used from the {@code digits} array
-     * @param exponent exponent value
+     * 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 ParsedDecimal(final boolean negative, final int[] digits, final int digitCount,
-            final int exponent) {
-        this.negative = negative;
-        this.digits = digits;
-        this.digitCount = digitCount;
-        this.exponent = exponent;
-    }
-
-    /**
-     * Gets the exponent value. This exponent produces a floating point value with the
-     * correct magnitude when applied to the internal unsigned integer.
-     * @return exponent value
-     */
-    public int getExponent() {
-        return exponent;
-    }
-
-    /**
-     * 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
-     */
-    public int getScientificExponent() {
-        return digitCount + exponent - 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);
-            }
-        }
-    }
-
-    /**
-     * 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);
-            }
-        }
-    }
-
-    /**
-     * 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;
-
-        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 int significantDigitCount = Math.max(0, Math.min(wholeCount, digitCount));
-
-        if (significantDigitCount > 0) {
-            int i;
-            for (i = 0; i < significantDigitCount; ++i) {
-                appendLocalizedDigit(digits[i], localizedDigits);
-            }
-
-            for (; i < wholeCount; ++i) {
-                append(localizedZero);
-            }
-        } else {
-            append(localizedZero);
-        }
-
-        return significantDigitCount;
-    }
-
-    /**
-     * Appends the whole number portion of this value to the output buffer, adding thousands
-     * separators as needed.
-     * @param wholeCount total number of digits required to the right of the decimal point
-     * @param opts format options
-     * @return number of digits from {@code digits} appended to the output buffer
-     * @see #appendWhole(int, FormatOptions)
-     */
-    private int appendWholeGrouped(final int wholeCount, final FormatOptions opts) {
-        if (shouldIncludeMinus(opts)) {
-            append(opts.getMinusSign());
-        }
-
-        final char[] localizedDigits = opts.getDigits();
-        final char localizedZero = localizedDigits[0];
-        final char groupingChar = opts.getGroupingSeparator();
-
-        final int appendCount = Math.max(0, Math.min(wholeCount, digitCount));
-
-        if (appendCount > 0) {
-            int i;
-            int pos = wholeCount;
-            for (i = 0; i < appendCount; ++i, --pos) {
-                appendLocalizedDigit(digits[i], localizedDigits);
-                if (requiresGroupingSeparatorAfterPosition(pos)) {
-                    append(groupingChar);
-                }
-            }
-
-            for (; i < wholeCount; ++i, --pos) {
-                append(localizedZero);
-                if (requiresGroupingSeparatorAfterPosition(pos)) {
-                    append(groupingChar);
-                }
-            }
-        } else {
-            append(localizedZero);
-        }
-
-        return appendCount;
-    }
-
-    /**
-     * 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
-     * @param opts format options
-     * @return number of characters required for the digit portion of a string representation of
-     *      this value
-     */
-    private int getDigitStringSize(final int decimalPos, final FormatOptions opts) {
-        int size = digitCount;
-        if (shouldIncludeMinus(opts)) {
-            ++size;
-        }
-        if (decimalPos < 1) {
-            // no whole component;
-            // add decimal point and leading zeros
-            size += 2 + Math.abs(decimalPos);
-        } else if (decimalPos >= digitCount) {
-            // no fraction component;
-            // add trailing zeros
-            size += decimalPos - digitCount;
-            if (opts.isIncludeFractionPlaceholder()) {
-                size += 2;
-            }
-        } else {
-            // whole and fraction components;
-            // add decimal point
-            size += 1;
-        }
-
-        return size;
-    }
-
-    /**
-     * Returns {@code true} if formatted strings should include the minus sign, considering
-     * the value of this instance and the given format options.
-     * @param opts format options
-     * @return {@code true} if a minus sign should be included in the output
-     */
-    private boolean shouldIncludeMinus(final FormatOptions opts) {
-        return negative && (opts.isSignedZero() || !isZero());
-    }
-
-    /**
-     * Returns {@code true} if a formatted string with the given target exponent should include
-     * the exponent field.
-     * @param targetExponent exponent of the formatted result
-     * @param opts format options
-     * @return {@code true} if the formatted string should include the exponent field
-     */
-    private boolean shouldIncludeExponent(final int targetExponent, final FormatOptions opts) {
-        return targetExponent != 0 || opts.isAlwaysIncludeExponent();
-    }
-
-    /**
-     * Returns {@code true} if a rounding operation for the given number of digits should
-     * round up.
-     * @param count number of digits to round to; must be greater than zero and less
-     *      than the current number of digits
-     * @return {@code true} if a rounding operation for the given number of digits should
-     *      round up
-     */
-    private boolean shouldRoundUp(final int count) {
-        // Round up in the following cases:
-        // 1. The digit after the last digit is greater than 5.
-        // 2. The digit after the last digit is 5 and there are additional (non-zero)
-        //      digits after it.
-        // 3. The digit after the last digit is 5, there are no additional digits afterward,
-        //      and the last digit is odd (half-even rounding).
-        final int digitAfterLast = digits[count];
-
-        return digitAfterLast > ROUND_CENTER || (digitAfterLast == ROUND_CENTER
-                && (count < digitCount - 1 || (digits[count - 1] % 2) != 0));
-    }
-
-    /**
-     * Rounds the value up to the given number of digits.
-     * @param count target number of digits; must be greater than zero and
-     *      less than the current number of digits
-     */
-    private void roundUp(final int count) {
-        int removedDigits = digitCount - count;
-        int i;
-        for (i = count - 1; i >= 0; --i) {
-            final int d = digits[i] + 1;
-
-            if (d < DECIMAL_RADIX) {
-                // value did not carry over; done adding
-                digits[i] = d;
-                break;
-            }
-            // value carried over; the current position is 0
-            // which we will ignore by shortening the digit count
-            ++removedDigits;
-        }
-
-        if (i < 0) {
-            // all values carried over
-            setSingleDigitValue(1, exponent + removedDigits);
-        } else {
-            // values were updated in-place; just need to update the length
-            truncate(digitCount - removedDigits);
-        }
-    }
-
-    /**
-     * 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
-     * @param newExponent new exponent value
-     */
-    private void setSingleDigitValue(final int digit, final int newExponent) {
-        digits[0] = digit;
-        digitCount = 1;
-        exponent = newExponent;
-    }
-
-    /**
-     * Truncates the value to the given number of digits.
-     * @param count number of digits; must be greater than zero and less than
-     *      the current number of digits
-     */
-    private void truncate(final int count) {
-        // trim all trailing zero digits, making sure to leave
-        // at least one digit left
-        int nonZeroCount = count;
-        for (int i = count - 1;
-                i > 0 && digits[i] == 0;
-                --i) {
-            --nonZeroCount;
-        }
-        exponent += digitCount - nonZeroCount;
-        digitCount = nonZeroCount;
+    private static int digitValue(final char ch) {
+        return ch - ZERO_CHAR;
     }
 
     /**
@@ -751,13 +225,539 @@
         return neg ? -exp : exp;
     }
 
+    /** True if the value is negative. */
+    final boolean negative;
+
+    /** Array containing the significant decimal digits for the value. */
+    final int[] digits;
+
+    /** Number of digits used in the digits array; not necessarily equal to the length. */
+    int digitCount;
+
+    /** Exponent for the value. */
+    int exponent;
+
+    /** Output buffer for use in creating string representations. */
+    private char[] outputChars;
+
+    /** Output buffer index. */
+    private int outputIdx;
+
     /**
-     * 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
+     * Constructs a new instance from its parts.
+     * @param negative {@code true} if the value is negative
+     * @param digits array containing significant digits
+     * @param digitCount number of digits used from the {@code digits} array
+     * @param exponent exponent value
      */
-    private static int digitValue(final char ch) {
-        return ch - ZERO_CHAR;
+    private ParsedDecimal(final boolean negative, final int[] digits, final int digitCount,
+            final int exponent) {
+        this.negative = negative;
+        this.digits = digits;
+        this.digitCount = digitCount;
+        this.exponent = exponent;
+    }
+
+    /**
+     * Appends the given character to the output buffer.
+     * @param ch character to append
+     */
+    private void append(final char ch) {
+        outputChars[outputIdx++] = ch;
+    }
+
+    /**
+     * Appends the given character array directly to the output buffer.
+     * @param chars characters to append
+     */
+    private void append(final char[] chars) {
+        for (final char c : chars) {
+            append(c);
+        }
+    }
+
+    /**
+     * Appends the fractional component of the number to the current output buffer.
+     * @param zeroCount number of zeros to add after the decimal point and before the
+     *      first significant digit
+     * @param startIdx significant digit start index
+     * @param opts format options
+     */
+    private void appendFraction(final int zeroCount, final int startIdx, final FormatOptions opts) {
+        final char[] localizedDigits = opts.getDigits();
+        final char localizedZero = localizedDigits[0];
+
+        if (startIdx < digitCount) {
+            append(opts.getDecimalSeparator());
+
+            // add the zero prefix
+            for (int i = 0; i < zeroCount; ++i) {
+                append(localizedZero);
+            }
+
+            // add the fraction digits
+            for (int i = startIdx; i < digitCount; ++i) {
+                appendLocalizedDigit(digits[i], localizedDigits);
+            }
+        } else if (opts.isIncludeFractionPlaceholder()) {
+            append(opts.getDecimalSeparator());
+            append(localizedZero);
+        }
+    }
+
+    /**
+     * Appends the localized representation of the digit {@code n} to the output buffer.
+     * @param n digit to append
+     * @param digitChars character array containing localized versions of the digits {@code 0-9}
+     *      in that order
+     */
+    private void appendLocalizedDigit(final int n, final char[] digitChars) {
+        append(digitChars[n]);
+    }
+
+    /**
+     * Appends the whole number portion of this value to the output buffer. No thousands
+     * separators are added.
+     * @param wholeCount total number of digits required to the left of the decimal point
+     * @param opts format options
+     * @return number of digits from {@code digits} appended to the output buffer
+     * @see #appendWholeGrouped(int, FormatOptions)
+     */
+    private int appendWhole(final int wholeCount, final FormatOptions opts) {
+        if (shouldIncludeMinus(opts)) {
+            append(opts.getMinusSign());
+        }
+
+        final char[] localizedDigits = opts.getDigits();
+        final char localizedZero = localizedDigits[0];
+
+        final int significantDigitCount = Math.max(0, Math.min(wholeCount, digitCount));
+
+        if (significantDigitCount > 0) {
+            int i;
+            for (i = 0; i < significantDigitCount; ++i) {
+                appendLocalizedDigit(digits[i], localizedDigits);
+            }
+
+            for (; i < wholeCount; ++i) {
+                append(localizedZero);
+            }
+        } else {
+            append(localizedZero);
+        }
+
+        return significantDigitCount;
+    }
+
+    /**
+     * Appends the whole number portion of this value to the output buffer, adding thousands
+     * separators as needed.
+     * @param wholeCount total number of digits required to the right of the decimal point
+     * @param opts format options
+     * @return number of digits from {@code digits} appended to the output buffer
+     * @see #appendWhole(int, FormatOptions)
+     */
+    private int appendWholeGrouped(final int wholeCount, final FormatOptions opts) {
+        if (shouldIncludeMinus(opts)) {
+            append(opts.getMinusSign());
+        }
+
+        final char[] localizedDigits = opts.getDigits();
+        final char localizedZero = localizedDigits[0];
+        final char groupingChar = opts.getGroupingSeparator();
+
+        final int appendCount = Math.max(0, Math.min(wholeCount, digitCount));
+
+        if (appendCount > 0) {
+            int i;
+            int pos = wholeCount;
+            for (i = 0; i < appendCount; ++i, --pos) {
+                appendLocalizedDigit(digits[i], localizedDigits);
+                if (requiresGroupingSeparatorAfterPosition(pos)) {
+                    append(groupingChar);
+                }
+            }
+
+            for (; i < wholeCount; ++i, --pos) {
+                append(localizedZero);
+                if (requiresGroupingSeparatorAfterPosition(pos)) {
+                    append(groupingChar);
+                }
+            }
+        } else {
+            append(localizedZero);
+        }
+
+        return appendCount;
+    }
+
+    /**
+     * Gets the number of characters required for the digit portion of a string representation of
+     * this value. This excludes any exponent or thousands groupings characters.
+     * @param decimalPos decimal point position relative to the {@code digits} array
+     * @param opts format options
+     * @return number of characters required for the digit portion of a string representation of
+     *      this value
+     */
+    private int getDigitStringSize(final int decimalPos, final FormatOptions opts) {
+        int size = digitCount;
+        if (shouldIncludeMinus(opts)) {
+            ++size;
+        }
+        if (decimalPos < 1) {
+            // no whole component;
+            // add decimal point and leading zeros
+            size += 2 + Math.abs(decimalPos);
+        } else if (decimalPos >= digitCount) {
+            // no fraction component;
+            // add trailing zeros
+            size += decimalPos - digitCount;
+            if (opts.isIncludeFractionPlaceholder()) {
+                size += 2;
+            }
+        } else {
+            // whole and fraction components;
+            // add decimal point
+            size += 1;
+        }
+
+        return size;
+    }
+
+    /**
+     * Gets the exponent value. This exponent produces a floating point value with the
+     * correct magnitude when applied to the internal unsigned integer.
+     * @return exponent value
+     */
+    public int getExponent() {
+        return exponent;
+    }
+
+    /**
+     * Gets the number of characters required to create a plain format representation
+     * of this value.
+     * @param decimalPos decimal position relative to the {@code digits} array
+     * @param opts format options
+     * @return number of characters in the plain string representation of this value,
+     *      created using the given parameters
+     */
+    private int getPlainStringSize(final int decimalPos, final FormatOptions opts) {
+        int size = getDigitStringSize(decimalPos, opts);
+
+        // adjust for groupings if needed
+        if (opts.isGroupThousands() && decimalPos > 0) {
+            size += (decimalPos - 1) / THOUSANDS_GROUP_SIZE;
+        }
+
+        return size;
+    }
+
+    /**
+     * 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
+     */
+    public int getScientificExponent() {
+        return digitCount + exponent - 1;
+    }
+
+    /**
+     * 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);
+            }
+        }
+    }
+
+    /**
+     * Rounds the value up to the given number of digits.
+     * @param count target number of digits; must be greater than zero and
+     *      less than the current number of digits
+     */
+    private void roundUp(final int count) {
+        int removedDigits = digitCount - count;
+        int i;
+        for (i = count - 1; i >= 0; --i) {
+            final int d = digits[i] + 1;
+
+            if (d < DECIMAL_RADIX) {
+                // value did not carry over; done adding
+                digits[i] = d;
+                break;
+            }
+            // value carried over; the current position is 0
+            // which we will ignore by shortening the digit count
+            ++removedDigits;
+        }
+
+        if (i < 0) {
+            // all values carried over
+            setSingleDigitValue(1, exponent + removedDigits);
+        } else {
+            // values were updated in-place; just need to update the length
+            truncate(digitCount - removedDigits);
+        }
+    }
+
+    /**
+     * Sets the value of this instance to a single digit with the given exponent.
+     * The sign of the value is retained.
+     * @param digit digit value
+     * @param newExponent new exponent value
+     */
+    private void setSingleDigitValue(final int digit, final int newExponent) {
+        digits[0] = digit;
+        digitCount = 1;
+        exponent = newExponent;
+    }
+
+    /**
+     * Returns {@code true} if a formatted string with the given target exponent should include
+     * the exponent field.
+     * @param targetExponent exponent of the formatted result
+     * @param opts format options
+     * @return {@code true} if the formatted string should include the exponent field
+     */
+    private boolean shouldIncludeExponent(final int targetExponent, final FormatOptions opts) {
+        return targetExponent != 0 || opts.isAlwaysIncludeExponent();
+    }
+
+    /**
+     * Returns {@code true} if formatted strings should include the minus sign, considering
+     * the value of this instance and the given format options.
+     * @param opts format options
+     * @return {@code true} if a minus sign should be included in the output
+     */
+    private boolean shouldIncludeMinus(final FormatOptions opts) {
+        return negative && (opts.isSignedZero() || !isZero());
+    }
+
+    /**
+     * Returns {@code true} if a rounding operation for the given number of digits should
+     * round up.
+     * @param count number of digits to round to; must be greater than zero and less
+     *      than the current number of digits
+     * @return {@code true} if a rounding operation for the given number of digits should
+     *      round up
+     */
+    private boolean shouldRoundUp(final int count) {
+        // Round up in the following cases:
+        // 1. The digit after the last digit is greater than 5.
+        // 2. The digit after the last digit is 5 and there are additional (non-zero)
+        //      digits after it.
+        // 3. The digit after the last digit is 5, there are no additional digits afterward,
+        //      and the last digit is odd (half-even rounding).
+        final int digitAfterLast = digits[count];
+
+        return digitAfterLast > ROUND_CENTER || (digitAfterLast == ROUND_CENTER
+                && (count < digitCount - 1 || (digits[count - 1] % 2) != 0));
+    }
+
+    /**
+     * Returns a string representation of this value in engineering notation. This
+     * is similar to {@link #toScientificString(FormatOptions) scientific notation}
+     * but with the exponent forced to be a multiple of 3, allowing easier alignment with SI prefixes.
+     * <pre>
+     * 0 = "0.0"
+     * 10 = "10.0"
+     * 1e-6 = "1.0E-6"
+     * 1e11 = "100.0E9"
+     * </pre>
+     * @param opts format options
+     * @return value in engineering format
+     */
+    public String toEngineeringString(final FormatOptions opts) {
+        final int decimalPos = 1 + Math.floorMod(getScientificExponent(), ENG_EXPONENT_MOD);
+        return toScientificString(decimalPos, opts);
+    }
+
+    /**
+     * Returns a string representation of this value with no exponent field. 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;
+
+        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 the value in scientific notation using the
+     * given decimal point position.
+     * @param decimalPos decimal position relative to the {@code digits} array; this value
+     *      is expected to be greater than 0
+     * @param opts format options
+     * @return value in scientific format
+     */
+    private String toScientificString(final int decimalPos, final FormatOptions opts) {
+        final int targetExponent = digitCount + exponent - decimalPos;
+        final int absTargetExponent = Math.abs(targetExponent);
+        final boolean includeExponent = shouldIncludeExponent(targetExponent, opts);
+        final boolean negativeExponent = targetExponent < 0;
+
+        // determine the size of the full formatted string, including the number of
+        // characters needed for the exponent digits
+        int size = getDigitStringSize(decimalPos, opts);
+        int exponentDigitCount = 0;
+        if (includeExponent) {
+            exponentDigitCount = absTargetExponent > 0
+                    ? (int) Math.floor(Math.log10(absTargetExponent)) + 1
+                    : 1;
+
+            size += opts.getExponentSeparatorChars().length + exponentDigitCount;
+            if (negativeExponent) {
+                ++size;
+            }
+        }
+
+        prepareOutput(size);
+
+        // append the portion before the exponent field
+        final int fractionStartIdx = appendWhole(decimalPos, opts);
+        appendFraction(0, fractionStartIdx, opts);
+
+        if (includeExponent) {
+            // append the exponent field
+            append(opts.getExponentSeparatorChars());
+
+            if (negativeExponent) {
+                append(opts.getMinusSign());
+            }
+
+            // append the exponent digits themselves; compute the
+            // string representation directly and add it to the output
+            // buffer to avoid the overhead of Integer.toString()
+            final char[] localizedDigits = opts.getDigits();
+            int rem = absTargetExponent;
+            for (int i = size - 1; i >= outputIdx; --i) {
+                outputChars[i] = localizedDigits[rem % DECIMAL_RADIX];
+                rem /= DECIMAL_RADIX;
+            }
+            outputIdx = size;
+        }
+
+        return outputString();
+    }
+
+    /**
+     * Truncates the value to the given number of digits.
+     * @param count number of digits; must be greater than zero and less than
+     *      the current number of digits
+     */
+    private void truncate(final int count) {
+        // trim all trailing zero digits, making sure to leave
+        // at least one digit left
+        int nonZeroCount = count;
+        for (int i = count - 1;
+                i > 0 && digits[i] == 0;
+                --i) {
+            --nonZeroCount;
+        }
+        exponent += digitCount - nonZeroCount;
+        digitCount = nonZeroCount;
     }
 }