1 /** 2 * datefmt provides parsing and formatting for std.datetime objects. 3 * 4 * The format is taken from strftime: 5 * %a The abbreviated name of the day of the week. 6 * %A The full name of the day of the week. 7 * %b The abbreviated month name. 8 * %B The full month name. 9 * %C The century number (year/100) as a 2-digit integer. 10 * %d The day of the month as a decimal number (range 01 to 31). 11 * %e Like %d, the day of the month as a decimal number, but space padded. 12 * %f Fractional seconds. Will parse any precision and emit six decimal places. 13 * %F Equivalent to %Y-%m-%d (the ISO 8601 date format). 14 * %g Milliseconds of the second. 15 * %G Nanoseconds of the second. 16 * %h The hour as a decimal number using a 12-hour clock (range 01 to 12). 17 * %H The hour as a decimal number using a 24-hour clock (range 00 to 23). 18 * %I The hour as a decimal number using a 12-hour clock (range 00 to 23). 19 * %j The day of the year as a decimal number (range 001 to 366). 20 * %k The hour (24-hour clock) as a decimal number (range 0 to 23), space padded. 21 * %l The hour (12-hour clock) as a decimal number (range 1 to 12), space padded. 22 * %m The month as a decimal number (range 01 to 12). 23 * %M The minute as a decimal number (range 00 to 59). 24 * %p "AM" / "PM" (midnight is AM; noon is PM). 25 * %P "am" / "pm" (midnight is AM; noon is PM). 26 * %r Equivalent to "%I:%M:%S %p". 27 * %R Equivalent to "%H:%M". 28 * %s The number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC). 29 * %S The second as a decimal number (range 00 to 60). 30 * %T Equivalent to "%H:%M:%S". 31 * %u The day of the week as a decimal, range 1 to 7, Monday being 1 (formatting only). 32 * %V The ISO 8601 week number (formatting only). 33 * %w The day of the week as a decimal, range 0 to 6, Sunday being 0 (formatting only). 34 * %y The year as a decimal number without a century (range 00 to 99). 35 * %Y The year as a decimal number including the century, minimum 4 digits. 36 * %z The +hhmm or -hhmm numeric timezone (that is, the hour and minute offset from UTC). 37 * %Z The timezone name or abbreviation. Formatting only. 38 * %+ The numeric offset, or 'Z' for UTC. This is common with ISO8601 timestamps. 39 * %% A literal '%' character. 40 * 41 * Timezone support is awkward. When time formats contain a GMT offset, that is honored. Otherwise, 42 * datefmt recognizes a subset of the timezone names defined in RFC1123. 43 */ 44 module datefmt; 45 46 @safe: 47 48 import core.time; 49 import std.array; 50 import std.conv; 51 import std.datetime; 52 import std..string; 53 import std.utf : codeLength; 54 alias to = std.conv.to; 55 56 57 /** 58 * A Format is the platonic ideal of a specific format string. 59 * 60 * For datetime formats such as RFC1123 or ISO8601, there are many potential formats. 61 * Like '2017-01-01' is a valid ISO8601 date. So is '2017-01-01T15:31:00'. 62 * A Format object can describe all allomorphs of a format. 63 */ 64 struct Format 65 { 66 /// The canonical format, to be used when formatting a datetime. 67 string primaryFormat; 68 /// Other formats that count as part of this Format, used for parsing. 69 string[] formatOptions; 70 } 71 72 73 /** 74 * Format the given datetime with the given Format. 75 */ 76 string format(SysTime dt, const Format fmt) 77 { 78 return format(dt, fmt.primaryFormat); 79 } 80 81 /** 82 * Format the given datetime with the given format string. 83 */ 84 string format(SysTime dt, string formatString) 85 { 86 Appender!string ap; 87 bool inPercent; 88 foreach (i, c; formatString) 89 { 90 if (inPercent) 91 { 92 inPercent = false; 93 interpretIntoString(ap, dt, c); 94 } 95 else if (c == '%') 96 { 97 inPercent = true; 98 } 99 else 100 { 101 ap ~= c; 102 } 103 } 104 return ap.data; 105 } 106 107 108 /** 109 * Parse the given datetime string with the given format string. 110 * 111 * This tries rather hard to produce a reasonable result. If the format string doesn't describe an 112 * unambiguous point time, the result will be a date that satisfies the inputs and should generally 113 * be the earliest such date. However, that is not guaranteed. 114 * 115 * For instance: 116 * --- 117 * SysTime time = parse("%d", "21"); 118 * writeln(time); // 0000-01-21T00:00:00.000000Z 119 * --- 120 */ 121 SysTime parse( 122 string data, 123 const Format fmt, 124 immutable(TimeZone) defaultTimeZone = null, 125 bool allowTrailingData = false) 126 { 127 SysTime st; 128 foreach (f; fmt.formatOptions) 129 { 130 if (tryParse(data, cast(string)f, st, defaultTimeZone)) 131 { 132 return st; 133 } 134 } 135 return parse(data, fmt.primaryFormat, defaultTimeZone, allowTrailingData); 136 } 137 138 139 /** 140 * Parse the given datetime string with the given format string. 141 * 142 * This tries rather hard to produce a reasonable result. If the format string doesn't describe an 143 * unambiguous point time, the result will be a date that satisfies the inputs and should generally 144 * be the earliest such date. However, that is not guaranteed. 145 * 146 * For instance: 147 * --- 148 * SysTime time = parse("%d", "21"); 149 * writeln(time); // 0000-01-21T00:00:00.000000Z 150 * --- 151 */ 152 SysTime parse( 153 string data, 154 string formatString, 155 immutable(TimeZone) defaultTimeZone = null, 156 bool allowTrailingData = false) 157 { 158 auto a = Interpreter(data); 159 auto res = a.parse(formatString, defaultTimeZone); 160 if (res.error) 161 { 162 throw new Exception(res.error ~ " around " ~ res.remaining); 163 } 164 if (!allowTrailingData && res.remaining.length > 0) 165 { 166 throw new Exception("trailing data: " ~ res.remaining); 167 } 168 return res.dt; 169 } 170 171 /** 172 * Try to parse the input string according to the given pattern. 173 * 174 * Return: true to indicate success; false to indicate failure 175 */ 176 bool tryParse( 177 string data, 178 const Format fmt, 179 out SysTime dt, 180 immutable(TimeZone) defaultTimeZone = null) 181 { 182 foreach (f; fmt.formatOptions) 183 { 184 if (tryParse(data, cast(string)f, dt, defaultTimeZone)) 185 { 186 return true; 187 } 188 } 189 return false; 190 } 191 192 /** 193 * Try to parse the input string according to the given pattern. 194 * 195 * Return: true to indicate success; false to indicate failure 196 */ 197 bool tryParse( 198 string data, 199 string formatString, 200 out SysTime dt, 201 immutable(TimeZone) defaultTimeZone = null) 202 { 203 auto a = Interpreter(data); 204 auto res = a.parse(formatString, defaultTimeZone); 205 if (res.error) 206 { 207 return false; 208 } 209 dt = res.dt; 210 return true; 211 } 212 213 import std.algorithm : map, cartesianProduct, joiner; 214 import std.array : array; 215 216 /** 217 * A Format suitable for RFC1123 dates. 218 * 219 * For instance, `Sun, 02 Jan 2004 15:31:10 GMT` is a valid RFC1123 date. 220 */ 221 immutable Format RFC1123FORMAT = { 222 primaryFormat: "%a, %d %b %Y %H:%M:%S %Z", 223 formatOptions: 224 // According to the spec, day-of-week is optional. 225 // Timezone can be specified in a few ways. 226 // In the wild, we have a number of variants. Like the day of week can be abbreviated or 227 // full. Likewise with the month. 228 cartesianProduct( 229 ["%a, ", "%A, ", ""], 230 ["%d "], 231 ["%b", "%B"], 232 [" %Y %H:%M:%S "], 233 ["%Z", "%z", "%.%.%.", "%.%.", "%."] 234 ) 235 .map!(x => joiner([x.tupleof], "").array.to!string) 236 .array 237 }; 238 239 /** 240 * A Format suitable for ISO8601 dates. 241 * 242 * For instance, `2010-01-15 06:17:21.015Z` is a valid ISO8601 date. 243 */ 244 immutable Format ISO8601FORMAT = { 245 primaryFormat: "%Y-%m-%dT%H:%M:%S.%f%+", 246 formatOptions: [ 247 "%Y-%m-%dT%H:%M:%S.%f%+", 248 "%Y-%m-%d %H:%M:%S.%f%+", 249 "%Y-%m-%dT%H:%M:%S%+", 250 "%Y-%m-%d %H:%M:%S%+", 251 "%Y-%m-%d %H:%M:%S.%f", 252 "%Y-%m-%d %H:%M:%S", 253 "%Y-%m-%dT%H:%M:%S", 254 "%Y-%m-%d", 255 ] 256 }; 257 258 /** Parse an RFC1123 date. */ 259 SysTime parseRFC1123(string data, bool allowTrailingData = false) 260 { 261 return parse(data, RFC1123FORMAT, UTC(), allowTrailingData); 262 } 263 264 /** Produce an RFC1123 date string from a SysTime. */ 265 string toRFC1123(SysTime date) 266 { 267 return format(date.toUTC(), RFC1123FORMAT); 268 } 269 270 /** Parse an ISO8601 date. */ 271 SysTime parseISO8601(string data, bool allowTrailingData = false) 272 { 273 return parse(data, ISO8601FORMAT, UTC(), allowTrailingData); 274 } 275 276 /** Produce an ISO8601 date string from a SysTime. */ 277 string toISO8601(SysTime date) 278 { 279 return format(date.toUTC(), ISO8601FORMAT); 280 } 281 282 private: 283 284 285 immutable(TimeZone) utc; 286 shared static this() { utc = UTC(); } 287 288 enum weekdayNames = [ 289 "Sunday", 290 "Monday", 291 "Tuesday", 292 "Wednesday", 293 "Thursday", 294 "Friday", 295 "Saturday" 296 ]; 297 298 enum weekdayAbbrev = [ 299 "Sun", 300 "Mon", 301 "Tue", 302 "Wed", 303 "Thu", 304 "Fri", 305 "Sat" 306 ]; 307 308 enum monthNames = [ 309 "January", 310 "February", 311 "March", 312 "April", 313 "May", 314 "June", 315 "July", 316 "August", 317 "September", 318 "October", 319 "November", 320 "December", 321 ]; 322 323 enum monthAbbrev = [ 324 "Jan", 325 "Feb", 326 "Mar", 327 "Apr", 328 "May", 329 "Jun", 330 "Jul", 331 "Aug", 332 "Sep", 333 "Oct", 334 "Nov", 335 "Dec", 336 ]; 337 338 struct Result 339 { 340 SysTime dt; 341 string error; 342 string remaining; 343 string remainingFormat; 344 } 345 346 // TODO support wstring, dstring 347 struct Interpreter 348 { 349 this(string data) 350 { 351 this.data = data; 352 } 353 string data; 354 355 int year; 356 int century; 357 int yearOfCentury; 358 Month month; 359 int dayOfWeek; 360 int dayOfMonth; 361 int dayOfYear; 362 int isoWeek; 363 int hour12; 364 int hour24; 365 int hour; 366 int minute; 367 int second; 368 int nanosecond; 369 int weekNumber; 370 import std.typecons : Nullable; 371 Nullable!Duration tzOffset; 372 string tzAbbreviation; 373 string tzName; 374 long epochSecond; 375 enum AMPM { AM, PM, None }; 376 AMPM amPm = AMPM.None; 377 Duration fracSecs; 378 379 import std.typecons : Rebindable; 380 Rebindable!(immutable(TimeZone)) tz; 381 382 Result parse(string formatString, immutable(TimeZone) defaultTimeZone) 383 { 384 tz = defaultTimeZone is null ? utc : defaultTimeZone; 385 bool inPercent; 386 foreach (size_t i, dchar c; formatString) 387 { 388 if (inPercent) 389 { 390 inPercent = false; 391 if (!interpretFromString(c)) 392 { 393 auto remainder = data; 394 if (remainder.length > 15) 395 { 396 remainder = remainder[0..15]; 397 } 398 return Result(SysTime.init, "unexpected value", data, formatString[i..$]); 399 } 400 } 401 else if (c == '%') 402 { 403 inPercent = true; 404 } 405 else 406 { 407 // TODO non-ASCII 408 auto b = data; 409 bool endedEarly = false; 410 foreach (size_t j, dchar dc; b) 411 { 412 data = b[j..$]; 413 if (j > 0) 414 { 415 endedEarly = true; 416 break; 417 } 418 if (c != dc) 419 { 420 return Result(SysTime.init, "unexpected literal", data, formatString[j..$]); 421 } 422 } 423 if (!endedEarly) data = ""; 424 } 425 } 426 427 if (!year) 428 { 429 year = century * 100 + yearOfCentury; 430 } 431 if (hour12) 432 { 433 if (amPm == AMPM.PM) 434 { 435 hour24 = (hour12 + 12) % 24; 436 } 437 else 438 { 439 hour24 = hour12; 440 } 441 } 442 auto dt = SysTime(DateTime(year, month, dayOfMonth, hour24, minute, second), getTimezone); 443 dt += fracSecs; 444 return Result(dt, null, data); 445 } 446 447 private immutable(TimeZone) getTimezone() 448 { 449 if (tzOffset.isNull) return tz; 450 auto off = tzOffset.get; 451 if (off == 0.seconds) return UTC(); 452 return new immutable SimpleTimeZone(off); 453 } 454 455 bool interpretFromString(dchar c) 456 { 457 switch (c) 458 { 459 case '.': 460 // TODO unicodes 461 if (data.length >= 1) 462 { 463 data = data[1..$]; 464 return true; 465 } 466 return false; 467 case 'a': 468 foreach (i, m; weekdayAbbrev) 469 { 470 if (data.startsWith(m)) 471 { 472 data = data[m.length .. $]; 473 return true; 474 } 475 } 476 return false; 477 case 'A': 478 foreach (i, m; weekdayNames) 479 { 480 if (data.startsWith(m)) 481 { 482 data = data[m.length .. $]; 483 return true; 484 } 485 } 486 return false; 487 case 'b': 488 foreach (i, m; monthAbbrev) 489 { 490 if (data.startsWith(m)) 491 { 492 month = cast(Month)(i + 1); 493 data = data[m.length .. $]; 494 return true; 495 } 496 } 497 return false; 498 case 'B': 499 foreach (i, m; monthNames) 500 { 501 if (data.startsWith(m)) 502 { 503 month = cast(Month)(i + 1); 504 data = data[m.length .. $]; 505 return true; 506 } 507 } 508 return false; 509 case 'C': 510 return parseInt!(x => century = x)(data); 511 case 'd': 512 return parseInt!(x => dayOfMonth = x)(data); 513 case 'e': 514 return parseInt!(x => dayOfMonth = x)(data); 515 case 'g': 516 return parseInt!(x => fracSecs = x.msecs)(data); 517 case 'G': 518 return parseInt!(x => fracSecs = x.nsecs)(data); 519 case 'f': 520 size_t end = data.length; 521 foreach (i, cc; data) 522 { 523 if ('0' > cc || '9' < cc) 524 { 525 end = i; 526 break; 527 } 528 } 529 auto fss = data[0..end]; 530 data = data[end..$]; 531 if (fss.length == 0) 532 { 533 return false; 534 } 535 auto fs = fss.to!ulong; 536 while (end < 7) 537 { 538 end++; 539 fs *= 10; 540 } 541 while (end > 7) 542 { 543 end--; 544 fs /= 10; 545 } 546 this.fracSecs = fs.hnsecs; 547 return true; 548 case 'F': 549 auto dash1 = data.indexOf('-'); 550 if (dash1 <= 0) return false; 551 if (dash1 >= data.length - 1) return false; 552 auto yearStr = data[0..dash1]; 553 auto year = yearStr.to!int; 554 data = data[dash1 + 1 .. $]; 555 556 if (data.length < 5) 557 { 558 // Month is 2 digits; day is 2 digits; dash between 559 return false; 560 } 561 if (data[2] != '-') 562 { 563 return false; 564 } 565 if (!parseInt!(x => month = cast(Month)x)(data)) return false; 566 if (!data.startsWith("-")) return false; 567 data = data[1..$]; 568 return parseInt!(x => dayOfMonth = x)(data); 569 case 'H': 570 case 'k': 571 auto h = parseInt!(x => hour24 = x)(data); 572 return h; 573 case 'h': 574 case 'I': 575 case 'l': 576 return parseInt!(x => hour12 = x)(data); 577 case 'j': 578 return parseInt!(x => dayOfYear = x, 3)(data); 579 case 'm': 580 return parseInt!(x => month = cast(Month)x)(data); 581 case 'M': 582 return parseInt!(x => minute = x)(data); 583 case 'p': 584 if (data.startsWith("AM")) 585 { 586 amPm = AMPM.AM; 587 } 588 else if (data.startsWith("PM")) 589 { 590 amPm = AMPM.PM; 591 } 592 else 593 { 594 return false; 595 } 596 return true; 597 case 'P': 598 if (data.startsWith("am")) 599 { 600 amPm = AMPM.AM; 601 } 602 else if (data.startsWith("pm")) 603 { 604 amPm = AMPM.PM; 605 } 606 else 607 { 608 return false; 609 } 610 return true; 611 case 'r': 612 return interpretFromString('I') && 613 pop(':') && 614 interpretFromString('M') && 615 pop(':') && 616 interpretFromString('S') && 617 pop(' ') && 618 interpretFromString('p'); 619 case 'R': 620 return interpretFromString('H') && 621 pop(':') && 622 interpretFromString('M'); 623 case 's': 624 size_t end = 0; 625 foreach (i2, c2; data) 626 { 627 if (c2 < '0' || c2 > '9') 628 { 629 end = cast()i2; 630 break; 631 } 632 } 633 if (end == 0) return false; 634 epochSecond = data[0..end].to!int; 635 data = data[end..$]; 636 return true; 637 case 'S': 638 return parseInt!(x => second = x)(data); 639 case 'T': 640 return interpretFromString('H') && 641 pop(':') && 642 interpretFromString('M') && 643 pop(':') && 644 interpretFromString('S'); 645 case 'u': 646 return parseInt!(x => dayOfWeek = cast(DayOfWeek)(x % 7))(data); 647 case 'V': 648 return parseInt!(x => isoWeek = x)(data); 649 case 'y': 650 return parseInt!(x => yearOfCentury = x)(data); 651 case 'Y': 652 size_t end = 0; 653 foreach (i2, c2; data) 654 { 655 if (c2 < '0' || c2 > '9') 656 { 657 end = i2; 658 break; 659 } 660 } 661 if (end == 0) return false; 662 year = data[0..end].to!int; 663 data = data[end..$]; 664 return true; 665 case 'z': 666 case '+': 667 if (pop('Z')) // for ISO8601 668 { 669 tzOffset = 0.seconds; 670 return true; 671 } 672 673 int sign = 0; 674 if (pop('-')) 675 { 676 sign = -1; 677 } 678 else if (pop('+')) 679 { 680 sign = 1; 681 } 682 else 683 { 684 return false; 685 } 686 int hour, minute; 687 parseInt!(x => hour = x)(data); 688 parseInt!(x => minute = x)(data); 689 tzOffset = dur!"minutes"(sign * (minute + 60 * hour)); 690 return true; 691 case 'Z': 692 foreach (i, v; canonicalZones) 693 { 694 if (data.startsWith(v.name)) 695 { 696 tz = canonicalZones[i].zone; 697 break; 698 } 699 } 700 return false; 701 default: 702 throw new Exception("unrecognized control character %" ~ c.to!string); 703 } 704 } 705 706 bool pop(dchar c) 707 { 708 if (data.startsWith(c)) 709 { 710 data = data[c.codeLength!char .. $]; 711 return true; 712 } 713 return false; 714 } 715 } 716 717 struct CanonicalZone 718 { 719 string name; 720 immutable(TimeZone) zone; 721 } 722 // array so we can control iteration order -- longest first 723 CanonicalZone[] canonicalZones; 724 shared static this() 725 { 726 version (Posix) 727 { 728 auto utc = PosixTimeZone.getTimeZone("Etc/UTC"); 729 auto est = PosixTimeZone.getTimeZone("America/New_York"); 730 auto cst = PosixTimeZone.getTimeZone("America/Boise"); 731 auto mst = PosixTimeZone.getTimeZone("America/Chicago"); 732 auto pst = PosixTimeZone.getTimeZone("America/Los_Angeles"); 733 } 734 else 735 { 736 auto utc = WindowsTimeZone.getTimeZone("UTC"); 737 auto est = WindowsTimeZone.getTimeZone("Eastern Standard Time"); 738 auto cst = WindowsTimeZone.getTimeZone("Central Standard Time"); 739 auto mst = WindowsTimeZone.getTimeZone("Mountain Standard Time"); 740 auto pst = WindowsTimeZone.getTimeZone("Pacific Standard Time"); 741 } 742 canonicalZones = 743 [ 744 // TODO ensure the MDT style variants prefer the daylight time version of the date 745 CanonicalZone("UTC", utc), 746 CanonicalZone("UT", utc), 747 CanonicalZone("Z", utc), 748 CanonicalZone("GMT", utc), 749 CanonicalZone("EST", est), 750 CanonicalZone("EDT", est), 751 CanonicalZone("CST", cst), 752 CanonicalZone("CDT", cst), 753 CanonicalZone("MST", mst), 754 CanonicalZone("MDT", mst), 755 CanonicalZone("PST", pst), 756 CanonicalZone("PDT", pst), 757 ]; 758 } 759 760 bool parseInt(alias setter, int length = 2)(ref string data) 761 { 762 if (data.length < length) 763 { 764 return false; 765 } 766 auto c = data[0..length].strip; 767 data = data[length..$]; 768 int v; 769 try 770 { 771 v = c.to!int; 772 } 773 catch (ConvException e) 774 { 775 return false; 776 } 777 cast(void)setter(c.to!int); 778 return true; 779 } 780 781 void interpretIntoString(ref Appender!string ap, SysTime dt, char c) 782 { 783 static import std.format; 784 switch (c) 785 { 786 case 'a': 787 ap ~= weekdayAbbrev[cast(size_t)dt.dayOfWeek]; 788 return; 789 case 'A': 790 ap ~= weekdayNames[cast(size_t)dt.dayOfWeek]; 791 return; 792 case 'b': 793 ap ~= monthAbbrev[cast(size_t)dt.month - 1]; 794 return; 795 case 'B': 796 ap ~= monthNames[cast(size_t)dt.month - 1]; 797 return; 798 case 'C': 799 ap ~= (dt.year / 100).to!string; 800 return; 801 case 'd': 802 auto s = dt.day.to!string; 803 if (s.length == 1) 804 { 805 ap ~= "0"; 806 } 807 ap ~= s; 808 return; 809 case 'e': 810 auto s = dt.day.to!string; 811 if (s.length == 1) 812 { 813 ap ~= " "; 814 } 815 ap ~= s; 816 return; 817 case 'f': 818 ap ~= std.format.format("%06d", dt.fracSecs.total!"usecs"); 819 return; 820 case 'g': 821 ap ~= std.format.format("%03d", dt.fracSecs.total!"msecs"); 822 return; 823 case 'G': 824 ap ~= std.format.format("%09d", dt.fracSecs.total!"nsecs"); 825 return; 826 case 'F': 827 interpretIntoString(ap, dt, 'Y'); 828 ap ~= '-'; 829 interpretIntoString(ap, dt, 'm'); 830 ap ~= '-'; 831 interpretIntoString(ap, dt, 'd'); 832 return; 833 case 'h': 834 case 'I': 835 auto h = dt.hour; 836 if (h == 0) 837 { 838 h = 12; 839 } 840 else if (h > 12) 841 { 842 h -= 12; 843 } 844 ap.pad(h.to!string, '0', 2); 845 return; 846 case 'H': 847 ap.pad(dt.hour.to!string, '0', 2); 848 return; 849 case 'j': 850 ap.pad(dt.dayOfYear.to!string, '0', 3); 851 return; 852 case 'k': 853 ap.pad(dt.hour.to!string, ' ', 2); 854 return; 855 case 'l': 856 auto h = dt.hour; 857 if (h == 0) 858 { 859 h = 12; 860 } 861 else if (h > 12) 862 { 863 h -= 12; 864 } 865 ap.pad(h.to!string, ' ', 2); 866 return; 867 case 'm': 868 uint m = cast(uint)dt.month; 869 ap.pad(m.to!string, '0', 2); 870 return; 871 case 'M': 872 ap.pad(dt.minute.to!string, '0', 2); 873 return; 874 case 'p': 875 if (dt.hour >= 12) 876 { 877 ap ~= "PM"; 878 } 879 else 880 { 881 ap ~= "AM"; 882 } 883 return; 884 case 'P': 885 if (dt.hour >= 12) 886 { 887 ap ~= "pm"; 888 } 889 else 890 { 891 ap ~= "am"; 892 } 893 return; 894 case 'r': 895 interpretIntoString(ap, dt, 'I'); 896 ap ~= ':'; 897 interpretIntoString(ap, dt, 'M'); 898 ap ~= ':'; 899 interpretIntoString(ap, dt, 'S'); 900 ap ~= ' '; 901 interpretIntoString(ap, dt, 'p'); 902 return; 903 case 'R': 904 interpretIntoString(ap, dt, 'H'); 905 ap ~= ':'; 906 interpretIntoString(ap, dt, 'M'); 907 return; 908 case 's': 909 auto delta = dt - SysTime(DateTime(1970, 1, 1), UTC()); 910 ap ~= delta.total!"seconds"().to!string; 911 return; 912 case 'S': 913 ap.pad(dt.second.to!string, '0', 2); 914 return; 915 case 'T': 916 interpretIntoString(ap, dt, 'H'); 917 ap ~= ':'; 918 interpretIntoString(ap, dt, 'M'); 919 ap ~= ':'; 920 interpretIntoString(ap, dt, 'S'); 921 return; 922 case 'u': 923 auto dow = cast(uint)dt.dayOfWeek; 924 if (dow == 0) dow = 7; 925 ap ~= dow.to!string; 926 return; 927 case 'w': 928 ap ~= (cast(uint)dt.dayOfWeek).to!string; 929 return; 930 case 'y': 931 ap.pad((dt.year % 100).to!string, '0', 2); 932 return; 933 case 'Y': 934 ap.pad(dt.year.to!string, '0', 4); 935 return; 936 case '+': 937 if (dt.utcOffset == dur!"seconds"(0)) 938 { 939 ap ~= 'Z'; 940 return; 941 } 942 // If it's not UTC, format as +HHMM 943 goto case; 944 case 'z': 945 import std.math : abs; 946 auto d = dt.utcOffset; 947 if (d < dur!"seconds"(0)) 948 { 949 ap ~= '-'; 950 } 951 else 952 { 953 ap ~= '+'; 954 } 955 auto minutes = abs(d.total!"minutes"); 956 ap.pad((minutes / 60).to!string, '0', 2); 957 ap.pad((minutes % 60).to!string, '0', 2); 958 return; 959 case 'Z': 960 if (dt.timezone is null || dt.timezone.isUTC()) 961 { 962 ap ~= 'Z'; 963 } 964 if (dt.dstInEffect) 965 { 966 ap ~= dt.timezone.stdName; 967 } 968 else 969 { 970 ap ~= dt.timezone.dstName; 971 } 972 return; 973 case '%': 974 ap ~= '%'; 975 return; 976 default: 977 throw new Exception("format element %" ~ c ~ " not recognized"); 978 } 979 } 980 981 private bool isUTC(const TimeZone zone) @trusted 982 { 983 return zone == UTC(); 984 } 985 986 void pad(ref Appender!string ap, string s, char pad, uint length) 987 { 988 if (s.length >= length) 989 { 990 ap ~= s; 991 return; 992 } 993 for (uint i = 0; i < length - s.length; i++) 994 { 995 ap ~= pad; 996 } 997 ap ~= s; 998 } 999 1000 unittest 1001 { 1002 import std.stdio; 1003 auto dt = SysTime( 1004 DateTime(2017, 5, 3, 14, 31, 57), 1005 UTC()); 1006 auto isoishFmt = "%Y-%m-%d %H:%M:%S %z"; 1007 auto isoish = dt.format(isoishFmt); 1008 assert(isoish == "2017-05-03 14:31:57 +0000", isoish); 1009 auto parsed = isoish.parse(isoishFmt); 1010 assert(parsed.timezone !is null); 1011 assert(parsed.timezone.isUTC()); 1012 assert(parsed == dt, parsed.format(isoishFmt)); 1013 } 1014 1015 unittest 1016 { 1017 auto isoishFmt = "%Y-%m-%d %H:%M:%S %z"; 1018 auto parsed = "2017-05-03 14:31:57 +0000".parse(isoishFmt, new immutable SimpleTimeZone(6.hours)); 1019 assert(parsed.timezone !is null); 1020 assert(parsed.timezone.isUTC()); 1021 } 1022 1023 unittest 1024 { 1025 auto isoishFmt = "%Y-%m-%d %H:%M:%S"; 1026 auto parsed = "2017-05-03 14:31:57".parse(isoishFmt, new immutable SimpleTimeZone(-6.hours)); 1027 assert(parsed.timezone !is null); 1028 assert(!parsed.timezone.isUTC()); 1029 assert(parsed.timezone.utcOffsetAt(parsed.stdTime) == -6.hours); 1030 } 1031 1032 unittest 1033 { 1034 SysTime st; 1035 assert(tryParse("2013-10-09T14:56:33.050-06:00", ISO8601FORMAT, st, UTC())); 1036 assert(st.year == 2013); 1037 assert(st.month == 10); 1038 assert(st.day == 9); 1039 assert(st.hour == 14, st.hour.to!string); 1040 assert(st.minute == 56); 1041 assert(st.second == 33); 1042 assert(st.fracSecs == 50.msecs); 1043 assert(st.timezone !is null); 1044 assert(!st.timezone.isUTC()); 1045 assert(st.timezone.utcOffsetAt(st.stdTime) == -6.hours); 1046 } 1047 1048 unittest 1049 { 1050 import std.stdio; 1051 auto dt = SysTime( 1052 DateTime(2017, 5, 3, 14, 31, 57), 1053 UTC()); 1054 auto isoishFmt = ISO8601FORMAT; 1055 auto isoish = "2017-05-03T14:31:57.000000Z"; 1056 auto parsed = isoish.parse(isoishFmt); 1057 assert(parsed.timezone !is null); 1058 assert(parsed.timezone.isUTC()); 1059 assert(parsed == dt, parsed.format(isoishFmt)); 1060 } 1061 1062 unittest 1063 { 1064 import std.stdio; 1065 auto dt = SysTime( 1066 DateTime(2017, 5, 3, 14, 31, 57), 1067 UTC()) + 10.msecs; 1068 assert(dt.fracSecs == 10.msecs, "can't add dates"); 1069 auto isoishFmt = ISO8601FORMAT; 1070 auto isoish = "2017-05-03T14:31:57.010000Z"; 1071 auto parsed = isoish.parse(isoishFmt); 1072 assert(parsed.fracSecs == 10.msecs, "can't parse millis"); 1073 assert(parsed.timezone !is null); 1074 assert(parsed.timezone.isUTC()); 1075 assert(parsed == dt, parsed.format(isoishFmt)); 1076 1077 } 1078 1079 unittest 1080 { 1081 auto formatted = "Thu, 04 Sep 2014 06:42:22 GMT"; 1082 auto dt = parseRFC1123(formatted); 1083 assert(dt == SysTime(DateTime(2014, 9, 4, 6, 42, 22), UTC()), dt.toISOString()); 1084 } 1085 1086 unittest 1087 { 1088 // RFC1123 dates 1089 /* 1090 // Uncomment to list out the generated format strings (for debugging) 1091 foreach (fmt; exhaustiveDateFormat.formatOptions) 1092 { 1093 import std.stdio : writeln; 1094 writeln(fmt); 1095 } 1096 */ 1097 void test(string date) @safe 1098 { 1099 SysTime st; 1100 assert(tryParse(date, RFC1123FORMAT, st, UTC()), 1101 "failed to parse date " ~ date); 1102 assert(st.year == 2017); 1103 assert(st.month == 6); 1104 assert(st.day == 11); 1105 assert(st.hour == 22); 1106 assert(st.minute == 15); 1107 assert(st.second == 47); 1108 assert(st.timezone.isUTC()); 1109 } 1110 1111 // RFC1123-ish 1112 test("Sun, 11 Jun 2017 22:15:47 UTC"); 1113 test("11 Jun 2017 22:15:47 UTC"); 1114 test("Sunday, 11 Jun 2017 22:15:47 UTC"); 1115 test("Sun, 11 June 2017 22:15:47 UTC"); 1116 test("11 June 2017 22:15:47 UTC"); 1117 test("Sunday, 11 June 2017 22:15:47 UTC"); 1118 test("Sun, 11 Jun 2017 22:15:47 +0000"); 1119 test("11 Jun 2017 22:15:47 +0000"); 1120 test("Sunday, 11 Jun 2017 22:15:47 +0000"); 1121 test("Sun, 11 June 2017 22:15:47 +0000"); 1122 test("11 June 2017 22:15:47 +0000"); 1123 test("Sunday, 11 June 2017 22:15:47 +0000"); 1124 } 1125 1126 unittest 1127 { 1128 SysTime st; 1129 assert(!tryParse("", RFC1123FORMAT, st, UTC())); 1130 } 1131 1132 unittest 1133 { 1134 // Thanks to Ha Le at https://github.com/haqle314 for catching this 1135 SysTime d1 = SysTime(DateTime(2019, 1, 1), UTC()); 1136 assert(d1.format("%b") == "Jan"); 1137 assert(d1.format("%B") == "January"); 1138 }