| // 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). |
| |
| 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 Digital Mars 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 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 $(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 $(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 |
| $(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)') |
| $(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 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.))) |
| |
| $(DL |
| $(DT $(I Width)) |
| $(DD |
| 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 $(D "true") or $(D "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), |
| ±, followed by at least a two digit exponent: |
| $(I d.dddddd)e$(I ±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 ±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 ±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 |
| $(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: |
| $(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; |
| import std.format : formattedWrite; |
| |
| 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, in 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); |
| 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); |
| 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.891,011"); |
| 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"); |
| } |
| |
| /** |
| Reads characters from input range $(D r), converts them according |
| to $(D fmt), and writes them to $(D 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: |
| An `Exception` if `S.length == 0` and `fmt` has format specifiers. |
| */ |
| uint formattedRead(alias fmt, R, S...)(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...)(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); |
| enforce(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(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]); |
| } |
| |
| 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. |
| */ |
| 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 $(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; |
| |
| /** |
| 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. $(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; |
| |
| /** |
| The format specifier contained a $(D ',') |
| */ |
| 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 $(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 "%|"). |
| `sep is null` means no separator else `sep.empty` means 0 length |
| separator. |
| */ |
| 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)(ref 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; |
| } |
| |
| @safe unittest |
| { |
| import std.array; |
| 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; |
| 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 Exception( |
| 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 Exception(text("Incorrect format specifier: ", trailing)); |
| } |
| |
| //-------------------------------------------------------------------------- |
| private bool readUpToNextSpec(R)(ref R r) |
| { |
| 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); |
| // Require a '%' |
| if (r.front != '%') break; |
| trailing = trailing[2 .. $]; |
| r.popFront(); |
| } |
| else |
| { |
| enforce(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 |
| { |
| enforce(!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; |
| } |
| |
| @safe unittest |
| { |
| // issue 5237 |
| 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"); |
| } |
| |
| 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; |
| } |
| |
| 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, |
| "\nflSeparator = ", flSeparator, |
| "\nnested = ", nested, |
| "\ntrailing = ", trailing, "\n"); |
| } |
| } |
| |
| /// |
| @safe pure unittest |
| { |
| import std.array; |
| 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'); |
| } |
| |
| // Issue 14059 |
| @safe unittest |
| { |
| import std.array : appender; |
| auto a = appender!(string)(); |
| |
| auto f = FormatSpec!char("%-(%s%"); // %)") |
| assertThrown(f.writeUpToNextSpec(a)); |
| |
| f = FormatSpec!char("%(%-"); // %)") |
| assertThrown(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); |
| } |
| |
| /** |
| Helper function that returns a $(D FormatSpec) for a single specifier given |
| in $(D fmt). |
| |
| Params: |
| fmt = A format specifier. |
| |
| Returns: |
| A $(D FormatSpec) with the specifier parsed. |
| Throws: |
| An `Exception` when more than one specifier is given or the specifier |
| is malformed. |
| */ |
| FormatSpec!Char singleSpec(Char)(Char[] fmt) |
| { |
| import std.conv : text; |
| enforce(fmt.length >= 2, "fmt must be at least 2 characters long"); |
| enforce(fmt.front == '%', "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, |
| 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(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. |
| |
| Params: |
| w = The $(D OutputRange) to write to. |
| obj = The value to write. |
| f = The $(D FormatSpec) defining how to write the value. |
| */ |
| void formatValue(Writer, T, Char)(auto ref Writer w, T obj, const 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 |
| { |
| import std.array : appender; |
| auto w = appender!string(); |
| auto spec = singleSpec("%s"); |
| formatValue(w, true, spec); |
| |
| assert(w.data == "true"); |
| } |
| |
| @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]"); |
| } |
| |
| /** |
| $(D null) literal is formatted as $(D "null"). |
| |
| Params: |
| w = The $(D OutputRange) to write to. |
| obj = The value to write. |
| f = The $(D FormatSpec) defining how to write the value. |
| */ |
| void formatValue(Writer, T, Char)(auto ref Writer w, T obj, const ref FormatSpec!Char f) |
| if (is(Unqual!T == typeof(null)) && !is(T == enum) && !hasToString!(T, Char)) |
| { |
| enforceFmt(f.spec == 's', |
| "null literal cannot match %" ~ f.spec); |
| |
| put(w, "null"); |
| } |
| |
| /// |
| @safe pure unittest |
| { |
| import std.array : appender; |
| auto w = appender!string(); |
| auto spec = singleSpec("%s"); |
| formatValue(w, null, spec); |
| |
| assert(w.data == "null"); |
| } |
| |
| @safe pure unittest |
| { |
| assert(collectExceptionMsg!FormatException(format("%p", null)).back == 'p'); |
| |
| assertCTFEable!( |
| { |
| formatTest( null, "null" ); |
| }); |
| } |
| |
| /** |
| Integrals are formatted like $(D printf) does. |
| |
| Params: |
| w = The $(D OutputRange) to write to. |
| obj = The value to write. |
| f = The $(D FormatSpec) defining how to write the value. |
| */ |
| void formatValue(Writer, T, Char)(auto ref Writer w, T obj, 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 |
| |
| 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 (needToSwapEndianess(f)) |
| { |
| foreach_reverse (c; raw) |
| put(w, c); |
| } |
| else |
| { |
| foreach (c; raw) |
| put(w, c); |
| } |
| return; |
| } |
| |
| immutable 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, |
| "incompatible format character for integral argument: %" ~ f.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); |
| } |
| |
| /// |
| @safe pure unittest |
| { |
| import std.array : appender; |
| auto w = appender!string(); |
| auto spec = singleSpec("%d"); |
| formatValue(w, 1337, spec); |
| |
| assert(w.data == "1337"); |
| } |
| |
| private void formatIntegral(Writer, T, Char)(ref Writer w, const(T) val, 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, 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 ptrdiff_t spacesToPrint = |
| fs.width - ( |
| (prefix1 != 0) |
| + (prefix2 != 0) |
| + zerofill |
| + digits.length |
| + ((fs.flSeparator != 0) * (digits.length / fs.separators)) |
| ); |
| 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); |
| |
| foreach (i ; 0 .. zerofill) |
| put(w, '0'); |
| |
| if (fs.flSeparator) |
| { |
| for (size_t j = 0; j < digits.length; ++j) |
| { |
| if (j != 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, ' '); |
| } |
| |
| @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" ); |
| } |
| |
| // bugzilla 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(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"); |
| } |
| |
| private enum ctfpMessage = "Cannot format floating point types at compile-time"; |
| |
| /** |
| Floating-point values are formatted like $(D printf) does. |
| |
| Params: |
| w = The $(D OutputRange) to write to. |
| obj = The value to write. |
| f = The $(D FormatSpec) defining how to write the value. |
| */ |
| void formatValue(Writer, T, Char)(auto ref Writer w, T obj, 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; |
| |
| 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 (needToSwapEndianess(f)) |
| { |
| foreach_reverse (c; raw) |
| put(w, c); |
| } |
| else |
| { |
| foreach (c; raw) |
| put(w, c); |
| } |
| return; |
| } |
| enforceFmt(find("fgFGaAeEs", fs.spec).length, |
| "incompatible format character for floating point argument: %" ~ fs.spec); |
| enforceFmt(!__ctfe, ctfpMessage); |
| |
| version (CRuntime_Microsoft) |
| { |
| import std.math : isNaN, isInfinity; |
| immutable 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:workaround |
| { |
| 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 tval = val; |
| 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 = void; |
| |
| immutable n = ()@trusted{ |
| import core.stdc.stdio : snprintf; |
| return 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"); |
| |
| auto len = min(n, buf.length-1); |
| ptrdiff_t dot = buf[0 .. len].indexOf('.'); |
| if (fs.flSeparator && dot != -1) |
| { |
| ptrdiff_t firstDigit = buf[0 .. len].indexOfAny("0123456789"); |
| ptrdiff_t ePos = buf[0 .. len].indexOf('e'); |
| size_t j; |
| |
| ptrdiff_t firstLen = dot - firstDigit; |
| |
| size_t separatorScoreCnt = firstLen / fs.separators; |
| |
| size_t afterDotIdx; |
| if (ePos != -1) |
| { |
| afterDotIdx = ePos; |
| } |
| else |
| { |
| afterDotIdx = len; |
| } |
| |
| if (dot != -1) |
| { |
| ptrdiff_t mantissaLen = afterDotIdx - (dot + 1); |
| separatorScoreCnt += (mantissaLen > 0) ? (mantissaLen - 1) / fs.separators : 0; |
| } |
| |
| // plus, minus prefix |
| ptrdiff_t digitsBegin = buf[0 .. separatorScoreCnt].indexOfNeither(" "); |
| if (digitsBegin == -1) |
| { |
| digitsBegin = separatorScoreCnt; |
| } |
| put(w, buf[digitsBegin .. firstDigit]); |
| |
| // digits until dot with separator |
| for (j = 0; j < firstLen; ++j) |
| { |
| if (j > 0 && (firstLen - j) % fs.separators == 0) |
| { |
| put(w, fs.separatorChar); |
| } |
| put(w, buf[j + firstDigit]); |
| } |
| put(w, '.'); |
| |
| // digits after dot |
| for (j = dot + 1; j < afterDotIdx; ++j) |
| { |
| auto realJ = (j - (dot + 1)); |
| if (realJ != 0 && realJ % fs.separators == 0) |
| { |
| put(w, fs.separatorChar); |
| } |
| put(w, buf[j]); |
| } |
| |
| // rest |
| if (ePos != -1) |
| { |
| put(w, buf[afterDotIdx .. len]); |
| } |
| } |
| else |
| { |
| put(w, buf[0 .. len]); |
| } |
| } |
| |
| /// |
| @safe unittest |
| { |
| import std.array : appender; |
| auto w = appender!string(); |
| auto spec = singleSpec("%.1f"); |
| formatValue(w, 1337.7, spec); |
| |
| assert(w.data == "1337.7"); |
| } |
| |
| @safe /*pure*/ unittest // formatting floating point values is now impure |
| { |
| import std.conv : to; |
| |
| assert(collectExceptionMsg!FormatException(format("%d", 5.1)).back == 'd'); |
| |
| 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" ); |
| } |
| |
| /* |
| Formatting a $(D creal) is deprecated but still kept around for a while. |
| |
| Params: |
| w = The $(D OutputRange) to write to. |
| obj = The value to write. |
| f = The $(D FormatSpec) defining how to write the value. |
| */ |
| void formatValue(Writer, T, Char)(auto ref Writer w, T obj, const ref FormatSpec!Char f) |
| if (is(Unqual!T : creal) && !is(T == enum) && !hasToString!(T, Char)) |
| { |
| immutable 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 // formatting floating point values is now impure |
| { |
| import std.conv : to; |
| 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" ); |
| } |
| 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" ); |
| } |
| } |
| |
| @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 $(D ireal) is deprecated but still kept around for a while. |
| |
| Params: |
| w = The $(D OutputRange) to write to. |
| obj = The value to write. |
| f = The $(D FormatSpec) defining how to write the value. |
| */ |
| void formatValue(Writer, T, Char)(auto ref Writer w, T obj, const ref FormatSpec!Char f) |
| if (is(Unqual!T : ireal) && !is(T == enum) && !hasToString!(T, Char)) |
| { |
| immutable ireal val = obj; |
| |
| formatValue(w, val.im, f); |
| put(w, 'i'); |
| } |
| |
| @safe /*pure*/ unittest // formatting floating point values is now impure |
| { |
| import std.conv : to; |
| foreach (T; AliasSeq!(ifloat, idouble, ireal)) |
| { |
| formatTest( to!( T)(1i), "1i" ); |
| formatTest( to!( const T)(1i), "1i" ); |
| formatTest( to!(immutable T)(1i), "1i" ); |
| } |
| } |
| |
| @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 ($(D char), $(D wchar), or $(D dchar)) are formatted as |
| Unicode characters with %s and as integers with integral-specific format |
| specs. |
| |
| Params: |
| w = The $(D OutputRange) to write to. |
| obj = The value to write. |
| f = The $(D FormatSpec) defining how to write the value. |
| */ |
| void formatValue(Writer, T, Char)(auto ref Writer w, T obj, const 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 = AliasSeq!(ubyte, ushort, uint)[CharTypeOf!T.sizeof/2]; |
| formatValue(w, cast(U) val, f); |
| } |
| } |
| |
| /// |
| @safe pure unittest |
| { |
| import std.array : appender; |
| auto w = appender!string(); |
| auto spec = singleSpec("%c"); |
| formatValue(w, 'a', spec); |
| |
| assert(w.data == "a"); |
| } |
| |
| @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'] ); |
| } |
| |
| /** |
| Strings are formatted like $(D printf) does. |
| |
| Params: |
| w = The $(D OutputRange) to write to. |
| obj = The value to write. |
| f = The $(D FormatSpec) defining how to write the value. |
| */ |
| void formatValue(Writer, T, Char)(auto ref Writer w, T obj, 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 pure unittest |
| { |
| import std.array : appender; |
| auto w = appender!string(); |
| auto spec = singleSpec("%s"); |
| formatValue(w, "hello", spec); |
| |
| assert(w.data == "hello"); |
| } |
| |
| @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'] ); |
| } |
| |
| /** |
| Static-size arrays are formatted as dynamic arrays. |
| |
| Params: |
| w = The $(D OutputRange) to write to. |
| obj = The value to write. |
| f = The $(D FormatSpec) defining how to write the value. |
| */ |
| void formatValue(Writer, T, Char)(auto ref Writer w, auto ref T obj, const ref FormatSpec!Char f) |
| if (is(StaticArrayTypeOf!T) && !is(T == enum) && !hasToString!(T, Char)) |
| { |
| formatValue(w, obj[], f); |
| } |
| |
| /// |
| @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"); |
| } |
| |
| @safe unittest // Test for issue 8310 |
| { |
| 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. |
| |
| Specializations: |
| $(UL $(LI $(D void[]) is formatted like $(D ubyte[]).) |
| $(LI Const array is converted to input range by removing its qualifier.)) |
| |
| Params: |
| w = The $(D OutputRange) to write to. |
| obj = The value to write. |
| f = The $(D FormatSpec) defining how to write the value. |
| */ |
| void formatValue(Writer, T, Char)(auto ref Writer w, T obj, 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[]))) |
| { |
| 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); |
| } |
| } |
| |
| /// |
| @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]"); |
| } |
| |
| // 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]" ); |
| } |
| |
| @safe unittest |
| { |
| // 6640 |
| 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. |
| 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, const ref FormatSpec!Char f) |
| if (isInputRange!T) |
| { |
| 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)) |
| { |
| 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); |
|