DoubleFormat.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.text.numbers;

import java.text.DecimalFormatSymbols;
import java.util.Objects;
import java.util.function.DoubleFunction;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * Enum containing standard double format types with methods to produce
 * configured formatter instances. This type is intended to provide a
 * quick and convenient way to create lightweight, thread-safe double format functions
 * for common format types using a builder pattern. Output can be localized by
 * passing a {@link DecimalFormatSymbols} instance to the
 * {@link Builder#formatSymbols(DecimalFormatSymbols) formatSymbols} method or by
 * directly calling the various other builder configuration methods, such as
 * {@link Builder#digits(String) digits}.
 *
 * <p><strong>Comparison with DecimalFormat</strong></p>
 * <p>
 * This type provides some of the same functionality as Java's own
 * {@link java.text.DecimalFormat}. However, unlike {@code DecimalFormat}, the format
 * functions produced by this type are lightweight and thread-safe, making them
 * much easier to work with in multi-threaded environments. They also provide performance
 * comparable to, and in many cases faster than, {@code DecimalFormat}.
 * </p>
 * <p>
 * It should be noted that the output {@code String} is created by formatting the output of
 * {@link Double#toString()}. This limits the output precision to the precision required
 * to exactly represent the input {@code double} and is dependent on the JDK implementation
 * of {@link Double#toString()}. A number formatted with the maximum
 * precision should be parsed to the same input {@code double}. This implementation
 * cannot extend the {@code String} to the required length to represent the exact decimal
 * value of the {@code double} as per
 * {@link java.math.BigDecimal#toString() BigDecimal#toString()}.
 * </p>
 * <p><strong>Examples</strong></p>
 * <pre>
 * // construct a formatter equivalent to Double.toString()
 * DoubleFunction&lt;String&gt; fmt = DoubleFormat.MIXED.builder().build();
 *
 * // construct a formatter equivalent to Double.toString() but using
 * // format symbols for a specific locale
 * DoubleFunction&lt;String&gt; fmt = DoubleFormat.MIXED.builder()
 *      .formatSymbols(DecimalFormatSymbols.getInstance(locale))
 *      .build();
 *
 * // construct a formatter equivalent to the DecimalFormat pattern "0.0##"
 * DoubleFunction&lt;String&gt; fmt = DoubleFormat.PLAIN.builder()
 *      .minDecimalExponent(-3)
 *      .build();
 *
 * // construct a formatter equivalent to the DecimalFormat pattern "#,##0.0##",
 * // where whole number groups of thousands are separated
 * DoubleFunction&lt;String&gt; fmt = DoubleFormat.PLAIN.builder()
 *      .minDecimalExponent(-3)
 *      .groupThousands(true)
 *      .build();
 *
 * // construct a formatter equivalent to the DecimalFormat pattern "0.0##E0"
 * DoubleFunction&lt;String&gt; fmt = DoubleFormat.SCIENTIFIC.builder()
 *      .maxPrecision(4)
 *      .alwaysIncludeExponent(true)
 *      .build()
 *
 * // construct a formatter equivalent to the DecimalFormat pattern "##0.0##E0",
 * // i.e. "engineering format"
 * DoubleFunction&lt;String&gt; fmt = DoubleFormat.ENGINEERING.builder()
 *      .maxPrecision(6)
 *      .alwaysIncludeExponent(true)
 *      .build()
 * </pre>
 *
 * <p><strong>Implementation Notes</strong></p>
 * <p>
 * {@link java.math.RoundingMode#HALF_EVEN Half-even} rounding is used in cases where the
 * decimal value must be rounded in order to meet the configuration requirements of the formatter
 * instance.
 * </p>
 *
 * @since 1.10.0
 */
public enum DoubleFormat {

    /**
     * Number format without exponents.
     * <p>
     * For example:
     * </p>
     *
     * <pre>
     * 0.0
     * 12.401
     * 100000.0
     * 1450000000.0
     * 0.0000000000123
     * </pre>
     */
    PLAIN(PlainDoubleFormat::new),

    /**
     * Number format that uses exponents and contains a single digit to the left of the decimal point.
     * <p>
     * For example:
     * </p>
     *
     * <pre>
     * 0.0
     * 1.2401E1
     * 1.0E5
     * 1.45E9
     * 1.23E-11
     * </pre>
     */
    SCIENTIFIC(ScientificDoubleFormat::new),

    /**
     * Number format similar to {@link #SCIENTIFIC scientific format} but adjusted so that the exponent value is always a multiple of 3, allowing easier
     * alignment with SI prefixes.
     * <p>
     * For example:
     * </p>
     *
     * <pre>
     * 0.0
     * 12.401
     * 100.0E3
     * 1.45E9
     * 12.3E-12
     * </pre>
     */
    ENGINEERING(EngineeringDoubleFormat::new),

    /**
     * Number format that uses {@link #PLAIN plain format} for small numbers and {@link #SCIENTIFIC scientific format} for large numbers. The number thresholds
     * can be configured through the {@link Builder#plainFormatMinDecimalExponent(int) plainFormatMinDecimalExponent} and
     * {@link Builder#plainFormatMaxDecimalExponent(int) plainFormatMaxDecimalExponent} properties.
     * <p>
     * For example:
     * </p>
     *
     * <pre>
     * 0.0
     * 12.401
     * 100000.0
     * 1.45E9
     * 1.23E-11
     * </pre>
     */
    MIXED(MixedDoubleFormat::new);

    /**
     * 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 String apply(final double d) {
            if (Double.isFinite(d)) {
                return applyFinite(d);
            }
            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;
        }
    }

    /**
     * Builds configured format functions for standard double format types.
     */
    public static final class Builder implements Supplier<DoubleFunction<String>> {

        /** Default value for the plain format max decimal exponent. */
        private static final int DEFAULT_PLAIN_FORMAT_MAX_DECIMAL_EXPONENT = 6;

        /** Default value for the plain format min decimal exponent. */
        private static final int DEFAULT_PLAIN_FORMAT_MIN_DECIMAL_EXPONENT = -3;

        /** Default decimal digit characters. */
        private static final String DEFAULT_DECIMAL_DIGITS = "0123456789";

        /**
         * 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 static 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);
        }

        /** Function used to construct format instances. */
        private final Function<Builder, DoubleFunction<String>> factory;

        /** Maximum number of significant decimal digits in formatted strings. */
        private int maxPrecision;

        /** Minimum decimal exponent. */
        private int minDecimalExponent = Integer.MIN_VALUE;

        /** Max decimal exponent to use with plain formatting with the mixed format type. */
        private int plainFormatMaxDecimalExponent = DEFAULT_PLAIN_FORMAT_MAX_DECIMAL_EXPONENT;

        /** Min decimal exponent to use with plain formatting with the mixed format type. */
        private int plainFormatMinDecimalExponent = DEFAULT_PLAIN_FORMAT_MIN_DECIMAL_EXPONENT;

        /** String representing infinity. */
        private String infinity = "Infinity";

        /** String representing NaN. */
        private String nan = "NaN";

        /** Flag determining if fraction placeholders should be used. */
        private boolean fractionPlaceholder = true;

        /** Flag determining if signed zero strings are allowed. */
        private boolean signedZero = true;

        /** String of digit characters 0-9. */
        private String digits = DEFAULT_DECIMAL_DIGITS;

        /** Decimal separator character. */
        private char decimalSeparator = '.';

        /** Character used to separate groups of thousands. */
        private char groupingSeparator = ',';

        /** If {@code true}, thousands groups will be separated by the grouping separator. */
        private boolean groupThousands;

        /** Minus sign character. */
        private char minusSign = '-';

        /** Exponent separator character. */
        private String exponentSeparator = "E";

        /** Flag indicating if the exponent value should always be included, even if zero. */
        private boolean alwaysIncludeExponent;

        /**
         * Builds a new instance that delegates double function construction to the given factory object.
         *
         * @param factory factory function
         */
        private Builder(final Function<Builder, DoubleFunction<String>> factory) {
            this.factory = factory;
        }

        /**
         * 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"} will be
         * returned, regardless of the sign of the input number. The default value is {@code true}.
         *
         * @param signedZero if {@code true}, the zero string may be returned with a preceding minus sign; if {@code false}, the zero string will only be
         *                   returned in its positive form
         * @return this instance
         */
        public Builder allowSignedZero(final boolean signedZero) {
            this.signedZero = signedZero;
            return this;
        }

        /**
         * 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
         * @deprecated Use {@link #get()}.
         */
        @Deprecated
        public DoubleFunction<String> build() {
            return get();
        }

        /**
         * 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
         * @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 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"}.
         *
         * @param exponentSeparator exponent separator string
         * @return this instance
         * @throws NullPointerException if the argument is {@code null}
         */
        public Builder exponentSeparator(final String exponentSeparator) {
            this.exponentSeparator = Objects.requireNonNull(exponentSeparator, "Exponent separator cannot be null");
            return this;
        }

        /**
         * Configures this instance with the given format symbols. The following values are set:
         * <ul>
         * <li>{@link #digits(String) digit characters}</li>
         * <li>{@link #decimalSeparator(char) decimal separator}</li>
         * <li>{@link #groupingSeparator(char) thousands grouping separator}</li>
         * <li>{@link #minusSign(char) minus sign}</li>
         * <li>{@link #exponentSeparator(String) exponent separator}</li>
         * <li>{@link #infinity(String) infinity}</li>
         * <li>{@link #nan(String) NaN}</li>
         * </ul>
         * The digit character string is constructed by starting at the configured {@link DecimalFormatSymbols#getZeroDigit() zero digit} and adding the next 9
         * consecutive characters.
         *
         * @param symbols format symbols
         * @return this instance
         * @throws NullPointerException if the argument is {@code null}
         */
        public Builder formatSymbols(final DecimalFormatSymbols symbols) {
            Objects.requireNonNull(symbols, "Decimal format symbols cannot be null");

            return digits(getDigitString(symbols)).decimalSeparator(symbols.getDecimalSeparator()).groupingSeparator(symbols.getGroupingSeparator())
                    .minusSign(symbols.getMinusSign()).exponentSeparator(symbols.getExponentSeparator()).infinity(symbols.getInfinity()).nan(symbols.getNaN());
        }

        /**
         * Builds a new double format function.
         *
         * @return format function
         */
        @Override
        public DoubleFunction<String> get() {
            return factory.apply(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 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 {@code 6}.
         *
         * <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 {@code -3}.
         *
         * <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;
        }
    }

    /**
     * Format class that uses engineering notation for all values.
     */
    private static final class EngineeringDoubleFormat extends AbstractDoubleFormat {

        /**
         * 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);
        }
    }

    /**
     * Format class producing results similar to {@link Double#toString()}, with plain decimal notation for small numbers relatively close to zero and
     * scientific notation otherwise.
     */
    private static final class MixedDoubleFormat extends AbstractDoubleFormat {

        /** Max decimal exponent for plain format. */
        private final int plainMaxExponent;

        /** Min decimal exponent for plain format. */
        private final int plainMinExponent;

        /**
         * Constructs a new instance.
         *
         * @param builder builder instance containing configuration values
         */
        MixedDoubleFormat(final Builder builder) {
            super(builder);

            this.plainMaxExponent = builder.plainFormatMaxDecimalExponent;
            this.plainMinExponent = builder.plainFormatMinDecimalExponent;
        }

        /** {@inheritDoc} */
        @Override
        protected String applyFiniteInternal(final ParsedDecimal val) {
            final int sciExp = val.getScientificExponent();
            if (sciExp <= plainMaxExponent && sciExp >= plainMinExponent) {
                return val.toPlainString(this);
            }
            return val.toScientificString(this);
        }
    }

    /**
     * Format class that produces plain decimal strings that do not use scientific notation.
     */
    private static final 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 final class ScientificDoubleFormat extends AbstractDoubleFormat {

        /**
         * Constructs a new instance.
         *
         * @param builder builder instance containing configuration values
         */
        ScientificDoubleFormat(final Builder builder) {
            super(builder);
        }

        /** {@inheritDoc} */
        @Override
        public String applyFiniteInternal(final ParsedDecimal val) {
            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);
    }
}