// Written in the D programming language.

/**
   This module implements the formatting functionality for strings and
   I/O. It's comparable to C99's `vsprintf()` and uses a similar
   _format encoding scheme.

   For an introductory look at $(B std._format)'s capabilities and how to use
   this module see the dedicated
   $(LINK2 http://wiki.dlang.org/Defining_custom_print_format_specifiers, DWiki article).

   This module centers around two functions:

$(BOOKTABLE ,
$(TR $(TH Function Name) $(TH Description)
)
    $(TR $(TD $(LREF formattedRead))
        $(TD Reads values according to the format string from an InputRange.
    ))
    $(TR $(TD $(LREF formattedWrite))
        $(TD Formats its arguments according to the format string and puts them
        to an OutputRange.
    ))
)

   Please see the documentation of function $(LREF formattedWrite) for a
   description of the format string.

   Two functions have been added for convenience:

$(BOOKTABLE ,
$(TR $(TH Function Name) $(TH Description)
)
    $(TR $(TD $(LREF format))
        $(TD Returns a GC-allocated string with the formatting result.
    ))
    $(TR $(TD $(LREF sformat))
        $(TD Puts the formatting result into a preallocated array.
    ))
)

   These two functions are publicly imported by $(MREF std, string)
   to be easily available.

   The functions $(LREF formatValue) and $(LREF unformatValue) are
   used for the plumbing.
   Copyright: Copyright The D Language Foundation 2000-2013.

   License: $(HTTP boost.org/LICENSE_1_0.txt, Boost License 1.0).

   Authors: $(HTTP walterbright.com, Walter Bright), $(HTTP erdani.com,
   Andrei Alexandrescu), and Kenji Hara

   Source: $(PHOBOSSRC std/format.d)
 */
module std.format;

//debug=format;                // uncomment to turn on debugging printf's

import core.vararg;
import std.exception;
import std.meta;
import std.range.primitives;
import std.traits;


/**
Signals a mismatch between a format and its corresponding argument.
 */
class FormatException : Exception
{
    @safe @nogc pure nothrow
    this()
    {
        super("format error");
    }

    @safe @nogc pure nothrow
    this(string msg, string fn = __FILE__, size_t ln = __LINE__, Throwable next = null)
    {
        super(msg, fn, ln, next);
    }
}

///
@safe unittest
{
    import std.exception : assertThrown;
    assertThrown!FormatException(format("%d", "foo"));
}

private alias enforceFmt = enforce!FormatException;


/**********************************************************************
   Interprets variadic argument list `args`, formats them according
   to `fmt`, and sends the resulting characters to `w`. The
   encoding of the output is the same as `Char`. The type `Writer`
   must satisfy $(D $(REF isOutputRange, std,range,primitives)!(Writer, Char)).

   The variadic arguments are normally consumed in order. POSIX-style
   $(HTTP opengroup.org/onlinepubs/009695399/functions/printf.html,
   positional parameter syntax) is also supported. Each argument is
   formatted into a sequence of chars according to the format
   specification, and the characters are passed to `w`. As many
   arguments as specified in the format string are consumed and
   formatted. If there are fewer arguments than format specifiers, a
   `FormatException` is thrown. If there are more remaining arguments
   than needed by the format specification, they are ignored but only
   if at least one argument was formatted.

   The format string supports the formatting of array and nested array elements
   via the grouping format specifiers $(B %&#40;) and $(B %&#41;). Each
   matching pair of $(B %&#40;) and $(B %&#41;) corresponds with a single array
   argument. The enclosed sub-format string is applied to individual array
   elements.  The trailing portion of the sub-format string following the
   conversion specifier for the array element is interpreted as the array
   delimiter, and is therefore omitted following the last array element. The
   $(B %|) specifier may be used to explicitly indicate the start of the
   delimiter, so that the preceding portion of the string will be included
   following the last array element.  (See below for explicit examples.)

   Params:

   w = Output is sent to this writer. Typical output writers include
   $(REF Appender!string, std,array) and $(REF LockingTextWriter, std,stdio).

   fmt = Format string.

   args = Variadic argument list.

   Returns: Formatted number of arguments.

   Throws: Mismatched arguments and formats result in a $(D
   FormatException) being thrown.

   Format_String: <a name="format-string">$(I Format strings)</a>
   consist of characters interspersed with $(I format
   specifications). Characters are simply copied to the output (such
   as putc) after any necessary conversion to the corresponding UTF-8
   sequence.

   The format string has the following grammar:

$(PRE
$(I FormatString):
    $(I FormatStringItem)*
$(I FormatStringItem):
    $(B '%%')
    $(B '%') $(I Position) $(I Flags) $(I Width) $(I Separator) $(I Precision) $(I FormatChar)
    $(B '%$(LPAREN)') $(I FormatString) $(B '%$(RPAREN)')
    $(B '%-$(LPAREN)') $(I FormatString) $(B '%$(RPAREN)')
    $(I OtherCharacterExceptPercent)
$(I Position):
    $(I empty)
    $(I Integer) $(B '$')
$(I Flags):
    $(I empty)
    $(B '-') $(I Flags)
    $(B '+') $(I Flags)
    $(B '#') $(I Flags)
    $(B '0') $(I Flags)
    $(B ' ') $(I Flags)
$(I Width):
    $(I empty)
    $(I Integer)
    $(B '*')
$(I Separator):
    $(I empty)
    $(B ',')
    $(B ',') $(B '?')
    $(B ',') $(B '*') $(B '?')
    $(B ',') $(I Integer) $(B '?')
    $(B ',') $(B '*')
    $(B ',') $(I Integer)
$(I Precision):
    $(I empty)
    $(B '.')
    $(B '.') $(I Integer)
    $(B '.*')
$(I Integer):
    $(I Digit)
    $(I Digit) $(I Integer)
$(I Digit):
    $(B '0')|$(B '1')|$(B '2')|$(B '3')|$(B '4')|$(B '5')|$(B '6')|$(B '7')|$(B '8')|$(B '9')
$(I FormatChar):
    $(B 's')|$(B 'c')|$(B 'b')|$(B 'd')|$(B 'o')|$(B 'x')|$(B 'X')|$(B 'e')|$(B 'E')|$(B 'f')|$(B 'F')|$(B 'g')|$(B 'G')|$(B 'a')|$(B 'A')|$(B '|')
)

    $(BOOKTABLE Flags affect formatting depending on the specifier as
    follows., $(TR $(TH Flag) $(TH Types&nbsp;affected) $(TH Semantics))

    $(TR $(TD $(B '-')) $(TD numeric, bool, null, char, string, enum, pointer) $(TD Left justify the result in
        the field.  It overrides any $(B 0) flag.))

    $(TR $(TD $(B '+')) $(TD numeric) $(TD Prefix positive numbers in
    a signed conversion with a $(B +).  It overrides any $(I space)
    flag.))

    $(TR $(TD $(B '#')) $(TD integral ($(B 'o'))) $(TD Add to
    precision as necessary so that the first digit of the octal
    formatting is a '0', even if both the argument and the $(I
    Precision) are zero.))

    $(TR $(TD $(B '#')) $(TD integral ($(B 'x'), $(B 'X'))) $(TD If
       non-zero, prefix result with $(B 0x) ($(B 0X)).))

    $(TR $(TD $(B '#')) $(TD floating) $(TD Always insert the decimal
       point and print trailing zeros.))

    $(TR $(TD $(B '0')) $(TD numeric) $(TD Use leading
    zeros to pad rather than spaces (except for the floating point
    values `nan` and `infinity`).  Ignore if there's a $(I
    Precision).))

    $(TR $(TD $(B ' ')) $(TD numeric) $(TD Prefix positive
    numbers in a signed conversion with a space.)))

    $(DL
        $(DT $(I Width))
        $(DD
        Only used for numeric, bool, null, char, string, enum and pointer types.
        Specifies the minimum field width.
        If the width is a $(B *), an additional argument of type $(B int),
        preceding the actual argument, is taken as the width.
        If the width is negative, it is as if the $(B -) was given
        as a $(I Flags) character.)

        $(DT $(I Precision))
        $(DD Gives the precision for numeric conversions.
        If the precision is a $(B *), an additional argument of type $(B int),
        preceding the actual argument, is taken as the precision.
        If it is negative, it is as if there was no $(I Precision) specifier.)

        $(DT $(I Separator))
        $(DD Inserts the separator symbols ',' every $(I X) digits, from right
        to left, into numeric values to increase readability.
        The fractional part of floating point values inserts the separator
        from left to right.
        Entering an integer after the ',' allows to specify $(I X).
        If a '*' is placed after the ',' then $(I X) is specified by an
        additional parameter to the format function.
        Adding a '?' after the ',' or $(I X) specifier allows to specify
        the separator character as an additional parameter.
        )

        $(DT $(I FormatChar))
        $(DD
        $(DL
            $(DT $(B 's'))
            $(DD The corresponding argument is formatted in a manner consistent
            with its type:
            $(DL
                $(DT $(B bool))
                $(DD The result is `"true"` or `"false"`.)
                $(DT integral types)
                $(DD The $(B %d) format is used.)
                $(DT floating point types)
                $(DD The $(B %g) format is used.)
                $(DT string types)
                $(DD The result is the string converted to UTF-8.
                A $(I Precision) specifies the maximum number of characters
                to use in the result.)
                $(DT structs)
                $(DD If the struct defines a $(B toString()) method the result is
                the string returned from this function. Otherwise the result is
                StructName(field<sub>0</sub>, field<sub>1</sub>, ...) where
                field<sub>n</sub> is the nth element formatted with the default
                format.)
                $(DT classes derived from $(B Object))
                $(DD The result is the string returned from the class instance's
                $(B .toString()) method.
                A $(I Precision) specifies the maximum number of characters
                to use in the result.)
                $(DT unions)
                $(DD If the union defines a $(B toString()) method the result is
                the string returned from this function. Otherwise the result is
                the name of the union, without its contents.)
                $(DT non-string static and dynamic arrays)
                $(DD The result is [s<sub>0</sub>, s<sub>1</sub>, ...]
                where s<sub>n</sub> is the nth element
                formatted with the default format.)
                $(DT associative arrays)
                $(DD The result is the equivalent of what the initializer
                would look like for the contents of the associative array,
                e.g.: ["red" : 10, "blue" : 20].)
            ))

            $(DT $(B 'c'))
            $(DD The corresponding argument must be a character type.)

            $(DT $(B 'b','d','o','x','X'))
            $(DD The corresponding argument must be an integral type
            and is formatted as an integer. If the argument is a signed type
            and the $(I FormatChar) is $(B d) it is converted to
            a signed string of characters, otherwise it is treated as
            unsigned. An argument of type $(B bool) is formatted as '1'
            or '0'. The base used is binary for $(B b), octal for $(B o),
            decimal
            for $(B d), and hexadecimal for $(B x) or $(B X).
            $(B x) formats using lower case letters, $(B X) uppercase.
            If there are fewer resulting digits than the $(I Precision),
            leading zeros are used as necessary.
            If the $(I Precision) is 0 and the number is 0, no digits
            result.)

            $(DT $(B 'e','E'))
            $(DD A floating point number is formatted as one digit before
            the decimal point, $(I Precision) digits after, the $(I FormatChar),
            &plusmn;, followed by at least a two digit exponent:
            $(I d.dddddd)e$(I &plusmn;dd).
            If there is no $(I Precision), six
            digits are generated after the decimal point.
            If the $(I Precision) is 0, no decimal point is generated.)

            $(DT $(B 'f','F'))
            $(DD A floating point number is formatted in decimal notation.
            The $(I Precision) specifies the number of digits generated
            after the decimal point. It defaults to six. At least one digit
            is generated before the decimal point. If the $(I Precision)
            is zero, no decimal point is generated.)

            $(DT $(B 'g','G'))
            $(DD A floating point number is formatted in either $(B e) or
            $(B f) format for $(B g); $(B E) or $(B F) format for
            $(B G).
            The $(B f) format is used if the exponent for an $(B e) format
            is greater than -5 and less than the $(I Precision).
            The $(I Precision) specifies the number of significant
            digits, and defaults to six.
            Trailing zeros are elided after the decimal point, if the fractional
            part is zero then no decimal point is generated.)

            $(DT $(B 'a','A'))
            $(DD A floating point number is formatted in hexadecimal
            exponential notation 0x$(I h.hhhhhh)p$(I &plusmn;d).
            There is one hexadecimal digit before the decimal point, and as
            many after as specified by the $(I Precision).
            If the $(I Precision) is zero, no decimal point is generated.
            If there is no $(I Precision), as many hexadecimal digits as
            necessary to exactly represent the mantissa are generated.
            The exponent is written in as few digits as possible,
            but at least one, is in decimal, and represents a power of 2 as in
            $(I h.hhhhhh)*2<sup>$(I &plusmn;d)</sup>.
            The exponent for zero is zero.
            The hexadecimal digits, x and p are in upper case if the
            $(I FormatChar) is upper case.)
        ))
    )

    Floating point NaN's are formatted as $(B nan) if the
    $(I FormatChar) is lower case, or $(B NAN) if upper.
    Floating point infinities are formatted as $(B inf) or
    $(B infinity) if the
    $(I FormatChar) is lower case, or $(B INF) or $(B INFINITY) if upper.

    The positional and non-positional styles can be mixed in the same
    format string. (POSIX leaves this behavior undefined.) The internal
    counter for non-positional parameters tracks the next parameter after
    the largest positional parameter already used.

    Example using array and nested array formatting:
    -------------------------
    import std.stdio;

    void main()
    {
        writefln("My items are %(%s %).", [1,2,3]);
        writefln("My items are %(%s, %).", [1,2,3]);
    }
    -------------------------
    The output is:
$(CONSOLE
My items are 1 2 3.
My items are 1, 2, 3.
)

    The trailing end of the sub-format string following the specifier for each
    item is interpreted as the array delimiter, and is therefore omitted
    following the last array item. The $(B %|) delimiter specifier may be used
    to indicate where the delimiter begins, so that the portion of the format
    string prior to it will be retained in the last array element:
    -------------------------
    import std.stdio;

    void main()
    {
        writefln("My items are %(-%s-%|, %).", [1,2,3]);
    }
    -------------------------
    which gives the output:
$(CONSOLE
My items are -1-, -2-, -3-.
)

    These compound format specifiers may be nested in the case of a nested
    array argument:
    -------------------------
    import std.stdio;
    void main() {
         auto mat = [[1, 2, 3],
                     [4, 5, 6],
                     [7, 8, 9]];

         writefln("%(%(%d %)\n%)", mat);
         writeln();

         writefln("[%(%(%d %)\n %)]", mat);
         writeln();

         writefln("[%([%(%d %)]%|\n %)]", mat);
         writeln();
    }
    -------------------------
    The output is:
$(CONSOLE
1 2 3
4 5 6
7 8 9

[1 2 3
 4 5 6
 7 8 9]

[[1 2 3]
 [4 5 6]
 [7 8 9]]
)

    Inside a compound format specifier, strings and characters are escaped
    automatically. To avoid this behavior, add $(B '-') flag to
    `"%$(LPAREN)"`.
    -------------------------
    import std.stdio;

    void main()
    {
        writefln("My friends are %s.", ["John", "Nancy"]);
        writefln("My friends are %(%s, %).", ["John", "Nancy"]);
        writefln("My friends are %-(%s, %).", ["John", "Nancy"]);
    }
    -------------------------
   which gives the output:
$(CONSOLE
My friends are ["John", "Nancy"].
My friends are "John", "Nancy".
My friends are John, Nancy.
)
 */
uint formattedWrite(alias fmt, Writer, A...)(auto ref Writer w, A args)
if (isSomeString!(typeof(fmt)))
{
    alias e = checkFormatException!(fmt, A);
    static assert(!e, e.msg);
    return .formattedWrite(w, fmt, args);
}

/// The format string can be checked at compile-time (see $(LREF format) for details):
@safe pure unittest
{
    import std.array : appender;

    auto writer = appender!string();
    writer.formattedWrite!"%s is the ultimate %s."(42, "answer");
    assert(writer.data == "42 is the ultimate answer.");

    // Clear the writer
    writer = appender!string();
    formattedWrite(writer, "Date: %2$s %1$s", "October", 5);
    assert(writer.data == "Date: 5 October");
}

/// ditto
uint formattedWrite(Writer, Char, A...)(auto ref Writer w, const scope Char[] fmt, A args)
{
    import std.conv : text;

    auto spec = FormatSpec!Char(fmt);

    // Are we already done with formats? Then just dump each parameter in turn
    uint currentArg = 0;
    while (spec.writeUpToNextSpec(w))
    {
        if (currentArg == A.length && !spec.indexStart)
        {
            // leftover spec?
            enforceFmt(fmt.length == 0,
                text("Orphan format specifier: %", spec.spec));
            break;
        }

        if (spec.width == spec.DYNAMIC)
        {
            auto width = getNthInt!"integer width"(currentArg, args);
            if (width < 0)
            {
                spec.flDash = true;
                width = -width;
            }
            spec.width = width;
            ++currentArg;
        }
        else if (spec.width < 0)
        {
            // means: get width as a positional parameter
            auto index = cast(uint) -spec.width;
            assert(index > 0, "The index must be greater than zero");
            auto width = getNthInt!"integer width"(index - 1, args);
            if (currentArg < index) currentArg = index;
            if (width < 0)
            {
                spec.flDash = true;
                width = -width;
            }
            spec.width = width;
        }

        if (spec.precision == spec.DYNAMIC)
        {
            auto precision = getNthInt!"integer precision"(currentArg, args);
            if (precision >= 0) spec.precision = precision;
            // else negative precision is same as no precision
            else spec.precision = spec.UNSPECIFIED;
            ++currentArg;
        }
        else if (spec.precision < 0)
        {
            // means: get precision as a positional parameter
            auto index = cast(uint) -spec.precision;
            assert(index > 0, "The precision must be greater than zero");
            auto precision = getNthInt!"integer precision"(index- 1, args);
            if (currentArg < index) currentArg = index;
            if (precision >= 0) spec.precision = precision;
            // else negative precision is same as no precision
            else spec.precision = spec.UNSPECIFIED;
        }

        if (spec.separators == spec.DYNAMIC)
        {
            auto separators = getNthInt!"separator digit width"(currentArg, args);
            spec.separators = separators;
            ++currentArg;
        }

        if (spec.separatorCharPos == spec.DYNAMIC)
        {
            auto separatorChar =
                getNth!("separator character", isSomeChar, dchar)(currentArg, args);
            spec.separatorChar = separatorChar;
            ++currentArg;
        }

        if (currentArg == A.length && !spec.indexStart)
        {
            // leftover spec?
            enforceFmt(fmt.length == 0,
                text("Orphan format specifier: %", spec.spec));
            break;
        }

        // Format an argument
        // This switch uses a static foreach to generate a jump table.
        // Currently `spec.indexStart` use the special value '0' to signal
        // we should use the current argument. An enhancement would be to
        // always store the index.
        size_t index = currentArg;
        if (spec.indexStart != 0)
            index = spec.indexStart - 1;
        else
            ++currentArg;
    SWITCH: switch (index)
        {
            foreach (i, Tunused; A)
            {
            case i:
                formatValue(w, args[i], spec);
                if (currentArg < spec.indexEnd)
                    currentArg = spec.indexEnd;
                // A little know feature of format is to format a range
                // of arguments, e.g. `%1:3$` will format the first 3
                // arguments. Since they have to be consecutive we can
                // just use explicit fallthrough to cover that case.
                if (i + 1 < spec.indexEnd)
                {
                    // You cannot goto case if the next case is the default
                    static if (i + 1 < A.length)
                        goto case;
                    else
                        goto default;
                }
                else
                    break SWITCH;
            }
        default:
            throw new FormatException(
                text("Positional specifier %", spec.indexStart, '$', spec.spec,
                     " index exceeds ", A.length));
        }
    }
    return currentArg;
}

///
@safe unittest
{
    assert(format("%,d", 1000) == "1,000");
    assert(format("%,f", 1234567.891011) == "1,234,567.891011");
    assert(format("%,?d", '?', 1000) == "1?000");
    assert(format("%,1d", 1000) == "1,0,0,0", format("%,1d", 1000));
    assert(format("%,*d", 4, -12345) == "-1,2345");
    assert(format("%,*?d", 4, '_', -12345) == "-1_2345");
    assert(format("%,6?d", '_', -12345678) == "-12_345678");
    assert(format("%12,3.3f", 1234.5678) == "   1,234.568", "'" ~
            format("%12,3.3f", 1234.5678) ~ "'");
}

@safe pure unittest
{
    import std.array;
    auto w = appender!string();
    formattedWrite(w, "%s %d", "@safe/pure", 42);
    assert(w.data == "@safe/pure 42");
}

@safe pure unittest
{
    char[20] buf;
    auto w = buf[];
    formattedWrite(w, "%s %d", "@safe/pure", 42);
    assert(buf[0 .. $ - w.length] == "@safe/pure 42");
}

/**
Reads characters from $(REF_ALTTEXT input range, isInputRange, std,range,primitives)
`r`, converts them according to `fmt`, and writes them to `args`.

Params:
    r = The range to read from.
    fmt = The format of the data to read.
    args = The drain of the data read.

Returns:

On success, the function returns the number of variables filled. This count
can match the expected number of readings or fewer, even zero, if a
matching failure happens.

Throws:
    A `FormatException` if `S.length == 0` and `fmt` has format specifiers.
 */
uint formattedRead(alias fmt, R, S...)(auto ref R r, auto ref S args)
if (isSomeString!(typeof(fmt)))
{
    alias e = checkFormatException!(fmt, S);
    static assert(!e, e.msg);
    return .formattedRead(r, fmt, args);
}

/// ditto
uint formattedRead(R, Char, S...)(auto ref R r, const(Char)[] fmt, auto ref S args)
{
    import std.typecons : isTuple;

    auto spec = FormatSpec!Char(fmt);
    static if (!S.length)
    {
        spec.readUpToNextSpec(r);
        enforceFmt(spec.trailing.empty, "Trailing characters in formattedRead format string");
        return 0;
    }
    else
    {
        enum hasPointer = isPointer!(typeof(args[0]));

        // The function below accounts for '*' == fields meant to be
        // read and skipped
        void skipUnstoredFields()
        {
            for (;;)
            {
                spec.readUpToNextSpec(r);
                if (spec.width != spec.DYNAMIC) break;
                // must skip this field
                skipData(r, spec);
            }
        }

        skipUnstoredFields();
        if (r.empty)
        {
            // Input is empty, nothing to read
            return 0;
        }
        static if (hasPointer)
            alias A = typeof(*args[0]);
        else
            alias A = typeof(args[0]);

        static if (isTuple!A)
        {
            foreach (i, T; A.Types)
            {
                static if (hasPointer)
                    (*args[0])[i] = unformatValue!(T)(r, spec);
                else
                    args[0][i] = unformatValue!(T)(r, spec);
                skipUnstoredFields();
            }
        }
        else
        {
            static if (hasPointer)
                *args[0] = unformatValue!(A)(r, spec);
            else
                args[0] = unformatValue!(A)(r, spec);
        }
        return 1 + formattedRead(r, spec.trailing, args[1 .. $]);
    }
}

/// The format string can be checked at compile-time (see $(LREF format) for details):
@safe pure unittest
{
    string s = "hello!124:34.5";
    string a;
    int b;
    double c;
    s.formattedRead!"%s!%s:%s"(a, b, c);
    assert(a == "hello" && b == 124 && c == 34.5);
}

@safe unittest
{
    import std.math;
    string s = " 1.2 3.4 ";
    double x, y, z;
    assert(formattedRead(s, " %s %s %s ", x, y, z) == 2);
    assert(s.empty);
    assert(approxEqual(x, 1.2));
    assert(approxEqual(y, 3.4));
    assert(isNaN(z));
}

// for backwards compatibility
@system pure unittest
{
    string s = "hello!124:34.5";
    string a;
    int b;
    double c;
    formattedRead(s, "%s!%s:%s", &a, &b, &c);
    assert(a == "hello" && b == 124 && c == 34.5);

    // mix pointers and auto-ref
    s = "world!200:42.25";
    formattedRead(s, "%s!%s:%s", a, &b, &c);
    assert(a == "world" && b == 200 && c == 42.25);

    s = "world1!201:42.5";
    formattedRead(s, "%s!%s:%s", &a, &b, c);
    assert(a == "world1" && b == 201 && c == 42.5);

    s = "world2!202:42.75";
    formattedRead(s, "%s!%s:%s", a, b, &c);
    assert(a == "world2" && b == 202 && c == 42.75);
}

// for backwards compatibility
@system pure unittest
{
    import std.math;
    string s = " 1.2 3.4 ";
    double x, y, z;
    assert(formattedRead(s, " %s %s %s ", &x, &y, &z) == 2);
    assert(s.empty);
    assert(approxEqual(x, 1.2));
    assert(approxEqual(y, 3.4));
    assert(isNaN(z));
}

@system pure unittest
{
    string line;

    bool f1;

    line = "true";
    formattedRead(line, "%s", &f1);
    assert(f1);

    line = "TrUE";
    formattedRead(line, "%s", &f1);
    assert(f1);

    line = "false";
    formattedRead(line, "%s", &f1);
    assert(!f1);

    line = "fALsE";
    formattedRead(line, "%s", &f1);
    assert(!f1);

    line = "1";
    formattedRead(line, "%d", &f1);
    assert(f1);

    line = "-1";
    formattedRead(line, "%d", &f1);
    assert(f1);

    line = "0";
    formattedRead(line, "%d", &f1);
    assert(!f1);

    line = "-0";
    formattedRead(line, "%d", &f1);
    assert(!f1);
}

@system pure unittest
{
     union B
     {
         char[int.sizeof] untyped;
         int typed;
     }
     B b;
     b.typed = 5;
     char[] input = b.untyped[];
     int witness;
     formattedRead(input, "%r", &witness);
     assert(witness == b.typed);
}

@system pure unittest
{
    union A
    {
        char[float.sizeof] untyped;
        float typed;
    }
    A a;
    a.typed = 5.5;
    char[] input = a.untyped[];
    float witness;
    formattedRead(input, "%r", &witness);
    assert(witness == a.typed);
}

@system pure unittest
{
    import std.typecons;
    char[] line = "1 2".dup;
    int a, b;
    formattedRead(line, "%s %s", &a, &b);
    assert(a == 1 && b == 2);

    line = "10 2 3".dup;
    formattedRead(line, "%d ", &a);
    assert(a == 10);
    assert(line == "2 3");

    Tuple!(int, float) t;
    line = "1 2.125".dup;
    formattedRead(line, "%d %g", &t);
    assert(t[0] == 1 && t[1] == 2.125);

    line = "1 7643 2.125".dup;
    formattedRead(line, "%s %*u %s", &t);
    assert(t[0] == 1 && t[1] == 2.125);
}

@system pure unittest
{
    string line;

    char c1, c2;

    line = "abc";
    formattedRead(line, "%s%c", &c1, &c2);
    assert(c1 == 'a' && c2 == 'b');
    assert(line == "c");
}

@system pure unittest
{
    string line;

    line = "[1,2,3]";
    int[] s1;
    formattedRead(line, "%s", &s1);
    assert(s1 == [1,2,3]);
}

@system pure unittest
{
    string line;

    line = "[1,2,3]";
    int[] s1;
    formattedRead(line, "[%(%s,%)]", &s1);
    assert(s1 == [1,2,3]);

    line = `["hello", "world"]`;
    string[] s2;
    formattedRead(line, "[%(%s, %)]", &s2);
    assert(s2 == ["hello", "world"]);

    line = "123 456";
    int[] s3;
    formattedRead(line, "%(%s %)", &s3);
    assert(s3 == [123, 456]);

    line = "h,e,l,l,o; w,o,r,l,d";
    string[] s4;
    formattedRead(line, "%(%(%c,%); %)", &s4);
    assert(s4 == ["hello", "world"]);
}

@system pure unittest
{
    string line;

    int[4] sa1;
    line = `[1,2,3,4]`;
    formattedRead(line, "%s", &sa1);
    assert(sa1 == [1,2,3,4]);

    int[4] sa2;
    line = `[1,2,3]`;
    assertThrown(formattedRead(line, "%s", &sa2));

    int[4] sa3;
    line = `[1,2,3,4,5]`;
    assertThrown(formattedRead(line, "%s", &sa3));
}

@system pure unittest
{
    string input;

    int[4] sa1;
    input = `[1,2,3,4]`;
    formattedRead(input, "[%(%s,%)]", &sa1);
    assert(sa1 == [1,2,3,4]);

    int[4] sa2;
    input = `[1,2,3]`;
    assertThrown!FormatException(formattedRead(input, "[%(%s,%)]", &sa2));
}

@system pure unittest
{
    string line;

    string s1, s2;

    line = "hello, world";
    formattedRead(line, "%s", &s1);
    assert(s1 == "hello, world", s1);

    line = "hello, world;yah";
    formattedRead(line, "%s;%s", &s1, &s2);
    assert(s1 == "hello, world", s1);
    assert(s2 == "yah", s2);

    line = `['h','e','l','l','o']`;
    string s3;
    formattedRead(line, "[%(%s,%)]", &s3);
    assert(s3 == "hello");

    line = `"hello"`;
    string s4;
    formattedRead(line, "\"%(%c%)\"", &s4);
    assert(s4 == "hello");
}

@system pure unittest
{
    string line;

    string[int] aa1;
    line = `[1:"hello", 2:"world"]`;
    formattedRead(line, "%s", &aa1);
    assert(aa1 == [1:"hello", 2:"world"]);

    int[string] aa2;
    line = `{"hello"=1; "world"=2}`;
    formattedRead(line, "{%(%s=%s; %)}", &aa2);
    assert(aa2 == ["hello":1, "world":2]);

    int[string] aa3;
    line = `{[hello=1]; [world=2]}`;
    formattedRead(line, "{%([%(%c%)=%s]%|; %)}", &aa3);
    assert(aa3 == ["hello":1, "world":2]);
}

// test rvalue using
@system pure unittest
{
    string[int] aa1;
    formattedRead!("%s")(`[1:"hello", 2:"world"]`, aa1);
    assert(aa1 == [1:"hello", 2:"world"]);

    int[string] aa2;
    formattedRead(`{"hello"=1; "world"=2}`, "{%(%s=%s; %)}", aa2);
    assert(aa2 == ["hello":1, "world":2]);
}

template FormatSpec(Char)
if (!is(Unqual!Char == Char))
{
    alias FormatSpec = FormatSpec!(Unqual!Char);
}

/**
 * A General handler for `printf` style format specifiers. Used for building more
 * specific formatting functions.
 */
struct FormatSpec(Char)
if (is(Unqual!Char == Char))
{
    import std.algorithm.searching : startsWith;
    import std.ascii : isDigit, isPunctuation, isAlpha;
    import std.conv : parse, text, to;

    /**
       Minimum _width, default `0`.
     */
    int width = 0;

    /**
       Precision. Its semantics depends on the argument type. For
       floating point numbers, _precision dictates the number of
       decimals printed.
     */
    int precision = UNSPECIFIED;

    /**
       Number of digits printed between _separators.
    */
    int separators = UNSPECIFIED;

    /**
       Set to `DYNAMIC` when the separator character is supplied at runtime.
    */
    int separatorCharPos = UNSPECIFIED;

    /**
       Character to insert between digits.
    */
    dchar separatorChar = ',';

    /**
       Special value for width and precision. `DYNAMIC` width or
       precision means that they were specified with `'*'` in the
       format string and are passed at runtime through the varargs.
     */
    enum int DYNAMIC = int.max;

    /**
       Special value for precision, meaning the format specifier
       contained no explicit precision.
     */
    enum int UNSPECIFIED = DYNAMIC - 1;

    /**
       The actual format specifier, `'s'` by default.
    */
    char spec = 's';

    /**
       Index of the argument for positional parameters, from `1` to
       `ubyte.max`. (`0` means not used).
    */
    ubyte indexStart;

    /**
       Index of the last argument for positional parameter range, from
       `1` to `ubyte.max`. (`0` means not used).
    */
    ubyte indexEnd;

    version (StdDdoc)
    {
        /**
         The format specifier contained a `'-'` (`printf`
         compatibility).
         */
        bool flDash;

        /**
         The format specifier contained a `'0'` (`printf`
         compatibility).
         */
        bool flZero;

        /**
         The format specifier contained a $(D ' ') (`printf`
         compatibility).
         */
        bool flSpace;

        /**
         The format specifier contained a `'+'` (`printf`
         compatibility).
         */
        bool flPlus;

        /**
         The format specifier contained a `'#'` (`printf`
         compatibility).
         */
        bool flHash;

        /**
         The format specifier contained a `','`
         */
        bool flSeparator;

        // Fake field to allow compilation
        ubyte allFlags;
    }
    else
    {
        union
        {
            import std.bitmanip : bitfields;
            mixin(bitfields!(
                        bool, "flDash", 1,
                        bool, "flZero", 1,
                        bool, "flSpace", 1,
                        bool, "flPlus", 1,
                        bool, "flHash", 1,
                        bool, "flSeparator", 1,
                        ubyte, "", 2));
            ubyte allFlags;
        }
    }

    /**
       In case of a compound format specifier starting with $(D
       "%$(LPAREN)") and ending with `"%$(RPAREN)"`, `_nested`
       contains the string contained within the two separators.
     */
    const(Char)[] nested;

    /**
       In case of a compound format specifier, `_sep` contains the
       string positioning after `"%|"`.
       `sep is null` means no separator else `sep.empty` means 0 length
        separator.
     */
    const(Char)[] sep;

    /**
       `_trailing` contains the rest of the format string.
     */
    const(Char)[] trailing;

    /*
       This string is inserted before each sequence (e.g. array)
       formatted (by default `"["`).
     */
    enum immutable(Char)[] seqBefore = "[";

    /*
       This string is inserted after each sequence formatted (by
       default `"]"`).
     */
    enum immutable(Char)[] seqAfter = "]";

    /*
       This string is inserted after each element keys of a sequence (by
       default `":"`).
     */
    enum immutable(Char)[] keySeparator = ":";

    /*
       This string is inserted in between elements of a sequence (by
       default $(D ", ")).
     */
    enum immutable(Char)[] seqSeparator = ", ";

    /**
       Construct a new `FormatSpec` using the format string `fmt`, no
       processing is done until needed.
     */
    this(in Char[] fmt) @safe pure
    {
        trailing = fmt;
    }

    /**
       Write the format string to an output range until the next format
       specifier is found and parse that format specifier.

       See $(LREF FormatSpec) for an example, how to use `writeUpToNextSpec`.

       Params:
           writer = the $(REF_ALTTEXT output range, isOutputRange, std, range, primitives)

       Returns:
           True, when a format specifier is found.

       Throws:
           A $(LREF FormatException) when the found format specifier
           could not be parsed.
     */
    bool writeUpToNextSpec(OutputRange)(ref OutputRange writer) scope
    {
        if (trailing.empty)
            return false;
        for (size_t i = 0; i < trailing.length; ++i)
        {
            if (trailing[i] != '%') continue;
            put(writer, trailing[0 .. i]);
            trailing = trailing[i .. $];
            enforceFmt(trailing.length >= 2, `Unterminated format specifier: "%"`);
            trailing = trailing[1 .. $];

            if (trailing[0] != '%')
            {
                // Spec found. Fill up the spec, and bailout
                fillUp();
                return true;
            }
            // Doubled! Reset and Keep going
            i = 0;
        }
        // no format spec found
        put(writer, trailing);
        trailing = null;
        return false;
    }

    private void fillUp() scope
    {
        // Reset content
        if (__ctfe)
        {
            flDash = false;
            flZero = false;
            flSpace = false;
            flPlus = false;
            flHash = false;
            flSeparator = false;
        }
        else
        {
            allFlags = 0;
        }

        width = 0;
        precision = UNSPECIFIED;
        nested = null;
        // Parse the spec (we assume we're past '%' already)
        for (size_t i = 0; i < trailing.length; )
        {
            switch (trailing[i])
            {
            case '(':
                // Embedded format specifier.
                auto j = i + 1;
                // Get the matching balanced paren
                for (uint innerParens;;)
                {
                    enforceFmt(j + 1 < trailing.length,
                        text("Incorrect format specifier: %", trailing[i .. $]));
                    if (trailing[j++] != '%')
                    {
                        // skip, we're waiting for %( and %)
                        continue;
                    }
                    if (trailing[j] == '-') // for %-(
                    {
                        ++j;    // skip
                        enforceFmt(j < trailing.length,
                            text("Incorrect format specifier: %", trailing[i .. $]));
                    }
                    if (trailing[j] == ')')
                    {
                        if (innerParens-- == 0) break;
                    }
                    else if (trailing[j] == '|')
                    {
                        if (innerParens == 0) break;
                    }
                    else if (trailing[j] == '(')
                    {
                        ++innerParens;
                    }
                }
                if (trailing[j] == '|')
                {
                    auto k = j;
                    for (++j;;)
                    {
                        if (trailing[j++] != '%')
                            continue;
                        if (trailing[j] == '%')
                            ++j;
                        else if (trailing[j] == ')')
                            break;
                        else
                            throw new FormatException(
                                text("Incorrect format specifier: %",
                                        trailing[j .. $]));
                    }
                    nested = trailing[i + 1 .. k - 1];
                    sep = trailing[k + 1 .. j - 1];
                }
                else
                {
                    nested = trailing[i + 1 .. j - 1];
                    sep = null; // no separator
                }
                //this = FormatSpec(innerTrailingSpec);
                spec = '(';
                // We practically found the format specifier
                trailing = trailing[j + 1 .. $];
                return;
            case '-': flDash = true; ++i; break;
            case '+': flPlus = true; ++i; break;
            case '#': flHash = true; ++i; break;
            case '0': flZero = true; ++i; break;
            case ' ': flSpace = true; ++i; break;
            case '*':
                if (isDigit(trailing[++i]))
                {
                    // a '*' followed by digits and '$' is a
                    // positional format
                    trailing = trailing[1 .. $];
                    width = -parse!(typeof(width))(trailing);
                    i = 0;
                    enforceFmt(trailing[i++] == '$',
                        "$ expected");
                }
                else
                {
                    // read result
                    width = DYNAMIC;
                }
                break;
            case '1': .. case '9':
                auto tmp = trailing[i .. $];
                const widthOrArgIndex = parse!uint(tmp);
                enforceFmt(tmp.length,
                    text("Incorrect format specifier %", trailing[i .. $]));
                i = arrayPtrDiff(tmp, trailing);
                if (tmp.startsWith('$'))
                {
                    // index of the form %n$
                    indexEnd = indexStart = to!ubyte(widthOrArgIndex);
                    ++i;
                }
                else if (tmp.startsWith(':'))
                {
                    // two indexes of the form %m:n$, or one index of the form %m:$
                    indexStart = to!ubyte(widthOrArgIndex);
                    tmp = tmp[1 .. $];
                    if (tmp.startsWith('$'))
                    {
                        indexEnd = indexEnd.max;
                    }
                    else
                    {
                        indexEnd = parse!(typeof(indexEnd))(tmp);
                    }
                    i = arrayPtrDiff(tmp, trailing);
                    enforceFmt(trailing[i++] == '$',
                        "$ expected");
                }
                else
                {
                    // width
                    width = to!int(widthOrArgIndex);
                }
                break;
            case ',':
                // Precision
                ++i;
                flSeparator = true;

                if (trailing[i] == '*')
                {
                    ++i;
                    // read result
                    separators = DYNAMIC;
                }
                else if (isDigit(trailing[i]))
                {
                    auto tmp = trailing[i .. $];
                    separators = parse!int(tmp);
                    i = arrayPtrDiff(tmp, trailing);
                }
                else
                {
                    // "," was specified, but nothing after it
                    separators = 3;
                }

                if (trailing[i] == '?')
                {
                    separatorCharPos = DYNAMIC;
                    ++i;
                }

                break;
            case '.':
                // Precision
                if (trailing[++i] == '*')
                {
                    if (isDigit(trailing[++i]))
                    {
                        // a '.*' followed by digits and '$' is a
                        // positional precision
                        trailing = trailing[i .. $];
                        i = 0;
                        precision = -parse!int(trailing);
                        enforceFmt(trailing[i++] == '$',
                            "$ expected");
                    }
                    else
                    {
                        // read result
                        precision = DYNAMIC;
                    }
                }
                else if (trailing[i] == '-')
                {
                    // negative precision, as good as 0
                    precision = 0;
                    auto tmp = trailing[i .. $];
                    parse!int(tmp); // skip digits
                    i = arrayPtrDiff(tmp, trailing);
                }
                else if (isDigit(trailing[i]))
                {
                    auto tmp = trailing[i .. $];
                    precision = parse!int(tmp);
                    i = arrayPtrDiff(tmp, trailing);
                }
                else
                {
                    // "." was specified, but nothing after it
                    precision = 0;
                }
                break;
            default:
                // this is the format char
                spec = cast(char) trailing[i++];
                trailing = trailing[i .. $];
                return;
            } // end switch
        } // end for
        throw new FormatException(text("Incorrect format specifier: ", trailing));
    }

    //--------------------------------------------------------------------------
    private bool readUpToNextSpec(R)(ref R r) scope
    {
        import std.ascii : isLower, isWhite;
        import std.utf : stride;

        // Reset content
        if (__ctfe)
        {
            flDash = false;
            flZero = false;
            flSpace = false;
            flPlus = false;
            flHash = false;
            flSeparator = false;
        }
        else
        {
            allFlags = 0;
        }
        width = 0;
        precision = UNSPECIFIED;
        nested = null;
        // Parse the spec
        while (trailing.length)
        {
            const c = trailing[0];
            if (c == '%' && trailing.length > 1)
            {
                const c2 = trailing[1];
                if (c2 == '%')
                {
                    assert(!r.empty, "Required at least one more input");
                    // Require a '%'
                    if (r.front != '%') break;
                    trailing = trailing[2 .. $];
                    r.popFront();
                }
                else
                {
                    enforceFmt(isLower(c2) || c2 == '*' ||
                            c2 == '(',
                            text("'%", c2,
                                    "' not supported with formatted read"));
                    trailing = trailing[1 .. $];
                    fillUp();
                    return true;
                }
            }
            else
            {
                if (c == ' ')
                {
                    while (!r.empty && isWhite(r.front)) r.popFront();
                    //r = std.algorithm.find!(not!(isWhite))(r);
                }
                else
                {
                    enforceFmt(!r.empty,
                            text("parseToFormatSpec: Cannot find character '",
                                    c, "' in the input string."));
                    if (r.front != trailing.front) break;
                    r.popFront();
                }
                trailing = trailing[stride(trailing, 0) .. $];
            }
        }
        return false;
    }

    private string getCurFmtStr() const
    {
        import std.array : appender;
        auto w = appender!string();
        auto f = FormatSpec!Char("%s"); // for stringnize

        put(w, '%');
        if (indexStart != 0)
        {
            formatValue(w, indexStart, f);
            put(w, '$');
        }
        if (flDash)  put(w, '-');
        if (flZero)  put(w, '0');
        if (flSpace) put(w, ' ');
        if (flPlus)  put(w, '+');
        if (flHash)  put(w, '#');
        if (flSeparator)  put(w, ',');
        if (width != 0)
            formatValue(w, width, f);
        if (precision != FormatSpec!Char.UNSPECIFIED)
        {
            put(w, '.');
            formatValue(w, precision, f);
        }
        put(w, spec);
        return w.data;
    }

    private const(Char)[] headUpToNextSpec()
    {
        import std.array : appender;
        auto w = appender!(typeof(return))();
        auto tr = trailing;

        while (tr.length)
        {
            if (tr[0] == '%')
            {
                if (tr.length > 1 && tr[1] == '%')
                {
                    tr = tr[2 .. $];
                    w.put('%');
                }
                else
                    break;
            }
            else
            {
                w.put(tr.front);
                tr.popFront();
            }
        }
        return w.data;
    }

    /**
     * Gives a string containing all of the member variables on their own
     * line.
     *
     * Params:
     *     writer = A `char` accepting
     *     $(REF_ALTTEXT output range, isOutputRange, std, range, primitives)
     * Returns:
     *     A `string` when not using an output range; `void` otherwise.
     */
    string toString() const @safe pure
    {
        import std.array : appender;
        auto app = appender!string();
        app.reserve(200 + trailing.length);
        toString(app);
        return app.data;
    }

    /// ditto
    void toString(OutputRange)(ref OutputRange writer) const
    if (isOutputRange!(OutputRange, char))
    {
        auto s = singleSpec("%s");

        put(writer, "address = ");
        formatValue(writer, &this, s);
        put(writer, "\nwidth = ");
        formatValue(writer, width, s);
        put(writer, "\nprecision = ");
        formatValue(writer, precision, s);
        put(writer, "\nspec = ");
        formatValue(writer, spec, s);
        put(writer, "\nindexStart = ");
        formatValue(writer, indexStart, s);
        put(writer, "\nindexEnd = ");
        formatValue(writer, indexEnd, s);
        put(writer, "\nflDash = ");
        formatValue(writer, flDash, s);
        put(writer, "\nflZero = ");
        formatValue(writer, flZero, s);
        put(writer, "\nflSpace = ");
        formatValue(writer, flSpace, s);
        put(writer, "\nflPlus = ");
        formatValue(writer, flPlus, s);
        put(writer, "\nflHash = ");
        formatValue(writer, flHash, s);
        put(writer, "\nflSeparator = ");
        formatValue(writer, flSeparator, s);
        put(writer, "\nnested = ");
        formatValue(writer, nested, s);
        put(writer, "\ntrailing = ");
        formatValue(writer, trailing, s);
        put(writer, '\n');
    }
}

@safe unittest
{
    import std.array;
    import std.conv : text;
    auto w = appender!(char[])();
    auto f = FormatSpec!char("abc%sdef%sghi");
    f.writeUpToNextSpec(w);
    assert(w.data == "abc", w.data);
    assert(f.trailing == "def%sghi", text(f.trailing));
    f.writeUpToNextSpec(w);
    assert(w.data == "abcdef", w.data);
    assert(f.trailing == "ghi");
    // test with embedded %%s
    f = FormatSpec!char("ab%%cd%%ef%sg%%h%sij");
    w.clear();
    f.writeUpToNextSpec(w);
    assert(w.data == "ab%cd%ef" && f.trailing == "g%%h%sij", w.data);
    f.writeUpToNextSpec(w);
    assert(w.data == "ab%cd%efg%h" && f.trailing == "ij");
    // https://issues.dlang.org/show_bug.cgi?id=4775
    f = FormatSpec!char("%%%s");
    w.clear();
    f.writeUpToNextSpec(w);
    assert(w.data == "%" && f.trailing == "");
    f = FormatSpec!char("%%%%%s%%");
    w.clear();
    while (f.writeUpToNextSpec(w)) continue;
    assert(w.data == "%%%");

    f = FormatSpec!char("a%%b%%c%");
    w.clear();
    assertThrown!FormatException(f.writeUpToNextSpec(w));
    assert(w.data == "a%b%c" && f.trailing == "%");
}

// https://issues.dlang.org/show_bug.cgi?id=5237
@safe unittest
{
    import std.array;
    auto w = appender!string();
    auto f = FormatSpec!char("%.16f");
    f.writeUpToNextSpec(w); // dummy eating
    assert(f.spec == 'f');
    auto fmt = f.getCurFmtStr();
    assert(fmt == "%.16f");
}

///
@safe pure unittest
{
    import std.array;
    auto a = appender!(string)();
    auto fmt = "Number: %6.4e\nString: %s";
    auto f = FormatSpec!char(fmt);

    assert(f.writeUpToNextSpec(a) == true);

    assert(a.data == "Number: ");
    assert(f.trailing == "\nString: %s");
    assert(f.spec == 'e');
    assert(f.width == 6);
    assert(f.precision == 4);

    assert(f.writeUpToNextSpec(a) == true);

    assert(a.data == "Number: \nString: ");
    assert(f.trailing == "");
    assert(f.spec == 's');

    assert(f.writeUpToNextSpec(a) == false);
    assert(a.data == "Number: \nString: ");
}

// https://issues.dlang.org/show_bug.cgi?id=14059
@safe unittest
{
    import std.array : appender;
    auto a = appender!(string)();

    auto f = FormatSpec!char("%-(%s%"); // %)")
    assertThrown!FormatException(f.writeUpToNextSpec(a));

    f = FormatSpec!char("%(%-"); // %)")
    assertThrown!FormatException(f.writeUpToNextSpec(a));
}

@safe unittest
{
    import std.array : appender;
    auto a = appender!(string)();

    auto f = FormatSpec!char("%,d");
    f.writeUpToNextSpec(a);

    assert(f.spec == 'd', format("%s", f.spec));
    assert(f.precision == FormatSpec!char.UNSPECIFIED);
    assert(f.separators == 3);

    f = FormatSpec!char("%5,10f");
    f.writeUpToNextSpec(a);
    assert(f.spec == 'f', format("%s", f.spec));
    assert(f.separators == 10);
    assert(f.width == 5);

    f = FormatSpec!char("%5,10.4f");
    f.writeUpToNextSpec(a);
    assert(f.spec == 'f', format("%s", f.spec));
    assert(f.separators == 10);
    assert(f.width == 5);
    assert(f.precision == 4);
}

@safe pure unittest
{
    import std.algorithm.searching : canFind, findSplitBefore;
    auto expected = "width = 2" ~
        "\nprecision = 5" ~
        "\nspec = f" ~
        "\nindexStart = 0" ~
        "\nindexEnd = 0" ~
        "\nflDash = false" ~
        "\nflZero = false" ~
        "\nflSpace = false" ~
        "\nflPlus = false" ~
        "\nflHash = false" ~
        "\nflSeparator = false" ~
        "\nnested = " ~
        "\ntrailing = \n";
    auto spec = singleSpec("%2.5f");
    auto res = spec.toString();
    // make sure the address exists, then skip it
    assert(res.canFind("address"));
    assert(res.findSplitBefore("width")[1] == expected);
}

/**
Helper function that returns a `FormatSpec` for a single specifier given
in `fmt`.

Params:
    fmt = A format specifier.

Returns:
    A `FormatSpec` with the specifier parsed.
Throws:
    A `FormatException` when more than one specifier is given or the specifier
    is malformed.
  */
FormatSpec!Char singleSpec(Char)(Char[] fmt)
{
    import std.conv : text;
    enforceFmt(fmt.length >= 2, "fmt must be at least 2 characters long");
    enforceFmt(fmt.front == '%', "fmt must start with a '%' character");

    static struct DummyOutputRange {
        void put(C)(scope const C[] buf) {} // eat elements
    }
    auto a = DummyOutputRange();
    auto spec = FormatSpec!Char(fmt);
    //dummy write
    spec.writeUpToNextSpec(a);

    enforceFmt(spec.trailing.empty,
            text("Trailing characters in fmt string: '", spec.trailing));

    return spec;
}

///
@safe pure unittest
{
    import std.exception : assertThrown;
    auto spec = singleSpec("%2.3e");

    assert(spec.trailing == "");
    assert(spec.spec == 'e');
    assert(spec.width == 2);
    assert(spec.precision == 3);

    assertThrown!FormatException(singleSpec(""));
    assertThrown!FormatException(singleSpec("2.3e"));
    assertThrown!FormatException(singleSpec("%2.3eTest"));
}

/**
 * Formats any value into `Char` accepting `OutputRange`, using the given `FormatSpec`.
 *
 * Aggregates:
 * `struct`, `union`, `class`, and `interface` are formatted by calling `toString`.
 *
 * `toString` should have one of the following signatures:
 *
 * ---
 * void toString(W)(ref W w, scope const ref FormatSpec fmt)
 * void toString(W)(ref W w)
 * string toString();
 * ---
 *
 * Where `W` is an $(REF_ALTTEXT output range, isOutputRange, std,range,primitives)
 * which accepts characters. The template type does not have to be called `W`.
 *
 * The following overloads are also accepted for legacy reasons or for use in virtual
 * functions. It's recommended that any new code forgo these overloads if possible for
 * speed and attribute acceptance reasons.
 *
 * ---
 * void toString(scope void delegate(const(char)[]) sink, const ref FormatSpec fmt);
 * void toString(scope void delegate(const(char)[]) sink, string fmt);
 * void toString(scope void delegate(const(char)[]) sink);
 * ---
 *
 * For the class objects which have input range interface,
 * $(UL
 *     $(LI If the instance `toString` has overridden `Object.toString`, it is used.)
 *     $(LI Otherwise, the objects are formatted as input range.)
 * )
 *
 * For the `struct` and `union` objects which does not have `toString`,
 * $(UL
 *     $(LI If they have range interface, formatted as input range.)
 *     $(LI Otherwise, they are formatted like `Type(field1, filed2, ...)`.)
 * )
 *
 * Otherwise, are formatted just as their type name.
 *
 * Params:
 *     w = The $(REF_ALTTEXT output range, isOutputRange, std,range,primitives) to write to.
 *     val = The value to write.
 *     f = The $(REF FormatSpec, std, format) defining how to write the value.
 */
void formatValue(Writer, T, Char)(auto ref Writer w, auto ref T val, scope const ref FormatSpec!Char f)
{
    formatValueImpl(w, val, f);
}

/++
   The following code compares the use of `formatValue` and `formattedWrite`.
 +/
@safe pure unittest
{
   import std.array : appender;

   auto writer1 = appender!string();
   writer1.formattedWrite("%08b", 42);

   auto writer2 = appender!string();
   auto f = singleSpec("%08b");
   writer2.formatValue(42, f);

   assert(writer1.data == writer2.data && writer1.data == "00101010");
}

/**
 * `bool`s are formatted as `"true"` or `"false"` with `%s` and as `1` or
 * `0` with integral-specific format specs.
 */
@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%s");
    formatValue(w, true, spec);

    assert(w.data == "true");
}

/// `null` literal is formatted as `"null"`.
@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%s");
    formatValue(w, null, spec);

    assert(w.data == "null");
}

/// Integrals are formatted like $(REF printf, core, stdc, stdio).
@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%d");
    formatValue(w, 1337, spec);

    assert(w.data == "1337");
}

/// Floating-point values are formatted like $(REF printf, core, stdc, stdio)
@safe unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%.1f");
    formatValue(w, 1337.7, spec);

    assert(w.data == "1337.7");
}

/**
 * Individual characters (`char, `wchar`, or `dchar`) are formatted as
 * Unicode characters with `%s` and as integers with integral-specific format
 * specs.
 */
@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%c");
    formatValue(w, 'a', spec);

    assert(w.data == "a");
}

/// Strings are formatted like $(REF printf, core, stdc, stdio)
@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%s");
    formatValue(w, "hello", spec);

    assert(w.data == "hello");
}

/// Static-size arrays are formatted as dynamic arrays.
@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%s");
    char[2] two = ['a', 'b'];
    formatValue(w, two, spec);

    assert(w.data == "ab");
}

/**
 * Dynamic arrays are formatted as input ranges.
 *
 * Specializations:
 *   $(UL
 *      $(LI `void[]` is formatted like `ubyte[]`.)
 *      $(LI Const array is converted to input range by removing its qualifier.)
 *   )
 */
@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%s");
    auto two = [1, 2];
    formatValue(w, two, spec);

    assert(w.data == "[1, 2]");
}

/**
 * Associative arrays are formatted by using `':'` and `", "` as
 * separators, and enclosed by `'['` and `']'`.
 */
@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%s");
    auto aa = ["H":"W"];
    formatValue(w, aa, spec);

    assert(w.data == "[\"H\":\"W\"]", w.data);
}

/// `enum`s are formatted like their base value
@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%s");

    enum A { first, second, third }

    formatValue(w, A.second, spec);

    assert(w.data == "second");
}

/**
 * Formatting a struct by defining a method `toString`, which takes an output
 * range.
 *
 * It's recommended that any `toString` using $(REF_ALTTEXT output ranges, isOutputRange, std,range,primitives)
 * use $(REF put, std,range,primitives) rather than use the `put` method of the range
 * directly.
 */
@safe unittest
{
    import std.array : appender;
    import std.range.primitives;

    static struct Point
    {
        int x, y;

        void toString(W)(ref W writer, scope const ref FormatSpec!char f)
        if (isOutputRange!(W, char))
        {
            // std.range.primitives.put
            put(writer, "(");
            formatValue(writer, x, f);
            put(writer, ",");
            formatValue(writer, y, f);
            put(writer, ")");
        }
    }

    auto w = appender!string();
    auto spec = singleSpec("%s");
    auto p = Point(16, 11);

    formatValue(w, p, spec);
    assert(w.data == "(16,11)");
}

/**
 * Another example of formatting a `struct` with a defined `toString`,
 * this time using the `scope delegate` method.
 *
 * $(RED This method is now discouraged for non-virtual functions).
 * If possible, please use the output range method instead.
 */
@safe unittest
{
   static struct Point
   {
       int x, y;

       void toString(scope void delegate(scope const(char)[]) @safe sink,
                     scope const FormatSpec!char fmt) const
       {
           sink("(");
           sink.formatValue(x, fmt);
           sink(",");
           sink.formatValue(y, fmt);
           sink(")");
       }
   }

   auto p = Point(16,11);
   assert(format("%03d", p) == "(016,011)");
   assert(format("%02x", p) == "(10,0b)");
}

/// Pointers are formatted as hex integers.
@system pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%s");

    auto q = cast(void*) 0xFFEECCAA;
    formatValue(w, q, spec);

    assert(w.data == "FFEECCAA");
}

/// SIMD vectors are formatted as arrays.
@safe unittest
{
    import core.simd;
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%s");

    static if (is(float4))
    {
        version (X86) {}
        else
        {
            float4 f4;
            f4.array[0] = 1;
            f4.array[1] = 2;
            f4.array[2] = 3;
            f4.array[3] = 4;

            formatValue(w, f4, spec);
            assert(w.data == "[1, 2, 3, 4]");
        }
    }
}

/// Delegates are formatted by `ReturnType delegate(Parameters) FunctionAttributes`
@safe unittest
{
    import std.conv : to;

    int i;

    int foo(short k) @nogc
    {
        return i + k;
    }

    @system int delegate(short) @nogc bar() nothrow pure
    {
        int* p = new int(1);
        i = *p;
        return &foo;
    }

    assert(to!string(&bar) == "int delegate(short) @nogc delegate() pure nothrow @system");
    assert(() @trusted { return bar()(3); }() == 4);
}

/*
    `bool`s are formatted as `"true"` or `"false"` with `%s` and as `1` or
    `0` with integral-specific format specs.
 */
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, T obj, scope const ref FormatSpec!Char f)
if (is(BooleanTypeOf!T) && !is(T == enum) && !hasToString!(T, Char))
{
    BooleanTypeOf!T val = obj;

    if (f.spec == 's')
        writeAligned(w, val ? "true" : "false", f);
    else
        formatValueImpl(w, cast(int) val, f);
}

@safe pure unittest
{
    assertCTFEable!(
    {
        formatTest( false, "false" );
        formatTest( true,  "true"  );
    });
}
@system unittest
{
    class C1 { bool val; alias val this; this(bool v){ val = v; } }
    class C2 { bool val; alias val this; this(bool v){ val = v; }
               override string toString() const { return "C"; } }
    formatTest( new C1(false), "false" );
    formatTest( new C1(true),  "true" );
    formatTest( new C2(false), "C" );
    formatTest( new C2(true),  "C" );

    struct S1 { bool val; alias val this; }
    struct S2 { bool val; alias val this;
                string toString() const { return "S"; } }
    formatTest( S1(false), "false" );
    formatTest( S1(true),  "true"  );
    formatTest( S2(false), "S" );
    formatTest( S2(true),  "S" );
}

@safe pure unittest
{
    string t1 = format("[%6s] [%6s] [%-6s]", true, false, true);
    assert(t1 == "[  true] [ false] [true  ]");

    string t2 = format("[%3s] [%-2s]", true, false);
    assert(t2 == "[true] [false]");
}

/*
    `null` literal is formatted as `"null"`
*/
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, T obj, scope const ref FormatSpec!Char f)
if (is(Unqual!T == typeof(null)) && !is(T == enum) && !hasToString!(T, Char))
{
    const spec = f.spec;
    enforceFmt(spec == 's',
        "null literal cannot match %" ~ spec);

    writeAligned(w, "null", f);
}

@safe pure unittest
{
    assert(collectExceptionMsg!FormatException(format("%p", null)).back == 'p');

    assertCTFEable!(
    {
        formatTest( null, "null" );
    });
}

@safe pure unittest
{
    string t = format("[%6s] [%-6s]", null, null);
    assert(t == "[  null] [null  ]");
}

/*
    Integrals are formatted like $(REF printf, core, stdc, stdio).
*/
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, T obj, scope const ref FormatSpec!Char f)
if (is(IntegralTypeOf!T) && !is(T == enum) && !hasToString!(T, Char))
{
    alias U = IntegralTypeOf!T;
    U val = obj;    // Extracting alias this may be impure/system/may-throw

    const spec = f.spec;
    if (spec == 'r')
    {
        // raw write, skip all else and write the thing
        auto raw = (ref val)@trusted{
            return (cast(const char*) &val)[0 .. val.sizeof];
        }(val);
        if (needToSwapEndianess(f))
        {
            foreach_reverse (c; raw)
                put(w, c);
        }
        else
        {
            foreach (c; raw)
                put(w, c);
        }
        return;
    }

    immutable uint base =
        spec == 'x' || spec == 'X' ? 16 :
        spec == 'o' ? 8 :
        spec == 'b' ? 2 :
        spec == 's' || spec == 'd' || spec == 'u' ? 10 :
        0;
    enforceFmt(base > 0,
        "incompatible format character for integral argument: %" ~ spec);

    // Forward on to formatIntegral to handle both U and const(U)
    // Saves duplication of code for both versions.
    static if (is(ucent) && (is(U == cent) || is(U == ucent)))
        alias C = U;
    else static if (isSigned!U)
        alias C = long;
    else
        alias C = ulong;
    formatIntegral(w, cast(C) val, f, base, Unsigned!U.max);
}

private void formatIntegral(Writer, T, Char)(ref Writer w, const(T) val, scope const ref FormatSpec!Char fs,
    uint base, ulong mask)
{
    T arg = val;

    immutable negative = (base == 10 && arg < 0);
    if (negative)
    {
        arg = -arg;
    }

    // All unsigned integral types should fit in ulong.
    static if (is(ucent) && is(typeof(arg) == ucent))
        formatUnsigned(w, (cast(ucent) arg) & mask, fs, base, negative);
    else
        formatUnsigned(w, (cast(ulong) arg) & mask, fs, base, negative);
}

private void formatUnsigned(Writer, T, Char)
(ref Writer w, T arg, scope const ref FormatSpec!Char fs, uint base, bool negative)
{
    /* Write string:
     *    leftpad prefix1 prefix2 zerofill digits rightpad
     */

    /* Convert arg to digits[].
     * Note that 0 becomes an empty digits[]
     */
    char[64] buffer = void; // 64 bits in base 2 at most
    char[] digits;
    if (arg < base && base <= 10 && arg)
    {
        // Most numbers are a single digit - avoid expensive divide
        buffer[0] = cast(char)(arg + '0');
        digits = buffer[0 .. 1];
    }
    else
    {
        size_t i = buffer.length;
        while (arg)
        {
            --i;
            char c = cast(char) (arg % base);
            arg /= base;
            if (c < 10)
                buffer[i] = cast(char)(c + '0');
            else
                buffer[i] = cast(char)(c + (fs.spec == 'x' ? 'a' - 10 : 'A' - 10));
        }
        digits = buffer[i .. $]; // got the digits without the sign
    }


    immutable precision = (fs.precision == fs.UNSPECIFIED) ? 1 : fs.precision;

    char padChar = 0;
    if (!fs.flDash)
    {
        padChar = (fs.flZero && fs.precision == fs.UNSPECIFIED) ? '0' : ' ';
    }

    // Compute prefix1 and prefix2
    char prefix1 = 0;
    char prefix2 = 0;
    if (base == 10)
    {
        if (negative)
            prefix1 = '-';
        else if (fs.flPlus)
            prefix1 = '+';
        else if (fs.flSpace)
            prefix1 = ' ';
    }
    else if (base == 16 && fs.flHash && digits.length)
    {
        prefix1 = '0';
        prefix2 = fs.spec == 'x' ? 'x' : 'X';
    }
    // adjust precision to print a '0' for octal if alternate format is on
    else if (base == 8 && fs.flHash &&
             (precision <= 1 || precision <= digits.length) && // too low precision
             digits.length > 0)
        prefix1 = '0';

    size_t zerofill = precision > digits.length ? precision - digits.length : 0;
    size_t leftpad = 0;
    size_t rightpad = 0;

    immutable prefixWidth = (prefix1 != 0) + (prefix2 != 0);
    size_t finalWidth, separatorsCount;
    if (fs.flSeparator != 0)
    {
        finalWidth = prefixWidth + digits.length + ((digits.length > 0) ? (digits.length - 1) / fs.separators : 0);
        if (finalWidth < fs.width)
            finalWidth = fs.width + (padChar == '0') * (((fs.width - prefixWidth) % (fs.separators + 1) == 0) ? 1 : 0);

        separatorsCount = (padChar == '0') ? (finalWidth - prefixWidth - 1) / (fs.separators + 1) :
                         ((digits.length > 0) ? (digits.length - 1) / fs.separators : 0);
    }
    else
    {
        import std.algorithm.comparison : max;
        finalWidth = max(fs.width, prefixWidth + digits.length);
    }

    immutable ptrdiff_t spacesToPrint =
        finalWidth - (
            + prefixWidth
            + zerofill
            + digits.length
            + separatorsCount
        );
    if (spacesToPrint > 0) // need to do some padding
    {
        if (padChar == '0')
            zerofill += spacesToPrint;
        else if (padChar)
            leftpad = spacesToPrint;
        else
            rightpad = spacesToPrint;
    }

    // Print
    foreach (i ; 0 .. leftpad)
        put(w, ' ');

    if (prefix1) put(w, prefix1);
    if (prefix2) put(w, prefix2);

    if (fs.flSeparator)
    {
        if (zerofill > 0)
        {
            put(w, '0');
            --zerofill;
        }

        int j = cast(int) (finalWidth - prefixWidth - separatorsCount - 1);
        for (size_t i = 0; i < zerofill; ++i, --j)
        {
            if (j % fs.separators == 0)
            {
                put(w, fs.separatorChar);
            }
            put(w, '0');
        }
    }
    else
    {
        foreach (i ; 0 .. zerofill)
            put(w, '0');
    }

    if (fs.flSeparator)
    {
        for (size_t j = 0; j < digits.length; ++j)
        {
            if (((j != 0) || ((spacesToPrint > 0) && (padChar == '0'))) && (digits.length - j) % fs.separators == 0)
            {
                put(w, fs.separatorChar);
            }
            put(w, digits[j]);
        }
    }
    else
    {
        put(w, digits);
    }

    foreach (i ; 0 .. rightpad)
        put(w, ' ');
}

// https://issues.dlang.org/show_bug.cgi?id=18838
@safe pure unittest
{
    assert("%12,d".format(0) == "           0");
}

@safe pure unittest
{
    assert(collectExceptionMsg!FormatException(format("%c", 5)).back == 'c');

    assertCTFEable!(
    {
        formatTest(9, "9");
        formatTest( 10, "10" );
    });
}

@system unittest
{
    class C1 { long val; alias val this; this(long v){ val = v; } }
    class C2 { long val; alias val this; this(long v){ val = v; }
               override string toString() const { return "C"; } }
    formatTest( new C1(10), "10" );
    formatTest( new C2(10), "C" );

    struct S1 { long val; alias val this; }
    struct S2 { long val; alias val this;
                string toString() const { return "S"; } }
    formatTest( S1(10), "10" );
    formatTest( S2(10), "S" );
}

// https://issues.dlang.org/show_bug.cgi?id=9117
@safe unittest
{
    static struct Frop {}

    static struct Foo
    {
        int n = 0;
        alias n this;
        T opCast(T) () if (is(T == Frop))
        {
            return Frop();
        }
        string toString()
        {
            return "Foo";
        }
    }

    static struct Bar
    {
        Foo foo;
        alias foo this;
        string toString()
        {
            return "Bar";
        }
    }

    const(char)[] result;
    void put(scope const char[] s){ result ~= s; }

    Foo foo;
    formattedWrite(&put, "%s", foo);    // OK
    assert(result == "Foo");

    result = null;

    Bar bar;
    formattedWrite(&put, "%s", bar);    // NG
    assert(result == "Bar");

    result = null;

    int i = 9;
    formattedWrite(&put, "%s", 9);
    assert(result == "9");
}

// https://issues.dlang.org/show_bug.cgi?id=20064
@safe unittest
{
    assert(format( "%03,d",  1234) ==              "1,234");
    assert(format( "%04,d",  1234) ==              "1,234");
    assert(format( "%05,d",  1234) ==              "1,234");
    assert(format( "%06,d",  1234) ==             "01,234");
    assert(format( "%07,d",  1234) ==            "001,234");
    assert(format( "%08,d",  1234) ==          "0,001,234");
    assert(format( "%09,d",  1234) ==          "0,001,234");
    assert(format("%010,d",  1234) ==         "00,001,234");
    assert(format("%011,d",  1234) ==        "000,001,234");
    assert(format("%012,d",  1234) ==      "0,000,001,234");
    assert(format("%013,d",  1234) ==      "0,000,001,234");
    assert(format("%014,d",  1234) ==     "00,000,001,234");
    assert(format("%015,d",  1234) ==    "000,000,001,234");
    assert(format("%016,d",  1234) ==  "0,000,000,001,234");
    assert(format("%017,d",  1234) ==  "0,000,000,001,234");

    assert(format( "%03,d", -1234) ==             "-1,234");
    assert(format( "%04,d", -1234) ==             "-1,234");
    assert(format( "%05,d", -1234) ==             "-1,234");
    assert(format( "%06,d", -1234) ==             "-1,234");
    assert(format( "%07,d", -1234) ==            "-01,234");
    assert(format( "%08,d", -1234) ==           "-001,234");
    assert(format( "%09,d", -1234) ==         "-0,001,234");
    assert(format("%010,d", -1234) ==         "-0,001,234");
    assert(format("%011,d", -1234) ==        "-00,001,234");
    assert(format("%012,d", -1234) ==       "-000,001,234");
    assert(format("%013,d", -1234) ==     "-0,000,001,234");
    assert(format("%014,d", -1234) ==     "-0,000,001,234");
    assert(format("%015,d", -1234) ==    "-00,000,001,234");
    assert(format("%016,d", -1234) ==   "-000,000,001,234");
    assert(format("%017,d", -1234) == "-0,000,000,001,234");
}

@safe pure unittest
{
    string t1 = format("[%6s] [%-6s]", 123, 123);
    assert(t1 == "[   123] [123   ]");

    string t2 = format("[%6s] [%-6s]", -123, -123);
    assert(t2 == "[  -123] [-123  ]");
}

private enum ctfpMessage = "Cannot format floating point types at compile-time";

private enum RoundingMode { up, down, toZero, toNearestTiesToEven, toNearestTiesAwayFromZero }

/*
    Floating-point values are formatted like $(REF printf, core, stdc, stdio)
 */
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, T obj, scope const ref FormatSpec!Char f)
if (is(FloatingPointTypeOf!T) && !is(T == enum) && !hasToString!(T, Char))
{
    import std.algorithm.comparison : min;
    import std.algorithm.searching : find;
    import std.string : indexOf, indexOfAny, indexOfNeither;
    import std.math : isInfinity, isNaN, signbit;
    import std.ascii : isUpper;

    string nanInfStr(scope const ref FormatSpec!Char f, const bool nan,
            const bool inf, const int sb, const bool up) @safe pure nothrow
    {
        return nan
            ? up
                ? sb ? "-NAN" : f.flPlus ? "+NAN" : (f.flSpace ? " NAN" : "NAN")
                : sb ? "-nan" : f.flPlus ? "+nan" : (f.flSpace ? " nan" : "nan")
            : inf
                ? up
                    ? sb ? "-INF" : f.flPlus ? "+INF" : (f.flSpace ? " INF" : "INF")
                    : sb ? "-inf" : f.flPlus ? "+inf" : (f.flSpace ? " inf" : "inf")
                : "";
    }

    FloatingPointTypeOf!T val = obj;
    const char spec = f.spec;

    if (spec == 'r')
    {
        // raw write, skip all else and write the thing
        auto raw = (ref val)@trusted{
            return (cast(const char*) &val)[0 .. val.sizeof];
        }(val);
        if (needToSwapEndianess(f))
        {
            foreach_reverse (c; raw)
                put(w, c);
        }
        else
        {
            foreach (c; raw)
                put(w, c);
        }
        return;
    }
    enforceFmt(find("fgFGaAeEs", spec).length,
        "incompatible format character for floating point argument: %" ~ spec);
    enforceFmt(!__ctfe, ctfpMessage);

    FormatSpec!Char fs = f; // fs is copy for change its values.
    const spec2 = spec == 's' ? 'g' : spec;

    version (CRuntime_Microsoft)
    {
        // convert early to get "inf" in case of overflow
        // windows handels inf and nan strange
        // https://devblogs.microsoft.com/oldnewthing/20130228-01/?p=5103
        immutable double tval = val;
    }
    else
    {
        alias tval = val;
    }

    const nan = isNaN(tval);
    const inf = isInfinity(tval);

    char[512] buf2 = void;
    size_t len;
    char[] buf;
    if (fs.spec=='a' || fs.spec=='A' || fs.spec=='e' || fs.spec=='E')
    {
        static if (is(T == float) || is(T == double) || (is(T == real) && T.mant_dig == double.mant_dig))
        {
            import std.math;

            auto mode = RoundingMode.toNearestTiesToEven;

            // std.math's FloatingPointControl isn't available on all target platforms
            static if (is(FloatingPointControl))
            {
                switch (FloatingPointControl.rounding)
                {
                case FloatingPointControl.roundUp:
                    mode = RoundingMode.up;
                    break;
                case FloatingPointControl.roundDown:
                    mode = RoundingMode.down;
                    break;
                case FloatingPointControl.roundToZero:
                    mode = RoundingMode.toZero;
                    break;
                case FloatingPointControl.roundToNearest:
                    mode = RoundingMode.toNearestTiesToEven;
                    break;
                default: assert(false);
                }
            }

            buf = printFloat(buf2[], val, fs, mode);
            len = buf.length;
        }
        else
            goto useSnprintf;
    }
    else
    {
useSnprintf:
        if (nan || inf)
        {
            const sb = signbit(tval);
            const up = isUpper(spec);
            string ns = nanInfStr(f, nan, inf, sb, up);
            FormatSpec!Char co;
            co.spec = 's';
            co.width = f.width;
            co.flDash = f.flDash;
            formatValue(w, ns, co);
            return;
        }

        char[1 /*%*/ + 5 /*flags*/ + 3 /*width.prec*/ + 2 /*format*/
             + 1 /*\0*/] sprintfSpec = void;
        sprintfSpec[0] = '%';
        uint i = 1;
        if (fs.flDash) sprintfSpec[i++] = '-';
        if (fs.flPlus) sprintfSpec[i++] = '+';
        if (fs.flZero) sprintfSpec[i++] = '0';
        if (fs.flSpace) sprintfSpec[i++] = ' ';
        if (fs.flHash) sprintfSpec[i++] = '#';
        sprintfSpec[i .. i + 3] = "*.*";
        i += 3;
        if (is(Unqual!(typeof(val)) == real)) sprintfSpec[i++] = 'L';
        sprintfSpec[i++] = spec2;
        sprintfSpec[i] = 0;
        //printf("format: '%s'; geeba: %g\n", sprintfSpec.ptr, val);

        //writefln("'%s'", sprintfSpec[0 .. i]);

        immutable n = ()@trusted{
            import core.stdc.stdio : snprintf;
            return snprintf(buf2.ptr, buf2.length,
                            sprintfSpec.ptr,
                            fs.width,
                            // negative precision is same as no precision specified
                            fs.precision == fs.UNSPECIFIED ? -1 : fs.precision,
                            tval);
        }();

        enforceFmt(n >= 0,
                   "floating point formatting failure");

        len = min(n, buf2.length-1);
        buf = buf2;
    }

    if (fs.flSeparator && !inf && !nan)
    {
        ptrdiff_t indexOfRemovable()
        {
            if (len < 2)
                return -1;

            size_t start = (buf[0 .. 1].indexOfAny(" 0123456789") == -1) ? 1 : 0;
            if (len < 2 + start)
                return -1;
            if ((buf[start] == ' ') || (buf[start] == '0' && buf[start + 1] != '.'))
                return start;

            return -1;
        }

        ptrdiff_t dot, firstDigit, ePos, dotIdx, firstLen;
        size_t separatorScoreCnt;

        while (true)
        {
            dot = buf[0 .. len].indexOf('.');
            firstDigit = buf[0 .. len].indexOfAny("0123456789");
            ePos = buf[0 .. len].indexOf('e');
            dotIdx = dot == -1 ? ePos == -1 ? len : ePos : dot;

            firstLen = dotIdx - firstDigit;
            separatorScoreCnt = (firstLen > 0) ? (firstLen - 1) / fs.separators : 0;

            ptrdiff_t removableIdx = (len + separatorScoreCnt > fs.width) ? indexOfRemovable() : -1;
            if ((removableIdx != -1) &&
                ((firstLen - (buf[removableIdx] == '0' ? 2 : 1)) / fs.separators + len - 1 >= fs.width))
            {
                buf[removableIdx .. $ - 1] = buf.dup[removableIdx + 1 .. $];
                len--;
            }
            else
                break;
        }

        immutable afterDotIdx = (ePos != -1) ? ePos : len;

        // plus, minus, prefix
        if (firstDigit > 0)
        {
            put(w, buf[0 .. firstDigit]);
        }

        // digits until dot with separator
        for (auto j = 0; j < firstLen; ++j)
        {
            if (j > 0 && (firstLen - j) % fs.separators == 0)
            {
                put(w, fs.separatorChar);
            }
            put(w, buf[j + firstDigit]);
        }

        // print dot for decimal numbers only or with '#' format specifier
        if (dot != -1 || fs.flHash)
        {
            put(w, '.');
        }

        // digits after dot
        for (auto j = dotIdx + 1; j < afterDotIdx; ++j)
        {
            put(w, buf[j]);
        }

        // rest
        if (ePos != -1)
        {
            put(w, buf[afterDotIdx .. len]);
        }
    }
    else
    {
        put(w, buf[0 .. len]);
    }
}

@safe unittest
{
    assert(format("%.1f", 1337.7) == "1337.7");
    assert(format("%,3.2f", 1331.982) == "1,331.98");
    assert(format("%,3.0f", 1303.1982) == "1,303");
    assert(format("%#,3.4f", 1303.1982) == "1,303.1982");
    assert(format("%#,3.0f", 1303.1982) == "1,303.");
}

@safe /*pure*/ unittest     // formatting floating point values is now impure
{
    import std.conv : to;

    assert(collectExceptionMsg!FormatException(format("%d", 5.1)).back == 'd');

    static foreach (T; AliasSeq!(float, double, real))
    {
        formatTest( to!(          T)(5.5), "5.5" );
        formatTest( to!(    const T)(5.5), "5.5" );
        formatTest( to!(immutable T)(5.5), "5.5" );

        formatTest( T.nan, "nan" );
    }
}

@system unittest
{
    formatTest( 2.25, "2.25" );

    class C1 { double val; alias val this; this(double v){ val = v; } }
    class C2 { double val; alias val this; this(double v){ val = v; }
               override string toString() const { return "C"; } }
    formatTest( new C1(2.25), "2.25" );
    formatTest( new C2(2.25), "C" );

    struct S1 { double val; alias val this; }
    struct S2 { double val; alias val this;
                string toString() const { return "S"; } }
    formatTest( S1(2.25), "2.25" );
    formatTest( S2(2.25), "S" );
}

// https://issues.dlang.org/show_bug.cgi?id=19939
@safe unittest
{
    assert(format("^%13,3.2f$",          1.00) == "^         1.00$");
    assert(format("^%13,3.2f$",         10.00) == "^        10.00$");
    assert(format("^%13,3.2f$",        100.00) == "^       100.00$");
    assert(format("^%13,3.2f$",      1_000.00) == "^     1,000.00$");
    assert(format("^%13,3.2f$",     10_000.00) == "^    10,000.00$");
    assert(format("^%13,3.2f$",    100_000.00) == "^   100,000.00$");
    assert(format("^%13,3.2f$",  1_000_000.00) == "^ 1,000,000.00$");
    assert(format("^%13,3.2f$", 10_000_000.00) == "^10,000,000.00$");
}

// https://issues.dlang.org/show_bug.cgi?id=20069
@safe unittest
{
    assert(format("%012,f",   -1234.0) ==    "-1,234.000000");
    assert(format("%013,f",   -1234.0) ==    "-1,234.000000");
    assert(format("%014,f",   -1234.0) ==   "-01,234.000000");
    assert(format("%011,f",    1234.0) ==     "1,234.000000");
    assert(format("%012,f",    1234.0) ==     "1,234.000000");
    assert(format("%013,f",    1234.0) ==    "01,234.000000");
    assert(format("%014,f",    1234.0) ==   "001,234.000000");
    assert(format("%015,f",    1234.0) == "0,001,234.000000");
    assert(format("%016,f",    1234.0) == "0,001,234.000000");

    assert(format( "%08,.2f", -1234.0) ==        "-1,234.00");
    assert(format( "%09,.2f", -1234.0) ==        "-1,234.00");
    assert(format("%010,.2f", -1234.0) ==       "-01,234.00");
    assert(format("%011,.2f", -1234.0) ==      "-001,234.00");
    assert(format("%012,.2f", -1234.0) ==    "-0,001,234.00");
    assert(format("%013,.2f", -1234.0) ==    "-0,001,234.00");
    assert(format("%014,.2f", -1234.0) ==   "-00,001,234.00");
    assert(format( "%08,.2f",  1234.0) ==         "1,234.00");
    assert(format( "%09,.2f",  1234.0) ==        "01,234.00");
    assert(format("%010,.2f",  1234.0) ==       "001,234.00");
    assert(format("%011,.2f",  1234.0) ==     "0,001,234.00");
    assert(format("%012,.2f",  1234.0) ==     "0,001,234.00");
    assert(format("%013,.2f",  1234.0) ==    "00,001,234.00");
    assert(format("%014,.2f",  1234.0) ==   "000,001,234.00");
    assert(format("%015,.2f",  1234.0) == "0,000,001,234.00");
    assert(format("%016,.2f",  1234.0) == "0,000,001,234.00");
}

@safe unittest
{
    string t1 = format("[%6s] [%-6s]", 12.3, 12.3);
    assert(t1 == "[  12.3] [12.3  ]");

    string t2 = format("[%6s] [%-6s]", -12.3, -12.3);
    assert(t2 == "[ -12.3] [-12.3 ]");
}

// https://issues.dlang.org/show_bug.cgi?id=20396
@safe unittest
{
    import std.math : nextUp;

    assert(format!"%a"(nextUp(0.0f)) == "0x0.000002p-126");
    assert(format!"%a"(nextUp(0.0)) == "0x0.0000000000001p-1022");
}

// https://issues.dlang.org/show_bug.cgi?id=20371
@safe unittest
{
    assert(format!"%.1000a"(1.0) ==
           "0x1.000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"0000000000000000000000000000000000000000000000000000000000000000000p+0");
}

/*
    Formatting a `creal` is deprecated but still kept around for a while.
 */
deprecated("Use of complex types is deprecated. Use std.complex")
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, T obj, scope const ref FormatSpec!Char f)
if (is(Unqual!T : creal) && !is(T == enum) && !hasToString!(T, Char))
{
    immutable creal val = obj;

    formatValueImpl(w, val.re, f);
    if (val.im >= 0)
    {
        put(w, '+');
    }
    formatValueImpl(w, val.im, f);
    put(w, 'i');
}

version (TestComplex)
deprecated
@safe /*pure*/ unittest     // formatting floating point values is now impure
{
    import std.conv : to;
    static foreach (T; AliasSeq!(cfloat, cdouble, creal))
    {
        formatTest( to!(          T)(1 + 1i), "1+1i" );
        formatTest( to!(    const T)(1 + 1i), "1+1i" );
        formatTest( to!(immutable T)(1 + 1i), "1+1i" );
    }
    static foreach (T; AliasSeq!(cfloat, cdouble, creal))
    {
        formatTest( to!(          T)(0 - 3i), "0-3i" );
        formatTest( to!(    const T)(0 - 3i), "0-3i" );
        formatTest( to!(immutable T)(0 - 3i), "0-3i" );
    }
}

version (TestComplex)
deprecated
@system unittest
{
    formatTest( 3+2.25i, "3+2.25i" );

    class C1 { cdouble val; alias val this; this(cdouble v){ val = v; } }
    class C2 { cdouble val; alias val this; this(cdouble v){ val = v; }
               override string toString() const { return "C"; } }
    formatTest( new C1(3+2.25i), "3+2.25i" );
    formatTest( new C2(3+2.25i), "C" );

    struct S1 { cdouble val; alias val this; }
    struct S2 { cdouble val; alias val this;
                string toString() const { return "S"; } }
    formatTest( S1(3+2.25i), "3+2.25i" );
    formatTest( S2(3+2.25i), "S" );
}

/*
    Formatting an `ireal` is deprecated but still kept around for a while.
 */
deprecated("Use of imaginary types is deprecated. Use std.complex")
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, T obj, scope const ref FormatSpec!Char f)
if (is(Unqual!T : ireal) && !is(T == enum) && !hasToString!(T, Char))
{
    immutable ireal val = obj;

    formatValueImpl(w, val.im, f);
    put(w, 'i');
}

version (TestComplex)
deprecated
@safe /*pure*/ unittest     // formatting floating point values is now impure
{
    import std.conv : to;
    static foreach (T; AliasSeq!(ifloat, idouble, ireal))
    {
        formatTest( to!(          T)(1i), "1i" );
        formatTest( to!(    const T)(1i), "1i" );
        formatTest( to!(immutable T)(1i), "1i" );
    }
}

version (TestComplex)
deprecated
@system unittest
{
    formatTest( 2.25i, "2.25i" );

    class C1 { idouble val; alias val this; this(idouble v){ val = v; } }
    class C2 { idouble val; alias val this; this(idouble v){ val = v; }
               override string toString() const { return "C"; } }
    formatTest( new C1(2.25i), "2.25i" );
    formatTest( new C2(2.25i), "C" );

    struct S1 { idouble val; alias val this; }
    struct S2 { idouble val; alias val this;
                string toString() const { return "S"; } }
    formatTest( S1(2.25i), "2.25i" );
    formatTest( S2(2.25i), "S" );
}

/*
    Individual characters are formatted as Unicode characters with `%s`
    and as integers with integral-specific format specs
 */
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, T obj, scope const ref FormatSpec!Char f)
if (is(CharTypeOf!T) && !is(T == enum) && !hasToString!(T, Char))
{
    CharTypeOf!T[1] val = obj;

    if (f.spec == 's' || f.spec == 'c')
        writeAligned(w, val[], f);
    else
    {
        alias U = AliasSeq!(ubyte, ushort, uint)[CharTypeOf!T.sizeof/2];
        formatValueImpl(w, cast(U) val[0], f);
    }
}

@safe pure unittest
{
    assertCTFEable!(
    {
        formatTest( 'c', "c" );
    });
}

@system unittest
{
    class C1 { char val; alias val this; this(char v){ val = v; } }
    class C2 { char val; alias val this; this(char v){ val = v; }
               override string toString() const { return "C"; } }
    formatTest( new C1('c'), "c" );
    formatTest( new C2('c'), "C" );

    struct S1 { char val; alias val this; }
    struct S2 { char val; alias val this;
                string toString() const { return "S"; } }
    formatTest( S1('c'), "c" );
    formatTest( S2('c'), "S" );
}

@safe unittest
{
    //Little Endian
    formatTest( "%-r", cast( char)'c', ['c'         ] );
    formatTest( "%-r", cast(wchar)'c', ['c', 0      ] );
    formatTest( "%-r", cast(dchar)'c', ['c', 0, 0, 0] );
    formatTest( "%-r", '本', ['\x2c', '\x67'] );

    //Big Endian
    formatTest( "%+r", cast( char)'c', [         'c'] );
    formatTest( "%+r", cast(wchar)'c', [0,       'c'] );
    formatTest( "%+r", cast(dchar)'c', [0, 0, 0, 'c'] );
    formatTest( "%+r", '本', ['\x67', '\x2c'] );
}


@safe pure unittest
{
    string t1 = format("[%6s] [%-6s]", 'A', 'A');
    assert(t1 == "[     A] [A     ]");
    string t2 = format("[%6s] [%-6s]", '本', '本');
    assert(t2 == "[     本] [本     ]");
}

/*
    Strings are formatted like $(REF printf, core, stdc, stdio)
 */
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, scope T obj, scope const ref FormatSpec!Char f)
if (is(StringTypeOf!T) && !is(StaticArrayTypeOf!T) && !is(T == enum) && !hasToString!(T, Char))
{
    Unqual!(StringTypeOf!T) val = obj;  // for `alias this`, see bug5371
    formatRange(w, val, f);
}

@safe unittest
{
    formatTest( "abc", "abc" );
}

@system unittest
{
    // Test for bug 5371 for classes
    class C1 { const string var; alias var this; this(string s){ var = s; } }
    class C2 {       string var; alias var this; this(string s){ var = s; } }
    formatTest( new C1("c1"), "c1" );
    formatTest( new C2("c2"), "c2" );

    // Test for bug 5371 for structs
    struct S1 { const string var; alias var this; }
    struct S2 {       string var; alias var this; }
    formatTest( S1("s1"), "s1" );
    formatTest( S2("s2"), "s2" );
}

@system unittest
{
    class  C3 { string val; alias val this; this(string s){ val = s; }
                override string toString() const { return "C"; } }
    formatTest( new C3("c3"), "C" );

    struct S3 { string val; alias val this;
                string toString() const { return "S"; } }
    formatTest( S3("s3"), "S" );
}

@safe pure unittest
{
    //Little Endian
    formatTest( "%-r", "ab"c, ['a'         , 'b'         ] );
    formatTest( "%-r", "ab"w, ['a', 0      , 'b', 0      ] );
    formatTest( "%-r", "ab"d, ['a', 0, 0, 0, 'b', 0, 0, 0] );
    formatTest( "%-r", "日本語"c, ['\xe6', '\x97', '\xa5', '\xe6', '\x9c', '\xac', '\xe8', '\xaa', '\x9e'] );
    formatTest( "%-r", "日本語"w, ['\xe5', '\x65', '\x2c', '\x67', '\x9e', '\x8a']);
    formatTest( "%-r", "日本語"d, ['\xe5', '\x65', '\x00', '\x00', '\x2c', '\x67',
        '\x00', '\x00', '\x9e', '\x8a', '\x00', '\x00'] );

    //Big Endian
    formatTest( "%+r", "ab"c, [         'a',          'b'] );
    formatTest( "%+r", "ab"w, [      0, 'a',       0, 'b'] );
    formatTest( "%+r", "ab"d, [0, 0, 0, 'a', 0, 0, 0, 'b'] );
    formatTest( "%+r", "日本語"c, ['\xe6', '\x97', '\xa5', '\xe6', '\x9c', '\xac', '\xe8', '\xaa', '\x9e'] );
    formatTest( "%+r", "日本語"w, ['\x65', '\xe5', '\x67', '\x2c', '\x8a', '\x9e'] );
    formatTest( "%+r", "日本語"d, ['\x00', '\x00', '\x65', '\xe5', '\x00', '\x00',
        '\x67', '\x2c', '\x00', '\x00', '\x8a', '\x9e'] );
}

@safe pure unittest
{
    string t1 = format("[%6s] [%-6s]", "AB", "AB");
    assert(t1 == "[    AB] [AB    ]");
    string t2 = format("[%6s] [%-6s]", "本Ä", "本Ä");
    assert(t2 == "[    本Ä] [本Ä    ]");
}

/*
    Static-size arrays are formatted as dynamic arrays.
 */
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, auto ref T obj, scope const ref FormatSpec!Char f)
if (is(StaticArrayTypeOf!T) && !is(T == enum) && !hasToString!(T, Char))
{
    formatValueImpl(w, obj[], f);
}

// Test for https://issues.dlang.org/show_bug.cgi?id=8310
@safe unittest
{
    import std.array : appender;
    FormatSpec!char f;
    auto w = appender!string();

    char[2] two = ['a', 'b'];
    formatValue(w, two, f);

    char[2] getTwo(){ return two; }
    formatValue(w, getTwo(), f);
}

/*
    Dynamic arrays are formatted as input ranges.
 */
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, T obj, scope const ref FormatSpec!Char f)
if (is(DynamicArrayTypeOf!T) && !is(StringTypeOf!T) && !is(T == enum) && !hasToString!(T, Char))
{
    static if (is(const(ArrayTypeOf!T) == const(void[])))
    {
        formatValueImpl(w, cast(const ubyte[]) obj, f);
    }
    else static if (!isInputRange!T)
    {
        alias U = Unqual!(ArrayTypeOf!T);
        static assert(isInputRange!U, U.stringof ~ " must be an InputRange");
        U val = obj;
        formatValueImpl(w, val, f);
    }
    else
    {
        formatRange(w, obj, f);
    }
}

// alias this, input range I/F, and toString()
@system unittest
{
    struct S(int flags)
    {
        int[] arr;
      static if (flags & 1)
        alias arr this;

      static if (flags & 2)
      {
        @property bool empty() const { return arr.length == 0; }
        @property int front() const { return arr[0] * 2; }
        void popFront() { arr = arr[1..$]; }
      }

      static if (flags & 4)
        string toString() const { return "S"; }
    }
    formatTest(S!0b000([0, 1, 2]), "S!0([0, 1, 2])");
    formatTest(S!0b001([0, 1, 2]), "[0, 1, 2]");        // Test for bug 7628
    formatTest(S!0b010([0, 1, 2]), "[0, 2, 4]");
    formatTest(S!0b011([0, 1, 2]), "[0, 2, 4]");
    formatTest(S!0b100([0, 1, 2]), "S");
    formatTest(S!0b101([0, 1, 2]), "S");                // Test for bug 7628
    formatTest(S!0b110([0, 1, 2]), "S");
    formatTest(S!0b111([0, 1, 2]), "S");

    class C(uint flags)
    {
        int[] arr;
      static if (flags & 1)
        alias arr this;

        this(int[] a) { arr = a; }

      static if (flags & 2)
      {
        @property bool empty() const { return arr.length == 0; }
        @property int front() const { return arr[0] * 2; }
        void popFront() { arr = arr[1..$]; }
      }

      static if (flags & 4)
        override string toString() const { return "C"; }
    }
    formatTest(new C!0b000([0, 1, 2]), (new C!0b000([])).toString());
    formatTest(new C!0b001([0, 1, 2]), "[0, 1, 2]");    // Test for bug 7628
    formatTest(new C!0b010([0, 1, 2]), "[0, 2, 4]");
    formatTest(new C!0b011([0, 1, 2]), "[0, 2, 4]");
    formatTest(new C!0b100([0, 1, 2]), "C");
    formatTest(new C!0b101([0, 1, 2]), "C");            // Test for bug 7628
    formatTest(new C!0b110([0, 1, 2]), "C");
    formatTest(new C!0b111([0, 1, 2]), "C");
}

@system unittest
{
    // void[]
    void[] val0;
    formatTest( val0, "[]" );

    void[] val = cast(void[]) cast(ubyte[])[1, 2, 3];
    formatTest( val, "[1, 2, 3]" );

    void[0] sval0 = [];
    formatTest( sval0, "[]");

    void[3] sval = cast(void[3]) cast(ubyte[3])[1, 2, 3];
    formatTest( sval, "[1, 2, 3]" );
}

@safe unittest
{
    // const(T[]) -> const(T)[]
    const short[] a = [1, 2, 3];
    formatTest( a, "[1, 2, 3]" );

    struct S { const(int[]) arr; alias arr this; }
    auto s = S([1,2,3]);
    formatTest( s, "[1, 2, 3]" );
}

// https://issues.dlang.org/show_bug.cgi?id=6640
@safe unittest
{
    struct Range
    {
      @safe:
        string value;
        @property bool empty() const { return !value.length; }
        @property dchar front() const { return value.front; }
        void popFront() { value.popFront(); }

        @property size_t length() const { return value.length; }
    }
    immutable table =
    [
        ["[%s]", "[string]"],
        ["[%10s]", "[    string]"],
        ["[%-10s]", "[string    ]"],
        ["[%(%02x %)]", "[73 74 72 69 6e 67]"],
        ["[%(%c %)]", "[s t r i n g]"],
    ];
    foreach (e; table)
    {
        formatTest(e[0], "string", e[1]);
        formatTest(e[0], Range("string"), e[1]);
    }
}

@system unittest
{
    // string literal from valid UTF sequence is encoding free.
    static foreach (StrType; AliasSeq!(string, wstring, dstring))
    {
        // Valid and printable (ASCII)
        formatTest( [cast(StrType)"hello"],
                    `["hello"]` );

        // 1 character escape sequences (' is not escaped in strings)
        formatTest( [cast(StrType)"\"'\0\\\a\b\f\n\r\t\v"],
                    `["\"'\0\\\a\b\f\n\r\t\v"]` );

        // 1 character optional escape sequences
        formatTest( [cast(StrType)"\'\?"],
                    `["'?"]` );

        // Valid and non-printable code point (<= U+FF)
        formatTest( [cast(StrType)"\x10\x1F\x20test"],
                    `["\x10\x1F test"]` );

        // Valid and non-printable code point (<= U+FFFF)
        formatTest( [cast(StrType)"\u200B..\u200F"],
                    `["\u200B..\u200F"]` );

        // Valid and non-printable code point (<= U+10FFFF)
        formatTest( [cast(StrType)"\U000E0020..\U000E007F"],
                    `["\U000E0020..\U000E007F"]` );
    }

    // invalid UTF sequence needs hex-string literal postfix (c/w/d)
    {
        // U+FFFF with UTF-8 (Invalid code point for interchange)
        formatTest( [cast(string)[0xEF, 0xBF, 0xBF]],
                    `[x"EF BF BF"c]` );

        // U+FFFF with UTF-16 (Invalid code point for interchange)
        formatTest( [cast(wstring)[0xFFFF]],
                    `[x"FFFF"w]` );

        // U+FFFF with UTF-32 (Invalid code point for interchange)
        formatTest( [cast(dstring)[0xFFFF]],
                    `[x"FFFF"d]` );
    }
}

@safe unittest
{
    // nested range formatting with array of string
    formatTest( "%({%(%02x %)}%| %)", ["test", "msg"],
                `{74 65 73 74} {6d 73 67}` );
}

@safe unittest
{
    // stop auto escaping inside range formatting
    auto arr = ["hello", "world"];
    formatTest( "%(%s, %)",  arr, `"hello", "world"` );
    formatTest( "%-(%s, %)", arr, `hello, world` );

    auto aa1 = [1:"hello", 2:"world"];
    formatTest( "%(%s:%s, %)",  aa1, [`1:"hello", 2:"world"`, `2:"world", 1:"hello"`] );
    formatTest( "%-(%s:%s, %)", aa1, [`1:hello, 2:world`, `2:world, 1:hello`] );

    auto aa2 = [1:["ab", "cd"], 2:["ef", "gh"]];
    formatTest( "%-(%s:%s, %)",        aa2, [`1:["ab", "cd"], 2:["ef", "gh"]`, `2:["ef", "gh"], 1:["ab", "cd"]`] );
    formatTest( "%-(%s:%(%s%), %)",    aa2, [`1:"ab""cd", 2:"ef""gh"`, `2:"ef""gh", 1:"ab""cd"`] );
    formatTest( "%-(%s:%-(%s%)%|, %)", aa2, [`1:abcd, 2:efgh`, `2:efgh, 1:abcd`] );
}

// input range formatting
private void formatRange(Writer, T, Char)(ref Writer w, ref T val, scope const ref FormatSpec!Char f)
if (isInputRange!T)
{
    // in this mode, we just want to do a representative print to discover if the format spec is valid
    enum formatTestMode = is(Writer == NoOpSink);

    import std.conv : text;

    // Formatting character ranges like string
    if (f.spec == 's')
    {
        alias E = ElementType!T;

        static if (!is(E == enum) && is(CharTypeOf!E))
        {
            static if (is(StringTypeOf!T))
                writeAligned(w, val[0 .. f.precision < $ ? f.precision : $], f);
            else
            {
                if (!f.flDash)
                {
                    static if (hasLength!T)
                    {
                        // right align
                        auto len = val.length;
                    }
                    else static if (isForwardRange!T && !isInfinite!T)
                    {
                        auto len = walkLength(val.save);
                    }
                    else
                    {
                        enforceFmt(f.width == 0, "Cannot right-align a range without length");
                        size_t len = 0;
                    }
                    if (f.precision != f.UNSPECIFIED && len > f.precision)
                        len = f.precision;

                    if (f.width > len)
                        foreach (i ; 0 .. f.width - len)
                            put(w, ' ');
                    if (f.precision == f.UNSPECIFIED)
                        put(w, val);
                    else
                    {
                        size_t printed = 0;
                        for (; !val.empty && printed < f.precision; val.popFront(), ++printed)
                            put(w, val.front);
                    }
                }
                else
                {
                    size_t printed = void;

                    // left align
                    if (f.precision == f.UNSPECIFIED)
                    {
                        static if (hasLength!T)
                        {
                            printed = val.length;
                            put(w, val);
                        }
                        else
                        {
                            printed = 0;
                            for (; !val.empty; val.popFront(), ++printed)
                            {
                                put(w, val.front);
                                static if (formatTestMode) break; // one is enough to test
                            }
                        }
                    }
                    else
                    {
                        printed = 0;
                        for (; !val.empty && printed < f.precision; val.popFront(), ++printed)
                            put(w, val.front);
                    }

                    if (f.width > printed)
                        foreach (i ; 0 .. f.width - printed)
                            put(w, ' ');
                }
            }
        }
        else
        {
            put(w, f.seqBefore);
            if (!val.empty)
            {
                formatElement(w, val.front, f);
                val.popFront();
                for (size_t i; !val.empty; val.popFront(), ++i)
                {
                    put(w, f.seqSeparator);
                    formatElement(w, val.front, f);
                    static if (formatTestMode) break; // one is enough to test
                }
            }
            static if (!isInfinite!T) put(w, f.seqAfter);
        }
    }
    else if (f.spec == 'r')
    {
        static if (is(DynamicArrayTypeOf!T))
        {
            alias ARR = DynamicArrayTypeOf!T;
            scope a = cast(ARR) val;
            foreach (e ; a)
            {
                formatValue(w, e, f);
                static if (formatTestMode) break; // one is enough to test
            }
        }
        else
        {
            for (size_t i; !val.empty; val.popFront(), ++i)
            {
                formatValue(w, val.front, f);
                static if (formatTestMode) break; // one is enough to test
            }
        }
    }
    else if (f.spec == '(')
    {
        if (val.empty)
            return;
        // Nested specifier is to be used
        for (;;)
        {
            auto fmt = FormatSpec!Char(f.nested);
            w: while (true)
            {
                immutable r = fmt.writeUpToNextSpec(w);
                // There was no format specifier, so break
                if (!r)
                    break;
                if (f.flDash)
                    formatValue(w, val.front, fmt);
                else
                    formatElement(w, val.front, fmt);
                // Check if there will be a format specifier farther on in the
                // string. If so, continue the loop, otherwise break. This
                // prevents extra copies of the `sep` from showing up.
                foreach (size_t i; 0 .. fmt.trailing.length)
                    if (fmt.trailing[i] == '%')
                        continue w;
                break w;
            }
            static if (formatTestMode)
            {
                break; // one is enough to test
            }
            else
            {
                if (f.sep !is null)
                {
                    put(w, fmt.trailing);
                    val.popFront();
                    if (val.empty)
                        break;
                    put(w, f.sep);
                }
                else
                {
                    val.popFront();
                    if (val.empty)
                        break;
                    put(w, fmt.trailing);
                }
            }
        }
    }
    else
        throw new FormatException(text("Incorrect format specifier for range: %", f.spec));
}

// https://issues.dlang.org/show_bug.cgi?id=18778
@safe pure unittest
{
    assert(format("%-(%1$s - %1$s, %)", ["A", "B", "C"]) == "A - A, B - B, C - C");
}

@safe pure unittest
{
    assert(collectExceptionMsg(format("%d", "hi")).back == 'd');
}

// character formatting with ecaping
private void formatChar(Writer)(ref Writer w, in dchar c, in char quote)
{
    import std.uni : isGraphical;

    string fmt;
    if (isGraphical(c))
    {
        if (c == quote || c == '\\')
            put(w, '\\');
        put(w, c);
        return;
    }
    else if (c <= 0xFF)
    {
        if (c < 0x20)
        {
            foreach (i, k; "\n\r\t\a\b\f\v\0")
            {
                if (c == k)
                {
                    put(w, '\\');
                    put(w, "nrtabfv0"[i]);
                    return;
                }
            }
        }
        fmt = "\\x%02X";
    }
    else if (c <= 0xFFFF)
        fmt = "\\u%04X";
    else
        fmt = "\\U%08X";

    formattedWrite(w, fmt, cast(uint) c);
}

// undocumented because of deprecation
// string elements are formatted like UTF-8 string literals.
void formatElement(Writer, T, Char)(auto ref Writer w, T val, scope const ref FormatSpec!Char f)
if (is(StringTypeOf!T) && !is(T == enum))
{
    import std.array : appender;
    import std.utf : decode, UTFException;

    StringTypeOf!T str = val;   // https://issues.dlang.org/show_bug.cgi?id=8015

    if (f.spec == 's')
    {
        try
        {
            // ignore other specifications and quote
            for (size_t i = 0; i < str.length; )
            {
                auto c = decode(str, i);
                // \uFFFE and \uFFFF are considered valid by isValidDchar,
                // so need checking for interchange.
                if (c == 0xFFFE || c == 0xFFFF)
                    goto LinvalidSeq;
            }
            put(w, '\"');
            for (size_t i = 0; i < str.length; )
            {
                auto c = decode(str, i);
                formatChar(w, c, '"');
            }
            put(w, '\"');
            return;
        }
        catch (UTFException)
        {
        }

        // If val contains invalid UTF sequence, formatted like HexString literal
    LinvalidSeq:
        static if (is(typeof(str[0]) : const(char)))
        {
            enum postfix = 'c';
            alias IntArr = const(ubyte)[];
        }
        else static if (is(typeof(str[0]) : const(wchar)))
        {
            enum postfix = 'w';
            alias IntArr = const(ushort)[];
        }
        else static if (is(typeof(str[0]) : const(dchar)))
        {
            enum postfix = 'd';
            alias IntArr = const(uint)[];
        }
        formattedWrite(w, "x\"%(%02X %)\"%s", cast(IntArr) str, postfix);
    }
    else
        formatValue(w, str, f);
}

@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%s");
    formatElement(w, "Hello World", spec);

    assert(w.data == "\"Hello World\"");
}

@safe unittest
{
    // Test for bug 8015
    import std.typecons;

    struct MyStruct {
        string str;
        @property string toStr() {
            return str;
        }
        alias toStr this;
    }

    Tuple!(MyStruct) t;
}

// undocumented because of deprecation
// Character elements are formatted like UTF-8 character literals.
void formatElement(Writer, T, Char)(auto ref Writer w, T val, scope const ref FormatSpec!Char f)
if (is(CharTypeOf!T) && !is(T == enum))
{
    if (f.spec == 's')
    {
        put(w, '\'');
        formatChar(w, val, '\'');
        put(w, '\'');
    }
    else
        formatValue(w, val, f);
}

///
@safe unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%s");
    formatElement(w, "H", spec);

    assert(w.data == "\"H\"", w.data);
}

// undocumented
// Maybe T is noncopyable struct, so receive it by 'auto ref'.
void formatElement(Writer, T, Char)(auto ref Writer w, auto ref T val, scope const ref FormatSpec!Char f)
if (!is(StringTypeOf!T) && !is(CharTypeOf!T) || is(T == enum))
{
    formatValue(w, val, f);
}

/*
   Associative arrays are formatted by using `':'` and $(D ", ") as
   separators, and enclosed by `'['` and `']'`.
 */
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, T obj, scope const ref FormatSpec!Char f)
if (is(AssocArrayTypeOf!T) && !is(T == enum) && !hasToString!(T, Char))
{
    AssocArrayTypeOf!T val = obj;
    const spec = f.spec;

    enforceFmt(spec == 's' || spec == '(',
        "incompatible format character for associative array argument: %" ~ spec);

    enum const(Char)[] defSpec = "%s" ~ f.keySeparator ~ "%s" ~ f.seqSeparator;
    auto fmtSpec = spec == '(' ? f.nested : defSpec;

    size_t i = 0;
    immutable end = val.length;

    if (spec == 's')
        put(w, f.seqBefore);
    foreach (k, ref v; val)
    {
        auto fmt = FormatSpec!Char(fmtSpec);
        fmt.writeUpToNextSpec(w);
        if (f.flDash)
        {
            formatValue(w, k, fmt);
            fmt.writeUpToNextSpec(w);
            formatValue(w, v, fmt);
        }
        else
        {
            formatElement(w, k, fmt);
            fmt.writeUpToNextSpec(w);
            formatElement(w, v, fmt);
        }
        if (f.sep !is null)
        {
            fmt.writeUpToNextSpec(w);
            if (++i != end)
                put(w, f.sep);
        }
        else
        {
            if (++i != end)
                fmt.writeUpToNextSpec(w);
        }
    }
    if (spec == 's')
        put(w, f.seqAfter);
}

@safe unittest
{
    assert(collectExceptionMsg!FormatException(format("%d", [0:1])).back == 'd');

    int[string] aa0;
    formatTest( aa0, `[]` );

    // elements escaping
    formatTest(  ["aaa":1, "bbb":2],
               [`["aaa":1, "bbb":2]`, `["bbb":2, "aaa":1]`] );
    formatTest(  ['c':"str"],
                `['c':"str"]` );
    formatTest(  ['"':"\"", '\'':"'"],
               [`['"':"\"", '\'':"'"]`, `['\'':"'", '"':"\""]`] );

    // range formatting for AA
    auto aa3 = [1:"hello", 2:"world"];
    // escape
    formatTest( "{%(%s:%s $ %)}", aa3,
               [`{1:"hello" $ 2:"world"}`, `{2:"world" $ 1:"hello"}`]);
    // use range formatting for key and value, and use %|
    formatTest( "{%([%04d->%(%c.%)]%| $ %)}", aa3,
               [`{[0001->h.e.l.l.o] $ [0002->w.o.r.l.d]}`, `{[0002->w.o.r.l.d] $ [0001->h.e.l.l.o]}`] );

    // https://issues.dlang.org/show_bug.cgi?id=12135
    formatTest("%(%s:<%s>%|,%)", [1:2], "1:<2>");
    formatTest("%(%s:<%s>%|%)" , [1:2], "1:<2>");
}

@system unittest
{
    class C1 { int[char] val; alias val this; this(int[char] v){ val = v; } }
    class C2 { int[char] val; alias val this; this(int[char] v){ val = v; }
               override string toString() const { return "C"; } }
    formatTest( new C1(['c':1, 'd':2]), [`['c':1, 'd':2]`, `['d':2, 'c':1]`] );
    formatTest( new C2(['c':1, 'd':2]), "C" );

    struct S1 { int[char] val; alias val this; }
    struct S2 { int[char] val; alias val this;
                string toString() const { return "S"; } }
    formatTest( S1(['c':1, 'd':2]), [`['c':1, 'd':2]`, `['d':2, 'c':1]`] );
    formatTest( S2(['c':1, 'd':2]), "S" );
}

// https://issues.dlang.org/show_bug.cgi?id=8921
@safe unittest
{
    enum E : char { A = 'a', B = 'b', C = 'c' }
    E[3] e = [E.A, E.B, E.C];
    formatTest(e, "[A, B, C]");

    E[] e2 = [E.A, E.B, E.C];
    formatTest(e2, "[A, B, C]");
}

private enum HasToStringResult
{
    none,
    hasSomeToString,
    constCharSink,
    constCharSinkFormatString,
    constCharSinkFormatSpec,
    customPutWriter,
    customPutWriterFormatSpec,
}

private template hasToString(T, Char)
{
    static if (isPointer!T && !isAggregateType!T)
    {
        // X* does not have toString, even if X is aggregate type has toString.
        enum hasToString = HasToStringResult.none;
    }
    else static if (is(typeof(
        {T val = void;
        const FormatSpec!Char f;
        static struct S {void put(scope Char s){}}
        S s;
        val.toString(s, f);
        // force toString to take parameters by ref
        static assert(!__traits(compiles, val.toString(s, FormatSpec!Char())));
        static assert(!__traits(compiles, val.toString(S(), f)));}
    )))
    {
        enum hasToString = HasToStringResult.customPutWriterFormatSpec;
    }
    else static if (is(typeof(
        {T val = void;
        static struct S {void put(scope Char s){}}
        S s;
        val.toString(s);
        // force toString to take parameters by ref
        static assert(!__traits(compiles, val.toString(S())));}
    )))
    {
        enum hasToString = HasToStringResult.customPutWriter;
    }
    else static if (is(typeof({ T val = void; FormatSpec!Char f; val.toString((scope const(char)[] s){}, f); })))
    {
        enum hasToString = HasToStringResult.constCharSinkFormatSpec;
    }
    else static if (is(typeof({ T val = void; val.toString((scope const(char)[] s){}, "%s"); })))
    {
        enum hasToString = HasToStringResult.constCharSinkFormatString;
    }
    else static if (is(typeof({ T val = void; val.toString((scope const(char)[] s){}); })))
    {
        enum hasToString = HasToStringResult.constCharSink;
    }
    else static if (is(typeof({ T val = void; return val.toString(); }()) S) && isSomeString!S)
    {
        enum hasToString = HasToStringResult.hasSomeToString;
    }
    else
    {
        enum hasToString = HasToStringResult.none;
    }
}

@safe unittest
{
    static struct A
    {
        void toString(Writer)(ref Writer w)
        if (isOutputRange!(Writer, string))
        {}
    }
    static struct B
    {
        void toString(scope void delegate(scope const(char)[]) sink, scope FormatSpec!char fmt) {}
    }
    static struct C
    {
        void toString(scope void delegate(scope const(char)[]) sink, string fmt) {}
    }
    static struct D
    {
        void toString(scope void delegate(scope const(char)[]) sink) {}
    }
    static struct E
    {
        string toString() {return "";}
    }
    static struct F
    {
        void toString(Writer)(ref Writer w, scope const ref FormatSpec!char fmt)
        if (isOutputRange!(Writer, string))
        {}
    }
    static struct G
    {
        string toString() {return "";}
        void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, string)) {}
    }
    static struct H
    {
        string toString() {return "";}
        void toString(Writer)(ref Writer w, scope const ref FormatSpec!char fmt)
        if (isOutputRange!(Writer, string))
        {}
    }
    static struct I
    {
        void toString(Writer)(ref Writer w) if (isOutputRange!(Writer, string)) {}
        void toString(Writer)(ref Writer w, scope const ref FormatSpec!char fmt)
        if (isOutputRange!(Writer, string))
        {}
    }
    static struct J
    {
        string toString() {return "";}
        void toString(Writer)(ref Writer w, scope ref FormatSpec!char fmt)
        if (isOutputRange!(Writer, string))
        {}
    }
    static struct K
    {
        void toString(Writer)(Writer w, scope const ref FormatSpec!char fmt)
        if (isOutputRange!(Writer, string))
        {}
    }
    static struct L
    {
        void toString(Writer)(ref Writer w, scope const FormatSpec!char fmt)
        if (isOutputRange!(Writer, string))
        {}
    }

    with(HasToStringResult)
    {
        static assert(hasToString!(A, char) == customPutWriter);
        static assert(hasToString!(B, char) == constCharSinkFormatSpec);
        static assert(hasToString!(C, char) == constCharSinkFormatString);
        static assert(hasToString!(D, char) == constCharSink);
        static assert(hasToString!(E, char) == hasSomeToString);
        static assert(hasToString!(F, char) == customPutWriterFormatSpec);
        static assert(hasToString!(G, char) == customPutWriter);
        static assert(hasToString!(H, char) == customPutWriterFormatSpec);
        static assert(hasToString!(I, char) == customPutWriterFormatSpec);
        static assert(hasToString!(J, char) == hasSomeToString);
        static assert(hasToString!(K, char) == constCharSinkFormatSpec);
        static assert(hasToString!(L, char) == none);
    }
}

// Like NullSink, but toString() isn't even called at all. Used to test the format string.
private struct NoOpSink
{
    void put(E)(scope const E) pure @safe @nogc nothrow {}
}

// object formatting with toString
private void formatObject(Writer, T, Char)(ref Writer w, ref T val, scope const ref FormatSpec!Char f)
if (hasToString!(T, Char))
{
    enum overload = hasToString!(T, Char);

    enum noop = is(Writer == NoOpSink);

    static if (overload == HasToStringResult.customPutWriterFormatSpec)
    {
        static if (!noop) val.toString(w, f);
    }
    else static if (overload == HasToStringResult.customPutWriter)
    {
        static if (!noop) val.toString(w);
    }
    else static if (overload == HasToStringResult.constCharSinkFormatSpec)
    {
        static if (!noop) val.toString((scope const(char)[] s) { put(w, s); }, f);
    }
    else static if (overload == HasToStringResult.constCharSinkFormatString)
    {
        static if (!noop) val.toString((scope const(char)[] s) { put(w, s); }, f.getCurFmtStr());
    }
    else static if (overload == HasToStringResult.constCharSink)
    {
        static if (!noop) val.toString((scope const(char)[] s) { put(w, s); });
    }
    else static if (overload == HasToStringResult.hasSomeToString)
    {
        static if (!noop) put(w, val.toString());
    }
    else
    {
        static assert(0, "No way found to format " ~ T.stringof ~ " as string");
    }
}

void enforceValidFormatSpec(T, Char)(scope const ref FormatSpec!Char f)
{
    enum overload = hasToString!(T, Char);
    static if (
            overload != HasToStringResult.constCharSinkFormatSpec &&
            overload != HasToStringResult.customPutWriterFormatSpec &&
            !isInputRange!T)
    {
        enforceFmt(f.spec == 's',
            "Expected '%s' format specifier for type '" ~ T.stringof ~ "'");
    }
}

@system unittest
{
    static interface IF1 { }
    class CIF1 : IF1 { }
    static struct SF1 { }
    static union UF1 { }
    static class CF1 { }

    static interface IF2 { string toString(); }
    static class CIF2 : IF2 { override string toString() { return ""; } }
    static struct SF2 { string toString() { return ""; } }
    static union UF2 { string toString() { return ""; } }
    static class CF2 { override string toString() { return ""; } }

    static interface IK1 { void toString(scope void delegate(scope const(char)[]) sink,
                           FormatSpec!char) const; }
    static class CIK1 : IK1 { override void toString(scope void delegate(scope const(char)[]) sink,
                              FormatSpec!char) const { sink("CIK1"); } }
    static struct KS1 { void toString(scope void delegate(scope const(char)[]) sink,
                        FormatSpec!char) const { sink("KS1"); } }

    static union KU1 { void toString(scope void delegate(scope const(char)[]) sink,
                       FormatSpec!char) const { sink("KU1"); } }

    static class KC1 { void toString(scope void delegate(scope const(char)[]) sink,
                       FormatSpec!char) const { sink("KC1"); } }

    IF1 cif1 = new CIF1;
    assertThrown!FormatException(format("%f", cif1));
    assertThrown!FormatException(format("%f", SF1()));
    assertThrown!FormatException(format("%f", UF1()));
    assertThrown!FormatException(format("%f", new CF1()));

    IF2 cif2 = new CIF2;
    assertThrown!FormatException(format("%f", cif2));
    assertThrown!FormatException(format("%f", SF2()));
    assertThrown!FormatException(format("%f", UF2()));
    assertThrown!FormatException(format("%f", new CF2()));

    IK1 cik1 = new CIK1;
    assert(format("%f", cik1) == "CIK1");
    assert(format("%f", KS1()) == "KS1");
    assert(format("%f", KU1()) == "KU1");
    assert(format("%f", new KC1()) == "KC1");
}

/*
   Aggregates
 */
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, T val, scope const ref FormatSpec!Char f)
if (is(T == class) && !is(T == enum))
{
    enforceValidFormatSpec!(T, Char)(f);

    // TODO: remove this check once `@disable override` deprecation cycle is finished
    static if (__traits(hasMember, T, "toString") && isSomeFunction!(val.toString))
        static assert(!__traits(isDisabled, T.toString), T.stringof ~
            " cannot be formatted because its `toString` is marked with `@disable`");

    if (val is null)
        put(w, "null");
    else
    {
        import std.algorithm.comparison : among;
        enum overload = hasToString!(T, Char);
        with(HasToStringResult)
        static if ((is(T == immutable) || is(T == const) || is(T == shared)) && overload == none)
        {
            // Remove this when Object gets const toString
            // https://issues.dlang.org/show_bug.cgi?id=7879
            static if (is(T == immutable))
                put(w, "immutable(");
            else static if (is(T == const))
                put(w, "const(");
            else static if (is(T == shared))
                put(w, "shared(");

            put(w, typeid(Unqual!T).name);
            put(w, ')');
        }
        else static if (overload.among(constCharSink, constCharSinkFormatString, constCharSinkFormatSpec) ||
                       (!isInputRange!T && !is(BuiltinTypeOf!T)))
        {
            formatObject!(Writer, T, Char)(w, val, f);
        }
        else
        {
          //string delegate() dg = &val.toString;
            Object o = val;     // workaround
            string delegate() dg = &o.toString;
            scope Object object = new Object();
            if (dg.funcptr != (&object.toString).funcptr) // toString is overridden
            {
                formatObject(w, val, f);
            }
            else static if (isInputRange!T)
            {
                formatRange(w, val, f);
            }
            else static if (is(BuiltinTypeOf!T X))
            {
                X x = val;
                formatValueImpl(w, x, f);
            }
            else
            {
                formatObject(w, val, f);
            }
        }
    }
}

@system unittest
{
    import std.array : appender;
    import std.range.interfaces;
    // class range (https://issues.dlang.org/show_bug.cgi?id=5154)
    auto c = inputRangeObject([1,2,3,4]);
    formatTest( c, "[1, 2, 3, 4]" );
    assert(c.empty);
    c = null;
    formatTest( c, "null" );
}

@system unittest
{
    // https://issues.dlang.org/show_bug.cgi?id=5354
    // If the class has both range I/F and custom toString, the use of custom
    // toString routine is prioritized.

    // Enable the use of custom toString that gets a sink delegate
    // for class formatting.

    enum inputRangeCode =
    q{
        int[] arr;
        this(int[] a){ arr = a; }
        @property int front() const { return arr[0]; }
        @property bool empty() const { return arr.length == 0; }
        void popFront(){ arr = arr[1..$]; }
    };

    class C1
    {
        mixin(inputRangeCode);
        void toString(scope void delegate(scope const(char)[]) dg,
                      scope const ref FormatSpec!char f) const
        {
            dg("[012]");
        }
    }
    class C2
    {
        mixin(inputRangeCode);
        void toString(scope void delegate(const(char)[]) dg, string f) const { dg("[012]"); }
    }
    class C3
    {
        mixin(inputRangeCode);
        void toString(scope void delegate(const(char)[]) dg) const { dg("[012]"); }
    }
    class C4
    {
        mixin(inputRangeCode);
        override string toString() const { return "[012]"; }
    }
    class C5
    {
        mixin(inputRangeCode);
    }

    formatTest( new C1([0, 1, 2]), "[012]" );
    formatTest( new C2([0, 1, 2]), "[012]" );
    formatTest( new C3([0, 1, 2]), "[012]" );
    formatTest( new C4([0, 1, 2]), "[012]" );
    formatTest( new C5([0, 1, 2]), "[0, 1, 2]" );
}

// outside the unittest block, otherwise the FQN of the
// class contains the line number of the unittest
version (StdUnittest)
{
    private class C {}
}

// https://issues.dlang.org/show_bug.cgi?id=7879
@safe unittest
{
    const(C) c;
    auto s = format("%s", c);
    assert(s == "null");

    immutable(C) c2 = new C();
    s = format("%s", c2);
    assert(s == "immutable(std.format.C)");

    const(C) c3 = new C();
    s = format("%s", c3);
    assert(s == "const(std.format.C)");

    shared(C) c4 = new C();
    s = format("%s", c4);
    assert(s == "shared(std.format.C)");
}

// https://issues.dlang.org/show_bug.cgi?id=19003
@safe unittest
{
    struct S
    {
        int i;

        @disable this();

        invariant { assert(this.i); }

        this(int i) @safe in { assert(i); } do { this.i = i; }

        string toString() { return "S"; }
    }

    S s = S(1);

    format!"%s"(s);
}

// https://issues.dlang.org/show_bug.cgi?id=20218
@safe pure unittest
{
    void notCalled()
    {
        import std.range : repeat;

        auto value = 1.repeat;

        // test that range is not evaluated to completion at compiletime
        format!"%s"(value);
    }
}

// https://issues.dlang.org/show_bug.cgi?id=7879
@safe unittest
{
    class F
    {
        override string toString() const @safe
        {
            return "Foo";
        }
    }

    const(F) c;
    auto s = format("%s", c);
    assert(s == "null");

    const(F) c2 = new F();
    s = format("%s", c2);
    assert(s == "Foo", s);
}

// ditto
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, T val, scope const ref FormatSpec!Char f)
if (is(T == interface) && (hasToString!(T, Char) || !is(BuiltinTypeOf!T)) && !is(T == enum))
{
    enforceValidFormatSpec!(T, Char)(f);
    if (val is null)
        put(w, "null");
    else
    {
        static if (__traits(hasMember, T, "toString") && isSomeFunction!(val.toString))
            static assert(!__traits(isDisabled, T.toString), T.stringof ~
                " cannot be formatted because its `toString` is marked with `@disable`");

        static if (hasToString!(T, Char) != HasToStringResult.none)
        {
            formatObject(w, val, f);
        }
        else static if (isInputRange!T)
        {
            formatRange(w, val, f);
        }
        else
        {
            version (Windows)
            {
                import core.sys.windows.com : IUnknown;
                static if (is(T : IUnknown))
                {
                    formatValueImpl(w, *cast(void**)&val, f);
                }
                else
                {
                    formatValueImpl(w, cast(Object) val, f);
                }
            }
            else
            {
                formatValueImpl(w, cast(Object) val, f);
            }
        }
    }
}

@system unittest
{
    // interface
    import std.range.interfaces;
    InputRange!int i = inputRangeObject([1,2,3,4]);
    formatTest( i, "[1, 2, 3, 4]" );
    assert(i.empty);
    i = null;
    formatTest( i, "null" );

    // interface (downcast to Object)
    interface Whatever {}
    class C : Whatever
    {
        override @property string toString() const { return "ab"; }
    }
    Whatever val = new C;
    formatTest( val, "ab" );

    // https://issues.dlang.org/show_bug.cgi?id=11175
    version (Windows)
    {
        import core.sys.windows.com : IUnknown, IID;
        import core.sys.windows.windef : HRESULT;

        interface IUnknown2 : IUnknown { }

        class D : IUnknown2
        {
            extern(Windows) HRESULT QueryInterface(const(IID)* riid, void** pvObject) { return typeof(return).init; }
            extern(Windows) uint AddRef() { return 0; }
            extern(Windows) uint Release() { return 0; }
        }

        IUnknown2 d = new D;
        string expected = format("%X", cast(void*) d);
        formatTest(d, expected);
    }
}

/// ditto
// Maybe T is noncopyable struct, so receive it by 'auto ref'.
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, auto ref T val, scope const ref FormatSpec!Char f)
if ((is(T == struct) || is(T == union)) && (hasToString!(T, Char) || !is(BuiltinTypeOf!T)) && !is(T == enum))
{

    static if (__traits(hasMember, T, "toString") && isSomeFunction!(val.toString))
        static assert(!__traits(isDisabled, T.toString), T.stringof ~
            " cannot be formatted because its `toString` is marked with `@disable`");

    enforceValidFormatSpec!(T, Char)(f);
    static if (hasToString!(T, Char))
    {
        formatObject(w, val, f);
    }
    else static if (isInputRange!T)
    {
        formatRange(w, val, f);
    }
    else static if (is(T == struct))
    {
        enum left = T.stringof~"(";
        enum separator = ", ";
        enum right = ")";

        put(w, left);
        foreach (i, e; val.tupleof)
        {
            static if (__traits(identifier, val.tupleof[i]) == "this")
                continue;
            else static if (0 < i && val.tupleof[i-1].offsetof == val.tupleof[i].offsetof)
            {
                static if (i == val.tupleof.length - 1 || val.tupleof[i].offsetof != val.tupleof[i+1].offsetof)
                    put(w, separator~val.tupleof[i].stringof[4..$]~"}");
                else
                    put(w, separator~val.tupleof[i].stringof[4..$]);
            }
            else static if (i+1 < val.tupleof.length && val.tupleof[i].offsetof == val.tupleof[i+1].offsetof)
                put(w, (i > 0 ? separator : "")~"#{overlap "~val.tupleof[i].stringof[4..$]);
            else
            {
                static if (i > 0)
                    put(w, separator);
                formatElement(w, e, f);
            }
        }
        put(w, right);
    }
    else
    {
        put(w, T.stringof);
    }
}

// https://issues.dlang.org/show_bug.cgi?id=9588
@safe pure unittest
{
    struct S { int x; bool empty() { return false; } }
    formatTest( S(), "S(0)" );
}

// https://issues.dlang.org/show_bug.cgi?id=4638
@safe unittest
{
    struct U8  {  string toString() const { return "blah"; } }
    struct U16 { wstring toString() const { return "blah"; } }
    struct U32 { dstring toString() const { return "blah"; } }
    formatTest( U8(), "blah" );
    formatTest( U16(), "blah" );
    formatTest( U32(), "blah" );
}

// https://issues.dlang.org/show_bug.cgi?id=3890
@safe unittest
{
    struct Int{ int n; }
    struct Pair{ string s; Int i; }
    formatTest( Pair("hello", Int(5)),
                `Pair("hello", Int(5))` );
}

@system unittest
{
    // union formatting without toString
    union U1
    {
        int n;
        string s;
    }
    U1 u1;
    formatTest( u1, "U1" );

    // union formatting with toString
    union U2
    {
        int n;
        string s;
        string toString() const { return s; }
    }
    U2 u2;
    u2.s = "hello";
    formatTest( u2, "hello" );
}

@system unittest
{
    import std.array;
    // https://issues.dlang.org/show_bug.cgi?id=7230
    static struct Bug7230
    {
        string s = "hello";
        union {
            string a;
            int b;
            double c;
        }
        long x = 10;
    }

    Bug7230 bug;
    bug.b = 123;

    FormatSpec!char f;
    auto w = appender!(char[])();
    formatValue(w, bug, f);
    assert(w.data == `Bug7230("hello", #{overlap a, b, c}, 10)`);
}

@safe unittest
{
    import std.array : appender;
    static struct S{ @disable this(this); }
    S s;

    FormatSpec!char f;
    auto w = appender!string();
    formatValue(w, s, f);
    assert(w.data == "S()");
}

@safe unittest
{
    //struct Foo { @disable string toString(); }
    //Foo foo;

    interface Bar { @disable string toString(); }
    Bar bar;

    import std.array : appender;
    auto w = appender!(char[])();
    FormatSpec!char f;

    // NOTE: structs cant be tested : the assertion is correct so compilation
    // continues and fails when trying to link the unimplemented toString.
    //static assert(!__traits(compiles, formatValue(w, foo, f)));
    static assert(!__traits(compiles, formatValue(w, bar, f)));
}

/*
    `enum`s are formatted like their base value
 */
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, T val, scope const ref FormatSpec!Char f)
if (is(T == enum))
{
    if (f.spec == 's')
    {
        foreach (i, e; EnumMembers!T)
        {
            if (val == e)
            {
                formatValueImpl(w, __traits(allMembers, T)[i], f);
                return;
            }
        }

        import std.array : appender;
        auto w2 = appender!string();

        // val is not a member of T, output cast(T) rawValue instead.
        put(w2, "cast(" ~ T.stringof ~ ")");
        static assert(!is(OriginalType!T == T), "OriginalType!" ~ T.stringof ~
                "must not be equal to " ~ T.stringof);

        FormatSpec!Char f2 = f;
        f2.width = 0;
        formatValueImpl(w2, cast(OriginalType!T) val, f2);
        writeAligned(w, w2.data, f);
        return;
    }
    formatValueImpl(w, cast(OriginalType!T) val, f);
}

@safe unittest
{
    enum A { first, second, third }
    formatTest( A.second, "second" );
    formatTest( cast(A) 72, "cast(A)72" );
}
@safe unittest
{
    enum A : string { one = "uno", two = "dos", three = "tres" }
    formatTest( A.three, "three" );
    formatTest( cast(A)"mill\&oacute;n", "cast(A)mill\&oacute;n" );
}
@safe unittest
{
    enum A : bool { no, yes }
    formatTest( A.yes, "yes" );
    formatTest( A.no, "no" );
}
@safe unittest
{
    // Test for bug 6892
    enum Foo { A = 10 }
    formatTest("%s",    Foo.A, "A");
    formatTest(">%4s<", Foo.A, ">   A<");
    formatTest("%04d",  Foo.A, "0010");
    formatTest("%+2u",  Foo.A, "+10");
    formatTest("%02x",  Foo.A, "0a");
    formatTest("%3o",   Foo.A, " 12");
    formatTest("%b",    Foo.A, "1010");
}

@safe pure unittest
{
    enum A { one, two, three }

    string t1 = format("[%6s] [%-6s]", A.one, A.one);
    assert(t1 == "[   one] [one   ]");
    string t2 = format("[%10s] [%-10s]", cast(A) 10, cast(A) 10);
    assert(t2 == "[ cast(A)" ~ "10] [cast(A)" ~ "10 ]"); // due to bug in style checker
}

/*
   Pointers are formatted as hex integers.
 */
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, scope T val, scope const ref FormatSpec!Char f)
if (isPointer!T && !is(T == enum) && !hasToString!(T, Char))
{
    static if (is(typeof({ shared const void* p = val; })))
        alias SharedOf(T) = shared(T);
    else
        alias SharedOf(T) = T;

    const SharedOf!(void*) p = val;
    const pnum = ()@trusted{ return cast(ulong) p; }();

    if (f.spec == 's')
    {
        if (p is null)
        {
            writeAligned(w, "null", f);
            return;
        }
        FormatSpec!Char fs = f; // fs is copy for change its values.
        fs.spec = 'X';
        formatValueImpl(w, pnum, fs);
    }
    else
    {
        enforceFmt(f.spec == 'X' || f.spec == 'x',
           "Expected one of %s, %x or %X for pointer type.");
        formatValueImpl(w, pnum, f);
    }
}

@safe pure unittest
{
    int* p;

    string t1 = format("[%6s] [%-6s]", p, p);
    assert(t1 == "[  null] [null  ]");
}

/*
   SIMD vectors are formatted as arrays.
 */
private void formatValueImpl(Writer, V, Char)(auto ref Writer w, V val, scope const ref FormatSpec!Char f)
if (isSIMDVector!V)
{
    formatValueImpl(w, val.array, f);
}

@safe unittest
{
    import core.simd;
    static if (is(float4))
    {
        version (X86)
        {
            version (OSX) {/* https://issues.dlang.org/show_bug.cgi?id=17823 */}
        }
        else
        {
            float4 f;
            f.array[0] = 1;
            f.array[1] = 2;
            f.array[2] = 3;
            f.array[3] = 4;
            formatTest(f, "[1, 2, 3, 4]");
        }
    }
}

@safe pure unittest
{
    int* p = null;
    formatTest( p, "null" );

    auto q = ()@trusted{ return cast(void*) 0xFFEECCAA; }();
    formatTest( q, "FFEECCAA" );
}

// https://issues.dlang.org/show_bug.cgi?id=11782
@safe pure unittest
{
    import std.range : iota;

    auto a = iota(0, 10);
    auto b = iota(0, 10);
    auto p = ()@trusted{ auto p = &a; return p; }();

    assert(format("%s",p) != format("%s",b));
}

@system pure unittest
{
    // Test for https://issues.dlang.org/show_bug.cgi?id=7869
    struct S
    {
        string toString() const { return ""; }
    }
    S* p = null;
    formatTest( p, "null" );

    S* q = cast(S*) 0xFFEECCAA;
    formatTest( q, "FFEECCAA" );
}

// https://issues.dlang.org/show_bug.cgi?id=8186
@system unittest
{
    class B
    {
        int*a;
        this(){ a = new int; }
        alias a this;
    }
    formatTest( B.init, "null" );
}

// https://issues.dlang.org/show_bug.cgi?id=9336
@system pure unittest
{
    shared int i;
    format("%s", &i);
}

// https://issues.dlang.org/show_bug.cgi?id=11778
@system pure unittest
{
    int* p = null;
    assertThrown!FormatException(format("%d", p));
    assertThrown!FormatException(format("%04d", p + 2));
}

// https://issues.dlang.org/show_bug.cgi?id=12505
@safe pure unittest
{
    void* p = null;
    formatTest( "%08X", p, "00000000" );
}

/*
   Delegates are formatted by `ReturnType delegate(Parameters) FunctionAttributes`
 */
private void formatValueImpl(Writer, T, Char)(auto ref Writer w, scope T, scope const ref FormatSpec!Char f)
if (isDelegate!T)
{
    formatValueImpl(w, T.stringof, f);
}

@safe unittest
{
    void func() @system { __gshared int x; ++x; throw new Exception("msg"); }
    version (linux) formatTest( &func, "void delegate() @system" );
}

@safe pure unittest
{
    int[] a = [ 1, 3, 2 ];
    formatTest( "testing %(%s & %) embedded", a,
                "testing 1 & 3 & 2 embedded");
    formatTest( "testing %((%s) %)) wyda3", a,
                "testing (1) (3) (2) wyda3" );

    int[0] empt = [];
    formatTest( "(%s)", empt,
                "([])" );
}

// Fix for https://issues.dlang.org/show_bug.cgi?id=1591
private int getNthInt(string kind, A...)(uint index, A args)
{
    return getNth!(kind, isIntegral,int)(index, args);
}

private T getNth(string kind, alias Condition, T, A...)(uint index, A args)
{
    import std.conv : text, to;

    switch (index)
    {
        foreach (n, _; A)
        {
            case n:
                static if (Condition!(typeof(args[n])))
                {
                    return to!T(args[n]);
                }
                else
                {
                    throw new FormatException(
                        text(kind, " expected, not ", typeof(args[n]).stringof,
                            " for argument #", index + 1));
                }
        }
        default:
            throw new FormatException(
                text("Missing ", kind, " argument"));
    }
}

@safe unittest
{
    // width/precision
    assert(collectExceptionMsg!FormatException(format("%*.d", 5.1, 2))
        == "integer width expected, not double for argument #1");
    assert(collectExceptionMsg!FormatException(format("%-1*.d", 5.1, 2))
        == "integer width expected, not double for argument #1");

    assert(collectExceptionMsg!FormatException(format("%.*d", '5', 2))
        == "integer precision expected, not char for argument #1");
    assert(collectExceptionMsg!FormatException(format("%-1.*d", 4.7, 3))
        == "integer precision expected, not double for argument #1");
    assert(collectExceptionMsg!FormatException(format("%.*d", 5))
        == "Orphan format specifier: %d");
    assert(collectExceptionMsg!FormatException(format("%*.*d", 5))
        == "Missing integer precision argument");

    // separatorCharPos
    assert(collectExceptionMsg!FormatException(format("%,?d", 5))
        == "separator character expected, not int for argument #1");
    assert(collectExceptionMsg!FormatException(format("%,?d", '?'))
        == "Orphan format specifier: %d");
    assert(collectExceptionMsg!FormatException(format("%.*,*?d", 5))
        == "Missing separator digit width argument");
}

/* ======================== Unit Tests ====================================== */

version (StdUnittest)
private void formatTest(T)(T val, string expected, size_t ln = __LINE__, string fn = __FILE__)
{
    import core.exception : AssertError;
    import std.array : appender;
    import std.conv : text;
    FormatSpec!char f;
    auto w = appender!string();
    formatValue(w, val, f);
    enforce!AssertError(
            w.data == expected,
            text("expected = `", expected, "`, result = `", w.data, "`"), fn, ln);
}

version (StdUnittest)
private void formatTest(T)(string fmt, T val, string expected, size_t ln = __LINE__, string fn = __FILE__) @safe
{
    import core.exception : AssertError;
    import std.array : appender;
    import std.conv : text;
    auto w = appender!string();
    formattedWrite(w, fmt, val);
    enforce!AssertError(
            w.data == expected,
            text("expected = `", expected, "`, result = `", w.data, "`"), fn, ln);
}

version (StdUnittest)
private void formatTest(T)(T val, string[] expected, size_t ln = __LINE__, string fn = __FILE__)
{
    import core.exception : AssertError;
    import std.array : appender;
    import std.conv : text;
    FormatSpec!char f;
    auto w = appender!string();
    formatValue(w, val, f);
    foreach (cur; expected)
    {
        if (w.data == cur) return;
    }
    enforce!AssertError(
            false,
            text("expected one of `", expected, "`, result = `", w.data, "`"), fn, ln);
}

version (StdUnittest)
private void formatTest(T)(string fmt, T val, string[] expected, size_t ln = __LINE__, string fn = __FILE__) @safe
{
    import core.exception : AssertError;
    import std.array : appender;
    import std.conv : text;
    auto w = appender!string();
    formattedWrite(w, fmt, val);
    foreach (cur; expected)
    {
        if (w.data == cur) return;
    }
    enforce!AssertError(
            false,
            text("expected one of `", expected, "`, result = `", w.data, "`"), fn, ln);
}

@safe /*pure*/ unittest     // formatting floating point values is now impure
{
    import std.array;

    auto stream = appender!string();
    formattedWrite(stream, "%s", 1.1);
    assert(stream.data == "1.1", stream.data);
}

@safe pure unittest
{
    import std.algorithm;
    import std.array;

    auto stream = appender!string();
    formattedWrite(stream, "%s", map!"a*a"([2, 3, 5]));
    assert(stream.data == "[4, 9, 25]", stream.data);

    // Test shared data.
    stream = appender!string();
    shared int s = 6;
    formattedWrite(stream, "%s", s);
    assert(stream.data == "6");
}

@safe pure unittest
{
    import std.array;
    auto stream = appender!string();
    formattedWrite(stream, "%u", 42);
    assert(stream.data == "42", stream.data);
}

@safe pure unittest
{
    // testing raw writes
    import std.array;
    auto w = appender!(char[])();
    uint a = 0x02030405;
    formattedWrite(w, "%+r", a);
    assert(w.data.length == 4 && w.data[0] == 2 && w.data[1] == 3
        && w.data[2] == 4 && w.data[3] == 5);
    w.clear();
    formattedWrite(w, "%-r", a);
    assert(w.data.length == 4 && w.data[0] == 5 && w.data[1] == 4
        && w.data[2] == 3 && w.data[3] == 2);
}

@safe pure unittest
{
    // testing positional parameters
    import std.array;
    auto w = appender!(char[])();
    formattedWrite(w,
            "Numbers %2$s and %1$s are reversed and %1$s%2$s repeated",
            42, 0);
    assert(w.data == "Numbers 0 and 42 are reversed and 420 repeated",
            w.data);
    assert(collectExceptionMsg!FormatException(formattedWrite(w, "%1$s, %3$s", 1, 2))
        == "Positional specifier %3$s index exceeds 2");

    w.clear();
    formattedWrite(w, "asd%s", 23);
    assert(w.data == "asd23", w.data);
    w.clear();
    formattedWrite(w, "%s%s", 23, 45);
    assert(w.data == "2345", w.data);
}

@safe unittest
{
    import core.stdc.string : strlen;
    import std.array : appender;
    import std.conv : text, octal;
    import core.stdc.stdio : snprintf;

    debug(format) printf("std.format.format.unittest\n");

    auto stream = appender!(char[])();
    //goto here;

    formattedWrite(stream,
            "hello world! %s %s ", true, 57, 1_000_000_000, 'x', " foo");
    assert(stream.data == "hello world! true 57 ",
        stream.data);

    stream.clear();
    formattedWrite(stream, "%g %A %s", 1.67, -1.28, float.nan);
    assert(stream.data == "1.67 -0X1.47AE147AE147BP+0 nan",
           stream.data);
    stream.clear();

    formattedWrite(stream, "%x %X", 0x1234AF, 0xAFAFAFAF);
    assert(stream.data == "1234af AFAFAFAF");
    stream.clear();

    formattedWrite(stream, "%b %o", 0x1234AF, 0xAFAFAFAF);
    assert(stream.data == "100100011010010101111 25753727657");
    stream.clear();

    formattedWrite(stream, "%d %s", 0x1234AF, 0xAFAFAFAF);
    assert(stream.data == "1193135 2947526575");
    stream.clear();

    // formattedWrite(stream, "%s", 1.2 + 3.4i);
    // assert(stream.data == "1.2+3.4i");
    // stream.clear();

    formattedWrite(stream, "%a %A", 1.32, 6.78f);
    //formattedWrite(stream, "%x %X", 1.32);
    assert(stream.data == "0x1.51eb851eb851fp+0 0X1.B1EB86P+2");
    stream.clear();

    formattedWrite(stream, "%#06.*f",2,12.345);
    assert(stream.data == "012.35");
    stream.clear();

    formattedWrite(stream, "%#0*.*f",6,2,12.345);
    assert(stream.data == "012.35");
    stream.clear();

    const real constreal = 1;
    formattedWrite(stream, "%g",constreal);
    assert(stream.data == "1");
    stream.clear();

    formattedWrite(stream, "%7.4g:", 12.678);
    assert(stream.data == "  12.68:");
    stream.clear();

    formattedWrite(stream, "%7.4g:", 12.678L);
    assert(stream.data == "  12.68:");
    stream.clear();

    formattedWrite(stream, "%04f|%05d|%#05x|%#5x",-4.0,-10,1,1);
    assert(stream.data == "-4.000000|-0010|0x001|  0x1",
            stream.data);
    stream.clear();

    int i;
    string s;

    i = -10;
    formattedWrite(stream, "%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i);
    assert(stream.data == "-10|-10|-10|-10|-10.0000");
    stream.clear();

    i = -5;
    formattedWrite(stream, "%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i);
    assert(stream.data == "-5| -5|-05|-5|-5.0000");
    stream.clear();

    i = 0;
    formattedWrite(stream, "%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i);
    assert(stream.data == "0|  0|000|0|0.0000");
    stream.clear();

    i = 5;
    formattedWrite(stream, "%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i);
    assert(stream.data == "5|  5|005|5|5.0000");
    stream.clear();

    i = 10;
    formattedWrite(stream, "%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i);
    assert(stream.data == "10| 10|010|10|10.0000");
    stream.clear();

    formattedWrite(stream, "%.0d", 0);
    assert(stream.data == "");
    stream.clear();

    formattedWrite(stream, "%.g", .34);
    assert(stream.data == "0.3");
    stream.clear();

    stream.clear(); formattedWrite(stream, "%.0g", .34);
    assert(stream.data == "0.3");

    stream.clear(); formattedWrite(stream, "%.2g", .34);
    assert(stream.data == "0.34");

    stream.clear(); formattedWrite(stream, "%0.0008f", 1e-08);
    assert(stream.data == "0.00000001");

    stream.clear(); formattedWrite(stream, "%0.0008f", 1e-05);
    assert(stream.data == "0.00001000");

    //return;
    //core.stdc.stdio.fwrite(stream.data.ptr, stream.data.length, 1, stderr);

    s = "helloworld";
    string r;
    stream.clear(); formattedWrite(stream, "%.2s", s[0 .. 5]);
    assert(stream.data == "he");
    stream.clear(); formattedWrite(stream, "%.20s", s[0 .. 5]);
    assert(stream.data == "hello");
    stream.clear(); formattedWrite(stream, "%8s", s[0 .. 5]);
    assert(stream.data == "   hello");

    byte[] arrbyte = new byte[4];
    arrbyte[0] = 100;
    arrbyte[1] = -99;
    arrbyte[3] = 0;
    stream.clear(); formattedWrite(stream, "%s", arrbyte);
    assert(stream.data == "[100, -99, 0, 0]", stream.data);

    ubyte[] arrubyte = new ubyte[4];
    arrubyte[0] = 100;
    arrubyte[1] = 200;
    arrubyte[3] = 0;
    stream.clear(); formattedWrite(stream, "%s", arrubyte);
    assert(stream.data == "[100, 200, 0, 0]", stream.data);

    short[] arrshort = new short[4];
    arrshort[0] = 100;
    arrshort[1] = -999;
    arrshort[3] = 0;
    stream.clear(); formattedWrite(stream, "%s", arrshort);
    assert(stream.data == "[100, -999, 0, 0]");
    stream.clear(); formattedWrite(stream, "%s",arrshort);
    assert(stream.data == "[100, -999, 0, 0]");

    ushort[] arrushort = new ushort[4];
    arrushort[0] = 100;
    arrushort[1] = 20_000;
    arrushort[3] = 0;
    stream.clear(); formattedWrite(stream, "%s", arrushort);
    assert(stream.data == "[100, 20000, 0, 0]");

    int[] arrint = new int[4];
    arrint[0] = 100;
    arrint[1] = -999;
    arrint[3] = 0;
    stream.clear(); formattedWrite(stream, "%s", arrint);
    assert(stream.data == "[100, -999, 0, 0]");
    stream.clear(); formattedWrite(stream, "%s",arrint);
    assert(stream.data == "[100, -999, 0, 0]");

    long[] arrlong = new long[4];
    arrlong[0] = 100;
    arrlong[1] = -999;
    arrlong[3] = 0;
    stream.clear(); formattedWrite(stream, "%s", arrlong);
    assert(stream.data == "[100, -999, 0, 0]");
    stream.clear(); formattedWrite(stream, "%s",arrlong);
    assert(stream.data == "[100, -999, 0, 0]");

    ulong[] arrulong = new ulong[4];
    arrulong[0] = 100;
    arrulong[1] = 999;
    arrulong[3] = 0;
    stream.clear(); formattedWrite(stream, "%s", arrulong);
    assert(stream.data == "[100, 999, 0, 0]");

    string[] arr2 = new string[4];
    arr2[0] = "hello";
    arr2[1] = "world";
    arr2[3] = "foo";
    stream.clear(); formattedWrite(stream, "%s", arr2);
    assert(stream.data == `["hello", "world", "", "foo"]`, stream.data);

    stream.clear(); formattedWrite(stream, "%.8d", 7);
    assert(stream.data == "00000007");

    stream.clear(); formattedWrite(stream, "%.8x", 10);
    assert(stream.data == "0000000a");

    stream.clear(); formattedWrite(stream, "%-3d", 7);
    assert(stream.data == "7  ");

    stream.clear(); formattedWrite(stream, "%*d", -3, 7);
    assert(stream.data == "7  ");

    stream.clear(); formattedWrite(stream, "%.*d", -3, 7);
    //writeln(stream.data);
    assert(stream.data == "7");

    stream.clear(); formattedWrite(stream, "%s", "abc"c);
    assert(stream.data == "abc");
    stream.clear(); formattedWrite(stream, "%s", "def"w);
    assert(stream.data == "def", text(stream.data.length));
    stream.clear(); formattedWrite(stream, "%s", "ghi"d);
    assert(stream.data == "ghi");

here:
    @trusted void* deadBeef() { return cast(void*) 0xDEADBEEF; }
    stream.clear(); formattedWrite(stream, "%s", deadBeef());
    assert(stream.data == "DEADBEEF", stream.data);

    stream.clear(); formattedWrite(stream, "%#x", 0xabcd);
    assert(stream.data == "0xabcd");
    stream.clear(); formattedWrite(stream, "%#X", 0xABCD);
    assert(stream.data == "0XABCD");

    stream.clear(); formattedWrite(stream, "%#o", octal!12345);
    assert(stream.data == "012345");
    stream.clear(); formattedWrite(stream, "%o", 9);
    assert(stream.data == "11");

    stream.clear(); formattedWrite(stream, "%+d", 123);
    assert(stream.data == "+123");
    stream.clear(); formattedWrite(stream, "%+d", -123);
    assert(stream.data == "-123");
    stream.clear(); formattedWrite(stream, "% d", 123);
    assert(stream.data == " 123");
    stream.clear(); formattedWrite(stream, "% d", -123);
    assert(stream.data == "-123");

    stream.clear(); formattedWrite(stream, "%%");
    assert(stream.data == "%");

    stream.clear(); formattedWrite(stream, "%d", true);
    assert(stream.data == "1");
    stream.clear(); formattedWrite(stream, "%d", false);
    assert(stream.data == "0");

    stream.clear(); formattedWrite(stream, "%d", 'a');
    assert(stream.data == "97", stream.data);
    wchar wc = 'a';
    stream.clear(); formattedWrite(stream, "%d", wc);
    assert(stream.data == "97");
    dchar dc = 'a';
    stream.clear(); formattedWrite(stream, "%d", dc);
    assert(stream.data == "97");

    byte b = byte.max;
    stream.clear(); formattedWrite(stream, "%x", b);
    assert(stream.data == "7f");
    stream.clear(); formattedWrite(stream, "%x", ++b);
    assert(stream.data == "80");
    stream.clear(); formattedWrite(stream, "%x", ++b);
    assert(stream.data == "81");

    short sh = short.max;
    stream.clear(); formattedWrite(stream, "%x", sh);
    assert(stream.data == "7fff");
    stream.clear(); formattedWrite(stream, "%x", ++sh);
    assert(stream.data == "8000");
    stream.clear(); formattedWrite(stream, "%x", ++sh);
    assert(stream.data == "8001");

    i = int.max;
    stream.clear(); formattedWrite(stream, "%x", i);
    assert(stream.data == "7fffffff");
    stream.clear(); formattedWrite(stream, "%x", ++i);
    assert(stream.data == "80000000");
    stream.clear(); formattedWrite(stream, "%x", ++i);
    assert(stream.data == "80000001");

    stream.clear(); formattedWrite(stream, "%x", 10);
    assert(stream.data == "a");
    stream.clear(); formattedWrite(stream, "%X", 10);
    assert(stream.data == "A");
    stream.clear(); formattedWrite(stream, "%x", 15);
    assert(stream.data == "f");
    stream.clear(); formattedWrite(stream, "%X", 15);
    assert(stream.data == "F");

    @trusted void ObjectTest()
    {
        Object c = null;
        stream.clear(); formattedWrite(stream, "%s", c);
        assert(stream.data == "null");
    }
    ObjectTest();

    enum TestEnum
    {
        Value1, Value2
    }
    stream.clear(); formattedWrite(stream, "%s", TestEnum.Value2);
    assert(stream.data == "Value2", stream.data);
    stream.clear(); formattedWrite(stream, "%s", cast(TestEnum) 5);
    assert(stream.data == "cast(TestEnum)5", stream.data);

    //immutable(char[5])[int] aa = ([3:"hello", 4:"betty"]);
    //stream.clear(); formattedWrite(stream, "%s", aa.values);
    //core.stdc.stdio.fwrite(stream.data.ptr, stream.data.length, 1, stderr);
    //assert(stream.data == "[[h,e,l,l,o],[b,e,t,t,y]]");
    //stream.clear(); formattedWrite(stream, "%s", aa);
    //assert(stream.data == "[3:[h,e,l,l,o],4:[b,e,t,t,y]]");

    static const dchar[] ds = ['a','b'];
    for (int j = 0; j < ds.length; ++j)
    {
        stream.clear(); formattedWrite(stream, " %d", ds[j]);
        if (j == 0)
            assert(stream.data == " 97");
        else
            assert(stream.data == " 98");
    }

    stream.clear(); formattedWrite(stream, "%.-3d", 7);
    assert(stream.data == "7", ">" ~ stream.data ~ "<");
}

@safe unittest
{
    import std.array;
    import std.stdio;

    immutable(char[5])[int] aa = ([3:"hello", 4:"betty"]);
    assert(aa[3] == "hello");
    assert(aa[4] == "betty");

    auto stream = appender!(char[])();
    alias AllNumerics =
        AliasSeq!(byte, ubyte, short, ushort, int, uint, long, ulong,
                  float, double, real);
    foreach (T; AllNumerics)
    {
        T value = 1;
        stream.clear();
        formattedWrite(stream, "%s", value);
        assert(stream.data == "1");
    }

    stream.clear();
    formattedWrite(stream, "%s", aa);
}

@system unittest
{
    string s = "hello!124:34.5";
    string a;
    int b;
    double c;
    formattedRead(s, "%s!%s:%s", &a, &b, &c);
    assert(a == "hello" && b == 124 && c == 34.5);
}

version (StdUnittest)
private void formatReflectTest(T)(ref T val, string fmt, string formatted, string fn = __FILE__, size_t ln = __LINE__)
{
    import core.exception : AssertError;
    import std.array : appender;
    auto w = appender!string();
    formattedWrite(w, fmt, val);

    auto input = w.data;
    enforce!AssertError(
            input == formatted,
            input, fn, ln);

    T val2;
    formattedRead(input, fmt, &val2);
    static if (isAssociativeArray!T)
    if (__ctfe)
    {
        alias aa1 = val;
        alias aa2 = val2;
        assert(aa1 == aa2);

        assert(aa1.length == aa2.length);

        assert(aa1.keys == aa2.keys);

        assert(aa1.values == aa2.values);
        assert(aa1.values.length == aa2.values.length);
        foreach (i; 0 .. aa1.values.length)
            assert(aa1.values[i] == aa2.values[i]);

        foreach (i, key; aa1.keys)
            assert(aa1.values[i] == aa1[key]);
        foreach (i, key; aa2.keys)
            assert(aa2.values[i] == aa2[key]);
        return;
    }
    enforce!AssertError(
            val == val2,
            input, fn, ln);
}

version (StdUnittest)
private void formatReflectTest(T)(ref T val, string fmt, string[] formatted, string fn = __FILE__, size_t ln = __LINE__)
{
    import core.exception : AssertError;
    import std.array : appender;
    auto w = appender!string();
    formattedWrite(w, fmt, val);

    auto input = w.data;

    foreach (cur; formatted)
    {
        if (input == cur) return;
    }
    enforce!AssertError(
            false,
            input,
            fn,
            ln);

    T val2;
    formattedRead(input, fmt, &val2);
    static if (isAssociativeArray!T)
    if (__ctfe)
    {
        alias aa1 = val;
        alias aa2 = val2;
        assert(aa1 == aa2);

        assert(aa1.length == aa2.length);

        assert(aa1.keys == aa2.keys);

        assert(aa1.values == aa2.values);
        assert(aa1.values.length == aa2.values.length);
        foreach (i; 0 .. aa1.values.length)
            assert(aa1.values[i] == aa2.values[i]);

        foreach (i, key; aa1.keys)
            assert(aa1.values[i] == aa1[key]);
        foreach (i, key; aa2.keys)
            assert(aa2.values[i] == aa2[key]);
        return;
    }
    enforce!AssertError(
            val == val2,
            input, fn, ln);
}

@system unittest
{
    void booleanTest()
    {
        auto b = true;
        formatReflectTest(b, "%s",  `true`);
        formatReflectTest(b, "%b",  `1`);
        formatReflectTest(b, "%o",  `1`);
        formatReflectTest(b, "%d",  `1`);
        formatReflectTest(b, "%u",  `1`);
        formatReflectTest(b, "%x",  `1`);
    }

    void integerTest()
    {
        auto n = 127;
        formatReflectTest(n, "%s",  `127`);
        formatReflectTest(n, "%b",  `1111111`);
        formatReflectTest(n, "%o",  `177`);
        formatReflectTest(n, "%d",  `127`);
        formatReflectTest(n, "%u",  `127`);
        formatReflectTest(n, "%x",  `7f`);
    }

    void floatingTest()
    {
        auto f = 3.14;
        formatReflectTest(f, "%s",  `3.14`);
        version (MinGW)
            formatReflectTest(f, "%e",  `3.140000e+000`);
        else
            formatReflectTest(f, "%e",  `3.140000e+00`);
        formatReflectTest(f, "%f",  `3.140000`);
        formatReflectTest(f, "%g",  `3.14`);
    }

    void charTest()
    {
        auto c = 'a';
        formatReflectTest(c, "%s",  `a`);
        formatReflectTest(c, "%c",  `a`);
        formatReflectTest(c, "%b",  `1100001`);
        formatReflectTest(c, "%o",  `141`);
        formatReflectTest(c, "%d",  `97`);
        formatReflectTest(c, "%u",  `97`);
        formatReflectTest(c, "%x",  `61`);
    }

    void strTest()
    {
        auto s = "hello";
        formatReflectTest(s, "%s",                      `hello`);
        formatReflectTest(s, "%(%c,%)",                 `h,e,l,l,o`);
        formatReflectTest(s, "%(%s,%)",                 `'h','e','l','l','o'`);
        formatReflectTest(s, "[%(<%c>%| $ %)]",         `[<h> $ <e> $ <l> $ <l> $ <o>]`);
    }

    void daTest()
    {
        auto a = [1,2,3,4];
        formatReflectTest(a, "%s",                      `[1, 2, 3, 4]`);
        formatReflectTest(a, "[%(%s; %)]",              `[1; 2; 3; 4]`);
        formatReflectTest(a, "[%(<%s>%| $ %)]",         `[<1> $ <2> $ <3> $ <4>]`);
    }

    void saTest()
    {
        int[4] sa = [1,2,3,4];
        formatReflectTest(sa, "%s",                     `[1, 2, 3, 4]`);
        formatReflectTest(sa, "[%(%s; %)]",             `[1; 2; 3; 4]`);
        formatReflectTest(sa, "[%(<%s>%| $ %)]",        `[<1> $ <2> $ <3> $ <4>]`);
    }

    void aaTest()
    {
        auto aa = [1:"hello", 2:"world"];
        formatReflectTest(aa, "%s",                     [`[1:"hello", 2:"world"]`, `[2:"world", 1:"hello"]`]);
        formatReflectTest(aa, "[%(%s->%s, %)]",         [`[1->"hello", 2->"world"]`, `[2->"world", 1->"hello"]`]);
        formatReflectTest(aa, "{%([%s=%(%c%)]%|; %)}",  [`{[1=hello]; [2=world]}`, `{[2=world]; [1=hello]}`]);
    }

    import std.exception;
    assertCTFEable!(
    {
        booleanTest();
        integerTest();
        if (!__ctfe) floatingTest();    // snprintf
        charTest();
        strTest();
        daTest();
        saTest();
        aaTest();
        return true;
    });
}

//------------------------------------------------------------------------------
private void skipData(Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
{
    import std.ascii : isDigit;
    import std.conv : text;

    switch (spec.spec)
    {
        case 'c': input.popFront(); break;
        case 'd':
            if (input.front == '+' || input.front == '-') input.popFront();
            goto case 'u';
        case 'u':
            while (!input.empty && isDigit(input.front)) input.popFront();
            break;
        default:
            assert(false,
                    text("Format specifier not understood: %", spec.spec));
    }
}

private template acceptedSpecs(T)
{
         static if (isIntegral!T)       enum acceptedSpecs = "bdosuxX";
    else static if (isFloatingPoint!T)  enum acceptedSpecs = "seEfgG";
    else static if (isSomeChar!T)       enum acceptedSpecs = "bcdosuxX";    // integral + 'c'
    else                                enum acceptedSpecs = "";
}

/**
 * Reads a value from the given _input range according to spec
 * and returns it as type `T`.
 *
 * Params:
 *     T = the type to return
 *     input = the _input range to read from
 *     spec = the `FormatSpec` to use when reading from `input`
 * Returns:
 *     A value from `input` of type `T`
 * Throws:
 *     A `FormatException` if `spec` cannot read a type `T`
 * See_Also:
 *     $(REF parse, std, conv) and $(REF to, std, conv)
 */
T unformatValue(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
{
    return unformatValueImpl!T(input, spec);
}

/// Booleans
@safe pure unittest
{
    auto str = "false";
    auto spec = singleSpec("%s");
    assert(unformatValue!bool(str, spec) == false);

    str = "1";
    spec = singleSpec("%d");
    assert(unformatValue!bool(str, spec));
}

/// Null values
@safe pure unittest
{
    auto str = "null";
    auto spec = singleSpec("%s");
    assert(str.unformatValue!(typeof(null))(spec) == null);
}

/// Integrals
@safe pure unittest
{
    auto str = "123";
    auto spec = singleSpec("%s");
    assert(str.unformatValue!int(spec) == 123);

    str = "ABC";
    spec = singleSpec("%X");
    assert(str.unformatValue!int(spec) == 2748);

    str = "11610";
    spec = singleSpec("%o");
    assert(str.unformatValue!int(spec) == 5000);
}

/// Floating point numbers
@safe pure unittest
{
    import std.math : approxEqual;

    auto str = "123.456";
    auto spec = singleSpec("%s");
    assert(str.unformatValue!double(spec).approxEqual(123.456));
}

/// Character input ranges
@safe pure unittest
{
    auto str = "aaa";
    auto spec = singleSpec("%s");
    assert(str.unformatValue!char(spec) == 'a');

    // Using a numerical format spec reads a Unicode value from a string
    str = "65";
    spec = singleSpec("%d");
    assert(str.unformatValue!char(spec) == 'A');

    str = "41";
    spec = singleSpec("%x");
    assert(str.unformatValue!char(spec) == 'A');

    str = "10003";
    spec = singleSpec("%d");
    assert(str.unformatValue!dchar(spec) == '✓');
}

/// Arrays and static arrays
@safe pure unittest
{
    string str = "aaa";
    auto spec = singleSpec("%s");
    assert(str.unformatValue!(dchar[])(spec) == "aaa"d);

    str = "aaa";
    spec = singleSpec("%s");
    dchar[3] ret = ['a', 'a', 'a'];
    assert(str.unformatValue!(dchar[3])(spec) == ret);

    str = "[1, 2, 3, 4]";
    spec = singleSpec("%s");
    assert(str.unformatValue!(int[])(spec) == [1, 2, 3, 4]);

    str = "[1, 2, 3, 4]";
    spec = singleSpec("%s");
    int[4] ret2 = [1, 2, 3, 4];
    assert(str.unformatValue!(int[4])(spec) == ret2);
}

/// Associative arrays
@safe pure unittest
{
    auto str = `["one": 1, "two": 2]`;
    auto spec = singleSpec("%s");
    assert(str.unformatValue!(int[string])(spec) == ["one": 1, "two": 2]);
}

// https://issues.dlang.org/show_bug.cgi?id=7241
@safe pure unittest
{
    string input = "a";
    auto spec = FormatSpec!char("%s");
    spec.readUpToNextSpec(input);
    auto result = unformatValue!(dchar[1])(input, spec);
    assert(result[0] == 'a');
}

private T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
if (isInputRange!Range && is(Unqual!T == bool))
{
    import std.algorithm.searching : find;
    import std.conv : parse, text;

    if (spec.spec == 's') return parse!T(input);

    enforceFmt(find(acceptedSpecs!long, spec.spec).length,
            text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof));

    return unformatValue!long(input, spec) != 0;
}

private T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
if (isInputRange!Range && is(T == typeof(null)))
{
    import std.conv : parse, text;
    enforceFmt(spec.spec == 's',
            text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof));

    return parse!T(input);
}

/// ditto
private T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
if (isInputRange!Range && isIntegral!T && !is(T == enum) && isSomeChar!(ElementType!Range))
{

    import std.algorithm.searching : find;
    import std.conv : parse, text;

    if (spec.spec == 'r')
    {
        static if (is(Unqual!(ElementEncodingType!Range) == char)
                || is(Unqual!(ElementEncodingType!Range) == byte)
                || is(Unqual!(ElementEncodingType!Range) == ubyte))
            return rawRead!T(input);
        else
            throw new FormatException(
                "The raw read specifier %r may only be used with narrow strings and ranges of bytes."
            );
    }

    enforceFmt(find(acceptedSpecs!T, spec.spec).length,
            text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof));

    enforceFmt(spec.width == 0, "Parsing integers with a width specification is not implemented");   // TODO

    immutable uint base =
        spec.spec == 'x' || spec.spec == 'X' ? 16 :
        spec.spec == 'o' ? 8 :
        spec.spec == 'b' ? 2 :
        spec.spec == 's' || spec.spec == 'd' || spec.spec == 'u' ? 10 : 0;
    assert(base != 0, "base must be not equal to zero");

    return parse!T(input, base);

}

/// ditto
private T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
if (isFloatingPoint!T && !is(T == enum) && isInputRange!Range
    && isSomeChar!(ElementType!Range)&& !is(Range == enum))
{
    import std.algorithm.searching : find;
    import std.conv : parse, text;

    if (spec.spec == 'r')
    {
        static if (is(Unqual!(ElementEncodingType!Range) == char)
                || is(Unqual!(ElementEncodingType!Range) == byte)
                || is(Unqual!(ElementEncodingType!Range) == ubyte))
            return rawRead!T(input);
        else
            throw new FormatException(
                "The raw read specifier %r may only be used with narrow strings and ranges of bytes."
            );
    }

    enforceFmt(find(acceptedSpecs!T, spec.spec).length,
            text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof));

    return parse!T(input);
}

/// ditto
private T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
if (isInputRange!Range && isSomeChar!T && !is(T == enum) && isSomeChar!(ElementType!Range))
{
    import std.algorithm.searching : find;
    import std.conv : to, text;
    if (spec.spec == 's' || spec.spec == 'c')
    {
        auto result = to!T(input.front);
        input.popFront();
        return result;
    }
    enforceFmt(find(acceptedSpecs!T, spec.spec).length,
            text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof));

    static if (T.sizeof == 1)
        return unformatValue!ubyte(input, spec);
    else static if (T.sizeof == 2)
        return unformatValue!ushort(input, spec);
    else static if (T.sizeof == 4)
        return unformatValue!uint(input, spec);
    else
        static assert(false, T.stringof ~ ".sizeof must be 1, 2, or 4 not " ~
                to!string(T.sizeof));
}

/// ditto
private T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char fmt)
if (isInputRange!Range && is(StringTypeOf!T) && !isAggregateType!T && !is(T == enum))
{
    import std.conv : text;

    const spec = fmt.spec;
    if (spec == '(')
    {
        return unformatRange!T(input, fmt);
    }
    enforceFmt(spec == 's',
            text("Wrong unformat specifier '%", spec , "' for ", T.stringof));

    static if (isStaticArray!T)
    {
        T result;
        auto app = result[];
    }
    else
    {
        import std.array : appender;
        auto app = appender!T();
    }
    if (fmt.trailing.empty)
    {
        for (; !input.empty; input.popFront())
        {
            static if (isStaticArray!T)
                if (app.empty)
                    break;
            app.put(input.front);
        }
    }
    else
    {
        immutable end = fmt.trailing.front;
        for (; !input.empty && input.front != end; input.popFront())
        {
            static if (isStaticArray!T)
                if (app.empty)
                    break;
            app.put(input.front);
        }
    }
    static if (isStaticArray!T)
    {
        enforceFmt(app.empty, "need more input");
        return result;
    }
    else
        return app.data;
}

/// ditto
private T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char fmt)
if (isInputRange!Range && isArray!T && !is(StringTypeOf!T) && !isAggregateType!T && !is(T == enum))
{
    import std.conv : parse, text;
    const spec = fmt.spec;
    if (spec == '(')
    {
        return unformatRange!T(input, fmt);
    }
    enforceFmt(spec == 's',
            text("Wrong unformat specifier '%", spec , "' for ", T.stringof));

    return parse!T(input);
}

/// ditto
private T unformatValueImpl(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char fmt)
if (isInputRange!Range && isAssociativeArray!T && !is(T == enum))
{
    import std.conv : parse, text;
    const spec = fmt.spec;
    if (spec == '(')
    {
        return unformatRange!T(input, fmt);
    }
    enforceFmt(spec == 's',
            text("Wrong unformat specifier '%", spec , "' for ", T.stringof));

    return parse!T(input);
}

/**
 * Function that performs raw reading. Used by unformatValue
 * for integral and float types.
 */
private T rawRead(T, Range)(ref Range input)
if (is(Unqual!(ElementEncodingType!Range) == char)
    || is(Unqual!(ElementEncodingType!Range) == byte)
    || is(Unqual!(ElementEncodingType!Range) == ubyte))
{
    union X
    {
        ubyte[T.sizeof] raw;
        T typed;
    }
    X x;
    foreach (i; 0 .. T.sizeof)
    {
        static if (isSomeString!Range)
        {
            x.raw[i] = input[0];
            input = input[1 .. $];
        }
        else
        {
            // TODO: recheck this
            x.raw[i] = input.front;
            input.popFront();
        }
    }
    return x.typed;
}

//debug = unformatRange;

private T unformatRange(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
in
{
    const char ss = spec.spec;
    assert(ss == '(', "spec.spec must be '(' not " ~ ss);
}
do
{
    debug (unformatRange) printf("unformatRange:\n");

    T result;
    static if (isStaticArray!T)
    {
        size_t i;
    }

    const(Char)[] cont = spec.trailing;
    for (size_t j = 0; j < spec.trailing.length; ++j)
    {
        if (spec.trailing[j] == '%')
        {
            cont = spec.trailing[0 .. j];
            break;
        }
    }
    debug (unformatRange) printf("\t");
    debug (unformatRange) if (!input.empty) printf("input.front = %c, ", input.front);
    debug (unformatRange) printf("cont = %.*s\n", cast(int) cont.length, cont.ptr);

    bool checkEnd()
    {
        return input.empty || !cont.empty && input.front == cont.front;
    }

    if (!checkEnd())
    {
        for (;;)
        {
            auto fmt = FormatSpec!Char(spec.nested);
            fmt.readUpToNextSpec(input);
            enforceFmt(!input.empty, "Unexpected end of input when parsing range");

            debug (unformatRange) printf("\t) spec = %c, front = %c ", fmt.spec, input.front);
            static if (isStaticArray!T)
            {
                result[i++] = unformatElement!(typeof(T.init[0]))(input, fmt);
            }
            else static if (isDynamicArray!T)
            {
                import std.conv : WideElementType;
                result ~= unformatElement!(WideElementType!T)(input, fmt);
            }
            else static if (isAssociativeArray!T)
            {
                auto key = unformatElement!(typeof(T.init.keys[0]))(input, fmt);
                fmt.readUpToNextSpec(input);        // eat key separator

                result[key] = unformatElement!(typeof(T.init.values[0]))(input, fmt);
            }
            debug (unformatRange) {
            if (input.empty) printf("-> front = [empty] ");
            else             printf("-> front = %c ", input.front);
            }

            static if (isStaticArray!T)
            {
                debug (unformatRange) printf("i = %u < %u\n", i, T.length);
                enforceFmt(i <= T.length, "Too many format specifiers for static array of length %d".format(T.length));
            }

            if (spec.sep !is null)
                fmt.readUpToNextSpec(input);
            auto sep = spec.sep !is null ? spec.sep
                         : fmt.trailing;
            debug (unformatRange) {
            if (!sep.empty && !input.empty) printf("-> %c, sep = %.*s\n", input.front, cast(int) sep.length, sep.ptr);
            else                            printf("\n");
            }

            if (checkEnd())
                break;

            if (!sep.empty && input.front == sep.front)
            {
                while (!sep.empty)
                {
                    enforceFmt(!input.empty, "Unexpected end of input when parsing range separator");
                    enforceFmt(input.front == sep.front, "Unexpected character when parsing range separator");
                    input.popFront();
                    sep.popFront();
                }
                debug (unformatRange) printf("input.front = %c\n", input.front);
            }
        }
    }
    static if (isStaticArray!T)
    {
        enforceFmt(i == T.length, "Too few (%d) format specifiers for static array of length %d".format(i, T.length));
    }
    return result;
}

// Undocumented
T unformatElement(T, Range, Char)(ref Range input, scope const ref FormatSpec!Char spec)
if (isInputRange!Range)
{
    import std.conv : parseElement;
    static if (isSomeString!T)
    {
        if (spec.spec == 's')
        {
            return parseElement!T(input);
        }
    }
    else static if (isSomeChar!T)
    {
        if (spec.spec == 's')
        {
            return parseElement!T(input);
        }
    }

    return unformatValue!T(input, spec);
}

private bool needToSwapEndianess(Char)(scope const ref FormatSpec!Char f)
{
    import std.system : endian, Endian;

    return endian == Endian.littleEndian && f.flPlus
        || endian == Endian.bigEndian && f.flDash;
}

/* ======================== Unit Tests ====================================== */

@system unittest
{
    import std.conv : octal;

    int i;
    string s;

    debug(format) printf("std.format.format.unittest\n");

    s = format("hello world! %s %s %s%s%s", true, 57, 1_000_000_000, 'x', " foo");
    assert(s == "hello world! true 57 1000000000x foo");

    s = format("%s %A %s", 1.67, -1.28, float.nan);
    assert(s == "1.67 -0X1.47AE147AE147BP+0 nan", s);

    s = format("%x %X", 0x1234AF, 0xAFAFAFAF);
    assert(s == "1234af AFAFAFAF");

    s = format("%b %o", 0x1234AF, 0xAFAFAFAF);
    assert(s == "100100011010010101111 25753727657");

    s = format("%d %s", 0x1234AF, 0xAFAFAFAF);
    assert(s == "1193135 2947526575");
}

version (TestComplex)
deprecated
@system unittest
{
        string s = format("%s", 1.2 + 3.4i);
        assert(s == "1.2+3.4i", s);
}

@system unittest
{
    import std.conv : octal;

    string s;
    int i;

    s = format("%#06.*f",2,12.345);
    assert(s == "012.35");

    s = format("%#0*.*f",6,2,12.345);
    assert(s == "012.35");

    s = format("%7.4g:", 12.678);
    assert(s == "  12.68:");

    s = format("%7.4g:", 12.678L);
    assert(s == "  12.68:");

    s = format("%04f|%05d|%#05x|%#5x",-4.0,-10,1,1);
    assert(s == "-4.000000|-0010|0x001|  0x1");

    i = -10;
    s = format("%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i);
    assert(s == "-10|-10|-10|-10|-10.0000");

    i = -5;
    s = format("%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i);
    assert(s == "-5| -5|-05|-5|-5.0000");

    i = 0;
    s = format("%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i);
    assert(s == "0|  0|000|0|0.0000");

    i = 5;
    s = format("%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i);
    assert(s == "5|  5|005|5|5.0000");

    i = 10;
    s = format("%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i);
    assert(s == "10| 10|010|10|10.0000");

    s = format("%.0d", 0);
    assert(s == "");

    s = format("%.g", .34);
    assert(s == "0.3");

    s = format("%.0g", .34);
    assert(s == "0.3");

    s = format("%.2g", .34);
    assert(s == "0.34");

    s = format("%0.0008f", 1e-08);
    assert(s == "0.00000001");

    s = format("%0.0008f", 1e-05);
    assert(s == "0.00001000");

    s = "helloworld";
    string r;
    r = format("%.2s", s[0 .. 5]);
    assert(r == "he");
    r = format("%.20s", s[0 .. 5]);
    assert(r == "hello");
    r = format("%8s", s[0 .. 5]);
    assert(r == "   hello");

    byte[] arrbyte = new byte[4];
    arrbyte[0] = 100;
    arrbyte[1] = -99;
    arrbyte[3] = 0;
    r = format("%s", arrbyte);
    assert(r == "[100, -99, 0, 0]");

    ubyte[] arrubyte = new ubyte[4];
    arrubyte[0] = 100;
    arrubyte[1] = 200;
    arrubyte[3] = 0;
    r = format("%s", arrubyte);
    assert(r == "[100, 200, 0, 0]");

    short[] arrshort = new short[4];
    arrshort[0] = 100;
    arrshort[1] = -999;
    arrshort[3] = 0;
    r = format("%s", arrshort);
    assert(r == "[100, -999, 0, 0]");

    ushort[] arrushort = new ushort[4];
    arrushort[0] = 100;
    arrushort[1] = 20_000;
    arrushort[3] = 0;
    r = format("%s", arrushort);
    assert(r == "[100, 20000, 0, 0]");

    int[] arrint = new int[4];
    arrint[0] = 100;
    arrint[1] = -999;
    arrint[3] = 0;
    r = format("%s", arrint);
    assert(r == "[100, -999, 0, 0]");

    long[] arrlong = new long[4];
    arrlong[0] = 100;
    arrlong[1] = -999;
    arrlong[3] = 0;
    r = format("%s", arrlong);
    assert(r == "[100, -999, 0, 0]");

    ulong[] arrulong = new ulong[4];
    arrulong[0] = 100;
    arrulong[1] = 999;
    arrulong[3] = 0;
    r = format("%s", arrulong);
    assert(r == "[100, 999, 0, 0]");

    string[] arr2 = new string[4];
    arr2[0] = "hello";
    arr2[1] = "world";
    arr2[3] = "foo";
    r = format("%s", arr2);
    assert(r == `["hello", "world", "", "foo"]`);

    r = format("%.8d", 7);
    assert(r == "00000007");
    r = format("%.8x", 10);
    assert(r == "0000000a");

    r = format("%-3d", 7);
    assert(r == "7  ");

    r = format("%-1*d", 4, 3);
    assert(r == "3   ");

    r = format("%*d", -3, 7);
    assert(r == "7  ");

    r = format("%.*d", -3, 7);
    assert(r == "7");

    r = format("%-1.*f", 2, 3.1415);
    assert(r == "3.14");

    r = format("abc"c);
    assert(r == "abc");

    //format() returns the same type as inputted.
    wstring wr;
    wr = format("def"w);
    assert(wr == "def"w);

    dstring dr;
    dr = format("ghi"d);
    assert(dr == "ghi"d);

    void* p = cast(void*) 0xDEADBEEF;
    r = format("%s", p);
    assert(r == "DEADBEEF");

    r = format("%#x", 0xabcd);
    assert(r == "0xabcd");
    r = format("%#X", 0xABCD);
    assert(r == "0XABCD");

    r = format("%#o", octal!12345);
    assert(r == "012345");
    r = format("%o", 9);
    assert(r == "11");
    r = format("%#o", 0);   // https://issues.dlang.org/show_bug.cgi?id=15663
    assert(r == "0");

    r = format("%+d", 123);
    assert(r == "+123");
    r = format("%+d", -123);
    assert(r == "-123");
    r = format("% d", 123);
    assert(r == " 123");
    r = format("% d", -123);
    assert(r == "-123");

    r = format("%%");
    assert(r == "%");

    r = format("%d", true);
    assert(r == "1");
    r = format("%d", false);
    assert(r == "0");

    r = format("%d", 'a');
    assert(r == "97");
    wchar wc = 'a';
    r = format("%d", wc);
    assert(r == "97");
    dchar dc = 'a';
    r = format("%d", dc);
    assert(r == "97");

    byte b = byte.max;
    r = format("%x", b);
    assert(r == "7f");
    r = format("%x", ++b);
    assert(r == "80");
    r = format("%x", ++b);
    assert(r == "81");

    short sh = short.max;
    r = format("%x", sh);
    assert(r == "7fff");
    r = format("%x", ++sh);
    assert(r == "8000");
    r = format("%x", ++sh);
    assert(r == "8001");

    i = int.max;
    r = format("%x", i);
    assert(r == "7fffffff");
    r = format("%x", ++i);
    assert(r == "80000000");
    r = format("%x", ++i);
    assert(r == "80000001");

    r = format("%x", 10);
    assert(r == "a");
    r = format("%X", 10);
    assert(r == "A");
    r = format("%x", 15);
    assert(r == "f");
    r = format("%X", 15);
    assert(r == "F");

    Object c = null;
    r = format("%s", c);
    assert(r == "null");

    enum TestEnum
    {
        Value1, Value2
    }
    r = format("%s", TestEnum.Value2);
    assert(r == "Value2");

    immutable(char[5])[int] aa = ([3:"hello", 4:"betty"]);
    r = format("%s", aa.values);
    assert(r == `["hello", "betty"]` || r == `["betty", "hello"]`);
    r = format("%s", aa);
    assert(r == `[3:"hello", 4:"betty"]` || r == `[4:"betty", 3:"hello"]`);

    static const dchar[] ds = ['a','b'];
    for (int j = 0; j < ds.length; ++j)
    {
        r = format(" %d", ds[j]);
        if (j == 0)
            assert(r == " 97");
        else
            assert(r == " 98");
    }

    r = format(">%14d<, %s", 15, [1,2,3]);
    assert(r == ">            15<, [1, 2, 3]");

    assert(format("%8s", "bar") == "     bar");
    assert(format("%8s", "b\u00e9ll\u00f4") == "   b\u00e9ll\u00f4");
}

// https://issues.dlang.org/show_bug.cgi?id=18205
@safe pure unittest
{
    assert("|%8s|".format("abc")        == "|     abc|");
    assert("|%8s|".format("αβγ")        == "|     αβγ|");
    assert("|%8s|".format("   ")        == "|        |");
    assert("|%8s|".format("été"d)       == "|     été|");
    assert("|%8s|".format("été 2018"w)  == "|été 2018|");

    assert("%2s".format("e\u0301"w) == " e\u0301");
    assert("%2s".format("a\u0310\u0337"d) == " a\u0310\u0337");
}

// https://issues.dlang.org/show_bug.cgi?id=3479
@safe unittest
{
    import std.array;
    auto stream = appender!(char[])();
    formattedWrite(stream, "%2$.*1$d", 12, 10);
    assert(stream.data == "000000000010", stream.data);
}

// https://issues.dlang.org/show_bug.cgi?id=6893
@safe unittest
{
    import std.array;
    enum E : ulong { A, B, C }
    auto stream = appender!(char[])();
    formattedWrite(stream, "%s", E.C);
    assert(stream.data == "C");
}

// Used to check format strings are compatible with argument types
package static const checkFormatException(alias fmt, Args...) =
{
    import std.conv : text;

    try
    {
        auto n = .formattedWrite(NoOpSink(), fmt, Args.init);

        enforceFmt(n == Args.length, text("Orphan format arguments: args[", n, "..", Args.length, "]"));
    }
    catch (Exception e)
        return (e.msg == ctfpMessage) ? null : e;
    return null;
}();

private void writeAligned(Writer, T, Char)(auto ref Writer w, T s, scope const ref FormatSpec!Char f)
if (isSomeString!T)
{
    size_t width;
    if (f.width > 0)
    {
        // check for non-ascii character
        import std.algorithm.searching : any;
        if (s.any!(a => a > 0x7F))
        {
            //TODO: optimize this
            import std.uni : graphemeStride;
            for (size_t i; i < s.length; i += graphemeStride(s, i))
                ++width;
        }
        else
            width = s.length;
    }
    else
        width = s.length;

    if (!f.flDash)
    {
        // right align
        if (f.width > width)
            foreach (i ; 0 .. f.width - width) put(w, ' ');
        put(w, s);
    }
    else
    {
        // left align
        put(w, s);
        if (f.width > width)
            foreach (i ; 0 .. f.width - width) put(w, ' ');
    }
}

@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%s");
    writeAligned(w, "a本Ä", spec);
    assert(w.data == "a本Ä", w.data);
}

@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%10s");
    writeAligned(w, "a本Ä", spec);
    assert(w.data == "       a本Ä", "|" ~ w.data ~ "|");
}

@safe pure unittest
{
    import std.array : appender;
    auto w = appender!string();
    auto spec = singleSpec("%-10s");
    writeAligned(w, "a本Ä", spec);
    assert(w.data == "a本Ä       ", w.data);
}

/**
 * Format arguments into a string.
 *
 * If the format string is fixed, passing it as a template parameter checks the
 * type correctness of the parameters at compile-time. This also can result in
 * better performance.
 *
 * Params: fmt  = Format string. For detailed specification, see $(LREF formattedWrite).
 *         args = Variadic list of arguments to format into returned string.
 *
 * Throws:
 *     $(LREF, FormatException) if the number of arguments doesn't match the number
 *     of format parameters and vice-versa.
 */
typeof(fmt) format(alias fmt, Args...)(Args args)
if (isSomeString!(typeof(fmt)))
{
    import std.array : appender;

    alias e = checkFormatException!(fmt, Args);
    alias Char = Unqual!(ElementEncodingType!(typeof(fmt)));

    static assert(!e, e.msg);
    auto w = appender!(immutable(Char)[]);

    // no need to traverse the string twice during compile time
    if (!__ctfe)
    {
        enum len = guessLength!Char(fmt);
        w.reserve(len);
    }
    else
    {
        w.reserve(fmt.length);
    }

    formattedWrite(w, fmt, args);
    return w.data;
}

/// Type checking can be done when fmt is known at compile-time:
@safe unittest
{
    auto s = format!"%s is %s"("Pi", 3.14);
    assert(s == "Pi is 3.14");

    static assert(!__traits(compiles, {s = format!"%l"();}));     // missing arg
    static assert(!__traits(compiles, {s = format!""(404);}));    // surplus arg
    static assert(!__traits(compiles, {s = format!"%d"(4.03);})); // incompatible arg
}

// called during compilation to guess the length of the
// result of format
private size_t guessLength(Char, S)(S fmtString)
{
    import std.array : appender;

    size_t len;
    auto output = appender!(immutable(Char)[])();
    auto spec = FormatSpec!Char(fmtString);
    while (spec.writeUpToNextSpec(output))
    {
        // take a guess
        if (spec.width == 0 && (spec.precision == spec.UNSPECIFIED || spec.precision == spec.DYNAMIC))
        {
            switch (spec.spec)
            {
                case 'c':
                    ++len;
                    break;
                case 'd':
                case 'x':
                case 'X':
                    len += 3;
                    break;
                case 'b':
                    len += 8;
                    break;
                case 'f':
                case 'F':
                    len += 10;
                    break;
                case 's':
                case 'e':
                case 'E':
                case 'g':
                case 'G':
                    len += 12;
                    break;
                default: break;
            }

            continue;
        }

        if ((spec.spec == 'e' || spec.spec == 'E' || spec.spec == 'g' ||
             spec.spec == 'G' || spec.spec == 'f' || spec.spec == 'F') &&
            spec.precision != spec.UNSPECIFIED && spec.precision != spec.DYNAMIC &&
            spec.width == 0
        )
        {
            len += spec.precision + 5;
            continue;
        }

        if (spec.width == spec.precision)
            len += spec.width;
        else if (spec.width > 0 && spec.width != spec.DYNAMIC &&
                 (spec.precision == spec.UNSPECIFIED || spec.width > spec.precision))
        {
            len += spec.width;
        }
        else if (spec.precision != spec.UNSPECIFIED && spec.precision > spec.width)
            len += spec.precision;
    }
    len += output.data.length;
    return len;
}

@safe pure
unittest
{
    assert(guessLength!char("%c") == 1);
    assert(guessLength!char("%d") == 3);
    assert(guessLength!char("%x") == 3);
    assert(guessLength!char("%b") == 8);
    assert(guessLength!char("%f") == 10);
    assert(guessLength!char("%s") == 12);
    assert(guessLength!char("%02d") == 2);
    assert(guessLength!char("%02d") == 2);
    assert(guessLength!char("%4.4d") == 4);
    assert(guessLength!char("%2.4f") == 4);
    assert(guessLength!char("%02d:%02d:%02d") == 8);
    assert(guessLength!char("%0.2f") == 7);
    assert(guessLength!char("%0*d") == 0);
}

/// ditto
immutable(Char)[] format(Char, Args...)(in Char[] fmt, Args args)
if (isSomeChar!Char)
{
    import std.array : appender;
    auto w = appender!(immutable(Char)[]);
    auto n = formattedWrite(w, fmt, args);
    version (all)
    {
        // In the future, this check will be removed to increase consistency
        // with formattedWrite
        import std.conv : text;
        enforceFmt(n == args.length, text("Orphan format arguments: args[", n, "..", args.length, "]"));
    }
    return w.data;
}

@safe pure unittest
{
    import core.exception;
    import std.exception;
    assertCTFEable!(
    {
//  assert(format(null) == "");
    assert(format("foo") == "foo");
    assert(format("foo%%") == "foo%");
    assert(format("foo%s", 'C') == "fooC");
    assert(format("%s foo", "bar") == "bar foo");
    assert(format("%s foo %s", "bar", "abc") == "bar foo abc");
    assert(format("foo %d", -123) == "foo -123");
    assert(format("foo %d", 123) == "foo 123");

    assertThrown!FormatException(format("foo %s"));
    assertThrown!FormatException(format("foo %s", 123, 456));

    assert(format("hel%slo%s%s%s", "world", -138, 'c', true) ==
                  "helworldlo-138ctrue");
    });

    assert(is(typeof(format("happy")) == string));
    assert(is(typeof(format("happy"w)) == wstring));
    assert(is(typeof(format("happy"d)) == dstring));
}

// https://issues.dlang.org/show_bug.cgi?id=16661
@safe unittest
{
    assert(format("%.2f"d, 0.4) == "0.40");
    assert("%02d"d.format(1) == "01"d);
}

/*****************************************************
 * Format arguments into buffer $(I buf) which must be large
 * enough to hold the result.
 *
 * Returns:
 *     The slice of `buf` containing the formatted string.
 *
 * Throws:
 *     A `RangeError` if `buf` isn't large enough to hold the
 *     formatted string.
 *
 *     A $(LREF FormatException) if the length of `args` is different
 *     than the number of format specifiers in `fmt`.
 */
char[] sformat(alias fmt, Args...)(char[] buf, Args args)
if (isSomeString!(typeof(fmt)))
{
    alias e = checkFormatException!(fmt, Args);
    static assert(!e, e.msg);
    return .sformat(buf, fmt, args);
}

/// ditto
char[] sformat(Char, Args...)(return scope char[] buf, scope const(Char)[] fmt, Args args)
{
    import core.exception : RangeError;
    import std.utf : encode;

    static struct Sink
    {
        char[] buf;
        size_t i;
        void put(dchar c)
        {
            char[4] enc;
            auto n = encode(enc, c);

            if (buf.length < i + n)
                throw new RangeError(__FILE__, __LINE__);

            buf[i .. i + n] = enc[0 .. n];
            i += n;
        }
        void put(scope const(char)[] s)
        {
            if (buf.length < i + s.length)
                throw new RangeError(__FILE__, __LINE__);

            buf[i .. i + s.length] = s[];
            i += s.length;
        }
        void put(scope const(wchar)[] s)
        {
            for (; !s.empty; s.popFront())
                put(s.front);
        }
        void put(scope const(dchar)[] s)
        {
            for (; !s.empty; s.popFront())
                put(s.front);
        }
    }
    auto sink = Sink(buf);
    auto n = formattedWrite(sink, fmt, args);
    version (all)
    {
        // In the future, this check will be removed to increase consistency
        // with formattedWrite
        import std.conv : text;
        enforceFmt(
            n == args.length,
            text("Orphan format arguments: args[", n, " .. ", args.length, "]")
        );
    }
    return buf[0 .. sink.i];
}

/// The format string can be checked at compile-time (see $(LREF format) for details):
@system unittest
{
    char[10] buf;

    assert(buf[].sformat!"foo%s"('C') == "fooC");
    assert(sformat(buf[], "%s foo", "bar") == "bar foo");
}

@system unittest
{
    import core.exception;

    debug(string) trustedPrintf("std.string.sformat.unittest\n");

    import std.exception;
    assertCTFEable!(
    {
    char[10] buf;

    assert(sformat(buf[], "foo") == "foo");
    assert(sformat(buf[], "foo%%") == "foo%");
    assert(sformat(buf[], "foo%s", 'C') == "fooC");
    assert(sformat(buf[], "%s foo", "bar") == "bar foo");
    assertThrown!RangeError(sformat(buf[], "%s foo %s", "bar", "abc"));
    assert(sformat(buf[], "foo %d", -123) == "foo -123");
    assert(sformat(buf[], "foo %d", 123) == "foo 123");

    assertThrown!FormatException(sformat(buf[], "foo %s"));
    assertThrown!FormatException(sformat(buf[], "foo %s", 123, 456));

    assert(sformat(buf[], "%s %s %s", "c"c, "w"w, "d"d) == "c w d");
    });
}

@system unittest // ensure that sformat avoids the GC
{
    import core.memory : GC;
    const a = ["foo", "bar"];
    const u = GC.stats().usedSize;
    char[20] buf;
    sformat(buf, "%d", 123);
    sformat(buf, "%s", a);
    sformat(buf, "%s", 'c');
    assert(u == GC.stats().usedSize);
}

/*****************************
 * The .ptr is unsafe because it could be dereferenced and the length of the array may be 0.
 * Returns:
 *      the difference between the starts of the arrays
 */
@trusted private pure nothrow @nogc
    ptrdiff_t arrayPtrDiff(T)(const T[] array1, const T[] array2)
{
    return array1.ptr - array2.ptr;
}

@safe unittest
{
    assertCTFEable!({
    auto tmp = format("%,d", 1000);
    assert(tmp == "1,000", "'" ~ tmp ~ "'");

    tmp = format("%,?d", 'z', 1234567);
    assert(tmp == "1z234z567", "'" ~ tmp ~ "'");

    tmp = format("%10,?d", 'z', 1234567);
    assert(tmp == " 1z234z567", "'" ~ tmp ~ "'");

    tmp = format("%11,2?d", 'z', 1234567);
    assert(tmp == " 1z23z45z67", "'" ~ tmp ~ "'");

    tmp = format("%11,*?d", 2, 'z', 1234567);
    assert(tmp == " 1z23z45z67", "'" ~ tmp ~ "'");

    tmp = format("%11,*d", 2, 1234567);
    assert(tmp == " 1,23,45,67", "'" ~ tmp ~ "'");

    tmp = format("%11,2d", 1234567);
    assert(tmp == " 1,23,45,67", "'" ~ tmp ~ "'");
    });
}

@safe unittest
{
    auto tmp = format("%,f", 1000.0);
    assert(tmp == "1,000.000000", "'" ~ tmp ~ "'");

    tmp = format("%,f", 1234567.891011);
    assert(tmp == "1,234,567.891011", "'" ~ tmp ~ "'");

    tmp = format("%,f", -1234567.891011);
    assert(tmp == "-1,234,567.891011", "'" ~ tmp ~ "'");

    tmp = format("%,2f", 1234567.891011);
    assert(tmp == "1,23,45,67.891011", "'" ~ tmp ~ "'");

    tmp = format("%18,f", 1234567.891011);
    assert(tmp == "  1,234,567.891011", "'" ~ tmp ~ "'");

    tmp = format("%18,?f", '.', 1234567.891011);
    assert(tmp == "  1.234.567.891011", "'" ~ tmp ~ "'");

    tmp = format("%,?.3f", 'ä', 1234567.891011);
    assert(tmp == "1ä234ä567.891", "'" ~ tmp ~ "'");

    tmp = format("%,*?.3f", 1, 'ä', 1234567.891011);
    assert(tmp == "1ä2ä3ä4ä5ä6ä7.891", "'" ~ tmp ~ "'");

    tmp = format("%,4?.3f", '_', 1234567.891011);
    assert(tmp == "123_4567.891", "'" ~ tmp ~ "'");

    tmp = format("%12,3.3f", 1234.5678);
    assert(tmp == "   1,234.568", "'" ~ tmp ~ "'");

    tmp = format("%,e", 3.141592653589793238462);
    assert(tmp == "3.141593e+00", "'" ~ tmp ~ "'");

    tmp = format("%15,e", 3.141592653589793238462);
    assert(tmp == "   3.141593e+00", "'" ~ tmp ~ "'");

    tmp = format("%15,e", -3.141592653589793238462);
    assert(tmp == "  -3.141593e+00", "'" ~ tmp ~ "'");

    tmp = format("%.4,*e", 2, 3.141592653589793238462);
    assert(tmp == "3.1416e+00", "'" ~ tmp ~ "'");

    tmp = format("%13.4,*e", 2, 3.141592653589793238462);
    assert(tmp == "   3.1416e+00", "'" ~ tmp ~ "'");

    tmp = format("%,.0f", 3.14);
    assert(tmp == "3", "'" ~ tmp ~ "'");

    tmp = format("%3,g", 1_000_000.123456);
    assert(tmp == "1e+06", "'" ~ tmp ~ "'");

    tmp = format("%19,?f", '.', -1234567.891011);
    assert(tmp == "  -1.234.567.891011", "'" ~ tmp ~ "'");
}

// Test for multiple indexes
@safe unittest
{
    auto tmp = format("%2:5$s", 1, 2, 3, 4, 5);
    assert(tmp == "2345", tmp);
}

// https://issues.dlang.org/show_bug.cgi?id=18047
@safe unittest
{
    auto cmp = "     123,456";
    assert(cmp.length == 12, format("%d", cmp.length));
    auto tmp = format("%12,d", 123456);
    assert(tmp.length == 12, format("%d", tmp.length));

    assert(tmp == cmp, "'" ~ tmp ~ "'");
}

// https://issues.dlang.org/show_bug.cgi?id=17459
@safe unittest
{
    auto cmp = "100";
    auto tmp  = format("%0d", 100);
    assert(tmp == cmp, tmp);

    cmp = "0100";
    tmp  = format("%04d", 100);
    assert(tmp == cmp, tmp);

    cmp = "0,000,000,100";
    tmp  = format("%012,3d", 100);
    assert(tmp == cmp, tmp);

    cmp = "0,000,001,000";
    tmp = format("%012,3d", 1_000);
    assert(tmp == cmp, tmp);

    cmp = "0,000,100,000";
    tmp = format("%012,3d", 100_000);
    assert(tmp == cmp, tmp);

    cmp = "0,001,000,000";
    tmp = format("%012,3d", 1_000_000);
    assert(tmp == cmp, tmp);

    cmp = "0,100,000,000";
    tmp = format("%012,3d", 100_000_000);
    assert(tmp == cmp, tmp);
}

// https://issues.dlang.org/show_bug.cgi?id=17459
@safe unittest
{
    auto cmp = "100,000";
    auto tmp  = format("%06,d", 100_000);
    assert(tmp == cmp, tmp);

    cmp = "100,000";
    tmp  = format("%07,d", 100_000);
    assert(tmp == cmp, tmp);

    cmp = "0,100,000";
    tmp  = format("%08,d", 100_000);
    assert(tmp == cmp, tmp);
}

// https://issues.dlang.org/show_bug.cgi?id=20288
@safe unittest
{
    string s = format("%,.2f", double.nan);
    assert(s == "nan", s);

    s = format("%,.2F", double.nan);
    assert(s == "NAN", s);

    s = format("%,.2f", -double.nan);
    assert(s == "-nan", s);

    s = format("%,.2F", -double.nan);
    assert(s == "-NAN", s);

    string g = format("^%13s$", "nan");
    string h = "^          nan$";
    assert(g == h, "\ngot:" ~ g ~ "\nexp:" ~ h);
    string a = format("^%13,3.2f$", double.nan);
    string b = format("^%13,3.2F$", double.nan);
    string c = format("^%13,3.2f$", -double.nan);
    string d = format("^%13,3.2F$", -double.nan);
    assert(a == "^          nan$", "\ngot:'"~ a ~ "'\nexp:'^          nan$'");
    assert(b == "^          NAN$", "\ngot:'"~ b ~ "'\nexp:'^          NAN$'");
    assert(c == "^         -nan$", "\ngot:'"~ c ~ "'\nexp:'^         -nan$'");
    assert(d == "^         -NAN$", "\ngot:'"~ d ~ "'\nexp:'^         -NAN$'");

    a = format("^%-13,3.2f$", double.nan);
    b = format("^%-13,3.2F$", double.nan);
    c = format("^%-13,3.2f$", -double.nan);
    d = format("^%-13,3.2F$", -double.nan);
    assert(a == "^nan          $", "\ngot:'"~ a ~ "'\nexp:'^nan          $'");
    assert(b == "^NAN          $", "\ngot:'"~ b ~ "'\nexp:'^NAN          $'");
    assert(c == "^-nan         $", "\ngot:'"~ c ~ "'\nexp:'^-nan         $'");
    assert(d == "^-NAN         $", "\ngot:'"~ d ~ "'\nexp:'^-NAN         $'");

    a = format("^%+13,3.2f$", double.nan);
    b = format("^%+13,3.2F$", double.nan);
    c = format("^%+13,3.2f$", -double.nan);
    d = format("^%+13,3.2F$", -double.nan);
    assert(a == "^         +nan$", "\ngot:'"~ a ~ "'\nexp:'^         +nan$'");
    assert(b == "^         +NAN$", "\ngot:'"~ b ~ "'\nexp:'^         +NAN$'");
    assert(c == "^         -nan$", "\ngot:'"~ c ~ "'\nexp:'^         -nan$'");
    assert(d == "^         -NAN$", "\ngot:'"~ d ~ "'\nexp:'^         -NAN$'");

    a = format("^%-+13,3.2f$", double.nan);
    b = format("^%-+13,3.2F$", double.nan);
    c = format("^%-+13,3.2f$", -double.nan);
    d = format("^%-+13,3.2F$", -double.nan);
    assert(a == "^+nan         $", "\ngot:'"~ a ~ "'\nexp:'^+nan         $'");
    assert(b == "^+NAN         $", "\ngot:'"~ b ~ "'\nexp:'^+NAN         $'");
    assert(c == "^-nan         $", "\ngot:'"~ c ~ "'\nexp:'^-nan         $'");
    assert(d == "^-NAN         $", "\ngot:'"~ d ~ "'\nexp:'^-NAN         $'");

    a = format("^%- 13,3.2f$", double.nan);
    b = format("^%- 13,3.2F$", double.nan);
    c = format("^%- 13,3.2f$", -double.nan);
    d = format("^%- 13,3.2F$", -double.nan);
    assert(a == "^ nan         $", "\ngot:'"~ a ~ "'\nexp:'^ nan         $'");
    assert(b == "^ NAN         $", "\ngot:'"~ b ~ "'\nexp:'^ NAN         $'");
    assert(c == "^-nan         $", "\ngot:'"~ c ~ "'\nexp:'^-nan         $'");
    assert(d == "^-NAN         $", "\ngot:'"~ d ~ "'\nexp:'^-NAN         $'");
}

private auto printFloat(T, Char)(return char[] buf, T val, FormatSpec!Char f,
                                 RoundingMode rm = RoundingMode.toNearestTiesToEven)
if (is(T == float) || is(T == double) || (is(T == real) && T.mant_dig == double.mant_dig))
{
    union FloatBits
    {
        T floatValue;
        ulong ulongValue;
    }

    FloatBits fb;
    fb.floatValue = val;
    ulong ival = fb.ulongValue;

    static if (!is(T == float))
    {
        version (DigitalMars)
        {
            // hack to work around https://issues.dlang.org/show_bug.cgi?id=20363
            ival ^= rm;
            ival ^= rm;
        }
    }

    import std.math : log2;
    enum log2_max_exp = cast(int) log2(T.max_exp);

    ulong mnt = ival & ((1L << (T.mant_dig - 1)) - 1);
    int exp = (ival >> (T.mant_dig - 1)) & ((1L << (log2_max_exp + 1)) - 1);
    enum maxexp = 2 * T.max_exp - 1;
    string sgn = (ival >> (T.mant_dig + log2_max_exp)) & 1 ? "-" : "";

    if (sgn == "" && f.flPlus) sgn = "+";
    if (sgn == "" && f.flSpace) sgn = " ";

    assert(f.spec == 'a' || f.spec == 'A' || f.spec == 'e' || f.spec == 'E');
    bool is_upper = f.spec == 'A' || f.spec == 'E';

    // special treatment for nan and inf
    if (exp == maxexp)
    {
        import std.algorithm.comparison : max;

        size_t length = max(f.width, sgn.length + 3);
        char[] result = length <= buf.length ? buf[0 .. length] : new char[length];
        result[] = ' ';

        auto offset = f.flDash ? 0 : (result.length - 3);

        if (sgn != "")
        {
            if (f.flDash) ++offset;
            result[offset-1] = sgn[0];
        }

        result[offset .. offset + 3] = (mnt == 0) ? ( is_upper ? "INF" : "inf" ) : ( is_upper ? "NAN" : "nan" );

        return result;
    }

    final switch (f.spec)
    {
        case 'a': case 'A':
            return printFloatA(buf, val, f, rm, sgn, exp, mnt, is_upper);
        case 'e': case 'E':
            return printFloatE(buf, val, f, rm, sgn, exp, mnt, is_upper);
    }
}

private auto printFloatA(T, Char)(return char[] buf, T val, FormatSpec!Char f, RoundingMode rm,
                                  string sgn, int exp, ulong mnt, bool is_upper)
if (is(T == float) || is(T == double) || (is(T == real) && T.mant_dig == double.mant_dig))
{
    import std.algorithm.comparison : max;

    enum char[16] alpha = ['0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'];
    enum char[16] Alpha = ['0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'];

    enum int bias = T.max_exp - 1;

    static if (is(T == float))
    {
        mnt <<= 1; // make mnt dividable by 4
        enum mant_len = T.mant_dig;
    }
    else
        enum mant_len = T.mant_dig - 1;
    static assert(mant_len % 4 == 0);

    // print full mantissa
    char[(mant_len - 1) / 4 + 1] hex_mant;
    size_t hex_mant_pos = 0;
    size_t pos = mant_len;

    while (pos >= 4 && (mnt & ((1L << pos) - 1)) != 0)
    {
        pos -= 4;
        size_t tmp = (mnt >> pos) & 15;
        hex_mant[hex_mant_pos++] = is_upper ? Alpha[tmp] : alpha[tmp];
    }

    // save integer part
    auto first = exp == 0 ? '0' : '1';

    // print exponent
    if (exp == 0 && mnt == 0)
        exp = 0; // special treatment for 0.0
    else if (exp == 0)
        exp = 1 - bias; // denormalized number
    else
        exp -= bias;

    auto exp_sgn = exp >= 0 ? '+' : '-';
    if (exp < 0) exp = -exp;

    static if (is(T == float))
        enum max_exp_digits = 4;
    else
        enum max_exp_digits = 5;

    char[max_exp_digits] exp_str;
    size_t exp_pos = max_exp_digits;

    do
    {
        exp_str[--exp_pos] = '0' + exp%10;
        exp /= 10;
    } while (exp>0);

    exp_str[--exp_pos] = exp_sgn;

    // calculate needed buffer width
    auto precision = f.precision == f.UNSPECIFIED ? hex_mant_pos : f.precision;
    bool dot = precision > 0 || f.flHash;

    size_t width = sgn.length + 3 + (dot ? 1 : 0) + precision + 1 + (max_exp_digits - exp_pos);

    size_t length = max(width,f.width);
    char[] buffer = length <= buf.length ? buf[0 .. length] : new char[length];
    size_t b_pos = 0;

    size_t delta = f.width - width; // only used, when f.width > width

    // fill buffer
    if (!f.flDash && !f.flZero && f.width > width)
    {
        buffer[b_pos .. b_pos + delta] = ' ';
        b_pos += delta;
    }

    if (sgn != "") buffer[b_pos++] = sgn[0];
    buffer[b_pos++] = '0';
    buffer[b_pos++] = is_upper ? 'X' : 'x';

    if (!f.flDash && f.flZero && f.width > width)
    {
        buffer[b_pos .. b_pos + delta] = '0';
        b_pos += delta;
    }

    buffer[b_pos++] = first;
    if (dot) buffer[b_pos++] = '.';
    if (precision < hex_mant_pos)
    {
        buffer[b_pos .. b_pos + precision] = hex_mant[0 .. precision];
        b_pos += precision;

        enum roundType { ZERO, LOWER, FIVE, UPPER }
        roundType next;

        if (hex_mant[precision] == '0')
            next = roundType.ZERO;
        else if (hex_mant[precision] < '8')
            next = roundType.LOWER;
        else if (hex_mant[precision] > '8')
            next = roundType.UPPER;
        else
            next = roundType.FIVE;

        if (next == roundType.ZERO || next == roundType.FIVE)
        {
            foreach (i;precision + 1 .. hex_mant_pos)
            {
                if (hex_mant[i] > '0')
                {
                    next = next == roundType.ZERO ? roundType.LOWER : roundType.UPPER;
                    break;
                }
            }
        }

        bool roundUp = false;

        if (rm == RoundingMode.up)
            roundUp = next != roundType.ZERO && sgn != "-";
        else if (rm == RoundingMode.down)
            roundUp = next != roundType.ZERO && sgn == "-";
        else if (rm == RoundingMode.toZero)
            roundUp = false;
        else
        {
            assert(rm == RoundingMode.toNearestTiesToEven || rm == RoundingMode.toNearestTiesAwayFromZero);
            roundUp = next == roundType.UPPER;

            if (next == roundType.FIVE)
            {
                // IEEE754 allows for two different ways of implementing roundToNearest:
                //
                // Round to nearest, ties away from zero
                if (rm == RoundingMode.toNearestTiesAwayFromZero)
                    roundUp = true;
                else
                {
                    // Round to nearest, ties to even
                    auto last = buffer[b_pos-1];
                    if (last == '.') last = buffer[b_pos-2];
                    roundUp = (last <= '9' && last % 2 != 0) || (last >= '9' && last % 2 == 0);
                }
            }
        }

        if (roundUp)
        {
            foreach_reverse (i;b_pos - precision - 2 .. b_pos)
            {
                if (buffer[i] == '.') continue;
                if (buffer[i] == 'f' || buffer[i] == 'F')
                    buffer[i] = '0';
                else
                {
                    if (buffer[i] == '9')
                        buffer[i] = is_upper ? 'A' : 'a';
                    else
                        buffer[i]++;
                    break;
                }
            }
        }
    }
    else
    {
        buffer[b_pos .. b_pos + hex_mant_pos] = hex_mant[0 .. hex_mant_pos];
        buffer[b_pos + hex_mant_pos .. b_pos + precision] = '0';
        b_pos += precision;
    }

    buffer[b_pos++] = is_upper ? 'P' : 'p';
    buffer[b_pos .. b_pos + (max_exp_digits - exp_pos)] = exp_str[exp_pos .. $];
    b_pos += max_exp_digits - exp_pos;

    if (f.flDash && f.width > width)
    {
        buffer[b_pos .. b_pos + delta] = ' ';
        b_pos += delta;
    }

    return buffer[0 .. b_pos];
}

@safe unittest
{
    auto f = FormatSpec!dchar("");
    f.spec = 'a';
    char[256] buf;
    assert(printFloat(buf[], float.nan, f) == "nan");
    assert(printFloat(buf[], -float.nan, f) == "-nan");
    assert(printFloat(buf[], float.infinity, f) == "inf");
    assert(printFloat(buf[], -float.infinity, f) == "-inf");
    assert(printFloat(buf[], 0.0f, f) == "0x0p+0");
    assert(printFloat(buf[], -0.0f, f) == "-0x0p+0");

    assert(printFloat(buf[], double.nan, f) == "nan");
    assert(printFloat(buf[], -double.nan, f) == "-nan");
    assert(printFloat(buf[], double.infinity, f) == "inf");
    assert(printFloat(buf[], -double.infinity, f) == "-inf");
    assert(printFloat(buf[], 0.0, f) == "0x0p+0");
    assert(printFloat(buf[], -0.0, f) == "-0x0p+0");

    import std.math : nextUp;

    assert(printFloat(buf[], nextUp(0.0f), f) == "0x0.000002p-126");
    assert(printFloat(buf[], float.epsilon, f) == "0x1p-23");
    assert(printFloat(buf[], float.min_normal, f) == "0x1p-126");
    assert(printFloat(buf[], float.max, f) == "0x1.fffffep+127");

    assert(printFloat(buf[], nextUp(0.0), f) == "0x0.0000000000001p-1022");
    assert(printFloat(buf[], double.epsilon, f) == "0x1p-52");
    assert(printFloat(buf[], double.min_normal, f) == "0x1p-1022");
    assert(printFloat(buf[], double.max, f) == "0x1.fffffffffffffp+1023");

    import std.math : E, PI, PI_2, PI_4, M_1_PI, M_2_PI, M_2_SQRTPI,
                      LN10, LN2, LOG2, LOG2E, LOG2T, LOG10E, SQRT2, SQRT1_2;

    assert(printFloat(buf[], cast(float) E, f) == "0x1.5bf0a8p+1");
    assert(printFloat(buf[], cast(float) PI, f) == "0x1.921fb6p+1");
    assert(printFloat(buf[], cast(float) PI_2, f) == "0x1.921fb6p+0");
    assert(printFloat(buf[], cast(float) PI_4, f) == "0x1.921fb6p-1");
    assert(printFloat(buf[], cast(float) M_1_PI, f) == "0x1.45f306p-2");
    assert(printFloat(buf[], cast(float) M_2_PI, f) == "0x1.45f306p-1");
    assert(printFloat(buf[], cast(float) M_2_SQRTPI, f) == "0x1.20dd76p+0");
    assert(printFloat(buf[], cast(float) LN10, f) == "0x1.26bb1cp+1");
    assert(printFloat(buf[], cast(float) LN2, f) == "0x1.62e43p-1");
    assert(printFloat(buf[], cast(float) LOG2, f) == "0x1.344136p-2");
    assert(printFloat(buf[], cast(float) LOG2E, f) == "0x1.715476p+0");
    assert(printFloat(buf[], cast(float) LOG2T, f) == "0x1.a934fp+1");
    assert(printFloat(buf[], cast(float) LOG10E, f) == "0x1.bcb7b2p-2");
    assert(printFloat(buf[], cast(float) SQRT2, f) == "0x1.6a09e6p+0");
    assert(printFloat(buf[], cast(float) SQRT1_2, f) == "0x1.6a09e6p-1");

    assert(printFloat(buf[], cast(double) E, f) == "0x1.5bf0a8b145769p+1");
    assert(printFloat(buf[], cast(double) PI, f) == "0x1.921fb54442d18p+1");
    assert(printFloat(buf[], cast(double) PI_2, f) == "0x1.921fb54442d18p+0");
    assert(printFloat(buf[], cast(double) PI_4, f) == "0x1.921fb54442d18p-1");
    assert(printFloat(buf[], cast(double) M_1_PI, f) == "0x1.45f306dc9c883p-2");
    assert(printFloat(buf[], cast(double) M_2_PI, f) == "0x1.45f306dc9c883p-1");
    assert(printFloat(buf[], cast(double) M_2_SQRTPI, f) == "0x1.20dd750429b6dp+0");
    assert(printFloat(buf[], cast(double) LN10, f) == "0x1.26bb1bbb55516p+1");
    assert(printFloat(buf[], cast(double) LN2, f) == "0x1.62e42fefa39efp-1");
    assert(printFloat(buf[], cast(double) LOG2, f) == "0x1.34413509f79ffp-2");
    assert(printFloat(buf[], cast(double) LOG2E, f) == "0x1.71547652b82fep+0");
    assert(printFloat(buf[], cast(double) LOG2T, f) == "0x1.a934f0979a371p+1");
    assert(printFloat(buf[], cast(double) LOG10E, f) == "0x1.bcb7b1526e50ep-2");
    assert(printFloat(buf[], cast(double) SQRT2, f) == "0x1.6a09e667f3bcdp+0");
    assert(printFloat(buf[], cast(double) SQRT1_2, f) == "0x1.6a09e667f3bcdp-1");

}

@safe unittest
{
    auto f = FormatSpec!dchar("");
    f.spec = 'a';
    f.precision = 3;
    char[32] buf;

    assert(printFloat(buf[], 1.0f, f) == "0x1.000p+0");
    assert(printFloat(buf[], 3.3f, f) == "0x1.a66p+1");
    assert(printFloat(buf[], 2.9f, f) == "0x1.733p+1");

    assert(printFloat(buf[], 1.0, f) == "0x1.000p+0");
    assert(printFloat(buf[], 3.3, f) == "0x1.a66p+1");
    assert(printFloat(buf[], 2.9, f) == "0x1.733p+1");
}

@safe unittest
{
    auto f = FormatSpec!dchar("");
    f.spec = 'a';
    f.precision = 0;
    char[32] buf;

    assert(printFloat(buf[], 1.0f, f) == "0x1p+0");
    assert(printFloat(buf[], 3.3f, f) == "0x2p+1");
    assert(printFloat(buf[], 2.9f, f) == "0x1p+1");

    assert(printFloat(buf[], 1.0, f) == "0x1p+0");
    assert(printFloat(buf[], 3.3, f) == "0x2p+1");
    assert(printFloat(buf[], 2.9, f) == "0x1p+1");
}

@safe unittest
{
    auto f = FormatSpec!dchar("");
    f.spec = 'a';
    f.precision = 0;
    f.flHash = true;
    char[32] buf;

    assert(printFloat(buf[], 1.0f, f) == "0x1.p+0");
    assert(printFloat(buf[], 3.3f, f) == "0x2.p+1");
    assert(printFloat(buf[], 2.9f, f) == "0x1.p+1");

    assert(printFloat(buf[], 1.0, f) == "0x1.p+0");
    assert(printFloat(buf[], 3.3, f) == "0x2.p+1");
    assert(printFloat(buf[], 2.9, f) == "0x1.p+1");
}

@safe unittest
{
    auto f = FormatSpec!dchar("");
    f.spec = 'a';
    f.width = 22;
    char[32] buf;

    assert(printFloat(buf[], 1.0f, f) == "                0x1p+0");
    assert(printFloat(buf[], 3.3f, f) == "         0x1.a66666p+1");
    assert(printFloat(buf[], 2.9f, f) == "         0x1.733334p+1");

    assert(printFloat(buf[], 1.0, f) == "                0x1p+0");
    assert(printFloat(buf[], 3.3, f) == "  0x1.a666666666666p+1");
    assert(printFloat(buf[], 2.9, f) == "  0x1.7333333333333p+1");
}

@safe unittest
{
    auto f = FormatSpec!dchar("");
    f.spec = 'a';
    f.width = 22;
    f.flDash = true;
    char[32] buf;

    assert(printFloat(buf[], 1.0f, f) == "0x1p+0                ");
    assert(printFloat(buf[], 3.3f, f) == "0x1.a66666p+1         ");
    assert(printFloat(buf[], 2.9f, f) == "0x1.733334p+1         ");

    assert(printFloat(buf[], 1.0, f) == "0x1p+0                ");
    assert(printFloat(buf[], 3.3, f) == "0x1.a666666666666p+1  ");
    assert(printFloat(buf[], 2.9, f) == "0x1.7333333333333p+1  ");
}

@safe unittest
{
    auto f = FormatSpec!dchar("");
    f.spec = 'a';
    f.width = 22;
    f.flZero = true;
    char[32] buf;

    assert(printFloat(buf[], 1.0f, f) == "0x00000000000000001p+0");
    assert(printFloat(buf[], 3.3f, f) == "0x0000000001.a66666p+1");
    assert(printFloat(buf[], 2.9f, f) == "0x0000000001.733334p+1");

    assert(printFloat(buf[], 1.0, f) == "0x00000000000000001p+0");
    assert(printFloat(buf[], 3.3, f) == "0x001.a666666666666p+1");
    assert(printFloat(buf[], 2.9, f) == "0x001.7333333333333p+1");
}

@safe unittest
{
    auto f = FormatSpec!dchar("");
    f.spec = 'a';
    f.width = 22;
    f.flPlus = true;
    char[32] buf;

    assert(printFloat(buf[], 1.0f, f) == "               +0x1p+0");
    assert(printFloat(buf[], 3.3f, f) == "        +0x1.a66666p+1");
    assert(printFloat(buf[], 2.9f, f) == "        +0x1.733334p+1");

    assert(printFloat(buf[], 1.0, f) == "               +0x1p+0");
    assert(printFloat(buf[], 3.3, f) == " +0x1.a666666666666p+1");
    assert(printFloat(buf[], 2.9, f) == " +0x1.7333333333333p+1");
}

@safe unittest
{
    auto f = FormatSpec!dchar("");
    f.spec = 'a';
    f.width = 22;
    f.flDash = true;
    f.flSpace = true;
    char[32] buf;

    assert(printFloat(buf[], 1.0f, f) == " 0x1p+0               ");
    assert(printFloat(buf[], 3.3f, f) == " 0x1.a66666p+1        ");
    assert(printFloat(buf[], 2.9f, f) == " 0x1.733334p+1        ");

    assert(printFloat(buf[], 1.0, f) == " 0x1p+0               ");
    assert(printFloat(buf[], 3.3, f) == " 0x1.a666666666666p+1 ");
    assert(printFloat(buf[], 2.9, f) == " 0x1.7333333333333p+1 ");
}

@safe unittest
{
    auto f = FormatSpec!dchar("");
    f.spec = 'a';
    f.precision = 1;
    char[32] buf;

    assert(printFloat(buf[], 0x1.18p0,  f, RoundingMode.toNearestTiesAwayFromZero) == "0x1.2p+0");
    assert(printFloat(buf[], 0x1.28p0,  f, RoundingMode.toNearestTiesAwayFromZero) == "0x1.3p+0");
    assert(printFloat(buf[], 0x1.1ap0,  f, RoundingMode.toNearestTiesAwayFromZero) == "0x1.2p+0");
    assert(printFloat(buf[], 0x1.16p0,  f, RoundingMode.toNearestTiesAwayFromZero) == "0x1.1p+0");
    assert(printFloat(buf[], 0x1.10p0,  f, RoundingMode.toNearestTiesAwayFromZero) == "0x1.1p+0");
    assert(printFloat(buf[], -0x1.18p0, f, RoundingMode.toNearestTiesAwayFromZero) == "-0x1.2p+0");
    assert(printFloat(buf[], -0x1.28p0, f, RoundingMode.toNearestTiesAwayFromZero) == "-0x1.3p+0");
    assert(printFloat(buf[], -0x1.1ap0, f, RoundingMode.toNearestTiesAwayFromZero) == "-0x1.2p+0");
    assert(printFloat(buf[], -0x1.16p0, f, RoundingMode.toNearestTiesAwayFromZero) == "-0x1.1p+0");
    assert(printFloat(buf[], -0x1.10p0, f, RoundingMode.toNearestTiesAwayFromZero) == "-0x1.1p+0");

    assert(printFloat(buf[], 0x1.18p0,  f) == "0x1.2p+0");
    assert(printFloat(buf[], 0x1.28p0,  f) == "0x1.2p+0");
    assert(printFloat(buf[], 0x1.1ap0,  f) == "0x1.2p+0");
    assert(printFloat(buf[], 0x1.16p0,  f) == "0x1.1p+0");
    assert(printFloat(buf[], 0x1.10p0,  f) == "0x1.1p+0");
    assert(printFloat(buf[], -0x1.18p0, f) == "-0x1.2p+0");
    assert(printFloat(buf[], -0x1.28p0, f) == "-0x1.2p+0");
    assert(printFloat(buf[], -0x1.1ap0, f) == "-0x1.2p+0");
    assert(printFloat(buf[], -0x1.16p0, f) == "-0x1.1p+0");
    assert(printFloat(buf[], -0x1.10p0, f) == "-0x1.1p+0");

    assert(printFloat(buf[], 0x1.18p0,  f, RoundingMode.toZero) == "0x1.1p+0");
    assert(printFloat(buf[], 0x1.28p0,  f, RoundingMode.toZero) == "0x1.2p+0");
    assert(printFloat(buf[], 0x1.1ap0,  f, RoundingMode.toZero) == "0x1.1p+0");
    assert(printFloat(buf[], 0x1.16p0,  f, RoundingMode.toZero) == "0x1.1p+0");
    assert(printFloat(buf[], 0x1.10p0,  f, RoundingMode.toZero) == "0x1.1p+0");
    assert(printFloat(buf[], -0x1.18p0, f, RoundingMode.toZero) == "-0x1.1p+0");
    assert(printFloat(buf[], -0x1.28p0, f, RoundingMode.toZero) == "-0x1.2p+0");
    assert(printFloat(buf[], -0x1.1ap0, f, RoundingMode.toZero) == "-0x1.1p+0");
    assert(printFloat(buf[], -0x1.16p0, f, RoundingMode.toZero) == "-0x1.1p+0");
    assert(printFloat(buf[], -0x1.10p0, f, RoundingMode.toZero) == "-0x1.1p+0");

    assert(printFloat(buf[], 0x1.18p0,  f, RoundingMode.up) == "0x1.2p+0");
    assert(printFloat(buf[], 0x1.28p0,  f, RoundingMode.up) == "0x1.3p+0");
    assert(printFloat(buf[], 0x1.1ap0,  f, RoundingMode.up) == "0x1.2p+0");
    assert(printFloat(buf[], 0x1.16p0,  f, RoundingMode.up) == "0x1.2p+0");
    assert(printFloat(buf[], 0x1.10p0,  f, RoundingMode.up) == "0x1.1p+0");
    assert(printFloat(buf[], -0x1.18p0, f, RoundingMode.up) == "-0x1.1p+0");
    assert(printFloat(buf[], -0x1.28p0, f, RoundingMode.up) == "-0x1.2p+0");
    assert(printFloat(buf[], -0x1.1ap0, f, RoundingMode.up) == "-0x1.1p+0");
    assert(printFloat(buf[], -0x1.16p0, f, RoundingMode.up) == "-0x1.1p+0");
    assert(printFloat(buf[], -0x1.10p0, f, RoundingMode.up) == "-0x1.1p+0");

    assert(printFloat(buf[], 0x1.18p0,  f, RoundingMode.down) == "0x1.1p+0");
    assert(printFloat(buf[], 0x1.28p0,  f, RoundingMode.down) == "0x1.2p+0");
    assert(printFloat(buf[], 0x1.1ap0,  f, RoundingMode.down) == "0x1.1p+0");
    assert(printFloat(buf[], 0x1.16p0,  f, RoundingMode.down) == "0x1.1p+0");
    assert(printFloat(buf[], 0x1.10p0,  f, RoundingMode.down) == "0x1.1p+0");
    assert(printFloat(buf[], -0x1.18p0, f, RoundingMode.down) == "-0x1.2p+0");
    assert(printFloat(buf[], -0x1.28p0, f, RoundingMode.down) == "-0x1.3p+0");
    assert(printFloat(buf[], -0x1.1ap0, f, RoundingMode.down) == "-0x1.2p+0");
    assert(printFloat(buf[], -0x1.16p0, f, RoundingMode.down) == "-0x1.2p+0");
    assert(printFloat(buf[], -0x1.10p0, f, RoundingMode.down) == "-0x1.1p+0");
}

// for 100% coverage
@safe unittest
{
    auto f = FormatSpec!dchar("");
    f.spec = 'a';
    f.precision = 3;
    char[32] buf;

    assert(printFloat(buf[], 0x1.19f81p0, f) == "0x1.1a0p+0");
    assert(printFloat(buf[], 0x1.19f01p0, f) == "0x1.19fp+0");
}

@safe unittest
{
    auto f = FormatSpec!dchar("");
    f.spec = 'A';
    f.precision = 3;
    char[32] buf;

    assert(printFloat(buf[], 0x1.19f81p0, f) == "0X1.1A0P+0");
    assert(printFloat(buf[], 0x1.19f01p0, f) == "0X1.19FP+0");
}

private auto printFloatE(T, Char)(return char[] buf, T val, FormatSpec!Char f, RoundingMode rm,
                                  string sgn, int exp, ulong mnt, bool is_upper)
if (is(T == float) || is(T == double) || (is(T == real) && T.mant_dig == double.mant_dig))
{
    import std.conv : to;
    import std.algorithm.comparison : max;

    enum int bias = T.max_exp - 1;

    if (f.precision == f.UNSPECIFIED)
        f.precision = 6;

    // special treatment for 0.0
    if (exp == 0 && mnt == 0)
    {
        size_t length = max(f.width, f.precision + ((f.precision == 0 && !f.flHash) ? 1 : 2) + sgn.length + 4);
        char[] result = length <= buf.length ? buf[0 .. length] : new char[length];
        result[] = '0';

        if (f.flDash)
        {
            if (sgn != "")
                result[0] = sgn[0];

            int dot_pos = cast(int) (sgn.length + 1);
            if (f.precision > 0 || f.flHash)
                result[dot_pos] = '.';

            auto exp_start = dot_pos + ((f.precision > 0 || f.flHash) ? 1 : 0) + f.precision;
            if (exp_start + 4 < result.length)
                result[exp_start + 4 .. $] = ' ';

            result[exp_start] = is_upper?'E':'e';
            result[exp_start + 1] = '+';
        }
        else
        {
            int sign_pos = cast(int) (result.length - 6);
            if (f.precision > 0 || f.flHash)
            {
                int dot_pos = cast(int) (result.length - f.precision - 5);
                result[dot_pos] = '.';
                sign_pos = dot_pos - 2;
            }

            if (f.flZero)
                sign_pos = 0;
            else if (sign_pos > 0)
                result[0 .. sign_pos + (sgn.length == 0 ? 1 : 0)] = ' ';

            if (sgn != "")
                result[sign_pos] = sgn[0];

            result[$ - 3] = '+';
            result[$ - 4] = is_upper ? 'E' : 'e';
        }

        return result;
    }

    // add leading 1 for normalized values or correct exponent for denormalied values
    if (exp != 0)
        mnt |= 1L << (T.mant_dig - 1);
    else
        exp = 1;
    exp -= bias;

    // estimate the number of bytes needed left and right of the decimal point
    // the speed of the algorithm depends on being as accurate as possible with
    // this estimate

    // Default for the right side is the number of digits given by f.precision plus one for the dot
    // plus 6 more for e+...
    auto max_right = f.precision + 7;

    // If the exponent is <= 0 there is only the sign and one digit left of the dot else
    // we have to estimate the number of digits. The factor between exp, which is the number of
    // digits in binary system and the searched number is log_2(10). We round this down to 3.32 to
    // get a conservative estimate. We need to add 3, because of the sign, the fact, that the
    // logarithm is one to small and because we need to round up instead of down, which to!int does.
    // And then we might need one more digit in case of a rounding overflow.
    auto max_left = exp>0 ? to!int(exp / 3.32) + 4 : 3;

    // If the result is not left justified, we may need to add more digits here for getting the
    // correct width.
    if (!f.flDash)
        max_left = max(max_left, f.width - max_right + max_left + 1);

    // If the result is left justified, we may need to add more digits to the right. This strongly
    // depends, on the exponent, see above. This time, we need to be conservative in the other direction
    // for not missing a digit; therefore we round log_2(10) up to 3.33.
    if (exp > 0 && f.flDash)
        max_right = max(max_right, f.width - to!int(exp / 3.33) - 2);
    else if (f.flDash)
        max_right = max(max_right, f.width);


    size_t length = max_left + max_right;
    char[] buffer = length <= buf.length ? buf[0 .. length] : new char[length];
    size_t start = max_left;
    size_t left = max_left;
    size_t right = max_left;

    int final_exp = 0;

    enum roundType { ZERO, LOWER, FIVE, UPPER }
    roundType next;

    // Depending on exp, we will use one of three algorithms:
    //
    // Algorithm A: For large exponents (exp >= T.mant_dig)
    // Algorithm B: For small exponents (exp < T.mant_dig - 61)
    // Algorithm C: For exponents close to 0.
    //
    // Algorithm A:
    //   The number to print looks like this: mantissa followed by several zeros.
    //
    //   We know, that there is no fractional part, so we can just use integer division,
    //   consecutivly dividing by 10 and writing down the remainder from right to left.
    //   Unfortunately the integer is too large to fit in an ulong, so we use something
    //   like BigInt: An array of ulongs. We only use 60 bits of that ulongs, because
    //   this simplifies (and speeds up) the division to come.
    //
    //   For the division we use integer division with reminder for each ulong and put
    //   the reminder of each step in the first 4 bits of ulong of the next step (think of
    //   long division for the rationale behind this). The final reminder is the next
    //   digit (from right to left).
    //
    //   This results in the output we would have for the %f specifier. We now adjust this
    //   for %e: First we calculate the place, where the exponent should be printed, filling
    //   up with zeros if needed and second we move the leftmost digit one to the left
    //   and inserting a dot.
    //
    //   After that we decide on the rounding type, using the digits right of the position,
    //   where the exponent will be printed (currently they are still there, but will be
    //   overwritten later).
    //
    // Algorithm B:
    //   The number to print looks like this: zero dot several zeros followed by the mantissa
    //
    //   We know, that the number has no integer part. The algorithm consecutivly multiplies
    //   by 10. The integer part (rounded down) after the multiplication is the next digit
    //   (from left to right). This integer part is removed after each step.
    //   Again, the number is represented as an array of ulongs, with only 60 bits used of
    //   every ulong.
    //
    //   For the multiplication we use normal integer multiplication, which can result in digits
    //   in the uppermost 4 bits. These 4 digits are the carry which is added to the result
    //   of the next multiplication and finally the last carry is the next digit.
    //
    //   Other than for the %f specifier, this multiplication is splitted into two almost
    //   identical parts. The first part lasts as long as we find zeros. We need to do this
    //   to calculate the correct exponent.
    //
    //   The second part will stop, when only zeros remain or when we've got enough digits
    //   for the requested precision. In the second case, we have to find out, which rounding
    //   we have. Aside from special cases we do this by calculating one more digit.
    //
    // Algorithm C:
    //   This time, we know, that the integral part and the fractional part each fit into a
    //   ulong. The mantissa might be partially in both parts or completely in the fractional
    //   part.
    //
    //   We first calculate the integral part by consecutive division by 10. Depending on the
    //   precision this might result in more digits, than we need. In that case we calculate
    //   the position of the exponent and the rounding type.
    //
    //   If there is no integral part, we need to find the first non zero digit. We do this by
    //   consecutive multiplication by 10, saving the first non zero digit followed by a dot.
    //
    //   In either case, we continue filling up with the fractional part until we have enough
    //   digits. If still necessary, we decide the rounding type, mainly by looking at the
    //   next digit.

    ulong[4] bigbuf;
    if (exp >= T.mant_dig)
    {
        // large number without fractional digits
        //
        // As this number does not fit in a ulong, we use an array of ulongs. We only use 60 of the 64 bits,
        // because this makes it much more easy to implement the division by 10.
        int count = exp / 60 + 1;

        // saved in big endian format
        ulong[] mybig = count <= bigbuf.length ? bigbuf[0 .. count] : new ulong[count];

        // only the first or the first two ulongs contain the mantiassa. The rest are zeros.
        int lower = 60 - (exp - T.mant_dig + 1) % 60;
        if (lower < T.mant_dig)
        {
            mybig[0] = mnt >> lower;
            mybig[1] = (mnt & ((1L << lower) - 1)) << 60 - lower;
        }
        else
            mybig[0] = (mnt & ((1L << lower) - 1)) << 60 - lower;

        // Generation of digits by consecutive division with reminder by 10.
        int msu = 0; // Most significant ulong; when it get's zero, we can ignore it further on
        while (msu < count - 1 || mybig[$-1] != 0)
        {
            ulong mod = 0;
            foreach (i;msu .. count)
            {
                mybig[i] |= mod << 60;
                mod = mybig[i] % 10;
                mybig[i] /= 10;
            }
            if (mybig[msu] == 0)
                ++msu;

            buffer[--left] = cast(byte) ('0' + mod);
            ++final_exp;
        }
        --final_exp;

        start = left + f.precision + 1;

        // we need more zeros for precision
        if (right < start)
            buffer[right .. start] = '0';

        // move leftmost digit one more left and add dot between
        buffer[left - 1] = buffer[left];
        buffer[left] = '.';
        --left;

        // rounding type
        if (start >= right)
            next = roundType.ZERO;
        else if (buffer[start] != '0' && buffer[start] != '5')
            next = buffer[start] > '5' ? roundType.UPPER : roundType.LOWER;
        else
        {
            next = buffer[start]=='5' ? roundType.FIVE : roundType.ZERO;
            foreach (i; start + 1 .. right)
                if (buffer[i] > '0')
                {
                    next = next == roundType.FIVE ? roundType.UPPER : roundType.LOWER;
                    break;
                }
        }

        right = start;
        if (f.precision == 0 && !f.flHash) --right;
    }
    else if (exp + 61 < T.mant_dig)
    {
        // small number without integer digits
        //
        // Again this number does not fit in a ulong and we use an array of ulongs. And again we
        // only use 60 bits, because this simplifies the multiplication by 10.
        int count = (T.mant_dig - exp - 2) / 60 + 1;

        // saved in little endian format
        ulong[] mybig = count <= bigbuf.length ? bigbuf[0 .. count] : new ulong[count];

        // only the last or the last two ulongs contain the mantiassa. Because of little endian
        // format these are the ulongs at index 0 and 1. The rest are zeros.
        int upper = 60 - (-exp - 1) % 60;
        if (upper < T.mant_dig)
        {
            mybig[0] = (mnt & ((1L << (T.mant_dig - upper)) - 1)) << 60 - (T.mant_dig - upper);
            mybig[1] = mnt >> (T.mant_dig - upper);
        }
        else
            mybig[0] = mnt << (upper - T.mant_dig);

        int lsu = 0; // Least significant ulong; when it get's zero, we can ignore it further on

        // adding zeros, until we reach first nonzero
        while (lsu < count - 1 || mybig[$ - 1]!=0)
        {
            ulong over = 0;
            foreach (i; lsu .. count)
            {
                mybig[i] = mybig[i] * 10 + over;
                over = mybig[i] >> 60;
                mybig[i] &= (1L << 60) - 1;
            }
            if (mybig[lsu] == 0)
                ++lsu;
            --final_exp;

            if (over != 0)
            {
                buffer[right++] = cast(byte) ('0' + over);
                buffer[right++] = '.';
                break;
            }
        }

        if (f.precision == 0 && !f.flHash) --right;

        // adding more digits
        start = right;
        while ((lsu < count - 1 || mybig[$ - 1] != 0) && right - start < f.precision)
        {
            ulong over = 0;
            foreach (i;lsu .. count)
            {
                mybig[i] = mybig[i] * 10 + over;
                over = mybig[i] >> 60;
                mybig[i] &= (1L << 60) - 1;
            }
            if (mybig[lsu] == 0)
                ++lsu;

            buffer[right++] = cast(byte) ('0' + over);
        }

        // filling up with zeros to match precision
        if (right < start + f.precision)
        {
            buffer[right .. start + f.precision] = '0';
            right = start + f.precision;
        }

        // rounding type
        if (lsu >= count - 1 && mybig[count - 1] == 0)
            next = roundType.ZERO;
        else if (lsu == count - 1 && mybig[lsu] == 1L << 59)
            next = roundType.FIVE;
        else
        {
            ulong over = 0;
            foreach (i;lsu .. count)
            {
                mybig[i] = mybig[i] * 10 + over;
                over = mybig[i] >> 60;
                mybig[i] &= (1L << 60) - 1;
            }
            next = over >= 5 ? roundType.UPPER : roundType.LOWER;
        }
    }
    else
    {
        // medium sized number, probably with integer and fractional digits
        // this is fastest, because both parts fit into a ulong each
        ulong int_part = mnt >> (T.mant_dig - 1 - exp);
        ulong frac_part = mnt & ((1L << (T.mant_dig - 1 - exp)) - 1);

        start = 0;

        // could we already decide on the rounding mode in the integer part?
        bool found = false;

        if (int_part > 0)
        {
            // integer part, if there is something to print
            while (int_part >= 10)
            {
                buffer[--left] = '0' + (int_part % 10);
                int_part /= 10;
                ++final_exp;
                ++start;
            }

            buffer[--left] = '.';
            buffer[--left] = cast(byte) ('0' + int_part);

            if (right - left > f.precision + 2)
            {
                auto old_right = right;
                right = left + f.precision + 2;

                if (buffer[right] == '5' || buffer[right] == '0')
                {
                    next = buffer[right] == '5' ? roundType.FIVE : roundType.ZERO;
                    if (frac_part != 0)
                        next = next == roundType.FIVE ? roundType.UPPER : roundType.LOWER;
                    else
                        foreach (i;right + 1 .. old_right)
                            if (buffer[i] > '0')
                            {
                                next = next == roundType.FIVE ? roundType.UPPER : roundType.LOWER;
                                break;
                            }
                }
                else
                    next = buffer[right] > '5' ? roundType.UPPER : roundType.LOWER;
                found = true;
            }
        }
        else
        {
            // fractional part, skipping leading zeros
            while (frac_part != 0)
            {
                --final_exp;
                frac_part *= 10;
                auto tmp = frac_part >> (T.mant_dig - 1 - exp);
                frac_part &= ((1L << (T.mant_dig - 1 - exp)) - 1);
                if (tmp > 0)
                {
                    buffer[right++] = cast(byte) ('0' + tmp);
                    buffer[right++] = '.';
                    break;
                }
            }

            next = roundType.ZERO;
        }

        if (f.precision == 0 && !f.flHash) right--;

        // the fractional part after the zeros
        while (frac_part != 0 && start < f.precision)
        {
            frac_part *= 10;
            buffer[right++] = cast(byte) ('0' + (frac_part >> (T.mant_dig - 1 - exp)));
            frac_part &= ((1L << (T.mant_dig - 1 - exp)) - 1);
            ++start;
        }

        if (start < f.precision)
        {
            buffer[right .. right + f.precision - start] = '0';
            right += f.precision - start;
            start = f.precision;
        }

        // rounding mode, if not allready known
        if (frac_part != 0 && !found)
        {
            frac_part *= 10;
            auto nextDigit = frac_part >> (T.mant_dig - 1 - exp);
            frac_part &= ((1L << (T.mant_dig - 1 - exp)) - 1);

            if (nextDigit == 5 && frac_part == 0)
                next = roundType.FIVE;
            else if (nextDigit >= 5)
                next = roundType.UPPER;
            else
                next = roundType.LOWER;
        }
    }

    // rounding
    bool roundUp = false;
    if (rm == RoundingMode.up)
        roundUp = next != roundType.ZERO && sgn != "-";
    else if (rm == RoundingMode.down)
        roundUp = next != roundType.ZERO && sgn == "-";
    else if (rm == RoundingMode.toZero)
        roundUp = false;
    else
    {
        assert(rm == RoundingMode.toNearestTiesToEven || rm == RoundingMode.toNearestTiesAwayFromZero);
        roundUp = next == roundType.UPPER;

        if (next == roundType.FIVE)
        {
            // IEEE754 allows for two different ways of implementing roundToNearest:

            // Round to nearest, ties away from zero
            if (rm == RoundingMode.toNearestTiesAwayFromZero)
                roundUp = true;
            else
            {
                // Round to nearest, ties to even
                auto last = buffer[right-1];
                if (last == '.') last = buffer[right-2];
                roundUp = last % 2 != 0;
            }
        }
    }

    if (roundUp)
    {
        foreach_reverse (i;left .. right)
        {
            if (buffer[i] == '.') continue;
            if (buffer[i] == '9')
                buffer[i] = '0';
            else
            {
                buffer[i]++;
                goto printFloat_done;
            }
        }

        // one more digit to the left, so we need to shift everything and increase exponent
        buffer[--left] = '1';
        buffer[left + 2] = buffer[left + 1];
        if (f.flHash || f.precision != 0)
            buffer[left + 1] = '.';
        right--;
        final_exp++;

printFloat_done:
    }

    // printing exponent
    buffer[right++] = is_upper ? 'E' : 'e';
    buffer[right++] = final_exp >= 0 ? '+' : '-';

    if (final_exp < 0) final_exp = -final_exp;

    static if (is(T == float))
        enum max_exp_digits = 2;
    else
        enum max_exp_digits = 3;

    char[max_exp_digits] exp_str;
    size_t exp_pos = max_exp_digits;

    do
    {
        exp_str[--exp_pos] = '0' + final_exp%10;
        final_exp /= 10;
    } while (final_exp>0);
    if (max_exp_digits - exp_pos == 1)
        exp_str[--exp_pos] = '0';

    buffer[right .. right + max_exp_digits - exp_pos] = exp_str[exp_pos .. $];
    right += max_exp_digits - exp_pos;

    // sign and padding
    bool need_sgn = false;
    if (sgn != "")
    {
        // when padding with zeros we need to postpone adding the sign
        if (right - left < f.width && !f.flDash && f.flZero)
            need_sgn = true;
        else
            buffer[--left] = sgn[0];
    }

    if (right - left < f.width)
    {
        if (f.flDash)
        {
            // padding right
            buffer[right .. f.width + left] = ' ';
            right = f.width + left;
        }
        else
        {
            // padding left
            buffer[right - f.width .. left] = f.flZero ? '0' : ' ';
            left = right - f.width;
        }
    }

    if (need_sgn)
        buffer[left] = sgn[0];

    // without this cast it's getting too slow
    return buffer[left .. right];
}

@safe unittest
{
    char[256] buf;
    auto f = FormatSpec!dchar("");
    f.spec = 'e';
    assert(printFloat(buf[], float.nan, f) == "nan");
    assert(printFloat(buf[], -float.nan, f) == "-nan");
    assert(printFloat(buf[], float.infinity, f) == "inf");
    assert(printFloat(buf[], -float.infinity, f) == "-inf");
    assert(printFloat(buf[], 0.0f, f) == "0.000000e+00");
    assert(printFloat(buf[], -0.0f, f) == "-0.000000e+00");
    // cast needed due to https://issues.dlang.org/show_bug.cgi?id=20361
    assert(printFloat(buf[], cast(float) 1e-40, f) == "9.999946e-41");
    assert(printFloat(buf[], cast(float) -1e-40, f) == "-9.999946e-41");
    assert(printFloat(buf[], 1e-30f, f) == "1.000000e-30");
    assert(printFloat(buf[], -1e-30f, f) == "-1.000000e-30");
    assert(printFloat(buf[], 1e-10f, f) == "1.000000e-10");
    assert(printFloat(buf[], -1e-10f, f) == "-1.000000e-10");
    assert(printFloat(buf[], 0.1f, f) == "1.000000e-01");
    assert(printFloat(buf[], -0.1f, f) == "-1.000000e-01");
    assert(printFloat(buf[], 10.0f, f) == "1.000000e+01");
    assert(printFloat(buf[], -10.0f, f) == "-1.000000e+01");
    assert(printFloat(buf[], 1e30f, f) == "1.000000e+30");
    assert(printFloat(buf[], -1e30f, f) == "-1.000000e+30");

    import std.math : nextUp, nextDown;
    assert(printFloat(buf[], nextUp(0.0f), f) == "1.401298e-45");
    assert(printFloat(buf[], nextDown(-0.0f), f) == "-1.401298e-45");
}

@safe unittest
{
    char[256] buf;
    auto f = FormatSpec!dchar("");
    f.spec = 'e';
    f.width = 20;
    f.precision = 10;

    assert(printFloat(buf[], float.nan, f) == "                 nan");
    assert(printFloat(buf[], -float.nan, f) == "                -nan");
    assert(printFloat(buf[], float.infinity, f) == "                 inf");
    assert(printFloat(buf[], -float.infinity, f) == "                -inf");
    assert(printFloat(buf[], 0.0f, f) == "    0.0000000000e+00");
    assert(printFloat(buf[], -0.0f, f) == "   -0.0000000000e+00");
    // cast needed due to bug 20361
    assert(printFloat(buf[], cast(float) 1e-40, f) == "    9.9999461011e-41");
    assert(printFloat(buf[], cast(float) -1e-40, f) == "   -9.9999461011e-41");
    assert(printFloat(buf[], 1e-30f, f) == "    1.0000000032e-30");
    assert(printFloat(buf[], -1e-30f, f) == "   -1.0000000032e-30");
    assert(printFloat(buf[], 1e-10f, f) == "    1.0000000134e-10");
    assert(printFloat(buf[], -1e-10f, f) == "   -1.0000000134e-10");
    assert(printFloat(buf[], 0.1f, f) == "    1.0000000149e-01");
    assert(printFloat(buf[], -0.1f, f) == "   -1.0000000149e-01");
    assert(printFloat(buf[], 10.0f, f) == "    1.0000000000e+01");
    assert(printFloat(buf[], -10.0f, f) == "   -1.0000000000e+01");
    assert(printFloat(buf[], 1e30f, f) == "    1.0000000150e+30");
    assert(printFloat(buf[], -1e30f, f) == "   -1.0000000150e+30");

    import std.math : nextUp, nextDown;
    assert(printFloat(buf[], nextUp(0.0f), f) == "    1.4012984643e-45");
    assert(printFloat(buf[], nextDown(-0.0f), f) == "   -1.4012984643e-45");
}

@safe unittest
{
    char[256] buf;
    auto f = FormatSpec!dchar("");
    f.spec = 'e';
    f.width = 20;
    f.precision = 10;
    f.flDash = true;

    assert(printFloat(buf[], float.nan, f) == "nan                 ");
    assert(printFloat(buf[], -float.nan, f) == "-nan                ");
    assert(printFloat(buf[], float.infinity, f) == "inf                 ");
    assert(printFloat(buf[], -float.infinity, f) == "-inf                ");
    assert(printFloat(buf[], 0.0f, f) == "0.0000000000e+00    ");
    assert(printFloat(buf[], -0.0f, f) == "-0.0000000000e+00   ");
    // cast needed due to bug 20361
    assert(printFloat(buf[], cast(float) 1e-40, f) == "9.9999461011e-41    ");
    assert(printFloat(buf[], cast(float) -1e-40, f) == "-9.9999461011e-41   ");
    assert(printFloat(buf[], 1e-30f, f) == "1.0000000032e-30    ");
    assert(printFloat(buf[], -1e-30f, f) == "-1.0000000032e-30   ");
    assert(printFloat(buf[], 1e-10f, f) == "1.0000000134e-10    ");
    assert(printFloat(buf[], -1e-10f, f) == "-1.0000000134e-10   ");
    assert(printFloat(buf[], 0.1f, f) == "1.0000000149e-01    ");
    assert(printFloat(buf[], -0.1f, f) == "-1.0000000149e-01   ");
    assert(printFloat(buf[], 10.0f, f) == "1.0000000000e+01    ");
    assert(printFloat(buf[], -10.0f, f) == "-1.0000000000e+01   ");
    assert(printFloat(buf[], 1e30f, f) == "1.0000000150e+30    ");
    assert(printFloat(buf[], -1e30f, f) == "-1.0000000150e+30   ");

    import std.math : nextUp, nextDown;
    assert(printFloat(buf[], nextUp(0.0f), f) == "1.4012984643e-45    ");
    assert(printFloat(buf[], nextDown(-0.0f), f) == "-1.4012984643e-45   ");
}

@safe unittest
{
    char[256] buf;
    auto f = FormatSpec!dchar("");
    f.spec = 'e';
    f.width = 20;
    f.precision = 10;
    f.flZero = true;

    assert(printFloat(buf[], float.nan, f) == "                 nan");
    assert(printFloat(buf[], -float.nan, f) == "                -nan");
    assert(printFloat(buf[], float.infinity, f) == "                 inf");
    assert(printFloat(buf[], -float.infinity, f) == "                -inf");
    assert(printFloat(buf[], 0.0f, f) == "00000.0000000000e+00");
    assert(printFloat(buf[], -0.0f, f) == "-0000.0000000000e+00");
    // cast needed due to bug 20361
    assert(printFloat(buf[], cast(float) 1e-40, f) == "00009.9999461011e-41");
    assert(printFloat(buf[], cast(float) -1e-40, f) == "-0009.9999461011e-41");
    assert(printFloat(buf[], 1e-30f, f) == "00001.0000000032e-30");
    assert(printFloat(buf[], -1e-30f, f) == "-0001.0000000032e-30");
    assert(printFloat(buf[], 1e-10f, f) == "00001.0000000134e-10");
    assert(printFloat(buf[], -1e-10f, f) == "-0001.0000000134e-10");
    assert(printFloat(buf[], 0.1f, f) == "00001.0000000149e-01");
    assert(printFloat(buf[], -0.1f, f) == "-0001.0000000149e-01");
    assert(printFloat(buf[], 10.0f, f) == "00001.0000000000e+01");
    assert(printFloat(buf[], -10.0f, f) == "-0001.0000000000e+01");
    assert(printFloat(buf[], 1e30f, f) == "00001.0000000150e+30");
    assert(printFloat(buf[], -1e30f, f) == "-0001.0000000150e+30");

    import std.math : nextUp, nextDown;
    assert(printFloat(buf[], nextUp(0.0f), f) == "00001.4012984643e-45");
    assert(printFloat(buf[], nextDown(-0.0f), f) == "-0001.4012984643e-45");
}

@safe unittest
{
    char[256] buf;
    auto f = FormatSpec!dchar("");
    f.spec = 'e';
    f.precision = 1;

    assert(printFloat(buf[], 11.5f, f, RoundingMode.toNearestTiesAwayFromZero) == "1.2e+01");
    assert(printFloat(buf[], 12.5f, f, RoundingMode.toNearestTiesAwayFromZero) == "1.3e+01");
    assert(printFloat(buf[], 11.7f, f, RoundingMode.toNearestTiesAwayFromZero) == "1.2e+01");
    assert(printFloat(buf[], 11.3f, f, RoundingMode.toNearestTiesAwayFromZero) == "1.1e+01");
    assert(printFloat(buf[], 11.0f, f, RoundingMode.toNearestTiesAwayFromZero) == "1.1e+01");
    assert(printFloat(buf[], -11.5f, f, RoundingMode.toNearestTiesAwayFromZero) == "-1.2e+01");
    assert(printFloat(buf[], -12.5f, f, RoundingMode.toNearestTiesAwayFromZero) == "-1.3e+01");
    assert(printFloat(buf[], -11.7f, f, RoundingMode.toNearestTiesAwayFromZero) == "-1.2e+01");
    assert(printFloat(buf[], -11.3f, f, RoundingMode.toNearestTiesAwayFromZero) == "-1.1e+01");
    assert(printFloat(buf[], -11.0f, f, RoundingMode.toNearestTiesAwayFromZero) == "-1.1e+01");

    assert(printFloat(buf[], 11.5f, f) == "1.2e+01");
    assert(printFloat(buf[], 12.5f, f) == "1.2e+01");
    assert(printFloat(buf[], 11.7f, f) == "1.2e+01");
    assert(printFloat(buf[], 11.3f, f) == "1.1e+01");
    assert(printFloat(buf[], 11.0f, f) == "1.1e+01");
    assert(printFloat(buf[], -11.5f, f) == "-1.2e+01");
    assert(printFloat(buf[], -12.5f, f) == "-1.2e+01");
    assert(printFloat(buf[], -11.7f, f) == "-1.2e+01");
    assert(printFloat(buf[], -11.3f, f) == "-1.1e+01");
    assert(printFloat(buf[], -11.0f, f) == "-1.1e+01");

    assert(printFloat(buf[], 11.5f, f, RoundingMode.toZero) == "1.1e+01");
    assert(printFloat(buf[], 12.5f, f, RoundingMode.toZero) == "1.2e+01");
    assert(printFloat(buf[], 11.7f, f, RoundingMode.toZero) == "1.1e+01");
    assert(printFloat(buf[], 11.3f, f, RoundingMode.toZero) == "1.1e+01");
    assert(printFloat(buf[], 11.0f, f, RoundingMode.toZero) == "1.1e+01");
    assert(printFloat(buf[], -11.5f, f, RoundingMode.toZero) == "-1.1e+01");
    assert(printFloat(buf[], -12.5f, f, RoundingMode.toZero) == "-1.2e+01");
    assert(printFloat(buf[], -11.7f, f, RoundingMode.toZero) == "-1.1e+01");
    assert(printFloat(buf[], -11.3f, f, RoundingMode.toZero) == "-1.1e+01");
    assert(printFloat(buf[], -11.0f, f, RoundingMode.toZero) == "-1.1e+01");

    assert(printFloat(buf[], 11.5f, f, RoundingMode.up) == "1.2e+01");
    assert(printFloat(buf[], 12.5f, f, RoundingMode.up) == "1.3e+01");
    assert(printFloat(buf[], 11.7f, f, RoundingMode.up) == "1.2e+01");
    assert(printFloat(buf[], 11.3f, f, RoundingMode.up) == "1.2e+01");
    assert(printFloat(buf[], 11.0f, f, RoundingMode.up) == "1.1e+01");
    assert(printFloat(buf[], -11.5f, f, RoundingMode.up) == "-1.1e+01");
    assert(printFloat(buf[], -12.5f, f, RoundingMode.up) == "-1.2e+01");
    assert(printFloat(buf[], -11.7f, f, RoundingMode.up) == "-1.1e+01");
    assert(printFloat(buf[], -11.3f, f, RoundingMode.up) == "-1.1e+01");
    assert(printFloat(buf[], -11.0f, f, RoundingMode.up) == "-1.1e+01");

    assert(printFloat(buf[], 11.5f, f, RoundingMode.down) == "1.1e+01");
    assert(printFloat(buf[], 12.5f, f, RoundingMode.down) == "1.2e+01");
    assert(printFloat(buf[], 11.7f, f, RoundingMode.down) == "1.1e+01");
    assert(printFloat(buf[], 11.3f, f, RoundingMode.down) == "1.1e+01");
    assert(printFloat(buf[], 11.0f, f, RoundingMode.down) == "1.1e+01");
    assert(printFloat(buf[], -11.5f, f, RoundingMode.down) == "-1.2e+01");
    assert(printFloat(buf[], -12.5f, f, RoundingMode.down) == "-1.3e+01");
    assert(printFloat(buf[], -11.7f, f, RoundingMode.down) == "-1.2e+01");
    assert(printFloat(buf[], -11.3f, f, RoundingMode.down) == "-1.2e+01");
    assert(printFloat(buf[], -11.0f, f, RoundingMode.down) == "-1.1e+01");
}

@safe unittest
{
    char[256] buf;
    auto f = FormatSpec!dchar("");
    f.spec = 'e';
    assert(printFloat(buf[], double.nan, f) == "nan");
    assert(printFloat(buf[], -double.nan, f) == "-nan");
    assert(printFloat(buf[], double.infinity, f) == "inf");
    assert(printFloat(buf[], -double.infinity, f) == "-inf");
    assert(printFloat(buf[], 0.0, f) == "0.000000e+00");
    assert(printFloat(buf[], -0.0, f) == "-0.000000e+00");
    // / 1000 needed due to bug 20361
    assert(printFloat(buf[], 1e-307 / 1000, f) == "1.000000e-310");
    assert(printFloat(buf[], -1e-307 / 1000, f) == "-1.000000e-310");
    assert(printFloat(buf[], 1e-30, f) == "1.000000e-30");
    assert(printFloat(buf[], -1e-30, f) == "-1.000000e-30");
    assert(printFloat(buf[], 1e-10, f) == "1.000000e-10");
    assert(printFloat(buf[], -1e-10, f) == "-1.000000e-10");
    assert(printFloat(buf[], 0.1, f) == "1.000000e-01");
    assert(printFloat(buf[], -0.1, f) == "-1.000000e-01");
    assert(printFloat(buf[], 10.0, f) == "1.000000e+01");
    assert(printFloat(buf[], -10.0, f) == "-1.000000e+01");
    assert(printFloat(buf[], 1e300, f) == "1.000000e+300");
    assert(printFloat(buf[], -1e300, f) == "-1.000000e+300");

    import std.math : nextUp, nextDown;
    assert(printFloat(buf[], nextUp(0.0), f) == "4.940656e-324");
    assert(printFloat(buf[], nextDown(-0.0), f) == "-4.940656e-324");
}

@safe unittest
{
    char[256] buf;
    auto f = FormatSpec!dchar("");
    f.spec = 'e';

    import std.math : nextUp;

    double eps = nextUp(0.0);
    f.precision = 1000;
    assert(printFloat(buf[], eps, f) ==
           "4.9406564584124654417656879286822137236505980261432476442558568250067550727020875186529983636163599"
           ~"23797965646954457177309266567103559397963987747960107818781263007131903114045278458171678489821036"
           ~"88718636056998730723050006387409153564984387312473397273169615140031715385398074126238565591171026"
           ~"65855668676818703956031062493194527159149245532930545654440112748012970999954193198940908041656332"
           ~"45247571478690147267801593552386115501348035264934720193790268107107491703332226844753335720832431"
           ~"93609238289345836806010601150616980975307834227731832924790498252473077637592724787465608477820373"
           ~"44696995336470179726777175851256605511991315048911014510378627381672509558373897335989936648099411"
           ~"64205702637090279242767544565229087538682506419718265533447265625000000000000000000000000000000000"
           ~"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
           ~"000000000000000000000e-324");

    f.precision = 50;
    assert(printFloat(buf[], double.max, f) ==
           "1.79769313486231570814527423731704356798070567525845e+308");
    assert(printFloat(buf[], double.epsilon, f) ==
           "2.22044604925031308084726333618164062500000000000000e-16");

    f.precision = 10;
    assert(printFloat(buf[], 1.0/3.0, f) == "3.3333333333e-01");
    assert(printFloat(buf[], 1.0/7.0, f) == "1.4285714286e-01");
    assert(printFloat(buf[], 1.0/9.0, f) == "1.1111111111e-01");
}

@safe unittest
{
    char[256] buf;
    auto f = FormatSpec!dchar("");
    f.spec = 'e';
    f.precision = 15;

    import std.math : E, PI, PI_2, PI_4, M_1_PI, M_2_PI, M_2_SQRTPI,
                      LN10, LN2, LOG2, LOG2E, LOG2T, LOG10E, SQRT2, SQRT1_2;

    assert(printFloat(buf[], cast(double) E, f) == "2.718281828459045e+00");
    assert(printFloat(buf[], cast(double) PI, f) == "3.141592653589793e+00");
    assert(printFloat(buf[], cast(double) PI_2, f) == "1.570796326794897e+00");
    assert(printFloat(buf[], cast(double) PI_4, f) == "7.853981633974483e-01");
    assert(printFloat(buf[], cast(double) M_1_PI, f) == "3.183098861837907e-01");
    assert(printFloat(buf[], cast(double) M_2_PI, f) == "6.366197723675814e-01");
    assert(printFloat(buf[], cast(double) M_2_SQRTPI, f) == "1.128379167095513e+00");
    assert(printFloat(buf[], cast(double) LN10, f) == "2.302585092994046e+00");
    assert(printFloat(buf[], cast(double) LN2, f) == "6.931471805599453e-01");
    assert(printFloat(buf[], cast(double) LOG2, f) == "3.010299956639812e-01");
    assert(printFloat(buf[], cast(double) LOG2E, f) == "1.442695040888963e+00");
    assert(printFloat(buf[], cast(double) LOG2T, f) == "3.321928094887362e+00");
    assert(printFloat(buf[], cast(double) LOG10E, f) == "4.342944819032518e-01");
    assert(printFloat(buf[], cast(double) SQRT2, f) == "1.414213562373095e+00");
    assert(printFloat(buf[], cast(double) SQRT1_2, f) == "7.071067811865476e-01");
}

// for 100% coverage
@safe unittest
{
    char[256] buf;
    auto f = FormatSpec!dchar("");
    f.spec = 'E';
    f.precision = 80;
    assert(printFloat(buf[], 5.62776e+12f, f) ==
           "5.62775982080000000000000000000000000000000000000000000000000000000000000000000000E+12");

    f.precision = 49;
    assert(printFloat(buf[], 2.5997869e-12f, f) ==
           "2.5997869221999758693186777236405760049819946289062E-12");

    f.precision = 6;
    assert(printFloat(buf[], -1.1418613e+07f, f) == "-1.141861E+07");
    assert(printFloat(buf[], -1.368281e+07f, f) == "-1.368281E+07");

    f.precision = 0;
    assert(printFloat(buf[], 709422.0f, f, RoundingMode.up) == "8E+05");

    f.precision = 1;
    assert(printFloat(buf[], -245.666f, f) == "-2.5E+02");
}

// check no allocations
@system unittest
{
    import core.memory;
    auto stats = GC.stats;

    char[256] buf;
    auto f = FormatSpec!dchar("");
    f.spec = 'a';
    assert(printFloat(buf[], float.nan, f) == "nan");
    assert(printFloat(buf[], -float.infinity, f) == "-inf");
    assert(printFloat(buf[], 0.0f, f) == "0x0p+0");

    assert(printFloat(buf[], -double.nan, f) == "-nan");
    assert(printFloat(buf[], double.infinity, f) == "inf");
    assert(printFloat(buf[], -0.0, f) == "-0x0p+0");

    import std.math : nextUp, E;

    assert(printFloat(buf[], nextUp(0.0f), f) == "0x0.000002p-126");
    assert(printFloat(buf[], cast(float) E, f) == "0x1.5bf0a8p+1");

    f.spec = 'E';
    f.precision = 80;
    assert(printFloat(buf[], 5.62776e+12f, f) ==
           "5.62775982080000000000000000000000000000000000000000000000000000000000000000000000E+12");

    f.precision = 6;
    assert(printFloat(buf[], -1.1418613e+07f, f) == "-1.141861E+07");

    assert(GC.stats.usedSize == stats.usedSize);
}
