phobos/std/format/write.d

656 lines
19 KiB
D

// Written in the D programming language.
/**
This is a submodule of $(MREF std, format).
It provides two functions for writing formatted output: $(LREF
formatValue) and $(LREF formattedWrite). The former writes a single
value. The latter writes several values at once, interspersed with
unformatted text.
The following combinations of format characters and types are
available:
$(BOOKTABLE ,
$(TR $(TH) $(TH s) $(TH c) $(TH d, u, b, o) $(TH x, X) $(TH e, E, f, F, g, G, a, A) $(TH r) $(TH compound))
$(TR $(TD `bool`) $(TD yes) $(TD $(MDASH)) $(TD yes) $(TD yes) $(TD $(MDASH)) $(TD yes) $(TD $(MDASH)))
$(TR $(TD `null`) $(TD yes) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)))
$(TR $(TD $(I integer)) $(TD yes) $(TD $(MDASH)) $(TD yes) $(TD yes) $(TD $(MDASH)) $(TD yes) $(TD $(MDASH)))
$(TR $(TD $(I floating point)) $(TD yes) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD yes) $(TD yes) $(TD $(MDASH)))
$(TR $(TD $(I character)) $(TD yes) $(TD yes) $(TD yes) $(TD yes) $(TD $(MDASH)) $(TD yes) $(TD $(MDASH)))
$(TR $(TD $(I string)) $(TD yes) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD yes) $(TD yes))
$(TR $(TD $(I array)) $(TD yes) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD yes) $(TD yes))
$(TR $(TD $(I associative array)) $(TD yes) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD yes))
$(TR $(TD $(I pointer)) $(TD yes) $(TD $(MDASH)) $(TD $(MDASH)) $(TD yes) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)))
$(TR $(TD $(I SIMD vectors)) $(TD yes) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD yes) $(TD yes))
$(TR $(TD $(I delegates)) $(TD yes) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD $(MDASH)) $(TD yes) $(TD yes))
)
Enums can be used with all format characters of the base type.
$(SECTION3 Structs$(COMMA) Unions$(COMMA) Classes$(COMMA) and Interfaces)
Aggregate types can define various `toString` functions. If this
function takes a $(REF_ALTTEXT FormatSpec, FormatSpec, std, format,
spec) as argument, the function decides which format characters are
accepted. If no `toString` is defined and the aggregate is an
$(REF_ALTTEXT input range, isInputRange, std, range, primitives), it
is treated like a range, that is $(B 's'), $(B 'r') and a compound
specifier are accepted. In all other cases aggregate types only
accept $(B 's').
Copyright: Copyright The D Language Foundation 2000-2013.
License: $(HTTP boost.org/LICENSE_1_0.txt, Boost License 1.0).
Authors: $(HTTP walterbright.com, Walter Bright), $(HTTP erdani.com,
Andrei Alexandrescu), and Kenji Hara
Source: $(PHOBOSSRC std/format/write.d)
*/
module std.format.write;
/**
* `bool`s are formatted as `"true"` or `"false"` with `%s` and as `1` or
* `0` with integral-specific format specs.
*/
@safe pure unittest
{
import std.array : appender;
import std.format.spec : singleSpec;
auto w = appender!string();
auto spec = singleSpec("%s");
formatValue(w, true, spec);
assert(w.data == "true");
}
/// `null` literal is formatted as `"null"`.
@safe pure unittest
{
import std.array : appender;
import std.format.spec : singleSpec;
auto w = appender!string();
auto spec = singleSpec("%s");
formatValue(w, null, spec);
assert(w.data == "null");
}
/// Integrals are formatted like $(REF printf, core, stdc, stdio).
@safe pure unittest
{
import std.array : appender;
import std.format.spec : singleSpec;
auto w = appender!string();
auto spec = singleSpec("%d");
formatValue(w, 1337, spec);
assert(w.data == "1337");
}
/// Floating-point values are formatted like $(REF printf, core, stdc, stdio)
@safe unittest
{
import std.array : appender;
import std.format.spec : singleSpec;
auto w = appender!string();
auto spec = singleSpec("%.1f");
formatValue(w, 1337.7, spec);
assert(w.data == "1337.7");
}
/**
* Individual characters (`char, `wchar`, or `dchar`) are formatted as
* Unicode characters with `%s` and as integers with integral-specific format
* specs.
*/
@safe pure unittest
{
import std.array : appender;
import std.format.spec : singleSpec;
auto w = appender!string();
auto spec = singleSpec("%c");
formatValue(w, 'a', spec);
assert(w.data == "a");
}
/// Strings are formatted like $(REF printf, core, stdc, stdio)
@safe pure unittest
{
import std.array : appender;
import std.format.spec : singleSpec;
auto w = appender!string();
auto spec = singleSpec("%s");
formatValue(w, "hello", spec);
assert(w.data == "hello");
}
/// Static-size arrays are formatted as dynamic arrays.
@safe pure unittest
{
import std.array : appender;
import std.format.spec : singleSpec;
auto w = appender!string();
auto spec = singleSpec("%s");
char[2] two = ['a', 'b'];
formatValue(w, two, spec);
assert(w.data == "ab");
}
/**
* Dynamic arrays are formatted as input ranges.
*
* Specializations:
* $(UL
* $(LI `void[]` is formatted like `ubyte[]`.)
* $(LI Const array is converted to input range by removing its qualifier.)
* )
*/
@safe pure unittest
{
import std.array : appender;
import std.format.spec : singleSpec;
auto w = appender!string();
auto spec = singleSpec("%s");
auto two = [1, 2];
formatValue(w, two, spec);
assert(w.data == "[1, 2]");
}
/**
* Associative arrays are formatted by using `':'` and `", "` as
* separators, and enclosed by `'['` and `']'`.
*/
@safe pure unittest
{
import std.array : appender;
import std.format.spec : singleSpec;
auto w = appender!string();
auto spec = singleSpec("%s");
auto aa = ["H":"W"];
formatValue(w, aa, spec);
assert(w.data == "[\"H\":\"W\"]", w.data);
}
/// `enum`s are formatted like their base value
@safe pure unittest
{
import std.array : appender;
import std.format.spec : singleSpec;
auto w = appender!string();
auto spec = singleSpec("%s");
enum A { first, second, third }
formatValue(w, A.second, spec);
assert(w.data == "second");
}
/**
* Formatting a struct by defining a method `toString`, which takes an output
* range.
*
* It's recommended that any `toString` using $(REF_ALTTEXT output ranges, isOutputRange, std,range,primitives)
* use $(REF put, std,range,primitives) rather than use the `put` method of the range
* directly.
*/
@safe unittest
{
import std.array : appender;
import std.format.spec : FormatSpec, singleSpec;
import std.range.primitives : isOutputRange, put;
static struct Point
{
int x, y;
void toString(W)(ref W writer, scope const ref FormatSpec!char f)
if (isOutputRange!(W, char))
{
// std.range.primitives.put
put(writer, "(");
formatValue(writer, x, f);
put(writer, ",");
formatValue(writer, y, f);
put(writer, ")");
}
}
auto w = appender!string();
auto spec = singleSpec("%s");
auto p = Point(16, 11);
formatValue(w, p, spec);
assert(w.data == "(16,11)");
}
/**
* Another example of formatting a `struct` with a defined `toString`,
* this time using the `scope delegate` method.
*
* $(RED This method is now discouraged for non-virtual functions).
* If possible, please use the output range method instead.
*/
@safe unittest
{
import std.format : format;
import std.format.spec : FormatSpec;
static struct Point
{
int x, y;
void toString(scope void delegate(scope const(char)[]) @safe sink,
scope const FormatSpec!char fmt) const
{
sink("(");
sink.formatValue(x, fmt);
sink(",");
sink.formatValue(y, fmt);
sink(")");
}
}
auto p = Point(16,11);
assert(format("%03d", p) == "(016,011)");
assert(format("%02x", p) == "(10,0b)");
}
/// Pointers are formatted as hex integers.
@system pure unittest
{
import std.array : appender;
import std.format.spec : singleSpec;
auto w = appender!string();
auto spec = singleSpec("%s");
auto q = cast(void*) 0xFFEECCAA;
formatValue(w, q, spec);
assert(w.data == "FFEECCAA");
}
/// SIMD vectors are formatted as arrays.
@safe unittest
{
import core.simd; // cannot be selective, because float4 might not be defined
import std.array : appender;
import std.format.spec : singleSpec;
auto w = appender!string();
auto spec = singleSpec("%s");
static if (is(float4))
{
version (X86) {}
else
{
float4 f4;
f4.array[0] = 1;
f4.array[1] = 2;
f4.array[2] = 3;
f4.array[3] = 4;
formatValue(w, f4, spec);
assert(w.data == "[1, 2, 3, 4]");
}
}
}
/**
* Delegates are formatted by `ReturnType delegate(Parameters) FunctionAttributes`
*
* Known Bug: Function attributes are not always correct.
* See $(BUGZILLA 18269) for more details.
*/
@safe unittest
{
import std.conv : to;
int i;
int foo(short k) @nogc
{
return i + k;
}
@system int delegate(short) @nogc bar() nothrow pure
{
int* p = new int(1);
i = *p;
return &foo;
}
assert(to!string(&bar) == "int delegate(short) @nogc delegate() pure nothrow @system");
assert(() @trusted { return bar()(3); }() == 4);
}
import std.format.internal.write;
import std.format.spec : FormatSpec;
import std.traits : isSomeString;
/**
Converts its arguments according to a format string and writes
the result to an output range.
The second version of `formattedWrite` takes the format string as a
template argument. In this case, it is checked for consistency at
compile-time.
Params:
w = an $(REF_ALTTEXT output range, isOutputRange, std, range, primitives),
where the formatted result is written to
fmt = a $(MREF_ALTTEXT format string, std,format)
args = a variadic list of arguments to be formatted
Writer = the type of the writer `w`
Char = character type of `fmt`
Args = a variadic list of types of the arguments
Returns:
The index of the last argument that was formatted. If no positional
arguments are used, this is the number of arguments that where formatted.
Throws:
A $(REF_ALTTEXT FormatException, FormatException, std, format)
if formatting did not succeed.
Note:
In theory this function should be `@nogc`. But with the current
implementation there are some cases where allocations occur.
See $(REF_ALTTEXT $(D sformat), sformat, std, format) for more details.
*/
uint formattedWrite(Writer, Char, Args...)(auto ref Writer w, const scope Char[] fmt, Args args)
{
import std.conv : text;
import std.format : enforceFmt, FormatException;
import std.traits : isSomeChar;
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 == Args.length && !spec.indexStart)
{
// leftover spec?
enforceFmt(fmt.length == 0,
text("Orphan format specifier: %", spec.spec));
break;
}
if (spec.width == spec.DYNAMIC)
{
auto width = getNthInt!"integer width"(currentArg, args);
if (width < 0)
{
spec.flDash = true;
width = -width;
}
spec.width = width;
++currentArg;
}
else if (spec.width < 0)
{
// means: get width as a positional parameter
auto index = cast(uint) -spec.width;
assert(index > 0, "The index must be greater than zero");
auto width = getNthInt!"integer width"(index - 1, args);
if (currentArg < index) currentArg = index;
if (width < 0)
{
spec.flDash = true;
width = -width;
}
spec.width = width;
}
if (spec.precision == spec.DYNAMIC)
{
auto precision = getNthInt!"integer precision"(currentArg, args);
if (precision >= 0) spec.precision = precision;
// else negative precision is same as no precision
else spec.precision = spec.UNSPECIFIED;
++currentArg;
}
else if (spec.precision < 0)
{
// means: get precision as a positional parameter
auto index = cast(uint) -spec.precision;
assert(index > 0, "The precision must be greater than zero");
auto precision = getNthInt!"integer precision"(index- 1, args);
if (currentArg < index) currentArg = index;
if (precision >= 0) spec.precision = precision;
// else negative precision is same as no precision
else spec.precision = spec.UNSPECIFIED;
}
if (spec.separators == spec.DYNAMIC)
{
auto separators = getNthInt!"separator digit width"(currentArg, args);
spec.separators = separators;
++currentArg;
}
if (spec.dynamicSeparatorChar)
{
auto separatorChar =
getNth!("separator character", isSomeChar, dchar)(currentArg, args);
spec.separatorChar = separatorChar;
spec.dynamicSeparatorChar = false;
++currentArg;
}
if (currentArg == Args.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; Args)
{
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 < Args.length)
goto case;
else
goto default;
}
else
break SWITCH;
}
default:
throw new FormatException(
text("Positional specifier %", spec.indexStart, '$', spec.spec,
" index exceeds ", Args.length));
}
}
return currentArg;
}
///
@safe pure unittest
{
import std.array : appender;
auto writer1 = appender!string();
formattedWrite(writer1, "%s is the ultimate %s.", 42, "answer");
assert(writer1[] == "42 is the ultimate answer.");
auto writer2 = appender!string();
formattedWrite(writer2, "Increase: %7.2f %%", 17.4285);
assert(writer2[] == "Increase: 17.43 %");
}
/// ditto
uint formattedWrite(alias fmt, Writer, Args...)(auto ref Writer w, Args args)
if (isSomeString!(typeof(fmt)))
{
import std.format : checkFormatException;
alias e = checkFormatException!(fmt, Args);
static assert(!e, e.msg);
return .formattedWrite(w, fmt, args);
}
/// The format string can be checked at compile-time:
@safe pure unittest
{
import std.array : appender;
auto writer = appender!string();
writer.formattedWrite!"%d is the ultimate %s."(42, "answer");
assert(writer[] == "42 is the ultimate answer.");
// This line doesn't compile, because 3.14 cannot be formatted with %d:
// writer.formattedWrite!"%d is the ultimate %s."(3.14, "answer");
}
@safe pure unittest
{
import std.array;
auto w = appender!string();
formattedWrite(w, "%s %d", "@safe/pure", 42);
assert(w.data == "@safe/pure 42");
}
@safe pure unittest
{
char[20] buf;
auto w = buf[];
formattedWrite(w, "%s %d", "@safe/pure", 42);
assert(buf[0 .. $ - w.length] == "@safe/pure 42");
}
/**
Formats a value of any type according to a format specifier and
writes the result to an output range.
More details about how types are formatted, and how the format
specifier influences the outcome, can be found in the definition of a
$(MREF_ALTTEXT format string, std,format).
Params:
w = an $(REF_ALTTEXT output range, isOutputRange, std, range, primitives) where
the formatted value is written to
val = the value to write
f = a $(REF_ALTTEXT FormatSpec, FormatSpec, std, format, spec) defining the
format specifier
Writer = the type of the output range `w`
T = the type of value `val`
Char = the character type used for `f`
Throws:
A $(LREF FormatException) if formatting did not succeed.
Note:
In theory this function should be `@nogc`. But with the current
implementation there are some cases where allocations occur.
See $(REF_ALTTEXT $(D sformat), sformat, std, format) for more details.
See_Also:
$(LREF formattedWrite) which formats several values at once.
*/
void formatValue(Writer, T, Char)(auto ref Writer w, auto ref T val, scope const ref FormatSpec!Char f)
{
import std.format : enforceFmt;
enforceFmt(f.width != f.DYNAMIC && f.precision != f.DYNAMIC
&& f.separators != f.DYNAMIC && !f.dynamicSeparatorChar,
"Dynamic argument not allowed for `formatValue`");
formatValueImpl(w, val, f);
}
///
@safe pure unittest
{
import std.array : appender;
import std.format.spec : singleSpec;
auto writer = appender!string();
auto spec = singleSpec("%08b");
writer.formatValue(42, spec);
assert(writer.data == "00101010");
spec = singleSpec("%2s");
writer.formatValue('=', spec);
assert(writer.data == "00101010 =");
spec = singleSpec("%+14.6e");
writer.formatValue(42.0, spec);
assert(writer.data == "00101010 = +4.200000e+01");
}
// https://issues.dlang.org/show_bug.cgi?id=15386
@safe pure unittest
{
import std.array : appender;
import std.format.spec : FormatSpec;
import std.format : FormatException;
import std.exception : assertThrown;
auto w = appender!(char[])();
auto dor = appender!(char[])();
auto fs = FormatSpec!char("%.*s");
fs.writeUpToNextSpec(dor);
assertThrown!FormatException(formatValue(w, 0, fs));
fs = FormatSpec!char("%*s");
fs.writeUpToNextSpec(dor);
assertThrown!FormatException(formatValue(w, 0, fs));
fs = FormatSpec!char("%,*s");
fs.writeUpToNextSpec(dor);
assertThrown!FormatException(formatValue(w, 0, fs));
fs = FormatSpec!char("%,?s");
fs.writeUpToNextSpec(dor);
assertThrown!FormatException(formatValue(w, 0, fs));
assertThrown!FormatException(formattedWrite(w, "%(%0*d%)", new int[1]));
}