// Written in the D programming language. /** This module implements the formatting functionality for strings and I/O. It's comparable to C99's $(D 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). Macros: WIKI = Phobos/StdFormat Copyright: Copyright Digital Mars 2000-2013. License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). Authors: $(WEB walterbright.com, Walter Bright), $(WEB 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.stdc.stdio, core.stdc.stdlib, core.stdc.string, core.vararg; import std.algorithm, std.ascii, std.bitmanip, std.conv, std.exception, std.range, std.system, std.traits, std.typetuple, std.utf; version (Win64) { import std.math : isnan, isInfinity; } version(unittest) { import std.math; import std.stdio; import std.string; import std.typecons; import core.exception; import std.string; } version (Win32) version (DigitalMars) { version = DigitalMarsC; } version (DigitalMarsC) { // This is DMC's internal floating point formatting function extern (C) { extern shared char* function(int c, int flags, int precision, in real* pdval, char* buf, size_t* psl, int width) __pfloatfmt; } } /********************************************************************** * Signals a mismatch between a format and its corresponding argument. */ class FormatException : Exception { @safe pure nothrow this() { super("format error"); } @safe pure nothrow this(string msg, string fn = __FILE__, size_t ln = __LINE__, Throwable next = null) { super(msg, fn, ln, next); } } private alias enforceFmt = enforceEx!FormatException; /********************************************************************** Interprets variadic argument list $(D args), formats them according to $(D fmt), and sends the resulting characters to $(D w). The encoding of the output is the same as $(D Char). The type $(D Writer) must satisfy $(XREF range,isOutputRange!(Writer, Char)). The variadic arguments are normally consumed in order. POSIX-style $(WEB 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 $(D w). As many arguments as specified in the format string are consumed and formatted. If there are fewer arguments than format specifiers, a $(D 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 %() and $(B %)). Each matching pair of $(B %() and $(B %)) 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 $(XREF array,Appender!string) and $(XREF stdio,LockingTextWriter). 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: $(I Format strings) 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 Precision) $(I FormatChar) $(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 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') ) $(BOOKTABLE Flags affect formatting depending on the specifier as follows., $(TR $(TH Flag) $(TH Types affected) $(TH Semantics)) $(TR $(TD $(B '-')) $(TD numeric) $(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 $(D nan) and $(D infinity)). Ignore if there's a $(I Precision).)) $(TR $(TD $(B ' ')) $(TD numeric) $(TD Prefix positive numbers in a signed conversion with a space.)))
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:
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:
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 $(D "%$(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:
My friends are ["John", "Nancy"]. My friends are "John", "Nancy". My friends are John, Nancy.*/ uint formattedWrite(Writer, Char, A...)(Writer w, in Char[] fmt, A args) { alias FPfmt = void function(Writer, const(void)*, ref FormatSpec!Char) @safe pure nothrow; auto spec = FormatSpec!Char(fmt); FPfmt[A.length] funs; const(void)*[A.length] argsAddresses; if (!__ctfe) { foreach (i, Arg; A) { funs[i] = ()@trusted{ return cast(FPfmt)&formatGeneric!(Writer, Arg, Char); }(); // We can safely cast away shared because all data is either // immutable or completely owned by this function. argsAddresses[i] = (ref arg)@trusted{ return cast(const void*) &arg; }(args[i]); // Reflect formatting @safe/pure ability of each arguments to this function if (0) formatValue(w, args[i], spec); } } // Are we already done with formats? Then just dump each parameter in turn uint currentArg = 0; while (spec.writeUpToNextSpec(w)) { if (currentArg == funs.length && !spec.indexStart) { // leftover spec? enforceFmt(fmt.length == 0, text("Orphan format specifier: %", fmt)); break; } if (spec.width == spec.DYNAMIC) { auto width = to!(typeof(spec.width))(getNthInt(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); auto width = to!(typeof(spec.width))(getNthInt(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 = to!(typeof(spec.precision))( getNthInt(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); auto precision = to!(typeof(spec.precision))( getNthInt(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; } // Format! if (spec.indexStart > 0) { // using positional parameters! foreach (i; spec.indexStart - 1 .. spec.indexEnd) { if (funs.length <= i) break; if (__ctfe) formatNth(w, spec, i, args); else funs[i](w, argsAddresses[i], spec); } if (currentArg < spec.indexEnd) currentArg = spec.indexEnd; } else { if (__ctfe) formatNth(w, spec, currentArg, args); else funs[currentArg](w, argsAddresses[currentArg], spec); ++currentArg; } } return currentArg; } @safe pure unittest { auto w = appender!string(); formattedWrite(w, "%s %d", "@safe/pure", 42); assert(w.data == "@safe/pure 42"); } /** Reads characters from input range $(D r), converts them according to $(D fmt), and writes them to $(D args). 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. Example: ---- 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); ---- */ uint formattedRead(R, Char, S...)(ref R r, const(Char)[] fmt, S args) { import std.typecons : isTuple; auto spec = FormatSpec!Char(fmt); static if (!S.length) { spec.readUpToNextSpec(r); enforce(spec.trailing.empty); return 0; } else { // 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; } alias A = typeof(*args[0]); static if (isTuple!A) { foreach (i, T; A.Types) { (*args[0])[i] = unformatValue!(T)(r, spec); skipUnstoredFields(); } } else { *args[0] = unformatValue!(A)(r, spec); } return 1 + formattedRead(r, spec.trailing, args[1 .. $]); } } 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)); } template FormatSpec(Char) if (!is(Unqual!Char == Char)) { alias FormatSpec = FormatSpec!(Unqual!Char); } /** * A General handler for $(D printf) style format specifiers. Used for building more * specific formatting functions. * * Example: * ---- * auto a = appender!(string)(); * auto fmt = "Number: %2.4e\nString: %s"; * auto f = FormatSpec!char(fmt); * * f.writeUpToNextSpec(a); * * assert(a.data == "Number: "); * assert(f.trailing == "\nString: %s"); * assert(f.spec == 'e'); * assert(f.width == 2); * assert(f.precision == 4); * * f.writeUpToNextSpec(a); * * assert(a.data == "Number: \nString: "); * assert(f.trailing == ""); * assert(f.spec == 's'); * ---- */ struct FormatSpec(Char) if (is(Unqual!Char == Char)) { /** Minimum _width, default $(D 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; /** Special value for width and precision. $(D DYNAMIC) width or precision means that they were specified with $(D '*') 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, $(D 's') by default. */ char spec = 's'; /** Index of the argument for positional parameters, from $(D 1) to $(D ubyte.max). ($(D 0) means not used). */ ubyte indexStart; /** Index of the last argument for positional parameter range, from $(D 1) to $(D ubyte.max). ($(D 0) means not used). */ ubyte indexEnd; version(StdDdoc) { /** The format specifier contained a $(D '-') ($(D printf) compatibility). */ bool flDash; /** The format specifier contained a $(D '0') ($(D printf) compatibility). */ bool flZero; /** The format specifier contained a $(D ' ') ($(D printf) compatibility). */ bool flSpace; /** The format specifier contained a $(D '+') ($(D printf) compatibility). */ bool flPlus; /** The format specifier contained a $(D '#') ($(D printf) compatibility). */ bool flHash; // Fake field to allow compilation ubyte allFlags; } else { union { mixin(bitfields!( bool, "flDash", 1, bool, "flZero", 1, bool, "flSpace", 1, bool, "flPlus", 1, bool, "flHash", 1, ubyte, "", 3)); ubyte allFlags; } } /** In case of a compound format specifier starting with $(D "%$(LPAREN)") and ending with $(D "%$(RPAREN)"), $(D _nested) contains the string contained within the two separators. */ const(Char)[] nested; /** In case of a compound format specifier, $(D _sep) contains the string positioning after $(D "%|"). */ const(Char)[] sep; /** $(D _trailing) contains the rest of the format string. */ const(Char)[] trailing; /* This string is inserted before each sequence (e.g. array) formatted (by default $(D "[")). */ enum immutable(Char)[] seqBefore = "["; /* This string is inserted after each sequence formatted (by default $(D "]")). */ enum immutable(Char)[] seqAfter = "]"; /* This string is inserted after each element keys of a sequence (by default $(D ":")). */ enum immutable(Char)[] keySeparator = ":"; /* This string is inserted in between elements of a sequence (by default $(D ", ")). */ enum immutable(Char)[] seqSeparator = ", "; /** Construct a new $(D FormatSpec) using the format string $(D fmt), no processing is done until needed. */ this(in Char[] fmt) @safe pure { trailing = fmt; } bool writeUpToNextSpec(OutputRange)(OutputRange writer) { 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; } unittest { auto w = appender!(char[])(); auto f = FormatSpec("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("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"); // bug4775 f = FormatSpec("%%%s"); w.clear(); f.writeUpToNextSpec(w); assert(w.data == "%" && f.trailing == ""); f = FormatSpec("%%%%%s%%"); w.clear(); while (f.writeUpToNextSpec(w)) continue; assert(w.data == "%%%"); f = FormatSpec("a%%b%%c%"); w.clear(); assertThrown!FormatException(f.writeUpToNextSpec(w)); assert(w.data == "a%b%c" && f.trailing == "%"); } private void fillUp() { // Reset content if (__ctfe) { flDash = false; flZero = false; flSpace = false; flPlus = false; flHash = 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;;) { enforce(j < trailing.length, text("Incorrect format specifier: %", trailing[i .. $])); if (trailing[j++] != '%') { // skip, we're waiting for %( and %) continue; } if (trailing[j] == '-') // for %-( ++j; // skip 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 Exception( text("Incorrect format specifier: %", trailing[j .. $])); } nested = to!(typeof(nested))(trailing[i + 1 .. k - 1]); sep = to!(typeof(nested))(trailing[k + 1 .. j - 1]); } else { nested = to!(typeof(nested))(trailing[i + 1 .. j - 1]); sep = null; // use null (issue 12135) } //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 = tmp.ptr - trailing.ptr; if (tmp.startsWith('$')) { // index of the form %n$ indexEnd = indexStart = to!ubyte(widthOrArgIndex); ++i; } else if (tmp.length && tmp[0] == ':') { // 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 = tmp.ptr - trailing.ptr; enforceFmt(trailing[i++] == '$', "$ expected"); } else { // width width = to!int(widthOrArgIndex); } 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 = tmp.ptr - trailing.ptr; } else if (isDigit(trailing[i])) { auto tmp = trailing[i .. $]; precision = .parse!int(tmp); i = tmp.ptr - trailing.ptr; } 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 Exception(text("Incorrect format specifier: ", trailing)); } //-------------------------------------------------------------------------- private bool readUpToNextSpec(R)(ref R r) { // Reset content if (__ctfe) { flDash = false; flZero = false; flSpace = false; flPlus = false; flHash = false; } else { allFlags = 0; } width = 0; precision = UNSPECIFIED; nested = null; // Parse the spec while (trailing.length) { if (*trailing.ptr == '%') { if (trailing.length > 1 && trailing.ptr[1] == '%') { assert(!r.empty); // Require a '%' if (r.front != '%') break; trailing = trailing[2 .. $]; r.popFront(); } else { enforce(isLower(trailing[1]) || trailing[1] == '*' || trailing[1] == '(', text("'%", trailing[1], "' not supported with formatted read")); trailing = trailing[1 .. $]; fillUp(); return true; } } else { if (trailing.ptr[0] == ' ') { while (!r.empty && std.ascii.isWhite(r.front)) r.popFront(); //r = std.algorithm.find!(not!(std.ascii.isWhite))(r); } else { enforce(!r.empty, text("parseToFormatSpec: Cannot find character `", trailing.ptr[0], "' in the input string.")); if (r.front != trailing.front) break; r.popFront(); } trailing = trailing[std.utf.stride(trailing, 0) .. $]; } } return false; } private const string getCurFmtStr() { 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 (width != 0) formatValue(w, width, f); if (precision != FormatSpec!Char.UNSPECIFIED) { put(w, '.'); formatValue(w, precision, f); } put(w, spec); return w.data; } unittest { // issue 5237 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"); } private const(Char)[] headUpToNextSpec() { auto w = appender!(typeof(return))(); auto tr = trailing; while (tr.length) { if (*tr.ptr == '%') { if (tr.length > 1 && tr.ptr[1] == '%') { tr = tr[2 .. $]; w.put('%'); } else break; } else { w.put(tr.front); tr.popFront(); } } return w.data; } string toString() { return text("address = ", cast(void*) &this, "\nwidth = ", width, "\nprecision = ", precision, "\nspec = ", spec, "\nindexStart = ", indexStart, "\nindexEnd = ", indexEnd, "\nflDash = ", flDash, "\nflZero = ", flZero, "\nflSpace = ", flSpace, "\nflPlus = ", flPlus, "\nflHash = ", flHash, "\nnested = ", nested, "\ntrailing = ", trailing, "\n"); } } @safe pure unittest { //Test the example auto a = appender!(string)(); auto fmt = "Number: %2.4e\nString: %s"; auto f = FormatSpec!char(fmt); f.writeUpToNextSpec(a); assert(a.data == "Number: "); assert(f.trailing == "\nString: %s"); assert(f.spec == 'e'); assert(f.width == 2); assert(f.precision == 4); f.writeUpToNextSpec(a); assert(a.data == "Number: \nString: "); assert(f.trailing == ""); assert(f.spec == 's'); } /** Helper function that returns a $(D FormatSpec) for a single specifier given in $(D fmt) Returns a $(D FormatSpec) with the specifier parsed. Enforces giving only one specifier to the function. */ FormatSpec!Char singleSpec(Char)(Char[] fmt) { enforce(fmt.length >= 2, new Exception("fmt must be at least 2 characters long")); enforce(fmt.front == '%', new Exception("fmt must start with a '%' character")); static struct DummyOutputRange { void put(C)(C[] buf) {} // eat elements } auto a = DummyOutputRange(); auto spec = FormatSpec!Char(fmt); //dummy write spec.writeUpToNextSpec(a); enforce(spec.trailing.empty, new Exception(text("Trailing characters in fmt string: '", spec.trailing))); return spec; } unittest { auto spec = singleSpec("%2.3e"); assert(spec.trailing == ""); assert(spec.spec == 'e'); assert(spec.width == 2); assert(spec.precision == 3); assertThrown(singleSpec("")); assertThrown(singleSpec("2.3e")); assertThrown(singleSpec("%2.3eTest")); } /** $(D bool)s are formatted as "true" or "false" with %s and as "1" or "0" with integral-specific format specs. */ void formatValue(Writer, T, Char)(Writer w, T obj, ref FormatSpec!Char f) if (is(BooleanTypeOf!T) && !is(T == enum) && !hasToString!(T, Char)) { BooleanTypeOf!T val = obj; if (f.spec == 's') { string s = val ? "true" : "false"; if (!f.flDash) { // right align if (f.width > s.length) foreach (i ; 0 .. f.width - s.length) put(w, ' '); put(w, s); } else { // left align put(w, s); if (f.width > s.length) foreach (i ; 0 .. f.width - s.length) put(w, ' '); } } else formatValue(w, cast(int) val, f); } @safe pure unittest { assertCTFEable!( { formatTest( false, "false" ); formatTest( true, "true" ); }); } 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" ); } 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]"); } /** $(D null) literal is formatted as $(D "null"). */ void formatValue(Writer, T, Char)(Writer w, T obj, ref FormatSpec!Char f) if (is(Unqual!T == typeof(null)) && !is(T == enum) && !hasToString!(T, Char)) { enforceFmt(f.spec == 's', "null"); put(w, "null"); } @safe pure unittest { assertCTFEable!( { formatTest( null, "null" ); }); } /** Integrals are formatted like $(D printf) does. */ void formatValue(Writer, T, Char)(Writer w, T obj, 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 if (f.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 (std.system.endian == Endian.littleEndian && f.flPlus || std.system.endian == Endian.bigEndian && f.flDash) { // must swap bytes foreach_reverse (c; raw) put(w, c); } else { foreach (c; raw) put(w, c); } return; } uint base = f.spec == 'x' || f.spec == 'X' ? 16 : f.spec == 'o' ? 8 : f.spec == 'b' ? 2 : f.spec == 's' || f.spec == 'd' || f.spec == 'u' ? 10 : 0; enforceFmt(base > 0, "integral"); // Forward on to formatIntegral to handle both U and const(U) // Saves duplication of code for both versions. static if (isSigned!U) formatIntegral(w, cast( long) val, f, base, Unsigned!U.max); else formatIntegral(w, cast(ulong) val, f, base, U.max); } private void formatIntegral(Writer, T, Char)(Writer w, const(T) val, ref FormatSpec!Char f, uint base, ulong mask) { FormatSpec!Char fs = f; // fs is copy for change its values. T arg = val; bool negative = (base == 10 && arg < 0); if (negative) { arg = -arg; } // All unsigned integral types should fit in ulong. formatUnsigned(w, (cast(ulong) arg) & mask, fs, base, negative); } private void formatUnsigned(Writer, Char)(Writer w, ulong arg, ref FormatSpec!Char fs, uint base, bool negative) { if (fs.precision == fs.UNSPECIFIED) { // default precision for integrals is 1 fs.precision = 1; } else { // if a precision is specified, the '0' flag is ignored. fs.flZero = false; } char leftPad = void; if (!fs.flDash && !fs.flZero) leftPad = ' '; else if (!fs.flDash && fs.flZero) leftPad = '0'; else leftPad = 0; // figure out sign and continue in unsigned mode char forcedPrefix = void; if (fs.flPlus) forcedPrefix = '+'; else if (fs.flSpace) forcedPrefix = ' '; else forcedPrefix = 0; if (base != 10) { // non-10 bases are always unsigned forcedPrefix = 0; } else if (negative) { // argument is signed forcedPrefix = '-'; } // fill the digits char[64] buffer; // 64 bits in base 2 at most char[] digits; { uint i = buffer.length; auto n = arg; do { --i; buffer[i] = cast(char) (n % base); n /= base; if (buffer[i] < 10) buffer[i] += '0'; else buffer[i] += (fs.spec == 'x' ? 'a' : 'A') - 10; } while (n); digits = buffer[i .. $]; // got the digits without the sign } // adjust precision to print a '0' for octal if alternate format is on if (base == 8 && fs.flHash && (fs.precision <= digits.length)) // too low precision { //fs.precision = digits.length + (arg != 0); forcedPrefix = '0'; } // write left pad; write sign; write 0x or 0X; write digits; // write right pad // Writing left pad ptrdiff_t spacesToPrint = fs.width // start with the minimum width - digits.length // take away digits to print - (forcedPrefix != 0) // take away the sign if any - (base == 16 && fs.flHash && arg ? 2 : 0); // 0x or 0X const ptrdiff_t delta = fs.precision - digits.length; if (delta > 0) spacesToPrint -= delta; if (spacesToPrint > 0) // need to do some padding { if (leftPad == '0') { // pad with zeros fs.precision = cast(typeof(fs.precision)) (spacesToPrint + digits.length); //to!(typeof(fs.precision))(spacesToPrint + digits.length); } else if (leftPad) foreach (i ; 0 .. spacesToPrint) put(w, ' '); } // write sign if (forcedPrefix) put(w, forcedPrefix); // write 0x or 0X if (base == 16 && fs.flHash && arg) { // @@@ overcome bug in dmd; //w.write(fs.spec == 'x' ? "0x" : "0X"); //crashes the compiler put(w, '0'); put(w, fs.spec == 'x' ? 'x' : 'X'); // x or X } // write the digits if (arg || fs.precision) { ptrdiff_t zerosToPrint = fs.precision - digits.length; foreach (i ; 0 .. zerosToPrint) put(w, '0'); put(w, digits); } // write the spaces to the right if left-align if (!leftPad) foreach (i ; 0 .. spacesToPrint) put(w, ' '); } @safe pure unittest { assertCTFEable!( { formatTest( 10, "10" ); }); } 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" ); } // bugzilla 9117 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(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"); } /** * Floating-point values are formatted like $(D printf) does. */ void formatValue(Writer, T, Char)(Writer w, T obj, ref FormatSpec!Char f) if (is(FloatingPointTypeOf!T) && !is(T == enum) && !hasToString!(T, Char)) { FormatSpec!Char fs = f; // fs is copy for change its values. FloatingPointTypeOf!T val = obj; if (fs.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 (std.system.endian == Endian.littleEndian && f.flPlus || std.system.endian == Endian.bigEndian && f.flDash) { // must swap bytes foreach_reverse (c; raw) put(w, c); } else { foreach (c; raw) put(w, c); } return; } enforceFmt(std.algorithm.find("fgFGaAeEs", fs.spec).length, "floating"); version (Win64) { double tval = val; // convert early to get "inf" in case of overflow string s; if (isnan(tval)) s = "nan"; // snprintf writes 1.#QNAN else if (isInfinity(tval)) s = val >= 0 ? "inf" : "-inf"; // snprintf writes 1.#INF if (s.length > 0) { version(none) { return formatValue(w, s, f); } else // FIXME:workaroun { s = s[0 .. f.precision < $ ? f.precision : $]; if (!f.flDash) { // right align if (f.width > s.length) foreach (j ; 0 .. f.width - s.length) put(w, ' '); put(w, s); } else { // left align put(w, s); if (f.width > s.length) foreach (j ; 0 .. f.width - s.length) put(w, ' '); } return; } } } else alias val tval; if (fs.spec == 's') fs.spec = 'g'; 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++] = fs.spec; sprintfSpec[i] = 0; //printf("format: '%s'; geeba: %g\n", sprintfSpec.ptr, val); char[512] buf; immutable n = snprintf(buf.ptr, buf.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"); put(w, buf[0 .. strlen(buf.ptr)]); } /*@safe pure */unittest { foreach (T; TypeTuple!(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" ); } } 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" ); } /* Formatting a $(D creal) is deprecated but still kept around for a while. */ void formatValue(Writer, T, Char)(Writer w, T obj, ref FormatSpec!Char f) if (is(Unqual!T : creal) && !is(T == enum) && !hasToString!(T, Char)) { creal val = obj; formatValue(w, val.re, f); if (val.im >= 0) { put(w, '+'); } formatValue(w, val.im, f); put(w, 'i'); } /*@safe pure */unittest { foreach (T; TypeTuple!(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" ); } foreach (T; TypeTuple!(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" ); } } 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 $(D ireal) is deprecated but still kept around for a while. */ void formatValue(Writer, T, Char)(Writer w, T obj, ref FormatSpec!Char f) if (is(Unqual!T : ireal) && !is(T == enum) && !hasToString!(T, Char)) { ireal val = obj; formatValue(w, val.im, f); put(w, 'i'); } /*@safe pure */unittest { foreach (T; TypeTuple!(ifloat, idouble, ireal)) { formatTest( to!( T)(1i), "1i" ); formatTest( to!( const T)(1i), "1i" ); formatTest( to!(immutable T)(1i), "1i" ); } } 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 ($(D char), $(D wchar), or $(D dchar)) are formatted as Unicode characters with %s and as integers with integral-specific format specs. */ void formatValue(Writer, T, Char)(Writer w, T obj, ref FormatSpec!Char f) if (is(CharTypeOf!T) && !is(T == enum) && !hasToString!(T, Char)) { CharTypeOf!T val = obj; if (f.spec == 's' || f.spec == 'c') { put(w, val); } else { alias U = TypeTuple!(ubyte, ushort, uint)[CharTypeOf!T.sizeof/2]; formatValue(w, cast(U) val, f); } } @safe pure unittest { assertCTFEable!( { formatTest( 'c', "c" ); }); } 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 pure 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'] ); } /** Strings are formatted like $(D printf) does. */ void formatValue(Writer, T, Char)(Writer w, T obj, 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); } unittest { formatTest( "abc", "abc" ); } 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" ); } 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'] ); } /** Static-size arrays are formatted as dynamic arrays. */ void formatValue(Writer, T, Char)(Writer w, auto ref T obj, ref FormatSpec!Char f) if (is(StaticArrayTypeOf!T) && !is(T == enum) && !hasToString!(T, Char)) { formatValue(w, obj[], f); } unittest // Test for issue 8310 { 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. Specializations: $(UL $(LI $(D void[]) is formatted like $(D ubyte[]).) $(LI Const array is converted to input range by removing its qualifier.)) */ void formatValue(Writer, T, Char)(Writer w, T obj, 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[]))) { formatValue(w, cast(const ubyte[])obj, f); } else static if (!isInputRange!T) { alias U = Unqual!(ArrayTypeOf!T); static assert(isInputRange!U); U val = obj; formatValue(w, val, f); } else { formatRange(w, obj, f); } } // alias this, input range I/F, and toString() 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"); } 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]" ); } 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]" ); } unittest { // 6640 struct Range { string value; const @property bool empty(){ return !value.length; } const @property dchar front(){ return value.front; } void popFront(){ value.popFront(); } const @property size_t length(){ 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]); } } unittest { // string literal from valid UTF sequence is encoding free. foreach (StrType; TypeTuple!(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]` ); } } unittest { // nested range formatting with array of string formatTest( "%({%(%02x %)}%| %)", ["test", "msg"], `{74 65 73 74} {6d 73 67}` ); } 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, ref FormatSpec!Char f) if (isInputRange!T) { // 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)) { auto s = val[0 .. f.precision < $ ? f.precision : $]; if (!f.flDash) { // right align if (f.width > s.length) foreach (i ; 0 .. f.width - s.length) put(w, ' '); put(w, s); } else { // left align put(w, s); if (f.width > s.length) foreach (i ; 0 .. f.width - s.length) put(w, ' '); } } 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 { enforce(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); } } 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 (!isInfinite!T) put(w, f.seqAfter); } } else if (f.spec == 'r') { static if (is(DynamicArrayTypeOf!T)) { alias ARR = DynamicArrayTypeOf!T; foreach (e ; cast(ARR)val) { formatValue(w, e, f); } } else { for (size_t i; !val.empty; val.popFront(), ++i) { formatValue(w, val.front, f); } } } else if (f.spec == '(') { if (val.empty) return; // Nested specifier is to be used for (;;) { auto fmt = FormatSpec!Char(f.nested); fmt.writeUpToNextSpec(w); if (f.flDash) formatValue(w, val.front, fmt); else formatElement(w, val.front, fmt); if (f.sep.ptr) { 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 Exception(text("Incorrect format specifier for range: %", f.spec)); } // character formatting with ecaping private void formatChar(Writer)(Writer w, in dchar c, in char quote) { import std.uni : isGraphical; if (std.uni.isGraphical(c)) { if (c == quote || c == '\\') { put(w, '\\'); put(w, c); } else put(w, c); } else if (c <= 0xFF) { put(w, '\\'); switch (c) { case '\0': put(w, '0'); break; case '\a': put(w, 'a'); break; case '\b': put(w, 'b'); break; case '\f': put(w, 'f'); break; case '\n': put(w, 'n'); break; case '\r': put(w, 'r'); break; case '\t': put(w, 't'); break; case '\v': put(w, 'v'); break; default: formattedWrite(w, "x%02X", cast(uint)c); } } else if (c <= 0xFFFF) formattedWrite(w, "\\u%04X", cast(uint)c); else formattedWrite(w, "\\U%08X", cast(uint)c); } // undocumented // string elements are formatted like UTF-8 string literals. void formatElement(Writer, T, Char)(Writer w, T val, ref FormatSpec!Char f) if (is(StringTypeOf!T) && !is(T == enum)) { StringTypeOf!T str = val; // bug 8015 if (f.spec == 's') { try { // ignore other specifications and quote auto app = appender!(typeof(val[0])[])(); put(app, '\"'); for (size_t i = 0; i < str.length; ) { auto c = std.utf.decode(str, i); // \uFFFE and \uFFFF are considered valid by isValidDchar, // so need checking for interchange. if (c == 0xFFFE || c == 0xFFFF) goto LinvalidSeq; formatChar(app, c, '"'); } put(app, '\"'); put(w, app.data); 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); } unittest { // Test for bug 8015 import std.typecons; struct MyStruct { string str; @property string toStr() { return str; } alias toStr this; } Tuple!(MyStruct) t; } // undocumented // character elements are formatted like UTF-8 character literals. void formatElement(Writer, T, Char)(Writer w, T val, 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); } // undocumented // Maybe T is noncopyable struct, so receive it by 'auto ref'. void formatElement(Writer, T, Char)(Writer w, auto ref T val, ref FormatSpec!Char f) if (!is(StringTypeOf!T) && !is(CharTypeOf!T) || is(T == enum)) { formatValue(w, val, f); } /** Associative arrays are formatted by using $(D ':') and $(D ", ") as separators, and enclosed by $(D '[') and $(D ']'). */ void formatValue(Writer, T, Char)(Writer w, T obj, ref FormatSpec!Char f) if (is(AssocArrayTypeOf!T) && !is(T == enum) && !hasToString!(T, Char)) { AssocArrayTypeOf!T val = obj; enforceFmt(f.spec == 's' || f.spec == '(', "associative"); enum const(Char)[] defSpec = "%s" ~ f.keySeparator ~ "%s" ~ f.seqSeparator; auto fmtSpec = f.spec == '(' ? f.nested : defSpec; size_t i = 0, end = val.length; if (f.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 (f.spec == 's') put(w, f.seqAfter); } unittest { 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]}`] ); // issue 12135 formatTest("%(%s:<%s>%|,%)", [1:2], "1:<2>"); formatTest("%(%s:<%s>%|%)" , [1:2], "1:<2>"); } 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" ); } unittest // Issue 8921 { 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]"); } 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 = 0; } else static if (is(typeof({ T val = void; FormatSpec!Char f; val.toString((const(char)[] s){}, f); }))) { enum hasToString = 4; } else static if (is(typeof({ T val = void; val.toString((const(char)[] s){}, "%s"); }))) { enum hasToString = 3; } else static if (is(typeof({ T val = void; val.toString((const(char)[] s){}); }))) { enum hasToString = 2; } else static if (is(typeof({ T val = void; return val.toString(); }()) S) && isSomeString!S) { enum hasToString = 1; } else { enum hasToString = 0; } } // object formatting with toString private void formatObject(Writer, T, Char)(ref Writer w, ref T val, ref FormatSpec!Char f) if (hasToString!(T, Char)) { static if (is(typeof(val.toString((const(char)[] s){}, f)))) { val.toString((const(char)[] s) { put(w, s); }, f); } else static if (is(typeof(val.toString((const(char)[] s){}, "%s")))) { val.toString((const(char)[] s) { put(w, s); }, f.getCurFmtStr()); } else static if (is(typeof(val.toString((const(char)[] s){})))) { val.toString((const(char)[] s) { put(w, s); }); } else static if (is(typeof(val.toString()) S) && isSomeString!S) { put(w, val.toString()); } else static assert(0); } void enforceValidFormatSpec(T, Char)(ref FormatSpec!Char f) { static if (!isInputRange!T && hasToString!(T, Char) != 4) { enforceFmt(f.spec == 's', "Expected '%s' format specifier for type '" ~ T.stringof ~ "'"); } } 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(const(char)[]) sink, FormatSpec!char) const; } static class CIK1 : IK1 { override void toString(scope void delegate(const(char)[]) sink, FormatSpec!char) const { sink("CIK1"); } } static struct KS1 { void toString(scope void delegate(const(char)[]) sink, FormatSpec!char) const { sink("KS1"); } } static union KU1 { void toString(scope void delegate(const(char)[]) sink, FormatSpec!char) const { sink("KU1"); } } static class KC1 { void toString(scope void delegate(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 ($(D struct), $(D union), $(D class), and $(D interface)) are basically formatted by calling $(D toString). $(D toString) should have one of the following signatures: --- const void toString(scope void delegate(const(char)[]) sink, FormatSpec fmt); const void toString(scope void delegate(const(char)[]) sink, string fmt); const void toString(scope void delegate(const(char)[]) sink); const string toString(); --- For the class objects which have input range interface, $(UL $(LI If the instance $(D toString) has overridden $(D Object.toString), it is used.) $(LI Otherwise, the objects are formatted as input range.)) For the struct and union objects which does not have $(D toString), $(UL $(LI If they have range interface, formatted as input range.) $(LI Otherwise, they are formatted like $(D Type(field1, filed2, ...)).)) Otherwise, are formatted just as their type name. */ void formatValue(Writer, T, Char)(Writer w, T val, ref FormatSpec!Char f) if (is(T == class) && !is(T == enum)) { enforceValidFormatSpec!(T, Char)(f); // TODO: Change this once toString() works for shared objects. static assert(!is(T == shared), "unable to format shared objects"); if (val is null) put(w, "null"); else { static if (hasToString!(T, Char) > 1 || (!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; if (dg.funcptr != &Object.toString) // 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; formatValue(w, x, f); } else { formatObject(w, val, f); } } } } unittest { // class range (issue 5154) auto c = inputRangeObject([1,2,3,4]); formatTest( c, "[1, 2, 3, 4]" ); assert(c.empty); c = null; formatTest( c, "null" ); } unittest { // 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(const(char)[]) dg, 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]" ); } /// ditto void formatValue(Writer, T, Char)(Writer w, T val, 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 (hasToString!(T, Char)) { formatObject(w, val, f); } else static if (isInputRange!T) { formatRange(w, val, f); } else { version (Windows) { import std.c.windows.com : IUnknown; static if (is(T : IUnknown)) { formatValue(w, *cast(void**)&val, f); } else { formatValue(w, cast(Object)val, f); } } else { formatValue(w, cast(Object)val, f); } } } } unittest { // interface 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" ); // Issue 11175 version (Windows) { import core.sys.windows.windows : HRESULT; import std.c.windows.com : IUnknown, IID; 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'. void formatValue(Writer, T, Char)(Writer w, auto ref T val, ref FormatSpec!Char f) if ((is(T == struct) || is(T == union)) && (hasToString!(T, Char) || !is(BuiltinTypeOf!T)) && !is(T == enum)) { 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 (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); } } unittest { // bug 4638 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" ); } unittest { // 3890 struct Int{ int n; } struct Pair{ string s; Int i; } formatTest( Pair("hello", Int(5)), `Pair("hello", Int(5))` ); } 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" ); } unittest { // 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)`); } unittest { static struct S{ @disable this(this); } S s; FormatSpec!char f; auto w = appender!string(); formatValue(w, s, f); assert(w.data == "S()"); } /** $(D enum) is formatted like its base value. */ void formatValue(Writer, T, Char)(Writer w, T val, ref FormatSpec!Char f) if (is(T == enum)) { if (f.spec == 's') { foreach (i, e; EnumMembers!T) { if (val == e) { formatValue(w, __traits(allMembers, T)[i], f); return; } } // val is not a member of T, output cast(T)rawValue instead. put(w, "cast(" ~ T.stringof ~ ")"); static assert(!is(OriginalType!T == T)); } formatValue(w, cast(OriginalType!T)val, f); } unittest { enum A { first, second, third } formatTest( A.second, "second" ); formatTest( cast(A)72, "cast(A)72" ); } unittest { enum A : string { one = "uno", two = "dos", three = "tres" } formatTest( A.three, "three" ); formatTest( cast(A)"mill\ón", "cast(A)mill\ón" ); } unittest { enum A : bool { no, yes } formatTest( A.yes, "yes" ); formatTest( A.no, "no" ); } 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"); } /** Pointers are formatted as hex integers. */ void formatValue(Writer, T, Char)(Writer w, T val, ref FormatSpec!Char f) if (isPointer!T && !is(T == enum) && !hasToString!(T, Char)) { static if (isInputRange!T) { if (val !is null) { formatRange(w, *val, f); return; } } 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) { put(w, "null"); return; } FormatSpec!Char fs = f; // fs is copy for change its values. fs.spec = 'X'; formatValue(w, pnum, fs); } else { enforceFmt(f.spec == 'X' || f.spec == 'x', "Expected one of %s, %x or %X for pointer type."); formatValue(w, pnum, f); } } @safe pure unittest { // pointer auto r = retro([1,2,3,4]); auto p = ()@trusted{ auto p = &r; return p; }(); formatTest( p, "[4, 3, 2, 1]" ); assert(p.empty); p = null; formatTest( p, "null" ); auto q = ()@trusted{ return cast(void*)0xFFEECCAA; }(); formatTest( q, "FFEECCAA" ); } unittest { // Test for issue 7869 struct S { string toString() const { return ""; } } S* p = null; formatTest( p, "null" ); S* q = cast(S*)0xFFEECCAA; formatTest( q, "FFEECCAA" ); } unittest { // Test for issue 8186 class B { int*a; this(){ a = new int; } alias a this; } formatTest( B.init, "null" ); } unittest { // Test for issue 9336 shared int i; format("%s", &i); } unittest { // Test for issue 11778 int* p = null; assertThrown(format("%d", p)); assertThrown(format("%04d", p + 2)); } @safe pure unittest { // Test for issue 12505 void* p = null; formatTest( "%08X", p, "00000000" ); } /** Delegates are formatted by 'Attributes ReturnType delegate(Parameters)' */ void formatValue(Writer, T, Char)(Writer w, T val, ref FormatSpec!Char f) if (is(T == delegate) && !is(T == enum) && !hasToString!(T, Char)) { alias FA = FunctionAttribute; if (functionAttributes!T & FA.pure_) formatValue(w, "pure ", f); if (functionAttributes!T & FA.nothrow_) formatValue(w, "nothrow ", f); if (functionAttributes!T & FA.ref_) formatValue(w, "ref ", f); if (functionAttributes!T & FA.property) formatValue(w, "@property ", f); if (functionAttributes!T & FA.trusted) formatValue(w, "@trusted ", f); if (functionAttributes!T & FA.safe) formatValue(w, "@safe ", f); formatValue(w, ReturnType!T.stringof, f); formatValue(w, " delegate", f); formatValue(w, ParameterTypeTuple!T.stringof, f); } unittest { void func() {} formatTest( &func, "void delegate()" ); } /* Formatting a $(D typedef) is deprecated but still kept around for a while. */ void formatValue(Writer, T, Char)(Writer w, T val, ref FormatSpec!Char f) if (is(T == typedef)) { static if (is(T U == typedef)) { formatValue(w, cast(U) val, f); } } /* Formats an object of type 'D' according to 'f' and writes it to 'w'. The pointer 'arg' is assumed to point to an object of type 'D'. The untyped signature is for the sake of taking this function's address. */ private void formatGeneric(Writer, D, Char)(Writer w, const(void)* arg, ref FormatSpec!Char f) { formatValue(w, *cast(D*) arg, f); } private void formatNth(Writer, Char, A...)(Writer w, ref FormatSpec!Char f, size_t index, A args) { static string gencode(size_t count)() { string result; foreach (n; 0 .. count) { auto num = to!string(n); result ~= "case "~num~":"~ " formatValue(w, args["~num~"], f);"~ " break;"; } return result; } switch (index) { mixin(gencode!(A.length)()); default: assert(0, "n = "~cast(char)(index + '0')); } } 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 issue 1591 private int getNthInt(A...)(uint index, A args) { static if (A.length) { if (index) { return getNthInt(index - 1, args[1 .. $]); } static if (isIntegral!(typeof(args[0]))) { return to!int(args[0]); } else { throw new FormatException("int expected"); } } else { throw new FormatException("int expected"); } } /* ======================== Unit Tests ====================================== */ version(unittest) void formatTest(T)(T val, string expected, size_t ln = __LINE__, string fn = __FILE__) { FormatSpec!char f; auto w = appender!string(); formatValue(w, val, f); enforceEx!AssertError( w.data == expected, text("expected = `", expected, "`, result = `", w.data, "`"), fn, ln); } version(unittest) void formatTest(T)(string fmt, T val, string expected, size_t ln = __LINE__, string fn = __FILE__) { auto w = appender!string(); formattedWrite(w, fmt, val); enforceEx!AssertError( w.data == expected, text("expected = `", expected, "`, result = `", w.data, "`"), fn, ln); } version(unittest) void formatTest(T)(T val, string[] expected, size_t ln = __LINE__, string fn = __FILE__) { FormatSpec!char f; auto w = appender!string(); formatValue(w, val, f); foreach(cur; expected) { if(w.data == cur) return; } enforceEx!AssertError( false, text("expected one of `", expected, "`, result = `", w.data, "`"), fn, ln); } version(unittest) void formatTest(T)(string fmt, T val, string[] expected, size_t ln = __LINE__, string fn = __FILE__) { auto w = appender!string(); formattedWrite(w, fmt, val); foreach(cur; expected) { if(w.data == cur) return; } enforceEx!AssertError( false, text("expected one of `", expected, "`, result = `", w.data, "`"), fn, ln); } unittest { auto stream = appender!string(); formattedWrite(stream, "%s", 1.1); assert(stream.data == "1.1", stream.data); 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"); } unittest { auto stream = appender!string(); formattedWrite(stream, "%u", 42); assert(stream.data == "42", stream.data); } unittest { // testing raw writes 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); } unittest { // testing positional parameters 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); 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); } unittest { 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); // std.c.stdio.fwrite(stream.data.ptr, stream.data.length, 1, stderr); /* The host C library is used to format floats. C99 doesn't * specify what the hex digit before the decimal point is for * %A. */ version (linux) { assert(stream.data == "1.67 -0X1.47AE147AE147BP+0 nan", stream.data); } else version (OSX) { assert(stream.data == "1.67 -0X1.47AE147AE147BP+0 nan", stream.data); } else version (MinGW) { assert(stream.data == "1.67 -0XA.3D70A3D70A3D8P-3 nan", stream.data); } else version (Win64) { assert(stream.data == "1.67 -0X1.47AE14P+0 nan", stream.data); } else { 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); version (Win64) assert(stream.data == "0x1.51eb85p+0 0X1.B1EB86P+2"); else 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; //std.c.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"); // assert(false); // typedef int myint; // myint m = -7; // stream.clear(); formattedWrite(stream, "", m); // 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: void* p = cast(void*)0xDEADBEEF; stream.clear(); formattedWrite(stream, "%s", p); 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"); Object c = null; stream.clear(); formattedWrite(stream, "%s", c); assert(stream.data == "null"); 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); //std.c.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 ~ "<"); // systematic test const string[] flags = [ "-", "+", "#", "0", " ", "" ]; const string[] widths = [ "", "0", "4", "20" ]; const string[] precs = [ "", ".", ".0", ".4", ".20" ]; const string formats = "sdoxXeEfFgGaA"; /+ foreach (flag1; flags) foreach (flag2; flags) foreach (flag3; flags) foreach (flag4; flags) foreach (flag5; flags) foreach (width; widths) foreach (prec; precs) foreach (format; formats) { stream.clear(); auto fmt = "%" ~ flag1 ~ flag2 ~ flag3 ~ flag4 ~ flag5 ~ width ~ prec ~ format ~ '\0'; fmt = fmt[0 .. $ - 1]; // keep it zero-term char buf[256]; buf[0] = 0; switch (format) { case 's': formattedWrite(stream, fmt, "wyda"); snprintf(buf.ptr, buf.length, fmt.ptr, "wyda\0".ptr); break; case 'd': formattedWrite(stream, fmt, 456); snprintf(buf.ptr, buf.length, fmt.ptr, 456); break; case 'o': formattedWrite(stream, fmt, 345); snprintf(buf.ptr, buf.length, fmt.ptr, 345); break; case 'x': formattedWrite(stream, fmt, 63546); snprintf(buf.ptr, buf.length, fmt.ptr, 63546); break; case 'X': formattedWrite(stream, fmt, 12566); snprintf(buf.ptr, buf.length, fmt.ptr, 12566); break; case 'e': formattedWrite(stream, fmt, 3245.345234); snprintf(buf.ptr, buf.length, fmt.ptr, 3245.345234); break; case 'E': formattedWrite(stream, fmt, 3245.2345234); snprintf(buf.ptr, buf.length, fmt.ptr, 3245.2345234); break; case 'f': formattedWrite(stream, fmt, 3245234.645675); snprintf(buf.ptr, buf.length, fmt.ptr, 3245234.645675); break; case 'F': formattedWrite(stream, fmt, 213412.43); snprintf(buf.ptr, buf.length, fmt.ptr, 213412.43); break; case 'g': formattedWrite(stream, fmt, 234134.34); snprintf(buf.ptr, buf.length, fmt.ptr, 234134.34); break; case 'G': formattedWrite(stream, fmt, 23141234.4321); snprintf(buf.ptr, buf.length, fmt.ptr, 23141234.4321); break; case 'a': formattedWrite(stream, fmt, 21341234.2134123); snprintf(buf.ptr, buf.length, fmt.ptr, 21341234.2134123); break; case 'A': formattedWrite(stream, fmt, 1092384098.45234); snprintf(buf.ptr, buf.length, fmt.ptr, 1092384098.45234); break; default: break; } auto exp = buf[0 .. strlen(buf.ptr)]; if (stream.data != exp) { writeln("Format: \"", fmt, '"'); writeln("Expected: >", exp, "<"); writeln("Actual: >", stream.data, "<"); assert(false); } }+/ } unittest { immutable(char[5])[int] aa = ([3:"hello", 4:"betty"]); if (false) writeln(aa.keys); assert(aa[3] == "hello"); assert(aa[4] == "betty"); // if (false) // { // writeln(aa.values[0]); // writeln(aa.values[1]); // writefln("%s", typeid(typeof(aa.values))); // writefln("%s", aa[3]); // writefln("%s", aa[4]); // writefln("%s", aa.values); // //writefln("%s", aa); // wstring a = "abcd"; // writefln(a); // dstring b = "abcd"; // writefln(b); // } auto stream = appender!(char[])(); alias AllNumerics = TypeTuple!(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"); } //auto r = std.string.format("%s", aa.values); stream.clear(); formattedWrite(stream, "%s", aa); //assert(stream.data == "[3:[h,e,l,l,o],4:[b,e,t,t,y]]", stream.data); // r = std.string.format("%s", aa); // assert(r == "[3:[h,e,l,l,o],4:[b,e,t,t,y]]"); } 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(unittest) void formatReflectTest(T)(ref T val, string fmt, string formatted, string fn = __FILE__, size_t ln = __LINE__) { auto w = appender!string(); formattedWrite(w, fmt, val); auto input = w.data; enforceEx!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; } enforceEx!AssertError( val == val2, input, fn, ln); } version(unittest) void formatReflectTest(T)(ref T val, string fmt, string[] formatted, string fn = __FILE__, size_t ln = __LINE__) { auto w = appender!string(); formattedWrite(w, fmt, val); auto input = w.data; foreach(cur; formatted) { if(input == cur) return; } enforceEx!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; } enforceEx!AssertError( val == val2, input, fn, ln); } 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>%| $ %)]", `[
$(I FormatSpecification): $(B '%%') $(B '%') $(I Flags) $(I Width) $(I Precision) $(I FormatChar) $(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 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 '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')