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