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 }