1 /** 2 * Time string formatting and such. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <vladimir@thecybershadow.net> 12 */ 13 14 module ae.utils.time; 15 16 import std.algorithm; 17 import std.conv : text; 18 import std.datetime; 19 import std.format : formattedWrite; 20 import std.math : abs; 21 import std.string; 22 import std.typecons; 23 24 import ae.utils.text; 25 import ae.utils.textout; 26 27 // *************************************************************************** 28 29 struct TimeFormats 30 { 31 static: 32 const ATOM = `Y-m-d\TH:i:sP`; 33 const COOKIE = `l, d-M-y H:i:s T`; 34 const ISO8601 = `Y-m-d\TH:i:sO`; 35 const RFC822 = `D, d M y H:i:s O`; 36 const RFC850 = `l, d-M-y H:i:s T`; 37 const RFC1036 = `D, d M y H:i:s O`; 38 const RFC1123 = `D, d M Y H:i:s O`; 39 const RFC2822 = `D, d M Y H:i:s O`; 40 const RFC3339 = `Y-m-d\TH:i:sP`; 41 const RSS = `D, d M Y H:i:s O`; 42 const W3C = `Y-m-d\TH:i:sP`; 43 44 const HTML5DATE = `Y-m-d`; 45 46 /// Format produced by std.date.toString, e.g. "Tue Jun 07 13:23:19 GMT+0100 2011" 47 const STD_DATE = `D M d H:i:s \G\M\TO Y`; 48 } 49 50 const WeekdayShortNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; 51 const WeekdayLongNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; 52 const MonthShortNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]; 53 const MonthLongNames = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; 54 55 // *************************************************************************** 56 57 /// We assume that no timezone will have a name longer than this. 58 /// If one does, it is truncated to this length. 59 enum MaxTimezoneNameLength = 256; 60 61 private struct FormatContext(Char) 62 { 63 SysTime t; 64 DateTime dt; 65 bool escaping; 66 } 67 68 private void putToken(alias c, alias context, alias sink)() 69 { 70 with (context) 71 { 72 void putOneDigit(uint i) 73 { 74 debug assert(i < 10); 75 sink.put(cast(char)('0' + i)); 76 } 77 78 void putOneOrTwoDigits(uint i) 79 { 80 debug assert(i < 100); 81 if (i >= 10) 82 { 83 sink.put(cast(char)('0' + (i / 10))); 84 sink.put(cast(char)('0' + (i % 10))); 85 } 86 else 87 sink.put(cast(char)('0' + i )); 88 } 89 90 void putTimezoneName(string tzStr) 91 { 92 if (tzStr.length) 93 sink.put(tzStr[0..min($, MaxTimezoneNameLength)]); 94 else 95 // if (t.timezone.utcToTZ(t.stdTime) == t.stdTime) 96 // sink.put("UTC"); 97 // else 98 { 99 enum fmt = 'C'; 100 putToken!(fmt, context, sink)(); 101 } 102 } 103 104 if (escaping) 105 sink.put(c), escaping = false; 106 else 107 switch (c) 108 { 109 // Day 110 case 'd': 111 sink.put(toDecFixed!2(dt.day)); 112 break; 113 case 'D': 114 sink.put(WeekdayShortNames[dt.dayOfWeek]); 115 break; 116 case 'j': 117 putOneOrTwoDigits(dt.day); 118 break; 119 case 'l': 120 sink.put(WeekdayLongNames[dt.dayOfWeek]); 121 break; 122 case 'N': 123 putOneDigit((dt.dayOfWeek+6)%7 + 1); 124 break; 125 case 'S': 126 switch (dt.day) 127 { 128 case 1: 129 case 21: 130 case 31: 131 sink.put("st"); 132 break; 133 case 2: 134 case 22: 135 sink.put("nd"); 136 break; 137 case 3: 138 case 23: 139 sink.put("rd"); 140 break; 141 default: 142 sink.put("th"); 143 } 144 break; 145 case 'w': 146 putOneDigit(cast(int)dt.dayOfWeek); 147 break; 148 case 'z': 149 sink.put(text(dt.dayOfYear-1)); 150 break; 151 152 // Week 153 case 'W': 154 sink.put(toDecFixed!2(dt.isoWeek)); 155 break; 156 157 // Month 158 case 'F': 159 sink.put(MonthLongNames[dt.month-1]); 160 break; 161 case 'm': 162 sink.put(toDecFixed!2(dt.month)); 163 break; 164 case 'M': 165 sink.put(MonthShortNames[dt.month-1]); 166 break; 167 case 'n': 168 putOneOrTwoDigits(dt.month); 169 break; 170 case 't': 171 putOneOrTwoDigits(dt.daysInMonth); 172 break; 173 174 // Year 175 case 'L': 176 sink.put(dt.isLeapYear ? '1' : '0'); 177 break; 178 // case 'o': TODO (ISO 8601 year number) 179 case 'Y': 180 sink.put(toDecFixed!4(cast(uint)dt.year)); // Hack? Assumes years are in 1000-9999 AD range 181 break; 182 case 'y': 183 sink.put(toDecFixed!2(cast(uint)dt.year % 100)); 184 break; 185 186 // Time 187 case 'a': 188 sink.put(dt.hour < 12 ? "am" : "pm"); 189 break; 190 case 'A': 191 sink.put(dt.hour < 12 ? "AM" : "PM"); 192 break; 193 // case 'B': TODO (Swatch Internet time) 194 case 'g': 195 putOneOrTwoDigits((dt.hour+11)%12 + 1); 196 break; 197 case 'G': 198 putOneOrTwoDigits(dt.hour); 199 break; 200 case 'h': 201 sink.put(toDecFixed!2(cast(uint)(dt.hour+11)%12 + 1)); 202 break; 203 case 'H': 204 sink.put(toDecFixed!2(dt.hour)); 205 break; 206 case 'i': 207 sink.put(toDecFixed!2(dt.minute)); 208 break; 209 case 's': 210 sink.put(toDecFixed!2(dt.second)); 211 break; 212 case 'u': 213 sink.put(toDecFixed!6(cast(uint)t.fracSecs.split!"usecs".usecs)); 214 break; 215 case 'E': // not standard 216 sink.put(toDecFixed!3(cast(uint)t.fracSecs.split!"msecs".msecs)); 217 break; 218 219 // Timezone 220 case 'e': 221 putTimezoneName(t.timezone.name); 222 break; 223 case 'I': 224 sink.put(t.dstInEffect ? '1': '0'); 225 break; 226 case 'O': 227 { 228 auto minutes = (t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000 / 60; 229 sink.reference.formattedWrite("%+03d%02d", minutes/60, abs(minutes%60)); 230 break; 231 } 232 case 'P': 233 { 234 auto minutes = (t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000 / 60; 235 sink.reference.formattedWrite("%+03d:%02d", minutes/60, abs(minutes%60)); 236 break; 237 } 238 case 'T': 239 putTimezoneName(t.timezone.stdName); 240 break; 241 case 'Z': 242 sink.putDecimal((t.timezone.utcToTZ(t.stdTime) - t.stdTime) / 10_000_000); 243 break; 244 245 // Full date/time 246 case 'c': 247 sink.put(dt.toISOExtString()); 248 break; 249 case 'r': 250 putTime(sink, t, TimeFormats.RFC2822); 251 break; 252 case 'U': 253 sink.putDecimal(t.toUnixTime()); 254 break; 255 256 // Escape next character 257 case '\\': 258 escaping = true; 259 break; 260 261 // Other characters (whitespace, delimiters) 262 default: 263 put(sink, c); 264 } 265 } 266 } 267 268 /// Format a SysTime using the format spec fmt. 269 /// This version generates specialized code for the given fmt. 270 string formatTime(string fmt)(SysTime t) 271 { 272 enum maxSize = timeFormatSize(fmt); 273 auto result = StringBuilder(maxSize); 274 putTime!fmt(result, t); 275 return result.get(); 276 } 277 278 /// ditto 279 void putTime(string fmt, S)(ref S sink, SysTime t) 280 if (IsStringSink!S) 281 { 282 putTimeImpl!fmt(sink, t); 283 } 284 285 /// Format a SysTime using the format spec fmt. 286 /// This version parses fmt at runtime. 287 string formatTime(SysTime t, string fmt) 288 { 289 auto result = StringBuilder(timeFormatSize(fmt)); 290 putTime(result, t, fmt); 291 return result.get(); 292 } 293 294 /// ditto 295 deprecated string formatTime(string fmt, SysTime t = Clock.currTime()) 296 { 297 auto result = StringBuilder(48); 298 putTime(result, fmt, t); 299 return result.get(); 300 } 301 302 /// ditto 303 void putTime(S)(ref S sink, SysTime t, string fmt) 304 if (IsStringSink!S) 305 { 306 putTimeImpl!fmt(sink, t); 307 } 308 309 deprecated alias format = formatTime; 310 311 /// ditto 312 deprecated void putTime(S)(ref S sink, string fmt, SysTime t = Clock.currTime()) 313 if (IsStringSink!S) 314 { 315 putTimeImpl!fmt(sink, t); 316 } 317 318 void putTimeImpl(alias fmt, S)(ref S sink, SysTime t) 319 { 320 FormatContext!(char) context; 321 context.t = t; 322 context.dt = cast(DateTime)t; 323 foreach (c; CTIterate!fmt) 324 putToken!(c, context, sink)(); 325 } 326 327 /// Calculate the maximum amount of characters needed to store a time in this format. 328 /// Can be evaluated at compile-time. 329 size_t timeFormatSize(string fmt) 330 { 331 static size_t maxLength(in string[] names) { return reduce!max(map!`a.length`(WeekdayShortNames)); } 332 333 size_t size = 0; 334 bool escaping = false; 335 foreach (char c; fmt) 336 if (escaping) 337 size++, escaping = false; 338 else 339 switch (c) 340 { 341 case 'N': 342 case 'w': 343 case 'L': 344 case 'I': 345 size++; 346 break; 347 case 'd': 348 case 'j': 349 case 'S': 350 case 'W': 351 case 'm': 352 case 'n': 353 case 't': 354 case 'y': 355 case 'a': 356 case 'A': 357 case 'g': 358 case 'G': 359 case 'h': 360 case 'H': 361 case 'i': 362 case 's': 363 size += 2; 364 break; 365 case 'z': 366 case 'E': // not standard 367 size += 3; 368 break; 369 case 'Y': 370 size += 4; 371 break; 372 case 'Z': // Timezone offset in seconds 373 case 'O': 374 size += 5; 375 break; 376 case 'u': 377 case 'P': 378 size += 6; 379 break; 380 case 'T': 381 size += 32; 382 break; 383 384 case 'D': 385 size += maxLength(WeekdayShortNames); 386 break; 387 case 'l': 388 size += maxLength(WeekdayLongNames); 389 break; 390 case 'F': 391 size += maxLength(MonthLongNames); 392 break; 393 case 'M': 394 size += maxLength(MonthShortNames); 395 break; 396 397 case 'e': // Timezone name 398 return MaxTimezoneNameLength; 399 400 // Full date/time 401 case 'c': 402 enum ISOExtLength = "-0004-01-05T00:00:02.052092+10:00".length; 403 size += ISOExtLength; 404 break; 405 case 'r': 406 size += timeFormatSize(TimeFormats.RFC2822); 407 break; 408 case 'U': 409 size += DecimalSize!int; 410 break; 411 412 // Escape next character 413 case '\\': 414 escaping = true; 415 break; 416 417 // Other characters (whitespace, delimiters) 418 default: 419 size++; 420 } 421 422 return size; 423 } 424 425 static assert(timeFormatSize(TimeFormats.STD_DATE) == "Tue Jun 07 13:23:19 GMT+0100 2011".length); 426 427 import std.exception : enforce; 428 import std.conv : to; 429 import std.ascii : isDigit, isWhite; 430 431 private struct ParseContext(Char, bool checked) 432 { 433 int year=0, month=1, day=1, hour=0, minute=0, second=0, usecs=0; 434 int hour12 = 0; bool pm; 435 Rebindable!(immutable(TimeZone)) tz; 436 int dow = -1; 437 Char[] t; 438 bool escaping; 439 440 void need(size_t n)() 441 { 442 static if (checked) 443 enforce(t.length >= n, "Not enough characters in date string"); 444 } 445 446 auto take(size_t n)() 447 { 448 need!n(); 449 auto result = t[0..n]; 450 t = t[n..$]; 451 return result; 452 } 453 454 char takeOne() 455 { 456 need!1(); 457 auto result = t[0]; 458 t = t[1..$]; 459 return result; 460 } 461 462 R takeNumber(size_t n, sizediff_t maxP = -1, R = int)() 463 { 464 enum max = maxP == -1 ? n : maxP; 465 need!n(); 466 foreach (i, c; t[0..n]) 467 enforce((i==0 && c=='-') || isDigit(c) || isWhite(c), "Number expected"); 468 static if (n == max) 469 enum i = n; 470 else 471 { 472 auto i = n; 473 while (i < max && (checked ? i < t.length : true) && isDigit(t[i])) 474 i++; 475 } 476 auto s = t[0..i]; 477 t = t[i..$]; 478 return s.strip().to!R(); 479 } 480 481 int takeWord(in string[] words, string name) 482 { 483 foreach (idx, string word; words) 484 { 485 static if (checked) 486 bool b = t.startsWith(word); 487 else 488 bool b = t[0..word.length] == word; 489 if (b) 490 { 491 t = t[word.length..$]; 492 return cast(int)idx; 493 } 494 } 495 throw new Exception(name ~ " expected"); 496 } 497 498 char peek() 499 { 500 need!1(); 501 return *t.ptr; 502 } 503 } 504 505 private void parseToken(alias c, alias context)() 506 { 507 with (context) 508 { 509 // TODO: check if the compiler optimizes this check away 510 // in the compile-time version. If not, "escaping" needs to 511 // be moved into an alias parameter. 512 if (escaping) 513 { 514 enforce(takeOne() == c, c ~ " expected"); 515 escaping = false; 516 return; 517 } 518 519 switch (c) 520 { 521 // Day 522 case 'd': 523 day = takeNumber!(2)(); 524 break; 525 case 'D': 526 dow = takeWord(WeekdayShortNames, "Weekday"); 527 break; 528 case 'j': 529 day = takeNumber!(1, 2); 530 break; 531 case 'l': 532 dow = takeWord(WeekdayLongNames, "Weekday"); 533 break; 534 case 'N': 535 dow = takeNumber!1 % 7; 536 break; 537 case 'S': // ordinal suffix 538 take!2; 539 break; 540 case 'w': 541 dow = takeNumber!1; 542 break; 543 //case 'z': TODO 544 545 // Week 546 //case 'W': TODO 547 548 // Month 549 case 'F': 550 month = takeWord(MonthLongNames, "Month") + 1; 551 break; 552 case 'm': 553 month = takeNumber!2; 554 break; 555 case 'M': 556 month = takeWord(MonthShortNames, "Month") + 1; 557 break; 558 case 'n': 559 month = takeNumber!(1, 2); 560 break; 561 case 't': 562 takeNumber!(1, 2); // TODO: validate DIM? 563 break; 564 565 // Year 566 case 'L': 567 takeNumber!1; // TODO: validate leapness? 568 break; 569 // case 'o': TODO (ISO 8601 year number) 570 case 'Y': 571 year = takeNumber!4; 572 break; 573 case 'y': 574 year = takeNumber!2; 575 if (year > 50) // TODO: find correct logic for this 576 year += 1900; 577 else 578 year += 2000; 579 break; 580 581 // Time 582 case 'a': 583 pm = takeWord(["am", "pm"], "am/pm")==1; 584 break; 585 case 'A': 586 pm = takeWord(["AM", "PM"], "AM/PM")==1; 587 break; 588 // case 'B': TODO (Swatch Internet time) 589 case 'g': 590 hour12 = takeNumber!(1, 2); 591 break; 592 case 'G': 593 hour = takeNumber!(1, 2); 594 break; 595 case 'h': 596 hour12 = takeNumber!2; 597 break; 598 case 'H': 599 hour = takeNumber!2; 600 break; 601 case 'i': 602 minute = takeNumber!2; 603 break; 604 case 's': 605 second = takeNumber!2; 606 break; 607 case 'u': 608 usecs = takeNumber!6; 609 break; 610 case 'E': // not standard 611 usecs = 1000 * takeNumber!3; 612 break; 613 614 // Timezone 615 // case 'e': ??? 616 case 'I': 617 takeNumber!1; 618 break; 619 case 'O': 620 { 621 if (peek() == 'Z') 622 { 623 t = t[1..$]; 624 tz = UTC(); 625 } 626 else 627 if (peek() == 'G') 628 { 629 enforce(take!3() == "GMT", "GMT expected"); 630 tz = UTC(); 631 } 632 else 633 { 634 auto tzStr = take!5(); 635 enforce(tzStr[0]=='-' || tzStr[0]=='+', "-/+ expected"); 636 auto n = (to!int(tzStr[1..3]) * 60 + to!int(tzStr[3..5])) * (tzStr[0]=='-' ? -1 : 1); 637 tz = new immutable(SimpleTimeZone)(minutes(n)); 638 } 639 break; 640 } 641 case 'P': 642 { 643 auto tzStr = take!6(); 644 enforce(tzStr[0]=='-' || tzStr[0]=='+', "-/+ expected"); 645 enforce(tzStr[3]==':', ": expected"); 646 auto n = (to!int(tzStr[1..3]) * 60 + to!int(tzStr[4..6])) * (tzStr[0]=='-' ? -1 : 1); 647 tz = new immutable(SimpleTimeZone)(minutes(n)); 648 break; 649 } 650 case 'T': 651 tz = TimeZone.getTimeZone(t.idup); 652 t = null; 653 break; 654 case 'Z': 655 { 656 // TODO: is this correct? 657 auto n = takeNumber!(1, 6); 658 tz = new immutable(SimpleTimeZone)(seconds(n)); 659 break; 660 } 661 662 // Full date/time 663 //case 'c': TODO 664 //case 'r': TODO 665 //case 'U': TODO 666 667 // Escape next character 668 case '\\': 669 escaping = true; 670 break; 671 672 // Other characters (whitespace, delimiters) 673 default: 674 { 675 enforce(t.length && t[0]==c, c~ " expected or unsupported format character"); 676 t = t[1..$]; 677 } 678 } 679 } 680 } 681 682 import ae.utils.meta; 683 684 private SysTime parseTimeImpl(alias fmt, bool checked, C)(C[] t) 685 { 686 ParseContext!(C, checked) context; 687 context.t = t; 688 689 foreach (c; CTIterate!fmt) 690 parseToken!(c, context)(); 691 692 enforce(context.t.length == 0, "Left-over characters: " ~ context.t); 693 694 SysTime result; 695 696 with (context) 697 { 698 if (hour12) 699 hour = hour12%12 + (pm ? 12 : 0); 700 701 // Compatibility with both <=2.066 and >=2.067 702 static if (__traits(hasMember, SysTime, "fracSecs")) 703 auto frac = dur!"usecs"(usecs); 704 else 705 auto frac = FracSec.from!"usecs"(usecs); 706 707 result = SysTime( 708 DateTime(year, month, day, hour, minute, second), 709 frac, 710 tz); 711 712 if (dow >= 0) 713 enforce(result.dayOfWeek == dow, "Mismatching weekday"); 714 } 715 716 return result; 717 } 718 719 /// Parse the given string into a SysTime, using the format spec fmt. 720 /// This version generates specialized code for the given fmt. 721 SysTime parseTime(string fmt, C)(C[] t) 722 { 723 // Omit length checks if we know the input string is long enough 724 enum maxLength = timeFormatSize(fmt); 725 if (t.length < maxLength) 726 return parseTimeImpl!(fmt, true )(t); 727 else 728 return parseTimeImpl!(fmt, false)(t); 729 } 730 731 /// Parse the given string into a SysTime, using the format spec fmt. 732 /// This version parses fmt at runtime. 733 SysTime parseTimeUsing(C)(C[] t, in char[] fmt) 734 { 735 return parseTimeImpl!(fmt, true)(t); 736 } 737 738 deprecated SysTime parseTime(C)(const(char)[] fmt, C[] t) 739 { 740 return t.parseTimeUsing(fmt); 741 } 742 743 unittest 744 { 745 const s0 = "Tue Jun 07 13:23:19 GMT+0100 2011"; 746 //enum t = s0.parseTime!(TimeFormats.STD_DATE); // https://d.puremagic.com/issues/show_bug.cgi?id=12042 747 auto t = s0.parseTime!(TimeFormats.STD_DATE); 748 auto s1 = t.formatTime(TimeFormats.STD_DATE); 749 assert(s0 == s1, s0 ~ "/" ~ s1); 750 auto t1 = s0.parseTimeUsing(TimeFormats.STD_DATE); 751 assert(t == t1); 752 } 753 754 unittest 755 { 756 "Tue, 21 Nov 2006 21:19:46 +0000".parseTime!(TimeFormats.RFC2822); 757 "Tue, 21 Nov 2006 21:19:46 +0000".parseTimeUsing(TimeFormats.RFC2822); 758 } 759 760 // *************************************************************************** 761 762 @property bool empty(Duration d) 763 { 764 return !d.total!"hnsecs"(); 765 } 766 767 /// Workaround SysTime.fracSecs only being available in 2.067, 768 /// and SysTime.fracSec becoming deprecated in the same version. 769 static if (!is(typeof(SysTime.init.fracSecs))) 770 @property Duration fracSecs(SysTime s) 771 { 772 enum hnsecsPerSecond = convert!("seconds", "hnsecs")(1); 773 return hnsecs(s.stdTime % hnsecsPerSecond); 774 } 775 776 /// As above, for Duration.split and Duration.get 777 static if (!is(typeof(Duration.init.split!()))) 778 @property auto split(units...)(Duration d) 779 { 780 static struct Result 781 { 782 mixin("long " ~ [units].join(", ") ~ ";"); 783 } 784 785 Result result; 786 foreach (unit; units) 787 { 788 static if (is(typeof(d.get!unit))) // unit == "msecs" || unit == "usecs" || unit == "hnsecs" || unit == "nsecs") 789 long value = d.get!unit(); 790 else 791 long value = mixin("d.fracSec." ~ unit); 792 mixin("result." ~ unit ~ " = value;"); 793 } 794 return result; 795 }