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