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 fss = data[0..end]; 519 data = data[end..$]; 520 if (fss.length == 0) 521 { 522 return false; 523 } 524 auto fs = fss.to!ulong; 525 while (end < 7) 526 { 527 end++; 528 fs *= 10; 529 } 530 while (end > 7) 531 { 532 end--; 533 fs /= 10; 534 } 535 this.fracSecs = fs.hnsecs; 536 return true; 537 case 'F': 538 auto dash1 = data.indexOf('-'); 539 if (dash1 <= 0) return false; 540 if (dash1 >= data.length - 1) return false; 541 auto yearStr = data[0..dash1]; 542 auto year = yearStr.to!int; 543 data = data[dash1 + 1 .. $]; 544 545 if (data.length < 5) 546 { 547 // Month is 2 digits; day is 2 digits; dash between 548 return false; 549 } 550 if (data[2] != '-') 551 { 552 return false; 553 } 554 if (!parseInt!(x => month = cast(Month)x)(data)) return false; 555 if (!data.startsWith("-")) return false; 556 data = data[1..$]; 557 return parseInt!(x => dayOfMonth = x)(data); 558 case 'H': 559 case 'k': 560 auto h = parseInt!(x => hour24 = x)(data); 561 return h; 562 case 'h': 563 case 'I': 564 case 'l': 565 return parseInt!(x => hour12 = x)(data); 566 case 'j': 567 return parseInt!(x => dayOfYear = x, 3)(data); 568 case 'm': 569 return parseInt!(x => month = cast(Month)x)(data); 570 case 'M': 571 return parseInt!(x => minute = x)(data); 572 case 'p': 573 if (data.startsWith("AM")) 574 { 575 amPm = AMPM.AM; 576 } 577 else if (data.startsWith("PM")) 578 { 579 amPm = AMPM.PM; 580 } 581 else 582 { 583 return false; 584 } 585 return true; 586 case 'P': 587 if (data.startsWith("am")) 588 { 589 amPm = AMPM.AM; 590 } 591 else if (data.startsWith("pm")) 592 { 593 amPm = AMPM.PM; 594 } 595 else 596 { 597 return false; 598 } 599 return true; 600 case 'r': 601 return interpretFromString('I') && 602 pop(':') && 603 interpretFromString('M') && 604 pop(':') && 605 interpretFromString('S') && 606 pop(' ') && 607 interpretFromString('p'); 608 case 'R': 609 return interpretFromString('H') && 610 pop(':') && 611 interpretFromString('M'); 612 case 's': 613 size_t end = 0; 614 foreach (i2, c2; data) 615 { 616 if (c2 < '0' || c2 > '9') 617 { 618 end = cast()i2; 619 break; 620 } 621 } 622 if (end == 0) return false; 623 epochSecond = data[0..end].to!int; 624 data = data[end..$]; 625 return true; 626 case 'S': 627 return parseInt!(x => second = x)(data); 628 case 'T': 629 return interpretFromString('H') && 630 pop(':') && 631 interpretFromString('M') && 632 pop(':') && 633 interpretFromString('S'); 634 case 'u': 635 return parseInt!(x => dayOfWeek = cast(DayOfWeek)(x % 7))(data); 636 case 'V': 637 return parseInt!(x => isoWeek = x)(data); 638 case 'y': 639 return parseInt!(x => yearOfCentury = x)(data); 640 case 'Y': 641 size_t end = 0; 642 foreach (i2, c2; data) 643 { 644 if (c2 < '0' || c2 > '9') 645 { 646 end = i2; 647 break; 648 } 649 } 650 if (end == 0) return false; 651 year = data[0..end].to!int; 652 data = data[end..$]; 653 return true; 654 case 'z': 655 case '+': 656 if (pop('Z')) // for ISO8601 657 { 658 tzOffset = 0.seconds; 659 return true; 660 } 661 662 int sign = 0; 663 if (pop('-')) 664 { 665 sign = -1; 666 } 667 else if (pop('+')) 668 { 669 sign = 1; 670 } 671 else 672 { 673 return false; 674 } 675 int hour, minute; 676 parseInt!(x => hour = x)(data); 677 parseInt!(x => minute = x)(data); 678 tzOffset = dur!"minutes"(sign * (minute + 60 * hour)); 679 return true; 680 case 'Z': 681 foreach (i, v; canonicalZones) 682 { 683 if (data.startsWith(v.name)) 684 { 685 tz = &canonicalZones[i].zone; 686 break; 687 } 688 } 689 return false; 690 default: 691 throw new Exception("unrecognized control character %" ~ c.to!string); 692 } 693 } 694 695 bool pop(dchar c) 696 { 697 if (data.startsWith(c)) 698 { 699 data = data[c.codeLength!char .. $]; 700 return true; 701 } 702 return false; 703 } 704 } 705 706 struct CanonicalZone 707 { 708 string name; 709 immutable(TimeZone) zone; 710 } 711 // array so we can control iteration order -- longest first 712 CanonicalZone[] canonicalZones; 713 shared static this() 714 { 715 version (Posix) 716 { 717 auto utc = PosixTimeZone.getTimeZone("Etc/UTC"); 718 auto est = PosixTimeZone.getTimeZone("America/New_York"); 719 auto cst = PosixTimeZone.getTimeZone("America/Boise"); 720 auto mst = PosixTimeZone.getTimeZone("America/Chicago"); 721 auto pst = PosixTimeZone.getTimeZone("America/Los_Angeles"); 722 } 723 else 724 { 725 auto utc = WindowsTimeZone.getTimeZone("UTC"); 726 auto est = WindowsTimeZone.getTimeZone("Eastern Standard Time"); 727 auto cst = WindowsTimeZone.getTimeZone("Central Standard Time"); 728 auto mst = WindowsTimeZone.getTimeZone("Mountain Standard Time"); 729 auto pst = WindowsTimeZone.getTimeZone("Pacific Standard Time"); 730 } 731 canonicalZones = 732 [ 733 // TODO ensure the MDT style variants prefer the daylight time version of the date 734 CanonicalZone("UTC", utc), 735 CanonicalZone("UT", utc), 736 CanonicalZone("Z", utc), 737 CanonicalZone("GMT", utc), 738 CanonicalZone("EST", est), 739 CanonicalZone("EDT", est), 740 CanonicalZone("CST", cst), 741 CanonicalZone("CDT", cst), 742 CanonicalZone("MST", mst), 743 CanonicalZone("MDT", mst), 744 CanonicalZone("PST", pst), 745 CanonicalZone("PDT", pst), 746 ]; 747 } 748 749 bool parseInt(alias setter, int length = 2)(ref string data) 750 { 751 if (data.length < length) 752 { 753 return false; 754 } 755 auto c = data[0..length].strip; 756 data = data[length..$]; 757 int v; 758 try 759 { 760 v = c.to!int; 761 } 762 catch (ConvException e) 763 { 764 return false; 765 } 766 cast(void)setter(c.to!int); 767 return true; 768 } 769 770 void interpretIntoString(ref Appender!string ap, SysTime dt, char c) 771 { 772 static import std.format; 773 switch (c) 774 { 775 case 'a': 776 ap ~= weekdayAbbrev[cast(size_t)dt.dayOfWeek]; 777 return; 778 case 'A': 779 ap ~= weekdayNames[cast(size_t)dt.dayOfWeek]; 780 return; 781 case 'b': 782 ap ~= monthAbbrev[cast(size_t)dt.month]; 783 return; 784 case 'B': 785 ap ~= monthNames[cast(size_t)dt.month]; 786 return; 787 case 'C': 788 ap ~= (dt.year / 100).to!string; 789 return; 790 case 'd': 791 auto s = dt.day.to!string; 792 if (s.length == 1) 793 { 794 ap ~= "0"; 795 } 796 ap ~= s; 797 return; 798 case 'e': 799 auto s = dt.day.to!string; 800 if (s.length == 1) 801 { 802 ap ~= " "; 803 } 804 ap ~= s; 805 return; 806 case 'f': 807 ap ~= std.format.format("%06d", dt.fracSecs.total!"usecs"); 808 return; 809 case 'g': 810 ap ~= std.format.format("%03d", dt.fracSecs.total!"msecs"); 811 return; 812 case 'G': 813 ap ~= std.format.format("%09d", dt.fracSecs.total!"nsecs"); 814 return; 815 case 'F': 816 interpretIntoString(ap, dt, 'Y'); 817 ap ~= '-'; 818 interpretIntoString(ap, dt, 'm'); 819 ap ~= '-'; 820 interpretIntoString(ap, dt, 'd'); 821 return; 822 case 'h': 823 case 'I': 824 auto h = dt.hour; 825 if (h == 0) 826 { 827 h = 12; 828 } 829 else if (h > 12) 830 { 831 h -= 12; 832 } 833 ap.pad(h.to!string, '0', 2); 834 return; 835 case 'H': 836 ap.pad(dt.hour.to!string, '0', 2); 837 return; 838 case 'j': 839 ap.pad(dt.dayOfYear.to!string, '0', 3); 840 return; 841 case 'k': 842 ap.pad(dt.hour.to!string, ' ', 2); 843 return; 844 case 'l': 845 auto h = dt.hour; 846 if (h == 0) 847 { 848 h = 12; 849 } 850 else if (h > 12) 851 { 852 h -= 12; 853 } 854 ap.pad(h.to!string, ' ', 2); 855 return; 856 case 'm': 857 uint m = cast(uint)dt.month; 858 ap.pad(m.to!string, '0', 2); 859 return; 860 case 'M': 861 ap.pad(dt.minute.to!string, '0', 2); 862 return; 863 case 'p': 864 if (dt.hour >= 12) 865 { 866 ap ~= "PM"; 867 } 868 else 869 { 870 ap ~= "AM"; 871 } 872 return; 873 case 'P': 874 if (dt.hour >= 12) 875 { 876 ap ~= "pm"; 877 } 878 else 879 { 880 ap ~= "am"; 881 } 882 return; 883 case 'r': 884 interpretIntoString(ap, dt, 'I'); 885 ap ~= ':'; 886 interpretIntoString(ap, dt, 'M'); 887 ap ~= ':'; 888 interpretIntoString(ap, dt, 'S'); 889 ap ~= ' '; 890 interpretIntoString(ap, dt, 'p'); 891 return; 892 case 'R': 893 interpretIntoString(ap, dt, 'H'); 894 ap ~= ':'; 895 interpretIntoString(ap, dt, 'M'); 896 return; 897 case 's': 898 auto delta = dt - SysTime(DateTime(1970, 1, 1), UTC()); 899 ap ~= delta.total!"seconds"().to!string; 900 return; 901 case 'S': 902 ap.pad(dt.second.to!string, '0', 2); 903 return; 904 case 'T': 905 interpretIntoString(ap, dt, 'H'); 906 ap ~= ':'; 907 interpretIntoString(ap, dt, 'M'); 908 ap ~= ':'; 909 interpretIntoString(ap, dt, 'S'); 910 return; 911 case 'u': 912 auto dow = cast(uint)dt.dayOfWeek; 913 if (dow == 0) dow = 7; 914 ap ~= dow.to!string; 915 return; 916 case 'w': 917 ap ~= (cast(uint)dt.dayOfWeek).to!string; 918 return; 919 case 'y': 920 ap.pad((dt.year % 100).to!string, '0', 2); 921 return; 922 case 'Y': 923 ap.pad(dt.year.to!string, '0', 4); 924 return; 925 case '+': 926 if (dt.utcOffset == dur!"seconds"(0)) 927 { 928 ap ~= 'Z'; 929 return; 930 } 931 // If it's not UTC, format as +HHMM 932 goto case; 933 case 'z': 934 import std.math : abs; 935 auto d = dt.utcOffset; 936 if (d < dur!"seconds"(0)) 937 { 938 ap ~= '-'; 939 } 940 else 941 { 942 ap ~= '+'; 943 } 944 auto minutes = abs(d.total!"minutes"); 945 ap.pad((minutes / 60).to!string, '0', 2); 946 ap.pad((minutes % 60).to!string, '0', 2); 947 return; 948 case 'Z': 949 if (dt.timezone is null || dt.timezone == UTC()) 950 { 951 ap ~= 'Z'; 952 } 953 if (dt.dstInEffect) 954 { 955 ap ~= dt.timezone.stdName; 956 } 957 else 958 { 959 ap ~= dt.timezone.dstName; 960 } 961 return; 962 case '%': 963 ap ~= '%'; 964 return; 965 default: 966 throw new Exception("format element %" ~ c ~ " not recognized"); 967 } 968 } 969 970 void pad(ref Appender!string ap, string s, char pad, uint length) 971 { 972 if (s.length >= length) 973 { 974 ap ~= s; 975 return; 976 } 977 for (uint i = 0; i < length - s.length; i++) 978 { 979 ap ~= pad; 980 } 981 ap ~= s; 982 } 983 984 unittest 985 { 986 import std.stdio; 987 auto dt = SysTime( 988 DateTime(2017, 5, 3, 14, 31, 57), 989 UTC()); 990 auto isoishFmt = "%Y-%m-%d %H:%M:%S %z"; 991 auto isoish = dt.format(isoishFmt); 992 assert(isoish == "2017-05-03 14:31:57 +0000", isoish); 993 auto parsed = isoish.parse(isoishFmt); 994 assert(parsed.timezone !is null); 995 assert(parsed.timezone == UTC()); 996 assert(parsed == dt, parsed.format(isoishFmt)); 997 } 998 999 unittest 1000 { 1001 SysTime st; 1002 assert(tryParse("2013-10-09T14:56:33.050-06:00", ISO8601FORMAT, st, UTC())); 1003 assert(st.year == 2013); 1004 assert(st.month == 10); 1005 assert(st.day == 9); 1006 assert(st.hour == 14, st.hour.to!string); 1007 assert(st.minute == 56); 1008 assert(st.second == 33); 1009 assert(st.fracSecs == 50.msecs); 1010 assert(st.timezone !is null); 1011 assert(st.timezone != UTC()); 1012 assert(st.timezone.utcOffsetAt(st.stdTime) == -6.hours); 1013 } 1014 1015 unittest 1016 { 1017 import std.stdio; 1018 auto dt = SysTime( 1019 DateTime(2017, 5, 3, 14, 31, 57), 1020 UTC()); 1021 auto isoishFmt = ISO8601FORMAT; 1022 auto isoish = "2017-05-03T14:31:57.000000Z"; 1023 auto parsed = isoish.parse(isoishFmt); 1024 assert(parsed.timezone !is null); 1025 assert(parsed.timezone == UTC()); 1026 assert(parsed == dt, parsed.format(isoishFmt)); 1027 } 1028 1029 unittest 1030 { 1031 import std.stdio; 1032 auto dt = SysTime( 1033 DateTime(2017, 5, 3, 14, 31, 57), 1034 UTC()) + 10.msecs; 1035 assert(dt.fracSecs == 10.msecs, "can't add dates"); 1036 auto isoishFmt = ISO8601FORMAT; 1037 auto isoish = "2017-05-03T14:31:57.010000Z"; 1038 auto parsed = isoish.parse(isoishFmt); 1039 assert(parsed.fracSecs == 10.msecs, "can't parse millis"); 1040 assert(parsed.timezone !is null); 1041 assert(parsed.timezone == UTC()); 1042 assert(parsed == dt, parsed.format(isoishFmt)); 1043 1044 } 1045 1046 unittest 1047 { 1048 auto formatted = "Thu, 04 Sep 2014 06:42:22 GMT"; 1049 auto dt = parseRFC1123(formatted); 1050 assert(dt == SysTime(DateTime(2014, 9, 4, 6, 42, 22), UTC()), dt.toISOString()); 1051 } 1052 1053 unittest 1054 { 1055 // RFC1123 dates 1056 /* 1057 // Uncomment to list out the generated format strings (for debugging) 1058 foreach (fmt; exhaustiveDateFormat.formatOptions) 1059 { 1060 import std.stdio : writeln; 1061 writeln(fmt); 1062 } 1063 */ 1064 void test(string date) 1065 { 1066 SysTime st; 1067 assert(tryParse(date, RFC1123FORMAT, st, UTC()), 1068 "failed to parse date " ~ date); 1069 assert(st.year == 2017); 1070 assert(st.month == 6); 1071 assert(st.day == 11); 1072 assert(st.hour == 22); 1073 assert(st.minute == 15); 1074 assert(st.second == 47); 1075 assert(st.timezone == UTC()); 1076 } 1077 1078 // RFC1123-ish 1079 test("Sun, 11 Jun 2017 22:15:47 UTC"); 1080 test("11 Jun 2017 22:15:47 UTC"); 1081 test("Sunday, 11 Jun 2017 22:15:47 UTC"); 1082 test("Sun, 11 June 2017 22:15:47 UTC"); 1083 test("11 June 2017 22:15:47 UTC"); 1084 test("Sunday, 11 June 2017 22:15:47 UTC"); 1085 test("Sun, 11 Jun 2017 22:15:47 +0000"); 1086 test("11 Jun 2017 22:15:47 +0000"); 1087 test("Sunday, 11 Jun 2017 22:15:47 +0000"); 1088 test("Sun, 11 June 2017 22:15:47 +0000"); 1089 test("11 June 2017 22:15:47 +0000"); 1090 test("Sunday, 11 June 2017 22:15:47 +0000"); 1091 } 1092 1093 unittest 1094 { 1095 SysTime st; 1096 assert(!tryParse("", RFC1123FORMAT, st, UTC())); 1097 }