1 /** D date/time format functions.
2 *
3 *    Authors: Vitaly Livshic, shiche@yandex.ru
4 *    Bugs: Uses C setLocale which uses system localization via dlocale library.
5 *    License: LGPLv3 
6 */
7 module dateformat;
8 @safe
9 
10 import std.datetime;
11 import std.conv;
12 import std.string : fromStringz;
13 import std.string : toStringz;
14 import core.stdc.time;
15 import core.stdc.locale;
16 static import std.string;
17 import std.math : abs;
18 
19 import dlocale;
20 
21 private immutable auto startOfTimes = DateTime(1, 1, 1, 0, 0, 0);
22 
23 /** 
24  * Full date.
25  * Examples:
26  * --------------------
27  * format(Clock.currTime()); // Gives Fr 06 Nov 2020 23:17:42 MSK for example
28  * --------------------
29 */
30 export string STANDARD_FORMAT = "%c %Z";
31 /** 
32  * Full date without timezone.
33  * Examples:
34  * --------------------
35  * auto dt = DateTime(2020, 11, 6, 23, 17, 42);
36  * format(dt); // Gives Fr 06 Nov 2020 23:17:42 for example
37  * --------------------
38 */
39 export string STANDARD_FORMAT_WITHOUT_TIMEZONE = "%c";
40 /// Full date-only, without timezone
41 version(Windows)
42 	export string STANDARD_DATE_FORMAT_WITHOUT_TIMEZONE = "%A, %B %d, %Y";	
43 else
44 	export string STANDARD_DATE_FORMAT_WITHOUT_TIMEZONE = "%a %d %b %Y";
45 	
46 /**
47  * Short date format.
48  *  Examples:
49  * --------------------
50  * auto dt = DateTime(2020, 11, 6, 23, 17, 42);
51  * formatShortDate(dt); // Gives 06.11.2020 for example
52  * --------------------
53 */
54 export string SHORT_DATE_FORMAT = "%x";
55 /**
56  * Short date format.
57  *  Examples:
58  * --------------------
59  * auto dt = DateTime(2020, 11, 6, 23, 17, 42);
60  * formatShortDateTime(dt); // Gives 06.11.2020 23:17:42 for example
61  * --------------------
62 */
63 export string SHORT_DATETIME_FORMAT = "%x %X";
64 
65 /// Use system locale
66 static this()
67 {
68     setlocale(LC_ALL, "");
69 }
70 
71 /// Format date with current locale.
72 export string format(SysTime date, Locale locale = getDefaultLocale)
73 {
74     return format(date, STANDARD_FORMAT, locale);
75 }
76 
77 /// Format date with current locale without timezone.
78 export string format(DateTime date, Locale locale = getDefaultLocale)
79 {
80     return format(cast(SysTime)date, STANDARD_FORMAT_WITHOUT_TIMEZONE, locale);
81 }
82 
83 /// Format date with current locale without timezone.
84 export string format(Date date, Locale locale = getDefaultLocale)
85 {
86     return format(cast(SysTime)date, STANDARD_DATE_FORMAT_WITHOUT_TIMEZONE, locale);
87 }
88 
89 /// Get short date-only and current locale
90 export string formatShortDate(SysTime date, Locale locale = getDefaultLocale)
91 {
92     return format(date, "%x", locale);
93 }
94 
95 /// Get short date-only and current locale
96 export string formatShortDate(DateTime date, Locale locale = getDefaultLocale)
97 {
98     return formatShortDate(cast(SysTime)date, locale);
99 }
100 
101 /// Get short date-only and current locale
102 export string formatShortDate(Date date, Locale locale = getDefaultLocale)
103 {
104     return format(cast(SysTime)date, "%x", locale);
105 }
106 
107 /// Get short datetime and current locale
108 export string formatShortDateTime(SysTime date, Locale locale = getDefaultLocale)
109 {
110     return format(date, "%x %X", locale);
111 }
112 
113 /// Get short datetime and current locale
114 export string formatShortDateTime(DateTime date, Locale locale = getDefaultLocale)
115 {
116     return formatShortDateTime(cast(SysTime)date, locale);
117 }
118 
119 /** Format date with specific mask and current locale.
120 *   Params:
121 *       date = date to format
122 *       formatString = format string, like as for C strftime function.
123 *       locale = locale to format
124 */
125 export string format(DateTime date, string formatString, Locale locale = getDefaultLocale)
126 {
127     return format(cast(SysTime)date, formatString, locale);
128 }
129 
130 /** Format date with specific mask and current locale.
131 *   Params:
132 *       date = date to format
133 *       formatString = format string, like as for C strftime function.
134 *       locale = locale to format
135 */
136 export string format(Date date, string formatString, Locale locale = getDefaultLocale)
137 {
138     return format(cast(SysTime)date, formatString, locale);
139 }
140 
141 /** Format date with specific mask and current locale.
142 *    Params:
143 *       date = date to format
144 *       format = format string, like as for C strftime function.
145 *       locale = locale to format
146 *   Bugs:
147 *       some modifiers is not currenly supported: %G, %E, %O, %V, %+, %N
148 *		Windows - formats with no leading zero replaced by leading zero
149 */
150 export string format(SysTime date, string format, Locale locale = getDefaultLocale)
151 {
152     // Null date
153     if (cast(DateTime)date == startOfTimes)
154         return "";
155 
156     char[] result;
157     while (format.length)
158     {
159         if (format[0] != '%')
160         {
161             result ~= format[0];
162             format = format[1 .. $ ];
163         } else
164         {
165             format = format[1 .. $ ];
166             char ch = format[0];
167             format = format[1 .. $ ];
168 
169             switch (ch)
170             {
171                 case '%':
172                     result ~= ch;
173                     break;
174                 case 'a':
175                     result ~= locale.abbrWeekDays[date.dayOfWeek];
176                     break;
177                 case 'A':
178                     result ~= locale.weekDays[date.dayOfWeek];
179                     break;
180                 case 'b': 
181                 case 'h':
182                     result ~= locale.abbrMonthes[date.month - 1];
183                     break;
184                 case 'B':
185                     result ~= locale.monthes[date.month - 1];
186                     break;
187                 case 'c':
188                     result ~= date.format(locale.dateTime, locale);
189                     break;
190                 case 'C':
191                     result ~= to!string(date.year)[ 0 .. $ - 2 ];
192                     break;
193                 case 'd':
194                     result ~= general(date.day);
195                     break;
196                 case 'e':
197                     result ~= spaced(date.day);
198                     break;
199                 case 'D':
200                     result ~= std..string.format("%s/%s/%d", general(date.month), general(date.day), date.year);
201                     break;
202                 case 'F':
203                     result ~= std..string.format("%d-%s-%s", date.year, general(date.month), general(date.day));
204                     break;
205                 case 'g':
206                     result ~= to!string(date.year)[ $ - 2 .. $ ]; 
207                     break;
208                 case 'G':
209                     result ~= to!string(date.year); 
210                     break;
211                 case 'H':
212                     result ~= general(date.hour); 
213                     break;
214                 case 'I':
215                     result ~= general(date.hour > 12 ? date.hour - 12 : date.hour); 
216                     break;
217                 case 'j':
218                     result ~= general(date.dayOfYear); 
219                     break;
220                 case 'k':
221                     result ~= spaced(date.hour); 
222                     break;
223                 case 'l':
224                     result ~= spaced(date.hour > 12 ? date.hour - 12 : date.hour); 
225                     break;
226                 case 'm':
227                     result ~= general(date.month); 
228                     break;
229                 case 'M':
230                     result ~= general(date.minute); 
231                     break;
232                 case 'n':
233                     result ~= '\n';
234                     break;
235                 case 'p':
236                     result ~= date.hour > 12 ? locale.pm : locale.am;
237                     break;
238                 case 'P':
239                     result ~= std..string.toLower(date.hour > 12 ? locale.pm : locale.am);
240                     break;
241                 case 'q':
242                     auto m = date.month;
243                     result ~= to!string(m <= Month.mar ? 1 : (m > Month.mar && m <= Month.jun ? 2 : (m >= Month.oct ? 4 : 3)));
244                     break;
245                 case 'r':
246                     result ~= std..string.format("%s:%s:%s %s", 
247                         general(date.hour > 12 ? date.hour - 12 : date.hour), 
248                         general(date.minute), general(date.second),
249                         date.hour > 12 ? locale.pm : locale.am);
250                     break;
251                 case 'R':
252                     result ~= general(date.hour) ~ ":" ~ general(date.minute);
253                     break;
254                 case 's':
255                     result ~= to!string(date.toUnixTime);
256                     break;
257                 case 'S':
258                     result ~= to!string(date.second);
259                     break;
260                 case 't':
261                     result ~= '\t';
262                     break;
263                 case 'T':
264                     result ~= general(date.hour) ~ ':' ~ general(date.minute) ~ ':' ~ general(date.second);
265                     break;
266                 case 'u':
267                     result ~= to!string(date.dayOfWeek == DayOfWeek.sun ? 7 : date.dayOfWeek); 
268                     break;
269                 case 'U':
270                     result ~= to!string(date.isoWeek);
271                     break;
272                 case 'w':
273                     result ~= to!string(cast(int)date.dayOfWeek);
274                     break;
275                 case 'W':
276                     result ~= to!string(date.isoWeek);
277                     break;
278                 case 'x':
279                     result ~= date.format(locale.date, locale);
280                     break;
281                 case 'X':
282                     result ~= date.format(locale.time, locale);
283                     break;
284                 case 'y':
285                     result ~= to!string(date.year)[ $ - 2 .. $];
286                     break;
287                 case 'Y':
288                     result ~= to!string(date.year);
289                     break;
290                 case 'z':
291                     int zHours, zMinutes;
292                     date.utcOffset.split!("hours", "minutes")(zHours, zMinutes);
293                     result ~= generalSigned(zHours) ~ general(zMinutes);
294                     break;
295                 case 'Z':
296                     result ~= date.timezone.stdName;
297                     break;
298                 default:
299                     result ~= "%" ~ ch;
300             }
301         }
302     }
303 
304     return to!string(result);
305 }
306 
307 /// Adds leading zero to one-digit natural numbers.
308 private string general(int arg)
309 {
310     arg = abs(arg);
311     return arg < 10 ? "0" ~ to!string(arg) : to!string(arg);
312 }
313 
314 /// Adds leading zero to one-digit integers.
315 private string generalSigned(int arg)
316 {
317     if (arg == 0)
318         return "00";
319     string result = (arg > 0 ? "+" : "") ~ to!string(arg);
320     if (result.length < 3)
321         result = result[ 0 .. 1 ] ~ "0" ~ result[ 1 .. $ ];
322     return result;    
323 }
324 
325 
326 /// Adds leading space to one-digit natural numbers.
327 private string spaced(int arg)
328 {
329     return arg < 10 ? " " ~ to!string(arg) : to!string(arg);
330 }
331 
332 unittest
333 {
334     import std.stdio;
335 
336     writeln("\nFormat test");
337     writeln("Default system locale is " ~ getDefaultLocale.name);
338 
339     Locale locale;
340     version(Posix)
341         locale = initDateformatLocale("C");
342     else
343         version(Windows)
344             locale = initDateformatLocale("en");
345         else
346             locale = initDateformatLocale("en-US");
347     setDefaultLocale(locale.name);
348     writeln("Test's locale is " ~ locale.name);
349 
350     // Test format routines
351     SysTime date;
352     assert(format(cast(SysTime)startOfTimes, "") == "");
353     
354     date = SysTime(DateTime(2020, 12, 7, 17, 45, 10), UTC());
355     auto morning = SysTime(DateTime(2020, 12, 7, 9, 21, 14), UTC());
356 
357     assert(date.format("%%") == "%");
358     
359     assert(date.format("%a") == "Mon");
360     assert(date.format("%A") == "Monday");
361     assert(date.format("%a%A lala") == "MonMonday lala");
362 
363     assert(date.format("%b") == "Dec");
364     assert(date.format("%h") == "Dec");
365     assert(date.format("%B") == "December");
366 
367 	version(Windows)
368 	{
369 		assert(date.format("%c") == "Monday, December 07, 2020 05:45");
370 
371         assert(date.format(locale) == "Monday, December 07, 2020 05:45 UTC");
372         assert((cast(DateTime)date).format(locale) == "Monday, December 07, 2020 05:45");
373         assert((cast(Date)date).format(locale) == "Monday, December 07, 2020");
374         
375         assert(date.formatShortDate(locale) == "12/07/2020");
376         assert((cast(DateTime)date).formatShortDate(locale) == "12/07/2020");
377         assert((cast(Date)date).formatShortDate(locale) == "12/07/2020");
378 
379         assert(date.formatShortDateTime(locale) == "12/07/2020 05:45");
380         assert((cast(DateTime)date).formatShortDateTime(locale) == "12/07/2020 05:45");
381 		
382 		assert(morning.format("%c") == "Monday, December 07, 2020 09:21");
383 	}
384 	else
385 	{
386 		assert(date.format("%c") == "Mon Dec  7 17:45:10 2020");
387 
388         assert(date.format(locale) == "Mon Dec  7 17:45:10 2020 UTC");
389         assert((cast(DateTime)date).format(locale) == "Mon Dec  7 17:45:10 2020");
390         assert((cast(Date)date).format(locale) == "Mon 07 Dec 2020");
391         
392         assert(date.formatShortDate(locale) == "12/07/20");
393         assert((cast(DateTime)date).formatShortDate(locale) == "12/07/20");
394         assert((cast(Date)date).formatShortDate(locale) == "12/07/20");
395 
396         assert(date.formatShortDateTime(locale) == "12/07/20 17:45:10");
397         assert((cast(DateTime)date).formatShortDateTime(locale) == "12/07/20 17:45:10");
398 
399 		assert(morning.format("%c") == "Mon Dec  7 09:21:14 2020");
400 	}
401 
402     assert(date.format("%C") == "20");
403 
404     assert(date.format("%d") == "07");
405     assert(date.format("%e") == " 7");
406 
407     assert(date.format("%D") == "12/07/2020");
408     assert(date.format("%F") == "2020-12-07");
409 
410     assert(date.format("%g") == "20");
411     assert(date.format("%G") == "2020");
412 
413     assert(date.format("%H") == "17");
414     assert(date.format("%I") == "05");
415 
416     assert(date.format("%j") == "342");
417     assert(date.format("%k") == "17");
418     assert(morning.format("%k") == " 9");
419     assert(date.format("%l") == " 5");
420 
421     assert(date.format("%m") == "12");
422 
423     assert(date.format("%M") == "45");
424     
425     assert(date.format("%n") == "\n");
426 
427     assert(date.format("%p") == "PM");
428     assert(date.format("%P") == "pm");
429     assert(morning.format("%p") == "AM");
430     assert(morning.format("%P") == "am");
431 
432     assert(date.format("%q") == "4");
433     auto moment = SysTime(DateTime(2020, 3, 8, 14, 33, 52), UTC());
434     assert(moment.format("%q") == "1");
435     moment.add!"months"(1);
436     assert(moment.format("%q") == "2");
437     moment.add!"months"(3);
438     assert(moment.format("%q") == "3");
439 
440     assert(date.format("%r") == "05:45:10 PM");
441     assert(morning.format("%r") == "09:21:14 AM");
442 
443     assert(date.format("%R") == "17:45");
444     assert(morning.format("%R") == "09:21");
445 
446     assert(date.format("%s") == "1607363110");
447 
448     assert(date.format("%S") == "10");
449 
450     assert(date.format("%t") == "\t");
451 
452     assert(date.format("%T") == "17:45:10");
453     assert(morning.format("%T") == "09:21:14");
454 
455     assert(date.format("%u") == "1");
456     moment = SysTime(DateTime(2020, 3, 8, 14, 33, 52), UTC());
457     assert(moment.format("%u") == "7");
458 
459     assert(moment.format("%U") == "10");
460     assert(date.format("%U") == "50");
461     
462     assert(date.format("%w") == "1");
463     assert(date.format("%W") == "50");
464 
465     version(Windows)
466 	{
467 		assert(date.format("%x") == "12/07/2020");
468 		assert(date.format("%X") == "05:45");
469 	}
470 	else
471 	{
472 		assert(date.format("%x") == "12/07/20");
473 		assert(date.format("%X") == "17:45:10");
474 	}
475 
476     assert(date.format("%y") == "20");
477     moment = SysTime(DateTime(2012, 5, 16, 14, 14, 41), UTC());
478     assert(moment.format("%y") == "12");
479     assert(date.format("%Y") == "2020");
480     assert(moment.format("%Y") == "2012");
481 
482     assert(date.format("%z") == "0000");
483     assert(date.format("%Z") == "UTC");
484 }