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