1 /** 2 * Contains functions for formatting a SysTime object 3 * 4 * Code taken from https://github.com/cmays90/datetimeformat 5 * with minor modifications: 6 * - no UTF validation 7 * - refactor with statement 8 **/ 9 10 module gfx.priv.datetimeformat; 11 12 package(gfx): 13 14 private import std.datetime : DayOfWeek, Month, SysTime; 15 private import std.ascii : toLower, isDigit, isAlpha; 16 private import std.utf : toUTF32, toUTF8, toUCSindex, toUTFindex, encode; 17 private import std..string : toStringz, format; 18 private import std.conv : to; 19 20 /// Short (three-letter) Days of the week 21 immutable string[] SHORT_DAY_NAME = [ 22 DayOfWeek.sun: "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" 23 ]; 24 25 /// Full names of the days of the week. 26 immutable string[] LONG_DAY_NAME = [ 27 DayOfWeek.sun: "Sunday", "Monday", "Tuesday", "Wednesday", 28 "Thursday", "Friday", "Saturday" 29 ]; 30 31 /// Short (three-letter) names of the months of the year. 32 immutable string[] SHORT_MONTH_NAME = [ 33 Month.jan: "Jan", "Feb", "Mar", "Apr", "May", "Jun", 34 "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" 35 ]; 36 37 /// Full names of the months of the year. 38 immutable string[Month.max + 1] LONG_MONTH_NAME = [ 39 Month.jan: "January", "February", "March", "April", "May", "June", 40 "July", "August", "September", "October", "November", "December" 41 ]; 42 43 /** Formats dt according to formatString. 44 * 45 * Returns: 46 * the formatted date string. 47 * Throws: 48 * SysTimeFormatException if the formatting fails, e.g. because of an error in the format 49 * string. 50 */ 51 52 string format(const SysTime dt, string formatString) { 53 return format(dt, dt.dayOfWeek, formatString); 54 } 55 56 string format(const SysTime dt, DayOfWeek dayOfWeek, string formatString) { 57 bool nonNull; 58 immutable(char)* charPos = toStringz(formatString); 59 scope(success) assert (*charPos == '\0'); 60 61 return format(dt, dayOfWeek, charPos, nonNull, '\0'); 62 } 63 64 65 private { 66 67 // taken from Phobos (where it is private in D2) 68 const ubyte[256] UTF8stride = [ 69 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 70 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 71 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 72 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 73 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 74 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 75 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 76 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 77 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 78 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 79 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 80 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, 81 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 82 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 83 3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, 84 4,4,4,4,4,4,4,4,5,5,5,5,6,6,0xFF,0xFF, 85 ]; 86 87 string format(const SysTime dt, DayOfWeek dayOfWeek, 88 ref immutable(char)* charPos, out bool nonNull, char portionEnd) { 89 90 // function uses null-terminated string to make finding the end easier 91 long lastNumber = int.min; 92 string result; 93 94 while (*charPos != portionEnd) { 95 if (beginsElement(*charPos)) { 96 bool newNonNull; 97 result ~= formatElement(dt, dayOfWeek, charPos, newNonNull, lastNumber); 98 if (newNonNull) nonNull = true; 99 } else if (beginsLiteral(*charPos)) { 100 result ~= formatLiteral(charPos); 101 } else switch (*charPos) { 102 case '\0': // unclosed portion 103 assert (portionEnd == '}'); 104 throw new SysTimeFormatException(E_UNCLOSED_COLLAPSIBLE); 105 106 case '}': 107 throw new SysTimeFormatException(E_UNOPENED_COLLAPSIBLE); 108 109 case ']': 110 throw new SysTimeFormatException(E_UNOPENED_FIELD); 111 112 default: // self-literal character 113 result ~= *(charPos++); 114 } 115 } 116 117 return result; 118 } 119 120 121 /* Processes a single format element. A format element is any of the following: 122 * - an alphabetical format specifier 123 * - a collapsible portion 124 * - an alignment field 125 * Literals and alignment field widths are not included. The purpose is 126 * to deal with those elements that cannot be part of an alignment field 127 * padding or width. 128 */ 129 string formatElement(const SysTime dt, DayOfWeek dayOfWeek, 130 ref immutable(char)* charPos, out bool nonNull, ref long lastNumber) 131 in { 132 assert (beginsElement(*charPos)); 133 } body { 134 switch (*charPos) { 135 case '[': { 136 charPos++; 137 string portion = formatField(dt, dayOfWeek, charPos, nonNull); 138 charPos++; 139 return portion; 140 } 141 142 case '{': { 143 charPos++; 144 string portion = format(dt, dayOfWeek, charPos, nonNull, '}'); 145 charPos++; 146 return nonNull ? portion : null; 147 } 148 149 default: 150 char letter = cast(char) toLower(*charPos); 151 immutable(char)* beginSpec = charPos; 152 153 do { 154 ++charPos; 155 } while (toLower(*charPos) == letter); 156 157 string formatted = formatBySpec(dt, dayOfWeek, 158 beginSpec[0 .. charPos-beginSpec], lastNumber); 159 160 if (formatted.length != 0) { 161 nonNull = true; 162 return formatted; 163 } else { 164 return null; 165 } 166 } 167 } 168 169 170 string formatLiteral(ref immutable(char)* charPos) 171 in { 172 assert (beginsLiteral(*charPos)); 173 } body { 174 switch (*charPos) { 175 case '`': { // literal character 176 if (*++charPos == '\0') { 177 throw new SysTimeFormatException(E_MISSING_LITERAL); 178 } 179 uint len = UTF8stride[*charPos]; 180 scope(exit) charPos += len; 181 return charPos[0..len]; 182 } 183 184 case '\'': { // literal portion 185 immutable(char)* beginLiteral = ++charPos; 186 while (*charPos != '\'') { 187 if (*charPos == '\0') { 188 throw new SysTimeFormatException(E_UNCLOSED_LITERAL); 189 } 190 charPos++; 191 } 192 return beginLiteral[0 .. (charPos++) - beginLiteral]; 193 } 194 195 default: assert (false); 196 } 197 } 198 199 200 struct Piece { 201 dstring dtext; 202 string text; 203 ubyte type; // 0 = raw, 1 = literal; 2 = formatted 204 205 @property uint asNumber() { 206 if (dtext.length > 9) throw new SysTimeFormatException(E_OVERFLOW_WIDTH); 207 uint result; 208 foreach (c; dtext) { 209 assert (c >= '0' && c <= '9'); 210 result = result * 10 + (c - '0'); 211 } 212 return result; 213 } 214 } 215 216 217 string formatField(const SysTime dt, DayOfWeek dayOfWeek, 218 ref immutable(char)* charPos, out bool nonNull) { 219 Piece[] pieces; 220 221 // first parse the format string within the [...] 222 { 223 Piece[] tempPieces; 224 long lastNumber = int.min; 225 226 while (*charPos != ']') { 227 if (beginsElement(*charPos)) { 228 bool newNonNull; 229 tempPieces ~= Piece(null, formatElement(dt, dayOfWeek, charPos, newNonNull, lastNumber), 2); 230 if (newNonNull) nonNull = true; 231 } else if (beginsLiteral(*charPos)) { 232 tempPieces ~= Piece(null, formatLiteral(charPos), 1); 233 } else switch (*charPos) { 234 case '\0': 235 throw new SysTimeFormatException(E_UNCLOSED_FIELD); 236 237 case '}': 238 throw new SysTimeFormatException(E_UNOPENED_COLLAPSIBLE); 239 240 default: { 241 immutable(char)* begin = charPos; 242 do { 243 charPos++; 244 } while (*charPos != '\0' && *charPos != ']' && *charPos != '}' 245 && !beginsElement(*charPos) && !beginsLiteral(*charPos)); 246 247 tempPieces ~= Piece(null, begin[0 .. charPos - begin], 0); 248 } 249 } 250 } 251 252 /* convert tempPieces into a form in which 253 * - no two consecutive tempPieces have the same type 254 * - only non-literalised numbers have type 0 255 */ 256 ubyte lastType = ubyte.max; 257 258 foreach (piece; tempPieces) { 259 switch (piece.type) { 260 case 0: 261 foreach (dchar c; piece.text) { 262 if (isDigit(c)) { 263 if (lastType == 0) { 264 pieces[$-1].dtext ~= c; 265 } else { 266 pieces ~= Piece([c], null, 0); 267 lastType = 0; 268 } 269 } else { 270 if (lastType == 1) { 271 pieces[$-1].dtext ~= c; 272 } else { 273 pieces ~= Piece([c], null, 1); 274 lastType = 1; 275 } 276 } 277 } 278 break; 279 280 case 1: 281 if (lastType == 1) { 282 pieces[$-1].dtext ~= toUTF32(piece.text); 283 } else { 284 pieces ~= Piece(toUTF32(piece.text), null, 1); 285 lastType = 1; 286 } 287 break; 288 289 case 2: 290 if (lastType == 2) { 291 pieces[$-1].text ~= piece.text; 292 } else { 293 pieces ~= piece; 294 lastType = 2; 295 } 296 break; 297 298 default: 299 assert (false); 300 } 301 } 302 } 303 304 if (pieces.length < 2) throw new SysTimeFormatException(E_INCOMPLETE_FIELD); 305 306 // detect the field width/padding 307 dchar padLeft, padRight; 308 size_t fieldWidth = 0; 309 bool moreOnRight; 310 311 if (pieces[0].type == 0) { 312 // field width on left 313 if (pieces[$-1].type == 0) throw new SysTimeFormatException(E_DOUBLE_WIDTH); 314 315 fieldWidth = pieces[0].asNumber; 316 if (fieldWidth == 0) throw new SysTimeFormatException(E_ZERO_FIELD); 317 if (pieces[1].type != 1) throw new SysTimeFormatException(E_INCOMPLETE_FIELD); 318 319 pieces = pieces[1..$]; 320 padLeft = pieces[0].dtext[0]; 321 pieces[0].dtext = pieces[0].dtext[1..$]; 322 if (pieces[$-1].type == 1) { 323 padRight = pieces[$-1].dtext[$-1]; 324 pieces[$-1].dtext.length = pieces[$-1].dtext.length - 1; 325 } 326 327 } else if (pieces[$-1].type == 0) { 328 // field width on right 329 moreOnRight = true; 330 fieldWidth = pieces[$-1].asNumber; 331 if (fieldWidth == 0) throw new SysTimeFormatException(E_ZERO_FIELD); 332 if (pieces[$-2].type != 1) throw new SysTimeFormatException(E_INCOMPLETE_FIELD); 333 334 pieces = pieces[0..$-1]; 335 padRight = pieces[$-1].dtext[$-1]; 336 pieces[$-1].dtext.length = pieces[$-1].dtext.length - 1; 337 if (pieces[0].type == 1) { 338 padLeft = pieces[0].dtext[0]; 339 pieces[0].dtext = pieces[0].dtext[1..$]; 340 } 341 342 } else { 343 // field width given by number of padding characters 344 if (pieces[0].type == 1) { 345 padLeft = pieces[0].dtext[0]; 346 for (fieldWidth = 1; 347 fieldWidth < pieces[0].dtext.length && pieces[0].dtext[fieldWidth] == padLeft; 348 fieldWidth++) {} 349 pieces[0].dtext = pieces[0].dtext[fieldWidth..$]; 350 } 351 if (pieces[$-1].type == 1) { 352 padRight = pieces[$-1].dtext[$-1]; 353 size_t pos; 354 for (pos = pieces[$-1].dtext.length - 1; 355 pos > 0 && pieces[$-1].dtext[pos - 1] == padRight; 356 pos--) {} 357 if (pieces[$-1].dtext.length - pos > fieldWidth) moreOnRight = true; 358 fieldWidth += pieces[$-1].dtext.length - pos; 359 pieces[$-1].dtext.length = pos; 360 } 361 } 362 363 assert (fieldWidth != 0); 364 365 debug (datetimeformat) { 366 writefln("padding chars: %s %s.", padLeft == dchar.init ? "none" : [padLeft], 367 padRight == dchar.init ? "none" : [padRight]); 368 writefln("width: %d", fieldWidth); 369 writefln("%d pieces", pieces.length); 370 } 371 372 // read the field format - now use it 373 // but first, concatenate and measure the content 374 size_t contentLength; 375 string formattedContent; 376 foreach (piece; pieces) { 377 assert (piece.dtext.length == 0 || piece.text.length == 0); 378 if (piece.text.length == 0) { 379 formattedContent ~= toUTF8(piece.dtext); 380 contentLength += piece.dtext.length; 381 } else { 382 formattedContent ~= piece.text; 383 contentLength += toUCSindex(piece.text, piece.text.length); 384 } 385 } 386 debug (datetimeformat) writefln("content length %d: %s", contentLength, formattedContent); 387 388 if (contentLength > fieldWidth) { 389 throw new SysTimeFormatException(E_FIELD_OVERFLOW); 390 } 391 if (contentLength >= fieldWidth) return formattedContent; 392 assert (formattedContent.length == toUTFindex(formattedContent, contentLength)); 393 394 // distribute padding 395 ulong padWidth = fieldWidth - contentLength, padLeftWidth = 0, padRightWidth = 0; 396 if (padLeft == dchar.init) { 397 padRightWidth = padWidth; 398 } else if (padRight == dchar.init) { 399 padLeftWidth = padWidth; 400 } else { 401 padLeftWidth = padRightWidth = padWidth / 2; 402 if (padWidth % 2 == 1) { 403 if (moreOnRight) { 404 padRightWidth++; 405 } else { 406 padLeftWidth++; 407 } 408 } 409 } 410 debug (datetimeformat) writefln("Padding distribution: %d %d %d = %d", 411 padLeftWidth, contentLength, padRightWidth, fieldWidth); 412 assert (padLeftWidth + contentLength + padRightWidth == fieldWidth); 413 414 // now do it! 415 char[] result; 416 417 for (int i = 0; i < padLeftWidth; i++) encode(result, padLeft); 418 result ~= formattedContent; 419 for (int i = 0; i < padRightWidth; i++) encode(result, padRight); 420 return cast(string) result; 421 } 422 423 bool beginsElement(char c) { return isAlpha(c) || c == '[' || c == '{'; } 424 bool beginsLiteral(char c) { return c == '\'' || c == '`'; } 425 426 immutable string DIGITS12 = "110123456789"; 427 /+TEN = DIGITS12[1..3], 428 ELEVEN = DIGITS12[0..2], 429 TWELVE = DIGITS12[3..5];+/ 430 431 /*const E_BAD_UTF 432 = "Error in date/time format string: invalid UTF-8 sequence";*/ 433 immutable E_MISSING_LITERAL 434 = "Error in date/time format string: missing character after '`'"; 435 immutable E_UNCLOSED_LITERAL 436 = "Error in date/time format string: unterminated literal portion"; 437 immutable E_UNCLOSED_FIELD 438 = "Error in date/time format string: '[' without matching ']'"; 439 immutable E_UNCLOSED_COLLAPSIBLE 440 = "Error in date/time format string: '{' without matching '}'"; 441 immutable E_UNOPENED_FIELD 442 = "Error in date/time format string: ']' without matching '['"; 443 immutable E_UNOPENED_COLLAPSIBLE 444 = "Error in date/time format string: '}' without matching '{'"; 445 immutable E_INCOMPLETE_FIELD 446 = "Error in date/time format string: Incomplete alignment field"; 447 immutable E_ZERO_FIELD 448 = "Error in date/time format string: Zero-width alignment field"; 449 immutable E_DOUBLE_WIDTH 450 = "Error in date/time format string: Width of alignment field doubly specified"; 451 immutable E_OVERFLOW_WIDTH 452 = "Error in date/time format string: Field width too large"; 453 immutable E_FIELD_OVERFLOW 454 = "Date/time formatting failed: Insufficient field width to hold content"; 455 immutable E_BC_YY 456 = "Date/time formatting failed: Format 'yy' for BC dates undefined"; 457 immutable E_INVALID_DATE_TIME 458 = "Date/time formatting failed: Invalid date/time"; 459 460 string formatBySpec(const SysTime dt, DayOfWeek dow, 461 string spec, ref long lastNumber) { 462 switch (spec) { 463 464 case "yy": 465 lastNumber = dt.year; 466 if (dt.year <= 0) { 467 throw new SysTimeFormatException(E_BC_YY); 468 } 469 return formatTwoDigit(cast(byte) (dt.year % 100)); 470 471 case "yyy": 472 lastNumber = dt.year; 473 return to!string(dt.year > 0 ? dt.year : (lastNumber = 1 - dt.year)); 474 475 case "yyyy": 476 lastNumber = dt.year; 477 return format("%04d", dt.year > 0 ? dt.year : 478 (lastNumber = 1 - dt.year)); 479 480 case "YYY": 481 lastNumber = dt.year < 0 ? -cast(int)dt.year : dt.year; // dt.year.min remains the same 482 return to!string(dt.year); 483 484 case "b": 485 return (dt.year == dt.year.min || dt.year > 0) ? null : "bc"; 486 487 case "bb": 488 return dt.year > 0 ? "ad" : "bc"; 489 490 case "bbb": 491 return dt.year > 0 ? "ce" : "bce"; 492 493 case "bbbb": 494 return (dt.year == dt.year.min || dt.year > 0) ? null : "bce"; 495 496 case "B": 497 return (dt.year == dt.year.min || dt.year > 0) ? null : "BC"; 498 499 case "BB": 500 return dt.year > 0 ? "AD" : "BC"; 501 502 case "BBB": 503 return dt.year > 0 ? "CE" : "BCE"; 504 505 case "BBBB": 506 return (dt.year == dt.year.min || dt.year > 0) ? null : "BCE"; 507 508 case "m": 509 lastNumber = dt.month; 510 return format12(dt.month); 511 512 case "mm": 513 lastNumber = dt.month; 514 char[] fmt = new char[2]; 515 if (dt.month < 10) { 516 fmt[0] = '0'; 517 fmt[1] = cast(char) ('0' + dt.month); 518 } else { 519 fmt[0] = '1'; 520 fmt[1] = cast(char) ('0' - 10 + dt.month); 521 } 522 return cast(string) fmt; 523 524 case "mmm": 525 return SHORT_L_MONTH_NAME[dt.month]; 526 527 case "Mmm": 528 return SHORT_MONTH_NAME[dt.month]; 529 530 case "MMM": 531 return SHORT_U_MONTH_NAME[dt.month]; 532 533 case "mmmm": 534 return LONG_L_MONTH_NAME[dt.month]; 535 536 case "Mmmm": 537 return LONG_MONTH_NAME[dt.month]; 538 539 case "MMMM": 540 return LONG_U_MONTH_NAME[dt.month]; 541 542 case "d": 543 lastNumber = dt.day; 544 return to!string(dt.day); 545 546 case "dd": 547 lastNumber = dt.day; 548 return formatTwoDigit(dt.day); 549 550 case "t": 551 return ordinalSuffix(lastNumber, false); 552 553 case "T": 554 return ordinalSuffix(lastNumber, true); 555 556 case "www": 557 return SHORT_L_DAY_NAME[dow]; 558 559 case "Www": 560 debug (datetimeformat) writefln("Day of week: %d", cast(byte) dow); 561 return SHORT_DAY_NAME[dow]; 562 563 case "WWW": 564 return SHORT_U_DAY_NAME[dow]; 565 566 case "wwww": 567 return LONG_L_DAY_NAME[dow]; 568 569 case "Wwww": 570 return LONG_DAY_NAME[dow]; 571 572 case "WWWW": 573 return LONG_U_DAY_NAME[dow]; 574 575 case "h": 576 lastNumber = dt.hour; 577 if (dt.hour == 0) { 578 return DIGITS12[3..5]; 579 } else if (dt.hour <= 12) { 580 return format12(dt.hour); 581 } else { 582 return format12(dt.hour - 12); 583 } 584 585 case "hh": 586 lastNumber = dt.hour; 587 if (dt.hour == 0) { 588 return DIGITS12[3..5]; 589 } else if (dt.hour <= 12) { 590 return formatTwoDigit(dt.hour); 591 } else { 592 return formatTwoDigit(dt.hour - 12); 593 } 594 595 case "H": 596 lastNumber = dt.hour; 597 return to!string(dt.hour); 598 599 case "HH": 600 lastNumber = dt.hour; 601 return formatTwoDigit(dt.hour); 602 603 case "a": 604 return dt.hour < 12 ? "a" : "p"; 605 606 case "aa": 607 return dt.hour < 12 ? "am" : "pm"; 608 609 case "A": 610 return dt.hour < 12 ? "A" : "P"; 611 612 case "AA": 613 return dt.hour < 12 ? "AM" : "PM"; 614 615 case "i": 616 lastNumber = dt.minute; 617 return to!string(dt.minute); 618 619 case "ii": 620 lastNumber = dt.minute; 621 return formatTwoDigit(dt.minute); 622 623 case "s": 624 lastNumber = dt.second; 625 return to!string(dt.second); 626 627 case "ss": 628 lastNumber = dt.second; 629 return formatTwoDigit(dt.second); 630 631 case "f": 632 lastNumber = dt.fracSecs().total!"msecs" / 100; 633 return DIGITS12[cast(size_t)(lastNumber+2) .. cast(size_t)(lastNumber+3)]; 634 635 case "ff": 636 lastNumber = dt.fracSecs().total!"msecs" / 10; 637 return to!string(lastNumber); 638 639 case "FF": 640 lastNumber = dt.fracSecs().total!"msecs" / 10; 641 return formatTwoDigit(cast(byte) lastNumber); 642 643 case "fff": 644 lastNumber = dt.fracSecs().total!"msecs"; 645 return to!string(dt.fracSecs().total!"msecs"); 646 647 case "FFF": 648 lastNumber = dt.fracSecs().total!"msecs"; 649 return format("%03d", dt.fracSecs().total!"msecs"); 650 651 652 /* 653 case "zzzz": 654 return dt.hour == dt.hour.min ? null : 655 timezone().utcOffsetAt(stdTime) >= 0 ? 656 format("+%02d%02d", timezone().utcOffsetAt(stdTime) / 60, 657 timezone().utcOffsetAt(stdTime) % 60) : 658 format("-%02d%02d", -timezone().utcOffsetAt(stdTime) / 60, 659 -timezone().utcOffsetAt(stdTime) % 60); 660 */ 661 662 default: 663 throw new SysTimeFormatException(cast(string) 664 ("Error in date/time format string: Undefined format specifier '" ~ spec ~ "'")); 665 } 666 } 667 668 string formatTwoDigit(int b) 669 in { 670 assert (b == byte.min || (b >= 0 && b <= 99)); 671 } body { 672 if (b == byte.min) return null; 673 char[] fmt = new char[2]; 674 fmt[0] = cast(byte) ('0' + b / 10); 675 fmt[1] = cast(byte) ('0' + b % 10); 676 return cast(string) fmt; 677 } 678 679 string format12(int b) 680 in { 681 assert (b >= 0); 682 assert (b <= 12); 683 } body { 684 switch (b) { 685 case 10: return DIGITS12[1..3]; 686 case 11: return DIGITS12[0..2]; 687 case 12: return DIGITS12[3..5]; 688 default: return DIGITS12[2+b .. 3+b]; 689 } 690 } 691 692 string ordinalSuffix(long lastNumber, bool upperCase) { 693 if (lastNumber < 0) return null; 694 lastNumber %= 100; 695 if (lastNumber >= 4 && lastNumber <= 20) { 696 return upperCase ? "TH" : "th"; 697 } 698 switch (lastNumber % 10) { 699 case 1: 700 return upperCase ? "ST" : "st"; 701 702 case 2: 703 return upperCase ? "ND" : "nd"; 704 705 case 3: 706 return upperCase ? "RD" : "rd"; 707 708 default: 709 return upperCase ? "TH" : "th"; 710 } 711 } 712 } 713 714 715 /// Exception thrown if there was a problem in formatting a date or time. 716 class SysTimeFormatException : Exception { 717 private this(string msg) { 718 super(msg); 719 } 720 } 721 722 723 /// Short (three-letter) names of the days of the week. 724 immutable string[7] SHORT_L_DAY_NAME = [ 725 DayOfWeek.sun: "sun", "mon", "tue", "wed", "thu", "fri", "sat" 726 ]; 727 728 /// Short (three-letter) names of the days of the week. 729 immutable string[7] SHORT_U_DAY_NAME = [ 730 DayOfWeek.sun: "SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT" 731 ]; 732 733 /// Full names of the days of the week. 734 immutable string[7] LONG_L_DAY_NAME = [ 735 DayOfWeek.sun: "sunday", "monday", "tuesday", "wednesday", 736 "thursday", "friday", "saturday" 737 ]; 738 739 /// Full names of the days of the week. 740 immutable string[7] LONG_U_DAY_NAME = [ 741 DayOfWeek.sun: "SUNDAY", "MONDAY", "TUESDAY", "WEDNESDAY", 742 "THURSDAY", "FRIDAY", "SATURDAY" 743 ]; 744 745 /// Short (three-letter) names of the months of the year. 746 immutable string[13] SHORT_L_MONTH_NAME = [ 747 ['\xFF', '\xFF', '\xFF'], 748 Month.jan: "jan", "feb", "mar", "apr", "may", "jun", 749 "jul", "aug", "sep", "oct", "nov", "dec" 750 ]; 751 752 /// Short (three-letter) names of the months of the year. 753 immutable string[13] SHORT_U_MONTH_NAME = [ 754 ['\xFF', '\xFF', '\xFF'], 755 Month.jan: "JAN", "FEB", "MAR", "APR", "MAY", "JUN", 756 "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" 757 ]; 758 759 /// Full names of the months of the year. 760 immutable string[13] LONG_L_MONTH_NAME = [ 761 null, Month.jan: "january", "february", "march", "april", "may", "june", 762 "july", "august", "september", "october", "november", "december" 763 ]; 764 765 /// Full names of the months of the year. 766 immutable string[13] LONG_U_MONTH_NAME = [ 767 null, Month.jan: "JANUARY", "FEBRUARY", "MARCH", "APRIL", "MAY", "JUNE", 768 "JULY", "AUGUST", "SEPTEMBER", "OCTOBER", "NOVEMBER", "DECEMBER" 769 ]; 770 771 unittest { 772 import std.datetime; 773 import std.stdio; 774 775 writefln("Unittest commenced at %s", Clock.currTime.toString); 776 777 SysTime dt = SysTime(DateTime(2005, 9, 8, 16, 51, 9), dur!"msecs"(427)); 778 // basic formatting 779 assert (dt.format("dd/mm/yy") == "08/09/05"); 780 assert (dt.format("Www dt Mmm yyyy BB") == "Thu 8th Sep 2005 AD"); 781 assert (dt.format("h:ii AA") == "4:51 PM"); 782 assert (dt.format("yyyy-mm-dd HH:ii:ss") == "2005-09-08 16:51:09"); 783 assert (dt.format("HH:ii:ss.FFF") == "16:51:09.427"); 784 // alignment fields 785 assert (dt.format("[------Wwww.....]") == "--Thursday."); 786 assert (dt.format("[11-Wwww.]") == "--Thursday."); 787 assert (dt.format("[-----Wwww......]") == "-Thursday.."); 788 assert (dt.format("[-Wwww.11]") == "-Thursday.."); 789 assert (dt.format("[9`1Www]") == "111111Thu"); 790 assert (dt.format("[`1Wwww-10]") == "1Thursday-"); 791 assert (dt.format("[d/m/yyy ]HH:ii:ss") == "8/9/2005 16:51:09"); 792 793 assert (dt.format("d Mmm yyy{ B}{ HH:ii:ss}") == "8 Sep 2005 16:51:09"); 794 assert (dt.format("{d }{Mmm }yyy BB") == "8 Sep 2005 AD"); 795 assert (dt.format("HH:ii{:ss}{.FFF}") == "16:51:09.427"); 796 797 assert (dt.format("HH:ii{:ss}{.FFF}") == "16:51:09.427"); 798 dt.fracSecs(dur!"msecs"(0)); 799 assert (dt.format("HH:ii{:ss}{.FFF}") == "16:51:09.000"); 800 dt.second = 0; 801 assert (dt.format("HH:ii{:ss}{.FFF}") == "16:51:00.000"); 802 assert (dt.format("d Mmm yyy{ B}{ HH:ii:ss}") == "8 Sep 2005 16:51:00"); 803 dt.hour = 0; 804 assert (dt.format("d Mmm yyy{ B}{ HH:ii:ss}") == "8 Sep 2005 00:51:00"); 805 dt.minute = 0; 806 assert (dt.format("d Mmm yyy{ B}{ HH:ii:ss}") == "8 Sep 2005 00:00:00"); 807 assert (dt.format("{d }{Mmm }yyy BB") == "8 Sep 2005 AD"); 808 dt.month = Month.min; 809 assert (dt.format("{d }{Mmm }yyy BB") == "8 Jan 2005 AD"); 810 dt.day = 1; 811 assert (dt.format("{d }{Mmm }yyy BB") == "1 Jan 2005 AD"); 812 813 dt.month = Month.sep; 814 dt.day = 8; 815 816 // nesting of fields and collapsible portions 817 assert (dt.format("[13 Mmmm [d..]]") == " September 8."); 818 assert (dt.format("[13 Mmmm{ d}]") == " September 8"); 819 dt.day = 1; 820 assert (dt.format("[13 Mmmm{ d}]") == " September 1"); 821 assert (dt.format("{[13 Mmmm{ d}]}") == " September 1"); 822 dt.month = Month.min; 823 assert (dt.format("{[13 Mmmm{ d}]}") == " January 1"); 824 dt.day = 8; 825 assert (dt.format("{[13 Mmmm{ d}]}") == " January 8"); 826 }