1 /++ 2 Timestamp 3 +/ 4 module mir.timestamp; 5 6 private alias isDigit = (dchar c) => uint(c - '0') < 10; 7 import mir.serde: serdeIgnore, serdeRegister; 8 9 version(D_Exceptions) 10 /// 11 class DateTimeException : Exception 12 { 13 /// 14 @nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable nextInChain = null) 15 { 16 super(msg, file, line, nextInChain); 17 } 18 19 /// ditto 20 @nogc @safe pure nothrow this(string msg, Throwable nextInChain, string file = __FILE__, size_t line = __LINE__) 21 { 22 super(msg, file, line, nextInChain); 23 } 24 } 25 26 version(D_Exceptions) 27 { 28 private static immutable InvalidMonth = new DateTimeException("Timestamp: Invalid Month"); 29 private static immutable InvalidDay = new DateTimeException("Timestamp: Invalid Day"); 30 private static immutable InvalidISOString = new DateTimeException("Timestamp: Invalid ISO String"); 31 private static immutable InvalidISOExtendedString = new DateTimeException("Timestamp: Invalid ISO Extended String"); 32 private static immutable InvalidYamlString = new DateTimeException("Timestamp: Invalid YAML String"); 33 private static immutable InvalidString = new DateTimeException("Timestamp: Invalid String"); 34 private static immutable ExpectedDuration = new DateTimeException("Timestamp: Expected Duration"); 35 } 36 37 /++ 38 Timestamp 39 40 Note: The component values in the binary encoding are always in UTC or local time with unknown offset, 41 while components in the text encoding are in a some timezone with known offset. 42 This means that transcoding requires a conversion between UTC and a timezone. 43 44 `Timestamp` precision is up to picosecond (second/10^12). 45 +/ 46 @serdeRegister 47 struct Timestamp 48 { 49 import std.traits: isSomeChar; 50 51 /// 52 enum Precision : ubyte 53 { 54 /// 55 year, 56 /// 57 month, 58 /// 59 day, 60 /// 61 minute, 62 /// 63 second, 64 /// 65 fraction, 66 } 67 68 @serdeIgnore: 69 70 /// 71 this(scope const(char)[] str) @safe pure @nogc 72 { 73 this = fromString(str); 74 } 75 76 /// 77 version (mir_test) 78 @safe pure @nogc unittest 79 { 80 assert(Timestamp("2010-07-04") == Timestamp(2010, 7, 4)); 81 assert(Timestamp("20100704") == Timestamp(2010, 7, 4)); 82 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOString("20210129T201244+0730")); 83 static assert(Timestamp(2021, 01, 29, 4, 42, 44).withOffset(- (7 * 60 + 30)) == Timestamp.fromISOExtString("2021-01-28T21:12:44-07:30")); 84 85 assert(Timestamp("T0740") == Timestamp.onlyTime(7, 40)); 86 assert(Timestamp("T074030Z") == Timestamp.onlyTime(7, 40, 30).withOffset(0)); 87 assert(Timestamp("T074030.056") == Timestamp.onlyTime(7, 40, 30, -3, 56)); 88 89 assert(Timestamp("07:40") == Timestamp.onlyTime(7, 40)); 90 assert(Timestamp("07:40:30") == Timestamp.onlyTime(7, 40, 30)); 91 assert(Timestamp("T07:40:30.056Z") == Timestamp.onlyTime(7, 40, 30, -3, 56).withOffset(0)); 92 } 93 94 private short _offset = short.min; 95 96 /++ 97 If the time in UTC is known, but the offset to local time is unknown, this can be represented with an offset of “-00:00”. 98 This differs semantically from an offset of “Z” or “+00:00”, which imply that UTC is the preferred reference point for the specified time. 99 RFC2822 describes a similar convention for email. 100 +/ 101 /++ 102 Timezone offset in minutes 103 +/ 104 short offset()() const @safe pure nothrow @nogc @property 105 { 106 return isLocalTime ? 0 : _offset; 107 } 108 109 /// 110 void offset()(ushort offset) @safe pure nothrow @nogc @property 111 { 112 _offset = offset; 113 } 114 115 /++ 116 Returns: true if it is a local time 117 +/ 118 bool isLocalTime()() const @safe pure nothrow @nogc @property 119 { 120 return _offset == _offset.min; 121 } 122 123 /++ 124 +/ 125 void setLocalTimezone()() @safe pure nothrow @nogc @property 126 { 127 _offset = _offset.min; 128 } 129 130 131 /++ 132 Year 133 +/ 134 short year; 135 /++ 136 +/ 137 Precision precision; 138 139 /++ 140 Month 141 142 If the value equals to thero then this and all the following members are undefined. 143 +/ 144 ubyte month; 145 /++ 146 Day 147 148 If the value equals to thero then this and all the following members are undefined. 149 +/ 150 ubyte day; 151 /++ 152 Hour 153 +/ 154 ubyte hour; 155 156 version(D_Ddoc) 157 { 158 159 /++ 160 Minute 161 162 Note: the field is implemented as property. 163 +/ 164 ubyte minute; 165 /++ 166 Second 167 168 Note: the field is implemented as property. 169 +/ 170 ubyte second; 171 /++ 172 Fraction 173 174 The `fractionExponent` and `fractionCoefficient` denote the fractional seconds of the timestamp as a decimal value 175 The fractional seconds’ value is `coefficient * 10 ^ exponent`. 176 It must be greater than or equal to zero and less than 1. 177 A missing coefficient defaults to zero. 178 Fractions whose coefficient is zero and exponent is greater than -1 are ignored. 179 180 'fractionCoefficient' allowed values are [0 ... 10^12-1]. 181 'fractionExponent' allowed values are [-12 ... 0]. 182 183 Note: the fields are implemented as property. 184 +/ 185 byte fractionExponent; 186 /// ditto 187 ulong fractionCoefficient; 188 } 189 else 190 { 191 import mir.bitmanip: bitfields; 192 version (LittleEndian) 193 { 194 195 mixin(bitfields!( 196 ubyte, "minute", 8, 197 ubyte, "second", 8, 198 byte, "fractionExponent", 8, 199 ulong, "fractionCoefficient", 40, 200 )); 201 } 202 else 203 { 204 mixin(bitfields!( 205 ulong, "fractionCoefficient", 40, 206 byte, "fractionExponent", 8, 207 ubyte, "second", 8, 208 ubyte, "minute", 8, 209 )); 210 } 211 } 212 213 /// 214 @safe pure nothrow @nogc 215 this(short year) 216 { 217 this.year = year; 218 this.precision = Precision.year; 219 } 220 221 /// 222 @safe pure nothrow @nogc 223 this(short year, ubyte month) 224 { 225 this.year = year; 226 this.month = month; 227 this.precision = Precision.month; 228 } 229 230 /// 231 @safe pure nothrow @nogc 232 this(short year, ubyte month, ubyte day) 233 { 234 this.year = year; 235 this.month = month; 236 this.day = day; 237 this.precision = Precision.day; 238 } 239 240 /// 241 @safe pure nothrow @nogc 242 this(short year, ubyte month, ubyte day, ubyte hour, ubyte minute) 243 { 244 this.year = year; 245 this.month = month; 246 this.day = day; 247 this.hour = hour; 248 this.minute = minute; 249 this.precision = Precision.minute; 250 } 251 252 /// 253 @safe pure nothrow @nogc 254 this(short year, ubyte month, ubyte day, ubyte hour, ubyte minute, ubyte second) 255 { 256 this.year = year; 257 this.month = month; 258 this.day = day; 259 this.hour = hour; 260 this.day = day; 261 this.minute = minute; 262 this.second = second; 263 this.precision = Precision.second; 264 } 265 266 /// 267 @safe pure nothrow @nogc 268 this(short year, ubyte month, ubyte day, ubyte hour, ubyte minute, ubyte second, byte fractionExponent, ulong fractionCoefficient) 269 { 270 this.year = year; 271 this.month = month; 272 this.day = day; 273 this.hour = hour; 274 this.day = day; 275 this.minute = minute; 276 this.second = second; 277 assert(fractionExponent < 0); 278 this.fractionExponent = fractionExponent; 279 this.fractionCoefficient = fractionCoefficient; 280 this.precision = Precision.fraction; 281 } 282 283 /// 284 @safe pure nothrow @nogc 285 static Timestamp onlyTime(ubyte hour, ubyte minute) 286 { 287 return Timestamp(0, 0, 0, hour, minute); 288 } 289 290 /// 291 @safe pure nothrow @nogc 292 static Timestamp onlyTime(ubyte hour, ubyte minute, ubyte second) 293 { 294 return Timestamp(0, 0, 0, hour, minute, second); 295 } 296 297 /// 298 @safe pure nothrow @nogc 299 static Timestamp onlyTime(ubyte hour, ubyte minute, ubyte second, byte fractionExponent, ulong fractionCoefficient) 300 { 301 return Timestamp(0, 0, 0, hour, minute, second, fractionExponent, fractionCoefficient); 302 } 303 304 /// 305 this(Date)(const Date datetime) 306 if (Date.stringof == "Date" || Date.stringof == "date") 307 { 308 static if (__traits(hasMember, Date, "yearMonthDay")) 309 with(datetime.yearMonthDay) this(year, cast(ubyte)month, day); 310 else 311 with(datetime) this(year, month, day); 312 } 313 314 /// 315 version (mir_test) 316 @safe unittest { 317 import mir.date : Date; 318 auto dt = Date(1982, 4, 1); 319 Timestamp ts = dt; 320 assert(ts.opCmp(ts) == 0); 321 assert(dt.toISOExtString == ts.toString); 322 assert(dt == cast(Date) ts); 323 } 324 325 /// 326 version (mir_test) 327 @safe unittest { 328 import std.datetime.date : Date; 329 auto dt = Date(1982, 4, 1); 330 Timestamp ts = dt; 331 assert(dt.toISOExtString == ts.toString); 332 assert(dt == cast(Date) ts); 333 } 334 335 /// 336 this(TimeOfDay)(const TimeOfDay timeOfDay) 337 if (TimeOfDay.stringof == "TimeOfDay") 338 { 339 with(timeOfDay) this = onlyTime(hour, minute, second); 340 } 341 342 /// 343 version (mir_test) 344 @safe unittest { 345 import mir.test: should; 346 import std.datetime.date : TimeOfDay; 347 auto dt = TimeOfDay(7, 14, 30); 348 Timestamp ts = dt; 349 (dt.toISOExtString ~ "-00:00").should == ts.toString; 350 assert(dt == cast(TimeOfDay) ts); 351 } 352 353 /// 354 this(DateTime)(const DateTime datetime) 355 if (DateTime.stringof == "DateTime") 356 { 357 with(datetime) this(year, cast(ubyte)month, day, hour, minute, second); 358 } 359 360 /// 361 version (mir_test) 362 @safe unittest { 363 import std.datetime.date : DateTime; 364 auto dt = DateTime(1982, 4, 1, 20, 59, 22); 365 Timestamp ts = dt; 366 assert(dt.toISOExtString ~ "-00:00" == ts.toString); 367 assert(dt == cast(DateTime) ts); 368 } 369 370 /// 371 this(SysTime)(const SysTime systime) pure 372 if (SysTime.stringof == "SysTime") 373 { 374 import std.datetime.timezone : LocalTime; 375 auto offset = assumePureSafe(()=>systime.utcOffset)(); 376 auto isLocal = systime.timezone is LocalTime(); 377 auto thisTimes = isLocal ? systime + offset : systime.toUTC; 378 this = fromUnixTime(thisTimes.toUnixTime); 379 this.fractionExponent = -7; 380 this.fractionCoefficient = assumePureSafe(()=>thisTimes.fracSecs)().total!"hnsecs"; 381 this.precision = Precision.fraction; 382 if (!isLocal) 383 this.offset = cast(short) offset.total!"minutes"; 384 } 385 386 /// 387 version (mir_test) 388 @safe unittest { 389 import core.time : hnsecs, minutes; 390 import std.datetime.date : DateTime; 391 import std.datetime.timezone : SimpleTimeZone; 392 import std.datetime.systime : SysTime; 393 394 auto dt = DateTime(1982, 4, 1, 20, 59, 22); 395 auto tz = new immutable SimpleTimeZone(-330.minutes); 396 auto st = SysTime(dt, 1234567.hnsecs, tz); 397 Timestamp ts = st; 398 399 assert(st.toISOExtString == ts.toString); 400 assert(st == cast(SysTime) ts); 401 } 402 403 /++ 404 Creates a fake timestamp from a Duration using `total!"hnsecs"` method. 405 For positive and zero timestamps the format is 406 `wwww-dd-88Thh:mm:ss.nnnnnnn` 407 and for negative timestamps 408 `wwww-dd-99Thh:mm:ss.nnnnnnn`. 409 +/ 410 this(Duration)(const Duration duration) 411 if (Duration.stringof == "Duration") 412 { 413 auto hnsecs = duration.total!"hnsecs"; 414 ulong abs = hnsecs < 0 ? -hnsecs : hnsecs; 415 precision = Precision.fraction; 416 day = hnsecs >= 0 ? 88 : 99; 417 fractionExponent = -7; 418 fractionCoefficient = abs % 10_000_000U; 419 abs /= 10_000_000U; 420 second = abs % 60; 421 abs /= 60; 422 minute = abs % 60; 423 abs /= 60; 424 hour = abs % 24; 425 abs /= 24; 426 month = abs % 7; 427 abs /= 7; 428 year = cast(typeof(year)) abs; 429 } 430 431 /// 432 version (mir_test) 433 @safe unittest { 434 import core.time : Duration, weeks, days, hours, minutes, seconds, hnsecs; 435 436 auto duration = 5.weeks + 2.days + 7.hours + 40.minutes + 4.seconds + 9876543.hnsecs; 437 Timestamp ts = duration; 438 439 assert(ts.toISOExtString == `0005-02-88T07:40:04.9876543-00:00`); 440 assert(duration == cast(Duration) ts); 441 442 duration = -duration; 443 ts = Timestamp(duration); 444 assert(ts.toISOExtString == `0005-02-99T07:40:04.9876543-00:00`); 445 assert(duration == cast(Duration) ts); 446 447 assert(Timestamp(Duration.zero).toISOExtString == `0000-00-88T00:00:00.0000000-00:00`); 448 } 449 450 /++ 451 Decomposes Timestamp to an algebraic type. 452 Supported types up to T.stringof equivalence: 453 454 $(UL 455 $(LI `Year`) 456 $(LI `YearMonth`) 457 $(LI `YearMonthDay`) 458 $(LI `Date`) 459 $(LI `date`) 460 $(LI `TimeOfDay`) 461 $(LI `DateTime`) 462 $(LI `SysTime`) 463 $(LI `Timestamp` as fallback type) 464 ) 465 466 467 Throws: an exception if timestamp cannot be converted to an algebraic type and there is no `Timestamp` type in the Algebraic set. 468 +/ 469 T opCast(T)() const 470 if (__traits(hasMember, T, "AllowedTypes")) 471 { 472 foreach (AT; T.AllowedTypes) 473 static if (AT.stringof == "Year") 474 if (precision == precision.year) 475 return T(opCast!AT); 476 477 foreach (AT; T.AllowedTypes) 478 static if (AT.stringof == "YearMonth") 479 if (precision == precision.month) 480 return T(opCast!AT); 481 482 foreach (AT; T.AllowedTypes) 483 static if (AT.stringof == "Duration") 484 if (isDuration) 485 return T(opCast!AT); 486 487 foreach (AT; T.AllowedTypes) 488 static if (AT.stringof == "YearMonthDay" || AT.stringof == "Date" || AT.stringof == "date") 489 if (precision == precision.day) 490 return T(opCast!AT); 491 492 foreach (AT; T.AllowedTypes) 493 static if (AT.stringof == "TimeOfDay") 494 if (isOnlyTime) 495 return T(opCast!AT); 496 497 if (!isOnlyTime && precision >= precision.day) 498 { 499 foreach (AT; T.AllowedTypes) 500 static if (AT.stringof == "DateTime") 501 if (offset == 0 && precision <= precision.second) 502 return T(opCast!AT); 503 504 foreach (AT; T.AllowedTypes) 505 static if (AT.stringof == "SysTime") 506 return T(opCast!AT); 507 } 508 509 import std.meta: staticIndexOf; 510 static if (staticIndexOf!(Timestamp, T.AllowedTypes) < 0) 511 { 512 static immutable exc = new Exception("Cannot cast Timestamp to " ~ T.stringof); 513 { import mir.exception : toMutable; throw exc.toMutable; } 514 } 515 else 516 { 517 return T(this); 518 } 519 } 520 521 /// 522 version (mir_test) 523 @safe unittest 524 { 525 import core.time : hnsecs, minutes, Duration; 526 import mir.algebraic; 527 import mir.date: Date; // Can be other Date type as well 528 import std.datetime.date : TimeOfDay, DateTime; 529 import std.datetime.systime : SysTime; 530 import std.datetime.timezone: UTC, SimpleTimeZone; 531 532 alias A = Variant!(Date, TimeOfDay, DateTime, Duration, SysTime, Timestamp, string); // non-date-time types is OK 533 assert(cast(A) Timestamp(1023) == Timestamp(1023)); // Year isn't represented in the algebraic, use fallback type 534 assert(cast(A) Timestamp.onlyTime(7, 40, 30) == TimeOfDay(7, 40, 30)); 535 assert(cast(A) Timestamp(1982, 4, 1, 20, 59, 22) == DateTime(1982, 4, 1, 20, 59, 22)); 536 537 auto dt = DateTime(1982, 4, 1, 20, 59, 22); 538 auto tz = new immutable SimpleTimeZone(-330.minutes); 539 auto st = SysTime(dt, 1234567.hnsecs, tz); 540 assert(cast(A) Timestamp(st) == st); 541 } 542 543 /++ 544 Casts timestamp to a date-time type. 545 546 Supported types up to T.stringof equivalence: 547 548 $(UL 549 $(LI `Year`) 550 $(LI `YearMonth`) 551 $(LI `YearMonthDay`) 552 $(LI `Date`) 553 $(LI `date`) 554 $(LI `TimeOfDay`) 555 $(LI `DateTime`) 556 $(LI `SysTime`) 557 ) 558 +/ 559 T opCast(T)() const 560 if ( 561 T.stringof == "Year" 562 || T.stringof == "YearMonth" 563 || T.stringof == "YearMonthDay" 564 || T.stringof == "Date" 565 || T.stringof == "date" 566 || T.stringof == "TimeOfDay" 567 || T.stringof == "Duration" 568 || T.stringof == "DateTime" 569 || T.stringof == "SysTime") 570 { 571 static if (T.stringof == "YearMonth") 572 { 573 return T(year, month); 574 } 575 else 576 static if (T.stringof == "Date" || T.stringof == "date" || T.stringof == "YearMonthDay") 577 { 578 return T(year, month, day); 579 } 580 else 581 static if (T.stringof == "DateTime") 582 { 583 return T(year, month, day, hour, minute, second); 584 } 585 else 586 static if (T.stringof == "TimeOfDay") 587 { 588 return T(hour, minute, second); 589 } 590 else 591 static if (T.stringof == "SysTime") 592 { 593 import core.time : hnsecs, minutes; 594 import std.datetime.date: DateTime; 595 import std.datetime.systime: SysTime; 596 import std.datetime.timezone: UTC, LocalTime, SimpleTimeZone; 597 auto ret = SysTime.fromUnixTime(toUnixTime, UTC()) + getFraction!7.hnsecs; 598 if (isLocalTime) 599 { 600 ret = ret.toLocalTime; 601 ret -= assumePureSafe(()=>ret.utcOffset)(); 602 } 603 else 604 if (offset) 605 { 606 ret.timezone = new immutable SimpleTimeZone(offset.minutes); 607 } 608 return ret; 609 } 610 else 611 static if (T.stringof == "Duration") 612 { 613 if (!isDuration) 614 { import mir.exception : toMutable; throw ExpectedDuration.toMutable; } 615 auto coeff = ((((long(year) * 7 + month) * 24 + hour) * 60 + minute) * 60 + second) * 10_000_000 + getFraction!7; 616 if (isNegativeDuration) 617 coeff = -coeff; 618 619 import mir.conv: to; 620 import core.time: hnsecs; 621 return coeff.hnsecs.to!T; 622 } 623 } 624 625 /++ 626 +/ 627 long getFraction(int digits)() @property const @safe pure nothrow @nogc 628 if (digits >= 1 && digits <= 12) 629 { 630 long coeff; 631 if (fractionCoefficient) 632 { 633 coeff = fractionCoefficient; 634 int exp = fractionExponent; 635 while (exp > -digits) 636 { 637 exp--; 638 coeff *= 10; 639 } 640 while (exp < -digits) 641 { 642 exp++; 643 coeff /= 10; 644 } 645 } 646 return coeff; 647 } 648 649 /++ 650 Returns: true if timestamp represent a time only value. 651 +/ 652 bool isOnlyTime() @property const @safe pure nothrow @nogc 653 { 654 return precision > Precision.day && day == 0; 655 } 656 657 /// 658 int opCmp(Timestamp rhs) const @safe pure nothrow @nogc 659 { 660 import std.meta: AliasSeq; 661 static foreach (member; [ 662 "year", 663 "month", 664 "day", 665 "hour", 666 "minute", 667 "second", 668 ]) 669 if (auto d = int(__traits(getMember, this, member)) - int(__traits(getMember, rhs, member))) 670 return d; 671 int frel = this.fractionExponent; 672 int frer = rhs.fractionExponent; 673 ulong frcl = this.fractionCoefficient; 674 ulong frcr = rhs.fractionCoefficient; 675 while(frel > frer) 676 { 677 frel--; 678 frcl *= 10; 679 } 680 while(frer > frel) 681 { 682 frer--; 683 frcr *= 10; 684 } 685 if (frcl < frcr) return -1; 686 if (frcl > frcr) return +1; 687 if (auto d = int(this.fractionExponent) - int(rhs.fractionExponent)) 688 return d; 689 return int(this.offset) - int(rhs.offset); 690 } 691 692 /++ 693 Attaches local offset, doesn't adjust other fields. 694 Local-time offsets may be represented as either `hour*60+minute` offsets from UTC, 695 or as the zero to denote a local time of UTC. They are required on timestamps with time and are not allowed on date values. 696 +/ 697 @safe pure nothrow @nogc const 698 Timestamp withOffset(short minutes) 699 { 700 assert(-24 * 60 <= minutes && minutes <= 24 * 60, "Offset absolute value should be less or equal to 24 * 60"); 701 assert(precision >= Precision.minute, "Offsets are not allowed on date values."); 702 Timestamp ret = this; 703 ret.offset = minutes; 704 return ret; 705 } 706 707 version(D_BetterC){} else 708 private string toStringImpl(alias fun)() const @safe pure nothrow 709 { 710 import mir.appender: UnsafeArrayBuffer; 711 char[64] buffer = void; 712 auto w = UnsafeArrayBuffer!char(buffer); 713 fun(w); 714 return w.data.idup; 715 } 716 717 /++ 718 Converts this $(LREF Timestamp) to a string with the format `yyyy-mm-ddThh:mm:ss[.mmm]±hh:mm`. 719 720 If `w` writer is set, the resulting string will be written directly 721 to it. 722 723 Returns: 724 A `string` when not using an output range; `void` otherwise. 725 +/ 726 alias toString = toISOExtString; 727 728 /// 729 version (mir_test) 730 @safe pure nothrow unittest 731 { 732 import mir.test; 733 Timestamp.init.toString.should == "0000T"; 734 Timestamp(2010, 7, 4).toString.should == "2010-07-04"; 735 Timestamp(1998, 12, 25).toString.should == "1998-12-25"; 736 Timestamp(0, 1, 5).toString.should == "0000-01-05"; 737 Timestamp(-4, 1, 5).toString.should == "-0004-01-05"; 738 739 // yyyy-mm-ddThh:mm:ss[.mmm]±hh:mm 740 Timestamp(2021).toString.should == "2021T"; 741 Timestamp(2021, 01).toString.should == "2021-01T"; 742 Timestamp(2021, 01, 29).toString.should == "2021-01-29"; 743 Timestamp(2021, 01, 29, 19, 42).withOffset(0).toString.should == "2021-01-29T19:42Z"; 744 Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60).toString.should == "2021-01-29T19:42:44+07"; 745 Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30).toString.should == "2021-01-29T20:12:44+07:30"; 746 747 Timestamp.onlyTime(7, 40).toString.should == "07:40-00:00"; 748 Timestamp.onlyTime(7, 40, 30).toString.should == "07:40:30-00:00"; 749 Timestamp.onlyTime(7, 40, 30, -3, 56).withOffset(0).toString.should == "07:40:30.056Z"; 750 } 751 752 /// 753 version (mir_test) 754 @safe unittest 755 { 756 // Test A.D. 757 assert(Timestamp(9, 12, 4).toISOExtString == "0009-12-04"); 758 assert(Timestamp(99, 12, 4).toISOExtString == "0099-12-04"); 759 assert(Timestamp(999, 12, 4).toISOExtString == "0999-12-04"); 760 assert(Timestamp(9999, 7, 4).toISOExtString == "9999-07-04"); 761 assert(Timestamp(10000, 10, 20).toISOExtString == "+10000-10-20"); 762 763 // Test B.C. 764 assert(Timestamp(0, 12, 4).toISOExtString == "0000-12-04"); 765 assert(Timestamp(-9, 12, 4).toISOExtString == "-0009-12-04"); 766 assert(Timestamp(-99, 12, 4).toISOExtString == "-0099-12-04"); 767 assert(Timestamp(-999, 12, 4).toISOExtString == "-0999-12-04"); 768 assert(Timestamp(-9999, 7, 4).toISOExtString == "-9999-07-04"); 769 assert(Timestamp(-10000, 10, 20).toISOExtString == "-10000-10-20"); 770 771 assert(Timestamp.onlyTime(7, 40).toISOExtString == "07:40-00:00"); 772 assert(Timestamp.onlyTime(7, 40, 30).toISOExtString == "07:40:30-00:00"); 773 assert(Timestamp.onlyTime(7, 40, 30, -3, 56).toISOExtString == "07:40:30.056-00:00"); 774 775 const cdate = Timestamp(1999, 7, 6); 776 immutable idate = Timestamp(1999, 7, 6); 777 assert(cdate.toISOExtString == "1999-07-06"); 778 assert(idate.toISOExtString == "1999-07-06"); 779 } 780 781 /// ditto 782 alias toISOExtString = toISOStringImp!true; 783 784 /++ 785 Converts this $(LREF Timestamp) to a string with the format `YYYYMMDDThhmmss±hhmm`. 786 787 If `w` writer is set, the resulting string will be written directly 788 to it. 789 790 Returns: 791 A `string` when not using an output range; `void` otherwise. 792 +/ 793 alias toISOString = toISOStringImp!false; 794 795 /// 796 version (mir_test) 797 @safe pure nothrow unittest 798 { 799 import mir.test; 800 Timestamp.init.toISOString.should == "0000T"; 801 Timestamp(2010, 7, 4).toISOString.should == "20100704"; 802 Timestamp(1998, 12, 25).toISOString.should == "19981225"; 803 Timestamp(0, 1, 5).toISOString.should == "00000105"; 804 Timestamp(-4, 1, 5).toISOString.should == "-00040105"; 805 806 // YYYYMMDDThhmmss±hhmm 807 Timestamp(2021).toISOString.should == "2021T"; 808 Timestamp(2021, 01).toISOString.should == "2021-01T"; // always extended 809 Timestamp(2021, 01, 29).toISOString.should == "20210129"; 810 Timestamp(2021, 01, 29, 19, 42).toISOString.should == "20210129T1942"; 811 Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60).toISOString.should == "20210129T194244+07"; 812 Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30).toISOString.should == "20210129T201244+0730"; 813 static assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30).toISOString == "20210129T201244+0730"); 814 815 assert(Timestamp.onlyTime(7, 40).toISOString == "T0740"); 816 assert(Timestamp.onlyTime(7, 40, 30).toISOString == "T074030"); 817 assert(Timestamp.onlyTime(7, 40, 30, -3, 56).toISOString == "T074030.056"); 818 } 819 820 /// 821 long toUnixTime() const @safe pure nothrow @nogc 822 { 823 import mir.date: Date; 824 long result; 825 if (!isDuration && !isOnlyTime) 826 { 827 enum fistDay = Date.trustedCreate(1970, 1, 1); 828 result = Date.trustedCreate(year, month ? month : 1, day ? day : 1) - fistDay; 829 result *= 24; 830 } 831 result += hour; 832 result *= 60; 833 result += minute; 834 result *= 60; 835 result += second; 836 return result; 837 } 838 839 /// 840 version(mir_test) 841 @safe pure nothrow @nogc unittest 842 { 843 assert(Timestamp(1970, 1, 1).toUnixTime == 0); 844 assert(Timestamp(2007, 12, 22, 8, 14, 45).toUnixTime == 1_198_311_285); 845 assert(Timestamp(2007, 12, 22, 8, 14, 45).withOffset(90).toUnixTime == 1_198_311_285); 846 assert(Timestamp(1969, 12, 31, 23, 59, 59).toUnixTime == -1); 847 } 848 849 /// 850 static Timestamp fromUnixTime(long time) @safe pure nothrow @nogc 851 { 852 import mir.date: Date; 853 auto days = time / (24 * 60 * 60); 854 time -= days * (24 * 60 * 60); 855 if (time < 0) 856 { 857 days--; 858 time += 24 * 60 * 60; 859 } 860 assert(time >= 0); 861 auto second = time % 60; 862 time /= 60; 863 auto minute = time % 60; 864 time /= 60; 865 auto hour = cast(ubyte) time; 866 enum fistDay = Date.trustedCreate(1970, 1, 1); 867 with((fistDay + cast(int)days).yearMonthDay) 868 return Timestamp(year, cast(ubyte)month, day, hour, cast(ubyte)minute, cast(ubyte)second); 869 } 870 871 /// 872 version(mir_test) 873 @safe pure nothrow @nogc unittest 874 { 875 import mir.format; 876 assert(Timestamp.fromUnixTime(0) == Timestamp(1970, 1, 1, 0, 0, 0)); 877 assert(Timestamp.fromUnixTime(1_198_311_285) == Timestamp(2007, 12, 22, 8, 14, 45)); 878 assert(Timestamp.fromUnixTime(-1) == Timestamp(1969, 12, 31, 23, 59, 59)); 879 } 880 881 /// Helpfer for time zone offsets 882 void addMinutes(short minutes) @safe pure nothrow @nogc 883 { 884 int totalMinutes = minutes + (this.minute + this.hour * 60u); 885 auto h = totalMinutes / 60; 886 887 int dayShift; 888 889 while (totalMinutes < 0) 890 { 891 totalMinutes += 24 * 60; 892 dayShift--; 893 } 894 895 while (totalMinutes >= 24 * 60) 896 { 897 totalMinutes -= 24 * 60; 898 dayShift++; 899 } 900 901 if (dayShift) 902 { 903 import mir.date: Date; 904 auto ymd = (Date.trustedCreate(year, month, day) + dayShift).yearMonthDay; 905 year = ymd.year; 906 month = cast(ubyte)ymd.month; 907 day = ymd.day; 908 } 909 910 hour = cast(ubyte) (totalMinutes / 60); 911 minute = cast(ubyte) (totalMinutes % 60); 912 } 913 914 template toISOStringImp(bool ext) 915 { 916 version(D_BetterC){} else 917 string toISOStringImp() const @safe pure nothrow 918 { 919 return toStringImpl!toISOStringImp; 920 } 921 922 /// ditto 923 void toISOStringImp(W)(ref scope W w) const scope 924 // if (isOutputRange!(W, char)) 925 { 926 import mir.format: printZeroPad; 927 // yyyy-mm-ddThh:mm:ss[.mmm]±hh:mm 928 Timestamp t = this; 929 930 if (t.offset) 931 { 932 assert(-24 * 60 <= t.offset && t.offset <= 24 * 60, "Offset absolute value should be less or equal to 24 * 60"); 933 assert(precision >= Precision.minute, "Offsets are not allowed on date values."); 934 t.addMinutes(t.offset); 935 } 936 937 if (!t.isOnlyTime) 938 { 939 if (t.year >= 10_000) 940 w.put('+'); 941 printZeroPad(w, t.year, t.year >= 0 ? t.year < 10_000 ? 4 : 5 : t.year > -10_000 ? 5 : 6); 942 if (precision == Precision.year) 943 { 944 w.put('T'); 945 return; 946 } 947 if (ext || precision == Precision.month) w.put('-'); 948 949 printZeroPad(w, cast(uint)t.month, 2); 950 if (precision == Precision.month) 951 { 952 w.put('T'); 953 return; 954 } 955 static if (ext) w.put('-'); 956 957 printZeroPad(w, t.day, 2); 958 if (precision == Precision.day) 959 return; 960 } 961 962 if (!ext || !t.isOnlyTime) 963 w.put('T'); 964 965 printZeroPad(w, t.hour, 2); 966 static if (ext) w.put(':'); 967 printZeroPad(w, t.minute, 2); 968 969 if (precision >= Precision.second) 970 { 971 static if (ext) w.put(':'); 972 printZeroPad(w, t.second, 2); 973 974 if (precision > Precision.second && (t.fractionExponent < 0 || t.fractionCoefficient)) 975 { 976 w.put('.'); 977 printZeroPad(w, t.fractionCoefficient, -int(t.fractionExponent)); 978 } 979 } 980 981 if (t.isLocalTime) 982 { 983 static if (ext) 984 w.put("-00:00"); 985 return; 986 } 987 988 if (t.offset == 0) 989 { 990 w.put('Z'); 991 return; 992 } 993 994 bool sign = t.offset < 0; 995 uint absoluteOffset = !sign ? t.offset : -int(t.offset); 996 uint offsetHour = absoluteOffset / 60u; 997 uint offsetMinute = absoluteOffset % 60u; 998 999 w.put(sign ? '-' : '+'); 1000 printZeroPad(w, offsetHour, 2); 1001 if (offsetMinute) 1002 { 1003 static if (ext) w.put(':'); 1004 printZeroPad(w, offsetMinute, 2); 1005 } 1006 } 1007 } 1008 1009 /++ 1010 Creates a $(LREF Timestamp) from a string with the format `YYYYMMDDThhmmss±hhmm 1011 or its leading part allowed by the standard. 1012 1013 or its leading part allowed by the standard. 1014 1015 Params: 1016 str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) formats dates. 1017 value = (optional) result value. 1018 1019 Throws: 1020 $(LREF DateTimeException) if the given string is 1021 not in the correct format. Two arguments overload is `nothrow`. 1022 Returns: 1023 `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload. 1024 +/ 1025 alias fromISOString = fromISOStringImpl!false; 1026 1027 /// 1028 version (mir_test) 1029 @safe unittest 1030 { 1031 assert(Timestamp.fromISOString("20100704") == Timestamp(2010, 7, 4)); 1032 assert(Timestamp.fromISOString("19981225") == Timestamp(1998, 12, 25)); 1033 assert(Timestamp.fromISOString("00000105") == Timestamp(0, 1, 5)); 1034 // assert(Timestamp.fromISOString("-00040105") == Timestamp(-4, 1, 5)); 1035 1036 assert(Timestamp(2021) == Timestamp.fromISOString("2021")); 1037 assert(Timestamp(2021) == Timestamp.fromISOString("2021T")); 1038 // assert(Timestamp(2021, 01) == Timestamp.fromISOString("2021-01")); 1039 // assert(Timestamp(2021, 01) == Timestamp.fromISOString("2021-01T")); 1040 assert(Timestamp(2021, 01, 29) == Timestamp.fromISOString("20210129")); 1041 assert(Timestamp(2021, 01, 29, 19, 42) == Timestamp.fromISOString("20210129T1942")); 1042 assert(Timestamp(2021, 01, 29, 19, 42).withOffset(0) == Timestamp.fromISOString("20210129T1942Z")); 1043 assert(Timestamp(2021, 01, 29, 19, 42, 12) == Timestamp.fromISOString("20210129T194212")); 1044 assert(Timestamp(2021, 01, 29, 19, 42, 12, -3, 67).withOffset(0) == Timestamp.fromISOString("20210129T194212.067Z")); 1045 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60) == Timestamp.fromISOString("20210129T194244+07")); 1046 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOString("20210129T201244+0730")); 1047 static assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOString("20210129T201244+0730")); 1048 static assert(Timestamp(2021, 01, 29, 4, 42, 44).withOffset(- (7 * 60 + 30)) == Timestamp.fromISOString("20210128T211244-0730")); 1049 } 1050 1051 version (mir_test) 1052 @safe unittest 1053 { 1054 import std.exception: assertThrown; 1055 assertThrown!DateTimeException(Timestamp.fromISOString("")); 1056 assertThrown!DateTimeException(Timestamp.fromISOString("990704")); 1057 assertThrown!DateTimeException(Timestamp.fromISOString("0100704")); 1058 assertThrown!DateTimeException(Timestamp.fromISOString("2010070")); 1059 assertThrown!DateTimeException(Timestamp.fromISOString("120100704")); 1060 assertThrown!DateTimeException(Timestamp.fromISOString("-0100704")); 1061 assertThrown!DateTimeException(Timestamp.fromISOString("+0100704")); 1062 assertThrown!DateTimeException(Timestamp.fromISOString("2010070a")); 1063 assertThrown!DateTimeException(Timestamp.fromISOString("20100a04")); 1064 assertThrown!DateTimeException(Timestamp.fromISOString("2010a704")); 1065 1066 assertThrown!DateTimeException(Timestamp.fromISOString("99-07-04")); 1067 assertThrown!DateTimeException(Timestamp.fromISOString("010-07-04")); 1068 assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-0")); 1069 assertThrown!DateTimeException(Timestamp.fromISOString("12010-07-04")); 1070 assertThrown!DateTimeException(Timestamp.fromISOString("-010-07-04")); 1071 assertThrown!DateTimeException(Timestamp.fromISOString("+010-07-04")); 1072 assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-0a")); 1073 assertThrown!DateTimeException(Timestamp.fromISOString("2010-0a-04")); 1074 assertThrown!DateTimeException(Timestamp.fromISOString("2010-a7-04")); 1075 assertThrown!DateTimeException(Timestamp.fromISOString("2010/07/04")); 1076 assertThrown!DateTimeException(Timestamp.fromISOString("2010/7/04")); 1077 assertThrown!DateTimeException(Timestamp.fromISOString("2010/7/4")); 1078 assertThrown!DateTimeException(Timestamp.fromISOString("2010/07/4")); 1079 assertThrown!DateTimeException(Timestamp.fromISOString("2010-7-04")); 1080 assertThrown!DateTimeException(Timestamp.fromISOString("2010-7-4")); 1081 assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-4")); 1082 1083 assertThrown!DateTimeException(Timestamp.fromISOString("99Jul04")); 1084 assertThrown!DateTimeException(Timestamp.fromISOString("010Jul04")); 1085 assertThrown!DateTimeException(Timestamp.fromISOString("2010Jul0")); 1086 assertThrown!DateTimeException(Timestamp.fromISOString("12010Jul04")); 1087 assertThrown!DateTimeException(Timestamp.fromISOString("-010Jul04")); 1088 assertThrown!DateTimeException(Timestamp.fromISOString("+010Jul04")); 1089 assertThrown!DateTimeException(Timestamp.fromISOString("2010Jul0a")); 1090 assertThrown!DateTimeException(Timestamp.fromISOString("2010Jua04")); 1091 assertThrown!DateTimeException(Timestamp.fromISOString("2010aul04")); 1092 1093 assertThrown!DateTimeException(Timestamp.fromISOString("99-Jul-04")); 1094 assertThrown!DateTimeException(Timestamp.fromISOString("010-Jul-04")); 1095 assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jul-0")); 1096 assertThrown!DateTimeException(Timestamp.fromISOString("12010-Jul-04")); 1097 assertThrown!DateTimeException(Timestamp.fromISOString("-010-Jul-04")); 1098 assertThrown!DateTimeException(Timestamp.fromISOString("+010-Jul-04")); 1099 assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jul-0a")); 1100 assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jua-04")); 1101 assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jal-04")); 1102 assertThrown!DateTimeException(Timestamp.fromISOString("2010-aul-04")); 1103 1104 // assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-04")); 1105 assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jul-04")); 1106 1107 assert(Timestamp.fromISOString("19990706") == Timestamp(1999, 7, 6)); 1108 // assert(Timestamp.fromISOString("-19990706") == Timestamp(-1999, 7, 6)); 1109 // assert(Timestamp.fromISOString("+019990706") == Timestamp(1999, 7, 6)); 1110 assert(Timestamp.fromISOString("19990706") == Timestamp(1999, 7, 6)); 1111 } 1112 1113 // bug# 17801 1114 version (mir_test) 1115 @safe unittest 1116 { 1117 import std.conv : to; 1118 import std.meta : AliasSeq; 1119 static foreach (C; AliasSeq!(char, wchar, dchar)) 1120 { 1121 static foreach (S; AliasSeq!(C[], const(C)[], immutable(C)[])) 1122 assert(Timestamp.fromISOString(to!S("20121221")) == Timestamp(2012, 12, 21)); 1123 } 1124 } 1125 1126 /++ 1127 Creates a $(LREF Timestamp) from a string with the format `yyyy-mm-ddThh:mm:ss[.mmm]±hh:mm` 1128 or its leading part allowed by the standard. 1129 1130 Params: 1131 str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) formats dates. 1132 value = (optional) result value. 1133 1134 Throws: 1135 $(LREF DateTimeException) if the given string is 1136 not in the correct format. Two arguments overload is `nothrow`. 1137 Returns: 1138 `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload. 1139 +/ 1140 alias fromISOExtString = fromISOStringImpl!true; 1141 1142 1143 /// 1144 version (mir_test) 1145 @safe unittest 1146 { 1147 assert(Timestamp.fromISOExtString("2010-07-04") == Timestamp(2010, 7, 4)); 1148 assert(Timestamp.fromISOExtString("1998-12-25") == Timestamp(1998, 12, 25)); 1149 assert(Timestamp.fromISOExtString("0000-01-05") == Timestamp(0, 1, 5)); 1150 assert(Timestamp.fromISOExtString("-0004-01-05") == Timestamp(-4, 1, 5)); 1151 1152 assert(Timestamp(2021) == Timestamp.fromISOExtString("2021")); 1153 assert(Timestamp(2021) == Timestamp.fromISOExtString("2021T")); 1154 assert(Timestamp(2021, 01) == Timestamp.fromISOExtString("2021-01")); 1155 assert(Timestamp(2021, 01) == Timestamp.fromISOExtString("2021-01T")); 1156 assert(Timestamp(2021, 01, 29) == Timestamp.fromISOExtString("2021-01-29")); 1157 assert(Timestamp(2021, 01, 29, 19, 42) == Timestamp.fromISOExtString("2021-01-29T19:42")); 1158 assert(Timestamp(2021, 01, 29, 19, 42).withOffset(0) == Timestamp.fromISOExtString("2021-01-29T19:42Z")); 1159 assert(Timestamp(2021, 01, 29, 19, 42, 12) == Timestamp.fromISOExtString("2021-01-29T19:42:12")); 1160 assert(Timestamp(2021, 01, 29, 19, 42, 12, -3, 67).withOffset(0) == Timestamp.fromISOExtString("2021-01-29T19:42:12.067Z")); 1161 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60) == Timestamp.fromISOExtString("2021-01-29T19:42:44+07")); 1162 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOExtString("2021-01-29T20:12:44+07:30")); 1163 static assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOExtString("2021-01-29T20:12:44+07:30")); 1164 static assert(Timestamp(2021, 01, 29, 4, 42, 44).withOffset(- (7 * 60 + 30)) == Timestamp.fromISOExtString("2021-01-28T21:12:44-07:30")); 1165 } 1166 1167 /++ 1168 Creates a $(LREF Timestamp) from a YAML string format 1169 or its leading part allowed by the standard. 1170 1171 Params: 1172 str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) formats dates. 1173 value = (optional) result value. 1174 1175 Throws: 1176 $(LREF DateTimeException) if the given string is 1177 not in the correct format. Two arguments overload is `nothrow`. 1178 Returns: 1179 `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload. 1180 +/ 1181 alias fromYamlString = fromISOStringImpl!(true, true); 1182 1183 /// 1184 version (mir_test) 1185 @safe unittest 1186 { 1187 // canonical 1188 assert(Timestamp.fromYamlString("2001-12-15T02:59:43.1Z") == Timestamp("2001-12-15T02:59:43.1Z")); 1189 // with lower 't' separator 1190 assert(Timestamp.fromYamlString("2001-12-14t21:59:43.1-05:30") == Timestamp("2001-12-14T21:59:43.1-05:30")); 1191 // yaml space separated 1192 assert(Timestamp.fromYamlString("2001-12-14 21:59:43.1 -5") == Timestamp("2001-12-14T21:59:43.1-05")); 1193 // no time zone (Z) 1194 assert(Timestamp.fromYamlString("2001-12-15 2:59:43.10") == Timestamp("2001-12-15T02:59:43.10")); 1195 // date 00:00:00Z 1196 assert(Timestamp.fromYamlString("2002-12-14") == Timestamp("2002-12-14")); 1197 } 1198 1199 version (mir_test) 1200 @safe unittest 1201 { 1202 import std.exception: assertThrown; 1203 1204 assertThrown!DateTimeException(Timestamp.fromISOExtString("")); 1205 assertThrown!DateTimeException(Timestamp.fromISOExtString("990704")); 1206 assertThrown!DateTimeException(Timestamp.fromISOExtString("0100704")); 1207 assertThrown!DateTimeException(Timestamp.fromISOExtString("120100704")); 1208 assertThrown!DateTimeException(Timestamp.fromISOExtString("-0100704")); 1209 assertThrown!DateTimeException(Timestamp.fromISOExtString("+0100704")); 1210 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010070a")); 1211 assertThrown!DateTimeException(Timestamp.fromISOExtString("20100a04")); 1212 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010a704")); 1213 1214 assertThrown!DateTimeException(Timestamp.fromISOExtString("99-07-04")); 1215 assertThrown!DateTimeException(Timestamp.fromISOExtString("010-07-04")); 1216 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-07-0")); 1217 assertThrown!DateTimeException(Timestamp.fromISOExtString("12010-07-04")); 1218 assertThrown!DateTimeException(Timestamp.fromISOExtString("-010-07-04")); 1219 assertThrown!DateTimeException(Timestamp.fromISOExtString("+010-07-04")); 1220 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-07-0a")); 1221 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-0a-04")); 1222 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-a7-04")); 1223 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/07/04")); 1224 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/7/04")); 1225 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/7/4")); 1226 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/07/4")); 1227 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-7-04")); 1228 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-7-4")); 1229 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-07-4")); 1230 1231 assertThrown!DateTimeException(Timestamp.fromISOExtString("99Jul04")); 1232 assertThrown!DateTimeException(Timestamp.fromISOExtString("010Jul04")); 1233 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010Jul0")); 1234 assertThrown!DateTimeException(Timestamp.fromISOExtString("12010Jul04")); 1235 assertThrown!DateTimeException(Timestamp.fromISOExtString("-010Jul04")); 1236 assertThrown!DateTimeException(Timestamp.fromISOExtString("+010Jul04")); 1237 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010Jul0a")); 1238 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010Jua04")); 1239 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010aul04")); 1240 1241 assertThrown!DateTimeException(Timestamp.fromISOExtString("99-Jul-04")); 1242 assertThrown!DateTimeException(Timestamp.fromISOExtString("010-Jul-04")); 1243 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jul-0")); 1244 assertThrown!DateTimeException(Timestamp.fromISOExtString("12010-Jul-04")); 1245 assertThrown!DateTimeException(Timestamp.fromISOExtString("-010-Jul-04")); 1246 assertThrown!DateTimeException(Timestamp.fromISOExtString("+010-Jul-04")); 1247 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jul-0a")); 1248 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jua-04")); 1249 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jal-04")); 1250 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-aul-04")); 1251 1252 assertThrown!DateTimeException(Timestamp.fromISOExtString("20100704")); 1253 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jul-04")); 1254 1255 assert(Timestamp.fromISOExtString("1999-07-06") == Timestamp(1999, 7, 6)); 1256 assert(Timestamp.fromISOExtString("-1999-07-06") == Timestamp(-1999, 7, 6)); 1257 assert(Timestamp.fromISOExtString("+01999-07-06") == Timestamp(1999, 7, 6)); 1258 } 1259 1260 // bug# 17801 1261 version (mir_test) 1262 @safe unittest 1263 { 1264 import std.conv : to; 1265 import std.meta : AliasSeq; 1266 static foreach (C; AliasSeq!(char, wchar, dchar)) 1267 { 1268 static foreach (S; AliasSeq!(C[], const(C)[], immutable(C)[])) 1269 assert(Timestamp.fromISOExtString(to!S("2012-12-21")) == Timestamp(2012, 12, 21)); 1270 } 1271 } 1272 1273 /++ 1274 Creates a $(LREF Timestamp) from a string. 1275 1276 Params: 1277 str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) and $(LREF .Timestamp.toISOString) format dates, also YAML like spaces seprated strings are accepted. The function is case sensetive. 1278 value = (optional) result value. 1279 1280 Throws: 1281 $(LREF DateTimeException) if the given string is 1282 not in the correct format. Two arguments overload is `nothrow`. 1283 Returns: 1284 `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload. 1285 +/ 1286 static bool fromString(C)(scope const(C)[] str, out Timestamp value) @safe pure nothrow @nogc 1287 { 1288 return fromYamlString(str, value) 1289 || fromISOString(str, value); 1290 } 1291 1292 /// 1293 version (mir_test) 1294 @safe pure @nogc unittest 1295 { 1296 assert(Timestamp.fromString("2010-07-04") == Timestamp(2010, 7, 4)); 1297 assert(Timestamp.fromString("20100704") == Timestamp(2010, 7, 4)); 1298 } 1299 1300 /// ditto 1301 static Timestamp fromString(C)(scope const(C)[] str) @safe pure 1302 if (isSomeChar!C) 1303 { 1304 Timestamp ret; 1305 if (fromString(str, ret)) 1306 return ret; 1307 { import mir.exception : toMutable; throw InvalidString.toMutable; } 1308 } 1309 1310 template fromISOStringImpl(bool ext, bool yaml = false) 1311 { 1312 static Timestamp fromISOStringImpl(C)(scope const(C)[] str) @safe pure 1313 if (isSomeChar!C) 1314 { 1315 Timestamp ret; 1316 if (fromISOStringImpl(str, ret)) 1317 return ret; 1318 static if (yaml) 1319 { import mir.exception : toMutable; throw InvalidYamlString.toMutable; } 1320 else 1321 static if (ext) 1322 { import mir.exception : toMutable; throw InvalidISOExtendedString.toMutable; } 1323 else 1324 { import mir.exception : toMutable; throw InvalidISOString.toMutable; } 1325 } 1326 1327 static bool fromISOStringImpl(C)(scope const(C)[] str, out Timestamp value) @safe pure nothrow @nogc 1328 if (isSomeChar!C) 1329 { 1330 import mir.parse: fromString, parse; 1331 1332 if (str.length < 4) 1333 return false; 1334 1335 static if (ext) 1336 auto isOnlyTime = (str[0] == 'T' || yaml && (str[0] == 't')) || str[2] == ':'; 1337 else 1338 auto isOnlyTime = str[0] == 'T' || yaml && (str[0] == 't'); 1339 1340 if (!isOnlyTime) 1341 { 1342 // YYYY 1343 static if (ext) 1344 {{ 1345 auto startIsDigit = str.length && str[0].isDigit; 1346 auto strOldLength = str.length; 1347 if (!parse(str, value.year)) 1348 return false; 1349 auto l = strOldLength - str.length; 1350 if ((l == 4) != startIsDigit) 1351 return false; 1352 }} 1353 else 1354 { 1355 if (str.length < 4 || !str[0].isDigit || !fromString(str[0 .. 4], value.year)) 1356 return false; 1357 str = str[4 .. $]; 1358 } 1359 1360 value.precision = Precision.year; 1361 if (str.length == 0 || str == "T") 1362 return true; 1363 1364 static if (ext) 1365 { 1366 if (str[0] != '-') 1367 return false; 1368 str = str[1 .. $]; 1369 } 1370 1371 // MM 1372 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], value.month)) 1373 return false; 1374 str = str[2 .. $]; 1375 value.precision = Precision.month; 1376 if (str.length == 0 || str.length == 1 && (str[0] == 'T' || (yaml && (str[0] == 't' || str[0] == ' ' || str[0] == '\t')))) 1377 return ext; 1378 1379 static if (ext) 1380 { 1381 if (str[0] != '-') 1382 return false; 1383 str = str[1 .. $]; 1384 } 1385 1386 // DD 1387 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], value.day)) 1388 return false; 1389 str = str[2 .. $]; 1390 value.precision = Precision.day; 1391 if (str.length == 0) 1392 return true; 1393 } 1394 1395 // str isn't empty here 1396 // T 1397 if ((str[0] == 'T' || (yaml && (str[0] == 't' || str[0] == ' ' || str[0] == '\t')))) 1398 { 1399 str = str[1 .. $]; 1400 // OK, onlyTime requires length >= 3 1401 if (str.length == 0) 1402 return true; 1403 } 1404 else 1405 { 1406 if (!(ext && isOnlyTime)) 1407 return false; 1408 } 1409 1410 value.precision = Precision.minute; // we don't have hour precision 1411 1412 // hh 1413 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], value.hour)) 1414 { 1415 static if (yaml) 1416 { 1417 if (str.length < 1 || !str[0].isDigit || !fromString(str[0 .. 1], value.hour)) 1418 return false; 1419 else 1420 str = str[1 .. $]; 1421 } 1422 else 1423 return false; 1424 } 1425 else 1426 str = str[2 .. $]; 1427 1428 if (str.length == 0) 1429 return true; 1430 1431 static if (ext) 1432 { 1433 if (str[0] != ':') 1434 return false; 1435 str = str[1 .. $]; 1436 } 1437 1438 // mm 1439 { 1440 uint minute; 1441 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], minute)) 1442 return false; 1443 value.minute = cast(ubyte) minute; 1444 str = str[2 .. $]; 1445 if (str.length == 0) 1446 return true; 1447 } 1448 1449 static if (ext) 1450 { 1451 if (str[0] != ':') 1452 goto TZ; 1453 str = str[1 .. $]; 1454 } 1455 1456 // ss 1457 { 1458 uint second; 1459 if (str.length < 2 || !str[0].isDigit) 1460 goto TZ; 1461 if (!fromString(str[0 .. 2], second)) 1462 return false; 1463 value.second = cast(ubyte) second; 1464 str = str[2 .. $]; 1465 value.precision = Precision.second; 1466 if (str.length == 0) 1467 return true; 1468 } 1469 1470 // . 1471 if (str[0] != '.') 1472 goto TZ; 1473 str = str[1 .. $]; 1474 value.precision = Precision.fraction; 1475 1476 // fraction 1477 { 1478 const strOldLength = str.length; 1479 ulong fractionCoefficient; 1480 if (str.length < 1 || !str[0].isDigit || !parse!ulong(str, fractionCoefficient)) 1481 return false; 1482 sizediff_t fractionExponent = str.length - strOldLength; 1483 if (fractionExponent < -12) 1484 return false; 1485 value.fractionExponent = cast(byte)fractionExponent; 1486 value.fractionCoefficient = fractionCoefficient; 1487 if (str.length == 0) 1488 return true; 1489 } 1490 1491 TZ: 1492 1493 static if (yaml) 1494 { 1495 if (str.length && (str[0] == ' ' || str[0] == '\t')) 1496 str = str[1 .. $]; 1497 } 1498 1499 if (str == "Z") 1500 { 1501 value.offset = 0; 1502 return true; 1503 } 1504 1505 bool neg = str[0] == '-'; 1506 1507 int hour; 1508 int minute; 1509 if (str.length < 3 || str[0].isDigit || !fromString(str[0 .. 3], hour)) 1510 { 1511 static if (yaml) 1512 { 1513 if (str.length < 2 || str[0].isDigit || !fromString(str[0 .. 2], hour)) 1514 return false; 1515 str = str[2 .. $]; 1516 } 1517 else 1518 return false; 1519 } 1520 else 1521 { 1522 str = str[3 .. $]; 1523 } 1524 1525 if (str.length) 1526 { 1527 static if (ext) 1528 { 1529 if (str[0] != ':') 1530 return false; 1531 str = str[1 .. $]; 1532 } 1533 if (str.length != 2 || !str[0].isDigit || !fromString(str[0 .. 2], minute)) 1534 return false; 1535 } 1536 1537 if (neg && hour == 0 && minute == 0) 1538 value.setLocalTimezone; 1539 else 1540 value.offset = cast(short)(hour * 60 + (hour < 0 ? -minute : minute)); 1541 value.addMinutes(cast(short)-int(value.offset)); 1542 return true; 1543 } 1544 } 1545 1546 /// 1547 bool isDuration() const @safe pure nothrow @nogc @property 1548 { 1549 return day == 88 || day == 99; 1550 } 1551 1552 /// 1553 bool isNegativeDuration() const @safe pure nothrow @nogc @property 1554 { 1555 return day == 99; 1556 } 1557 } 1558 1559 version(mir_test) 1560 unittest 1561 { 1562 long sec = -2208988800; 1563 uint nanosec = 0; 1564 auto ts = Timestamp.fromUnixTime(sec); 1565 if (nanosec >= 0) 1566 { 1567 ts.precision = Timestamp.Precision.fraction; 1568 ts.fractionCoefficient = nanosec; 1569 ts.fractionExponent = -9; 1570 } 1571 auto ts2 = "1900-01-01T00:00:00.000000000".Timestamp; 1572 assert(ts == ts2); 1573 } 1574 1575 version(mir_test) 1576 @safe pure unittest 1577 { 1578 import std.datetime.systime : SysTime; 1579 import mir.test; 1580 auto ts = "2001-12-15T2:59:43.1234567".Timestamp; 1581 // ts.toString.should == "2001-12-15T02:59:43.1234567-00:00"; 1582 auto st = cast(SysTime)ts; 1583 // st.toISOExtString.should == "2001-12-15T02:59:43.1234567"; 1584 st.Timestamp.should == ts; 1585 } 1586 1587 private auto assumePureSafe(T)(T t) @trusted 1588 // if (isFunctionPointer!T || isDelegate!T) 1589 { 1590 import std.traits; 1591 enum attrs = (functionAttributes!T | FunctionAttribute.pure_ | FunctionAttribute.safe) & ~FunctionAttribute.system; 1592 return cast(SetFunctionAttributes!(T, functionLinkage!T, attrs)) t; 1593 }