1 /++ 2 ASDF Representation 3 4 Copyright: Tamedia Digital, 2016 5 6 Authors: Ilia Ki 7 8 License: MIT 9 10 Macros: 11 SUBMODULE = $(LINK2 asdf_$1.html, asdf.$1) 12 SUBREF = $(LINK2 asdf_$1.html#.$2, $(TT $2))$(NBSP) 13 T2=$(TR $(TDNW $(LREF $1)) $(TD $+)) 14 T4=$(TR $(TDNW $(LREF $1)) $(TD $2) $(TD $3) $(TD $4)) 15 +/ 16 module asdf.asdf; 17 18 import std.exception; 19 import std.range.primitives; 20 import std.typecons; 21 import std.traits; 22 23 import asdf.jsonbuffer; 24 import asdf.jsonparser: assumePure; 25 26 version(X86_64) 27 version = X86_Any; 28 else 29 version(X86) 30 version = X86_Any; 31 32 version (D_Exceptions) 33 { 34 import mir.serde: SerdeException; 35 /++ 36 Serde Exception 37 +/ 38 class AsdfSerdeException : SerdeException 39 { 40 /// zero based faulty location 41 size_t location; 42 43 /// 44 this( 45 string msg, 46 size_t location, 47 string file = __FILE__, 48 size_t line = __LINE__, 49 ) pure nothrow @nogc @safe 50 { 51 this.location = location; 52 super(msg, file, line); 53 } 54 55 /// 56 this( 57 string msg, 58 string file = __FILE__, 59 size_t line = __LINE__, 60 Throwable next = null) pure nothrow @nogc @safe 61 { 62 super(msg, file, line, next); 63 } 64 65 /// 66 this( 67 string msg, 68 Throwable next, 69 string file = __FILE__, 70 size_t line = __LINE__, 71 ) pure nothrow @nogc @safe 72 { 73 this(msg, file, line, next); 74 } 75 76 override AsdfSerdeException toMutable() @trusted pure nothrow @nogc const 77 { 78 return cast() this; 79 } 80 81 alias toMutable this; 82 } 83 } 84 85 deprecated("use mir.serde: SerdeException instead") 86 alias AsdfException = SerdeException; 87 88 /// 89 class InvalidAsdfException: SerdeException 90 { 91 /// 92 this( 93 uint kind, 94 string file = __FILE__, 95 size_t line = __LINE__, 96 Throwable next = null) pure nothrow @safe 97 { 98 import mir.format: text; 99 super(text("ASDF values is invalid for kind = ", kind), file, line, next); 100 } 101 102 /// 103 this( 104 uint kind, 105 Throwable next, 106 string file = __FILE__, 107 size_t line = __LINE__, 108 ) pure nothrow @safe 109 { 110 this(kind, file, line, next); 111 } 112 } 113 114 private void enforceValidAsdf( 115 bool condition, 116 uint kind, 117 string file = __FILE__, 118 size_t line = __LINE__) @safe pure 119 { 120 if(!condition) 121 throw new InvalidAsdfException(kind, file, line); 122 } 123 124 /// 125 class EmptyAsdfException: SerdeException 126 { 127 /// 128 this( 129 string msg = "ASDF value is empty", 130 string file = __FILE__, 131 size_t line = __LINE__, 132 Throwable next = null) pure nothrow @nogc @safe 133 { 134 super(msg, file, line, next); 135 } 136 } 137 138 /++ 139 The structure for ASDF manipulation. 140 +/ 141 struct Asdf 142 { 143 /// 144 enum Kind : ubyte 145 { 146 /// 147 null_ = 0x00, 148 /// 149 true_ = 0x01, 150 /// 151 false_ = 0x02, 152 /// 153 number = 0x03, 154 /// 155 string = 0x05, 156 /// 157 array = 0x09, 158 /// 159 object = 0x0A, 160 } 161 162 /// Returns ASDF Kind 163 ubyte kind() const pure @safe @nogc 164 { 165 if (!data.length) 166 { 167 static immutable exc = new EmptyAsdfException; 168 throw exc; 169 } 170 return data[0]; 171 } 172 173 /++ 174 Plain ASDF data. 175 +/ 176 ubyte[] data; 177 178 /// Creates ASDF using already allocated data 179 this(ubyte[] data) pure @safe nothrow @nogc 180 { 181 this.data = data; 182 } 183 184 /// Creates ASDF from a string 185 this(in char[] str) pure @safe 186 { 187 data = new ubyte[str.length + 5]; 188 data[0] = Kind..string; 189 length4 = str.length; 190 data[5 .. $] = cast(const(ubyte)[])str; 191 } 192 193 /// 194 unittest 195 { 196 assert(Asdf("string") == "string"); 197 assert(Asdf("string") != "String"); 198 } 199 200 // \uXXXX character support 201 unittest 202 { 203 import mir.conv: to; 204 import asdf.jsonparser; 205 assert(Asdf("begin\u000bend").to!string == `"begin\u000Bend"`); 206 assert("begin\u000bend" == cast(string) `"begin\u000Bend"`.parseJson, to!string(cast(ubyte[]) cast(string)( `"begin\u000Bend"`.parseJson))); 207 } 208 209 /// Sets deleted bit on 210 void remove() pure @safe nothrow @nogc 211 { 212 if(data.length) 213 data[0] |= 0x80; 214 } 215 216 /// 217 unittest 218 { 219 import mir.conv: to; 220 import asdf.jsonparser; 221 auto asdfData = `{"foo":"bar","inner":{"a":true,"b":false,"c":"32323","d":null,"e":{}}}`.parseJson; 222 asdfData["inner", "d"].remove; 223 assert(asdfData.to!string == `{"foo":"bar","inner":{"a":true,"b":false,"c":"32323","e":{}}}`); 224 } 225 226 /// 227 void toString(Dg)(scope Dg sink) const 228 { 229 scope buffer = JsonBuffer!Dg(sink); 230 toStringImpl(buffer); 231 buffer.flush; 232 } 233 234 /+ 235 Internal recursive toString implementation. 236 Params: 237 sink = output range that accepts `char`, `in char[]` and compile time string `(string str)()` 238 +/ 239 private void toStringImpl(Dg)(ref JsonBuffer!Dg sink) const 240 { 241 if (!data.length) 242 { 243 static immutable exc = new EmptyAsdfException("Data buffer is empty"); 244 throw exc; 245 } 246 auto t = data[0]; 247 switch(t) 248 { 249 case Kind.null_: 250 enforceValidAsdf(data.length == 1, t); 251 sink.put!"null"; 252 break; 253 case Kind.true_: 254 enforceValidAsdf(data.length == 1, t); 255 sink.put!"true"; 256 break; 257 case Kind.false_: 258 enforceValidAsdf(data.length == 1, t); 259 sink.put!"false"; 260 break; 261 case Kind.number: 262 enforceValidAsdf(data.length > 1, t); 263 size_t length = data[1]; 264 enforceValidAsdf(data.length == length + 2, t); 265 sink.putSmallEscaped(cast(const(char)[]) data[2 .. $]); 266 break; 267 case Kind..string: 268 enforceValidAsdf(data.length >= 5, Kind.object); 269 enforceValidAsdf(data.length == length4 + 5, t); 270 sink.put('"'); 271 sink.put(cast(const(char)[]) data[5 .. $]); 272 sink.put('"'); 273 break; 274 case Kind.array: 275 auto elems = Asdf(cast(ubyte[])data).byElement; 276 if(elems.empty) 277 { 278 sink.put!"[]"; 279 break; 280 } 281 sink.put('['); 282 elems.front.toStringImpl(sink); 283 elems.popFront; 284 foreach(e; elems) 285 { 286 sink.put(','); 287 e.toStringImpl(sink); 288 } 289 sink.put(']'); 290 break; 291 case Kind.object: 292 auto pairs = Asdf(cast(ubyte[])data).byKeyValue; 293 if(pairs.empty) 294 { 295 sink.put!"{}"; 296 break; 297 } 298 sink.put!"{\""; 299 sink.put(pairs.front.key); 300 sink.put!"\":"; 301 pairs.front.value.toStringImpl(sink); 302 pairs.popFront; 303 foreach(e; pairs) 304 { 305 sink.put!",\""; 306 sink.put(e.key); 307 sink.put!"\":"; 308 e.value.toStringImpl(sink); 309 } 310 sink.put('}'); 311 break; 312 default: 313 enforceValidAsdf(0, t); 314 } 315 } 316 317 /// 318 unittest 319 { 320 import mir.conv: to; 321 import asdf.jsonparser; 322 auto text = `{"foo":"bar","inner":{"a":true,"b":false,"c":"32323","d":null,"e":{}}}`; 323 const asdfData = text.parseJson; 324 assert(asdfData.to!string == text); 325 } 326 327 /++ 328 `==` operator overloads for `null` 329 +/ 330 bool opEquals(in Asdf rhs) const @safe pure nothrow @nogc 331 { 332 return data == rhs.data; 333 } 334 335 /// 336 unittest 337 { 338 import asdf.jsonparser; 339 auto asdfData = `null`.parseJson; 340 assert(asdfData == asdfData); 341 } 342 343 /++ 344 `==` operator overloads for `null` 345 +/ 346 bool opEquals(typeof(null)) const pure @safe nothrow 347 { 348 return data.length == 1 && data[0] == 0; 349 } 350 351 /// 352 unittest 353 { 354 import asdf.jsonparser; 355 auto asdfData = `null`.parseJson; 356 assert(asdfData == null); 357 } 358 359 /++ 360 `==` operator overloads for `bool` 361 +/ 362 bool opEquals(bool boolean) const pure @safe nothrow 363 { 364 return data.length == 1 && (data[0] == Kind.true_ && boolean || data[0] == Kind.false_ && !boolean); 365 } 366 367 /// 368 unittest 369 { 370 import asdf.jsonparser; 371 auto asdfData = `true`.parseJson; 372 assert(asdfData == true); 373 assert(asdfData != false); 374 } 375 376 /++ 377 `==` operator overloads for `string` 378 +/ 379 bool opEquals(in char[] str) const pure @trusted nothrow 380 { 381 return data.length >= 5 && data[0] == Kind..string && data[5 .. 5 + length4] == cast(const(ubyte)[]) str; 382 } 383 384 /// 385 unittest 386 { 387 import asdf.jsonparser; 388 auto asdfData = `"str"`.parseJson; 389 assert(asdfData == "str"); 390 assert(asdfData != "stR"); 391 } 392 393 /++ 394 Returns: 395 input range composed of elements of an array. 396 +/ 397 auto byElement() pure 398 { 399 static struct Range 400 { 401 private ubyte[] _data; 402 private Asdf _front; 403 404 auto save()() pure @property 405 { 406 return this; 407 } 408 409 void popFront() pure 410 { 411 while(!_data.empty) 412 { 413 uint t = cast(ubyte) _data.front; 414 switch(t) 415 { 416 case Kind.null_: 417 case Kind.true_: 418 case Kind.false_: 419 _front = Asdf(_data[0 .. 1]); 420 _data.popFront; 421 return; 422 case Kind.number: 423 enforceValidAsdf(_data.length >= 2, t); 424 size_t len = _data[1] + 2; 425 enforceValidAsdf(_data.length >= len, t); 426 _front = Asdf(_data[0 .. len]); 427 _data = _data[len .. $]; 428 return; 429 case Kind..string: 430 case Kind.array: 431 case Kind.object: 432 enforceValidAsdf(_data.length >= 5, t); 433 size_t len = Asdf(_data).length4 + 5; 434 enforceValidAsdf(_data.length >= len, t); 435 _front = Asdf(_data[0 .. len]); 436 _data = _data[len .. $]; 437 return; 438 case 0x80 | Kind.null_: 439 case 0x80 | Kind.true_: 440 case 0x80 | Kind.false_: 441 _data.popFront; 442 continue; 443 case 0x80 | Kind.number: 444 enforceValidAsdf(_data.length >= 2, t); 445 _data.popFrontExactly(_data[1] + 2); 446 continue; 447 case 0x80 | Kind..string: 448 case 0x80 | Kind.array: 449 case 0x80 | Kind.object: 450 enforceValidAsdf(_data.length >= 5, t); 451 size_t len = Asdf(_data).length4 + 5; 452 _data.popFrontExactly(len); 453 continue; 454 default: 455 enforceValidAsdf(0, t); 456 } 457 } 458 _front = Asdf.init; 459 } 460 461 auto front() pure @property 462 { 463 assert(!empty); 464 return _front; 465 } 466 467 bool empty() pure @property 468 { 469 return _front.data.length == 0; 470 } 471 } 472 if(data.empty || data[0] != Kind.array) 473 return Range.init; 474 enforceValidAsdf(data.length >= 5, Kind.array); 475 enforceValidAsdf(length4 == data.length - 5, Kind.array); 476 auto ret = Range(data[5 .. $]); 477 if(ret._data.length) 478 ret.popFront; 479 return ret; 480 } 481 482 /++ 483 Returns: 484 Input range composed of key-value pairs of an object. 485 Elements are type of `Tuple!(const(char)[], "key", Asdf, "value")`. 486 +/ 487 auto byKeyValue() pure 488 { 489 static struct Range 490 { 491 private ubyte[] _data; 492 private Tuple!(const(char)[], "key", Asdf, "value") _front; 493 494 auto save() pure @property 495 { 496 return this; 497 } 498 499 void popFront() pure 500 { 501 while(!_data.empty) 502 { 503 enforceValidAsdf(_data.length > 1, Kind.object); 504 size_t l = cast(ubyte) _data[0]; 505 _data.popFront; 506 enforceValidAsdf(_data.length >= l, Kind.object); 507 _front.key = cast(const(char)[])_data[0 .. l]; 508 _data.popFrontExactly(l); 509 uint t = cast(ubyte) _data.front; 510 switch(t) 511 { 512 case Kind.null_: 513 case Kind.true_: 514 case Kind.false_: 515 _front.value = Asdf(_data[0 .. 1]); 516 _data.popFront; 517 return; 518 case Kind.number: 519 enforceValidAsdf(_data.length >= 2, t); 520 size_t len = _data[1] + 2; 521 enforceValidAsdf(_data.length >= len, t); 522 _front.value = Asdf(_data[0 .. len]); 523 _data = _data[len .. $]; 524 return; 525 case Kind..string: 526 case Kind.array: 527 case Kind.object: 528 enforceValidAsdf(_data.length >= 5, t); 529 size_t len = Asdf(_data).length4 + 5; 530 enforceValidAsdf(_data.length >= len, t); 531 _front.value = Asdf(_data[0 .. len]); 532 _data = _data[len .. $]; 533 return; 534 case 0x80 | Kind.null_: 535 case 0x80 | Kind.true_: 536 case 0x80 | Kind.false_: 537 _data.popFront; 538 continue; 539 case 0x80 | Kind.number: 540 enforceValidAsdf(_data.length >= 2, t); 541 _data.popFrontExactly(_data[1] + 2); 542 continue; 543 case 0x80 | Kind..string: 544 case 0x80 | Kind.array: 545 case 0x80 | Kind.object: 546 enforceValidAsdf(_data.length >= 5, t); 547 size_t len = Asdf(_data).length4 + 5; 548 _data.popFrontExactly(len); 549 continue; 550 default: 551 enforceValidAsdf(0, t); 552 } 553 } 554 _front = _front.init; 555 } 556 557 auto front() pure @property 558 { 559 assert(!empty); 560 return _front; 561 } 562 563 bool empty() pure @property 564 { 565 return _front.value.data.length == 0; 566 } 567 } 568 if(data.empty || data[0] != Kind.object) 569 return Range.init; 570 enforceValidAsdf(data.length >= 5, Kind.object); 571 enforceValidAsdf(length4 == data.length - 5, Kind.object); 572 auto ret = Range(data[5 .. $]); 573 if(ret._data.length) 574 ret.popFront; 575 return ret; 576 } 577 578 /// returns 4-byte length 579 private size_t length4() const @property pure nothrow @nogc @trusted 580 { 581 assert(data.length >= 5); 582 version(X86_Any) 583 { 584 return (cast(uint*)(data.ptr + 1))[0]; 585 } 586 else 587 { 588 align(4) auto ret = *cast(ubyte[4]*)(data.ptr + 1); 589 return (cast(uint[1])ret)[0]; 590 } 591 } 592 593 /// ditto 594 private void length4(size_t len) const @property pure nothrow @nogc @trusted 595 { 596 assert(data.length >= 5); 597 assert(len <= uint.max); 598 version(X86_Any) 599 { 600 *(cast(uint*)(data.ptr + 1)) = cast(uint) len; 601 } 602 else 603 { 604 *(cast(ubyte[4]*)(data.ptr + 1)) = cast(ubyte[4]) cast(uint[1]) [cast(uint) len]; 605 } 606 } 607 608 /++ 609 Searches for a value recursively in an ASDF object. 610 611 Params: 612 keys = list of keys keys 613 Returns 614 ASDF value if it was found (first win) or ASDF with empty plain data. 615 +/ 616 Asdf opIndex(in char[][] keys...) pure 617 { 618 auto asdf = this; 619 if(asdf.data.empty) 620 return Asdf.init; 621 L: foreach(key; keys) 622 { 623 if(asdf.data[0] != Asdf.Kind.object) 624 return Asdf.init; 625 foreach(e; asdf.byKeyValue) 626 { 627 if(e.key == key) 628 { 629 asdf = e.value; 630 continue L; 631 } 632 } 633 return Asdf.init; 634 } 635 return asdf; 636 } 637 638 /// 639 unittest 640 { 641 import asdf.jsonparser; 642 auto asdfData = `{"foo":"bar","inner":{"a":true,"b":false,"c":"32323","d":null,"e":{}}}`.parseJson; 643 assert(asdfData["inner", "a"] == true); 644 assert(asdfData["inner", "b"] == false); 645 assert(asdfData["inner", "c"] == "32323"); 646 assert(asdfData["inner", "d"] == null); 647 assert(asdfData["no", "such", "keys"] == Asdf.init); 648 } 649 650 /++ 651 Params: 652 def = default value. It is used when ASDF value equals `Asdf.init`. 653 Returns: 654 `cast(T) this` if `this != Asdf.init` and `def` otherwise. 655 +/ 656 T get(T)(T def) 657 { 658 if(data.length) 659 { 660 return cast(T) this; 661 } 662 return def; 663 } 664 665 /// 666 unittest 667 { 668 import asdf.jsonparser; 669 auto asdfData = `{"foo":"bar","inner":{"a":true,"b":false,"c":"32323","d":null,"e":{}}}`.parseJson; 670 assert(asdfData["inner", "a"].get(false) == true); 671 assert(asdfData["inner", "b"].get(true) == false); 672 assert(asdfData["inner", "c"].get(100) == 32323); 673 assert(asdfData["no", "such", "keys"].get(100) == 100); 674 } 675 676 /++ 677 `cast` operator overloading. 678 +/ 679 T opCast(T)() 680 { 681 import std.datetime: SysTime, DateTime, usecs, UTC; 682 import std.traits: isNumeric; 683 import mir.conv: to; 684 import std.conv: ConvException; 685 import std.format: format; 686 import std.math: trunc; 687 import asdf.serialization; 688 auto k = kind; 689 with(Kind) switch(kind) 690 { 691 case null_ : 692 static if (isNumeric!T 693 || is(T == interface) 694 || is(T == class) 695 || is(T == E[], E) 696 || is(T == E[K], E, K) 697 || is(T == bool)) 698 return T.init; 699 else goto default; 700 case true_ : 701 static if(__traits(compiles, true.to!T)) 702 return true.to!T; 703 else goto default; 704 case false_: 705 static if(__traits(compiles, false.to!T)) 706 return false.to!T; 707 else goto default; 708 case number: 709 { 710 auto str = cast(const(char)[]) data[2 .. $]; 711 static if(is(T == bool)) 712 return assumePure(() => str.to!double)() != 0; 713 else 714 static if(is(T == SysTime) || is(T == DateTime)) 715 { 716 auto unixTime = assumePure(() => str.to!real)(); 717 auto secsR = assumePure(() => unixTime.trunc)(); 718 auto rem = unixTime - secsR; 719 auto st = SysTime.fromUnixTime(cast(long)(secsR), UTC()); 720 assumePure((ref SysTime st) => st.fracSecs = usecs(cast(long)(rem * 1_000_000)))(st); 721 return assumePure(() => st.to!T)(); 722 } 723 else 724 static if(__traits(compiles, assumePure(() => str.to!T)())) 725 { 726 static if (isFloatingPoint!T) 727 { 728 import mir.bignum.internal.dec2float: decimalToFloatImpl; 729 import mir.bignum.internal.parse: parseJsonNumberImpl; 730 auto result = str.parseJsonNumberImpl; 731 if (!result.success) 732 throw new Exception("Failed to deserialize number"); 733 734 auto fp = decimalToFloatImpl!(Unqual!T)(result.coefficient, result.exponent); 735 if (result.sign) 736 fp = -fp; 737 return fp; 738 } 739 else 740 { 741 return assumePure(() => str.to!T)(); 742 } 743 } 744 else goto default; 745 } 746 case string: 747 { 748 auto str = cast(const(char)[]) data[5 .. $]; 749 static if(is(T == bool)) 750 return str != "0" && str != "false" && str != ""; 751 else 752 static if(__traits(compiles, str.to!T)) 753 return str.to!T; 754 else goto default; 755 } 756 static if (isAggregateType!T || isArray!T) 757 { 758 case array : 759 case object: 760 static if(__traits(compiles, {T t = deserialize!T(this);})) 761 return deserialize!T(this); 762 else goto default; 763 } 764 default: 765 throw new ConvException(format("Cannot convert kind %s(\\x%02X) to %s", cast(Kind) k, k, T.stringof)); 766 } 767 } 768 769 /// null 770 unittest 771 { 772 import std.math; 773 import asdf.serialization; 774 auto null_ = serializeToAsdf(null); 775 interface I {} 776 class C {} 777 assert(cast(uint[]) null_ is null); 778 assert(cast(uint[uint]) null_ is null); 779 assert(cast(I) null_ is null); 780 assert(cast(C) null_ is null); 781 assert(isNaN(cast(double) null_)); 782 assert(! cast(bool) null_); 783 } 784 785 /// boolean 786 unittest 787 { 788 import std.math; 789 import asdf.serialization; 790 auto true_ = serializeToAsdf(true); 791 auto false_ = serializeToAsdf(false); 792 static struct C { 793 this(bool){} 794 } 795 auto a = cast(C) true_; 796 auto b = cast(C) false_; 797 assert(cast(bool) true_ == true); 798 assert(cast(bool) false_ == false); 799 assert(cast(uint) true_ == 1); 800 assert(cast(uint) false_ == 0); 801 assert(cast(double) true_ == 1); 802 assert(cast(double) false_ == 0); 803 } 804 805 /// numbers 806 unittest 807 { 808 import std.bigint; 809 import asdf.serialization; 810 auto number = serializeToAsdf(1234); 811 auto zero = serializeToAsdf(0); 812 static struct C 813 { 814 this(in char[] numberString) 815 { 816 assert(numberString == "1234"); 817 } 818 } 819 auto a = cast(C) number; 820 assert(cast(bool) number == true); 821 assert(cast(bool) zero == false); 822 assert(cast(uint) number == 1234); 823 assert(cast(double) number == 1234); 824 assert(cast(BigInt) number == 1234); 825 assert(cast(uint) zero == 0); 826 assert(cast(double) zero == 0); 827 assert(cast(BigInt) zero == 0); 828 } 829 830 /// string 831 unittest 832 { 833 import std.bigint; 834 import asdf.serialization; 835 auto number = serializeToAsdf("1234"); 836 auto false_ = serializeToAsdf("false"); 837 auto bar = serializeToAsdf("bar"); 838 auto zero = serializeToAsdf("0"); 839 static struct C 840 { 841 this(in char[] str) 842 { 843 assert(str == "1234"); 844 } 845 } 846 auto a = cast(C) number; 847 assert(cast(string) number == "1234"); 848 assert(cast(bool) number == true); 849 assert(cast(bool) bar == true); 850 assert(cast(bool) zero == false); 851 assert(cast(bool) false_ == false); 852 assert(cast(uint) number == 1234); 853 assert(cast(double) number == 1234); 854 assert(cast(BigInt) number == 1234); 855 assert(cast(uint) zero == 0); 856 assert(cast(double) zero == 0); 857 assert(cast(BigInt) zero == 0); 858 } 859 860 /++ 861 For ASDF arrays and objects `cast(T)` just returns `this.deserialize!T`. 862 +/ 863 unittest 864 { 865 import std.bigint; 866 import asdf.serialization; 867 assert(cast(int[]) serializeToAsdf([100, 20]) == [100, 20]); 868 } 869 870 /// UNIX Time 871 unittest 872 { 873 import std.datetime; 874 import asdf.serialization; 875 876 auto num = serializeToAsdf(0.123456789); // rounding up to usecs 877 assert(cast(DateTime) num == DateTime(1970, 1, 1)); 878 assert(cast(SysTime) num == SysTime(DateTime(1970, 1, 1), usecs(123456), UTC())); // UTC time zone is used. 879 } 880 }