1 /**
2  * @nogc formatting utilities
3  *
4  * Inspired by: https://github.com/weka-io/mecca/blob/master/src/mecca/lib/string.d
5  *
6  * Sink Types:
7  * various functions in this module use "sinks" which are buffers or objects that get filled with
8  * the formatting data while the format functions are running. The following sink types are
9  * supported to be passed into these arguments:
10  * - Arrays (`isArray!S && is(ForeachType!S : char))`)
11  * - $(LREF NullSink)
12  * - Object with `put(const(char)[])` and `put(char)` functions
13  *
14  * Passing in arrays will make the sink `@nogc pure nothrow @safe` as everything will be written
15  * into that memory. Passing in arrays that are too short to hold all the data will trigger a
16  * `RangeError` or terminate the program in betterC.
17  *
18  * Passing in a $(LREF NullSink) instance will not allocate any memory and just count the bytes that
19  * will be allocated.
20  *
21  * Otherwise any type that contains a `put` method that can be called both with `const(char)[]` and
22  * with `char` arguments can be used.
23  */
24 module bc..string.format;
25 
26 import bc.core.intrinsics;
27 import bc.core.system.backtrace;
28 import bc.core.traits;
29 import bc..string.string;
30 import std.algorithm : among;
31 import std.datetime.date : TimeOfDay;
32 import std.traits :
33     EnumMembers, FieldNameTuple, ForeachType, hasMember,
34     isArray, isPointer, isSigned, isSomeChar, isStaticArray,
35     PointerTarget, Unqual;
36 import std.range : ElementEncodingType, isForwardRange, isInputRange;
37 import std.typecons : Flag, Tuple, isTuple;
38 
39 version (D_BetterC) {}
40 else
41 {
42     import core.time : Duration;
43     import std.datetime.systime : SysTime;
44     import std.uuid : UUID;
45 }
46 
47 private template isUUID(T)
48 {
49     version (D_BetterC) enum isUUID = false;
50     else enum isUUID = is(T == UUID);
51 }
52 
53 private template isSysTime(T)
54 {
55     version (D_BetterC) enum isSysTime = false;
56     else enum isSysTime = is(T == SysTime);
57 }
58 
59 private template isDuration(T)
60 {
61     version (D_BetterC) enum isDuration = false;
62     else enum isDuration = is(T == Duration);
63 }
64 
65 private template isTraceInfo(T)
66 {
67     version (D_BetterC) enum isTraceInfo = false;
68     else version (linux) enum isTraceInfo = is(T == TraceInfo);
69     else enum isTraceInfo = false;
70 }
71 
72 /**
73  * Formats values to with fmt template into provided sink.
74  * Note: it supports only a basic subset of format type specifiers, main usage is for nogc logging
75  * and error messages formatting. But more cases can be added as needed.
76  *
77  * WARN: %s accepts pointer to some char assuming it's a zero terminated string
78  *
79  * Params:
80  *     fmt  = The format string, much like in std.format
81  *     sink = The sink where the full string should be written to, see section "Sink Types"
82  *     args = The arguments to fill the format string with
83  *
84  * Returns: the length of the formatted string.
85  */
86 size_t nogcFormatTo(string fmt = "%s", S, ARGS...)(ref S sink, auto ref ARGS args)
87 {
88     // TODO: not pure because of float formatter
89     alias sfmt = splitFmt!fmt;
90     static assert (sfmt.numFormatters == ARGS.length, "Expected " ~ sfmt.numFormatters.stringof ~
91         " arguments, got " ~ ARGS.length.stringof);
92 
93     mixin SinkWriter!S;
94 
95     foreach (tok; sfmt.tokens) {
96         // pragma(msg, "tok: ", tok);
97         static if (is(typeof(tok) == string))
98         {
99             static if (tok.length > 0) {
100                 write(tok);
101             }
102         }
103         else static if (is(typeof(tok) == ArrFmtSpec))
104         {
105             enum j = tok.idx;
106             alias Typ = Unqual!(ARGS[j]);
107             static assert(
108                 __traits(compiles, ForeachType!Typ), "Expected foreach type range instead of " ~ Typ.stringof);
109             static assert(
110                 !is(S == NullSink) || isArray!Typ || isForwardRange!Typ,
111                 "Don't determine format output length with range argument " ~ Typ.stringof ~ " it'd be consumed.");
112             static if (!is(S == NullSink) && !isArray!Typ && !isForwardRange!Typ)
113                 pragma(msg, "WARN: Argument of type " ~ Typ.stringof ~ " would be consumed during format");
114 
115             static if (tok.del.length) bool first = true;
116             static if (!isArray!Typ && isForwardRange!Typ) auto val = args[j].save();
117             else auto val = args[j];
118             foreach (ref e; val)
119             {
120                 static if (tok.del.length) {
121                     if (_expect(!first, true)) write(tok.del);
122                     else first = false;
123                 }
124                 advance(s.nogcFormatTo!(tok.fmt)(e));
125             }
126         }
127         else static if (is(typeof(tok) == FmtSpec)) {
128             enum j = tok.idx;
129             enum f = tok.type;
130 
131             alias Typ = Unqual!(ARGS[j]);
132             auto val = args[j];
133 
134             static if (isStdNullable!Typ) {
135                 if (val.isNull) write("null");
136                 else advance(s.nogcFormatTo!"%s"(val.get));
137             }
138             else static if (f == FMT.STR) {
139                 static if ((isArray!Typ && is(Unqual!(ForeachType!Typ) == char)))
140                     write(val[]);
141                 else static if (isInputRange!Typ && isSomeChar!(Unqual!(ElementEncodingType!Typ))) {
142                     import bc.internal.utf : byUTF;
143                     foreach (c; val.byUTF!char) write(c);
144                 }
145                 else static if (is(Typ == bool))
146                     write(val ? "true" : "false");
147                 else static if (is(Typ == enum)) {
148                     auto tmp = enumToStr(val);
149                     if (_expect(tmp is null, false)) advance(s.nogcFormatTo!"%s(%d)"(Typ.stringof, val));
150                     else write(tmp);
151                 } else static if (isUUID!Typ) advance(s.formatUUID(val));
152                 else static if (isSysTime!Typ) advance(s.formatSysTime(val));
153                 else static if (is(Typ == TimeOfDay))
154                     advance(s.nogcFormatTo!"%02d:%02d:%02d"(val.hour, val.minute, val.second));
155                 else static if (isDuration!Typ) advance(s.formatDuration(val));
156                 else static if (isArray!Typ || isInputRange!Typ) {
157                     import std.range : empty;
158                     if (!val.empty) advance(s.nogcFormatTo!"[%(%s%|, %)]"(val));
159                     else write("[]");
160                 }
161                 else static if (isPointer!Typ) {
162                     static if (is(typeof(*Typ)) && isSomeChar!(typeof(*Typ))) {
163                         // NOTE: not safe, we can only trust that the provided char pointer is really stringz
164                         size_t i;
165                         while (val[i] != '\0') ++i;
166                         if (i) write(val[0..i]);
167                     }
168                     else advance(s.formatPtr(val));
169                 }
170                 else static if (is(Typ == char)) write(val);
171                 else static if (isSomeChar!Typ) {
172                     import std.range : only;
173                     import bc.internal.utf : byUTF;
174 
175                     foreach (c; val.only.byUTF!char) write(c);
176                 }
177                 else static if (is(Typ : ulong)) advance(s.formatDecimal(val));
178                 else static if (isTuple!Typ) {
179                     write("Tuple(");
180                     foreach (i, _; Typ.Types)
181                     {
182                         static if (Typ.fieldNames[i] == "") enum prefix = (i == 0 ? "" : ", ");
183                         else enum prefix = (i == 0 ? "" : ", ") ~ Typ.fieldNames[i] ~ "=";
184                         write(prefix);
185                         advance(s.nogcFormatTo!"%s"(val[i]));
186                     }
187                     write(")");
188                 }
189                 else static if (is(Typ : Throwable)) {
190                     auto obj = cast(Object)val;
191                     static if (__traits(compiles, TraceInfo(val))) {
192                         advance(s.nogcFormatTo!"%s@%s(%d): %s\n----------------\n%s"(
193                             typeid(obj).name, val.file, val.line, val.msg, TraceInfo(val)));
194                     }
195                     else
196                         advance(s.nogcFormatTo!"%s@%s(%d): %s"(
197                             typeid(obj).name, val.file, val.line, val.msg));
198                 }
199                 else static if (isTraceInfo!Typ) {
200                     auto sw = sinkWrap(s);
201                     val.dumpTo(sw);
202                     advance(sw.totalLen);
203                 }
204                 else static if (is(typeof(val[])))
205                     advance(s.nogcFormatTo!"%s"(val[])); // sliceable values
206                 else static if (is(Typ == struct)) {
207                     static if (__traits(compiles, (v) @nogc {auto sw = sinkWrap(s); v.toString(sw); }(val))) {
208                         // we can use custom defined toString
209                         auto sw = sinkWrap(s);
210                         val.toString(sw);
211                         advance(sw.totalLen);
212                     } else {
213                         static if (hasMember!(Typ, "toString"))
214                             pragma(msg, Typ.stringof ~ " has toString defined, but can't be used with nogcFormatter");
215                         {
216                             enum Prefix = Typ.stringof ~ "(";
217                             write(Prefix);
218                         }
219                         alias Names = FieldNameTuple!Typ;
220                         foreach(i, field; val.tupleof) {
221                             enum string Name = Names[i];
222                             enum Prefix = (i == 0 ? "" : ", ") ~ Name ~ "=";
223                             write(Prefix);
224                             advance(s.nogcFormatTo!"%s"(field));
225                         }
226                         write(")");
227                     }
228                 } else static if (is(Typ : double)) advance(s.nogcFormatTo!"%g"(val));
229                 else static assert (false, "Unsupported value type for string format: " ~ Typ.stringof);
230             }
231             else static if (f == FMT.CHR) {
232                 static assert (is(Typ : char), "Requested char format, but provided: " ~ Typ.stringof);
233                 write((&val)[0..1]);
234             }
235             else static if (f == FMT.DEC) {
236                 static assert (is(Typ : ulong), "Requested decimal format, but provided: " ~ Typ.stringof);
237                 enum fs = formatSpec(f, tok.def);
238                 advance(s.formatDecimal!(fs.width, fs.fill)(val));
239             }
240             else static if (f == FMT.HEX || f == FMT.UHEX) {
241                 static assert (is(Typ : ulong) || isPointer!(Typ), "Requested hex format, but provided: " ~ Typ.stringof);
242                 enum u = f == FMT.HEX ? Upper.yes : Upper.no;
243                 enum fs = formatSpec(f, tok.def);
244                 static if (isPointer!(Typ)) {
245                     import std.stdint : intptr_t;
246                     advance(s.formatHex!(fs.width, fs.fill, u)(cast(intptr_t)val));
247                 }
248                 else advance(s.formatHex!(fs.width, fs.fill, u)(val));
249             }
250             else static if (f == FMT.PTR) {
251                 static assert (is(Typ : ulong) || isPointer!(Typ), "Requested pointer format, but provided: " ~ Typ.stringof);
252                 advance(s.formatPtr(val));
253             }
254             else static if (f == FMT.FLT) {
255                 static assert (is(Typ : double), "Requested float format, but provided: " ~ Typ.stringof);
256                 advance(s.formatFloat(val));
257             }
258         }
259         else static assert(false);
260     }
261 
262     return totalLen;
263 }
264 
265 ///
266 @("combined")
267 @safe @nogc unittest
268 {
269     char[100] buf;
270     ubyte[3] data = [1, 2, 3];
271     immutable ret = nogcFormatTo!"hello %s %s %% world %d %x %p"(buf, data, "moshe", -567, 7, 7);
272     assert(ret == 53);
273     assert(buf[0..53] == "hello [1, 2, 3] moshe % world -567 7 0000000000000007");
274 }
275 
276 /**
277  * Same as `nogcFormatTo`, but it internally uses static malloc buffer to write formatted string to.
278  * So be careful that next call replaces internal buffer data and previous result isn't valid anymore.
279  */
280 const(char)[] nogcFormat(string fmt = "%s", ARGS...)(auto ref ARGS args)
281 {
282     import bc..string.string : String;
283     static StringZ str;
284     str.clear();
285     nogcFormatTo!fmt(str, args);
286     return cast(const(char)[])str.data;
287 }
288 
289 ///
290 @("formatters")
291 @safe unittest
292 {
293     import bc.core.memory;
294     import std.algorithm : filter;
295     import std.range : chunks;
296 
297     assert(nogcFormat!"abcd abcd" == "abcd abcd");
298     assert(nogcFormat!"123456789a" == "123456789a");
299     version (D_NoBoundsChecks) {}
300     else version (D_Exceptions)
301     {
302         () @trusted
303         {
304             import core.exception : RangeError;
305             import std.exception : assertThrown;
306             char[5] buf;
307             assertThrown!RangeError(buf.nogcFormatTo!"123412341234");
308         }();
309     }
310 
311     // literal escape
312     assert(nogcFormat!"123 %%" == "123 %");
313     assert(nogcFormat!"%%%%" == "%%");
314 
315     // %d
316     assert(nogcFormat!"%d"(1234) == "1234");
317     assert(nogcFormat!"%4d"(42) == "  42");
318     assert(nogcFormat!"%04d"(42) == "0042");
319     assert(nogcFormat!"%04d"(-42) == "-042");
320     assert(nogcFormat!"ab%dcd"(1234) == "ab1234cd");
321     assert(nogcFormat!"ab%d%d"(1234, 56) == "ab123456");
322 
323     // %x
324     assert(nogcFormat!"0x%x"(0x1234) == "0x1234");
325 
326     // %p
327     assert(nogcFormat!("%p")(0x1234) == "0000000000001234");
328 
329     // %s
330     assert(nogcFormat!"12345%s"("12345") == "1234512345");
331     assert(nogcFormat!"12345%s"(12345) == "1234512345");
332     enum Floop {XXX, YYY, ZZZ}
333     assert(nogcFormat!"12345%s"(Floop.YYY) == "12345YYY");
334     char[4] str = "foo\0";
335     assert(() @trusted { return nogcFormat!"%s"(str.ptr); }() == "foo");
336 
337     version (D_BetterC) {}
338     else
339     {
340         assert(nogcFormat!"%s"(
341             UUID([138, 179, 6, 14, 44, 186, 79, 35, 183, 76, 181, 45, 179, 189, 251, 70]))
342             == "8ab3060e-2cba-4f23-b74c-b52db3bdfb46");
343     }
344 
345     // array format
346     version (D_BetterC)
347     {
348         int[] arr = () @trusted { return (cast(int*)enforceMalloc(int.sizeof*10))[0..10]; }();
349         foreach (i; 0..10) arr[i] = i;
350         scope (exit) () @trusted { pureFree(arr.ptr); }();
351     }
352     else auto arr = [0,1,2,3,4,5,6,7,8,9];
353 
354     assert(nogcFormat!"foo %(%d %)"(arr[1..4]) == "foo 1 2 3");
355     assert(nogcFormat!"foo %-(%d %)"(arr[1..4]) == "foo 1 2 3");
356     assert(nogcFormat!"foo %(-%d-%|, %)"(arr[1..4]) == "foo -1-, -2-, -3-");
357     assert(nogcFormat!"%(0x%02x %)"(arr[1..4]) == "0x01 0x02 0x03");
358     assert(nogcFormat!"%(%(%d %)\n%)"(arr[1..$].chunks(3)) == "1 2 3\n4 5 6\n7 8 9");
359 
360     // range format
361     auto r = arr.filter!(a => a < 5);
362     assert(nogcFormat!"%s"(r) == "[0, 1, 2, 3, 4]");
363 
364     // Arg num
365     assert(!__traits(compiles, nogcFormat!"abc"(5)));
366     assert(!__traits(compiles, nogcFormat!"%d"()));
367     assert(!__traits(compiles, nogcFormat!"%d a %d"(5)));
368 
369     // Format error
370     assert(!__traits(compiles, nogcFormat!"%"()));
371     assert(!__traits(compiles, nogcFormat!"abcd%d %"(15)));
372     assert(!__traits(compiles, nogcFormat!"%$"(1)));
373     assert(!__traits(compiles, nogcFormat!"%d"("hello")));
374     assert(!__traits(compiles, nogcFormat!"%x"("hello")));
375 
376     assert(nogcFormat!"Hello %s"(5) == "Hello 5");
377 
378     struct Foo { int x, y; }
379     assert(nogcFormat!("Hello %s")(Foo(1, 2)) == "Hello Foo(x=1, y=2)");
380 
381     version (D_BetterC)
382     {
383         struct Nullable(T) // can't be instanciated in betterC - fake just for the UT
384         {
385             T get() { return T.init; }
386             bool isNull() { return true; }
387             void nullify() {}
388         }
389     }
390     else import std.typecons : Nullable;
391 
392     struct Msg { Nullable!string foo; }
393     assert(nogcFormat!"%s"(Msg.init) == "Msg(foo=null)");
394 
395     RCString s = "abcd";
396     assert(nogcFormat!"%s"(s) == "abcd");
397 }
398 
399 ///
400 @("tuple")
401 @safe unittest
402 {
403     {
404         alias T = Tuple!(int, "foo", bool);
405         T t = T(42, true);
406         assert(nogcFormat(t) == "Tuple(foo=42, true)");
407     }
408 
409     {
410         alias T = Tuple!(int, "foo", string, "bar", char, "baz");
411         T t = T(42, "bar", 'z');
412         assert(nogcFormat(t) == "Tuple(foo=42, bar=bar, baz=z)");
413     }
414 }
415 
416 ///
417 @("custom format")
418 @safe unittest
419 {
420     static struct Custom
421     {
422         int foo = 42;
423         void toString(S)(ref S sink) const
424         {
425             sink.put("custom: ");
426             sink.nogcFormatTo!"foo=%d"(foo);
427         }
428     }
429 
430     Custom c;
431     assert(nogcFormat(c) == "custom: foo=42");
432     assert(getFormatSize(c) == "custom: foo=42".length);
433 }
434 
435 version (D_BetterC) {}
436 else version (linux)
437 {
438     // Only Posix is supported ATM
439     @("Exception stack trace format")
440     @safe unittest
441     {
442         import std.algorithm : startsWith;
443         static class TestException : Exception { this(string msg) nothrow { super(msg); } }
444         static void fn() { throw new TestException("foo"); }
445 
446         try fn();
447         catch (Exception ex)
448         {
449             import std.format : format;
450             string std = () @trusted { return format!"Now how cool is that!: %s"(ex); }();
451             (Exception ex, string std) nothrow @nogc @trusted
452             {
453                 auto str = nogcFormat!"Now how cool is that!: %s"(ex);
454                 assert(str.startsWith("Now how cool is that!: bc.string.format.__unittest_L"));
455                 // import core.stdc.stdio; printf("%s\nvs\n%s\n", std.ptr, str.ptr);
456                 static if (__VERSION__ >= 2095)
457                 {
458                     // we try to reflect last compiler behavior, previous might differ
459                     assert(str[0..$] == std[0..$]);
460                 }
461                 else
462                 {
463                     int ln = -1;
464                     foreach (i, c; str[]) {
465                         if (c=='\n') {
466                             ln = cast(int)i;
467                             break;
468                         }
469                     }
470 
471                     assert(ln>0);
472                     assert(str[0..ln] == std[0..ln]);
473                 }
474             }(ex, std);
475         }
476     }
477 }
478 
479 /**
480  * Gets size needed to hold formatted string result
481  */
482 size_t getFormatSize(string fmt = "%s", ARGS...)(auto ref ARGS args) @safe nothrow @nogc
483 {
484     NullSink ns;
485     return ns.nogcFormatTo!fmt(args);
486 }
487 
488 @("getFormatSize")
489 @safe unittest
490 {
491     assert(getFormatSize!"foo" == 3);
492     assert(getFormatSize!"foo=%d"(42) == 6);
493     assert(getFormatSize!"%04d-%02d-%02dT%02d:%02d:%02d.%03d"(2020, 4, 28, 19, 20, 32, 207) == 23);
494     assert(getFormatSize!"%x"(0x2C38) == 4);
495     assert(getFormatSize!"%s"(9896) == 4);
496 }
497 
498 /// pseudosink used just for calculation of resulting string length
499 struct NullSink {}
500 
501 private enum FMT: ubyte {
502     STR,
503     CHR,
504     DEC, // also for BOOL
505     HEX,
506     UHEX,
507     PTR,
508     FLT,
509 }
510 
511 private struct FmtParams
512 {
513     bool leftJustify; // Left justify the result in the field. It overrides any 0 flag.
514     bool signed; // Prefix positive numbers in a signed conversion with a +. It overrides any space flag.
515     bool prefixHex; // If non-zero, prefix result with 0x (0X).
516     int width; // pad with characters, if -1, use previous argument as width
517     char fill = ' '; // character to pad with
518     int sep; // insert separator each X digits, if -1, use previous argument as X
519     bool sepChar; // is separator char defined in additional arg?
520     int prec; // precision, if -1, use previous argument as precision value
521 }
522 
523 private bool isDigit()(immutable char c) { return c >= '0' && c <= '9'; }
524 
525 // Parses format specifier in CTFE
526 // See: https://dlang.org/phobos/std_format.html for details
527 // Note: Just a subset of the specification is supported ATM. Parser here parses the spec, but
528 // formatter doesn't use it all.
529 //
530 // FormatStringItem:
531 //     '%%'
532 //     '%' Position Flags Width Separator Precision FormatChar
533 //     '%(' FormatString '%)'
534 //     '%-(' FormatString '%)'
535 //
536 auto formatSpec()(FMT f, string spec)
537 {
538     FmtParams res; int idx;
539 
540     if (spec.length)
541     {
542         assert(spec.indexOf('$') < 0, "Position specifier not supported");
543 
544         // Flags:
545         //   empty
546         //   '-' Flags
547         //   '+' Flags
548         //   '#' Flags
549         //   '0' Flags
550         //   ' ' Flags
551         while (idx < spec.length)
552         {
553             if (spec[idx] == '-') {
554                 res.leftJustify = true; idx++; continue;
555             } else if (f.among(FMT.DEC, FMT.FLT) && spec[idx] == '+') {
556                 res.signed = true; idx++; continue;
557             } else if (f == FMT.HEX && spec[idx] == '#') {
558                 // TODO: 'o' - Add to precision as necessary so that the first digit of the octal formatting is a '0', even if both the argument and the Precision are zero.
559                 res.prefixHex = true; idx++; continue;
560             } else if (f == FMT.FLT && spec[idx] == '#') {
561                 // TODO: Always insert the decimal point and print trailing zeros.
562                 idx++; continue;
563             } else if (f.among(FMT.DEC, FMT.FLT, FMT.HEX, FMT.UHEX, FMT.PTR) && spec[idx].among('0', ' ')) {
564                 res.fill = spec[idx++]; continue;
565             }
566             break;
567         }
568 
569         if (idx == spec.length) goto done;
570 
571         // Width:
572         //     empty
573         //     Integer
574         //     '*'
575         if (spec[idx] == '*') { res.width = -1; idx++; }
576         else {
577             while (idx < spec.length && spec[idx].isDigit) res.width = res.width*10 + (spec[idx++] - '0');
578         }
579 
580         if (idx == spec.length) goto done;
581 
582         // Separator:
583         //     empty
584         //     ','
585         //     ',' '?'
586         //     ',' '*' '?'
587         //     ',' Integer '?'
588         //     ',' '*'
589         //     ',' Integer
590         if (spec[idx] == ',') {
591             // ie: writefln("'%,*?d'", 4, '$', 123456789);
592             idx++;
593             if (idx == spec.length) {res.sep = 3; goto done; }
594             if (spec[idx].isDigit) {
595                 while (idx < spec.length && spec[idx].isDigit) res.sep = res.sep*10 + (spec[idx++] - '0');
596             } else if (spec[idx] == '*') {
597                 idx++; res.sep = -1;
598             } else res.sep = 3;
599 
600             if (idx == spec.length) goto done;
601             if (spec[idx] == '?') { res.sepChar = true; idx++; }
602         }
603         if (idx == spec.length) goto done;
604 
605         // Precision:
606         //     empty
607         //     '.'
608         //     '.' Integer
609         //     '.*'
610         if (spec[idx] == '.') {
611             idx++;
612             if (idx == spec.length) { res.prec = 6; goto done; }
613             if (spec[idx].isDigit) {
614                 while (idx < spec.length && spec[idx].isDigit) res.prec = res.prec*10 + (spec[idx++] - '0');
615             } else if (spec[idx] == '*') {
616                 idx++; res.prec = -1;
617             }
618         }
619     }
620 
621 done:
622     assert(idx == spec.length, "Parser error");
623     return res;
624 }
625 
626 // Used to find end of the format specifier.
627 // See: https://dlang.org/phobos/std_format.html for grammar and valid characters for fmt spec
628 // Note: Nested array fmt spec is handled separately so no '(', ')' characters here
629 private ulong getNextNonDigitFrom()(string fmt)
630 {
631     ulong idx;
632     foreach (c; fmt) {
633         if ("0123456789+-.,#*?$ ".indexOf(c) < 0)
634             return idx;
635         ++idx;
636     }
637     return idx;
638 }
639 
640 private long getNestedArrayFmtLen()(string fmt)
641 {
642     long idx; int lvl;
643     while (idx < fmt.length)
644     {
645         // detect next level of nested array format spec
646         if (fmt[idx] == '(' // new nested array can be '%(' or '%-('
647             && (
648                 (idx > 0 && fmt[idx-1] == '%')
649                 || (idx > 1 && fmt[idx-2] == '%' && fmt[idx-1] == '-')
650             )) lvl++;
651         // detect end of nested array format spec
652         if (fmt[idx] == '%' && fmt.length > idx+1 && fmt[idx+1] == ')') {
653             if (!lvl) return idx+2;
654             else --lvl;
655         }
656         ++idx;
657     }
658     return -1;
659 }
660 
661 @("getNestedArrayFmtLen")
662 unittest
663 {
664     static assert(getNestedArrayFmtLen("%d%)foo") == 4);
665     static assert(getNestedArrayFmtLen("%d%| %)foo") == 7);
666     static assert(getNestedArrayFmtLen("%(%d%)%)foo") == 8);
667 }
668 
669 // workaround for std.string.indexOf not working in betterC
670 private ptrdiff_t indexOf()(string fmt, char c)
671 {
672     for (ptrdiff_t i = 0; i < fmt.length; ++i)
673         if (fmt[i] == c) return i;
674     return -1;
675 }
676 
677 // Phobos version has bug in CTFE, see: https://issues.dlang.org/show_bug.cgi?id=20783
678 private ptrdiff_t fixedLastIndexOf()(string s, string sub)
679 {
680     if (!__ctfe) assert(0);
681 
682     LOOP: for (ptrdiff_t i = s.length - sub.length; i >= 0; --i)
683     {
684         version (D_BetterC)
685         {
686             // workaround for missing symbol used by DMD
687             for (ptrdiff_t j=0; j<sub.length; ++j)
688                 if (s[i+j] != sub[j]) continue LOOP;
689             return i;
690         }
691         else
692         {
693             if (s[i .. i + sub.length] == sub[])
694                 return i;
695         }
696     }
697     return -1;
698 }
699 
700 private template getNestedArrayFmt(string fmt)
701 {
702     import std.meta : AliasSeq;
703 
704     // make sure we're searching in top level only
705     enum lastSubEnd = fmt.fixedLastIndexOf("%)");
706     static if (lastSubEnd > 0)
707     {
708         enum idx = fmt[lastSubEnd+2..$].fixedLastIndexOf("%|"); // delimiter separator used
709         static if (idx >= 0)
710             alias getNestedArrayFmt = AliasSeq!(fmt[0..lastSubEnd+2+idx], fmt[lastSubEnd+idx+4..$]);
711         else
712             alias getNestedArrayFmt = AliasSeq!(fmt[0..lastSubEnd+2], fmt[lastSubEnd+2..$]);
713     }
714     else
715     {
716         enum idx = fmt.fixedLastIndexOf("%|"); // delimiter separator used
717         static if (idx >= 0)
718             alias getNestedArrayFmt = AliasSeq!(fmt[0..idx], fmt[idx+2..$]); // we can return delimiter directly
719         else {
720             // we need to find end of inner fmt spec first
721             static assert(fmt.length >= 2, "Invalid nested array element format specifier: " ~ fmt);
722             enum startIdx = fmt.indexOf('%');
723             static assert(startIdx >=0, "No nested array element format specified");
724             enum endIdx = startIdx + 1 + getNextNonDigitFrom(fmt[startIdx + 1 .. $]);
725             enum len = endIdx-startIdx+1;
726 
727             static if ((len == 2 && fmt[startIdx+1] == '(') || (len == 3 && fmt[startIdx+1..startIdx+3] == "-("))
728             {
729                 // further nested array fmt spec -> split by end of nested highest level
730                 enum nlen = fmt[1] == '(' ? (2 + getNestedArrayFmtLen(fmt[2..$])) : (3 + getNestedArrayFmtLen(fmt[3..$]));
731                 static assert(nlen > 0, "Invalid nested array format specifier: " ~ fmt);
732                 alias getNestedArrayFmt = AliasSeq!(fmt[0..nlen], fmt[nlen..$]);
733             }
734             else
735                 // split at the end of element fmt spec
736                 alias getNestedArrayFmt = AliasSeq!(fmt[0..endIdx+1], fmt[endIdx+1..$]);
737         }
738     }
739 }
740 
741 @("getNestedArrayFmt")
742 unittest
743 {
744     import std.meta : AliasSeq;
745     static assert(getNestedArrayFmt!"%d " == AliasSeq!("%d", " "));
746     static assert(getNestedArrayFmt!"%d %|, " == AliasSeq!("%d ", ", "));
747     static assert(getNestedArrayFmt!"%(%d %|, %)" == AliasSeq!("%(%d %|, %)", ""));
748     static assert(getNestedArrayFmt!"%(%d %|, %),-" == AliasSeq!("%(%d %|, %)", ",-"));
749     static assert(getNestedArrayFmt!"foo%(%d %|, %)-%|;" == AliasSeq!("foo%(%d %|, %)-", ";"));
750 }
751 
752 private struct FmtSpec
753 {
754     int idx;
755     FMT type;
756     string def;
757 }
758 
759 // Nested array format specifier
760 private struct ArrFmtSpec
761 {
762     int idx;
763     string fmt; // item format
764     string del; // delimiter
765     bool esc; // escape strings and characters
766 }
767 
768 /**
769  * Splits format string based on the same rules as described here: https://dlang.org/phobos/std_format.html
770  * In addition it supports 'p' as a pointer format specifier to be more compatible with `printf`.
771  * It supports nested arrays format specifier too.
772  */
773 template splitFmt(string fmt) {
774     template spec(int j, FMT f, string def) {
775         enum spec = FmtSpec(j, f, def);
776     }
777     template arrSpec(int j, string fmt, string del, bool esc) {
778         enum arrSpec = ArrFmtSpec(j, fmt, del, esc);
779     }
780 
781     template helper(int from, int j) {
782         import std.typetuple : TypeTuple;
783         enum idx = fmt[from .. $].indexOf('%');
784         static if (idx < 0) {
785             enum helper = TypeTuple!(fmt[from .. $]);
786         }
787         else {
788             enum idx1 = idx + from;
789             static if (idx1 >= fmt.length - 1) {
790                 static assert (false, "Expected formatter after %");
791             } else {
792                 enum idx2 = idx1 + getNextNonDigitFrom(fmt[idx1+1 .. $]);
793                 // pragma(msg, "fmt: ", fmt[from .. idx2]);
794                 static if (fmt[idx2+1] == 's')
795                     enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.STR, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1));
796                 else static if (fmt[idx2+1] == 'c')
797                     enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.CHR, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1));
798                 else static if (fmt[idx2+1] == 'b') // TODO: should be binary, but use hex for now
799                     enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.HEX, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1));
800                 else static if (fmt[idx2+1].among('d', 'u'))
801                     enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.DEC, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1));
802                 else static if (fmt[idx2+1] == 'o') // TODO: should be octal, but use hex for now
803                     enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.DEC, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1));
804                 else static if (fmt[idx2+1] == 'x')
805                     enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.HEX, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1));
806                 else static if (fmt[idx2+1] == 'X')
807                     enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.UHEX, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1));
808                 else static if (fmt[idx2+1].among('e', 'E', 'f', 'F', 'g', 'G', 'a', 'A')) // TODO: float number formatters
809                     enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.FLT, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1));
810                 else static if (fmt[idx2+1] == 'p')
811                     enum helper = TypeTuple!(fmt[from .. idx1], spec!(j, FMT.PTR, fmt[idx1+1 .. idx2+1]), helper!(idx2+2, j+1));
812                 else static if (fmt[idx2+1] == '%')
813                     enum helper = TypeTuple!(fmt[from .. idx1+1], helper!(idx2+2, j));
814                 else static if (fmt[idx2+1] == '(' || fmt[idx2+1..idx2+3] == "-(") {
815                     // nested array format specifier
816                     enum l = fmt[idx2+1] == '('
817                         ? getNestedArrayFmtLen(fmt[idx2+2..$])
818                         : getNestedArrayFmtLen(fmt[idx2+3..$]);
819                     alias naSpec = getNestedArrayFmt!(fmt[idx2+2 .. idx2+2+l-2]);
820                     // pragma(msg, fmt[from .. idx1], "|", naSpec[0], "|", naSpec[1], "|");
821                     enum helper = TypeTuple!(
822                         fmt[from .. idx1],
823                         arrSpec!(j, naSpec[0], naSpec[1], fmt[idx2+1] != '('),
824                         helper!(idx2+2+l, j+1));
825                 }
826                 else static assert (false, "Invalid formatter '" ~ fmt[idx2+1] ~ "' in fmt='" ~ fmt ~ "'");
827             }
828         }
829     }
830 
831     template countFormatters(tup...) {
832         static if (tup.length == 0)
833             enum countFormatters = 0;
834         else static if (is(typeof(tup[0]) == FmtSpec) || is(typeof(tup[0]) == ArrFmtSpec))
835             enum countFormatters = 1 + countFormatters!(tup[1 .. $]);
836         else
837             enum countFormatters = countFormatters!(tup[1 .. $]);
838     }
839 
840     alias tokens = helper!(0, 0);
841     alias numFormatters = countFormatters!tokens;
842 }
843 
844 /// Returns string of enum member value
845 string enumToStr(E)(E value) pure @safe nothrow @nogc
846 {
847     foreach (i, e; EnumMembers!E)
848     {
849         if (value == e) return __traits(allMembers, E)[i];
850     }
851     return null;
852 }
853 
854 size_t formatPtr(S)(ref S sink, ulong p) pure @trusted nothrow @nogc
855 {
856     pragma(inline);
857     return formatPtr(sink, cast(void*)p);
858 }
859 
860 size_t formatPtr(S)(ref S sink, const void* ptr) pure @safe nothrow @nogc
861 {
862     pragma(inline);
863     mixin SinkWriter!S;
864     if (ptr is null) { write("null"); return 4; }
865     else {
866         import std.stdint : intptr_t;
867         return sink.formatHex!((void*).sizeof*2)(cast(intptr_t)ptr);
868     }
869 }
870 
871 @("pointer")
872 @safe unittest
873 {
874     char[100] buf;
875 
876     () @nogc @safe
877     {
878         assert(formatPtr(buf, 0x123) && buf[0..16] == "0000000000000123");
879         assert(formatPtr(buf, 0) && buf[0..4] == "null");
880         assert(formatPtr(buf, null) && buf[0..4] == "null");
881     }();
882 }
883 
884 alias Upper = Flag!"Upper";
885 
886 pure @safe nothrow @nogc
887 size_t formatHex(size_t W = 0, char fill = '0', Upper upper = Upper.no, S)(ref S sink, ulong val)
888 {
889     static if (is(S == NullSink))
890     {
891         // just formatted length calculation
892         import std.algorithm : max;
893         size_t len = 0;
894         if (!val) len = 1;
895         else { while (val) { val >>= 4; len++; } }
896         return max(W, len);
897     }
898     else
899     {
900         mixin SinkWriter!S;
901 
902         size_t len = 0;
903         auto v = val;
904         char[16] buf = void;
905 
906         while (v) { v >>= 4; len++; }
907         static if (W > 0) {
908             if (W > len) {
909                 buf[0..W-len] = '0';
910                 len = W;
911             }
912         }
913 
914         v = val;
915         if (v == 0) {
916             static if (W == 0) { buf[0] = '0'; len = 1; } // special case for null
917         }
918         else {
919             auto idx = len;
920             while (v) {
921                 static if (upper)
922                     buf[--idx] = "0123456789ABCDEF"[v & 0x0f];
923                 else buf[--idx] = "0123456789abcdef"[v & 0x0f];
924                 v >>= 4;
925             }
926         }
927 
928         write(buf[0..len]);
929         return len;
930     }
931 }
932 
933 @("hexadecimal")
934 @safe @nogc unittest
935 {
936     char[100] buf;
937     assert(formatHex(buf, 0x123) && buf[0..3] == "123");
938     assert(formatHex!10(buf, 0x123) && buf[0..10] == "0000000123");
939     assert(formatHex(buf, 0) && buf[0..1] == "0");
940     assert(formatHex!10(buf, 0) && buf[0..10] == "0000000000");
941     assert(formatHex!10(buf, 0xa23456789) && buf[0..10] == "0a23456789");
942     assert(formatHex!10(buf, 0x1234567890) && buf[0..10] == "1234567890");
943     assert(formatHex!(10, '0', Upper.yes)(buf, 0x1234567890a) && buf[0..11] == "1234567890A");
944 }
945 
946 size_t formatDecimal(size_t W = 0, char fillChar = ' ', S, T)(ref S sink, T val) pure @safe nothrow @nogc
947     if (is(typeof({ulong v = val;})))
948 {
949     import bc..string.numeric : numDigits;
950 
951     static if (is(Unqual!T == bool)) size_t len = 1;
952     else size_t len = numDigits(val);
953 
954     static if (is(S == NullSink))
955     {
956         // just formatted length calculation
957         import std.algorithm : max;
958         return max(W, len);
959     }
960     else
961     {
962         mixin SinkWriter!S;
963 
964         ulong v;
965         char[20] buf = void; // max number of digits for 8bit numbers is 20
966         size_t idx;
967 
968         static if (isSigned!T)
969         {
970             import std.ascii : isWhite;
971             if (_expect(val < 0, false))
972             {
973                 if (_expect(val == long.min, false))
974                 {
975                     // special case for unconvertable value
976                     write("-9223372036854775808");
977                     return 20;
978                 }
979 
980                 static if (!isWhite(fillChar)) buf[idx++] = '-'; // to write minus character after padding
981                 v = -long(val);
982             }
983             else v = val;
984         }
985         else v = val;
986 
987         static if (W > 0) {
988             if (W > len) {
989                 buf[idx .. idx + W - len] = fillChar;
990                 idx += W-len;
991                 len = W;
992             }
993         }
994 
995         static if (isSigned!T && isWhite(fillChar))
996             if (val < 0) buf[idx++] = '-';
997 
998         if (v == 0) buf[idx++] = '0';
999         else {
1000             idx = len;
1001             while (v) {
1002                 buf[--idx] = "0123456789"[v % 10];
1003                 v /= 10;
1004             }
1005         }
1006 
1007         write(buf[0..len]);
1008         return len;
1009     }
1010 }
1011 
1012 @("decimal")
1013 @safe @nogc unittest
1014 {
1015     char[100] buf;
1016     assert(formatDecimal!10(buf, -1234)  && buf[0..10] == "     -1234");
1017     assert(formatDecimal!10(buf, 0)      && buf[0..10] == "         0");
1018     assert(formatDecimal(buf, -1234)     && buf[0..5] == "-1234");
1019     assert(formatDecimal(buf, 0)         && buf[0..1] == "0");
1020     assert(formatDecimal!3(buf, 1234)    && buf[0..4] == "1234");
1021     assert(formatDecimal!3(buf, -1234)   && buf[0..5] == "-1234");
1022     assert(formatDecimal!3(buf, 0)       && buf[0..3] == "  0");
1023     assert(formatDecimal!(3,'0')(buf, 0) && buf[0..3] == "000");
1024     assert(formatDecimal!(3,'a')(buf, 0) && buf[0..3] == "aa0");
1025     assert(formatDecimal!(10, '0')(buf, -1234) && buf[0..10] == "-000001234");
1026     assert(formatDecimal(buf, true) && buf[0..1] == "1");
1027 }
1028 
1029 size_t formatFloat(S)(ref S sink, double val) @trusted nothrow @nogc // not pure with this implementation
1030 {
1031     import core.stdc.stdio : snprintf;
1032     import std.algorithm : min;
1033     char[20] buf = void;
1034     auto len = snprintf(&buf[0], 20, "%g", val);
1035     len = min(len, 19);
1036     static if (!is(S == NullSink))
1037     {
1038         mixin SinkWriter!S;
1039         write(buf[0..len]);
1040     }
1041     return len;
1042 }
1043 
1044 @("float")
1045 @safe unittest
1046 {
1047     char[100] buf;
1048     assert(formatFloat(buf, 1.2345) && buf[0..6] == "1.2345");
1049     assert(formatFloat(buf, double.init) && buf[0..3] == "nan");
1050     assert(formatFloat(buf, double.infinity) && buf[0..3] == "inf");
1051 }
1052 
1053 size_t formatUUID(S)(ref S sink, UUID val) pure @safe nothrow @nogc
1054 {
1055     static if (!is(S == NullSink))
1056     {
1057         import std.meta : AliasSeq;
1058 
1059         mixin SinkWriter!S;
1060 
1061         alias skipSeq = AliasSeq!(8, 13, 18, 23);
1062         alias byteSeq = AliasSeq!(0,2,4,6,9,11,14,16,19,21,24,26,28,30,32,34);
1063 
1064         char[36] buf = void;
1065 
1066         static foreach (pos; skipSeq)
1067             buf[pos] = '-';
1068 
1069         static foreach (i, pos; byteSeq)
1070         {
1071             buf[pos  ] = toChar!char(val.data[i] >> 4);
1072             buf[pos+1] = toChar!char(val.data[i] & 0x0F);
1073         }
1074 
1075         write(buf[0..36]);
1076     }
1077     return 36;
1078 }
1079 
1080 version (D_BetterC) {}
1081 else
1082 @("UUID")
1083 @safe unittest
1084 {
1085     char[100] buf;
1086     assert(formatUUID(buf, UUID([138, 179, 6, 14, 44, 186, 79, 35, 183, 76, 181, 45, 179, 189, 251, 70])) == 36);
1087     assert(buf[0..36] == "8ab3060e-2cba-4f23-b74c-b52db3bdfb46");
1088 }
1089 
1090 /**
1091  * Formats SysTime as ISO extended string.
1092  * Only UTC format supported.
1093  */
1094 size_t formatSysTime(S)(ref S sink, SysTime val) @trusted nothrow @nogc // not pure because of gmtime_r
1095 {
1096     mixin SinkWriter!S;
1097 
1098     // Note: we don't format based on the timezone set in SysTime, but just use UTC here
1099     enum hnsecsToUnixEpoch = 621_355_968_000_000_000L;
1100     enum hnsecsFrom1601    = 504_911_232_000_000_000L;
1101 
1102     static immutable char[7] invalidTimeBuf = "invalid";
1103 
1104     long time = __traits(getMember, val, "_stdTime"); // access private field
1105     long hnsecs = time % 10_000_000;
1106 
1107     // check for invalid time value
1108     version (Windows) {
1109         if (time < hnsecsFrom1601) { write(invalidTimeBuf); return invalidTimeBuf.length; }
1110     }
1111 
1112     static if (is(S == NullSink))
1113     {
1114         // just count required number of characters needed for hnsecs
1115         int len = 20; // fixed part for date time with just seconds resolution (including 'Z')
1116         if (hnsecs == 0) return len; // no fract seconds part
1117         len += 2; // dot and at least one number
1118         foreach (i; [1_000_000, 100_000, 10_000, 1_000, 100, 10])
1119         {
1120             hnsecs %= i;
1121             if (hnsecs == 0) break;
1122             len++;
1123         }
1124         return len;
1125     }
1126     else
1127     {
1128         char[28] buf; // maximal length for UTC extended ISO string
1129 
1130         version (Posix)
1131         {
1132             import core.sys.posix.sys.types : time_t;
1133             import core.sys.posix.time : gmtime_r, tm;
1134 
1135             time -= hnsecsToUnixEpoch; // convert to unix time but still with hnsecs
1136 
1137             // split to hnsecs and time in seconds
1138             time_t unixTime = time / 10_000_000;
1139 
1140             tm timeSplit;
1141             gmtime_r(&unixTime, &timeSplit);
1142 
1143             buf.nogcFormatTo!"%04d-%02d-%02dT%02d:%02d:%02d"(
1144                 timeSplit.tm_year + 1900,
1145                 timeSplit.tm_mon + 1,
1146                 timeSplit.tm_mday,
1147                 timeSplit.tm_hour,
1148                 timeSplit.tm_min,
1149                 timeSplit.tm_sec
1150             );
1151         }
1152         else version (Windows)
1153         {
1154             import core.sys.windows.winbase : FILETIME, FileTimeToSystemTime, SYSTEMTIME;
1155             import core.sys.windows.winnt : ULARGE_INTEGER;
1156 
1157             ULARGE_INTEGER ul;
1158             ul.QuadPart = cast(ulong)time - hnsecsFrom1601;
1159 
1160             FILETIME ft;
1161             ft.dwHighDateTime = ul.HighPart;
1162             ft.dwLowDateTime = ul.LowPart;
1163 
1164             SYSTEMTIME stime;
1165             FileTimeToSystemTime(&ft, &stime);
1166 
1167             buf.nogcFormatTo!"%04d-%02d-%02dT%02d:%02d:%02d"(
1168                 stime.wYear,
1169                 stime.wMonth,
1170                 stime.wDay,
1171                 stime.wHour,
1172                 stime.wMinute,
1173                 stime.wSecond
1174             );
1175         }
1176         else static assert(0, "SysTime format not supported for this platform yet");
1177 
1178         if (hnsecs == 0)
1179         {
1180             buf[19] = 'Z';
1181             write(buf[0..20]);
1182             return 20;
1183         }
1184 
1185         buf[19] = '.';
1186 
1187         int len = 20;
1188         foreach (i; [1_000_000, 100_000, 10_000, 1_000, 100, 10, 1])
1189         {
1190             buf[len++] = cast(char)(hnsecs / i + '0');
1191             hnsecs %= i;
1192             if (hnsecs == 0) break;
1193         }
1194         buf[len++] = 'Z';
1195         write(buf[0..len]);
1196         return len;
1197     }
1198 }
1199 
1200 version (D_BetterC) {}
1201 else
1202 @("SysTime")
1203 @safe unittest
1204 {
1205     char[100] buf;
1206 
1207     assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.1234567Z")) == 28);
1208     assert(buf[0..28] == "2020-06-08T14:25:30.1234567Z");
1209     assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.123456Z")) == 27);
1210     assert(buf[0..27] == "2020-06-08T14:25:30.123456Z");
1211     assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.12345Z")) == 26);
1212     assert(buf[0..26] == "2020-06-08T14:25:30.12345Z");
1213     assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.1234Z")) == 25);
1214     assert(buf[0..25] == "2020-06-08T14:25:30.1234Z");
1215     assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.123Z")) == 24);
1216     assert(buf[0..24] == "2020-06-08T14:25:30.123Z");
1217     assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.12Z")) == 23);
1218     assert(buf[0..23] == "2020-06-08T14:25:30.12Z");
1219     assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30.1Z")) == 22);
1220     assert(buf[0..22] == "2020-06-08T14:25:30.1Z");
1221     assert(formatSysTime(buf, SysTime.fromISOExtString("2020-06-08T14:25:30Z")) == 20);
1222     assert(buf[0..20] == "2020-06-08T14:25:30Z");
1223     version (Posix) {
1224         assert(formatSysTime(buf, SysTime.init) == 20);
1225         assert(buf[0..20] == "0001-01-01T00:00:00Z");
1226     }
1227     else version (Windows) {
1228         assert(formatSysTime(buf, SysTime.init) == 7);
1229         assert(buf[0..7] == "invalid");
1230     }
1231 
1232     assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.1234567Z")) == 28);
1233     assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.123456Z")) == 27);
1234     assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.12345Z")) == 26);
1235     assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.1234Z")) == 25);
1236     assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.123Z")) == 24);
1237     assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.12Z")) == 23);
1238     assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30.1Z")) == 22);
1239     assert(getFormatSize!"%s"(SysTime.fromISOExtString("2020-06-08T14:25:30Z")) == 20);
1240 }
1241 
1242 /**
1243  * Formats duration.
1244  * It uses custom formatter that is inspired by std.format output, but a bit shorter.
1245  * Note: ISO 8601 was considered, but it's not as human readable as used format.
1246  */
1247 size_t formatDuration(S)(ref S sink, Duration val) @trusted nothrow @nogc pure
1248 {
1249     mixin SinkWriter!S;
1250 
1251     enum secondsInDay = 86_400;
1252     enum secondsInHour = 3_600;
1253     enum secondsInMinute = 60;
1254 
1255     long totalHNS = __traits(getMember, val, "_hnsecs"); // access private member
1256     if (totalHNS < 0) { write("-"); totalHNS = -totalHNS; }
1257 
1258     immutable long fracSecs = totalHNS % 10_000_000;
1259     long totalSeconds = totalHNS / 10_000_000;
1260 
1261     if (totalSeconds)
1262     {
1263         immutable long days = totalSeconds / secondsInDay;
1264         long seconds = totalSeconds % secondsInDay;
1265         if (days) advance(s.nogcFormatTo!"%d days"(days));
1266         if (seconds)
1267         {
1268             immutable hours = seconds / secondsInHour;
1269             seconds %= secondsInHour;
1270             if (hours)
1271                 advance(days ? s.nogcFormatTo!", %d hrs"(hours) : s.nogcFormatTo!"%d hrs"(hours));
1272 
1273             if (seconds)
1274             {
1275                 immutable minutes = seconds / secondsInMinute;
1276                 seconds %= secondsInMinute;
1277                 if (minutes)
1278                     advance(days || hours ? s.nogcFormatTo!", %d mins"(minutes) : s.nogcFormatTo!"%d mins"(minutes));
1279 
1280                 if (seconds)
1281                     advance(days || hours || minutes ? s.nogcFormatTo!", %d secs"(seconds) : s.nogcFormatTo!"%d secs"(seconds));
1282             }
1283         }
1284     }
1285 
1286     if (fracSecs)
1287     {
1288         immutable msecs = fracSecs / 10_000;
1289         int usecs = fracSecs % 10_000;
1290 
1291         if (msecs | usecs)
1292         {
1293             advance(totalSeconds ? s.nogcFormatTo!", %d"(msecs) : s.nogcFormatTo!"%d"(msecs));
1294 
1295             if (usecs)
1296             {
1297                 char[5] buf = void;
1298                 buf[0] = '.';
1299 
1300                 int ulen = 1;
1301                 foreach (i; [1_000, 100, 10, 1])
1302                 {
1303                     buf[ulen++] = cast(char)(usecs / i + '0');
1304                     usecs %= i;
1305                     if (usecs == 0) break;
1306                 }
1307                 write(buf[0..ulen]);
1308             }
1309 
1310             write(" ms");
1311         }
1312     }
1313 
1314     if (!totalLen) write("0 ms");
1315 
1316     return totalLen;
1317 }
1318 
1319 version (D_BetterC) {}
1320 else
1321 @("duration")
1322 @safe unittest
1323 {
1324     import core.time;
1325     char[100] buf;
1326 
1327     assert(formatDuration(buf, 1.seconds) == 6);
1328     assert(buf[0..6] == "1 secs");
1329 
1330     assert(formatDuration(buf, 1.seconds + 15.msecs + 5.hnsecs) == 18);
1331     assert(buf[0..18] == "1 secs, 15.0005 ms");
1332 
1333     assert(formatDuration(buf, 1.seconds + 1215.msecs + 15.hnsecs) == 19);
1334     assert(buf[0..19] == "2 secs, 215.0015 ms");
1335 
1336     assert(formatDuration(buf, 5.days) == 6);
1337     assert(buf[0..6] == "5 days");
1338 
1339     assert(formatDuration(buf, 5.days + 25.hours) == 13);
1340     assert(buf[0..13] == "6 days, 1 hrs");
1341 
1342     assert(formatDuration(buf, 5.days + 25.hours + 78.minutes) == 22);
1343     assert(buf[0..22] == "6 days, 2 hrs, 18 mins");
1344 
1345     assert(formatDuration(buf, 5.days + 25.hours + 78.minutes + 102.seconds) == 31);
1346     assert(buf[0..31] == "6 days, 2 hrs, 19 mins, 42 secs");
1347 
1348     assert(formatDuration(buf, 5.days + 25.hours + 78.minutes + 102.seconds + 2321.msecs) == 39);
1349     assert(buf[0..39] == "6 days, 2 hrs, 19 mins, 44 secs, 321 ms");
1350 
1351     assert(formatDuration(buf, 5.days + 25.hours + 78.minutes + 102.seconds + 2321.msecs + 1987.usecs) == 43);
1352     assert(buf[0..43] == "6 days, 2 hrs, 19 mins, 44 secs, 322.987 ms");
1353 
1354     assert(formatDuration(buf, 5.days + 25.hours + 78.minutes + 102.seconds + 2321.msecs + 1987.usecs + 15.hnsecs) == 44);
1355     assert(buf[0..44] == "6 days, 2 hrs, 19 mins, 44 secs, 322.9885 ms");
1356 
1357     assert(formatDuration(buf, -42.msecs) == 6);
1358     assert(buf[0..6] == "-42 ms");
1359 
1360     assert(formatDuration(buf, Duration.zero) == 4);
1361     assert(buf[0..4] == "0 ms");
1362 }
1363 
1364 @safe pure nothrow @nogc Char toChar(Char)(size_t i)
1365 {
1366     pragma(inline);
1367     if (i <= 9) return cast(Char)('0' + i);
1368     else return cast(Char)('a' + (i-10));
1369 }
1370 
1371 /// Output range wrapper for used sinks (so it can be used in toString functions)
1372 private struct SinkWrap(S)
1373 {
1374     private S s;
1375 
1376     static if (isArray!S && is(ForeachType!S : char))
1377         mixin SinkWriter!(S, false);
1378     else static if (isPointer!S)
1379         mixin SinkWriter!(PointerTarget!S, false);
1380     else static assert(0, "Unsupported sink type: " ~ S.stringof);
1381 
1382     this(S sink) @safe pure nothrow @nogc
1383     {
1384         this.s = sink;
1385     }
1386 }
1387 
1388 // helper to create `SinkWrap` that handles various sink types
1389 private auto sinkWrap(S)(ref S sink) @trusted // we're only using this internally and don't escape the pointer
1390 {
1391     static if (isStaticArray!S && is(ForeachType!S : char))
1392         return SinkWrap!(char[])(sink[]); // we need to slice it
1393     else static if (is(S == struct))
1394         return SinkWrap!(S*)(&sink); // work with a pointer to an original sink (ie `MallocBuffer`)
1395     else static assert(0, "Unsupported sink type: " ~ S.stringof);
1396 }
1397 
1398 @("sink wrapper")
1399 unittest
1400 {
1401     char[42] buf;
1402     auto sink = sinkWrap(buf);
1403     sink.put("foo");
1404     assert(sink.totalLen == 3);
1405     assert(buf[0..3] == "foo");
1406 }
1407 
1408 // helper functions used in formatters to write formatted string to sink
1409 private mixin template SinkWriter(S, bool field = true)
1410 {
1411     size_t totalLen;
1412     static if (isArray!S && is(ForeachType!S : char))
1413     {
1414         static if (field) char[] s = sink[];
1415 
1416         @nogc pure nothrow @trusted
1417         {
1418             void advance(size_t len)
1419             {
1420                 s = s[len..$];
1421                 totalLen += len;
1422             }
1423 
1424             void write(const(char)[] str) {
1425                 s[0..str.length] = str;
1426                 advance(str.length);
1427             }
1428 
1429             void write(char ch) {
1430                 s[0] = ch;
1431                 advance(1);
1432             }
1433         }
1434     }
1435     else static if (is(S == NullSink))
1436     {
1437         static if (field) alias s = sink;
1438         void advance(size_t len) { totalLen += len; }
1439         void write(const(char)[] str) { advance(str.length); }
1440         void write(char ch) { advance(1); }
1441     }
1442     else
1443     {
1444         static if (field) alias s = sink;
1445         @nogc pure nothrow @safe
1446         {
1447             import std.range : rput = put;
1448             void advance(size_t len) { totalLen += len; }
1449             void write(const(char)[] str) { rput(s, str); advance(str.length); }
1450             void write(char ch) { rput(s, ch); advance(1); }
1451         }
1452     }
1453 
1454     alias put = write; // output range interface
1455 }