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 }