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     writeln("Default system locale is " ~ getDefaultLocale.name);
336 
337     Locale locale;
338     version(Posix)
339         locale = initDateformatLocale("C");
340     else
341         version(Windows)
342             locale = initDateformatLocale("en");
343         else
344             locale = initDateformatLocale("en-US");
345     setDefaultLocale(locale.name);
346     writeln("Test's locale is " ~ locale.name);
347 
348     assert(locale.weekDays[0] == "Sunday");
349     assert(locale.weekDays[1] == "Monday");
350     assert(locale.weekDays[2] == "Tuesday");
351     assert(locale.weekDays[3] == "Wednesday");
352     assert(locale.weekDays[4] == "Thursday");
353     assert(locale.weekDays[5] == "Friday");
354     assert(locale.weekDays[6] == "Saturday");
355     
356     assert(locale.abbrWeekDays[0] == "Sun");
357     assert(locale.abbrWeekDays[1] == "Mon");
358     assert(locale.abbrWeekDays[2] == "Tue");
359     assert(locale.abbrWeekDays[3] == "Wed");
360     assert(locale.abbrWeekDays[4] == "Thu");
361     assert(locale.abbrWeekDays[5] == "Fri");
362     assert(locale.abbrWeekDays[6] == "Sat");
363 
364     assert(locale.monthes[0] == "January");
365     assert(locale.monthes[1] == "February");
366     assert(locale.monthes[2] == "March");
367     assert(locale.monthes[3] == "April");
368     assert(locale.monthes[4] == "May");
369     assert(locale.monthes[5] == "June");
370     assert(locale.monthes[6] == "July");
371     assert(locale.monthes[7] == "August");
372     assert(locale.monthes[8] == "September");
373     assert(locale.monthes[9] == "October");
374     assert(locale.monthes[10] == "November");
375     assert(locale.monthes[11] == "December");
376 
377     assert(locale.abbrMonthes[0] == "Jan");
378     assert(locale.abbrMonthes[1] == "Feb");
379     assert(locale.abbrMonthes[2] == "Mar");
380     assert(locale.abbrMonthes[3] == "Apr");
381     assert(locale.abbrMonthes[4] == "May");
382     assert(locale.abbrMonthes[5] == "Jun");
383     assert(locale.abbrMonthes[6] == "Jul");
384     assert(locale.abbrMonthes[7] == "Aug");
385     assert(locale.abbrMonthes[8] == "Sep");
386     assert(locale.abbrMonthes[9] == "Oct");
387     assert(locale.abbrMonthes[10] == "Nov");
388     assert(locale.abbrMonthes[11] == "Dec");
389 
390     // Test new format routines
391     SysTime date;
392     assert(format(cast(SysTime)startOfTimes, "") == "");
393     
394     date = SysTime(DateTime(2020, 12, 7, 17, 45, 10), UTC());
395     auto morning = SysTime(DateTime(2020, 12, 7, 9, 21, 14), UTC());
396 
397     assert(date.format("%%") == "%");
398     
399     assert(date.format("%a") == "Mon");
400     assert(date.format("%A") == "Monday");
401     assert(date.format("%a%A lala") == "MonMonday lala");
402 
403     assert(date.format("%b") == "Dec");
404     assert(date.format("%h") == "Dec");
405     assert(date.format("%B") == "December");
406 
407 	version(Windows)
408 	{
409 		assert(date.format("%c") == "Monday, December 07, 2020 05:45");
410 
411         assert(date.format(locale) == "Monday, December 07, 2020 05:45 UTC");
412         assert((cast(DateTime)date).format(locale) == "Monday, December 07, 2020 05:45");
413         assert((cast(Date)date).format(locale) == "Monday, December 07, 2020");
414         
415         assert(date.formatShortDate(locale) == "12/07/2020");
416         assert((cast(DateTime)date).formatShortDate(locale) == "12/07/2020");
417         assert((cast(Date)date).formatShortDate(locale) == "12/07/2020");
418 
419         assert(date.formatShortDateTime(locale) == "12/07/2020 05:45");
420         assert((cast(DateTime)date).formatShortDateTime(locale) == "12/07/2020 05:45");
421 		
422 		assert(morning.format("%c") == "Monday, December 07, 2020 09:21");
423 	}
424 	else
425 	{
426 		assert(date.format("%c") == "Mon Dec  7 17:45:10 2020");
427 
428         assert(date.format(locale) == "Mon Dec  7 17:45:10 2020 UTC");
429         assert((cast(DateTime)date).format(locale) == "Mon Dec  7 17:45:10 2020");
430         assert((cast(Date)date).format(locale) == "Mon 07 Dec 2020");
431         
432         assert(date.formatShortDate(locale) == "12/07/20");
433         assert((cast(DateTime)date).formatShortDate(locale) == "12/07/20");
434         assert((cast(Date)date).formatShortDate(locale) == "12/07/20");
435 
436         assert(date.formatShortDateTime(locale) == "12/07/20 17:45:10");
437         assert((cast(DateTime)date).formatShortDateTime(locale) == "12/07/20 17:45:10");
438 
439 		assert(morning.format("%c") == "Mon Dec  7 09:21:14 2020");
440 	}
441 
442     assert(date.format("%C") == "20");
443 
444     assert(date.format("%d") == "07");
445     assert(date.format("%e") == " 7");
446 
447     assert(date.format("%D") == "12/07/2020");
448     assert(date.format("%F") == "2020-12-07");
449 
450     assert(date.format("%g") == "20");
451     assert(date.format("%G") == "2020");
452 
453     assert(date.format("%H") == "17");
454     assert(date.format("%I") == "05");
455 
456     assert(date.format("%j") == "342");
457     assert(date.format("%k") == "17");
458     assert(morning.format("%k") == " 9");
459     assert(date.format("%l") == " 5");
460 
461     assert(date.format("%m") == "12");
462 
463     assert(date.format("%M") == "45");
464     
465     assert(date.format("%n") == "\n");
466 
467     assert(date.format("%p") == "PM");
468     assert(date.format("%P") == "pm");
469     assert(morning.format("%p") == "AM");
470     assert(morning.format("%P") == "am");
471 
472     assert(date.format("%q") == "4");
473     auto moment = SysTime(DateTime(2020, 3, 8, 14, 33, 52), UTC());
474     assert(moment.format("%q") == "1");
475     moment.add!"months"(1);
476     assert(moment.format("%q") == "2");
477     moment.add!"months"(3);
478     assert(moment.format("%q") == "3");
479 
480     assert(date.format("%r") == "05:45:10 PM");
481     assert(morning.format("%r") == "09:21:14 AM");
482 
483     assert(date.format("%R") == "17:45");
484     assert(morning.format("%R") == "09:21");
485 
486     assert(date.format("%s") == "1607363110");
487 
488     assert(date.format("%S") == "10");
489 
490     assert(date.format("%t") == "\t");
491 
492     assert(date.format("%T") == "17:45:10");
493     assert(morning.format("%T") == "09:21:14");
494 
495     assert(date.format("%u") == "1");
496     moment = SysTime(DateTime(2020, 3, 8, 14, 33, 52), UTC());
497     assert(moment.format("%u") == "7");
498 
499     assert(moment.format("%U") == "10");
500     assert(date.format("%U") == "50");
501     
502     assert(date.format("%w") == "1");
503     assert(date.format("%W") == "50");
504 
505     version(Windows)
506 	{
507 		assert(date.format("%x") == "12/07/2020");
508 		assert(date.format("%X") == "05:45");
509 	}
510 	else
511 	{
512 		assert(date.format("%x") == "12/07/20");
513 		assert(date.format("%X") == "17:45:10");
514 	}
515 
516     assert(date.format("%y") == "20");
517     moment = SysTime(DateTime(2012, 5, 16, 14, 14, 41), UTC());
518     assert(moment.format("%y") == "12");
519     assert(date.format("%Y") == "2020");
520     assert(moment.format("%Y") == "2012");
521 
522     assert(date.format("%z") == "0000");
523     assert(date.format("%Z") == "UTC");
524 }