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 }