1 /++ 2 Timestamp 3 +/ 4 module mir.timestamp; 5 6 private alias isDigit = (dchar c) => uint(c - '0') < 10; 7 import mir.serde: serdeIgnore; 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("Invalid Month"); 29 private static immutable InvalidDay = new DateTimeException("Invalid Day"); 30 private static immutable InvalidISOString = new DateTimeException("Invalid ISO String"); 31 private static immutable InvalidISOExtendedString = new DateTimeException("Invalid ISO Extended String"); 32 private static immutable InvalidString = new DateTimeException("Invalid String"); 33 } 34 35 /++ 36 Timestamp 37 38 Note: The component values in the binary encoding are always in UTC, while components in the text encoding are in the local time! 39 This means that transcoding requires a conversion between UTC and local time. 40 41 `Timestamp` precision is up to picosecond (second/10^12). 42 +/ 43 struct Timestamp 44 { 45 import std.traits: isSomeChar; 46 47 /// 48 enum Precision : ubyte 49 { 50 /// 51 year, 52 /// 53 month, 54 /// 55 day, 56 /// 57 minute, 58 /// 59 second, 60 /// 61 fraction, 62 } 63 64 /// 65 this(scope const(char)[] str) @safe pure @nogc 66 { 67 this = fromString(str); 68 } 69 70 /// 71 version (mir_test) 72 @safe pure @nogc unittest 73 { 74 assert(Timestamp("2010-07-04") == Timestamp(2010, 7, 4)); 75 assert(Timestamp("20100704") == Timestamp(2010, 7, 4)); 76 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOString("20210129T201244+0730")); 77 static assert(Timestamp(2021, 01, 29, 4, 42, 44).withOffset(- (7 * 60 + 30)) == Timestamp.fromISOExtString("2021-01-28T21:12:44-07:30")); 78 79 assert(Timestamp("T0740Z") == Timestamp.onlyTime(7, 40)); 80 assert(Timestamp("T074030Z") == Timestamp.onlyTime(7, 40, 30)); 81 assert(Timestamp("T074030.056Z") == Timestamp.onlyTime(7, 40, 30, -3, 56)); 82 83 assert(Timestamp("07:40Z") == Timestamp.onlyTime(7, 40)); 84 assert(Timestamp("07:40:30Z") == Timestamp.onlyTime(7, 40, 30)); 85 assert(Timestamp("T07:40:30.056Z") == Timestamp.onlyTime(7, 40, 30, -3, 56)); 86 } 87 88 version(all) 89 { 90 short offset; 91 } 92 else 93 /+ 94 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”. 95 This differs semantically from an offset of “Z” or “+00:00”, which imply that UTC is the preferred reference point for the specified time. 96 RFC2822 describes a similar convention for email. 97 private short _offset; 98 +/ 99 { 100 101 /++ 102 Timezone offset in minutes 103 +/ 104 short offset() const @safe pure nothrow @nogc @property 105 { 106 return _offset >> 1; 107 } 108 109 /++ 110 Returns: true if timezone has offset 111 +/ 112 bool hasOffset() const @safe pure nothrow @nogc @property 113 { 114 return _offset & 1; 115 } 116 } 117 118 @serdeIgnore: 119 120 /++ 121 Year 122 +/ 123 short year; 124 /++ 125 +/ 126 Precision precision; 127 128 /++ 129 Month 130 131 If the value equals to thero then this and all the following members are undefined. 132 +/ 133 ubyte month; 134 /++ 135 Day 136 137 If the value equals to thero then this and all the following members are undefined. 138 +/ 139 ubyte day; 140 /++ 141 Hour 142 +/ 143 ubyte hour; 144 145 version(D_Ddoc) 146 { 147 148 /++ 149 Minute 150 151 Note: the field is implemented as property. 152 +/ 153 ubyte minute; 154 /++ 155 Second 156 157 Note: the field is implemented as property. 158 +/ 159 ubyte second; 160 /++ 161 Fraction 162 163 The `fraction_exponent` and `fraction_coefficient` denote the fractional seconds of the timestamp as a decimal value 164 The fractional seconds’ value is `coefficient * 10 ^ exponent`. 165 It must be greater than or equal to zero and less than 1. 166 A missing coefficient defaults to zero. 167 Fractions whose coefficient is zero and exponent is greater than -1 are ignored. 168 169 'fractionCoefficient' allowed values are [0 ... 10^12-1]. 170 'fractionExponent' allowed values are [-12 ... 0]. 171 172 Note: the fields are implemented as property. 173 +/ 174 byte fractionExponent; 175 /// ditto 176 long fractionCoefficient; 177 } 178 else 179 { 180 import mir.bitmanip: bitfields; 181 version (LittleEndian) 182 { 183 184 mixin(bitfields!( 185 ubyte, "minute", 8, 186 ubyte, "second", 8, 187 byte, "fractionExponent", 8, 188 long, "fractionCoefficient", 40, 189 )); 190 } 191 else 192 { 193 mixin(bitfields!( 194 long, "fractionCoefficient", 40, 195 byte, "fractionExponent", 8, 196 ubyte, "second", 8, 197 ubyte, "minute", 8, 198 )); 199 } 200 } 201 202 /// 203 @safe pure nothrow @nogc 204 this(short year) 205 { 206 this.year = year; 207 this.precision = Precision.year; 208 } 209 210 /// 211 @safe pure nothrow @nogc 212 this(short year, ubyte month) 213 { 214 this.year = year; 215 this.month = month; 216 this.precision = Precision.month; 217 } 218 219 /// 220 @safe pure nothrow @nogc 221 this(short year, ubyte month, ubyte day) 222 { 223 this.year = year; 224 this.month = month; 225 this.day = day; 226 this.precision = Precision.day; 227 } 228 229 /// 230 @safe pure nothrow @nogc 231 this(short year, ubyte month, ubyte day, ubyte hour, ubyte minute) 232 { 233 this.year = year; 234 this.month = month; 235 this.day = day; 236 this.hour = hour; 237 this.minute = minute; 238 this.precision = Precision.minute; 239 } 240 241 /// 242 @safe pure nothrow @nogc 243 this(short year, ubyte month, ubyte day, ubyte hour, ubyte minute, ubyte second) 244 { 245 this.year = year; 246 this.month = month; 247 this.day = day; 248 this.hour = hour; 249 this.day = day; 250 this.minute = minute; 251 this.second = second; 252 this.precision = Precision.second; 253 } 254 255 /// 256 @safe pure nothrow @nogc 257 this(short year, ubyte month, ubyte day, ubyte hour, ubyte minute, ubyte second, byte fractionExponent, ulong fractionCoefficient) 258 { 259 this.year = year; 260 this.month = month; 261 this.day = day; 262 this.hour = hour; 263 this.day = day; 264 this.minute = minute; 265 this.second = second; 266 assert(fractionExponent < 0); 267 this.fractionExponent = fractionExponent; 268 this.fractionCoefficient = fractionCoefficient; 269 this.precision = Precision.fraction; 270 } 271 272 /// 273 @safe pure nothrow @nogc 274 static Timestamp onlyTime(ubyte hour, ubyte minute) 275 { 276 return Timestamp(0, 0, 0, hour, minute); 277 } 278 279 /// 280 @safe pure nothrow @nogc 281 static Timestamp onlyTime(ubyte hour, ubyte minute, ubyte second) 282 { 283 return Timestamp(0, 0, 0, hour, minute, second); 284 } 285 286 /// 287 @safe pure nothrow @nogc 288 static Timestamp onlyTime(ubyte hour, ubyte minute, ubyte second, byte fractionExponent, ulong fractionCoefficient) 289 { 290 return Timestamp(0, 0, 0, hour, minute, second, fractionExponent, fractionCoefficient); 291 } 292 293 /// 294 this(Date)(const Date datetime) 295 if (Date.stringof == "Date" || Date.stringof == "date") 296 { 297 static if (__traits(hasMember, Date, "yearMonthDay")) 298 with(datetime.yearMonthDay) this(year, cast(ubyte)month, day); 299 else 300 with(datetime) this(year, month, day); 301 } 302 303 /// 304 version (mir_test) 305 @safe unittest { 306 import mir.date : Date; 307 auto dt = Date(1982, 4, 1); 308 Timestamp ts = dt; 309 assert(ts.opCmp(ts) == 0); 310 assert(dt.toISOExtString == ts.toString); 311 assert(dt == cast(Date) ts); 312 } 313 314 /// 315 version (mir_test) 316 @safe unittest { 317 import std.datetime.date : Date; 318 auto dt = Date(1982, 4, 1); 319 Timestamp ts = dt; 320 assert(dt.toISOExtString == ts.toString); 321 assert(dt == cast(Date) ts); 322 } 323 324 /// 325 this(TimeOfDay)(const TimeOfDay timeOfDay) 326 if (TimeOfDay.stringof == "TimeOfDay") 327 { 328 with(timeOfDay) this = onlyTime(hour, minute, second); 329 } 330 331 /// 332 version (mir_test) 333 @safe unittest { 334 import std.datetime.date : TimeOfDay; 335 auto dt = TimeOfDay(7, 14, 30); 336 Timestamp ts = dt; 337 assert(dt.toISOExtString ~ "Z" == ts.toString); 338 assert(dt == cast(TimeOfDay) ts); 339 } 340 341 /// 342 this(DateTime)(const DateTime datetime) 343 if (DateTime.stringof == "DateTime") 344 { 345 with(datetime) this(year, cast(ubyte)month, day, hour, minute, second); 346 } 347 348 /// 349 version (mir_test) 350 @safe unittest { 351 import std.datetime.date : DateTime; 352 auto dt = DateTime(1982, 4, 1, 20, 59, 22); 353 Timestamp ts = dt; 354 assert(dt.toISOExtString ~ "Z" == ts.toString); 355 assert(dt == cast(DateTime) ts); 356 } 357 358 /// 359 this(SysTime)(const SysTime systime) 360 if (SysTime.stringof == "SysTime") 361 { 362 with(systime.toUTC) this(year, month, day, hour, minute, second, -7, fracSecs.total!"hnsecs"); 363 offset = cast(short) systime.utcOffset.total!"minutes"; 364 } 365 366 /// 367 version (mir_test) 368 @safe unittest { 369 import core.time : hnsecs, minutes; 370 import std.datetime.date : DateTime; 371 import std.datetime.timezone : SimpleTimeZone; 372 import std.datetime.systime : SysTime; 373 374 auto dt = DateTime(1982, 4, 1, 20, 59, 22); 375 auto tz = new immutable SimpleTimeZone(-330.minutes); 376 auto st = SysTime(dt, 1234567.hnsecs, tz); 377 Timestamp ts = st; 378 379 assert(st.toISOExtString == ts.toString); 380 assert(st == cast(SysTime) ts); 381 } 382 383 /// 384 T opCast(T)() const 385 if (T.stringof == "YearMonth" 386 || T.stringof == "YearMonthDay" 387 || T.stringof == "Date" 388 || T.stringof == "TimeOfDay" 389 || T.stringof == "date" 390 || T.stringof == "DateTime" 391 || T.stringof == "SysTime") 392 { 393 static if (T.stringof == "YearMonth") 394 { 395 return T(year, month, day); 396 } 397 else 398 static if (T.stringof == "Date" || T.stringof == "date" || T.stringof == "YearMonthDay") 399 { 400 return T(year, month, day); 401 } 402 else 403 static if (T.stringof == "DateTime") 404 { 405 return T(year, month, day, hour, minute, second); 406 } 407 else 408 static if (T.stringof == "TimeOfDay") 409 { 410 return T(hour, minute, second); 411 } 412 else 413 static if (T.stringof == "SysTime") 414 { 415 import core.time : hnsecs, minutes; 416 import std.datetime.date: DateTime; 417 import std.datetime.systime: SysTime; 418 import std.datetime.timezone: UTC, SimpleTimeZone; 419 auto ret = SysTime(DateTime(year, month, day, hour, minute, second), UTC()); 420 if (fractionCoefficient) 421 { 422 long coeff = fractionCoefficient; 423 int exp = fractionExponent; 424 while (exp > -7) 425 { 426 exp--; 427 coeff *= 10; 428 } 429 while (exp < -7) 430 { 431 exp++; 432 coeff /= 10; 433 } 434 ret.fracSecs = coeff.hnsecs; 435 } 436 if (offset) 437 { 438 ret = ret.toOtherTZ(new immutable SimpleTimeZone(offset.minutes)); 439 } 440 return ret; 441 } 442 } 443 444 /++ 445 Returns: true if timestamp represent a time only value. 446 +/ 447 bool isOnlyTime() @property const @safe pure nothrow @nogc 448 { 449 return precision > Precision.day && day == 0; 450 } 451 452 /// 453 int opCmp(Timestamp rhs) const @safe pure nothrow @nogc 454 { 455 import std.meta: AliasSeq; 456 static foreach (member; [ 457 "year", 458 "month", 459 "day", 460 "hour", 461 "minute", 462 "second", 463 ]) 464 if (auto d = int(__traits(getMember, this, member)) - int(__traits(getMember, rhs, member))) 465 return d; 466 int frel = this.fractionExponent; 467 int frer = rhs.fractionExponent; 468 ulong frcl = this.fractionCoefficient; 469 ulong frcr = rhs.fractionCoefficient; 470 while(frel > frer) 471 { 472 frel--; 473 frcl *= 10; 474 } 475 while(frer > frel) 476 { 477 frer--; 478 frcr *= 10; 479 } 480 if (frcl < frcr) return -1; 481 if (frcl > frcr) return +1; 482 if (auto d = int(this.fractionExponent) - int(rhs.fractionExponent)) 483 return d; 484 return int(this.offset) - int(rhs.offset); 485 } 486 487 /++ 488 Attaches local offset, doesn't adjust other fields. 489 Local-time offsets may be represented as either `hour*60+minute` offsets from UTC, 490 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. 491 +/ 492 @safe pure nothrow @nogc const 493 Timestamp withOffset(short minutes) 494 { 495 assert(-24 * 60 <= minutes && minutes <= 24 * 60, "Offset absolute value should be less or equal to 24 * 60"); 496 assert(precision >= Precision.minute, "Offsets are not allowed on date values."); 497 Timestamp ret = this; 498 ret.offset = minutes; 499 return ret; 500 } 501 502 version(D_BetterC){} else 503 private string toStringImpl(alias fun)() const @safe pure nothrow 504 { 505 import mir.appender: UnsafeArrayBuffer; 506 char[64] buffer = void; 507 auto w = UnsafeArrayBuffer!char(buffer); 508 fun(w); 509 return w.data.idup; 510 } 511 512 /++ 513 Converts this $(LREF Timestamp) to a string with the format `YYYY-MM-DDThh:mm:ss±hh:mm`. 514 515 If `w` writer is set, the resulting string will be written directly 516 to it. 517 518 Returns: 519 A `string` when not using an output range; `void` otherwise. 520 +/ 521 alias toString = toISOExtString; 522 523 /// 524 version (mir_test) 525 @safe pure nothrow unittest 526 { 527 assert(Timestamp.init.toString == "0000T"); 528 assert(Timestamp(2010, 7, 4).toString == "2010-07-04"); 529 assert(Timestamp(1998, 12, 25).toString == "1998-12-25"); 530 assert(Timestamp(0, 1, 5).toString == "0000-01-05"); 531 assert(Timestamp(-4, 1, 5).toString == "-0004-01-05"); 532 533 // YYYY-MM-DDThh:mm:ss±hh:mm 534 assert(Timestamp(2021).toString == "2021T"); 535 assert(Timestamp(2021, 01).toString == "2021-01T", Timestamp(2021, 01).toString); 536 assert(Timestamp(2021, 01, 29).toString == "2021-01-29"); 537 assert(Timestamp(2021, 01, 29, 19, 42).toString == "2021-01-29T19:42Z"); 538 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60).toString == "2021-01-29T19:42:44+07", Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60).toString); 539 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30).toString == "2021-01-29T20:12:44+07:30"); 540 541 assert(Timestamp.onlyTime(7, 40).toString == "07:40Z"); 542 assert(Timestamp.onlyTime(7, 40, 30).toString == "07:40:30Z"); 543 assert(Timestamp.onlyTime(7, 40, 30, -3, 56).toString == "07:40:30.056Z"); 544 } 545 546 /// 547 version (mir_test) 548 @safe unittest 549 { 550 // Test A.D. 551 assert(Timestamp(9, 12, 4).toISOExtString == "0009-12-04"); 552 assert(Timestamp(99, 12, 4).toISOExtString == "0099-12-04"); 553 assert(Timestamp(999, 12, 4).toISOExtString == "0999-12-04"); 554 assert(Timestamp(9999, 7, 4).toISOExtString == "9999-07-04"); 555 assert(Timestamp(10000, 10, 20).toISOExtString == "+10000-10-20"); 556 557 // Test B.C. 558 assert(Timestamp(0, 12, 4).toISOExtString == "0000-12-04"); 559 assert(Timestamp(-9, 12, 4).toISOExtString == "-0009-12-04"); 560 assert(Timestamp(-99, 12, 4).toISOExtString == "-0099-12-04"); 561 assert(Timestamp(-999, 12, 4).toISOExtString == "-0999-12-04"); 562 assert(Timestamp(-9999, 7, 4).toISOExtString == "-9999-07-04"); 563 assert(Timestamp(-10000, 10, 20).toISOExtString == "-10000-10-20"); 564 565 assert(Timestamp.onlyTime(7, 40).toISOExtString == "07:40Z"); 566 assert(Timestamp.onlyTime(7, 40, 30).toISOExtString == "07:40:30Z"); 567 assert(Timestamp.onlyTime(7, 40, 30, -3, 56).toISOExtString == "07:40:30.056Z"); 568 569 const cdate = Timestamp(1999, 7, 6); 570 immutable idate = Timestamp(1999, 7, 6); 571 assert(cdate.toISOExtString == "1999-07-06"); 572 assert(idate.toISOExtString == "1999-07-06"); 573 } 574 575 /// ditto 576 alias toISOExtString = toISOStringImp!true; 577 578 /++ 579 Converts this $(LREF Timestamp) to a string with the format `YYYYMMDDThhmmss±hhmm`. 580 581 If `w` writer is set, the resulting string will be written directly 582 to it. 583 584 Returns: 585 A `string` when not using an output range; `void` otherwise. 586 +/ 587 alias toISOString = toISOStringImp!false; 588 589 /// 590 version (mir_test) 591 @safe pure nothrow unittest 592 { 593 assert(Timestamp.init.toISOString == "0000T"); 594 assert(Timestamp(2010, 7, 4).toISOString == "20100704"); 595 assert(Timestamp(1998, 12, 25).toISOString == "19981225"); 596 assert(Timestamp(0, 1, 5).toISOString == "00000105"); 597 assert(Timestamp(-4, 1, 5).toISOString == "-00040105"); 598 599 // YYYYMMDDThhmmss±hhmm 600 assert(Timestamp(2021).toISOString == "2021T"); 601 assert(Timestamp(2021, 01).toISOString == "2021-01T"); // always extended 602 assert(Timestamp(2021, 01, 29).toISOString == "20210129"); 603 assert(Timestamp(2021, 01, 29, 19, 42).toISOString == "20210129T1942Z"); 604 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60).toISOString == "20210129T194244+07"); 605 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30).toISOString == "20210129T201244+0730"); 606 static assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30).toISOString == "20210129T201244+0730"); 607 608 assert(Timestamp.onlyTime(7, 40).toISOString == "T0740Z"); 609 assert(Timestamp.onlyTime(7, 40, 30).toISOString == "T074030Z"); 610 assert(Timestamp.onlyTime(7, 40, 30, -3, 56).toISOString == "T074030.056Z"); 611 } 612 613 /// Helpfer for time zone offsets 614 void addMinutes(short minutes) @safe pure nothrow @nogc 615 { 616 int totalMinutes = minutes + (this.minute + this.hour * 60u); 617 auto h = totalMinutes / 60; 618 619 int dayShift; 620 621 while (totalMinutes < 0) 622 { 623 totalMinutes += 24 * 60; 624 dayShift--; 625 } 626 627 while (totalMinutes >= 24 * 60) 628 { 629 totalMinutes -= 24 * 60; 630 dayShift++; 631 } 632 633 if (dayShift) 634 { 635 import mir.date: Date; 636 auto ymd = (Date.trustedCreate(year, month, day) + dayShift).yearMonthDay; 637 year = ymd.year; 638 month = cast(ubyte)ymd.month; 639 day = ymd.day; 640 } 641 642 hour = cast(ubyte) (totalMinutes / 60); 643 minute = cast(ubyte) (totalMinutes % 60); 644 } 645 646 template toISOStringImp(bool ext) 647 { 648 version(D_BetterC){} else 649 string toISOStringImp() const @safe pure nothrow 650 { 651 return toStringImpl!toISOStringImp; 652 } 653 654 /// ditto 655 void toISOStringImp(W)(scope ref W w) const scope 656 // if (isOutputRange!(W, char)) 657 { 658 import mir.format: printZeroPad; 659 // YYYY-MM-DDThh:mm:ss±hh:mm 660 Timestamp t = this; 661 662 if (t.offset) 663 { 664 assert(-24 * 60 <= t.offset && t.offset <= 24 * 60, "Offset absolute value should be less or equal to 24 * 60"); 665 assert(precision >= Precision.minute, "Offsets are not allowed on date values."); 666 t.addMinutes(t.offset); 667 } 668 669 if (!t.isOnlyTime) 670 { 671 if (t.year >= 10_000) 672 w.put('+'); 673 printZeroPad(w, t.year, t.year >= 0 ? t.year < 10_000 ? 4 : 5 : t.year > -10_000 ? 5 : 6); 674 if (precision == Precision.year) 675 { 676 w.put('T'); 677 return; 678 } 679 if (ext || precision == Precision.month) w.put('-'); 680 681 printZeroPad(w, cast(uint)t.month, 2); 682 if (precision == Precision.month) 683 { 684 w.put('T'); 685 return; 686 } 687 static if (ext) w.put('-'); 688 689 printZeroPad(w, t.day, 2); 690 if (precision == Precision.day) 691 return; 692 } 693 694 if (!ext || !t.isOnlyTime) 695 w.put('T'); 696 697 printZeroPad(w, t.hour, 2); 698 static if (ext) w.put(':'); 699 printZeroPad(w, t.minute, 2); 700 701 if (precision >= Precision.second) 702 { 703 static if (ext) w.put(':'); 704 printZeroPad(w, t.second, 2); 705 706 if (precision > Precision.second && (t.fractionExponent < 0 || t.fractionCoefficient)) 707 { 708 w.put('.'); 709 printZeroPad(w, t.fractionCoefficient, -int(t.fractionExponent)); 710 } 711 } 712 713 if (t.offset == 0) 714 { 715 w.put('Z'); 716 return; 717 } 718 719 bool sign = t.offset < 0; 720 uint absoluteOffset = !sign ? t.offset : -int(t.offset); 721 uint offsetHour = absoluteOffset / 60u; 722 uint offsetMinute = absoluteOffset % 60u; 723 724 w.put(sign ? '-' : '+'); 725 printZeroPad(w, offsetHour, 2); 726 if (offsetMinute) 727 { 728 static if (ext) w.put(':'); 729 printZeroPad(w, offsetMinute, 2); 730 } 731 } 732 } 733 734 /++ 735 Creates a $(LREF Timestamp) from a string with the format `YYYYMMDDThhmmss±hhmm 736 or its leading part allowed by the standard. 737 738 or its leading part allowed by the standard. 739 740 Params: 741 str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) formats dates. 742 value = (optional) result value. 743 744 Throws: 745 $(LREF DateTimeException) if the given string is 746 not in the correct format. Two arguments overload is `nothrow`. 747 Returns: 748 `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload. 749 +/ 750 alias fromISOString = fromISOStringImpl!false; 751 752 /// 753 version (mir_test) 754 @safe unittest 755 { 756 assert(Timestamp.fromISOString("20100704") == Timestamp(2010, 7, 4)); 757 assert(Timestamp.fromISOString("19981225") == Timestamp(1998, 12, 25)); 758 assert(Timestamp.fromISOString("00000105") == Timestamp(0, 1, 5)); 759 // assert(Timestamp.fromISOString("-00040105") == Timestamp(-4, 1, 5)); 760 761 assert(Timestamp(2021) == Timestamp.fromISOString("2021")); 762 assert(Timestamp(2021) == Timestamp.fromISOString("2021T")); 763 // assert(Timestamp(2021, 01) == Timestamp.fromISOString("2021-01")); 764 // assert(Timestamp(2021, 01) == Timestamp.fromISOString("2021-01T")); 765 assert(Timestamp(2021, 01, 29) == Timestamp.fromISOString("20210129")); 766 assert(Timestamp(2021, 01, 29, 19, 42) == Timestamp.fromISOString("20210129T1942")); 767 assert(Timestamp(2021, 01, 29, 19, 42) == Timestamp.fromISOString("20210129T1942Z")); 768 assert(Timestamp(2021, 01, 29, 19, 42, 12) == Timestamp.fromISOString("20210129T194212")); 769 assert(Timestamp(2021, 01, 29, 19, 42, 12, -3, 67) == Timestamp.fromISOString("20210129T194212.067Z")); 770 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60) == Timestamp.fromISOString("20210129T194244+07")); 771 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOString("20210129T201244+0730")); 772 static assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOString("20210129T201244+0730")); 773 static assert(Timestamp(2021, 01, 29, 4, 42, 44).withOffset(- (7 * 60 + 30)) == Timestamp.fromISOString("20210128T211244-0730")); 774 } 775 776 version (mir_test) 777 @safe unittest 778 { 779 import std.exception: assertThrown; 780 assertThrown!DateTimeException(Timestamp.fromISOString("")); 781 assertThrown!DateTimeException(Timestamp.fromISOString("990704")); 782 assertThrown!DateTimeException(Timestamp.fromISOString("0100704")); 783 assertThrown!DateTimeException(Timestamp.fromISOString("2010070")); 784 assertThrown!DateTimeException(Timestamp.fromISOString("120100704")); 785 assertThrown!DateTimeException(Timestamp.fromISOString("-0100704")); 786 assertThrown!DateTimeException(Timestamp.fromISOString("+0100704")); 787 assertThrown!DateTimeException(Timestamp.fromISOString("2010070a")); 788 assertThrown!DateTimeException(Timestamp.fromISOString("20100a04")); 789 assertThrown!DateTimeException(Timestamp.fromISOString("2010a704")); 790 791 assertThrown!DateTimeException(Timestamp.fromISOString("99-07-04")); 792 assertThrown!DateTimeException(Timestamp.fromISOString("010-07-04")); 793 assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-0")); 794 assertThrown!DateTimeException(Timestamp.fromISOString("12010-07-04")); 795 assertThrown!DateTimeException(Timestamp.fromISOString("-010-07-04")); 796 assertThrown!DateTimeException(Timestamp.fromISOString("+010-07-04")); 797 assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-0a")); 798 assertThrown!DateTimeException(Timestamp.fromISOString("2010-0a-04")); 799 assertThrown!DateTimeException(Timestamp.fromISOString("2010-a7-04")); 800 assertThrown!DateTimeException(Timestamp.fromISOString("2010/07/04")); 801 assertThrown!DateTimeException(Timestamp.fromISOString("2010/7/04")); 802 assertThrown!DateTimeException(Timestamp.fromISOString("2010/7/4")); 803 assertThrown!DateTimeException(Timestamp.fromISOString("2010/07/4")); 804 assertThrown!DateTimeException(Timestamp.fromISOString("2010-7-04")); 805 assertThrown!DateTimeException(Timestamp.fromISOString("2010-7-4")); 806 assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-4")); 807 808 assertThrown!DateTimeException(Timestamp.fromISOString("99Jul04")); 809 assertThrown!DateTimeException(Timestamp.fromISOString("010Jul04")); 810 assertThrown!DateTimeException(Timestamp.fromISOString("2010Jul0")); 811 assertThrown!DateTimeException(Timestamp.fromISOString("12010Jul04")); 812 assertThrown!DateTimeException(Timestamp.fromISOString("-010Jul04")); 813 assertThrown!DateTimeException(Timestamp.fromISOString("+010Jul04")); 814 assertThrown!DateTimeException(Timestamp.fromISOString("2010Jul0a")); 815 assertThrown!DateTimeException(Timestamp.fromISOString("2010Jua04")); 816 assertThrown!DateTimeException(Timestamp.fromISOString("2010aul04")); 817 818 assertThrown!DateTimeException(Timestamp.fromISOString("99-Jul-04")); 819 assertThrown!DateTimeException(Timestamp.fromISOString("010-Jul-04")); 820 assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jul-0")); 821 assertThrown!DateTimeException(Timestamp.fromISOString("12010-Jul-04")); 822 assertThrown!DateTimeException(Timestamp.fromISOString("-010-Jul-04")); 823 assertThrown!DateTimeException(Timestamp.fromISOString("+010-Jul-04")); 824 assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jul-0a")); 825 assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jua-04")); 826 assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jal-04")); 827 assertThrown!DateTimeException(Timestamp.fromISOString("2010-aul-04")); 828 829 // assertThrown!DateTimeException(Timestamp.fromISOString("2010-07-04")); 830 assertThrown!DateTimeException(Timestamp.fromISOString("2010-Jul-04")); 831 832 assert(Timestamp.fromISOString("19990706") == Timestamp(1999, 7, 6)); 833 // assert(Timestamp.fromISOString("-19990706") == Timestamp(-1999, 7, 6)); 834 // assert(Timestamp.fromISOString("+019990706") == Timestamp(1999, 7, 6)); 835 assert(Timestamp.fromISOString("19990706") == Timestamp(1999, 7, 6)); 836 } 837 838 // bug# 17801 839 version (mir_test) 840 @safe unittest 841 { 842 import std.conv : to; 843 import std.meta : AliasSeq; 844 static foreach (C; AliasSeq!(char, wchar, dchar)) 845 { 846 static foreach (S; AliasSeq!(C[], const(C)[], immutable(C)[])) 847 assert(Timestamp.fromISOString(to!S("20121221")) == Timestamp(2012, 12, 21)); 848 } 849 } 850 851 /++ 852 Creates a $(LREF Timestamp) from a string with the format `YYYY-MM-DDThh:mm:ss±hh:mm` 853 or its leading part allowed by the standard. 854 855 856 Params: 857 str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) formats dates. 858 value = (optional) result value. 859 860 Throws: 861 $(LREF DateTimeException) if the given string is 862 not in the correct format. Two arguments overload is `nothrow`. 863 Returns: 864 `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload. 865 +/ 866 alias fromISOExtString = fromISOStringImpl!true; 867 868 869 /// 870 version (mir_test) 871 @safe unittest 872 { 873 assert(Timestamp.fromISOExtString("2010-07-04") == Timestamp(2010, 7, 4)); 874 assert(Timestamp.fromISOExtString("1998-12-25") == Timestamp(1998, 12, 25)); 875 assert(Timestamp.fromISOExtString("0000-01-05") == Timestamp(0, 1, 5)); 876 assert(Timestamp.fromISOExtString("-0004-01-05") == Timestamp(-4, 1, 5)); 877 878 assert(Timestamp(2021) == Timestamp.fromISOExtString("2021")); 879 assert(Timestamp(2021) == Timestamp.fromISOExtString("2021T")); 880 assert(Timestamp(2021, 01) == Timestamp.fromISOExtString("2021-01")); 881 assert(Timestamp(2021, 01) == Timestamp.fromISOExtString("2021-01T")); 882 assert(Timestamp(2021, 01, 29) == Timestamp.fromISOExtString("2021-01-29")); 883 assert(Timestamp(2021, 01, 29, 19, 42) == Timestamp.fromISOExtString("2021-01-29T19:42")); 884 assert(Timestamp(2021, 01, 29, 19, 42) == Timestamp.fromISOExtString("2021-01-29T19:42Z")); 885 assert(Timestamp(2021, 01, 29, 19, 42, 12) == Timestamp.fromISOExtString("2021-01-29T19:42:12")); 886 assert(Timestamp(2021, 01, 29, 19, 42, 12, -3, 67) == Timestamp.fromISOExtString("2021-01-29T19:42:12.067Z")); 887 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60) == Timestamp.fromISOExtString("2021-01-29T19:42:44+07")); 888 assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOExtString("2021-01-29T20:12:44+07:30")); 889 static assert(Timestamp(2021, 01, 29, 12, 42, 44).withOffset(7 * 60 + 30) == Timestamp.fromISOExtString("2021-01-29T20:12:44+07:30")); 890 static assert(Timestamp(2021, 01, 29, 4, 42, 44).withOffset(- (7 * 60 + 30)) == Timestamp.fromISOExtString("2021-01-28T21:12:44-07:30")); 891 } 892 893 version (mir_test) 894 @safe unittest 895 { 896 import std.exception: assertThrown; 897 898 assertThrown!DateTimeException(Timestamp.fromISOExtString("")); 899 assertThrown!DateTimeException(Timestamp.fromISOExtString("990704")); 900 assertThrown!DateTimeException(Timestamp.fromISOExtString("0100704")); 901 assertThrown!DateTimeException(Timestamp.fromISOExtString("120100704")); 902 assertThrown!DateTimeException(Timestamp.fromISOExtString("-0100704")); 903 assertThrown!DateTimeException(Timestamp.fromISOExtString("+0100704")); 904 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010070a")); 905 assertThrown!DateTimeException(Timestamp.fromISOExtString("20100a04")); 906 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010a704")); 907 908 assertThrown!DateTimeException(Timestamp.fromISOExtString("99-07-04")); 909 assertThrown!DateTimeException(Timestamp.fromISOExtString("010-07-04")); 910 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-07-0")); 911 assertThrown!DateTimeException(Timestamp.fromISOExtString("12010-07-04")); 912 assertThrown!DateTimeException(Timestamp.fromISOExtString("-010-07-04")); 913 assertThrown!DateTimeException(Timestamp.fromISOExtString("+010-07-04")); 914 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-07-0a")); 915 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-0a-04")); 916 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-a7-04")); 917 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/07/04")); 918 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/7/04")); 919 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/7/4")); 920 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010/07/4")); 921 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-7-04")); 922 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-7-4")); 923 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-07-4")); 924 925 assertThrown!DateTimeException(Timestamp.fromISOExtString("99Jul04")); 926 assertThrown!DateTimeException(Timestamp.fromISOExtString("010Jul04")); 927 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010Jul0")); 928 assertThrown!DateTimeException(Timestamp.fromISOExtString("12010Jul04")); 929 assertThrown!DateTimeException(Timestamp.fromISOExtString("-010Jul04")); 930 assertThrown!DateTimeException(Timestamp.fromISOExtString("+010Jul04")); 931 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010Jul0a")); 932 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010Jua04")); 933 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010aul04")); 934 935 assertThrown!DateTimeException(Timestamp.fromISOExtString("99-Jul-04")); 936 assertThrown!DateTimeException(Timestamp.fromISOExtString("010-Jul-04")); 937 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jul-0")); 938 assertThrown!DateTimeException(Timestamp.fromISOExtString("12010-Jul-04")); 939 assertThrown!DateTimeException(Timestamp.fromISOExtString("-010-Jul-04")); 940 assertThrown!DateTimeException(Timestamp.fromISOExtString("+010-Jul-04")); 941 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jul-0a")); 942 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jua-04")); 943 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jal-04")); 944 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-aul-04")); 945 946 assertThrown!DateTimeException(Timestamp.fromISOExtString("20100704")); 947 assertThrown!DateTimeException(Timestamp.fromISOExtString("2010-Jul-04")); 948 949 assert(Timestamp.fromISOExtString("1999-07-06") == Timestamp(1999, 7, 6)); 950 assert(Timestamp.fromISOExtString("-1999-07-06") == Timestamp(-1999, 7, 6)); 951 assert(Timestamp.fromISOExtString("+01999-07-06") == Timestamp(1999, 7, 6)); 952 } 953 954 // bug# 17801 955 version (mir_test) 956 @safe unittest 957 { 958 import std.conv : to; 959 import std.meta : AliasSeq; 960 static foreach (C; AliasSeq!(char, wchar, dchar)) 961 { 962 static foreach (S; AliasSeq!(C[], const(C)[], immutable(C)[])) 963 assert(Timestamp.fromISOExtString(to!S("2012-12-21")) == Timestamp(2012, 12, 21)); 964 } 965 } 966 967 /++ 968 Creates a $(LREF Timestamp) from a string with the format YYYY-MM-DD, YYYYMMDD, or YYYY-Mon-DD. 969 970 Params: 971 str = A string formatted in the way that $(LREF .Timestamp.toISOExtString) and $(LREF .Timestamp.toISOString) format dates. The function is case sensetive. 972 value = (optional) result value. 973 974 Throws: 975 $(LREF DateTimeException) if the given string is 976 not in the correct format. Two arguments overload is `nothrow`. 977 Returns: 978 `bool` on success for two arguments overload, and the resulting timestamp for single argument overdload. 979 +/ 980 static bool fromString(C)(scope const(C)[] str, out Timestamp value) @safe pure nothrow @nogc 981 { 982 return fromISOExtString(str, value) 983 || fromISOString(str, value); 984 } 985 986 /// 987 version (mir_test) 988 @safe pure @nogc unittest 989 { 990 assert(Timestamp.fromString("2010-07-04") == Timestamp(2010, 7, 4)); 991 assert(Timestamp.fromString("20100704") == Timestamp(2010, 7, 4)); 992 } 993 994 /// ditto 995 static Timestamp fromString(C)(scope const(C)[] str) @safe pure 996 if (isSomeChar!C) 997 { 998 Timestamp ret; 999 if (fromString(str, ret)) 1000 return ret; 1001 throw InvalidString; 1002 } 1003 1004 template fromISOStringImpl(bool ext) 1005 { 1006 static Timestamp fromISOStringImpl(C)(scope const(C)[] str) @safe pure 1007 if (isSomeChar!C) 1008 { 1009 Timestamp ret; 1010 if (fromISOStringImpl(str, ret)) 1011 return ret; 1012 throw InvalidISOExtendedString; 1013 } 1014 1015 static bool fromISOStringImpl(C)(scope const(C)[] str, out Timestamp value) @safe pure nothrow @nogc 1016 if (isSomeChar!C) 1017 { 1018 import mir.parse: fromString, parse; 1019 1020 static if (ext) 1021 auto isOnlyTime = str.length >= 3 && (str[0] == 'T' || str[2] == ':'); 1022 else 1023 auto isOnlyTime = str.length >= 3 && str[0] == 'T'; 1024 1025 if (!isOnlyTime) 1026 { 1027 // YYYY 1028 static if (ext) 1029 {{ 1030 auto startIsDigit = str.length && str[0].isDigit; 1031 auto strOldLength = str.length; 1032 if (!parse(str, value.year)) 1033 return false; 1034 auto l = strOldLength - str.length; 1035 if ((l == 4) != startIsDigit) 1036 return false; 1037 }} 1038 else 1039 { 1040 if (str.length < 4 || !str[0].isDigit || !fromString(str[0 .. 4], value.year)) 1041 return false; 1042 str = str[4 .. $]; 1043 } 1044 1045 value.precision = Precision.year; 1046 if (str.length == 0 || str == "T") 1047 return true; 1048 1049 static if (ext) 1050 { 1051 if (str[0] != '-') 1052 return false; 1053 str = str[1 .. $]; 1054 } 1055 1056 // MM 1057 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], value.month)) 1058 return false; 1059 str = str[2 .. $]; 1060 value.precision = Precision.month; 1061 if (str.length == 0 || str == "T") 1062 return ext; 1063 1064 static if (ext) 1065 { 1066 if (str[0] != '-') 1067 return false; 1068 str = str[1 .. $]; 1069 } 1070 1071 // DD 1072 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], value.day)) 1073 return false; 1074 str = str[2 .. $]; 1075 value.precision = Precision.day; 1076 if (str.length == 0) 1077 return true; 1078 } 1079 1080 // str isn't empty here 1081 // T 1082 if (str[0] == 'T') 1083 { 1084 str = str[1 .. $]; 1085 // OK, onlyTime requires length >= 3 1086 if (str.length == 0) 1087 return true; 1088 } 1089 else 1090 { 1091 if (!(ext && isOnlyTime)) 1092 return false; 1093 } 1094 1095 value.precision = Precision.minute; // we don't have hour precision 1096 1097 // hh 1098 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], value.hour)) 1099 return false; 1100 str = str[2 .. $]; 1101 if (str.length == 0) 1102 return true; 1103 1104 static if (ext) 1105 { 1106 if (str[0] != ':') 1107 return false; 1108 str = str[1 .. $]; 1109 } 1110 1111 // mm 1112 { 1113 uint minute; 1114 if (str.length < 2 || !str[0].isDigit || !fromString(str[0 .. 2], minute)) 1115 return false; 1116 value.minute = cast(ubyte) minute; 1117 str = str[2 .. $]; 1118 if (str.length == 0) 1119 return true; 1120 } 1121 1122 static if (ext) 1123 { 1124 if (str[0] != ':') 1125 goto TZ; 1126 str = str[1 .. $]; 1127 } 1128 1129 // ss 1130 { 1131 uint second; 1132 if (str.length < 2 || !str[0].isDigit) 1133 goto TZ; 1134 if (!fromString(str[0 .. 2], second)) 1135 return false; 1136 value.second = cast(ubyte) second; 1137 str = str[2 .. $]; 1138 value.precision = Precision.second; 1139 if (str.length == 0) 1140 return true; 1141 } 1142 1143 // . 1144 if (str[0] != '.') 1145 goto TZ; 1146 str = str[1 .. $]; 1147 value.precision = Precision.fraction; 1148 1149 // fraction 1150 { 1151 const strOldLength = str.length; 1152 ulong fractionCoefficient; 1153 if (str.length < 1 || !str[0].isDigit || !parse!ulong(str, fractionCoefficient)) 1154 return false; 1155 sizediff_t fractionExponent = str.length - strOldLength; 1156 if (fractionExponent < -12) 1157 return false; 1158 value.fractionExponent = cast(byte)fractionExponent; 1159 value.fractionCoefficient = fractionCoefficient; 1160 if (str.length == 0) 1161 return true; 1162 } 1163 1164 TZ: 1165 1166 if (str == "Z") 1167 return true; 1168 1169 int hour; 1170 int minute; 1171 if (str.length < 3 || str[0].isDigit || !fromString(str[0 .. 3], hour)) 1172 return false; 1173 str = str[3 .. $]; 1174 1175 if (str.length) 1176 { 1177 static if (ext) 1178 { 1179 if (str[0] != ':') 1180 return false; 1181 str = str[1 .. $]; 1182 } 1183 if (str.length != 2 || !str[0].isDigit || !fromString(str[0 .. 2], minute)) 1184 return false; 1185 } 1186 1187 value.offset = cast(short)(hour * 60 + (hour < 0 ? -minute : minute)); 1188 value.addMinutes(cast(short)-int(value.offset)); 1189 return true; 1190 } 1191 } 1192 }