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 dateparser2; 28 29 debug(dateparser2) import std.stdio; 30 import std.datetime; 31 import std.traits; 32 import std.typecons; 33 import std.exception : enforce; 34 import std.regex; 35 import std.range; 36 import dateparser2.timelexer; 37 import dateparser2.ymd; 38 import dateparser2.parseresult; 39 public import dateparser2.parserinfo; 40 41 private: 42 43 Parser defaultParser; 44 static this() 45 { 46 defaultParser = new Parser(new ParserInfo()); 47 } 48 49 /** 50 * Parse a I[.F] seconds value into (seconds, microseconds) 51 * 52 * Params: 53 * value = value to parse 54 * Returns: 55 * tuple of two `int`s 56 */ 57 auto parseMS(R)(R s) if ( 58 isForwardRange!R && 59 !isInfinite!R && 60 isSomeChar!(ElementEncodingType!R)) 61 { 62 import std.string : leftJustifier; 63 import std.algorithm.searching : canFind; 64 import std.algorithm.iteration : splitter; 65 import std.typecons : tuple; 66 import std.conv : parse; 67 import std.utf : byCodeUnit; 68 69 // auto decoding special case 70 static if (isNarrowString!R) 71 auto value = s.byCodeUnit; 72 else 73 alias value = s; 74 75 if (!(value.save.canFind('.'))) 76 { 77 return tuple(parse!int(value), 0); 78 } 79 else 80 { 81 auto splitValue = value.splitter('.'); 82 auto secs = splitValue.front; 83 splitValue.popFront(); 84 auto msecs = splitValue.front.leftJustifier(6, '0'); 85 return tuple( 86 parse!int(secs), 87 parse!int(msecs) 88 ); 89 } 90 } 91 92 @safe pure unittest 93 { 94 import std.typecons : tuple; 95 import std.utf : byChar; 96 97 auto s = "123"; 98 assert(s.parseMS == tuple(123, 0)); 99 100 auto s2 = "123.4"; 101 assert(s2.parseMS == tuple(123, 400000)); 102 103 auto s3 = "123.4567".byChar; 104 assert(s3.parseMS == tuple(123, 456700)); 105 } 106 107 void setAttribute(P, T)(ref P p, string name, auto ref T value) 108 { 109 foreach (mem; __traits(allMembers, P)) 110 { 111 static if (is(typeof(__traits(getMember, p, mem)) Q)) 112 { 113 static if (is(T : Q)) 114 { 115 if (mem == name) 116 { 117 __traits(getMember, p, mem) = value; 118 return; 119 } 120 } 121 } 122 } 123 assert(0, P.stringof ~ " has no member " ~ name); 124 } 125 126 public: 127 128 /** 129 This function offers a generic date/time string Parser which is able to parse 130 most known formats to represent a date and/or time. 131 132 This function attempts to be forgiving with regards to unlikely input formats, 133 returning a `SysTime` object even for dates which are ambiguous. 134 135 If an element of a date/time stamp is omitted, the following rules are applied: 136 137 $(UL 138 $(LI If AM or PM is left unspecified, a 24-hour clock is assumed, however, 139 an hour on a 12-hour clock (0 <= hour <= 12) *must* be specified if 140 AM or PM is specified.) 141 $(LI If a time zone is omitted, a SysTime is given with the timezone of the 142 host machine.) 143 ) 144 145 Missing information is allowed, and what ever is given is applied on top of 146 the `defaultDate` parameter, which defaults to January 1, 1 AD at midnight. 147 E.g. a string of `"10:00 AM"` with a `defaultDate` of 148 `SysTime(Date(2016, 1, 1))` will yield `SysTime(DateTime(2016, 1, 1, 10, 0, 0))`. 149 150 If your date string uses timezone names in place of UTC offsets, then timezone 151 information must be user provided, as there is no way to reliably get timezones 152 from the OS by abbreviation. But, the timezone will be properly set if an offset 153 is given. Timezone info and their abbreviations change constantly, so it's a 154 good idea to not rely on `timezoneInfos` too much. 155 156 This function allocates memory and throws on the GC. In order to reduce GC allocations, 157 use a custom `Parser` instance with a different allocator. 158 159 Unicode_Specifics: 160 $(OL 161 $(LI The AA key comparisons done with `ParserInfo` are on a code unit by code 162 unit basis. As such, if user data passed to this function has a different 163 normalization than the AAs in the used `ParserInfo` class, then you will 164 get parser exceptions.) 165 $(LI While other languages have writing systems without Arabic numerals, 166 the overwhelming majority of dates are written with them. As such, 167 this function does not work with other number systems and expects ASCII 168 numbers.) 169 ) 170 171 Params: 172 timeString = A forward range containing a date/time stamp. 173 ignoreTimezone = Set to false by default, time zones in parsed strings are ignored and a 174 SysTime with the local time zone is returned. If timezone information 175 is not important, setting this to true is slightly faster. 176 timezoneInfos = Time zone names / aliases which may be present in the 177 string. This argument maps time zone names (and optionally offsets 178 from those time zones) to time zones. This parameter is ignored if 179 ignoreTimezone is set. 180 dayFirst = Whether to interpret the first value in an ambiguous 3-integer date 181 (e.g. 01/05/09) as the day (`true`) or month (`false`). If 182 yearFirst is set to true, this distinguishes between YDM and 183 YMD. 184 yearFirst = Whether to interpret the first value in an ambiguous 3-integer date 185 (e.g. 01/05/09) as the year. If true, the first number is taken to 186 be the year, otherwise the last number is taken to be the year. 187 fuzzy = Whether to allow fuzzy parsing, allowing for string like "Today is 188 January 1, 2047 at 8:21:00AM". 189 defaultDate = The date to apply the given information on top of. Defaults to 190 January 1st, 1 AD 191 192 Returns: 193 A SysTime object representing the parsed string 194 195 Throws: 196 `ConvException` will be thrown for invalid string or unknown string format 197 198 Throws: 199 `TimeException` if the date string is successfully parsed but the created 200 date would be invalid 201 202 Throws: 203 `ConvOverflowException` if one of the numbers in the parsed date exceeds 204 `float.max` 205 */ 206 SysTime parse(Range)(Range timeString, 207 Flag!"ignoreTimezone" ignoreTimezone = No.ignoreTimezone, 208 const(TimeZone)[string] timezoneInfos = null, 209 Flag!"dayFirst" dayFirst = No.dayFirst, 210 Flag!"yearFirst" yearFirst = No.yearFirst, 211 Flag!"fuzzy" fuzzy = No.fuzzy, 212 SysTime defaultDate = SysTime(DateTime(1, 1, 1))) if ( 213 isForwardRange!Range && !isInfinite!Range && isSomeChar!(ElementEncodingType!Range)) 214 { 215 enforce(defaultParser !is null, "Accessing defaultParser before static this initalization. Use your own Parser instance."); 216 // dfmt off 217 return defaultParser.parse( 218 timeString, 219 ignoreTimezone, 220 timezoneInfos, 221 dayFirst, 222 yearFirst, 223 fuzzy, 224 defaultDate 225 ); 226 } 227 228 /// 229 @safe unittest 230 { 231 immutable brazilTime = new SimpleTimeZone(dur!"seconds"(-10_800)); 232 const(TimeZone)[string] timezones = ["BRST" : brazilTime]; 233 234 immutable parsed = parse("Thu Sep 25 10:36:28 BRST 2003", No.ignoreTimezone, timezones); 235 // SysTime opEquals ignores timezones 236 assert(parsed == SysTime(DateTime(2003, 9, 25, 10, 36, 28))); 237 238 () @trusted { 239 assert(parsed.timezone == brazilTime); 240 }(); 241 242 assert(parse( 243 "2003 10:36:28 BRST 25 Sep Thu", 244 No.ignoreTimezone, 245 timezones 246 ) == SysTime(DateTime(2003, 9, 25, 10, 36, 28))); 247 assert(parse("Thu Sep 25 10:36:28") == SysTime(DateTime(1, 9, 25, 10, 36, 28))); 248 assert(parse("20030925T104941") == SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 249 assert(parse("2003-09-25T10:49:41") == SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 250 assert(parse("10:36:28") == SysTime(DateTime(1, 1, 1, 10, 36, 28))); 251 assert(parse("09-25-2003") == SysTime(DateTime(2003, 9, 25))); 252 } 253 254 /// Apply information on top of `defaultDate` 255 @safe unittest 256 { 257 assert("10:36:28".parse(No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 258 No.fuzzy, SysTime(DateTime(2016, 3, 15))) 259 == SysTime(DateTime(2016, 3, 15, 10, 36, 28))); 260 assert("August 07".parse(No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 261 No.fuzzy, SysTime(DateTime(2016, 1, 1))) 262 == SysTime(Date(2016, 8, 7))); 263 assert("2000".parse(No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 264 No.fuzzy, SysTime(DateTime(2016, 3, 1))) 265 == SysTime(Date(2000, 3, 1))); 266 } 267 268 @safe unittest 269 { 270 auto customParser = new Parser(new ParserInfo()); 271 assert(customParser.parse("2003-09-25T10:49:41") == 272 SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 273 } 274 275 /// Exceptions 276 @safe unittest 277 { 278 import std.exception : assertThrown; 279 import std.conv : ConvException; 280 281 assertThrown!ConvException(parse("")); 282 assertThrown!ConvException(parse("AM")); 283 assertThrown!ConvException(parse("The quick brown fox jumps over the lazy dog")); 284 assertThrown!TimeException(parse("Feb 30, 2007")); 285 assertThrown!TimeException(parse("Jan 20, 2015 PM")); 286 assertThrown!ConvException(parse("01-Jane-01")); 287 assertThrown!ConvException(parse("13:44 AM")); 288 assertThrown!ConvException(parse("January 25, 1921 23:13 PM")); 289 } 290 // dfmt on 291 292 @safe unittest { 293 auto p = parse("0000-00-00"); 294 assert(p == SysTime(DateTime(0, 1, 1, 0, 0, 0)), (cast(DateTime)p).toISOExtString()); 295 } 296 297 @safe 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 @safe 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 @safe 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 @safe 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 @safe 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 @safe 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 @safe 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 @safe 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 @safe 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 @safe 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 @safe 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 @safe 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 @safe 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 @safe 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(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 @safe 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 @safe 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 @safe 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 * parserInfo = the parser info to reference when parsing 659 */ 660 final class Parser { 661 private const ParserInfo info; 662 663 public: 664 /// 665 this(const ParserInfo parserInfo = null) @safe 666 { 667 if (parserInfo is null) 668 { 669 info = new ParserInfo(); 670 } 671 else 672 { 673 info = parserInfo; 674 } 675 } 676 677 /** 678 * This function has the same functionality as the free version of `parse`. 679 * The only difference is this will use your custom `ParserInfo` or allocator 680 * if provided. 681 */ 682 SysTime parse(Range)(Range timeString, 683 Flag!"ignoreTimezone" ignoreTimezone = No.ignoreTimezone, 684 const(TimeZone)[string] timezoneInfos = null, 685 Flag!"dayFirst" dayFirst = No.dayFirst, 686 Flag!"yearFirst" yearFirst = No.yearFirst, 687 Flag!"fuzzy" fuzzy = No.fuzzy, 688 SysTime defaultDate = SysTime(Date(1, 1, 1))) @safe 689 if(isForwardRange!Range && !isInfinite!Range && isSomeChar!(ElementEncodingType!Range)) 690 { 691 import std.conv : to, ConvException; 692 693 auto res = parseImpl(timeString, dayFirst, yearFirst, fuzzy); 694 695 if (res.badData) 696 throw new ConvException("Unknown string format"); 697 698 if (res.year.isNull() && res.month.isNull() && res.day.isNull() 699 && res.hour.isNull() && res.minute.isNull() 700 && res.second.isNull() && res.weekday.isNull() 701 && res.shortcutResult.isNull() && res.shortcutTimeResult.isNull()) 702 throw new ConvException("String does not contain a date."); 703 704 if (res.shortcutResult.isNull && res.shortcutTimeResult.isNull) { 705 if (!res.year.isNull) 706 defaultDate.year(res.year); 707 708 if (!res.day.isNull) 709 defaultDate.day(res.day); 710 711 if (!res.month.isNull) { 712 () @trusted { 713 defaultDate.month(to!Month(res.month)); 714 }(); 715 } 716 717 if (!res.hour.isNull) 718 defaultDate.hour(res.hour); 719 720 if (!res.minute.isNull) 721 defaultDate.minute(res.minute); 722 723 if (!res.second.isNull) 724 defaultDate.second(res.second); 725 726 if (!res.microsecond.isNull) 727 defaultDate.fracSecs(usecs(res.microsecond)); 728 729 if (!res.weekday.isNull() && (res.day.isNull || !res.day)) { 730 immutable delta_days = () @trusted { 731 return daysToDayOfWeek(defaultDate.dayOfWeek(), 732 to!DayOfWeek(res.weekday)); 733 }(); 734 defaultDate += dur!"days"(delta_days); 735 } 736 } else if (!res.shortcutTimeResult.isNull) { 737 defaultDate = SysTime(DateTime(Date( 738 defaultDate.year, 739 defaultDate.month, 740 defaultDate.day, 741 ), res.shortcutTimeResult.get())); 742 } 743 744 if (!ignoreTimezone) 745 { 746 if (res.tzname in timezoneInfos) 747 () @trusted { 748 defaultDate = defaultDate.toOtherTZ( 749 cast(immutable) timezoneInfos[res.tzname] 750 ); 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.array : appender, Appender; 798 import std.algorithm.searching : canFind, countUntil; 799 import std.algorithm.iteration : filter; 800 import std.uni : isUpper; 801 import std.ascii : isDigit; 802 import std.utf : byCodeUnit, byChar; 803 import std.conv : to, ConvException; 804 805 ParseResult res; 806 807 //DynamicArray!(string, Allocator, true) tokens; 808 auto tokensAppender = appender!(string[])(); 809 810 static if (is(Unqual!(ElementEncodingType!Range) == dchar) || 811 is(Unqual!(ElementEncodingType!Range) == wchar)) 812 { 813 put(tokensAppender, timeString.save.byChar.timeLexer); 814 } 815 else static if (isSomeString!Range && is(Unqual!(ElementEncodingType!Range) == char)) 816 { 817 put(tokensAppender, timeString.save.byCodeUnit.timeLexer); 818 } 819 else 820 { 821 put(tokensAppender, timeString.save.timeLexer); 822 } 823 824 debug(dateparser2) writeln("tokens: ", tokens[]); 825 826 //keep up with the last token skipped so we can recombine 827 //consecutively skipped tokens (-2 for when i begins at 0). 828 int last_skipped_token_i = -2; 829 830 //year/month/day list 831 YMD ymd; 832 833 //Index of the month string in ymd 834 ptrdiff_t mstridx = -1; 835 836 auto tokens = tokensAppender.data; 837 immutable size_t tokensLength = tokens.length; 838 debug(dateparser2) writeln("tokensLength: ", tokensLength); 839 uint i = 0; 840 while (i < tokensLength) 841 { 842 //Check if it's a number 843 Nullable!(float, float.infinity) value; 844 string value_repr; 845 debug(dateparser2) writeln("index: ", i); 846 debug(dateparser2) writeln("tokens[i]: ", tokens[i]); 847 848 if (tokens[i][0].isDigit) 849 { 850 value_repr = tokens[i]; 851 debug(dateparser2) writeln("value_repr: ", value_repr); 852 value = to!float(value_repr); 853 } 854 855 //Token is a number 856 if (!value.isNull()) 857 { 858 immutable tokensItemLength = tokens[i].length; 859 ++i; 860 861 if (ymd.length == 3 && (tokensItemLength == 2 862 || tokensItemLength == 4) && res.hour.isNull 863 && (i >= tokensLength || (tokens[i] != ":" && info.hms(tokens[i]) == -1))) 864 { 865 debug(dateparser2) writeln("branch 1"); 866 //19990101T23[59] 867 auto s = tokens[i - 1]; 868 res.hour = to!int(s[0 .. 2]); 869 870 if (tokensItemLength == 4) 871 { 872 res.minute = to!int(s[2 .. $]); 873 } 874 } 875 else if (tokensItemLength == 6 || (tokensItemLength > 6 876 && tokens[i - 1].countUntil('.') == 6)) 877 { 878 debug(dateparser2) writeln("branch 2"); 879 //YYMMDD || HHMMSS[.ss] 880 auto s = tokens[i - 1]; 881 882 if (ymd.length == 0 && !tokens[i - 1].canFind('.')) 883 { 884 ymd.put(s[0 .. 2]); 885 ymd.put(s[2 .. 4]); 886 ymd.put(s[4 .. $]); 887 } 888 else 889 { 890 //19990101T235959[.59] 891 res.hour = to!int(s[0 .. 2]); 892 res.minute = to!int(s[2 .. 4]); 893 auto ms = parseMS(s[4 .. $]); 894 res.second = ms[0]; 895 res.microsecond = ms[1]; 896 } 897 } 898 else if (tokensItemLength == 8 || tokensItemLength == 12 || tokensItemLength == 14) 899 { 900 debug(dateparser2) writeln("branch 3"); 901 //YYYYMMDD 902 auto s = tokens[i - 1]; 903 ymd.put(s[0 .. 4]); 904 ymd.put(s[4 .. 6]); 905 ymd.put(s[6 .. 8]); 906 907 if (tokensItemLength > 8) 908 { 909 res.hour = to!int(s[8 .. 10]); 910 res.minute = to!int(s[10 .. 12]); 911 912 if (tokensItemLength > 12) 913 { 914 res.second = to!int(s[12 .. $]); 915 } 916 } 917 } 918 else if ((i < tokensLength && info.hms(tokens[i]) > -1) 919 || (i + 1 < tokensLength && tokens[i] == " " && info.hms(tokens[i + 1]) > -1)) 920 { 921 debug(dateparser2) writeln("branch 4"); 922 //HH[ ]h or MM[ ]m or SS[.ss][ ]s 923 if (tokens[i] == " ") 924 { 925 ++i; 926 } 927 928 auto idx = info.hms(tokens[i]); 929 930 while (true) 931 { 932 if (idx == 0) 933 { 934 res.hour = to!int(value.get()); 935 936 if (value % 1) 937 res.minute = to!int(60 * (value % 1)); 938 } 939 else if (idx == 1) 940 { 941 res.minute = to!int(value.get()); 942 943 if (value % 1) 944 res.second = to!int(60 * (value % 1)); 945 } 946 else if (idx == 2) 947 { 948 auto temp = parseMS(value_repr); 949 res.second = temp[0]; 950 res.microsecond = temp[1]; 951 } 952 953 ++i; 954 955 if (i >= tokensLength || idx == 2) 956 break; 957 958 //12h00 959 try 960 { 961 value_repr = tokens[i]; 962 value = to!float(value_repr); 963 } 964 catch (ConvException) 965 { 966 break; 967 } 968 969 ++i; 970 ++idx; 971 972 if (i < tokensLength) 973 { 974 immutable newidx = info.hms(tokens[i]); 975 976 if (newidx > -1) 977 idx = newidx; 978 } 979 } 980 } 981 else if (i == tokensLength && tokensLength > 3 982 && tokens[i - 2] == " " && info.hms(tokens[i - 3]) > -1) 983 { 984 debug(dateparser2) writeln("branch 5"); 985 //X h MM or X m SS 986 immutable idx = info.hms(tokens[i - 3]) + 1; 987 988 if (idx == 1) 989 { 990 res.minute = to!int(value.get()); 991 992 if (value % 1) 993 res.second = to!int(60 * (value % 1)); 994 else if (idx == 2) 995 { 996 auto seconds = parseMS(value_repr); 997 res.second = seconds[0]; 998 res.microsecond = seconds[1]; 999 ++i; 1000 } 1001 } 1002 } 1003 else if (i + 1 < tokensLength && tokens[i] == ":") 1004 { 1005 debug(dateparser2) writeln("branch 6"); 1006 //HH:MM[:SS[.ss]] 1007 static if (isSomeString!Range) 1008 { 1009 if (tokensLength == 5 && info.ampm(tokens[4]) == -1) 1010 { 1011 try 1012 { 1013 res.shortcutTimeResult = TimeOfDay.fromISOExtString(timeString); 1014 return res; 1015 } 1016 catch (DateTimeException) {} 1017 } 1018 } 1019 res.hour = to!int(value.get()); 1020 ++i; 1021 value = to!float(tokens[i]); 1022 res.minute = to!int(value.get()); 1023 1024 if (value % 1) 1025 res.second = to!int(60 * (value % 1)); 1026 1027 ++i; 1028 1029 if (i < tokensLength && tokens[i] == ":") 1030 { 1031 auto temp = parseMS(tokens[i + 1]); 1032 res.second = temp[0]; 1033 res.microsecond = temp[1]; 1034 i += 2; 1035 } 1036 } 1037 else if (i < tokensLength && (tokens[i] == "-" || tokens[i] == "/" 1038 || tokens[i] == ".")) 1039 { 1040 debug(dateparser2) writeln("branch 7"); 1041 immutable string separator = tokens[i]; 1042 ymd.put(value_repr); 1043 ++i; 1044 1045 if (i < tokensLength && !info.jump(tokens[i])) 1046 { 1047 if (tokens[i][0].isDigit) 1048 { 1049 //01-01[-01] 1050 static if (isSomeString!Range) 1051 { 1052 if (tokensLength >= 11) 1053 { 1054 try 1055 { 1056 res.shortcutResult = SysTime.fromISOExtString(timeString); 1057 return res; 1058 } 1059 catch (DateTimeException) {} 1060 } 1061 } 1062 1063 ymd.put(tokens[i]); 1064 } 1065 else 1066 { 1067 //01-Jan[-01] 1068 value = info.month(tokens[i]); 1069 1070 if (value > -1) 1071 { 1072 ymd.put(value.get()); 1073 mstridx = cast(ptrdiff_t) (ymd.length == 0 ? 0 : ymd.length - 1); 1074 } 1075 else 1076 { 1077 res.badData = true; 1078 return res; 1079 } 1080 } 1081 1082 ++i; 1083 1084 if (i < tokensLength && tokens[i] == separator) 1085 { 1086 //We have three members 1087 ++i; 1088 value = info.month(tokens[i]); 1089 1090 if (value > -1) 1091 { 1092 ymd.put(value.get()); 1093 mstridx = ymd.length - 1; 1094 } 1095 else 1096 ymd.put(tokens[i]); 1097 1098 ++i; 1099 } 1100 } 1101 } 1102 else if (i >= tokensLength || info.jump(tokens[i])) 1103 { 1104 debug(dateparser2) writeln("branch 8"); 1105 if (i + 1 < tokensLength && info.ampm(tokens[i + 1]) > -1) 1106 { 1107 //12 am 1108 res.hour = to!int(value.get()); 1109 1110 if (res.hour < 12 && info.ampm(tokens[i + 1]) == 1) 1111 res.hour += 12; 1112 else if (res.hour == 12 && info.ampm(tokens[i + 1]) == 0) 1113 res.hour = 0; 1114 1115 ++i; 1116 } 1117 else 1118 { 1119 //Year, month or day 1120 ymd.put(value.get()); 1121 } 1122 ++i; 1123 } 1124 else if (info.ampm(tokens[i]) > -1) 1125 { 1126 debug(dateparser2) writeln("branch 9"); 1127 //12am 1128 res.hour = to!int(value.get()); 1129 1130 if (res.hour < 12 && info.ampm(tokens[i]) == 1) 1131 res.hour += 12; 1132 else if (res.hour == 12 && info.ampm(tokens[i]) == 0) 1133 res.hour = 0; 1134 1135 ++i; 1136 } 1137 else if (!fuzzy) 1138 { 1139 debug(dateparser2) writeln("branch 10"); 1140 res.badData = true; 1141 return res; 1142 } 1143 else 1144 { 1145 debug(dateparser2) writeln("branch 11"); 1146 ++i; 1147 } 1148 continue; 1149 } 1150 1151 //Check weekday 1152 value = info.weekday(tokens[i]); 1153 if (value > -1) 1154 { 1155 debug(dateparser2) writeln("branch 12"); 1156 res.weekday = to!uint(value.get()); 1157 ++i; 1158 continue; 1159 } 1160 1161 //Check month name 1162 value = info.month(tokens[i]); 1163 if (value > -1) 1164 { 1165 debug(dateparser2) writeln("branch 13"); 1166 ymd.put(value.get); 1167 assert(mstridx == -1); 1168 mstridx = ymd.length - 1; 1169 1170 ++i; 1171 if (i < tokensLength) 1172 { 1173 if (tokens[i] == "-" || tokens[i] == "/") 1174 { 1175 //Jan-01[-99] 1176 immutable separator = tokens[i]; 1177 ++i; 1178 ymd.put(tokens[i]); 1179 ++i; 1180 1181 if (i < tokensLength && tokens[i] == separator) 1182 { 1183 //Jan-01-99 1184 ++i; 1185 ymd.put(tokens[i]); 1186 ++i; 1187 } 1188 } 1189 else if (i + 3 < tokensLength && tokens[i] == " " 1190 && tokens[i + 2] == " " && info.pertain(tokens[i + 1])) 1191 { 1192 //Jan of 01 1193 //In this case, 01 is clearly year 1194 try 1195 { 1196 value = to!int(tokens[i + 3]); 1197 //Convert it here to become unambiguous 1198 ymd.put(convertYear(value.get.to!int())); 1199 } 1200 catch (ConvException) {} 1201 i += 4; 1202 } 1203 } 1204 continue; 1205 } 1206 1207 //Check am/pm 1208 value = info.ampm(tokens[i]); 1209 if (value > -1) 1210 { 1211 debug(dateparser2) writeln("branch 14"); 1212 //For fuzzy parsing, 'a' or 'am' (both valid English words) 1213 //may erroneously trigger the AM/PM flag. Deal with that 1214 //here. 1215 bool valIsAMPM = true; 1216 1217 //If there's already an AM/PM flag, this one isn't one. 1218 if (fuzzy && !res.ampm.isNull()) 1219 valIsAMPM = false; 1220 1221 //If AM/PM is found and hour is not, raise a ValueError 1222 if (res.hour.isNull) 1223 { 1224 if (fuzzy) 1225 valIsAMPM = false; 1226 else 1227 throw new ConvException("No hour specified with AM or PM flag."); 1228 } 1229 else if (!(0 <= res.hour && res.hour <= 12)) 1230 { 1231 //If AM/PM is found, it's a 12 hour clock, so raise 1232 //an error for invalid range 1233 if (fuzzy) 1234 valIsAMPM = false; 1235 else 1236 throw new ConvException("Invalid hour specified for 12-hour clock."); 1237 } 1238 1239 if (valIsAMPM) 1240 { 1241 if (value == 1 && res.hour < 12) 1242 res.hour += 12; 1243 else if (value == 0 && res.hour == 12) 1244 res.hour = 0; 1245 1246 res.ampm = to!uint(value.get()); 1247 } 1248 1249 ++i; 1250 continue; 1251 } 1252 1253 //Check for a timezone name 1254 immutable upperItems = tokens[i] 1255 .byCodeUnit 1256 .filter!(a => !isUpper(a)) 1257 .walkLength(1); 1258 if (!res.hour.isNull && tokens[i].length <= 5 1259 && res.tzname.length == 0 && res.tzoffset.isNull && upperItems == 0) 1260 { 1261 debug(dateparser2) writeln("branch 15"); 1262 res.tzname = tokens[i]; 1263 1264 ++i; 1265 1266 //Check for something like GMT+3, or BRST+3. Notice 1267 //that it doesn't mean "I am 3 hours after GMT", but 1268 //"my time +3 is GMT". If found, we reverse the 1269 //logic so that timezone parsing code will get it 1270 //right. 1271 if (i < tokensLength && (tokens[i][0] == '+' || tokens[i][0] == '-')) 1272 { 1273 tokens[i] = tokens[i][0] == '+' ? "-" : "+"; 1274 res.tzoffset = 0; 1275 if (info.utczone(res.tzname)) 1276 { 1277 //With something like GMT+3, the timezone 1278 //is *not* GMT. 1279 res.tzname = []; 1280 } 1281 } 1282 1283 continue; 1284 } 1285 1286 //Check for a numbered timezone 1287 if (!res.hour.isNull && (tokens[i] == "+" || tokens[i] == "-")) 1288 { 1289 debug(dateparser2) writeln("branch 16"); 1290 immutable int signal = tokens[i][0] == '+' ? 1 : -1; 1291 ++i; 1292 immutable size_t tokensItemLength = tokens[i].length; 1293 1294 if (tokensItemLength == 4) 1295 { 1296 //-0300 1297 res.tzoffset = to!int(tokens[i][0 .. 2]) * 3600 + to!int(tokens[i][2 .. $]) * 60; 1298 } 1299 else if (i + 1 < tokensLength && tokens[i + 1] == ":") 1300 { 1301 //-03:00 1302 res.tzoffset = to!int(tokens[i]) * 3600 + to!int(tokens[i + 2]) * 60; 1303 i += 2; 1304 } 1305 else if (tokensItemLength <= 2) 1306 { 1307 //-[0]3 1308 res.tzoffset = to!int(tokens[i]) * 3600; 1309 } 1310 else 1311 { 1312 res.badData = true; 1313 return res; 1314 } 1315 ++i; 1316 1317 res.tzoffset *= signal; 1318 1319 //Look for a timezone name between parenthesis 1320 if (i + 3 < tokensLength) 1321 { 1322 immutable notUpperItems = tokens[i + 2] 1323 .byCodeUnit 1324 .filter!(a => !isUpper(a)) 1325 .walkLength(1); 1326 if (info.jump(tokens[i]) && tokens[i + 1] == "(" 1327 && tokens[i + 3] == ")" && 3 <= tokens[i + 2].length 1328 && tokens[i + 2].length <= 5 && notUpperItems == 0) 1329 { 1330 //-0300 (BRST) 1331 res.tzname = tokens[i + 2]; 1332 i += 4; 1333 } 1334 } 1335 continue; 1336 } 1337 1338 //Check jumps 1339 if (!(info.jump(tokens[i]) || fuzzy)) 1340 { 1341 debug(dateparser2) writeln("branch 17"); 1342 res.badData = true; 1343 return res; 1344 } 1345 1346 last_skipped_token_i = i; 1347 ++i; 1348 } 1349 1350 auto ymdResult = ymd.resolveYMD(tokens[], mstridx, yearFirst, dayFirst); 1351 1352 // year 1353 if (ymdResult[0] > -1) 1354 { 1355 res.year = ymdResult[0]; 1356 res.centurySpecified = ymd.centurySpecified; 1357 } 1358 1359 // month 1360 if (ymdResult[1] > 0) 1361 res.month = ymdResult[1]; 1362 1363 // day 1364 if (ymdResult[2] > 0) 1365 res.day = ymdResult[2]; 1366 1367 info.validate(res); 1368 return res; 1369 } 1370 }