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