1 /** 2 * Boost Software License - Version 1.0 - August 17th, 2003 3 * 4 * Permission is hereby granted, free of charge, to any person or organization 5 * obtaining a copy of the software and accompanying documentation covered by 6 * this license (the "Software") to use, reproduce, display, distribute, 7 * execute, and transmit the Software, and to prepare derivative works of the 8 * Software, and to permit third-parties to whom the Software is furnished to 9 * do so, all subject to the following: 10 * 11 * The copyright notices in the Software and this entire statement, including 12 * the above license grant, this restriction and the following disclaimer, 13 * must be included in all copies of the Software, in whole or in part, and 14 * all derivative works of the Software, unless such copies or derivative 15 * works are solely in the form of machine-executable object code generated by 16 * a source language processor. 17 * 18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 21 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 22 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 23 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 24 * DEALINGS IN THE SOFTWARE. 25 */ 26 27 module dateparser; 28 29 debug(dateparser) import std.stdio; 30 import std.datetime; 31 import std.traits; 32 import std.typecons; 33 import std.compiler; 34 import std.regex; 35 import std.range; 36 import std.experimental.allocator.common; 37 import std.experimental.allocator.gc_allocator; 38 import dateparser.timelexer; 39 import dateparser.ymd; 40 41 private: 42 43 // Copied from EMSI containers 44 mixin template AllocatorState(Allocator) 45 { 46 static if (stateSize!Allocator == 0) 47 alias allocator = Allocator.instance; 48 else 49 Allocator allocator; 50 } 51 52 Parser!GCAllocator defaultParser; 53 static this() 54 { 55 defaultParser = new Parser!GCAllocator(new ParserInfo()); 56 } 57 58 // dfmt off 59 // m from a.m/p.m, t from ISO T separator, order doesn't 60 // matter here, just a presence check 61 enum JUMP_DEFAULT = [ 62 "and":9, "'":6, 63 "at":7, "/":5, 64 "st":14, 65 ";":3, " ":0, 66 "of":13, "nd":15, 67 "rd":16, ".":1, 68 "th":17, "on":8, 69 "m":11, ",":2, 70 "ad":10, "-":4, "t":12 71 ]; 72 enum WEEKDAYS_DEFAULT = [ 73 "mon":0, "monday":0, 74 "tue":1, "tuesday":1, 75 "wed":2, "wednesday":2, 76 "thu":3, "thursday":3, 77 "fri":4, "friday":4, 78 "sat":5, "saturday":5, 79 "sun":6, "sunday":6, 80 ]; 81 enum MONTHS_DEFAULT = [ 82 "jan":0, "january":0, 83 "feb":1, "february":1, 84 "mar":2, "march":2, 85 "apr":3, "april":3, 86 "may":4, 87 "jun":5, "june":5, 88 "jul":6, "july":6, 89 "aug":7, "august":7, 90 "sep":8, "sept":8, "september":8, 91 "oct":9, "october":9, 92 "nov":10, "november":10, 93 "dec":11, "december":11 94 ]; 95 enum HMS_DEFAULT = [ 96 "h":0, "hour":0, "hours":0, 97 "m":1, "minute":1, "minutes":1, 98 "s":2, "second":2, "seconds":2 99 ]; 100 enum AMPM_DEFAULT = [ 101 "am":0, "a":0, 102 "pm":1, "p":1 103 ]; 104 enum UTCZONE_DEFAULT = [ 105 "UTC":0, "GMT":0, "Z":0 106 ]; 107 enum PERTAIN_DEFAULT = [ 108 "of":0 109 ]; 110 int[string] TZOFFSET; 111 // dfmt on 112 113 struct ParseResult 114 { 115 bool badData = false; 116 Nullable!(int, int.min) year; 117 Nullable!(int, int.min) month; 118 Nullable!(int, int.min) day; 119 Nullable!(int, int.min) weekday; 120 Nullable!(int, int.min) hour; 121 Nullable!(int, int.min) minute; 122 Nullable!(int, int.min) second; 123 Nullable!(int, int.min) microsecond; 124 Nullable!(int, int.min) tzoffset; 125 Nullable!(int, int.min) ampm; 126 bool centurySpecified; 127 string tzname; 128 Nullable!(SysTime) shortcutResult; 129 Nullable!(TimeOfDay) shortcutTimeResult; 130 } 131 132 /** 133 * Parse a I[.F] seconds value into (seconds, microseconds) 134 * 135 * Params: 136 * value = value to parse 137 * Returns: 138 * tuple of two ints 139 */ 140 auto parseMS(R)(R s) if ( 141 isForwardRange!R && 142 !isInfinite!R && 143 is(Unqual!(ElementEncodingType!R) : char)) 144 { 145 import std.string : leftJustifier; 146 import std.algorithm.searching : canFind; 147 import std.algorithm.iteration : splitter; 148 import std.typecons : tuple; 149 import std.conv : parse; 150 import std.utf : byCodeUnit; 151 152 // auto decoding special case 153 static if (isNarrowString!R) 154 auto value = s.byCodeUnit; 155 else 156 alias value = s; 157 158 if (!(value.save.canFind('.'))) 159 { 160 return tuple(parse!int(value), 0); 161 } 162 else 163 { 164 auto splitValue = value.splitter('.'); 165 auto secs = splitValue.front; 166 splitValue.popFront(); 167 auto msecs = splitValue.front.leftJustifier(6, '0'); 168 return tuple( 169 parse!int(secs), 170 parse!int(msecs) 171 ); 172 } 173 } 174 175 void setAttribute(P, T)(ref P p, string name, auto ref T value) 176 { 177 foreach (mem; __traits(allMembers, P)) 178 { 179 static if (is(typeof(__traits(getMember, p, mem)) Q)) 180 { 181 static if (is(T : Q)) 182 { 183 if (mem == name) 184 { 185 __traits(getMember, p, mem) = value; 186 return; 187 } 188 } 189 } 190 } 191 assert(0, P.stringof ~ " has no member " ~ name); 192 } 193 194 public: 195 196 /** 197 This function offers a generic date/time string Parser which is able to parse 198 most known formats to represent a date and/or time. 199 200 This function attempts to be forgiving with regards to unlikely input formats, 201 returning a `SysTime` object even for dates which are ambiguous. While other languages 202 have writing systems without Arabic numerals, the overwhelming majority of dates 203 are written with them. As such, this function does not work with other 204 number systems. 205 206 If an element of a date/time stamp is omitted, the following rules are applied: 207 208 $(UL 209 $(LI If AM or PM is left unspecified, a 24-hour clock is assumed, however, 210 an hour on a 12-hour clock (0 <= hour <= 12) *must* be specified if 211 AM or PM is specified.) 212 $(LI If a time zone is omitted, a SysTime is given with the timezone of the 213 host machine.) 214 ) 215 216 Missing information is allowed, and what ever is given is applied on top of 217 the `defaultDate` parameter, which defaults to January 1, 1 AD at midnight. 218 E.g. a string of `"10:00 AM"` with a `defaultDate` of 219 `SysTime(Date(2016, 1, 1))` will yield `SysTime(DateTime(2016, 1, 1, 10, 0, 0))`. 220 221 If your date string uses timezone names in place of UTC offsets, then timezone 222 information must be user provided, as there is no way to reliably get timezones 223 from the OS by abbreviation. But, the timezone will be properly set if an offset 224 is given. Timezone info and their abbreviations change constantly, so it's a 225 good idea to not rely on `timezoneInfos` too much. 226 227 This function allocates memory and throws on the GC. In order to reduce GC allocations, 228 use a custom `Parser` instance with a different allocator. 229 230 Params: 231 timeString = A forward range with UTF-8 encoded elements containing a date/time stamp. 232 ignoreTimezone = Set to false by default, time zones in parsed strings are ignored and a 233 SysTime with the local time zone is returned. If timezone information 234 is not important, setting this to true is slightly faster. 235 timezoneInfos = Time zone names / aliases which may be present in the 236 string. This argument maps time zone names (and optionally offsets 237 from those time zones) to time zones. This parameter is ignored if 238 ignoreTimezone is set. 239 dayFirst = Whether to interpret the first value in an ambiguous 3-integer date 240 (e.g. 01/05/09) as the day (`true`) or month (`false`). If 241 yearFirst is set to true, this distinguishes between YDM and 242 YMD. 243 yearFirst = Whether to interpret the first value in an ambiguous 3-integer date 244 (e.g. 01/05/09) as the year. If true, the first number is taken to 245 be the year, otherwise the last number is taken to be the year. 246 fuzzy = Whether to allow fuzzy parsing, allowing for string like "Today is 247 January 1, 2047 at 8:21:00AM". 248 defaultDate = The date to apply the given information on top of. Defaults to 249 January 1st, 1 AD 250 251 Returns: 252 A SysTime object representing the parsed string 253 254 Throws: 255 `ConvException` will be thrown for invalid string or unknown string format 256 257 Throws: 258 `TimeException` if the date string is successfully parsed but the created 259 date would be invalid 260 261 Throws: 262 `ConvOverflowException` if one of the numbers in the parsed date exceeds 263 `float.max` 264 */ 265 SysTime parse(Range)(Range timeString, 266 Flag!"ignoreTimezone" ignoreTimezone = No.ignoreTimezone, 267 const(TimeZone)[string] timezoneInfos = null, 268 Flag!"dayFirst" dayFirst = No.dayFirst, 269 Flag!"yearFirst" yearFirst = No.yearFirst, 270 Flag!"fuzzy" fuzzy = No.fuzzy, 271 SysTime defaultDate = SysTime(DateTime(1, 1, 1))) if ( 272 isForwardRange!Range && !isInfinite!Range && is(Unqual!(ElementEncodingType!Range) : char)) 273 { 274 // dfmt off 275 return defaultParser.parse( 276 timeString, 277 ignoreTimezone, 278 timezoneInfos, 279 dayFirst, 280 yearFirst, 281 fuzzy, 282 defaultDate 283 ); 284 } 285 286 /// 287 unittest 288 { 289 immutable brazilTime = new SimpleTimeZone(dur!"seconds"(-10_800)); 290 const(TimeZone)[string] timezones = ["BRST" : brazilTime]; 291 292 immutable parsed = parse("Thu Sep 25 10:36:28 BRST 2003", No.ignoreTimezone, timezones); 293 // SysTime opEquals ignores timezones 294 assert(parsed == SysTime(DateTime(2003, 9, 25, 10, 36, 28))); 295 assert(parsed.timezone == brazilTime); 296 297 assert(parse( 298 "2003 10:36:28 BRST 25 Sep Thu", 299 No.ignoreTimezone, 300 timezones 301 ) == SysTime(DateTime(2003, 9, 25, 10, 36, 28))); 302 assert(parse("Thu Sep 25 10:36:28") == SysTime(DateTime(1, 9, 25, 10, 36, 28))); 303 assert(parse("20030925T104941") == SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 304 assert(parse("2003-09-25T10:49:41") == SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 305 assert(parse("10:36:28") == SysTime(DateTime(1, 1, 1, 10, 36, 28))); 306 assert(parse("09-25-2003") == SysTime(DateTime(2003, 9, 25))); 307 } 308 309 /// Apply information on top of `defaultDate` 310 unittest 311 { 312 assert("10:36:28".parse(No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 313 No.fuzzy, SysTime(DateTime(2016, 3, 15))) 314 == SysTime(DateTime(2016, 3, 15, 10, 36, 28))); 315 assert("August 07".parse(No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 316 No.fuzzy, SysTime(DateTime(2016, 1, 1))) 317 == SysTime(Date(2016, 8, 7))); 318 assert("2000".parse(No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 319 No.fuzzy, SysTime(DateTime(2016, 3, 1))) 320 == SysTime(Date(2000, 3, 1))); 321 } 322 323 /// Custom allocators 324 unittest 325 { 326 import std.experimental.allocator.mallocator; 327 328 auto customParser = new Parser!Mallocator(new ParserInfo()); 329 assert(customParser.parse("2003-09-25T10:49:41") == 330 SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 331 } 332 333 /// Exceptions 334 unittest 335 { 336 import std.exception : assertThrown; 337 import std.conv : ConvException; 338 339 assertThrown!ConvException(parse("")); 340 assertThrown!ConvException(parse("AM")); 341 assertThrown!ConvException(parse("The quick brown fox jumps over the lazy dog")); 342 assertThrown!TimeException(parse("Feb 30, 2007")); 343 assertThrown!TimeException(parse("Jan 20, 2015 PM")); 344 assertThrown!ConvException(parse("13:44 AM")); 345 assertThrown!ConvException(parse("January 25, 1921 23:13 PM")); 346 } 347 // dfmt on 348 349 unittest 350 { 351 assert(parse("Thu Sep 10:36:28") == SysTime(DateTime(1, 9, 5, 10, 36, 28))); 352 assert(parse("Thu 10:36:28") == SysTime(DateTime(1, 1, 3, 10, 36, 28))); 353 assert(parse("Sep 10:36:28") == SysTime(DateTime(1, 9, 1, 10, 36, 28))); 354 assert(parse("Sep 2003") == SysTime(DateTime(2003, 9, 1))); 355 assert(parse("Sep") == SysTime(DateTime(1, 9, 1))); 356 assert(parse("2003") == SysTime(DateTime(2003, 1, 1))); 357 assert(parse("10:36") == SysTime(DateTime(1, 1, 1, 10, 36))); 358 } 359 360 unittest 361 { 362 assert(parse("Thu 10:36:28") == SysTime(DateTime(1, 1, 3, 10, 36, 28))); 363 assert(parse("20030925T104941") == SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 364 assert(parse("20030925T1049") == SysTime(DateTime(2003, 9, 25, 10, 49, 0))); 365 assert(parse("20030925T10") == SysTime(DateTime(2003, 9, 25, 10))); 366 assert(parse("20030925") == SysTime(DateTime(2003, 9, 25))); 367 assert(parse("2003-09-25 10:49:41,502") == SysTime(DateTime(2003, 9, 25, 10, 368 49, 41), msecs(502))); 369 assert(parse("199709020908") == SysTime(DateTime(1997, 9, 2, 9, 8))); 370 assert(parse("19970902090807") == SysTime(DateTime(1997, 9, 2, 9, 8, 7))); 371 } 372 373 unittest 374 { 375 assert(parse("2003 09 25") == SysTime(DateTime(2003, 9, 25))); 376 assert(parse("2003 Sep 25") == SysTime(DateTime(2003, 9, 25))); 377 assert(parse("25 Sep 2003") == SysTime(DateTime(2003, 9, 25))); 378 assert(parse("25 Sep 2003") == SysTime(DateTime(2003, 9, 25))); 379 assert(parse("Sep 25 2003") == SysTime(DateTime(2003, 9, 25))); 380 assert(parse("09 25 2003") == SysTime(DateTime(2003, 9, 25))); 381 assert(parse("25 09 2003") == SysTime(DateTime(2003, 9, 25))); 382 assert(parse("10 09 2003", No.ignoreTimezone, null, 383 Yes.dayFirst) == SysTime(DateTime(2003, 9, 10))); 384 assert(parse("10 09 2003") == SysTime(DateTime(2003, 10, 9))); 385 assert(parse("10 09 03") == SysTime(DateTime(2003, 10, 9))); 386 assert(parse("10 09 03", No.ignoreTimezone, null, No.dayFirst, 387 Yes.yearFirst) == SysTime(DateTime(2010, 9, 3))); 388 assert(parse("25 09 03") == SysTime(DateTime(2003, 9, 25))); 389 } 390 391 unittest 392 { 393 assert(parse("03 25 Sep") == SysTime(DateTime(2003, 9, 25))); 394 assert(parse("2003 25 Sep") == SysTime(DateTime(2003, 9, 25))); 395 assert(parse("25 03 Sep") == SysTime(DateTime(2025, 9, 3))); 396 assert(parse("Thu Sep 25 2003") == SysTime(DateTime(2003, 9, 25))); 397 assert(parse("Sep 25 2003") == SysTime(DateTime(2003, 9, 25))); 398 } 399 400 // Naked times 401 unittest 402 { 403 assert(parse("10h36m28.5s") == SysTime(DateTime(1, 1, 1, 10, 36, 28), msecs(500))); 404 assert(parse("10h36m28s") == SysTime(DateTime(1, 1, 1, 10, 36, 28))); 405 assert(parse("10h36m") == SysTime(DateTime(1, 1, 1, 10, 36))); 406 assert(parse("10h") == SysTime(DateTime(1, 1, 1, 10, 0, 0))); 407 assert(parse("10 h 36") == SysTime(DateTime(1, 1, 1, 10, 36, 0))); 408 } 409 410 // AM vs PM 411 unittest 412 { 413 assert(parse("10h am") == SysTime(DateTime(1, 1, 1, 10))); 414 assert(parse("10h pm") == SysTime(DateTime(1, 1, 1, 22))); 415 assert(parse("10am") == SysTime(DateTime(1, 1, 1, 10))); 416 assert(parse("10pm") == SysTime(DateTime(1, 1, 1, 22))); 417 assert(parse("10:00 am") == SysTime(DateTime(1, 1, 1, 10))); 418 assert(parse("10:00 pm") == SysTime(DateTime(1, 1, 1, 22))); 419 assert(parse("10:00am") == SysTime(DateTime(1, 1, 1, 10))); 420 assert(parse("10:00pm") == SysTime(DateTime(1, 1, 1, 22))); 421 assert(parse("10:00a.m") == SysTime(DateTime(1, 1, 1, 10))); 422 assert(parse("10:00p.m") == SysTime(DateTime(1, 1, 1, 22))); 423 assert(parse("10:00a.m.") == SysTime(DateTime(1, 1, 1, 10))); 424 assert(parse("10:00p.m.") == SysTime(DateTime(1, 1, 1, 22))); 425 } 426 427 // ISO and ISO stripped 428 unittest 429 { 430 immutable zone = new SimpleTimeZone(dur!"seconds"(-10_800)); 431 432 immutable parsed = parse("2003-09-25T10:49:41.5-03:00"); 433 assert(parsed == SysTime(DateTime(2003, 9, 25, 10, 49, 41), msecs(500), zone)); 434 assert((cast(immutable(SimpleTimeZone)) parsed.timezone).utcOffset == hours(-3)); 435 436 immutable parsed2 = parse("2003-09-25T10:49:41-03:00"); 437 assert(parsed2 == SysTime(DateTime(2003, 9, 25, 10, 49, 41), zone)); 438 assert((cast(immutable(SimpleTimeZone)) parsed2.timezone).utcOffset == hours(-3)); 439 440 assert(parse("2003-09-25T10:49:41") == SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 441 assert(parse("2003-09-25T10:49") == SysTime(DateTime(2003, 9, 25, 10, 49))); 442 assert(parse("2003-09-25T10") == SysTime(DateTime(2003, 9, 25, 10))); 443 assert(parse("2003-09-25") == SysTime(DateTime(2003, 9, 25))); 444 445 immutable parsed3 = parse("2003-09-25T10:49:41-03:00"); 446 assert(parsed3 == SysTime(DateTime(2003, 9, 25, 10, 49, 41), zone)); 447 assert((cast(immutable(SimpleTimeZone)) parsed3.timezone).utcOffset == hours(-3)); 448 449 immutable parsed4 = parse("20030925T104941-0300"); 450 assert(parsed4 == SysTime(DateTime(2003, 9, 25, 10, 49, 41), zone)); 451 assert((cast(immutable(SimpleTimeZone)) parsed4.timezone).utcOffset == hours(-3)); 452 453 assert(parse("20030925T104941") == SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 454 assert(parse("20030925T1049") == SysTime(DateTime(2003, 9, 25, 10, 49, 0))); 455 assert(parse("20030925T10") == SysTime(DateTime(2003, 9, 25, 10))); 456 assert(parse("20030925") == SysTime(DateTime(2003, 9, 25))); 457 } 458 459 // Dashes 460 unittest 461 { 462 assert(parse("2003-09-25") == SysTime(DateTime(2003, 9, 25))); 463 assert(parse("2003-Sep-25") == SysTime(DateTime(2003, 9, 25))); 464 assert(parse("25-Sep-2003") == SysTime(DateTime(2003, 9, 25))); 465 assert(parse("25-Sep-2003") == SysTime(DateTime(2003, 9, 25))); 466 assert(parse("Sep-25-2003") == SysTime(DateTime(2003, 9, 25))); 467 assert(parse("09-25-2003") == SysTime(DateTime(2003, 9, 25))); 468 assert(parse("25-09-2003") == SysTime(DateTime(2003, 9, 25))); 469 assert(parse("10-09-2003", No.ignoreTimezone, null, 470 Yes.dayFirst) == SysTime(DateTime(2003, 9, 10))); 471 assert(parse("10-09-2003") == SysTime(DateTime(2003, 10, 9))); 472 assert(parse("10-09-03") == SysTime(DateTime(2003, 10, 9))); 473 assert(parse("10-09-03", No.ignoreTimezone, null, No.dayFirst, 474 Yes.yearFirst) == SysTime(DateTime(2010, 9, 3))); 475 assert(parse("01-99") == SysTime(DateTime(1999, 1, 1))); 476 assert(parse("99-01") == SysTime(DateTime(1999, 1, 1))); 477 assert(parse("13-01", No.ignoreTimezone, null, Yes.dayFirst) == SysTime(DateTime(1, 478 1, 13))); 479 assert(parse("01-13") == SysTime(DateTime(1, 1, 13))); 480 assert(parse("01-99-Jan") == SysTime(DateTime(1999, 1, 1))); 481 } 482 483 // Dots 484 unittest 485 { 486 assert(parse("2003.09.25") == SysTime(DateTime(2003, 9, 25))); 487 assert(parse("2003.Sep.25") == SysTime(DateTime(2003, 9, 25))); 488 assert(parse("25.Sep.2003") == SysTime(DateTime(2003, 9, 25))); 489 assert(parse("25.Sep.2003") == SysTime(DateTime(2003, 9, 25))); 490 assert(parse("Sep.25.2003") == SysTime(DateTime(2003, 9, 25))); 491 assert(parse("09.25.2003") == SysTime(DateTime(2003, 9, 25))); 492 assert(parse("25.09.2003") == SysTime(DateTime(2003, 9, 25))); 493 assert(parse("10.09.2003", No.ignoreTimezone, null, 494 Yes.dayFirst) == SysTime(DateTime(2003, 9, 10))); 495 assert(parse("10.09.2003") == SysTime(DateTime(2003, 10, 9))); 496 assert(parse("10.09.03") == SysTime(DateTime(2003, 10, 9))); 497 assert(parse("10.09.03", No.ignoreTimezone, null, No.dayFirst, 498 Yes.yearFirst) == SysTime(DateTime(2010, 9, 3))); 499 } 500 501 // Slashes 502 unittest 503 { 504 assert(parse("2003/09/25") == SysTime(DateTime(2003, 9, 25))); 505 assert(parse("2003/Sep/25") == SysTime(DateTime(2003, 9, 25))); 506 assert(parse("25/Sep/2003") == SysTime(DateTime(2003, 9, 25))); 507 assert(parse("25/Sep/2003") == SysTime(DateTime(2003, 9, 25))); 508 assert(parse("Sep/25/2003") == SysTime(DateTime(2003, 9, 25))); 509 assert(parse("09/25/2003") == SysTime(DateTime(2003, 9, 25))); 510 assert(parse("25/09/2003") == SysTime(DateTime(2003, 9, 25))); 511 assert(parse("10/09/2003", No.ignoreTimezone, null, 512 Yes.dayFirst) == SysTime(DateTime(2003, 9, 10))); 513 assert(parse("10/09/2003") == SysTime(DateTime(2003, 10, 9))); 514 assert(parse("10/09/03") == SysTime(DateTime(2003, 10, 9))); 515 assert(parse("10/09/03", No.ignoreTimezone, null, No.dayFirst, 516 Yes.yearFirst) == SysTime(DateTime(2010, 9, 3))); 517 } 518 519 // Random formats 520 unittest 521 { 522 assert(parse("Wed, July 10, '96") == SysTime(DateTime(1996, 7, 10, 0, 0))); 523 assert(parse("1996.07.10 AD at 15:08:56 PDT", 524 Yes.ignoreTimezone) == SysTime(DateTime(1996, 7, 10, 15, 8, 56))); 525 assert(parse("1996.July.10 AD 12:08 PM") == SysTime(DateTime(1996, 7, 10, 12, 8))); 526 assert(parse("Tuesday, April 12, 1952 AD 3:30:42pm PST", 527 Yes.ignoreTimezone) == SysTime(DateTime(1952, 4, 12, 15, 30, 42))); 528 assert(parse("November 5, 1994, 8:15:30 am EST", 529 Yes.ignoreTimezone) == SysTime(DateTime(1994, 11, 5, 8, 15, 30))); 530 assert(parse("1994-11-05T08:15:30-05:00", 531 Yes.ignoreTimezone) == SysTime(DateTime(1994, 11, 5, 8, 15, 30))); 532 assert(parse("1994-11-05T08:15:30Z", 533 Yes.ignoreTimezone) == SysTime(DateTime(1994, 11, 5, 8, 15, 30))); 534 assert(parse("July 4, 1976") == SysTime(DateTime(1976, 7, 4))); 535 assert(parse("7 4 1976") == SysTime(DateTime(1976, 7, 4))); 536 assert(parse("4 jul 1976") == SysTime(DateTime(1976, 7, 4))); 537 assert(parse("7-4-76") == SysTime(DateTime(1976, 7, 4))); 538 assert(parse("19760704") == SysTime(DateTime(1976, 7, 4))); 539 assert(parse("0:01:02") == SysTime(DateTime(1, 1, 1, 0, 1, 2))); 540 assert(parse("12h 01m02s am") == SysTime(DateTime(1, 1, 1, 0, 1, 2))); 541 assert(parse("0:01:02 on July 4, 1976") == SysTime(DateTime(1976, 7, 4, 0, 1, 2))); 542 assert(parse("0:01:02 on July 4, 1976") == SysTime(DateTime(1976, 7, 4, 0, 1, 2))); 543 assert(parse("1976-07-04T00:01:02Z", 544 Yes.ignoreTimezone) == SysTime(DateTime(1976, 7, 4, 0, 1, 2))); 545 assert(parse("July 4, 1976 12:01:02 am") == SysTime(DateTime(1976, 7, 4, 0, 1, 546 2))); 547 assert(parse("Mon Jan 2 04:24:27 1995") == SysTime(DateTime(1995, 1, 2, 4, 24, 548 27))); 549 assert(parse("Tue Apr 4 00:22:12 PDT 1995", 550 Yes.ignoreTimezone) == SysTime(DateTime(1995, 4, 4, 0, 22, 12))); 551 assert(parse("04.04.95 00:22") == SysTime(DateTime(1995, 4, 4, 0, 22))); 552 assert(parse("Jan 1 1999 11:23:34.578") == SysTime(DateTime(1999, 1, 1, 11, 23, 553 34), msecs(578))); 554 assert(parse("950404 122212") == SysTime(DateTime(1995, 4, 4, 12, 22, 12))); 555 assert(parse("0:00 PM, PST", Yes.ignoreTimezone) == SysTime(DateTime(1, 1, 1, 12, 556 0))); 557 assert(parse("12:08 PM") == SysTime(DateTime(1, 1, 1, 12, 8))); 558 assert(parse("5:50 A.M. on June 13, 1990") == SysTime(DateTime(1990, 6, 13, 5, 559 50))); 560 assert(parse("3rd of May 2001") == SysTime(DateTime(2001, 5, 3))); 561 assert(parse("5th of March 2001") == SysTime(DateTime(2001, 3, 5))); 562 assert(parse("1st of May 2003") == SysTime(DateTime(2003, 5, 1))); 563 assert(parse("01h02m03") == SysTime(DateTime(1, 1, 1, 1, 2, 3))); 564 assert(parse("01h02") == SysTime(DateTime(1, 1, 1, 1, 2))); 565 assert(parse("01h02s") == SysTime(DateTime(1, 1, 1, 1, 0, 2))); 566 assert(parse("01m02") == SysTime(DateTime(1, 1, 1, 0, 1, 2))); 567 assert(parse("01m02h") == SysTime(DateTime(1, 1, 1, 2, 1))); 568 assert(parse("2004 10 Apr 11h30m") == SysTime(DateTime(2004, 4, 10, 11, 30))); 569 } 570 571 // Pertain, weekday, and month 572 unittest 573 { 574 assert(parse("Sep 03") == SysTime(DateTime(1, 9, 3))); 575 assert(parse("Sep of 03") == SysTime(DateTime(2003, 9, 1))); 576 assert(parse("Wed") == SysTime(DateTime(1, 1, 2))); 577 assert(parse("Wednesday") == SysTime(DateTime(1, 1, 2))); 578 assert(parse("October") == SysTime(DateTime(1, 10, 1))); 579 assert(parse("31-Dec-00") == SysTime(DateTime(2000, 12, 31))); 580 } 581 582 // Fuzzy 583 unittest 584 { 585 // Sometimes fuzzy parsing results in AM/PM flag being set without 586 // hours - if it's fuzzy it should ignore that. 587 auto s1 = "I have a meeting on March 1 1974."; 588 auto s2 = "On June 8th, 2020, I am going to be the first man on Mars"; 589 590 // Also don't want any erroneous AM or PMs changing the parsed time 591 auto s3 = "Meet me at the AM/PM on Sunset at 3:00 AM on December 3rd, 2003"; 592 auto s4 = "Meet me at 3:00AM on December 3rd, 2003 at the AM/PM on Sunset"; 593 auto s5 = "Today is 25 of September of 2003, exactly at 10:49:41 with timezone -03:00."; 594 auto s6 = "Jan 29, 1945 14:45 AM I going to see you there?"; 595 596 assert(parse(s1, No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 597 Yes.fuzzy) == SysTime(DateTime(1974, 3, 1))); 598 assert(parse(s2, No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 599 Yes.fuzzy) == SysTime(DateTime(2020, 6, 8))); 600 assert(parse(s3, No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 601 Yes.fuzzy) == SysTime(DateTime(2003, 12, 3, 3))); 602 assert(parse(s4, No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 603 Yes.fuzzy) == SysTime(DateTime(2003, 12, 3, 3))); 604 605 immutable zone = new SimpleTimeZone(dur!"hours"(-3)); 606 immutable parsed = parse(s5, No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 607 Yes.fuzzy); 608 assert(parsed == SysTime(DateTime(2003, 9, 25, 10, 49, 41), zone)); 609 610 assert(parse(s6, No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 611 Yes.fuzzy) == SysTime(DateTime(1945, 1, 29, 14, 45))); 612 } 613 614 // dfmt off 615 /// Custom parser info allows for international time representation 616 unittest 617 { 618 import std.utf; 619 620 class RusParserInfo : ParserInfo 621 { 622 this() 623 { 624 super(false, false); 625 monthsAA = ParserInfo.convert([ 626 ["янв", "Январь"], 627 ["фев", "Февраль"], 628 ["мар", "Март"], 629 ["апр", "Апрель"], 630 ["май", "Май"], 631 ["июн", "Июнь"], 632 ["июл", "Июль"], 633 ["авг", "Август"], 634 ["сен", "Сентябрь"], 635 ["окт", "Октябрь"], 636 ["ноя", "Ноябрь"], 637 ["дек", "Декабрь"] 638 ]); 639 } 640 } 641 642 auto rusParser = new Parser!GCAllocator(new RusParserInfo()); 643 immutable parsedTime = rusParser.parse("10 Сентябрь 2015 10:20"); 644 assert(parsedTime == SysTime(DateTime(2015, 9, 10, 10, 20))); 645 646 immutable parsedTime2 = rusParser.parse("10 Сентябрь 2015 10:20"d.byChar); 647 assert(parsedTime2 == SysTime(DateTime(2015, 9, 10, 10, 20))); 648 } 649 // dfmt on 650 651 // Test ranges 652 unittest 653 { 654 import std.utf : byCodeUnit, byChar; 655 656 // forward ranges 657 assert("10h36m28s".byChar.parse == SysTime( 658 DateTime(1, 1, 1, 10, 36, 28))); 659 assert("Thu Sep 10:36:28".byChar.parse == SysTime( 660 DateTime(1, 9, 5, 10, 36, 28))); 661 662 // bidirectional ranges 663 assert("2003-09-25T10:49:41".byCodeUnit.parse == SysTime( 664 DateTime(2003, 9, 25, 10, 49, 41))); 665 assert("Thu Sep 10:36:28".byCodeUnit.parse == SysTime( 666 DateTime(1, 9, 5, 10, 36, 28))); 667 } 668 669 // Issue #1 670 unittest 671 { 672 assert(parse("Sat, 12 Mar 2016 01:30:59 -0900", 673 Yes.ignoreTimezone) == SysTime(DateTime(2016, 3, 12, 01, 30, 59))); 674 } 675 676 /** 677 Class which handles what inputs are accepted. Subclass this to customize 678 the language and acceptable values for each parameter. 679 680 Params: 681 dayFirst = Whether to interpret the first value in an ambiguous 3-integer date 682 (e.g. 01/05/09) as the day (`true`) or month (`false`). If 683 `yearFirst` is set to `true`, this distinguishes between YDM 684 and YMD. Default is `false`. 685 yearFirst = Whether to interpret the first value in an ambiguous 3-integer date 686 (e.g. 01/05/09) as the year. If `true`, the first number is taken 687 to be the year, otherwise the last number is taken to be the year. 688 Default is `false`. 689 */ 690 class ParserInfo 691 { 692 import std.uni : toLower, asLowerCase; 693 694 private: 695 bool dayFirst; 696 bool yearFirst; 697 short year; 698 short century; 699 700 /** 701 * If the century isn't specified, e.g. `"'07"`, then assume that the year 702 * is in the current century and return it as such. Otherwise do nothing 703 * 704 * Params: 705 * convertYear = year to be converted 706 * centurySpecified = is the century given in the year 707 * 708 * Returns: 709 * the converted year 710 */ 711 final int convertYear(int convertYear, bool centurySpecified = false) @safe @nogc pure nothrow const 712 { 713 import std.math : abs; 714 715 if (convertYear < 100 && !centurySpecified) 716 { 717 convertYear += century; 718 if (abs(convertYear - year) >= 50) 719 { 720 if (convertYear < year) 721 convertYear += 100; 722 else 723 convertYear -= 100; 724 } 725 } 726 727 return convertYear; 728 } 729 730 /** 731 * Takes and Result and converts it year and checks if the timezone is UTC 732 */ 733 final void validate(ref ParseResult res) @safe pure const 734 { 735 //move to info 736 if (!res.year.isNull) 737 res.year = convertYear(res.year, res.centurySpecified); 738 739 if ((!res.tzoffset.isNull && res.tzoffset == 0) 740 && (res.tzname.length == 0 || res.tzname == "Z")) 741 { 742 res.tzname = "UTC"; 743 res.tzoffset = 0; 744 } 745 else if (!res.tzoffset.isNull && res.tzoffset != 0 && res.tzname.length > 0 746 && this.utczone(res.tzname)) 747 res.tzoffset = 0; 748 } 749 750 public: 751 /** 752 * AAs used for matching strings to calendar numbers, e.g. Jan is 1 753 */ 754 int[string] jumpAA; 755 ///ditto 756 int[string] weekdaysAA; 757 ///ditto 758 int[string] monthsAA; 759 ///ditto 760 int[string] hmsAA; 761 ///ditto 762 int[string] ampmAA; 763 ///ditto 764 int[string] utczoneAA; 765 ///ditto 766 int[string] pertainAA; 767 768 /** 769 * Take a range of character ranges or a range of ranges of character 770 * ranges and converts it to an associative array that the internal 771 * parser info methods can use. 772 * 773 * Use this method in order to override the default parser info field 774 * values. See the example on the $(REF parse). 775 * 776 * Params: 777 * list = a range of character ranges 778 * 779 * Returns: 780 * An associative array of `int`s accessed by strings 781 */ 782 static int[string] convert(Range)(Range list) if (isInputRange!Range 783 && isSomeChar!(ElementEncodingType!(ElementEncodingType!(Range))) 784 || isSomeChar!( 785 ElementEncodingType!(ElementEncodingType!(ElementEncodingType!(Range))))) 786 { 787 import std.array : array; 788 import std.conv : to; 789 790 int[string] dictionary; 791 792 foreach (int i, value; list) 793 { 794 // tuple of strings or multidimensional string array 795 static if (isInputRange!(ElementType!(ElementType!(Range)))) 796 foreach (item; value) 797 dictionary[item.asLowerCase.array.to!string] = i; 798 else 799 dictionary[value.asLowerCase.array.to!string] = i; 800 } 801 802 return dictionary; 803 } 804 805 /// Ctor 806 this(bool dayFirst = false, bool yearFirst = false) @safe 807 { 808 dayFirst = dayFirst; 809 yearFirst = yearFirst; 810 811 year = Clock.currTime.year; 812 century = (year / 100) * 100; 813 814 jumpAA = JUMP_DEFAULT; 815 weekdaysAA = WEEKDAYS_DEFAULT; 816 monthsAA = MONTHS_DEFAULT; 817 hmsAA = HMS_DEFAULT; 818 ampmAA = AMPM_DEFAULT; 819 utczoneAA = UTCZONE_DEFAULT; 820 pertainAA = PERTAIN_DEFAULT; 821 } 822 823 /// Tests for presence of `name` in each of the AAs 824 final bool jump(S)(const S name) const if (isSomeString!S) 825 { 826 return name.toLower() in jumpAA ? true : false; 827 } 828 829 /// ditto 830 final int weekday(S)(const S name) const if (isSomeString!S) 831 { 832 if (name.toLower() in weekdaysAA) 833 return weekdaysAA[name.toLower()]; 834 else 835 return -1; 836 } 837 838 /// ditto 839 final int month(S)(const S name) const if (isSomeString!S) 840 { 841 if (name.toLower() in monthsAA) 842 return monthsAA[name.toLower()] + 1; 843 else 844 return -1; 845 } 846 847 /// ditto 848 final int hms(S)(const S name) const if (isSomeString!S) 849 { 850 if (name.toLower() in hmsAA) 851 return hmsAA[name.toLower()]; 852 else 853 return -1; 854 } 855 856 /// ditto 857 final int ampm(S)(const S name) const if (isSomeString!S) 858 { 859 if (name.toLower() in ampmAA) 860 return ampmAA[name.toLower()]; 861 else 862 return -1; 863 } 864 865 /// ditto 866 final bool pertain(S)(const S name) const if (isSomeString!S) 867 { 868 return name.toLower() in pertainAA ? true : false; 869 } 870 871 /// ditto 872 final bool utczone(S)(const S name) const if (isSomeString!S) 873 { 874 return name.toLower() in utczoneAA ? true : false; 875 } 876 877 /// ditto 878 final int tzoffset(S)(const S name) const if (isSomeString!S) 879 { 880 return name in TZOFFSET ? TZOFFSET[name] : -1; 881 } 882 } 883 884 /** 885 * Implements the parsing functionality for the parse function. If you are 886 * using a custom `ParserInfo` many times in the same program, you can avoid 887 * unnecessary allocations by using the `Parser.parse` function directly. 888 * 889 * Params: 890 * Allocator = the allocator type to use 891 * parserInfo = the parser info to reference when parsing 892 */ 893 final class Parser(Allocator) if ( 894 hasMember!(Allocator, "allocate") && hasMember!(Allocator, "deallocate")) 895 { 896 private const ParserInfo info; 897 private mixin AllocatorState!Allocator; 898 899 public: 900 /// 901 this(const ParserInfo parserInfo = null) 902 { 903 if (parserInfo is null) 904 { 905 info = new ParserInfo(); 906 } 907 else 908 { 909 info = parserInfo; 910 } 911 } 912 913 /** 914 * This function has the same functionality as the free version of `parse`. 915 * The only difference is this will use your custom `ParserInfo` or allocator 916 * if provided. 917 */ 918 SysTime parse(Range)(Range timeString, 919 Flag!"ignoreTimezone" ignoreTimezone = No.ignoreTimezone, 920 const(TimeZone)[string] timezoneInfos = null, 921 Flag!"dayFirst" dayFirst = No.dayFirst, 922 Flag!"yearFirst" yearFirst = No.yearFirst, 923 Flag!"fuzzy" fuzzy = No.fuzzy, 924 SysTime defaultDate = SysTime(Date(1, 1, 1))) if ( 925 isForwardRange!Range && !isInfinite!Range && is(Unqual!(ElementEncodingType!Range) : char)) 926 { 927 import std.conv : to, ConvException; 928 929 auto res = parseImpl(timeString, dayFirst, yearFirst, fuzzy); 930 931 if (res.badData) 932 throw new ConvException("Unknown string format"); 933 934 if (res.year.isNull() && res.month.isNull() && res.day.isNull() 935 && res.hour.isNull() && res.minute.isNull() 936 && res.second.isNull() && res.weekday.isNull() 937 && res.shortcutResult.isNull() && res.shortcutTimeResult.isNull()) 938 throw new ConvException("String does not contain a date."); 939 940 if (res.shortcutResult.isNull && res.shortcutTimeResult.isNull) 941 { 942 if (!res.year.isNull) 943 defaultDate.year(res.year); 944 945 if (!res.day.isNull) 946 defaultDate.day(res.day); 947 948 if (!res.month.isNull) 949 defaultDate.month(to!Month(res.month)); 950 951 if (!res.hour.isNull) 952 defaultDate.hour(res.hour); 953 954 if (!res.minute.isNull) 955 defaultDate.minute(res.minute); 956 957 if (!res.second.isNull) 958 defaultDate.second(res.second); 959 960 if (!res.microsecond.isNull) 961 defaultDate.fracSecs(usecs(res.microsecond)); 962 963 if (!res.weekday.isNull() && (res.day.isNull || !res.day)) 964 { 965 int delta_days = daysToDayOfWeek( 966 defaultDate.dayOfWeek(), 967 to!DayOfWeek(res.weekday) 968 ); 969 defaultDate += dur!"days"(delta_days); 970 } 971 } 972 else if (!res.shortcutTimeResult.isNull) 973 defaultDate = SysTime(DateTime(Date( 974 defaultDate.year, 975 defaultDate.month, 976 defaultDate.day, 977 ), res.shortcutTimeResult.get())); 978 979 if (!ignoreTimezone) 980 { 981 if (res.tzname in timezoneInfos) 982 defaultDate = defaultDate.toOtherTZ( 983 cast(immutable) timezoneInfos[res.tzname] 984 ); 985 else if (res.tzname.length > 0 && (res.tzname == LocalTime().stdName 986 || res.tzname == LocalTime().dstName)) 987 defaultDate = SysTime(cast(DateTime) defaultDate); 988 else if (!res.tzoffset.isNull && res.tzoffset == 0) 989 defaultDate = SysTime(cast(DateTime) defaultDate, cast(immutable) UTC()); 990 else if (!res.tzoffset.isNull && res.tzoffset != 0) 991 { 992 defaultDate = SysTime( 993 cast(DateTime) defaultDate, 994 new immutable SimpleTimeZone(dur!"seconds"(res.tzoffset), res.tzname) 995 ); 996 } 997 } 998 else if (ignoreTimezone && !res.shortcutResult.isNull) 999 res.shortcutResult = SysTime(cast(DateTime) res.shortcutResult); 1000 1001 if (!res.shortcutResult.isNull) 1002 return res.shortcutResult; 1003 else 1004 return defaultDate; 1005 } 1006 1007 private: 1008 /** 1009 * Private method which performs the heavy lifting of parsing, called from 1010 * `parse`. 1011 * 1012 * Params: 1013 * timeString = the string to parse. 1014 * dayFirst = Whether to interpret the first value in an ambiguous 1015 * 3-integer date (e.g. 01/05/09) as the day (true) or month (false). If 1016 * yearFirst is set to true, this distinguishes between YDM 1017 * and YMD. If set to null, this value is retrieved from the 1018 * current :class:ParserInfo object (which itself defaults to 1019 * false). 1020 * yearFirst = Whether to interpret the first value in an ambiguous 3-integer date 1021 * (e.g. 01/05/09) as the year. If true, the first number is taken 1022 * to be the year, otherwise the last number is taken to be the year. 1023 * fuzzy = Whether to allow fuzzy parsing, allowing for string like "Today is 1024 * January 1, 2047 at 8:21:00AM". 1025 */ 1026 ParseResult parseImpl(Range)(Range timeString, bool dayFirst = false, 1027 bool yearFirst = false, bool fuzzy = false) if (isForwardRange!Range 1028 && !isInfinite!Range && is(Unqual!(ElementEncodingType!Range) : char)) 1029 { 1030 import std.algorithm.searching : canFind, countUntil; 1031 import std.algorithm.iteration : filter; 1032 import std.uni : isUpper; 1033 import std.utf : byCodeUnit, byChar; 1034 import std.conv : to, ConvException; 1035 import std.experimental.allocator : makeArray, dispose; 1036 import containers.dynamicarray : DynamicArray; 1037 1038 ParseResult res; 1039 1040 DynamicArray!(string, Allocator, true) tokens; 1041 1042 // auto decoding special case 1043 static if (isSomeString!Range && is(ElementType!Range == dchar)) 1044 put(tokens, timeString.byChar.save.timeLexer); 1045 else 1046 put(tokens, timeString.save.timeLexer); 1047 1048 debug(dateparser) writeln("tokens: ", tokens[]); 1049 1050 //keep up with the last token skipped so we can recombine 1051 //consecutively skipped tokens (-2 for when i begins at 0). 1052 int last_skipped_token_i = -2; 1053 1054 //year/month/day list 1055 YMD ymd; 1056 1057 //Index of the month string in ymd 1058 ptrdiff_t mstridx = -1; 1059 1060 immutable size_t tokensLength = tokens.length; 1061 debug(dateparser) writeln("tokensLength: ", tokensLength); 1062 uint i = 0; 1063 while (i < tokensLength) 1064 { 1065 //Check if it's a number 1066 Nullable!(float, float.infinity) value; 1067 string value_repr; 1068 debug(dateparser) writeln("index: ", i); 1069 debug(dateparser) writeln("tokens[i]: ", tokens[i]); 1070 1071 if (tokens[i][0].isNumber) 1072 { 1073 value_repr = tokens[i]; 1074 debug(dateparser) writeln("value_repr: ", value_repr); 1075 value = to!float(value_repr); 1076 } 1077 1078 //Token is a number 1079 if (!value.isNull()) 1080 { 1081 immutable tokensItemLength = tokens[i].length; 1082 ++i; 1083 1084 if (ymd.length == 3 && (tokensItemLength == 2 1085 || tokensItemLength == 4) && res.hour.isNull 1086 && (i >= tokensLength || (tokens[i] != ":" && info.hms(tokens[i]) == -1))) 1087 { 1088 debug(dateparser) writeln("branch 1"); 1089 //19990101T23[59] 1090 auto s = tokens[i - 1]; 1091 res.hour = to!int(s[0 .. 2]); 1092 1093 if (tokensItemLength == 4) 1094 { 1095 res.minute = to!int(s[2 .. $]); 1096 } 1097 } 1098 else if (tokensItemLength == 6 || (tokensItemLength > 6 1099 && tokens[i - 1].countUntil('.') == 6)) 1100 { 1101 debug(dateparser) writeln("branch 2"); 1102 //YYMMDD || HHMMSS[.ss] 1103 auto s = tokens[i - 1]; 1104 1105 if (ymd.length == 0 && !tokens[i - 1].canFind('.')) 1106 { 1107 ymd.put(s[0 .. 2]); 1108 ymd.put(s[2 .. 4]); 1109 ymd.put(s[4 .. $]); 1110 } 1111 else 1112 { 1113 //19990101T235959[.59] 1114 res.hour = to!int(s[0 .. 2]); 1115 res.minute = to!int(s[2 .. 4]); 1116 auto ms = parseMS(s[4 .. $]); 1117 res.second = ms[0]; 1118 res.microsecond = ms[1]; 1119 } 1120 } 1121 else if (tokensItemLength == 8 || tokensItemLength == 12 || tokensItemLength == 14) 1122 { 1123 debug(dateparser) writeln("branch 3"); 1124 //YYYYMMDD 1125 auto s = tokens[i - 1]; 1126 ymd.put(s[0 .. 4]); 1127 ymd.put(s[4 .. 6]); 1128 ymd.put(s[6 .. 8]); 1129 1130 if (tokensItemLength > 8) 1131 { 1132 res.hour = to!int(s[8 .. 10]); 1133 res.minute = to!int(s[10 .. 12]); 1134 1135 if (tokensItemLength > 12) 1136 { 1137 res.second = to!int(s[12 .. $]); 1138 } 1139 } 1140 } 1141 else if ((i < tokensLength && info.hms(tokens[i]) > -1) 1142 || (i + 1 < tokensLength && tokens[i] == " " && info.hms(tokens[i + 1]) > -1)) 1143 { 1144 debug(dateparser) writeln("branch 4"); 1145 //HH[ ]h or MM[ ]m or SS[.ss][ ]s 1146 if (tokens[i] == " ") 1147 { 1148 ++i; 1149 } 1150 1151 auto idx = info.hms(tokens[i]); 1152 1153 while (true) 1154 { 1155 if (idx == 0) 1156 { 1157 res.hour = to!int(value.get()); 1158 1159 if (value % 1) 1160 res.minute = to!int(60 * (value % 1)); 1161 } 1162 else if (idx == 1) 1163 { 1164 res.minute = to!int(value.get()); 1165 1166 if (value % 1) 1167 res.second = to!int(60 * (value % 1)); 1168 } 1169 else if (idx == 2) 1170 { 1171 auto temp = parseMS(value_repr); 1172 res.second = temp[0]; 1173 res.microsecond = temp[1]; 1174 } 1175 1176 ++i; 1177 1178 if (i >= tokensLength || idx == 2) 1179 break; 1180 1181 //12h00 1182 try 1183 { 1184 value_repr = tokens[i]; 1185 value = to!float(value_repr); 1186 } 1187 catch (ConvException) 1188 { 1189 break; 1190 } 1191 1192 ++i; 1193 ++idx; 1194 1195 if (i < tokensLength) 1196 { 1197 immutable newidx = info.hms(tokens[i]); 1198 1199 if (newidx > -1) 1200 idx = newidx; 1201 } 1202 } 1203 } 1204 else if (i == tokensLength && tokensLength > 3 1205 && tokens[i - 2] == " " && info.hms(tokens[i - 3]) > -1) 1206 { 1207 debug(dateparser) writeln("branch 5"); 1208 //X h MM or X m SS 1209 immutable idx = info.hms(tokens[i - 3]) + 1; 1210 1211 if (idx == 1) 1212 { 1213 res.minute = to!int(value.get()); 1214 1215 if (value % 1) 1216 res.second = to!int(60 * (value % 1)); 1217 else if (idx == 2) 1218 { 1219 auto seconds = parseMS(value_repr); 1220 res.second = seconds[0]; 1221 res.microsecond = seconds[1]; 1222 ++i; 1223 } 1224 } 1225 } 1226 else if (i + 1 < tokensLength && tokens[i] == ":") 1227 { 1228 debug(dateparser) writeln("branch 6"); 1229 //HH:MM[:SS[.ss]] 1230 static if (isSomeString!Range) 1231 { 1232 if (tokensLength == 5 && info.ampm(tokens[4]) == -1) 1233 { 1234 try 1235 { 1236 res.shortcutTimeResult = TimeOfDay.fromISOExtString(timeString); 1237 return res; 1238 } 1239 catch (DateTimeException) {} 1240 } 1241 } 1242 res.hour = to!int(value.get()); 1243 ++i; 1244 value = to!float(tokens[i]); 1245 res.minute = to!int(value.get()); 1246 1247 if (value % 1) 1248 res.second = to!int(60 * (value % 1)); 1249 1250 ++i; 1251 1252 if (i < tokensLength && tokens[i] == ":") 1253 { 1254 auto temp = parseMS(tokens[i + 1]); 1255 res.second = temp[0]; 1256 res.microsecond = temp[1]; 1257 i += 2; 1258 } 1259 } 1260 else if (i < tokensLength && (tokens[i] == "-" || tokens[i] == "/" 1261 || tokens[i] == ".")) 1262 { 1263 debug(dateparser) writeln("branch 7"); 1264 immutable string separator = tokens[i]; 1265 ymd.put(value_repr); 1266 ++i; 1267 1268 if (i < tokensLength && !info.jump(tokens[i])) 1269 { 1270 if (tokens[i][0].isNumber) 1271 { 1272 //01-01[-01] 1273 static if (isSomeString!Range) 1274 { 1275 if (tokensLength >= 11) 1276 { 1277 try 1278 { 1279 res.shortcutResult = SysTime.fromISOExtString(timeString); 1280 return res; 1281 } 1282 catch (DateTimeException) {} 1283 } 1284 } 1285 1286 ymd.put(tokens[i]); 1287 } 1288 else 1289 { 1290 //01-Jan[-01] 1291 value = info.month(tokens[i]); 1292 1293 if (!value.isNull()) 1294 { 1295 ymd.put(value.get()); 1296 mstridx = cast(ptrdiff_t) (ymd.length == 0 ? 0 : ymd.length - 1); 1297 } 1298 else 1299 { 1300 res.badData = true; 1301 return res; 1302 } 1303 } 1304 1305 ++i; 1306 1307 if (i < tokensLength && tokens[i] == separator) 1308 { 1309 //We have three members 1310 ++i; 1311 value = info.month(tokens[i]); 1312 1313 if (value > -1) 1314 { 1315 ymd.put(value.get()); 1316 mstridx = ymd.length - 1; 1317 } 1318 else 1319 ymd.put(tokens[i]); 1320 1321 ++i; 1322 } 1323 } 1324 } 1325 else if (i >= tokensLength || info.jump(tokens[i])) 1326 { 1327 debug(dateparser) writeln("branch 8"); 1328 if (i + 1 < tokensLength && info.ampm(tokens[i + 1]) > -1) 1329 { 1330 //12 am 1331 res.hour = to!int(value.get()); 1332 1333 if (res.hour < 12 && info.ampm(tokens[i + 1]) == 1) 1334 res.hour += 12; 1335 else if (res.hour == 12 && info.ampm(tokens[i + 1]) == 0) 1336 res.hour = 0; 1337 1338 ++i; 1339 } 1340 else 1341 { 1342 //Year, month or day 1343 ymd.put(value.get()); 1344 } 1345 ++i; 1346 } 1347 else if (info.ampm(tokens[i]) > -1) 1348 { 1349 debug(dateparser) writeln("branch 9"); 1350 //12am 1351 res.hour = to!int(value.get()); 1352 1353 if (res.hour < 12 && info.ampm(tokens[i]) == 1) 1354 res.hour += 12; 1355 else if (res.hour == 12 && info.ampm(tokens[i]) == 0) 1356 res.hour = 0; 1357 1358 ++i; 1359 } 1360 else if (!fuzzy) 1361 { 1362 debug(dateparser) writeln("branch 10"); 1363 res.badData = true; 1364 return res; 1365 } 1366 else 1367 { 1368 debug(dateparser) writeln("branch 11"); 1369 ++i; 1370 } 1371 continue; 1372 } 1373 1374 //Check weekday 1375 value = info.weekday(tokens[i]); 1376 if (value > -1) 1377 { 1378 debug(dateparser) writeln("branch 12"); 1379 res.weekday = to!uint(value.get()); 1380 ++i; 1381 continue; 1382 } 1383 1384 //Check month name 1385 value = info.month(tokens[i]); 1386 if (value > -1) 1387 { 1388 debug(dateparser) writeln("branch 13"); 1389 ymd.put(value.get); 1390 assert(mstridx == -1); 1391 mstridx = ymd.length - 1; 1392 1393 ++i; 1394 if (i < tokensLength) 1395 { 1396 if (tokens[i] == "-" || tokens[i] == "/") 1397 { 1398 //Jan-01[-99] 1399 immutable separator = tokens[i]; 1400 ++i; 1401 ymd.put(tokens[i]); 1402 ++i; 1403 1404 if (i < tokensLength && tokens[i] == separator) 1405 { 1406 //Jan-01-99 1407 ++i; 1408 ymd.put(tokens[i]); 1409 ++i; 1410 } 1411 } 1412 else if (i + 3 < tokensLength && tokens[i] == " " 1413 && tokens[i + 2] == " " && info.pertain(tokens[i + 1])) 1414 { 1415 //Jan of 01 1416 //In this case, 01 is clearly year 1417 try 1418 { 1419 value = to!int(tokens[i + 3]); 1420 //Convert it here to become unambiguous 1421 ymd.put(info.convertYear(value.get.to!int())); 1422 } 1423 catch (ConvException) {} 1424 i += 4; 1425 } 1426 } 1427 continue; 1428 } 1429 1430 //Check am/pm 1431 value = info.ampm(tokens[i]); 1432 if (value > -1) 1433 { 1434 debug(dateparser) writeln("branch 14"); 1435 //For fuzzy parsing, 'a' or 'am' (both valid English words) 1436 //may erroneously trigger the AM/PM flag. Deal with that 1437 //here. 1438 bool valIsAMPM = true; 1439 1440 //If there's already an AM/PM flag, this one isn't one. 1441 if (fuzzy && !res.ampm.isNull()) 1442 valIsAMPM = false; 1443 1444 //If AM/PM is found and hour is not, raise a ValueError 1445 if (res.hour.isNull) 1446 { 1447 if (fuzzy) 1448 valIsAMPM = false; 1449 else 1450 throw new ConvException("No hour specified with AM or PM flag."); 1451 } 1452 else if (!(0 <= res.hour && res.hour <= 12)) 1453 { 1454 //If AM/PM is found, it's a 12 hour clock, so raise 1455 //an error for invalid range 1456 if (fuzzy) 1457 valIsAMPM = false; 1458 else 1459 throw new ConvException("Invalid hour specified for 12-hour clock."); 1460 } 1461 1462 if (valIsAMPM) 1463 { 1464 if (value == 1 && res.hour < 12) 1465 res.hour += 12; 1466 else if (value == 0 && res.hour == 12) 1467 res.hour = 0; 1468 1469 res.ampm = to!uint(value.get()); 1470 } 1471 1472 ++i; 1473 continue; 1474 } 1475 1476 //Check for a timezone name 1477 auto itemUpper = allocator.makeArray!(Unqual!(typeof(tokens[i].byCodeUnit[0])))( 1478 tokens[i].byCodeUnit.filter!(a => !isUpper(a)) 1479 ); 1480 scope(exit) allocator.dispose(itemUpper); 1481 1482 if (!res.hour.isNull && tokens[i].length <= 5 1483 && res.tzname.length == 0 && res.tzoffset.isNull && itemUpper.length == 0) 1484 { 1485 debug(dateparser) writeln("branch 15"); 1486 res.tzname = tokens[i]; 1487 1488 if (info.tzoffset(res.tzname) > -1) 1489 res.tzoffset = info.tzoffset(res.tzname); 1490 1491 ++i; 1492 1493 //Check for something like GMT+3, or BRST+3. Notice 1494 //that it doesn't mean "I am 3 hours after GMT", but 1495 //"my time +3 is GMT". If found, we reverse the 1496 //logic so that timezone parsing code will get it 1497 //right. 1498 if (i < tokensLength && (tokens[i] == "+" || tokens[i] == "-")) 1499 { 1500 tokens[i] = tokens[i] == "+" ? "-" : "+"; 1501 res.tzoffset = 0; 1502 if (info.utczone(res.tzname)) 1503 { 1504 //With something like GMT+3, the timezone 1505 //is *not* GMT. 1506 res.tzname = []; 1507 } 1508 } 1509 1510 continue; 1511 } 1512 1513 //Check for a numbered timezone 1514 if (!res.hour.isNull && (tokens[i] == "+" || tokens[i] == "-")) 1515 { 1516 debug(dateparser) writeln("branch 16"); 1517 immutable int signal = tokens[i] == "+" ? 1 : -1; 1518 ++i; 1519 immutable size_t tokensItemLength = tokens[i].length; 1520 1521 if (tokensItemLength == 4) 1522 { 1523 //-0300 1524 res.tzoffset = to!int(tokens[i][0 .. 2]) * 3600 + to!int(tokens[i][2 .. $]) * 60; 1525 } 1526 else if (i + 1 < tokensLength && tokens[i + 1] == ":") 1527 { 1528 //-03:00 1529 res.tzoffset = to!int(tokens[i]) * 3600 + to!int(tokens[i + 2]) * 60; 1530 i += 2; 1531 } 1532 else if (tokensItemLength <= 2) 1533 { 1534 //-[0]3 1535 res.tzoffset = to!int(tokens[i][0 .. 2]) * 3600; 1536 } 1537 else 1538 { 1539 res.badData = true; 1540 return res; 1541 } 1542 ++i; 1543 1544 res.tzoffset *= signal; 1545 1546 //Look for a timezone name between parenthesis 1547 if (i + 3 < tokensLength) 1548 { 1549 auto itemForwardUpper = allocator.makeArray!(Unqual!(typeof(tokens[i + 2].byCodeUnit[0])))( 1550 tokens[i + 2].byCodeUnit.filter!(a => !isUpper(a)) 1551 ); 1552 scope(exit) allocator.dispose(itemForwardUpper); 1553 1554 if (info.jump(tokens[i]) && tokens[i + 1] == "(" 1555 && tokens[i + 3] == ")" && 3 <= tokens[i + 2].length 1556 && tokens[i + 2].length <= 5 && itemForwardUpper.length == 0) 1557 { 1558 //-0300 (BRST) 1559 res.tzname = tokens[i + 2]; 1560 i += 4; 1561 } 1562 } 1563 continue; 1564 } 1565 1566 //Check jumps 1567 if (!(info.jump(tokens[i]) || fuzzy)) 1568 { 1569 debug(dateparser) writeln("branch 17"); 1570 res.badData = true; 1571 return res; 1572 } 1573 1574 last_skipped_token_i = i; 1575 ++i; 1576 } 1577 1578 auto ymdResult = ymd.resolveYMD(tokens[], mstridx, yearFirst, dayFirst); 1579 1580 // year 1581 if (ymdResult[0] > -1) 1582 { 1583 res.year = ymdResult[0]; 1584 res.centurySpecified = ymd.centurySpecified; 1585 } 1586 1587 // month 1588 if (ymdResult[1] > 0) 1589 res.month = ymdResult[1]; 1590 1591 // day 1592 if (ymdResult[2] > 0) 1593 res.day = ymdResult[2]; 1594 1595 info.validate(res); 1596 return res; 1597 } 1598 }