|
// 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); |
|
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 !is null) |
|
{ |
|
put(w, fmt.trailing); |
|
val.popFront(); |
|
if (val.empty) |
|
break; |
|
put(w, f.sep); |
|
} |
|
else |
|
{ |
|
val.popFront(); |
|
if (val.empty) |
|
break; |
|
put(w, fmt.trailing); |
|
} |
|
} |
|
} |
|
else |
|
throw new Exception(text("Incorrect format specifier for range: %", f.spec)); |
|
} |
|
|
|
@safe pure unittest |
|
{ |
|
assert(collectExceptionMsg(format("%d", "hi")).back == 'd'); |
|
} |
|
|
|
// character formatting with ecaping |
|
private void formatChar(Writer)(ref Writer w, in dchar c, in char quote) |
|
{ |
|
import std.uni : isGraphical; |
|
|
|
string fmt; |
|
if (isGraphical(c)) |
|
{ |
|
if (c == quote || c == '\\') |
|
put(w, '\\'); |
|
put(w, c); |
|
return; |
|
} |
|
else if (c <= 0xFF) |
|
{ |
|
if (c < 0x20) |
|
{ |
|
foreach (i, k; "\n\r\t\a\b\f\v\0") |
|
{ |
|
if (c == k) |
|
{ |
|
put(w, '\\'); |
|
put(w, "nrtabfv0"[i]); |
|
return; |
|
} |
|
} |
|
} |
|
fmt = "\\x%02X"; |
|
} |
|
else if (c <= 0xFFFF) |
|
fmt = "\\u%04X"; |
|
else |
|
fmt = "\\U%08X"; |
|
|
|
formattedWrite(w, fmt, cast(uint) c); |
|
} |
|
|
|
// undocumented because of deprecation |
|
// string elements are formatted like UTF-8 string literals. |
|
void formatElement(Writer, T, Char)(auto ref Writer w, T val, const ref FormatSpec!Char f) |
|
if (is(StringTypeOf!T) && !is(T == enum)) |
|
{ |
|
import std.array : appender; |
|
import std.utf : UTFException; |
|
|
|
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; ) |
|
{ |
|
import std.utf : decode; |
|
|
|
auto c = decode(str, i); |
|
// \uFFFE and \uFFFF are considered valid by isValidDchar, |
|
// so need checking for interchange. |
|
if (c == 0xFFFE || c == 0xFFFF) |
|
goto LinvalidSeq; |
|
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); |
|
} |
|
|
|
@safe pure unittest |
|
{ |
|
import std.array : appender; |
|
auto w = appender!string(); |
|
auto spec = singleSpec("%s"); |
|
formatElement(w, "Hello World", spec); |
|
|
|
assert(w.data == "\"Hello World\""); |
|
} |
|
|
|
@safe unittest |
|
{ |
|
// Test for bug 8015 |
|
import std.typecons; |
|
|
|
struct MyStruct { |
|
string str; |
|
@property string toStr() { |
|
return str; |
|
} |
|
alias toStr this; |
|
} |
|
|
|
Tuple!(MyStruct) t; |
|
} |
|
|
|
// undocumented because of deprecation |
|
// Character elements are formatted like UTF-8 character literals. |
|
void formatElement(Writer, T, Char)(auto ref Writer w, T val, const ref FormatSpec!Char f) |
|
if (is(CharTypeOf!T) && !is(T == enum)) |
|
{ |
|
if (f.spec == 's') |
|
{ |
|
put(w, '\''); |
|
formatChar(w, val, '\''); |
|
put(w, '\''); |
|
} |
|
else |
|
formatValue(w, val, f); |
|
} |
|
|
|
/// |
|
@safe unittest |
|
{ |
|
import std.array : appender; |
|
auto w = appender!string(); |
|
auto spec = singleSpec("%s"); |
|
formatElement(w, "H", spec); |
|
|
|
assert(w.data == "\"H\"", w.data); |
|
} |
|
|
|
// undocumented |
|
// Maybe T is noncopyable struct, so receive it by 'auto ref'. |
|
void formatElement(Writer, T, Char)(auto ref Writer w, auto ref T val, const ref FormatSpec!Char f) |
|
if (!is(StringTypeOf!T) && !is(CharTypeOf!T) || is(T == enum)) |
|
{ |
|
formatValue(w, val, f); |
|
} |
|
|
|
/** |
|
Associative arrays are formatted by using $(D ':') and $(D ", ") as |
|
separators, and enclosed by $(D '[') and $(D ']'). |
|
|
|
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(AssocArrayTypeOf!T) && !is(T == enum) && !hasToString!(T, Char)) |
|
{ |
|
AssocArrayTypeOf!T val = obj; |
|
|
|
enforceFmt(f.spec == 's' || f.spec == '(', |
|
"incompatible format character for associative array argument: %" ~ f.spec); |
|
|
|
enum const(Char)[] defSpec = "%s" ~ f.keySeparator ~ "%s" ~ f.seqSeparator; |
|
auto fmtSpec = f.spec == '(' ? f.nested : defSpec; |
|
|
|
size_t i = 0; |
|
immutable 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); |
|
} |
|
|
|
/// |
|
@safe pure unittest |
|
{ |
|
import std.array : appender; |
|
auto w = appender!string(); |
|
auto spec = singleSpec("%s"); |
|
auto aa = ["H":"W"]; |
|
formatElement(w, aa, spec); |
|
|
|
assert(w.data == "[\"H\":\"W\"]", w.data); |
|
} |
|
|
|
@safe unittest |
|
{ |
|
assert(collectExceptionMsg!FormatException(format("%d", [0:1])).back == 'd'); |
|
|
|
int[string] aa0; |
|
formatTest( aa0, `[]` ); |
|
|
|
// elements escaping |
|
formatTest( ["aaa":1, "bbb":2], |
|
[`["aaa":1, "bbb":2]`, `["bbb":2, "aaa":1]`] ); |
|
formatTest( ['c':"str"], |
|
`['c':"str"]` ); |
|
formatTest( ['"':"\"", '\'':"'"], |
|
[`['"':"\"", '\'':"'"]`, `['\'':"'", '"':"\""]`] ); |
|
|
|
// range formatting for AA |
|
auto aa3 = [1:"hello", 2:"world"]; |
|
// escape |
|
formatTest( "{%(%s:%s $ %)}", aa3, |
|
[`{1:"hello" $ 2:"world"}`, `{2:"world" $ 1:"hello"}`]); |
|
// use range formatting for key and value, and use %| |
|
formatTest( "{%([%04d->%(%c.%)]%| $ %)}", aa3, |
|
[`{[0001->h.e.l.l.o] $ [0002->w.o.r.l.d]}`, `{[0002->w.o.r.l.d] $ [0001->h.e.l.l.o]}`] ); |
|
|
|
// issue 12135 |
|
formatTest("%(%s:<%s>%|,%)", [1:2], "1:<2>"); |
|
formatTest("%(%s:<%s>%|%)" , [1:2], "1:<2>"); |
|
} |
|
|
|
@system unittest |
|
{ |
|
class C1 { int[char] val; alias val this; this(int[char] v){ val = v; } } |
|
class C2 { int[char] val; alias val this; this(int[char] v){ val = v; } |
|
override string toString() const { return "C"; } } |
|
formatTest( new C1(['c':1, 'd':2]), [`['c':1, 'd':2]`, `['d':2, 'c':1]`] ); |
|
formatTest( new C2(['c':1, 'd':2]), "C" ); |
|
|
|
struct S1 { int[char] val; alias val this; } |
|
struct S2 { int[char] val; alias val this; |
|
string toString() const { return "S"; } } |
|
formatTest( S1(['c':1, 'd':2]), [`['c':1, 'd':2]`, `['d':2, 'c':1]`] ); |
|
formatTest( S2(['c':1, 'd':2]), "S" ); |
|
} |
|
|
|
@safe 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, const 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)(const ref FormatSpec!Char f) |
|
{ |
|
static if (!isInputRange!T && hasToString!(T, Char) != 4) |
|
{ |
|
enforceFmt(f.spec == 's', |
|
"Expected '%s' format specifier for type '" ~ T.stringof ~ "'"); |
|
} |
|
} |
|
|
|
@system unittest |
|
{ |
|
static interface IF1 { } |
|
class CIF1 : IF1 { } |
|
static struct SF1 { } |
|
static union UF1 { } |
|
static class CF1 { } |
|
|
|
static interface IF2 { string toString(); } |
|
static class CIF2 : IF2 { override string toString() { return ""; } } |
|
static struct SF2 { string toString() { return ""; } } |
|
static union UF2 { string toString() { return ""; } } |
|
static class CF2 { override string toString() { return ""; } } |
|
|
|
static interface IK1 { void toString(scope void delegate(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)(auto ref Writer w, T val, const 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); |
|
} |
|
} |
|
} |
|
} |
|
|
|
/++ |
|
$(D formatValue) allows to reuse existing format specifiers: |
|
+/ |
|
@system unittest |
|
{ |
|
import std.format; |
|
|
|
struct Point |
|
{ |
|
int x, y; |
|
|
|
void toString(scope void delegate(const(char)[]) sink, |
|
FormatSpec!char fmt) const |
|
{ |
|
sink("("); |
|
sink.formatValue(x, fmt); |
|
sink(","); |
|
sink.formatValue(y, fmt); |
|
sink(")"); |
|
} |
|
} |
|
|
|
auto p = Point(16,11); |
|
assert(format("%03d", p) == "(016,011)"); |
|
assert(format("%02x", p) == "(10,0b)"); |
|
} |
|
|
|
/++ |
|
The following code compares the use of $(D formatValue) and $(D formattedWrite). |
|
+/ |
|
@safe pure unittest |
|
{ |
|
import std.array : appender; |
|
import std.format; |
|
|
|
auto writer1 = appender!string(); |
|
writer1.formattedWrite("%08b", 42); |
|
|
|
auto writer2 = appender!string(); |
|
auto f = singleSpec("%08b"); |
|
writer2.formatValue(42, f); |
|
|
|
assert(writer1.data == writer2.data && writer1.data == "00101010"); |
|
} |
|
|
|
@system unittest |
|
{ |
|
import std.array : appender; |
|
import std.range.interfaces; |
|
// 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" ); |
|
} |
|
|
|
@system 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, const ref FormatSpec!char f) const { dg("[012]"); } |
|
} |
|
class C2 |
|
{ |
|
mixin(inputRangeCode); |
|
void toString(scope void delegate(const(char)[]) dg, string f) const { dg("[012]"); } |
|
} |
|
class C3 |
|
{ |
|
mixin(inputRangeCode); |
|
void toString(scope void delegate(const(char)[]) dg) const { dg("[012]"); } |
|
} |
|
class C4 |
|
{ |
|
mixin(inputRangeCode); |
|
override string toString() const { return "[012]"; } |
|
} |
|
class C5 |
|
{ |
|
mixin(inputRangeCode); |
|
} |
|
|
|
formatTest( new C1([0, 1, 2]), "[012]" ); |
|
formatTest( new C2([0, 1, 2]), "[012]" ); |
|
formatTest( new C3([0, 1, 2]), "[012]" ); |
|
formatTest( new C4([0, 1, 2]), "[012]" ); |
|
formatTest( new C5([0, 1, 2]), "[0, 1, 2]" ); |
|
} |
|
|
|
/// ditto |
|
void formatValue(Writer, T, Char)(auto ref Writer w, T val, const ref FormatSpec!Char f) |
|
if (is(T == interface) && (hasToString!(T, Char) || !is(BuiltinTypeOf!T)) && !is(T == enum)) |
|
{ |
|
enforceValidFormatSpec!(T, Char)(f); |
|
if (val is null) |
|
put(w, "null"); |
|
else |
|
{ |
|
static if (hasToString!(T, Char)) |
|
{ |
|
formatObject(w, val, f); |
|
} |
|
else static if (isInputRange!T) |
|
{ |
|
formatRange(w, val, f); |
|
} |
|
else |
|
{ |
|
version (Windows) |
|
{ |
|
import core.sys.windows.com : IUnknown; |
|
static if (is(T : IUnknown)) |
|
{ |
|
formatValue(w, *cast(void**)&val, f); |
|
} |
|
else |
|
{ |
|
formatValue(w, cast(Object) val, f); |
|
} |
|
} |
|
else |
|
{ |
|
formatValue(w, cast(Object) val, f); |
|
} |
|
} |
|
} |
|
} |
|
|
|
@system unittest |
|
{ |
|
// interface |
|
import std.range.interfaces; |
|
InputRange!int i = inputRangeObject([1,2,3,4]); |
|
formatTest( i, "[1, 2, 3, 4]" ); |
|
assert(i.empty); |
|
i = null; |
|
formatTest( i, "null" ); |
|
|
|
// interface (downcast to Object) |
|
interface Whatever {} |
|
class C : Whatever |
|
{ |
|
override @property string toString() const { return "ab"; } |
|
} |
|
Whatever val = new C; |
|
formatTest( val, "ab" ); |
|
|
|
// Issue 11175 |
|
version (Windows) |
|
{ |
|
import core.sys.windows.com : IUnknown, IID; |
|
import core.sys.windows.windows : HRESULT; |
|
|
|
interface IUnknown2 : IUnknown { } |
|
|
|
class D : IUnknown2 |
|
{ |
|
extern(Windows) HRESULT QueryInterface(const(IID)* riid, void** pvObject) { return typeof(return).init; } |
|
extern(Windows) uint AddRef() { return 0; } |
|
extern(Windows) uint Release() { return 0; } |
|
} |
|
|
|
IUnknown2 d = new D; |
|
string expected = format("%X", cast(void*) d); |
|
formatTest(d, expected); |
|
} |
|
} |
|
|
|
/// ditto |
|
// Maybe T is noncopyable struct, so receive it by 'auto ref'. |
|
void formatValue(Writer, T, Char)(auto ref Writer w, auto ref T val, const 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); |
|
} |
|
} |
|
|
|
@safe 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" ); |
|
} |
|
|
|
@safe unittest |
|
{ |
|
// 3890 |
|
struct Int{ int n; } |
|
struct Pair{ string s; Int i; } |
|
formatTest( Pair("hello", Int(5)), |
|
`Pair("hello", Int(5))` ); |
|
} |
|
|
|
@system unittest |
|
{ |
|
// union formatting without toString |
|
union U1 |
|
{ |
|
int n; |
|
string s; |
|
} |
|
U1 u1; |
|
formatTest( u1, "U1" ); |
|
|
|
// union formatting with toString |
|
union U2 |
|
{ |
|
int n; |
|
string s; |
|
string toString() const { return s; } |
|
} |
|
U2 u2; |
|
u2.s = "hello"; |
|
formatTest( u2, "hello" ); |
|
} |
|
|
|
@system unittest |
|
{ |
|
import std.array; |
|
// 7230 |
|
static struct Bug7230 |
|
{ |
|
string s = "hello"; |
|
union { |
|
string a; |
|
int b; |
|
double c; |
|
} |
|
long x = 10; |
|
} |
|
|
|
Bug7230 bug; |
|
bug.b = 123; |
|
|
|
FormatSpec!char f; |
|
auto w = appender!(char[])(); |
|
formatValue(w, bug, f); |
|
assert(w.data == `Bug7230("hello", #{overlap a, b, c}, 10)`); |
|
} |
|
|
|
@safe unittest |
|
{ |
|
import std.array; |
|
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. |
|
|
|
Params: |
|
w = The $(D OutputRange) to write to. |
|
val = The value to write. |
|
f = The $(D FormatSpec) defining how to write the value. |
|
*/ |
|
void formatValue(Writer, T, Char)(auto ref Writer w, T val, const 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); |
|
} |
|
|
|
/// |
|
@safe pure unittest |
|
{ |
|
import std.array : appender; |
|
auto w = appender!string(); |
|
auto spec = singleSpec("%s"); |
|
|
|
enum A { first, second, third } |
|
|
|
formatElement(w, A.second, spec); |
|
|
|
assert(w.data == "second"); |
|
} |
|
|
|
@safe unittest |
|
{ |
|
enum A { first, second, third } |
|
formatTest( A.second, "second" ); |
|
formatTest( cast(A) 72, "cast(A)72" ); |
|
} |
|
@safe unittest |
|
{ |
|
enum A : string { one = "uno", two = "dos", three = "tres" } |
|
formatTest( A.three, "three" ); |
|
formatTest( cast(A)"mill\ón", "cast(A)mill\ón" ); |
|
} |
|
@safe unittest |
|
{ |
|
enum A : bool { no, yes } |
|
formatTest( A.yes, "yes" ); |
|
formatTest( A.no, "no" ); |
|
} |
|
@safe unittest |
|
{ |
|
// Test for bug 6892 |
|
enum Foo { A = 10 } |
|
formatTest("%s", Foo.A, "A"); |
|
formatTest(">%4s<", Foo.A, "> A<"); |
|
formatTest("%04d", Foo.A, "0010"); |
|
formatTest("%+2u", Foo.A, "+10"); |
|
formatTest("%02x", Foo.A, "0a"); |
|
formatTest("%3o", Foo.A, " 12"); |
|
formatTest("%b", Foo.A, "1010"); |
|
} |
|
|
|
/** |
|
Pointers are formatted as hex integers. |
|
*/ |
|
void formatValue(Writer, T, Char)(auto ref Writer w, T val, const 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 |
|
import std.range; |
|
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" ); |
|
} |
|
|
|
@system pure 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" ); |
|
} |
|
|
|
@system unittest |
|
{ |
|
// Test for issue 8186 |
|
class B |
|
{ |
|
int*a; |
|
this(){ a = new int; } |
|
alias a this; |
|
} |
|
formatTest( B.init, "null" ); |
|
} |
|
|
|
@system pure unittest |
|
{ |
|
// Test for issue 9336 |
|
shared int i; |
|
format("%s", &i); |
|
} |
|
|
|
@system pure 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 'ReturnType delegate(Parameters) FunctionAttributes' |
|
*/ |
|
void formatValue(Writer, T, Char)(auto ref Writer w, scope T, const ref FormatSpec!Char f) |
|
if (isDelegate!T) |
|
{ |
|
formatValue(w, T.stringof, f); |
|
} |
|
|
|
/// |
|
@safe pure unittest |
|
{ |
|
import std.conv : to; |
|
|
|
int i; |
|
|
|
int foo(short k) @nogc |
|
{ |
|
return i + k; |
|
} |
|
|
|
@system int delegate(short) @nogc bar() nothrow pure |
|
{ |
|
int* p = new int; |
|
return &foo; |
|
} |
|
|
|
assert(to!string(&bar) == "int delegate(short) @nogc delegate() pure nothrow @system"); |
|
} |
|
|
|
@safe unittest |
|
{ |
|
void func() @system { __gshared int x; ++x; throw new Exception("msg"); } |
|
version (linux) formatTest( &func, "void delegate() @system" ); |
|
} |
|
|
|
@safe pure unittest |
|
{ |
|
int[] a = [ 1, 3, 2 ]; |
|
formatTest( "testing %(%s & %) embedded", a, |
|
"testing 1 & 3 & 2 embedded"); |
|
formatTest( "testing %((%s) %)) wyda3", a, |
|
"testing (1) (3) (2) wyda3" ); |
|
|
|
int[0] empt = []; |
|
formatTest( "(%s)", empt, |
|
"([])" ); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
// Fix for issue 1591 |
|
private int getNthInt(string kind, A...)(uint index, A args) |
|
{ |
|
return getNth!(kind, isIntegral,int)(index, args); |
|
} |
|
|
|
private T getNth(string kind, alias Condition, T, A...)(uint index, A args) |
|
{ |
|
import std.conv : text, to; |
|
|
|
switch (index) |
|
{ |
|
foreach (n, _; A) |
|
{ |
|
case n: |
|
static if (Condition!(typeof(args[n]))) |
|
{ |
|
return to!T(args[n]); |
|
} |
|
else |
|
{ |
|
throw new FormatException( |
|
text(kind, " expected, not ", typeof(args[n]).stringof, |
|
" for argument #", index + 1)); |
|
} |
|
} |
|
default: |
|
throw new FormatException( |
|
text("Missing ", kind, " argument")); |
|
} |
|
} |
|
|
|
@safe unittest |
|
{ |
|
// width/precision |
|
assert(collectExceptionMsg!FormatException(format("%*.d", 5.1, 2)) |
|
== "integer width expected, not double for argument #1"); |
|
assert(collectExceptionMsg!FormatException(format("%-1*.d", 5.1, 2)) |
|
== "integer width expected, not double for argument #1"); |
|
|
|
assert(collectExceptionMsg!FormatException(format("%.*d", '5', 2)) |
|
== "integer precision expected, not char for argument #1"); |
|
assert(collectExceptionMsg!FormatException(format("%-1.*d", 4.7, 3)) |
|
== "integer precision expected, not double for argument #1"); |
|
assert(collectExceptionMsg!FormatException(format("%.*d", 5)) |
|
== "Orphan format specifier: %d"); |
|
assert(collectExceptionMsg!FormatException(format("%*.*d", 5)) |
|
== "Missing integer precision argument"); |
|
|
|
// separatorCharPos |
|
assert(collectExceptionMsg!FormatException(format("%,?d", 5)) |
|
== "separator character expected, not int for argument #1"); |
|
assert(collectExceptionMsg!FormatException(format("%,?d", '?')) |
|
== "Orphan format specifier: %d"); |
|
assert(collectExceptionMsg!FormatException(format("%.*,*?d", 5)) |
|
== "Missing separator digit width argument"); |
|
} |
|
|
|
/* ======================== Unit Tests ====================================== */ |
|
|
|
version (unittest) |
|
void formatTest(T)(T val, string expected, size_t ln = __LINE__, string fn = __FILE__) |
|
{ |
|
import core.exception : AssertError; |
|
import std.array : appender; |
|
import std.conv : text; |
|
FormatSpec!char f; |
|
auto w = appender!string(); |
|
formatValue(w, val, f); |
|
enforce!AssertError( |
|
w.data == expected, |
|
text("expected = `", expected, "`, result = `", w.data, "`"), fn, ln); |
|
} |
|
|
|
version (unittest) |
|
void formatTest(T)(string fmt, T val, string expected, size_t ln = __LINE__, string fn = __FILE__) @safe |
|
{ |
|
import core.exception : AssertError; |
|
import std.array : appender; |
|
import std.conv : text; |
|
auto w = appender!string(); |
|
formattedWrite(w, fmt, val); |
|
enforce!AssertError( |
|
w.data == expected, |
|
text("expected = `", expected, "`, result = `", w.data, "`"), fn, ln); |
|
} |
|
|
|
version (unittest) |
|
void formatTest(T)(T val, string[] expected, size_t ln = __LINE__, string fn = __FILE__) |
|
{ |
|
import core.exception : AssertError; |
|
import std.array : appender; |
|
import std.conv : text; |
|
FormatSpec!char f; |
|
auto w = appender!string(); |
|
formatValue(w, val, f); |
|
foreach (cur; expected) |
|
{ |
|
if (w.data == cur) return; |
|
} |
|
enforce!AssertError( |
|
false, |
|
text("expected one of `", expected, "`, result = `", w.data, "`"), fn, ln); |
|
} |
|
|
|
version (unittest) |
|
void formatTest(T)(string fmt, T val, string[] expected, size_t ln = __LINE__, string fn = __FILE__) @safe |
|
{ |
|
import core.exception : AssertError; |
|
import std.array : appender; |
|
import std.conv : text; |
|
auto w = appender!string(); |
|
formattedWrite(w, fmt, val); |
|
foreach (cur; expected) |
|
{ |
|
if (w.data == cur) return; |
|
} |
|
enforce!AssertError( |
|
false, |
|
text("expected one of `", expected, "`, result = `", w.data, "`"), fn, ln); |
|
} |
|
|
|
@safe /*pure*/ unittest // formatting floating point values is now impure |
|
{ |
|
import std.array; |
|
|
|
auto stream = appender!string(); |
|
formattedWrite(stream, "%s", 1.1); |
|
assert(stream.data == "1.1", stream.data); |
|
} |
|
|
|
@safe pure unittest |
|
{ |
|
import std.algorithm; |
|
import std.array; |
|
|
|
auto stream = appender!string(); |
|
formattedWrite(stream, "%s", map!"a*a"([2, 3, 5])); |
|
assert(stream.data == "[4, 9, 25]", stream.data); |
|
|
|
// Test shared data. |
|
stream = appender!string(); |
|
shared int s = 6; |
|
formattedWrite(stream, "%s", s); |
|
assert(stream.data == "6"); |
|
} |
|
|
|
@safe pure unittest |
|
{ |
|
import std.array; |
|
auto stream = appender!string(); |
|
formattedWrite(stream, "%u", 42); |
|
assert(stream.data == "42", stream.data); |
|
} |
|
|
|
@safe pure unittest |
|
{ |
|
// testing raw writes |
|
import std.array; |
|
auto w = appender!(char[])(); |
|
uint a = 0x02030405; |
|
formattedWrite(w, "%+r", a); |
|
assert(w.data.length == 4 && w.data[0] == 2 && w.data[1] == 3 |
|
&& w.data[2] == 4 && w.data[3] == 5); |
|
w.clear(); |
|
formattedWrite(w, "%-r", a); |
|
assert(w.data.length == 4 && w.data[0] == 5 && w.data[1] == 4 |
|
&& w.data[2] == 3 && w.data[3] == 2); |
|
} |
|
|
|
@safe pure unittest |
|
{ |
|
// testing positional parameters |
|
import std.array; |
|
auto w = appender!(char[])(); |
|
formattedWrite(w, |
|
"Numbers %2$s and %1$s are reversed and %1$s%2$s repeated", |
|
42, 0); |
|
assert(w.data == "Numbers 0 and 42 are reversed and 420 repeated", |
|
w.data); |
|
assert(collectExceptionMsg!FormatException(formattedWrite(w, "%1$s, %3$s", 1, 2)) |
|
== "Positional specifier %3$s index exceeds 2"); |
|
|
|
w.clear(); |
|
formattedWrite(w, "asd%s", 23); |
|
assert(w.data == "asd23", w.data); |
|
w.clear(); |
|
formattedWrite(w, "%s%s", 23, 45); |
|
assert(w.data == "2345", w.data); |
|
} |
|
|
|
@safe unittest |
|
{ |
|
import core.stdc.string : strlen; |
|
import std.array : appender; |
|
import std.conv : text, octal; |
|
import core.stdc.stdio : snprintf; |
|
|
|
debug(format) printf("std.format.format.unittest\n"); |
|
|
|
auto stream = appender!(char[])(); |
|
//goto here; |
|
|
|
formattedWrite(stream, |
|
"hello world! %s %s ", true, 57, 1_000_000_000, 'x', " foo"); |
|
assert(stream.data == "hello world! true 57 ", |
|
stream.data); |
|
|
|
stream.clear(); |
|
formattedWrite(stream, "%g %A %s", 1.67, -1.28, float.nan); |
|
// core.stdc.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 (CRuntime_Glibc) |
|
{ |
|
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 (CRuntime_Microsoft) |
|
{ |
|
assert(stream.data == "1.67 -0X1.47AE14P+0 nan" |
|
|| stream.data == "1.67 -0X1.47AE147AE147BP+0 nan", // MSVCRT 14+ (VS 2015) |
|
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 (CRuntime_Microsoft) |
|
assert(stream.data == "0x1.51eb85p+0 0X1.B1EB86P+2" |
|
|| stream.data == "0x1.51eb851eb851fp+0 0X1.B1EB860000000P+2"); // MSVCRT 14+ (VS 2015) |
|
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; |
|
//core.stdc.stdio.fwrite(stream.data.ptr, stream.data.length, 1, stderr); |
|
|
|
s = "helloworld"; |
|
string r; |
|
stream.clear(); formattedWrite(stream, "%.2s", s[0 .. 5]); |
|
assert(stream.data == "he"); |
|
stream.clear(); formattedWrite(stream, "%.20s", s[0 .. 5]); |
|
assert(stream.data == "hello"); |
|
stream.clear(); formattedWrite(stream, "%8s", s[0 .. 5]); |
|
assert(stream.data == " hello"); |
|
|
|
byte[] arrbyte = new byte[4]; |
|
arrbyte[0] = 100; |
|
arrbyte[1] = -99; |
|
arrbyte[3] = 0; |
|
stream.clear(); formattedWrite(stream, "%s", arrbyte); |
|
assert(stream.data == "[100, -99, 0, 0]", stream.data); |
|
|
|
ubyte[] arrubyte = new ubyte[4]; |
|
arrubyte[0] = 100; |
|
arrubyte[1] = 200; |
|
arrubyte[3] = 0; |
|
stream.clear(); formattedWrite(stream, "%s", arrubyte); |
|
assert(stream.data == "[100, 200, 0, 0]", stream.data); |
|
|
|
short[] arrshort = new short[4]; |
|
arrshort[0] = 100; |
|
arrshort[1] = -999; |
|
arrshort[3] = 0; |
|
stream.clear(); formattedWrite(stream, "%s", arrshort); |
|
assert(stream.data == "[100, -999, 0, 0]"); |
|
stream.clear(); formattedWrite(stream, "%s",arrshort); |
|
assert(stream.data == "[100, -999, 0, 0]"); |
|
|
|
ushort[] arrushort = new ushort[4]; |
|
arrushort[0] = 100; |
|
arrushort[1] = 20_000; |
|
arrushort[3] = 0; |
|
stream.clear(); formattedWrite(stream, "%s", arrushort); |
|
assert(stream.data == "[100, 20000, 0, 0]"); |
|
|
|
int[] arrint = new int[4]; |
|
arrint[0] = 100; |
|
arrint[1] = -999; |
|
arrint[3] = 0; |
|
stream.clear(); formattedWrite(stream, "%s", arrint); |
|
assert(stream.data == "[100, -999, 0, 0]"); |
|
stream.clear(); formattedWrite(stream, "%s",arrint); |
|
assert(stream.data == "[100, -999, 0, 0]"); |
|
|
|
long[] arrlong = new long[4]; |
|
arrlong[0] = 100; |
|
arrlong[1] = -999; |
|
arrlong[3] = 0; |
|
stream.clear(); formattedWrite(stream, "%s", arrlong); |
|
assert(stream.data == "[100, -999, 0, 0]"); |
|
stream.clear(); formattedWrite(stream, "%s",arrlong); |
|
assert(stream.data == "[100, -999, 0, 0]"); |
|
|
|
ulong[] arrulong = new ulong[4]; |
|
arrulong[0] = 100; |
|
arrulong[1] = 999; |
|
arrulong[3] = 0; |
|
stream.clear(); formattedWrite(stream, "%s", arrulong); |
|
assert(stream.data == "[100, 999, 0, 0]"); |
|
|
|
string[] arr2 = new string[4]; |
|
arr2[0] = "hello"; |
|
arr2[1] = "world"; |
|
arr2[3] = "foo"; |
|
stream.clear(); formattedWrite(stream, "%s", arr2); |
|
assert(stream.data == `["hello", "world", "", "foo"]`, stream.data); |
|
|
|
stream.clear(); formattedWrite(stream, "%.8d", 7); |
|
assert(stream.data == "00000007"); |
|
|
|
stream.clear(); formattedWrite(stream, "%.8x", 10); |
|
assert(stream.data == "0000000a"); |
|
|
|
stream.clear(); formattedWrite(stream, "%-3d", 7); |
|
assert(stream.data == "7 "); |
|
|
|
stream.clear(); formattedWrite(stream, "%*d", -3, 7); |
|
assert(stream.data == "7 "); |
|
|
|
stream.clear(); formattedWrite(stream, "%.*d", -3, 7); |
|
//writeln(stream.data); |
|
assert(stream.data == "7"); |
|
|
|
stream.clear(); formattedWrite(stream, "%s", "abc"c); |
|
assert(stream.data == "abc"); |
|
stream.clear(); formattedWrite(stream, "%s", "def"w); |
|
assert(stream.data == "def", text(stream.data.length)); |
|
stream.clear(); formattedWrite(stream, "%s", "ghi"d); |
|
assert(stream.data == "ghi"); |
|
|
|
here: |
|
@trusted void* deadBeef() { return cast(void*) 0xDEADBEEF; } |
|
stream.clear(); formattedWrite(stream, "%s", deadBeef()); |
|
assert(stream.data == "DEADBEEF", stream.data); |
|
|
|
stream.clear(); formattedWrite(stream, "%#x", 0xabcd); |
|
assert(stream.data == "0xabcd"); |
|
stream.clear(); formattedWrite(stream, "%#X", 0xABCD); |
|
assert(stream.data == "0XABCD"); |
|
|
|
stream.clear(); formattedWrite(stream, "%#o", octal!12345); |
|
assert(stream.data == "012345"); |
|
stream.clear(); formattedWrite(stream, "%o", 9); |
|
assert(stream.data == "11"); |
|
|
|
stream.clear(); formattedWrite(stream, "%+d", 123); |
|
assert(stream.data == "+123"); |
|
stream.clear(); formattedWrite(stream, "%+d", -123); |
|
assert(stream.data == "-123"); |
|
stream.clear(); formattedWrite(stream, "% d", 123); |
|
assert(stream.data == " 123"); |
|
stream.clear(); formattedWrite(stream, "% d", -123); |
|
assert(stream.data == "-123"); |
|
|
|
stream.clear(); formattedWrite(stream, "%%"); |
|
assert(stream.data == "%"); |
|
|
|
stream.clear(); formattedWrite(stream, "%d", true); |
|
assert(stream.data == "1"); |
|
stream.clear(); formattedWrite(stream, "%d", false); |
|
assert(stream.data == "0"); |
|
|
|
stream.clear(); formattedWrite(stream, "%d", 'a'); |
|
assert(stream.data == "97", stream.data); |
|
wchar wc = 'a'; |
|
stream.clear(); formattedWrite(stream, "%d", wc); |
|
assert(stream.data == "97"); |
|
dchar dc = 'a'; |
|
stream.clear(); formattedWrite(stream, "%d", dc); |
|
assert(stream.data == "97"); |
|
|
|
byte b = byte.max; |
|
stream.clear(); formattedWrite(stream, "%x", b); |
|
assert(stream.data == "7f"); |
|
stream.clear(); formattedWrite(stream, "%x", ++b); |
|
assert(stream.data == "80"); |
|
stream.clear(); formattedWrite(stream, "%x", ++b); |
|
assert(stream.data == "81"); |
|
|
|
short sh = short.max; |
|
stream.clear(); formattedWrite(stream, "%x", sh); |
|
assert(stream.data == "7fff"); |
|
stream.clear(); formattedWrite(stream, "%x", ++sh); |
|
assert(stream.data == "8000"); |
|
stream.clear(); formattedWrite(stream, "%x", ++sh); |
|
assert(stream.data == "8001"); |
|
|
|
i = int.max; |
|
stream.clear(); formattedWrite(stream, "%x", i); |
|
assert(stream.data == "7fffffff"); |
|
stream.clear(); formattedWrite(stream, "%x", ++i); |
|
assert(stream.data == "80000000"); |
|
stream.clear(); formattedWrite(stream, "%x", ++i); |
|
assert(stream.data == "80000001"); |
|
|
|
stream.clear(); formattedWrite(stream, "%x", 10); |
|
assert(stream.data == "a"); |
|
stream.clear(); formattedWrite(stream, "%X", 10); |
|
assert(stream.data == "A"); |
|
stream.clear(); formattedWrite(stream, "%x", 15); |
|
assert(stream.data == "f"); |
|
stream.clear(); formattedWrite(stream, "%X", 15); |
|
assert(stream.data == "F"); |
|
|
|
@trusted void ObjectTest() |
|
{ |
|
Object c = null; |
|
stream.clear(); formattedWrite(stream, "%s", c); |
|
assert(stream.data == "null"); |
|
} |
|
ObjectTest(); |
|
|
|
enum TestEnum |
|
{ |
|
Value1, Value2 |
|
} |
|
stream.clear(); formattedWrite(stream, "%s", TestEnum.Value2); |
|
assert(stream.data == "Value2", stream.data); |
|
stream.clear(); formattedWrite(stream, "%s", cast(TestEnum) 5); |
|
assert(stream.data == "cast(TestEnum)5", stream.data); |
|
|
|
//immutable(char[5])[int] aa = ([3:"hello", 4:"betty"]); |
|
//stream.clear(); formattedWrite(stream, "%s", aa.values); |
|
//core.stdc.stdio.fwrite(stream.data.ptr, stream.data.length, 1, stderr); |
|
//assert(stream.data == "[[h,e,l,l,o],[b,e,t,t,y]]"); |
|
//stream.clear(); formattedWrite(stream, "%s", aa); |
|
//assert(stream.data == "[3:[h,e,l,l,o],4:[b,e,t,t,y]]"); |
|
|
|
static const dchar[] ds = ['a','b']; |
|
for (int j = 0; j < ds.length; ++j) |
|
{ |
|
stream.clear(); formattedWrite(stream, " %d", ds[j]); |
|
if (j == 0) |
|
assert(stream.data == " 97"); |
|
else |
|
assert(stream.data == " 98"); |
|
} |
|
|
|
stream.clear(); formattedWrite(stream, "%.-3d", 7); |
|
assert(stream.data == "7", ">" ~ stream.data ~ "<"); |
|
} |
|
|
|
@safe unittest |
|
{ |
|
import std.array; |
|
import std.stdio; |
|
|
|
immutable(char[5])[int] aa = ([3:"hello", 4:"betty"]); |
|
assert(aa[3] == "hello"); |
|
assert(aa[4] == "betty"); |
|
|
|
auto stream = appender!(char[])(); |
|
alias AllNumerics = |
|
AliasSeq!(byte, ubyte, short, ushort, int, uint, long, ulong, |
|
float, double, real); |
|
foreach (T; AllNumerics) |
|
{ |
|
T value = 1; |
|
stream.clear(); |
|
formattedWrite(stream, "%s", value); |
|
assert(stream.data == "1"); |
|
} |
|
|
|
stream.clear(); |
|
formattedWrite(stream, "%s", aa); |
|
} |
|
|
|
@system unittest |
|
{ |
|
string s = "hello!124:34.5"; |
|
string a; |
|
int b; |
|
double c; |
|
formattedRead(s, "%s!%s:%s", &a, &b, &c); |
|
assert(a == "hello" && b == 124 && c == 34.5); |
|
} |
|
|
|
version (unittest) |
|
void formatReflectTest(T)(ref T val, string fmt, string formatted, string fn = __FILE__, size_t ln = __LINE__) |
|
{ |
|
import core.exception : AssertError; |
|
import std.array : appender; |
|
auto w = appender!string(); |
|
formattedWrite(w, fmt, val); |
|
|
|
auto input = w.data; |
|
enforce!AssertError( |
|
input == formatted, |
|
input, fn, ln); |
|
|
|
T val2; |
|
formattedRead(input, fmt, &val2); |
|
static if (isAssociativeArray!T) |
|
if (__ctfe) |
|
{ |
|
alias aa1 = val; |
|
alias aa2 = val2; |
|
assert(aa1 == aa2); |
|
|
|
assert(aa1.length == aa2.length); |
|
|
|
assert(aa1.keys == aa2.keys); |
|
|
|
assert(aa1.values == aa2.values); |
|
assert(aa1.values.length == aa2.values.length); |
|
foreach (i; 0 .. aa1.values.length) |
|
assert(aa1.values[i] == aa2.values[i]); |
|
|
|
foreach (i, key; aa1.keys) |
|
assert(aa1.values[i] == aa1[key]); |
|
foreach (i, key; aa2.keys) |
|
assert(aa2.values[i] == aa2[key]); |
|
return; |
|
} |
|
enforce!AssertError( |
|
val == val2, |
|
input, fn, ln); |
|
} |
|
|
|
version (unittest) |
|
void formatReflectTest(T)(ref T val, string fmt, string[] formatted, string fn = __FILE__, size_t ln = __LINE__) |
|
{ |
|
import core.exception : AssertError; |
|
import std.array : appender; |
|
auto w = appender!string(); |
|
formattedWrite(w, fmt, val); |
|
|
|
auto input = w.data; |
|
|
|
foreach (cur; formatted) |
|
{ |
|
if (input == cur) return; |
|
} |
|
enforce!AssertError( |
|
false, |
|
input, |
|
fn, |
|
ln); |
|
|
|
T val2; |
|
formattedRead(input, fmt, &val2); |
|
static if (isAssociativeArray!T) |
|
if (__ctfe) |
|
{ |
|
alias aa1 = val; |
|
alias aa2 = val2; |
|
assert(aa1 == aa2); |
|
|
|
assert(aa1.length == aa2.length); |
|
|
|
assert(aa1.keys == aa2.keys); |
|
|
|
assert(aa1.values == aa2.values); |
|
assert(aa1.values.length == aa2.values.length); |
|
foreach (i; 0 .. aa1.values.length) |
|
assert(aa1.values[i] == aa2.values[i]); |
|
|
|
foreach (i, key; aa1.keys) |
|
assert(aa1.values[i] == aa1[key]); |
|
foreach (i, key; aa2.keys) |
|
assert(aa2.values[i] == aa2[key]); |
|
return; |
|
} |
|
enforce!AssertError( |
|
val == val2, |
|
input, fn, ln); |
|
} |
|
|
|
@system unittest |
|
{ |
|
void booleanTest() |
|
{ |
|
auto b = true; |
|
formatReflectTest(b, "%s", `true`); |
|
formatReflectTest(b, "%b", `1`); |
|
formatReflectTest(b, "%o", `1`); |
|
formatReflectTest(b, "%d", `1`); |
|
formatReflectTest(b, "%u", `1`); |
|
formatReflectTest(b, "%x", `1`); |
|
} |
|
|
|
void integerTest() |
|
{ |
|
auto n = 127; |
|
formatReflectTest(n, "%s", `127`); |
|
formatReflectTest(n, "%b", `1111111`); |
|
formatReflectTest(n, "%o", `177`); |
|
formatReflectTest(n, "%d", `127`); |
|
formatReflectTest(n, "%u", `127`); |
|
formatReflectTest(n, "%x", `7f`); |
|
} |
|
|
|
void floatingTest() |
|
{ |
|
auto f = 3.14; |
|
formatReflectTest(f, "%s", `3.14`); |
|
version (MinGW) |
|
formatReflectTest(f, "%e", `3.140000e+000`); |
|
else |
|
formatReflectTest(f, "%e", `3.140000e+00`); |
|
formatReflectTest(f, "%f", `3.140000`); |
|
formatReflectTest(f, "%g", `3.14`); |
|
} |
|
|
|
void charTest() |
|
{ |
|
auto c = 'a'; |
|
formatReflectTest(c, "%s", `a`); |
|
formatReflectTest(c, "%c", `a`); |
|
formatReflectTest(c, "%b", `1100001`); |
|
formatReflectTest(c, "%o", `141`); |
|
formatReflectTest(c, "%d", `97`); |
|
formatReflectTest(c, "%u", `97`); |
|
formatReflectTest(c, "%x", `61`); |
|
} |
|
|
|
void strTest() |
|
{ |
|
auto s = "hello"; |
|
formatReflectTest(s, "%s", `hello`); |
|
formatReflectTest(s, "%(%c,%)", `h,e,l,l,o`); |
|
formatReflectTest(s, "%(%s,%)", `'h','e','l','l','o'`); |
|
formatReflectTest(s, "[%(<%c>%| $ %)]", `[<h> $ <e> $ <l> $ <l> $ <o>]`); |
|
} |
|
|
|
void daTest() |
|
{ |
|
auto a = [1,2,3,4]; |
|
formatReflectTest(a, "%s", `[1, 2, 3, 4]`); |
|
formatReflectTest(a, "[%(%s; %)]", `[1; 2; 3; 4]`); |
|
formatReflectTest(a, "[%(<%s>%| $ %)]", `[<1> $ <2> $ <3> $ <4>]`); |
|
} |
|
|
|
void saTest() |
|
{ |
|
int[4] sa = [1,2,3,4]; |
|
formatReflectTest(sa, "%s", `[1, 2, 3, 4]`); |
|
formatReflectTest(sa, "[%(%s; %)]", `[1; 2; 3; 4]`); |
|
formatReflectTest(sa, "[%(<%s>%| $ %)]", `[<1> $ <2> $ <3> $ <4>]`); |
|
} |
|
|
|
void aaTest() |
|
{ |
|
auto aa = [1:"hello", 2:"world"]; |
|
formatReflectTest(aa, "%s", [`[1:"hello", 2:"world"]`, `[2:"world", 1:"hello"]`]); |
|
formatReflectTest(aa, "[%(%s->%s, %)]", [`[1->"hello", 2->"world"]`, `[2->"world", 1->"hello"]`]); |
|
formatReflectTest(aa, "{%([%s=%(%c%)]%|; %)}", [`{[1=hello]; [2=world]}`, `{[2=world]; [1=hello]}`]); |
|
} |
|
|
|
import std.exception; |
|
assertCTFEable!( |
|
{ |
|
booleanTest(); |
|
integerTest(); |
|
if (!__ctfe) floatingTest(); // snprintf |
|
charTest(); |
|
strTest(); |
|
daTest(); |
|
saTest(); |
|
aaTest(); |
|
return true; |
|
}); |
|
} |
|
|
|
//------------------------------------------------------------------------------ |
|
private void skipData(Range, Char)(ref Range input, const ref FormatSpec!Char spec) |
|
{ |
|
import std.ascii : isDigit; |
|
import std.conv : text; |
|
|
|
switch (spec.spec) |
|
{ |
|
case 'c': input.popFront(); break; |
|
case 'd': |
|
if (input.front == '+' || input.front == '-') input.popFront(); |
|
goto case 'u'; |
|
case 'u': |
|
while (!input.empty && isDigit(input.front)) input.popFront(); |
|
break; |
|
default: |
|
assert(false, |
|
text("Format specifier not understood: %", spec.spec)); |
|
} |
|
} |
|
|
|
private template acceptedSpecs(T) |
|
{ |
|
static if (isIntegral!T) enum acceptedSpecs = "bdosuxX"; |
|
else static if (isFloatingPoint!T) enum acceptedSpecs = "seEfgG"; |
|
else static if (isSomeChar!T) enum acceptedSpecs = "bcdosuxX"; // integral + 'c' |
|
else enum acceptedSpecs = ""; |
|
} |
|
|
|
/** |
|
* Reads a value from the given _input range according to spec |
|
* and returns it as type `T`. |
|
* |
|
* Params: |
|
* T = the type to return |
|
* input = the _input range to read from |
|
* spec = the `FormatSpec` to use when reading from `input` |
|
* Returns: |
|
* A value from `input` of type `T` |
|
* Throws: |
|
* An `Exception` if `spec` cannot read a type `T` |
|
* See_Also: |
|
* $(REF parse, std, conv) and $(REF to, std, conv) |
|
*/ |
|
T unformatValue(T, Range, Char)(ref Range input, const ref FormatSpec!Char spec) |
|
{ |
|
return unformatValueImpl!T(input, spec); |
|
} |
|
|
|
/// Booleans |
|
@safe pure unittest |
|
{ |
|
auto str = "false"; |
|
auto spec = singleSpec("%s"); |
|
assert(unformatValue!bool(str, spec) == false); |
|
|
|
str = "1"; |
|
spec = singleSpec("%d"); |
|
assert(unformatValue!bool(str, spec)); |
|
} |
|
|
|
/// Null values |
|
@safe pure unittest |
|
{ |
|
auto str = "null"; |
|
auto spec = singleSpec("%s"); |
|
assert(str.unformatValue!(typeof(null))(spec) == null); |
|
} |
|
|
|
/// Integrals |
|
@safe pure unittest |
|
{ |
|
auto str = "123"; |
|
auto spec = singleSpec("%s"); |
|
assert(str.unformatValue!int(spec) == 123); |
|
|
|
str = "ABC"; |
|
spec = singleSpec("%X"); |
|
assert(str.unformatValue!int(spec) == 2748); |
|
|
|
str = "11610"; |
|
spec = singleSpec("%o"); |
|
assert(str.unformatValue!int(spec) == 5000); |
|
} |
|
|
|
/// Floating point numbers |
|
@safe pure unittest |
|
{ |
|
import std.math : approxEqual; |
|
|
|
auto str = "123.456"; |
|
auto spec = singleSpec("%s"); |
|
assert(str.unformatValue!double(spec).approxEqual(123.456)); |
|
} |
|
|
|
/// Character input ranges |
|
@safe pure unittest |
|
{ |
|
auto str = "aaa"; |
|
auto spec = singleSpec("%s"); |
|
assert(str.unformatValue!char(spec) == 'a'); |
|
|
|
// Using a numerical format spec reads a Unicode value from a string |
|
str = "65"; |
|
spec = singleSpec("%d"); |
|
assert(str.unformatValue!char(spec) == 'A'); |
|
|
|
str = "41"; |
|
spec = singleSpec("%x"); |
|
assert(str.unformatValue!char(spec) == 'A'); |
|
|
|
str = "10003"; |
|
spec = singleSpec("%d"); |
|
assert(str.unformatValue!dchar(spec) == '✓'); |
|
} |
|
|
|
/// Arrays and static arrays |
|
@safe pure unittest |
|
{ |
|
string str = "aaa"; |
|
auto spec = singleSpec("%s"); |
|
assert(str.unformatValue!(dchar[])(spec) == "aaa"d); |
|
|
|
str = "aaa"; |
|
spec = singleSpec("%s"); |
|
dchar[3] ret = ['a', 'a', 'a']; |
|
assert(str.unformatValue!(dchar[3])(spec) == ret); |
|
|
|
str = "[1, 2, 3, 4]"; |
|
spec = singleSpec("%s"); |
|
assert(str.unformatValue!(int[])(spec) == [1, 2, 3, 4]); |
|
|
|
str = "[1, 2, 3, 4]"; |
|
spec = singleSpec("%s"); |
|
int[4] ret2 = [1, 2, 3, 4]; |
|
assert(str.unformatValue!(int[4])(spec) == ret2); |
|
} |
|
|
|
/// Associative arrays |
|
@safe pure unittest |
|
{ |
|
auto str = `["one": 1, "two": 2]`; |
|
auto spec = singleSpec("%s"); |
|
assert(str.unformatValue!(int[string])(spec) == ["one": 1, "two": 2]); |
|
} |
|
|
|
@safe pure unittest |
|
{ |
|
// 7241 |
|
string input = "a"; |
|
auto spec = FormatSpec!char("%s"); |
|
spec.readUpToNextSpec(input); |
|
auto result = unformatValue!(dchar[1])(input, spec); |
|
assert(result[0] == 'a'); |
|
} |
|
|
|
private T unformatValueImpl(T, Range, Char)(ref Range input, const ref FormatSpec!Char spec) |
|
if (isInputRange!Range && is(Unqual!T == bool)) |
|
{ |
|
import std.algorithm.searching : find; |
|
import std.conv : parse, text; |
|
|
|
if (spec.spec == 's') return parse!T(input); |
|
|
|
enforce(find(acceptedSpecs!long, spec.spec).length, |
|
text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof)); |
|
|
|
return unformatValue!long(input, spec) != 0; |
|
} |
|
|
|
private T unformatValueImpl(T, Range, Char)(ref Range input, const ref FormatSpec!Char spec) |
|
if (isInputRange!Range && is(T == typeof(null))) |
|
{ |
|
import std.conv : parse, text; |
|
enforce(spec.spec == 's', |
|
text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof)); |
|
|
|
return parse!T(input); |
|
} |
|
|
|
/// ditto |
|
private T unformatValueImpl(T, Range, Char)(ref Range input, const ref FormatSpec!Char spec) |
|
if (isInputRange!Range && isIntegral!T && !is(T == enum) && isSomeChar!(ElementType!Range)) |
|
{ |
|
|
|
import std.algorithm.searching : find; |
|
import std.conv : parse, text; |
|
|
|
if (spec.spec == 'r') |
|
{ |
|
static if (is(Unqual!(ElementEncodingType!Range) == char) |
|
|| is(Unqual!(ElementEncodingType!Range) == byte) |
|
|| is(Unqual!(ElementEncodingType!Range) == ubyte)) |
|
return rawRead!T(input); |
|
else |
|
throw new Exception("The raw read specifier %r may only be used with narrow strings and ranges of bytes."); |
|
} |
|
|
|
enforce(find(acceptedSpecs!T, spec.spec).length, |
|
text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof)); |
|
|
|
enforce(spec.width == 0, "Parsing integers with a width specification is not implemented"); // TODO |
|
|
|
immutable uint base = |
|
spec.spec == 'x' || spec.spec == 'X' ? 16 : |
|
spec.spec == 'o' ? 8 : |
|
spec.spec == 'b' ? 2 : |
|
spec.spec == 's' || spec.spec == 'd' || spec.spec == 'u' ? 10 : 0; |
|
assert(base != 0); |
|
|
|
return parse!T(input, base); |
|
|
|
} |
|
|
|
/// ditto |
|
private T unformatValueImpl(T, Range, Char)(ref Range input, const ref FormatSpec!Char spec) |
|
if (isFloatingPoint!T && !is(T == enum) && isInputRange!Range |
|
&& isSomeChar!(ElementType!Range)&& !is(Range == enum)) |
|
{ |
|
import std.algorithm.searching : find; |
|
import std.conv : parse, text; |
|
|
|
if (spec.spec == 'r') |
|
{ |
|
static if (is(Unqual!(ElementEncodingType!Range) == char) |
|
|| is(Unqual!(ElementEncodingType!Range) == byte) |
|
|| is(Unqual!(ElementEncodingType!Range) == ubyte)) |
|
return rawRead!T(input); |
|
else |
|
throw new Exception("The raw read specifier %r may only be used with narrow strings and ranges of bytes."); |
|
} |
|
|
|
enforce(find(acceptedSpecs!T, spec.spec).length, |
|
text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof)); |
|
|
|
return parse!T(input); |
|
} |
|
|
|
/// ditto |
|
private T unformatValueImpl(T, Range, Char)(ref Range input, const ref FormatSpec!Char spec) |
|
if (isInputRange!Range && isSomeChar!T && !is(T == enum) && isSomeChar!(ElementType!Range)) |
|
{ |
|
import std.algorithm.searching : find; |
|
import std.conv : to, text; |
|
if (spec.spec == 's' || spec.spec == 'c') |
|
{ |
|
auto result = to!T(input.front); |
|
input.popFront(); |
|
return result; |
|
} |
|
enforce(find(acceptedSpecs!T, spec.spec).length, |
|
text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof)); |
|
|
|
static if (T.sizeof == 1) |
|
return unformatValue!ubyte(input, spec); |
|
else static if (T.sizeof == 2) |
|
return unformatValue!ushort(input, spec); |
|
else static if (T.sizeof == 4) |
|
return unformatValue!uint(input, spec); |
|
else |
|
static assert(0); |
|
} |
|
|
|
/// ditto |
|
private T unformatValueImpl(T, Range, Char)(ref Range input, const ref FormatSpec!Char spec) |
|
if (isInputRange!Range && is(StringTypeOf!T) && !isAggregateType!T && !is(T == enum)) |
|
{ |
|
import std.conv : text; |
|
|
|
if (spec.spec == '(') |
|
{ |
|
return unformatRange!T(input, spec); |
|
} |
|
enforce(spec.spec == 's', |
|
text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof)); |
|
|
|
static if (isStaticArray!T) |
|
{ |
|
T result; |
|
auto app = result[]; |
|
} |
|
else |
|
{ |
|
import std.array : appender; |
|
auto app = appender!T(); |
|
} |
|
if (spec.trailing.empty) |
|
{ |
|
for (; !input.empty; input.popFront()) |
|
{ |
|
static if (isStaticArray!T) |
|
if (app.empty) |
|
break; |
|
app.put(input.front); |
|
} |
|
} |
|
else |
|
{ |
|
immutable end = spec.trailing.front; |
|
for (; !input.empty && input.front != end; input.popFront()) |
|
{ |
|
static if (isStaticArray!T) |
|
if (app.empty) |
|
break; |
|
app.put(input.front); |
|
} |
|
} |
|
static if (isStaticArray!T) |
|
{ |
|
enforce(app.empty, "need more input"); |
|
return result; |
|
} |
|
else |
|
return app.data; |
|
} |
|
|
|
/// ditto |
|
private T unformatValueImpl(T, Range, Char)(ref Range input, const ref FormatSpec!Char spec) |
|
if (isInputRange!Range && isArray!T && !is(StringTypeOf!T) && !isAggregateType!T && !is(T == enum)) |
|
{ |
|
import std.conv : parse, text; |
|
if (spec.spec == '(') |
|
{ |
|
return unformatRange!T(input, spec); |
|
} |
|
enforce(spec.spec == 's', |
|
text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof)); |
|
|
|
return parse!T(input); |
|
} |
|
|
|
/// ditto |
|
private T unformatValueImpl(T, Range, Char)(ref Range input, const ref FormatSpec!Char spec) |
|
if (isInputRange!Range && isAssociativeArray!T && !is(T == enum)) |
|
{ |
|
import std.conv : parse, text; |
|
if (spec.spec == '(') |
|
{ |
|
return unformatRange!T(input, spec); |
|
} |
|
enforce(spec.spec == 's', |
|
text("Wrong unformat specifier '%", spec.spec , "' for ", T.stringof)); |
|
|
|
return parse!T(input); |
|
} |
|
|
|
/** |
|
* Function that performs raw reading. Used by unformatValue |
|
* for integral and float types. |
|
*/ |
|
private T rawRead(T, Range)(ref Range input) |
|
if (is(Unqual!(ElementEncodingType!Range) == char) |
|
|| is(Unqual!(ElementEncodingType!Range) == byte) |
|
|| is(Unqual!(ElementEncodingType!Range) == ubyte)) |
|
{ |
|
union X |
|
{ |
|
ubyte[T.sizeof] raw; |
|
T typed; |
|
} |
|
X x; |
|
foreach (i; 0 .. T.sizeof) |
|
{ |
|
static if (isSomeString!Range) |
|
{ |
|
x.raw[i] = input[0]; |
|
input = input[1 .. $]; |
|
} |
|
else |
|
{ |
|
// TODO: recheck this |
|
x.raw[i] = input.front; |
|
input.popFront(); |
|
} |
|
} |
|
return x.typed; |
|
} |
|
|
|
//debug = unformatRange; |
|
|
|
private T unformatRange(T, Range, Char)(ref Range input, const ref FormatSpec!Char spec) |
|
in |
|
{ |
|
assert(spec.spec == '('); |
|
} |
|
body |
|
{ |
|
debug (unformatRange) printf("unformatRange:\n"); |
|
|
|
T result; |
|
static if (isStaticArray!T) |
|
{ |
|
size_t i; |
|
} |
|
|
|
const(Char)[] cont = spec.trailing; |
|
for (size_t j = 0; j < spec.trailing.length; ++j) |
|
{ |
|
if (spec.trailing[j] == '%') |
|
{ |
|
cont = spec.trailing[0 .. j]; |
|
break; |
|
} |
|
} |
|
debug (unformatRange) printf("\t"); |
|
debug (unformatRange) if (!input.empty) printf("input.front = %c, ", input.front); |
|
debug (unformatRange) printf("cont = %.*s\n", cont); |
|
|
|
bool checkEnd() |
|
{ |
|
return input.empty || !cont.empty && input.front == cont.front; |
|
} |
|
|
|
if (!checkEnd()) |
|
{ |
|
for (;;) |
|
{ |
|
auto fmt = FormatSpec!Char(spec.nested); |
|
fmt.readUpToNextSpec(input); |
|
enforce(!input.empty, "Unexpected end of input when parsing range"); |
|
|
|
debug (unformatRange) printf("\t) spec = %c, front = %c ", fmt.spec, input.front); |
|
static if (isStaticArray!T) |
|
{ |
|
result[i++] = unformatElement!(typeof(T.init[0]))(input, fmt); |
|
} |
|
else static if (isDynamicArray!T) |
|
{ |
|
result ~= unformatElement!(ElementType!T)(input, fmt); |
|
} |
|
else static if (isAssociativeArray!T) |
|
{ |
|
auto key = unformatElement!(typeof(T.init.keys[0]))(input, fmt); |
|
fmt.readUpToNextSpec(input); // eat key separator |
|
|
|
result[key] = unformatElement!(typeof(T.init.values[0]))(input, fmt); |
|
} |
|
debug (unformatRange) { |
|
if (input.empty) printf("-> front = [empty] "); |
|
else printf("-> front = %c ", input.front); |
|
} |
|
|
|
static if (isStaticArray!T) |
|
{ |
|
debug (unformatRange) printf("i = %u < %u\n", i, T.length); |
|
enforce(i <= T.length, "Too many format specifiers for static array of length %d".format(T.length)); |
|
} |
|
|
|
if (spec.sep !is null) |
|
fmt.readUpToNextSpec(input); |
|
auto sep = spec.sep !is null ? spec.sep |
|
: fmt.trailing; |
|
debug (unformatRange) { |
|
if (!sep.empty && !input.empty) printf("-> %c, sep = %.*s\n", input.front, sep); |
|
else printf("\n"); |
|
} |
|
|
|
if (checkEnd()) |
|
break; |
|
|
|
if (!sep.empty && input.front == sep.front) |
|
{ |
|
while (!sep.empty) |
|
{ |
|
enforce(!input.empty, "Unexpected end of input when parsing range separator"); |
|
enforce(input.front == sep.front, "Unexpected character when parsing range separator"); |
|
input.popFront(); |
|
sep.popFront(); |
|
} |
|
debug (unformatRange) printf("input.front = %c\n", input.front); |
|
} |
|
} |
|
} |
|
static if (isStaticArray!T) |
|
{ |
|
enforce(i == T.length, "Too few (%d) format specifiers for static array of length %d".format(i, T.length)); |
|
} |
|
return result; |
|
} |
|
|
|
// Undocumented |
|
T unformatElement(T, Range, Char)(ref Range input, const ref FormatSpec!Char spec) |
|
if (isInputRange!Range) |
|
{ |
|
import std.conv : parseElement; |
|
static if (isSomeString!T) |
|
{ |
|
if (spec.spec == 's') |
|
{ |
|
return parseElement!T(input); |
|
} |
|
} |
|
else static if (isSomeChar!T) |
|
{ |
|
if (spec.spec == 's') |
|
{ |
|
return parseElement!T(input); |
|
} |
|
} |
|
|
|
return unformatValue!T(input, spec); |
|
} |
|
|
|
|
|
// Legacy implementation |
|
|
|
enum Mangle : char |
|
{ |
|
Tvoid = 'v', |
|
Tbool = 'b', |
|
Tbyte = 'g', |
|
Tubyte = 'h', |
|
Tshort = 's', |
|
Tushort = 't', |
|
Tint = 'i', |
|
Tuint = 'k', |
|
Tlong = 'l', |
|
Tulong = 'm', |
|
Tfloat = 'f', |
|
Tdouble = 'd', |
|
Treal = 'e', |
|
|
|
Tifloat = 'o', |
|
Tidouble = 'p', |
|
Tireal = 'j', |
|
Tcfloat = 'q', |
|
Tcdouble = 'r', |
|
Tcreal = 'c', |
|
|
|
Tchar = 'a', |
|
Twchar = 'u', |
|
Tdchar = 'w', |
|
|
|
Tarray = 'A', |
|
Tsarray = 'G', |
|
Taarray = 'H', |
|
Tpointer = 'P', |
|
Tfunction = 'F', |
|
Tident = 'I', |
|
Tclass = 'C', |
|
Tstruct = 'S', |
|
Tenum = 'E', |
|
Ttypedef = 'T', |
|
Tdelegate = 'D', |
|
|
|
Tconst = 'x', |
|
Timmutable = 'y', |
|
} |
|
|
|
// return the TypeInfo for a primitive type and null otherwise. This |
|
// is required since for arrays of ints we only have the mangled char |
|
// to work from. If arrays always subclassed TypeInfo_Array this |
|
// routine could go away. |
|
private TypeInfo primitiveTypeInfo(Mangle m) |
|
{ |
|
// BUG: should fix this in static this() to avoid double checked locking bug |
|
__gshared TypeInfo[Mangle] dic; |
|
if (!dic.length) |
|
{ |
|
dic = [ |
|
Mangle.Tvoid : typeid(void), |
|
Mangle.Tbool : typeid(bool), |
|
Mangle.Tbyte : typeid(byte), |
|
Mangle.Tubyte : typeid(ubyte), |
|
Mangle.Tshort : typeid(short), |
|
Mangle.Tushort : typeid(ushort), |
|
Mangle.Tint : typeid(int), |
|
Mangle.Tuint : typeid(uint), |
|
Mangle.Tlong : typeid(long), |
|
Mangle.Tulong : typeid(ulong), |
|
Mangle.Tfloat : typeid(float), |
|
Mangle.Tdouble : typeid(double), |
|
Mangle.Treal : typeid(real), |
|
Mangle.Tifloat : typeid(ifloat), |
|
Mangle.Tidouble : typeid(idouble), |
|
Mangle.Tireal : typeid(ireal), |
|
Mangle.Tcfloat : typeid(cfloat), |
|
Mangle.Tcdouble : typeid(cdouble), |
|
Mangle.Tcreal : typeid(creal), |
|
Mangle.Tchar : typeid(char), |
|
Mangle.Twchar : typeid(wchar), |
|
Mangle.Tdchar : typeid(dchar) |
|
]; |
|
} |
|
auto p = m in dic; |
|
return p ? *p : null; |
|
} |
|
|
|
private bool needToSwapEndianess(Char)(const ref FormatSpec!Char f) |
|
{ |
|
import std.system : endian, Endian; |
|
|
|
return endian == Endian.littleEndian && f.flPlus |
|
|| endian == Endian.bigEndian && f.flDash; |
|
} |
|
|
|
/* ======================== Unit Tests ====================================== */ |
|
|
|
@system unittest |
|
{ |
|
import std.conv : octal; |
|
|
|
int i; |
|
string s; |
|
|
|
debug(format) printf("std.format.format.unittest\n"); |
|
|
|
s = format("hello world! %s %s %s%s%s", true, 57, 1_000_000_000, 'x', " foo"); |
|
assert(s == "hello world! true 57 1000000000x foo"); |
|
|
|
s = format("%s %A %s", 1.67, -1.28, float.nan); |
|
/* 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(s == "1.67 -0XA.3D70A3D70A3D8P-3 nan"); |
|
//else version (OSX) |
|
// assert(s == "1.67 -0XA.3D70A3D70A3D8P-3 nan", s); |
|
//else |
|
version (MinGW) |
|
assert(s == "1.67 -0XA.3D70A3D70A3D8P-3 nan", s); |
|
else version (CRuntime_Microsoft) |
|
assert(s == "1.67 -0X1.47AE14P+0 nan" |
|
|| s == "1.67 -0X1.47AE147AE147BP+0 nan", s); // MSVCRT 14+ (VS 2015) |
|
else |
|
assert(s == "1.67 -0X1.47AE147AE147BP+0 nan", s); |
|
|
|
s = format("%x %X", 0x1234AF, 0xAFAFAFAF); |
|
assert(s == "1234af AFAFAFAF"); |
|
|
|
s = format("%b %o", 0x1234AF, 0xAFAFAFAF); |
|
assert(s == "100100011010010101111 25753727657"); |
|
|
|
s = format("%d %s", 0x1234AF, 0xAFAFAFAF); |
|
assert(s == "1193135 2947526575"); |
|
|
|
//version (X86_64) |
|
//{ |
|
// pragma(msg, "several format tests disabled on x86_64 due to bug 5625"); |
|
//} |
|
//else |
|
//{ |
|
s = format("%s", 1.2 + 3.4i); |
|
assert(s == "1.2+3.4i", s); |
|
|
|
//s = format("%x %X", 1.32, 6.78f); |
|
//assert(s == "3ff51eb851eb851f 40D8F5C3"); |
|
|
|
//} |
|
|
|
s = format("%#06.*f",2,12.345); |
|
assert(s == "012.35"); |
|
|
|
s = format("%#0*.*f",6,2,12.345); |
|
assert(s == "012.35"); |
|
|
|
s = format("%7.4g:", 12.678); |
|
assert(s == " 12.68:"); |
|
|
|
s = format("%7.4g:", 12.678L); |
|
assert(s == " 12.68:"); |
|
|
|
s = format("%04f|%05d|%#05x|%#5x",-4.0,-10,1,1); |
|
assert(s == "-4.000000|-0010|0x001| 0x1"); |
|
|
|
i = -10; |
|
s = format("%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i); |
|
assert(s == "-10|-10|-10|-10|-10.0000"); |
|
|
|
i = -5; |
|
s = format("%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i); |
|
assert(s == "-5| -5|-05|-5|-5.0000"); |
|
|
|
i = 0; |
|
s = format("%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i); |
|
assert(s == "0| 0|000|0|0.0000"); |
|
|
|
i = 5; |
|
s = format("%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i); |
|
assert(s == "5| 5|005|5|5.0000"); |
|
|
|
i = 10; |
|
s = format("%d|%3d|%03d|%1d|%01.4f",i,i,i,i,cast(double) i); |
|
assert(s == "10| 10|010|10|10.0000"); |
|
|
|
s = format("%.0d", 0); |
|
assert(s == ""); |
|
|
|
s = format("%.g", .34); |
|
assert(s == "0.3"); |
|
|
|
s = format("%.0g", .34); |
|
assert(s == "0.3"); |
|
|
|
s = format("%.2g", .34); |
|
assert(s == "0.34"); |
|
|
|
s = format("%0.0008f", 1e-08); |
|
assert(s == "0.00000001"); |
|
|
|
s = format("%0.0008f", 1e-05); |
|
assert(s == "0.00001000"); |
|
|
|
s = "helloworld"; |
|
string r; |
|
r = format("%.2s", s[0 .. 5]); |
|
assert(r == "he"); |
|
r = format("%.20s", s[0 .. 5]); |
|
assert(r == "hello"); |
|
r = format("%8s", s[0 .. 5]); |
|
assert(r == " hello"); |
|
|
|
byte[] arrbyte = new byte[4]; |
|
arrbyte[0] = 100; |
|
arrbyte[1] = -99; |
|
arrbyte[3] = 0; |
|
r = format("%s", arrbyte); |
|
assert(r == "[100, -99, 0, 0]"); |
|
|
|
ubyte[] arrubyte = new ubyte[4]; |
|
arrubyte[0] = 100; |
|
arrubyte[1] = 200; |
|
arrubyte[3] = 0; |
|
r = format("%s", arrubyte); |
|
assert(r == "[100, 200, 0, 0]"); |
|
|
|
short[] arrshort = new short[4]; |
|
arrshort[0] = 100; |
|
arrshort[1] = -999; |
|
arrshort[3] = 0; |
|
r = format("%s", arrshort); |
|
assert(r == "[100, -999, 0, 0]"); |
|
|
|
ushort[] arrushort = new ushort[4]; |
|
arrushort[0] = 100; |
|
arrushort[1] = 20_000; |
|
arrushort[3] = 0; |
|
r = format("%s", arrushort); |
|
assert(r == "[100, 20000, 0, 0]"); |
|
|
|
int[] arrint = new int[4]; |
|
arrint[0] = 100; |
|
arrint[1] = -999; |
|
arrint[3] = 0; |
|
r = format("%s", arrint); |
|
assert(r == "[100, -999, 0, 0]"); |
|
|
|
long[] arrlong = new long[4]; |
|
arrlong[0] = 100; |
|
arrlong[1] = -999; |
|
arrlong[3] = 0; |
|
r = format("%s", arrlong); |
|
assert(r == "[100, -999, 0, 0]"); |
|
|
|
ulong[] arrulong = new ulong[4]; |
|
arrulong[0] = 100; |
|
arrulong[1] = 999; |
|
arrulong[3] = 0; |
|
r = format("%s", arrulong); |
|
assert(r == "[100, 999, 0, 0]"); |
|
|
|
string[] arr2 = new string[4]; |
|
arr2[0] = "hello"; |
|
arr2[1] = "world"; |
|
arr2[3] = "foo"; |
|
r = format("%s", arr2); |
|
assert(r == `["hello", "world", "", "foo"]`); |
|
|
|
r = format("%.8d", 7); |
|
assert(r == "00000007"); |
|
r = format("%.8x", 10); |
|
assert(r == "0000000a"); |
|
|
|
r = format("%-3d", 7); |
|
assert(r == "7 "); |
|
|
|
r = format("%-1*d", 4, 3); |
|
assert(r == "3 "); |
|
|
|
r = format("%*d", -3, 7); |
|
assert(r == "7 "); |
|
|
|
r = format("%.*d", -3, 7); |
|
assert(r == "7"); |
|
|
|
r = format("%-1.*f", 2, 3.1415); |
|
assert(r == "3.14"); |
|
|
|
r = format("abc"c); |
|
assert(r == "abc"); |
|
|
|
//format() returns the same type as inputted. |
|
wstring wr; |
|
wr = format("def"w); |
|
assert(wr == "def"w); |
|
|
|
dstring dr; |
|
dr = format("ghi"d); |
|
assert(dr == "ghi"d); |
|
|
|
void* p = cast(void*) 0xDEADBEEF; |
|
r = format("%s", p); |
|
assert(r == "DEADBEEF"); |
|
|
|
r = format("%#x", 0xabcd); |
|
assert(r == "0xabcd"); |
|
r = format("%#X", 0xABCD); |
|
assert(r == "0XABCD"); |
|
|
|
r = format("%#o", octal!12345); |
|
assert(r == "012345"); |
|
r = format("%o", 9); |
|
assert(r == "11"); |
|
r = format("%#o", 0); // issue 15663 |
|
assert(r == "0"); |
|
|
|
r = format("%+d", 123); |
|
assert(r == "+123"); |
|
r = format("%+d", -123); |
|
assert(r == "-123"); |
|
r = format("% d", 123); |
|
assert(r == " 123"); |
|
r = format("% d", -123); |
|
assert(r == "-123"); |
|
|
|
r = format("%%"); |
|
assert(r == "%"); |
|
|
|
r = format("%d", true); |
|
assert(r == "1"); |
|
r = format("%d", false); |
|
assert(r == "0"); |
|
|
|
r = format("%d", 'a'); |
|
assert(r == "97"); |
|
wchar wc = 'a'; |
|
r = format("%d", wc); |
|
assert(r == "97"); |
|
dchar dc = 'a'; |
|
r = format("%d", dc); |
|
assert(r == "97"); |
|
|
|
byte b = byte.max; |
|
r = format("%x", b); |
|
assert(r == "7f"); |
|
r = format("%x", ++b); |
|
assert(r == "80"); |
|
r = format("%x", ++b); |
|
assert(r == "81"); |
|
|
|
short sh = short.max; |
|
r = format("%x", sh); |
|
assert(r == "7fff"); |
|
r = format("%x", ++sh); |
|
assert(r == "8000"); |
|
r = format("%x", ++sh); |
|
assert(r == "8001"); |
|
|
|
i = int.max; |
|
r = format("%x", i); |
|
assert(r == "7fffffff"); |
|
r = format("%x", ++i); |
|
assert(r == "80000000"); |
|
r = format("%x", ++i); |
|
assert(r == "80000001"); |
|
|
|
r = format("%x", 10); |
|
assert(r == "a"); |
|
r = format("%X", 10); |
|
assert(r == "A"); |
|
r = format("%x", 15); |
|
assert(r == "f"); |
|
r = format("%X", 15); |
|
assert(r == "F"); |
|
|
|
Object c = null; |
|
r = format("%s", c); |
|
assert(r == "null"); |
|
|
|
enum TestEnum |
|
{ |
|
Value1, Value2 |
|
} |
|
r = format("%s", TestEnum.Value2); |
|
assert(r == "Value2"); |
|
|
|
immutable(char[5])[int] aa = ([3:"hello", 4:"betty"]); |
|
r = format("%s", aa.values); |
|
assert(r == `["hello", "betty"]` || r == `["betty", "hello"]`); |
|
r = format("%s", aa); |
|
assert(r == `[3:"hello", 4:"betty"]` || r == `[4:"betty", 3:"hello"]`); |
|
|
|
static const dchar[] ds = ['a','b']; |
|
for (int j = 0; j < ds.length; ++j) |
|
{ |
|
r = format(" %d", ds[j]); |
|
if (j == 0) |
|
assert(r == " 97"); |
|
else |
|
assert(r == " 98"); |
|
} |
|
|
|
r = format(">%14d<, %s", 15, [1,2,3]); |
|
assert(r == "> 15<, [1, 2, 3]"); |
|
|
|
assert(format("%8s", "bar") == " bar"); |
|
assert(format("%8s", "b\u00e9ll\u00f4") == " b\u00e9ll\u00f4"); |
|
} |
|
|
|
@safe unittest |
|
{ |
|
// bugzilla 3479 |
|
import std.array; |
|
auto stream = appender!(char[])(); |
|
formattedWrite(stream, "%2$.*1$d", 12, 10); |
|
assert(stream.data == "000000000010", stream.data); |
|
} |
|
|
|
@safe unittest |
|
{ |
|
// bug 6893 |
|
import std.array; |
|
enum E : ulong { A, B, C } |
|
auto stream = appender!(char[])(); |
|
formattedWrite(stream, "%s", E.C); |
|
assert(stream.data == "C"); |
|
} |
|
|
|
// Used to check format strings are compatible with argument types |
|
package static const checkFormatException(alias fmt, Args...) = |
|
{ |
|
try |
|
.format(fmt, Args.init); |
|
catch (Exception e) |
|
return (e.msg == ctfpMessage) ? null : e; |
|
return null; |
|
}(); |
|
|
|
/***************************************************** |
|
* Format arguments into a string. |
|
* |
|
* Params: fmt = Format string. For detailed specification, see $(LREF formattedWrite). |
|
* args = Variadic list of arguments to _format into returned string. |
|
*/ |
|
typeof(fmt) format(alias fmt, Args...)(Args args) |
|
if (isSomeString!(typeof(fmt))) |
|
{ |
|
alias e = checkFormatException!(fmt, Args); |
|
static assert(!e, e.msg); |
|
return .format(fmt, args); |
|
} |
|
|
|
/// Type checking can be done when fmt is known at compile-time: |
|
@safe unittest |
|
{ |
|
auto s = format!"%s is %s"("Pi", 3.14); |
|
assert(s == "Pi is 3.14"); |
|
|
|
static assert(!__traits(compiles, {s = format!"%l"();})); // missing arg |
|
static assert(!__traits(compiles, {s = format!""(404);})); // surplus arg |
|
static assert(!__traits(compiles, {s = format!"%d"(4.03);})); // incompatible arg |
|
} |
|
|
|
/// ditto |
|
immutable(Char)[] format(Char, Args...)(in Char[] fmt, Args args) |
|
if (isSomeChar!Char) |
|
{ |
|
import std.array : appender; |
|
import std.format : formattedWrite, FormatException; |
|
auto w = appender!(immutable(Char)[]); |
|
auto n = formattedWrite(w, fmt, args); |
|
version (all) |
|
{ |
|
// In the future, this check will be removed to increase consistency |
|
// with formattedWrite |
|
import std.conv : text; |
|
import std.exception : enforce; |
|
enforce(n == args.length, new FormatException( |
|
text("Orphan format arguments: args[", n, "..", args.length, "]"))); |
|
} |
|
return w.data; |
|
} |
|
|
|
@safe pure unittest |
|
{ |
|
import core.exception; |
|
import std.exception; |
|
import std.format; |
|
assertCTFEable!( |
|
{ |
|
// assert(format(null) == ""); |
|
assert(format("foo") == "foo"); |
|
assert(format("foo%%") == "foo%"); |
|
assert(format("foo%s", 'C') == "fooC"); |
|
assert(format("%s foo", "bar") == "bar foo"); |
|
assert(format("%s foo %s", "bar", "abc") == "bar foo abc"); |
|
assert(format("foo %d", -123) == "foo -123"); |
|
assert(format("foo %d", 123) == "foo 123"); |
|
|
|
assertThrown!FormatException(format("foo %s")); |
|
assertThrown!FormatException(format("foo %s", 123, 456)); |
|
|
|
assert(format("hel%slo%s%s%s", "world", -138, 'c', true) == |
|
"helworldlo-138ctrue"); |
|
}); |
|
|
|
assert(is(typeof(format("happy")) == string)); |
|
assert(is(typeof(format("happy"w)) == wstring)); |
|
assert(is(typeof(format("happy"d)) == dstring)); |
|
} |
|
|
|
// https://issues.dlang.org/show_bug.cgi?id=16661 |
|
@safe unittest |
|
{ |
|
assert(format("%.2f"d, 0.4) == "0.40"); |
|
assert("%02d"d.format(1) == "01"d); |
|
} |
|
|
|
/***************************************************** |
|
* Format arguments into buffer $(I buf) which must be large |
|
* enough to hold the result. |
|
* |
|
* Returns: |
|
* The slice of `buf` containing the formatted string. |
|
* |
|
* Throws: |
|
* A `RangeError` if `buf` isn't large enough to hold the |
|
* formatted string. |
|
* |
|
* A $(LREF FormatException) if the length of `args` is different |
|
* than the number of format specifiers in `fmt`. |
|
*/ |
|
char[] sformat(alias fmt, Args...)(char[] buf, Args args) |
|
if (isSomeString!(typeof(fmt))) |
|
{ |
|
alias e = checkFormatException!(fmt, Args); |
|
static assert(!e, e.msg); |
|
return .sformat(buf, fmt, args); |
|
} |
|
|
|
/// ditto |
|
char[] sformat(Char, Args...)(char[] buf, in Char[] fmt, Args args) |
|
{ |
|
import core.exception : RangeError; |
|
import std.format : formattedWrite, FormatException; |
|
import std.utf : encode; |
|
|
|
size_t i; |
|
|
|
struct Sink |
|
{ |
|
void put(dchar c) |
|
{ |
|
char[4] enc; |
|
auto n = encode(enc, c); |
|
|
|
if (buf.length < i + n) |
|
throw new RangeError(__FILE__, __LINE__); |
|
|
|
buf[i .. i + n] = enc[0 .. n]; |
|
i += n; |
|
} |
|
void put(const(char)[] s) |
|
{ |
|
if (buf.length < i + s.length) |
|
throw new RangeError(__FILE__, __LINE__); |
|
|
|
buf[i .. i + s.length] = s[]; |
|
i += s.length; |
|
} |
|
void put(const(wchar)[] s) |
|
{ |
|
for (; !s.empty; s.popFront()) |
|
put(s.front); |
|
} |
|
void put(const(dchar)[] s) |
|
{ |
|
for (; !s.empty; s.popFront()) |
|
put(s.front); |
|
} |
|
} |
|
auto n = formattedWrite(Sink(), fmt, args); |
|
version (all) |
|
{ |
|
// In the future, this check will be removed to increase consistency |
|
// with formattedWrite |
|
import std.conv : text; |
|
import std.exception : enforce; |
|
enforce!FormatException( |
|
n == args.length, |
|
text("Orphan format arguments: args[", n, " .. ", args.length, "]") |
|
); |
|
} |
|
return buf[0 .. i]; |
|
} |
|
|
|
/// The format string can be checked at compile-time (see $(LREF format) for details): |
|
@system unittest |
|
{ |
|
char[10] buf; |
|
|
|
assert(buf[].sformat!"foo%s"('C') == "fooC"); |
|
assert(sformat(buf[], "%s foo", "bar") == "bar foo"); |
|
} |
|
|
|
@system unittest |
|
{ |
|
import core.exception; |
|
import std.format; |
|
|
|
debug(string) trustedPrintf("std.string.sformat.unittest\n"); |
|
|
|
import std.exception; |
|
assertCTFEable!( |
|
{ |
|
char[10] buf; |
|
|
|
assert(sformat(buf[], "foo") == "foo"); |
|
assert(sformat(buf[], "foo%%") == "foo%"); |
|
assert(sformat(buf[], "foo%s", 'C') == "fooC"); |
|
assert(sformat(buf[], "%s foo", "bar") == "bar foo"); |
|
assertThrown!RangeError(sformat(buf[], "%s foo %s", "bar", "abc")); |
|
assert(sformat(buf[], "foo %d", -123) == "foo -123"); |
|
assert(sformat(buf[], "foo %d", 123) == "foo 123"); |
|
|
|
assertThrown!FormatException(sformat(buf[], "foo %s")); |
|
assertThrown!FormatException(sformat(buf[], "foo %s", 123, 456)); |
|
|
|
assert(sformat(buf[], "%s %s %s", "c"c, "w"w, "d"d) == "c w d"); |
|
}); |
|
} |
|
|
|
/***************************** |
|
* The .ptr is unsafe because it could be dereferenced and the length of the array may be 0. |
|
* Returns: |
|
* the difference between the starts of the arrays |
|
*/ |
|
@trusted private pure nothrow @nogc |
|
ptrdiff_t arrayPtrDiff(T)(const T[] array1, const T[] array2) |
|
{ |
|
return array1.ptr - array2.ptr; |
|
} |
|
|
|
@safe unittest |
|
{ |
|
assertCTFEable!({ |
|
auto tmp = format("%,d", 1000); |
|
assert(tmp == "1,000", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%,?d", 'z', 1234567); |
|
assert(tmp == "1z234z567", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%10,?d", 'z', 1234567); |
|
assert(tmp == " 1z234z567", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%11,2?d", 'z', 1234567); |
|
assert(tmp == " 1z23z45z67", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%11,*?d", 2, 'z', 1234567); |
|
assert(tmp == " 1z23z45z67", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%11,*d", 2, 1234567); |
|
assert(tmp == " 1,23,45,67", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%11,2d", 1234567); |
|
assert(tmp == " 1,23,45,67", "'" ~ tmp ~ "'"); |
|
}); |
|
} |
|
|
|
@safe unittest |
|
{ |
|
auto tmp = format("%,f", 1000.0); |
|
assert(tmp == "1,000.000,000", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%,f", 1234567.891011); |
|
assert(tmp == "1,234,567.891,011", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%,f", -1234567.891011); |
|
assert(tmp == "-1,234,567.891,011", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%,2f", 1234567.891011); |
|
assert(tmp == "1,23,45,67.89,10,11", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%18,f", 1234567.891011); |
|
assert(tmp == " 1,234,567.891,011", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%18,?f", '.', 1234567.891011); |
|
assert(tmp == " 1.234.567.891.011", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%,?.3f", 'ä', 1234567.891011); |
|
assert(tmp == "1ä234ä567.891", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%,*?.3f", 1, 'ä', 1234567.891011); |
|
assert(tmp == "1ä2ä3ä4ä5ä6ä7.8ä9ä1", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%,4?.3f", '_', 1234567.891011); |
|
assert(tmp == "123_4567.891", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%12,3.3f", 1234.5678); |
|
assert(tmp == " 1,234.568", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%,e", 3.141592653589793238462); |
|
assert(tmp == "3.141,593e+00", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%15,e", 3.141592653589793238462); |
|
assert(tmp == " 3.141,593e+00", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%15,e", -3.141592653589793238462); |
|
assert(tmp == " -3.141,593e+00", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%.4,*e", 2, 3.141592653589793238462); |
|
assert(tmp == "3.14,16e+00", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%13.4,*e", 2, 3.141592653589793238462); |
|
assert(tmp == " 3.14,16e+00", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%,.0f", 3.14); |
|
assert(tmp == "3", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%3,g", 1_000_000.123456); |
|
assert(tmp == "1e+06", "'" ~ tmp ~ "'"); |
|
|
|
tmp = format("%19,?f", '.', -1234567.891011); |
|
assert(tmp == " -1.234.567.891.011", "'" ~ tmp ~ "'"); |
|
} |
|
|
|
// Test for multiple indexes |
|
@safe unittest |
|
{ |
|
auto tmp = format("%2:5$s", 1, 2, 3, 4, 5); |
|
assert(tmp == "2345", tmp); |
|
} |