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 }