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