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 { 294 assert(parse("Thu Sep 10:36:28") == SysTime(DateTime(1, 9, 5, 10, 36, 28))); 295 assert(parse("Thu 10:36:28") == SysTime(DateTime(1, 1, 3, 10, 36, 28))); 296 assert(parse("Sep 10:36:28") == SysTime(DateTime(1, 9, 1, 10, 36, 28))); 297 assert(parse("Sep 2003") == SysTime(DateTime(2003, 9, 1))); 298 assert(parse("Sep") == SysTime(DateTime(1, 9, 1))); 299 assert(parse("2003") == SysTime(DateTime(2003, 1, 1))); 300 assert(parse("10:36") == SysTime(DateTime(1, 1, 1, 10, 36))); 301 } 302 303 @safe unittest 304 { 305 assert(parse("Thu 10:36:28") == SysTime(DateTime(1, 1, 3, 10, 36, 28))); 306 assert(parse("20030925T104941") == SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 307 assert(parse("20030925T1049") == SysTime(DateTime(2003, 9, 25, 10, 49, 0))); 308 assert(parse("20030925T10") == SysTime(DateTime(2003, 9, 25, 10))); 309 assert(parse("20030925") == SysTime(DateTime(2003, 9, 25))); 310 assert(parse("2003-09-25 10:49:41,502") == SysTime(DateTime(2003, 9, 25, 10, 311 49, 41), msecs(502))); 312 assert(parse("199709020908") == SysTime(DateTime(1997, 9, 2, 9, 8))); 313 assert(parse("19970902090807") == SysTime(DateTime(1997, 9, 2, 9, 8, 7))); 314 } 315 316 @safe unittest 317 { 318 assert(parse("2003 09 25") == SysTime(DateTime(2003, 9, 25))); 319 assert(parse("2003 Sep 25") == SysTime(DateTime(2003, 9, 25))); 320 assert(parse("25 Sep 2003") == SysTime(DateTime(2003, 9, 25))); 321 assert(parse("25 Sep 2003") == SysTime(DateTime(2003, 9, 25))); 322 assert(parse("Sep 25 2003") == SysTime(DateTime(2003, 9, 25))); 323 assert(parse("09 25 2003") == SysTime(DateTime(2003, 9, 25))); 324 assert(parse("25 09 2003") == SysTime(DateTime(2003, 9, 25))); 325 assert(parse("10 09 2003", No.ignoreTimezone, null, 326 Yes.dayFirst) == SysTime(DateTime(2003, 9, 10))); 327 assert(parse("10 09 2003") == SysTime(DateTime(2003, 10, 9))); 328 assert(parse("10 09 03") == SysTime(DateTime(2003, 10, 9))); 329 assert(parse("10 09 03", No.ignoreTimezone, null, No.dayFirst, 330 Yes.yearFirst) == SysTime(DateTime(2010, 9, 3))); 331 assert(parse("25 09 03") == SysTime(DateTime(2003, 9, 25))); 332 } 333 334 @safe unittest 335 { 336 assert(parse("03 25 Sep") == SysTime(DateTime(2003, 9, 25))); 337 assert(parse("2003 25 Sep") == SysTime(DateTime(2003, 9, 25))); 338 assert(parse("25 03 Sep") == SysTime(DateTime(2025, 9, 3))); 339 assert(parse("Thu Sep 25 2003") == SysTime(DateTime(2003, 9, 25))); 340 assert(parse("Sep 25 2003") == SysTime(DateTime(2003, 9, 25))); 341 } 342 343 // Naked times 344 @safe unittest 345 { 346 assert(parse("10h36m28.5s") == SysTime(DateTime(1, 1, 1, 10, 36, 28), msecs(500))); 347 assert(parse("10h36m28s") == SysTime(DateTime(1, 1, 1, 10, 36, 28))); 348 assert(parse("10h36m") == SysTime(DateTime(1, 1, 1, 10, 36))); 349 assert(parse("10h") == SysTime(DateTime(1, 1, 1, 10, 0, 0))); 350 assert(parse("10 h 36") == SysTime(DateTime(1, 1, 1, 10, 36, 0))); 351 assert(parse("10 hours 36 minutes") == SysTime(DateTime(1, 1, 1, 10, 36, 0))); 352 } 353 354 // AM vs PM 355 @safe unittest 356 { 357 assert(parse("10h am") == SysTime(DateTime(1, 1, 1, 10))); 358 assert(parse("10h pm") == SysTime(DateTime(1, 1, 1, 22))); 359 assert(parse("10am") == SysTime(DateTime(1, 1, 1, 10))); 360 assert(parse("10pm") == SysTime(DateTime(1, 1, 1, 22))); 361 assert(parse("12 am") == SysTime(DateTime(1, 1, 1, 0, 0))); 362 assert(parse("12am") == SysTime(DateTime(1, 1, 1, 0, 0))); 363 assert(parse("11 pm") == SysTime(DateTime(1, 1, 1, 23, 0))); 364 assert(parse("10:00 am") == SysTime(DateTime(1, 1, 1, 10))); 365 assert(parse("10:00 pm") == SysTime(DateTime(1, 1, 1, 22))); 366 assert(parse("10:00am") == SysTime(DateTime(1, 1, 1, 10))); 367 assert(parse("10:00pm") == SysTime(DateTime(1, 1, 1, 22))); 368 assert(parse("10:00a.m") == SysTime(DateTime(1, 1, 1, 10))); 369 assert(parse("10:00p.m") == SysTime(DateTime(1, 1, 1, 22))); 370 assert(parse("10:00a.m.") == SysTime(DateTime(1, 1, 1, 10))); 371 assert(parse("10:00p.m.") == SysTime(DateTime(1, 1, 1, 22))); 372 } 373 374 // ISO and ISO stripped 375 @safe unittest 376 { 377 immutable zone = new SimpleTimeZone(dur!"seconds"(-10_800)); 378 379 immutable parsed = parse("2003-09-25T10:49:41.5-03:00"); 380 assert(parsed == SysTime(DateTime(2003, 9, 25, 10, 49, 41), msecs(500), zone)); 381 assert((cast(immutable(SimpleTimeZone)) parsed.timezone).utcOffset == hours(-3)); 382 383 immutable parsed2 = parse("2003-09-25T10:49:41-03:00"); 384 assert(parsed2 == SysTime(DateTime(2003, 9, 25, 10, 49, 41), zone)); 385 assert((cast(immutable(SimpleTimeZone)) parsed2.timezone).utcOffset == hours(-3)); 386 387 assert(parse("2003-09-25T10:49:41") == SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 388 assert(parse("2003-09-25T10:49") == SysTime(DateTime(2003, 9, 25, 10, 49))); 389 assert(parse("2003-09-25T10") == SysTime(DateTime(2003, 9, 25, 10))); 390 assert(parse("2003-09-25") == SysTime(DateTime(2003, 9, 25))); 391 392 immutable parsed3 = parse("2003-09-25T10:49:41-03:00"); 393 assert(parsed3 == SysTime(DateTime(2003, 9, 25, 10, 49, 41), zone)); 394 assert((cast(immutable(SimpleTimeZone)) parsed3.timezone).utcOffset == hours(-3)); 395 396 immutable parsed4 = parse("20030925T104941-0300"); 397 assert(parsed4 == SysTime(DateTime(2003, 9, 25, 10, 49, 41), zone)); 398 assert((cast(immutable(SimpleTimeZone)) parsed4.timezone).utcOffset == hours(-3)); 399 400 assert(parse("20030925T104941") == SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 401 assert(parse("20030925T1049") == SysTime(DateTime(2003, 9, 25, 10, 49, 0))); 402 assert(parse("20030925T10") == SysTime(DateTime(2003, 9, 25, 10))); 403 assert(parse("20030925") == SysTime(DateTime(2003, 9, 25))); 404 } 405 406 // Dashes 407 @safe unittest 408 { 409 assert(parse("2003-09-25") == SysTime(DateTime(2003, 9, 25))); 410 assert(parse("2003-Sep-25") == SysTime(DateTime(2003, 9, 25))); 411 assert(parse("25-Sep-2003") == SysTime(DateTime(2003, 9, 25))); 412 assert(parse("25-Sep-2003") == SysTime(DateTime(2003, 9, 25))); 413 assert(parse("Sep-25-2003") == SysTime(DateTime(2003, 9, 25))); 414 assert(parse("09-25-2003") == SysTime(DateTime(2003, 9, 25))); 415 assert(parse("25-09-2003") == SysTime(DateTime(2003, 9, 25))); 416 assert(parse("10-09-2003", No.ignoreTimezone, null, 417 Yes.dayFirst) == SysTime(DateTime(2003, 9, 10))); 418 assert(parse("10-09-2003") == SysTime(DateTime(2003, 10, 9))); 419 assert(parse("10-09-03") == SysTime(DateTime(2003, 10, 9))); 420 assert(parse("10-09-03", No.ignoreTimezone, null, No.dayFirst, 421 Yes.yearFirst) == SysTime(DateTime(2010, 9, 3))); 422 assert(parse("01-99") == SysTime(DateTime(1999, 1, 1))); 423 assert(parse("99-01") == SysTime(DateTime(1999, 1, 1))); 424 assert(parse("13-01", No.ignoreTimezone, null, Yes.dayFirst) == SysTime(DateTime(1, 425 1, 13))); 426 assert(parse("01-13") == SysTime(DateTime(1, 1, 13))); 427 assert(parse("01-99-Jan") == SysTime(DateTime(1999, 1, 1))); 428 } 429 430 // Dots 431 @safe unittest 432 { 433 assert(parse("2003.09.25") == SysTime(DateTime(2003, 9, 25))); 434 assert(parse("2003.Sep.25") == SysTime(DateTime(2003, 9, 25))); 435 assert(parse("25.Sep.2003") == SysTime(DateTime(2003, 9, 25))); 436 assert(parse("25.Sep.2003") == SysTime(DateTime(2003, 9, 25))); 437 assert(parse("Sep.25.2003") == SysTime(DateTime(2003, 9, 25))); 438 assert(parse("09.25.2003") == SysTime(DateTime(2003, 9, 25))); 439 assert(parse("25.09.2003") == SysTime(DateTime(2003, 9, 25))); 440 assert(parse("10.09.2003", No.ignoreTimezone, null, 441 Yes.dayFirst) == SysTime(DateTime(2003, 9, 10))); 442 assert(parse("10.09.2003") == SysTime(DateTime(2003, 10, 9))); 443 assert(parse("10.09.03") == SysTime(DateTime(2003, 10, 9))); 444 assert(parse("10.09.03", No.ignoreTimezone, null, No.dayFirst, 445 Yes.yearFirst) == SysTime(DateTime(2010, 9, 3))); 446 } 447 448 // Slashes 449 @safe unittest 450 { 451 assert(parse("2003/09/25") == SysTime(DateTime(2003, 9, 25))); 452 assert(parse("2003/Sep/25") == SysTime(DateTime(2003, 9, 25))); 453 assert(parse("25/Sep/2003") == SysTime(DateTime(2003, 9, 25))); 454 assert(parse("25/Sep/2003") == SysTime(DateTime(2003, 9, 25))); 455 assert(parse("Sep/25/2003") == SysTime(DateTime(2003, 9, 25))); 456 assert(parse("09/25/2003") == SysTime(DateTime(2003, 9, 25))); 457 assert(parse("25/09/2003") == SysTime(DateTime(2003, 9, 25))); 458 assert(parse("10/09/2003", No.ignoreTimezone, null, 459 Yes.dayFirst) == SysTime(DateTime(2003, 9, 10))); 460 assert(parse("10/09/2003") == SysTime(DateTime(2003, 10, 9))); 461 assert(parse("10/09/03") == SysTime(DateTime(2003, 10, 9))); 462 assert(parse("10/09/03", No.ignoreTimezone, null, No.dayFirst, 463 Yes.yearFirst) == SysTime(DateTime(2010, 9, 3))); 464 } 465 466 // Random formats 467 @safe unittest 468 { 469 assert(parse("Wed, July 10, '96") == SysTime(DateTime(1996, 7, 10, 0, 0))); 470 assert(parse("1996.07.10 AD at 15:08:56 PDT", 471 Yes.ignoreTimezone) == SysTime(DateTime(1996, 7, 10, 15, 8, 56))); 472 assert(parse("1996.July.10 AD 12:08 PM") == SysTime(DateTime(1996, 7, 10, 12, 8))); 473 assert(parse("Tuesday, April 12, 1952 AD 3:30:42pm PST", 474 Yes.ignoreTimezone) == SysTime(DateTime(1952, 4, 12, 15, 30, 42))); 475 assert(parse("November 5, 1994, 8:15:30 am EST", 476 Yes.ignoreTimezone) == SysTime(DateTime(1994, 11, 5, 8, 15, 30))); 477 assert(parse("1994-11-05T08:15:30-05:00", 478 Yes.ignoreTimezone) == SysTime(DateTime(1994, 11, 5, 8, 15, 30))); 479 assert(parse("1994-11-05T08:15:30Z", 480 Yes.ignoreTimezone) == SysTime(DateTime(1994, 11, 5, 8, 15, 30))); 481 assert(parse("July 4, 1976") == SysTime(DateTime(1976, 7, 4))); 482 assert(parse("7 4 1976") == SysTime(DateTime(1976, 7, 4))); 483 assert(parse("4 jul 1976") == SysTime(DateTime(1976, 7, 4))); 484 assert(parse("7-4-76") == SysTime(DateTime(1976, 7, 4))); 485 assert(parse("19760704") == SysTime(DateTime(1976, 7, 4))); 486 assert(parse("0:01:02") == SysTime(DateTime(1, 1, 1, 0, 1, 2))); 487 assert(parse("12h 01m02s am") == SysTime(DateTime(1, 1, 1, 0, 1, 2))); 488 assert(parse("0:01:02 on July 4, 1976") == SysTime(DateTime(1976, 7, 4, 0, 1, 2))); 489 assert(parse("0:01:02 on July 4, 1976") == SysTime(DateTime(1976, 7, 4, 0, 1, 2))); 490 assert(parse("1976-07-04T00:01:02Z", 491 Yes.ignoreTimezone) == SysTime(DateTime(1976, 7, 4, 0, 1, 2))); 492 assert(parse("July 4, 1976 12:01:02 am") == SysTime(DateTime(1976, 7, 4, 0, 1, 493 2))); 494 assert(parse("Mon Jan 2 04:24:27 1995") == SysTime(DateTime(1995, 1, 2, 4, 24, 495 27))); 496 assert(parse("Tue Apr 4 00:22:12 PDT 1995", 497 Yes.ignoreTimezone) == SysTime(DateTime(1995, 4, 4, 0, 22, 12))); 498 assert(parse("04.04.95 00:22") == SysTime(DateTime(1995, 4, 4, 0, 22))); 499 assert(parse("Jan 1 1999 11:23:34.578") == SysTime(DateTime(1999, 1, 1, 11, 23, 500 34), msecs(578))); 501 assert(parse("950404 122212") == SysTime(DateTime(1995, 4, 4, 12, 22, 12))); 502 assert(parse("0:00 PM, PST", Yes.ignoreTimezone) == SysTime(DateTime(1, 1, 1, 12, 503 0))); 504 assert(parse("12:08 PM") == SysTime(DateTime(1, 1, 1, 12, 8))); 505 assert(parse("5:50 A.M. on June 13, 1990") == SysTime(DateTime(1990, 6, 13, 5, 506 50))); 507 assert(parse("3rd of May 2001") == SysTime(DateTime(2001, 5, 3))); 508 assert(parse("5th of March 2001") == SysTime(DateTime(2001, 3, 5))); 509 assert(parse("1st of May 2003") == SysTime(DateTime(2003, 5, 1))); 510 assert(parse("01h02m03") == SysTime(DateTime(1, 1, 1, 1, 2, 3))); 511 assert(parse("01h02") == SysTime(DateTime(1, 1, 1, 1, 2))); 512 assert(parse("01h02s") == SysTime(DateTime(1, 1, 1, 1, 0, 2))); 513 assert(parse("01m02") == SysTime(DateTime(1, 1, 1, 0, 1, 2))); 514 assert(parse("01m02h") == SysTime(DateTime(1, 1, 1, 2, 1))); 515 assert(parse("2004 10 Apr 11h30m") == SysTime(DateTime(2004, 4, 10, 11, 30))); 516 } 517 518 // Pertain, weekday, and month 519 @safe unittest 520 { 521 assert(parse("Sep 03") == SysTime(DateTime(1, 9, 3))); 522 assert(parse("Sep of 03") == SysTime(DateTime(2003, 9, 1))); 523 assert(parse("Wed") == SysTime(DateTime(1, 1, 2))); 524 assert(parse("Wednesday") == SysTime(DateTime(1, 1, 2))); 525 assert(parse("October") == SysTime(DateTime(1, 10, 1))); 526 assert(parse("31-Dec-00") == SysTime(DateTime(2000, 12, 31))); 527 } 528 529 // Fuzzy 530 @safe unittest 531 { 532 // Sometimes fuzzy parsing results in AM/PM flag being set without 533 // hours - if it's fuzzy it should ignore that. 534 auto s1 = "I have a meeting on March 1 1974."; 535 auto s2 = "On June 8th, 2020, I am going to be the first man on Mars"; 536 537 // Also don't want any erroneous AM or PMs changing the parsed time 538 auto s3 = "Meet me at the AM/PM on Sunset at 3:00 AM on December 3rd, 2003"; 539 auto s4 = "Meet me at 3:00AM on December 3rd, 2003 at the AM/PM on Sunset"; 540 auto s5 = "Today is 25 of September of 2003, exactly at 10:49:41 with timezone -03:00."; 541 auto s6 = "Jan 29, 1945 14:45 AM I going to see you there?"; 542 543 assert(parse(s1, No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 544 Yes.fuzzy) == SysTime(DateTime(1974, 3, 1))); 545 assert(parse(s2, No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 546 Yes.fuzzy) == SysTime(DateTime(2020, 6, 8))); 547 assert(parse(s3, No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 548 Yes.fuzzy) == SysTime(DateTime(2003, 12, 3, 3))); 549 assert(parse(s4, No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 550 Yes.fuzzy) == SysTime(DateTime(2003, 12, 3, 3))); 551 552 immutable zone = new SimpleTimeZone(dur!"hours"(-3)); 553 immutable parsed = parse(s5, No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 554 Yes.fuzzy); 555 assert(parsed == SysTime(DateTime(2003, 9, 25, 10, 49, 41), zone)); 556 557 assert(parse(s6, No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 558 Yes.fuzzy) == SysTime(DateTime(1945, 1, 29, 14, 45))); 559 } 560 561 // dfmt off 562 /// Custom parser info allows for international time representation 563 @safe unittest 564 { 565 import std.utf : byChar; 566 567 class RusParserInfo : ParserInfo 568 { 569 this() 570 { 571 monthsAA = ParserInfo.convert([ 572 ["янв", "Январь"], 573 ["фев", "Февраль"], 574 ["мар", "Март"], 575 ["апр", "Апрель"], 576 ["май", "Май"], 577 ["июн", "Июнь"], 578 ["июл", "Июль"], 579 ["авг", "Август"], 580 ["сен", "Сентябрь"], 581 ["окт", "Октябрь"], 582 ["ноя", "Ноябрь"], 583 ["дек", "Декабрь"] 584 ]); 585 } 586 } 587 588 auto rusParser = new Parser(new RusParserInfo()); 589 immutable parsedTime = rusParser.parse("10 Сентябрь 2015 10:20"); 590 assert(parsedTime == SysTime(DateTime(2015, 9, 10, 10, 20))); 591 592 immutable parsedTime2 = rusParser.parse("10 Сентябрь 2015 10:20"d.byChar); 593 assert(parsedTime2 == SysTime(DateTime(2015, 9, 10, 10, 20))); 594 } 595 // dfmt on 596 597 // Test ranges 598 @safe unittest 599 { 600 import std.utf : byCodeUnit, byChar; 601 602 // forward ranges 603 assert("10h36m28s".byChar.parse == SysTime( 604 DateTime(1, 1, 1, 10, 36, 28))); 605 assert("Thu Sep 10:36:28".byChar.parse == SysTime( 606 DateTime(1, 9, 5, 10, 36, 28))); 607 608 // bidirectional ranges 609 assert("2003-09-25T10:49:41".byCodeUnit.parse == SysTime( 610 DateTime(2003, 9, 25, 10, 49, 41))); 611 assert("Thu Sep 10:36:28".byCodeUnit.parse == SysTime( 612 DateTime(1, 9, 5, 10, 36, 28))); 613 } 614 615 // Test different string types 616 @safe unittest 617 { 618 import std.meta : AliasSeq; 619 import std.conv : to; 620 621 alias StringTypes = AliasSeq!( 622 char[], string, 623 wchar[], wstring, 624 dchar[], dstring 625 ); 626 627 foreach (T; StringTypes) 628 { 629 assert("10h36m28s".to!T.parse == SysTime( 630 DateTime(1, 1, 1, 10, 36, 28))); 631 assert("Thu Sep 10:36:28".to!T.parse == SysTime( 632 DateTime(1, 9, 5, 10, 36, 28))); 633 assert("2003-09-25T10:49:41".to!T.parse == SysTime( 634 DateTime(2003, 9, 25, 10, 49, 41))); 635 assert("Thu Sep 10:36:28".to!T.parse == SysTime( 636 DateTime(1, 9, 5, 10, 36, 28))); 637 } 638 } 639 640 // Issue #1 641 @safe unittest 642 { 643 assert(parse("Sat, 12 Mar 2016 01:30:59 -0900", 644 Yes.ignoreTimezone) == SysTime(DateTime(2016, 3, 12, 01, 30, 59))); 645 } 646 647 /** 648 * Implements the parsing functionality for the parse function. If you are 649 * using a custom `ParserInfo` many times in the same program, you can avoid 650 * unnecessary allocations by using the `Parser.parse` function directly. 651 * 652 * Params: 653 * parserInfo = the parser info to reference when parsing 654 */ 655 final class Parser { 656 private const ParserInfo info; 657 658 public: 659 /// 660 this(const ParserInfo parserInfo = null) @safe 661 { 662 if (parserInfo is null) 663 { 664 info = new ParserInfo(); 665 } 666 else 667 { 668 info = parserInfo; 669 } 670 } 671 672 /** 673 * This function has the same functionality as the free version of `parse`. 674 * The only difference is this will use your custom `ParserInfo` or allocator 675 * if provided. 676 */ 677 SysTime parse(Range)(Range timeString, 678 Flag!"ignoreTimezone" ignoreTimezone = No.ignoreTimezone, 679 const(TimeZone)[string] timezoneInfos = null, 680 Flag!"dayFirst" dayFirst = No.dayFirst, 681 Flag!"yearFirst" yearFirst = No.yearFirst, 682 Flag!"fuzzy" fuzzy = No.fuzzy, 683 SysTime defaultDate = SysTime(Date(1, 1, 1))) @safe 684 if(isForwardRange!Range && !isInfinite!Range && isSomeChar!(ElementEncodingType!Range)) 685 { 686 import std.conv : to, ConvException; 687 688 auto res = parseImpl(timeString, dayFirst, yearFirst, fuzzy); 689 690 if (res.badData) 691 throw new ConvException("Unknown string format"); 692 693 if (res.year.isNull() && res.month.isNull() && res.day.isNull() 694 && res.hour.isNull() && res.minute.isNull() 695 && res.second.isNull() && res.weekday.isNull() 696 && res.shortcutResult.isNull() && res.shortcutTimeResult.isNull()) 697 throw new ConvException("String does not contain a date."); 698 699 if (res.shortcutResult.isNull && res.shortcutTimeResult.isNull) { 700 if (!res.year.isNull) 701 defaultDate.year(res.year); 702 703 if (!res.day.isNull) 704 defaultDate.day(res.day); 705 706 if (!res.month.isNull) { 707 () @trusted { 708 defaultDate.month(to!Month(res.month)); 709 }(); 710 } 711 712 if (!res.hour.isNull) 713 defaultDate.hour(res.hour); 714 715 if (!res.minute.isNull) 716 defaultDate.minute(res.minute); 717 718 if (!res.second.isNull) 719 defaultDate.second(res.second); 720 721 if (!res.microsecond.isNull) 722 defaultDate.fracSecs(usecs(res.microsecond)); 723 724 if (!res.weekday.isNull() && (res.day.isNull || !res.day)) { 725 immutable delta_days = () @trusted { 726 return daysToDayOfWeek(defaultDate.dayOfWeek(), 727 to!DayOfWeek(res.weekday)); 728 }(); 729 defaultDate += dur!"days"(delta_days); 730 } 731 } else if (!res.shortcutTimeResult.isNull) { 732 defaultDate = SysTime(DateTime(Date( 733 defaultDate.year, 734 defaultDate.month, 735 defaultDate.day, 736 ), res.shortcutTimeResult.get())); 737 } 738 739 if (!ignoreTimezone) 740 { 741 if (res.tzname in timezoneInfos) 742 () @trusted { 743 defaultDate = defaultDate.toOtherTZ( 744 cast(immutable) timezoneInfos[res.tzname] 745 ); 746 }(); 747 else if (res.tzname.length > 0 && (res.tzname == LocalTime().stdName 748 || res.tzname == LocalTime().dstName)) 749 defaultDate = SysTime(cast(DateTime) defaultDate); 750 else if (!res.tzoffset.isNull && res.tzoffset == 0) 751 defaultDate = SysTime(cast(DateTime) defaultDate, cast(immutable) UTC()); 752 else if (!res.tzoffset.isNull && res.tzoffset != 0) 753 { 754 defaultDate = SysTime( 755 cast(DateTime) defaultDate, 756 new immutable SimpleTimeZone(dur!"seconds"(res.tzoffset), res.tzname) 757 ); 758 } 759 } 760 else if (ignoreTimezone && !res.shortcutResult.isNull) 761 res.shortcutResult = SysTime(cast(DateTime) res.shortcutResult.get); 762 763 if (!res.shortcutResult.isNull) 764 return res.shortcutResult.get; 765 else 766 return defaultDate; 767 } 768 769 private: 770 /** 771 * Private method which performs the heavy lifting of parsing, called from 772 * `parse`. 773 * 774 * Params: 775 * timeString = the string to parse. 776 * dayFirst = Whether to interpret the first value in an ambiguous 777 * 3-integer date (e.g. 01/05/09) as the day (true) or month (false). If 778 * yearFirst is set to true, this distinguishes between YDM 779 * and YMD. If set to null, this value is retrieved from the 780 * current :class:ParserInfo object (which itself defaults to 781 * false). 782 * yearFirst = Whether to interpret the first value in an ambiguous 3-integer date 783 * (e.g. 01/05/09) as the year. If true, the first number is taken 784 * to be the year, otherwise the last number is taken to be the year. 785 * fuzzy = Whether to allow fuzzy parsing, allowing for string like "Today is 786 * January 1, 2047 at 8:21:00AM". 787 */ 788 ParseResult parseImpl(Range)(Range timeString, bool dayFirst = false, 789 bool yearFirst = false, bool fuzzy = false) if (isForwardRange!Range 790 && !isInfinite!Range && isSomeChar!(ElementEncodingType!Range)) 791 { 792 import std.array : appender, Appender; 793 import std.algorithm.searching : canFind, countUntil; 794 import std.algorithm.iteration : filter; 795 import std.uni : isUpper; 796 import std.ascii : isDigit; 797 import std.utf : byCodeUnit, byChar; 798 import std.conv : to, ConvException; 799 800 ParseResult res; 801 802 //DynamicArray!(string, Allocator, true) tokens; 803 auto tokensAppender = appender!(string[])(); 804 805 static if (is(Unqual!(ElementEncodingType!Range) == dchar) || 806 is(Unqual!(ElementEncodingType!Range) == wchar)) 807 { 808 put(tokensAppender, timeString.save.byChar.timeLexer); 809 } 810 else static if (isSomeString!Range && is(Unqual!(ElementEncodingType!Range) == char)) 811 { 812 put(tokensAppender, timeString.save.byCodeUnit.timeLexer); 813 } 814 else 815 { 816 put(tokensAppender, timeString.save.timeLexer); 817 } 818 819 debug(dateparser2) writeln("tokens: ", tokens[]); 820 821 //keep up with the last token skipped so we can recombine 822 //consecutively skipped tokens (-2 for when i begins at 0). 823 int last_skipped_token_i = -2; 824 825 //year/month/day list 826 YMD ymd; 827 828 //Index of the month string in ymd 829 ptrdiff_t mstridx = -1; 830 831 auto tokens = tokensAppender.data; 832 immutable size_t tokensLength = tokens.length; 833 debug(dateparser2) writeln("tokensLength: ", tokensLength); 834 uint i = 0; 835 while (i < tokensLength) 836 { 837 //Check if it's a number 838 Nullable!(float, float.infinity) value; 839 string value_repr; 840 debug(dateparser2) writeln("index: ", i); 841 debug(dateparser2) writeln("tokens[i]: ", tokens[i]); 842 843 if (tokens[i][0].isDigit) 844 { 845 value_repr = tokens[i]; 846 debug(dateparser2) writeln("value_repr: ", value_repr); 847 value = to!float(value_repr); 848 } 849 850 //Token is a number 851 if (!value.isNull()) 852 { 853 immutable tokensItemLength = tokens[i].length; 854 ++i; 855 856 if (ymd.length == 3 && (tokensItemLength == 2 857 || tokensItemLength == 4) && res.hour.isNull 858 && (i >= tokensLength || (tokens[i] != ":" && info.hms(tokens[i]) == -1))) 859 { 860 debug(dateparser2) writeln("branch 1"); 861 //19990101T23[59] 862 auto s = tokens[i - 1]; 863 res.hour = to!int(s[0 .. 2]); 864 865 if (tokensItemLength == 4) 866 { 867 res.minute = to!int(s[2 .. $]); 868 } 869 } 870 else if (tokensItemLength == 6 || (tokensItemLength > 6 871 && tokens[i - 1].countUntil('.') == 6)) 872 { 873 debug(dateparser2) writeln("branch 2"); 874 //YYMMDD || HHMMSS[.ss] 875 auto s = tokens[i - 1]; 876 877 if (ymd.length == 0 && !tokens[i - 1].canFind('.')) 878 { 879 ymd.put(s[0 .. 2]); 880 ymd.put(s[2 .. 4]); 881 ymd.put(s[4 .. $]); 882 } 883 else 884 { 885 //19990101T235959[.59] 886 res.hour = to!int(s[0 .. 2]); 887 res.minute = to!int(s[2 .. 4]); 888 auto ms = parseMS(s[4 .. $]); 889 res.second = ms[0]; 890 res.microsecond = ms[1]; 891 } 892 } 893 else if (tokensItemLength == 8 || tokensItemLength == 12 || tokensItemLength == 14) 894 { 895 debug(dateparser2) writeln("branch 3"); 896 //YYYYMMDD 897 auto s = tokens[i - 1]; 898 ymd.put(s[0 .. 4]); 899 ymd.put(s[4 .. 6]); 900 ymd.put(s[6 .. 8]); 901 902 if (tokensItemLength > 8) 903 { 904 res.hour = to!int(s[8 .. 10]); 905 res.minute = to!int(s[10 .. 12]); 906 907 if (tokensItemLength > 12) 908 { 909 res.second = to!int(s[12 .. $]); 910 } 911 } 912 } 913 else if ((i < tokensLength && info.hms(tokens[i]) > -1) 914 || (i + 1 < tokensLength && tokens[i] == " " && info.hms(tokens[i + 1]) > -1)) 915 { 916 debug(dateparser2) writeln("branch 4"); 917 //HH[ ]h or MM[ ]m or SS[.ss][ ]s 918 if (tokens[i] == " ") 919 { 920 ++i; 921 } 922 923 auto idx = info.hms(tokens[i]); 924 925 while (true) 926 { 927 if (idx == 0) 928 { 929 res.hour = to!int(value.get()); 930 931 if (value % 1) 932 res.minute = to!int(60 * (value % 1)); 933 } 934 else if (idx == 1) 935 { 936 res.minute = to!int(value.get()); 937 938 if (value % 1) 939 res.second = to!int(60 * (value % 1)); 940 } 941 else if (idx == 2) 942 { 943 auto temp = parseMS(value_repr); 944 res.second = temp[0]; 945 res.microsecond = temp[1]; 946 } 947 948 ++i; 949 950 if (i >= tokensLength || idx == 2) 951 break; 952 953 //12h00 954 try 955 { 956 value_repr = tokens[i]; 957 value = to!float(value_repr); 958 } 959 catch (ConvException) 960 { 961 break; 962 } 963 964 ++i; 965 ++idx; 966 967 if (i < tokensLength) 968 { 969 immutable newidx = info.hms(tokens[i]); 970 971 if (newidx > -1) 972 idx = newidx; 973 } 974 } 975 } 976 else if (i == tokensLength && tokensLength > 3 977 && tokens[i - 2] == " " && info.hms(tokens[i - 3]) > -1) 978 { 979 debug(dateparser2) writeln("branch 5"); 980 //X h MM or X m SS 981 immutable idx = info.hms(tokens[i - 3]) + 1; 982 983 if (idx == 1) 984 { 985 res.minute = to!int(value.get()); 986 987 if (value % 1) 988 res.second = to!int(60 * (value % 1)); 989 else if (idx == 2) 990 { 991 auto seconds = parseMS(value_repr); 992 res.second = seconds[0]; 993 res.microsecond = seconds[1]; 994 ++i; 995 } 996 } 997 } 998 else if (i + 1 < tokensLength && tokens[i] == ":") 999 { 1000 debug(dateparser2) writeln("branch 6"); 1001 //HH:MM[:SS[.ss]] 1002 static if (isSomeString!Range) 1003 { 1004 if (tokensLength == 5 && info.ampm(tokens[4]) == -1) 1005 { 1006 try 1007 { 1008 res.shortcutTimeResult = TimeOfDay.fromISOExtString(timeString); 1009 return res; 1010 } 1011 catch (DateTimeException) {} 1012 } 1013 } 1014 res.hour = to!int(value.get()); 1015 ++i; 1016 value = to!float(tokens[i]); 1017 res.minute = to!int(value.get()); 1018 1019 if (value % 1) 1020 res.second = to!int(60 * (value % 1)); 1021 1022 ++i; 1023 1024 if (i < tokensLength && tokens[i] == ":") 1025 { 1026 auto temp = parseMS(tokens[i + 1]); 1027 res.second = temp[0]; 1028 res.microsecond = temp[1]; 1029 i += 2; 1030 } 1031 } 1032 else if (i < tokensLength && (tokens[i] == "-" || tokens[i] == "/" 1033 || tokens[i] == ".")) 1034 { 1035 debug(dateparser2) writeln("branch 7"); 1036 immutable string separator = tokens[i]; 1037 ymd.put(value_repr); 1038 ++i; 1039 1040 if (i < tokensLength && !info.jump(tokens[i])) 1041 { 1042 if (tokens[i][0].isDigit) 1043 { 1044 //01-01[-01] 1045 static if (isSomeString!Range) 1046 { 1047 if (tokensLength >= 11) 1048 { 1049 try 1050 { 1051 res.shortcutResult = SysTime.fromISOExtString(timeString); 1052 return res; 1053 } 1054 catch (DateTimeException) {} 1055 } 1056 } 1057 1058 ymd.put(tokens[i]); 1059 } 1060 else 1061 { 1062 //01-Jan[-01] 1063 value = info.month(tokens[i]); 1064 1065 if (value > -1) 1066 { 1067 ymd.put(value.get()); 1068 mstridx = cast(ptrdiff_t) (ymd.length == 0 ? 0 : ymd.length - 1); 1069 } 1070 else 1071 { 1072 res.badData = true; 1073 return res; 1074 } 1075 } 1076 1077 ++i; 1078 1079 if (i < tokensLength && tokens[i] == separator) 1080 { 1081 //We have three members 1082 ++i; 1083 value = info.month(tokens[i]); 1084 1085 if (value > -1) 1086 { 1087 ymd.put(value.get()); 1088 mstridx = ymd.length - 1; 1089 } 1090 else 1091 ymd.put(tokens[i]); 1092 1093 ++i; 1094 } 1095 } 1096 } 1097 else if (i >= tokensLength || info.jump(tokens[i])) 1098 { 1099 debug(dateparser2) writeln("branch 8"); 1100 if (i + 1 < tokensLength && info.ampm(tokens[i + 1]) > -1) 1101 { 1102 //12 am 1103 res.hour = to!int(value.get()); 1104 1105 if (res.hour < 12 && info.ampm(tokens[i + 1]) == 1) 1106 res.hour += 12; 1107 else if (res.hour == 12 && info.ampm(tokens[i + 1]) == 0) 1108 res.hour = 0; 1109 1110 ++i; 1111 } 1112 else 1113 { 1114 //Year, month or day 1115 ymd.put(value.get()); 1116 } 1117 ++i; 1118 } 1119 else if (info.ampm(tokens[i]) > -1) 1120 { 1121 debug(dateparser2) writeln("branch 9"); 1122 //12am 1123 res.hour = to!int(value.get()); 1124 1125 if (res.hour < 12 && info.ampm(tokens[i]) == 1) 1126 res.hour += 12; 1127 else if (res.hour == 12 && info.ampm(tokens[i]) == 0) 1128 res.hour = 0; 1129 1130 ++i; 1131 } 1132 else if (!fuzzy) 1133 { 1134 debug(dateparser2) writeln("branch 10"); 1135 res.badData = true; 1136 return res; 1137 } 1138 else 1139 { 1140 debug(dateparser2) writeln("branch 11"); 1141 ++i; 1142 } 1143 continue; 1144 } 1145 1146 //Check weekday 1147 value = info.weekday(tokens[i]); 1148 if (value > -1) 1149 { 1150 debug(dateparser2) writeln("branch 12"); 1151 res.weekday = to!uint(value.get()); 1152 ++i; 1153 continue; 1154 } 1155 1156 //Check month name 1157 value = info.month(tokens[i]); 1158 if (value > -1) 1159 { 1160 debug(dateparser2) writeln("branch 13"); 1161 ymd.put(value.get); 1162 assert(mstridx == -1); 1163 mstridx = ymd.length - 1; 1164 1165 ++i; 1166 if (i < tokensLength) 1167 { 1168 if (tokens[i] == "-" || tokens[i] == "/") 1169 { 1170 //Jan-01[-99] 1171 immutable separator = tokens[i]; 1172 ++i; 1173 ymd.put(tokens[i]); 1174 ++i; 1175 1176 if (i < tokensLength && tokens[i] == separator) 1177 { 1178 //Jan-01-99 1179 ++i; 1180 ymd.put(tokens[i]); 1181 ++i; 1182 } 1183 } 1184 else if (i + 3 < tokensLength && tokens[i] == " " 1185 && tokens[i + 2] == " " && info.pertain(tokens[i + 1])) 1186 { 1187 //Jan of 01 1188 //In this case, 01 is clearly year 1189 try 1190 { 1191 value = to!int(tokens[i + 3]); 1192 //Convert it here to become unambiguous 1193 ymd.put(convertYear(value.get.to!int())); 1194 } 1195 catch (ConvException) {} 1196 i += 4; 1197 } 1198 } 1199 continue; 1200 } 1201 1202 //Check am/pm 1203 value = info.ampm(tokens[i]); 1204 if (value > -1) 1205 { 1206 debug(dateparser2) writeln("branch 14"); 1207 //For fuzzy parsing, 'a' or 'am' (both valid English words) 1208 //may erroneously trigger the AM/PM flag. Deal with that 1209 //here. 1210 bool valIsAMPM = true; 1211 1212 //If there's already an AM/PM flag, this one isn't one. 1213 if (fuzzy && !res.ampm.isNull()) 1214 valIsAMPM = false; 1215 1216 //If AM/PM is found and hour is not, raise a ValueError 1217 if (res.hour.isNull) 1218 { 1219 if (fuzzy) 1220 valIsAMPM = false; 1221 else 1222 throw new ConvException("No hour specified with AM or PM flag."); 1223 } 1224 else if (!(0 <= res.hour && res.hour <= 12)) 1225 { 1226 //If AM/PM is found, it's a 12 hour clock, so raise 1227 //an error for invalid range 1228 if (fuzzy) 1229 valIsAMPM = false; 1230 else 1231 throw new ConvException("Invalid hour specified for 12-hour clock."); 1232 } 1233 1234 if (valIsAMPM) 1235 { 1236 if (value == 1 && res.hour < 12) 1237 res.hour += 12; 1238 else if (value == 0 && res.hour == 12) 1239 res.hour = 0; 1240 1241 res.ampm = to!uint(value.get()); 1242 } 1243 1244 ++i; 1245 continue; 1246 } 1247 1248 //Check for a timezone name 1249 immutable upperItems = tokens[i] 1250 .byCodeUnit 1251 .filter!(a => !isUpper(a)) 1252 .walkLength(1); 1253 if (!res.hour.isNull && tokens[i].length <= 5 1254 && res.tzname.length == 0 && res.tzoffset.isNull && upperItems == 0) 1255 { 1256 debug(dateparser2) writeln("branch 15"); 1257 res.tzname = tokens[i]; 1258 1259 ++i; 1260 1261 //Check for something like GMT+3, or BRST+3. Notice 1262 //that it doesn't mean "I am 3 hours after GMT", but 1263 //"my time +3 is GMT". If found, we reverse the 1264 //logic so that timezone parsing code will get it 1265 //right. 1266 if (i < tokensLength && (tokens[i][0] == '+' || tokens[i][0] == '-')) 1267 { 1268 tokens[i] = tokens[i][0] == '+' ? "-" : "+"; 1269 res.tzoffset = 0; 1270 if (info.utczone(res.tzname)) 1271 { 1272 //With something like GMT+3, the timezone 1273 //is *not* GMT. 1274 res.tzname = []; 1275 } 1276 } 1277 1278 continue; 1279 } 1280 1281 //Check for a numbered timezone 1282 if (!res.hour.isNull && (tokens[i] == "+" || tokens[i] == "-")) 1283 { 1284 debug(dateparser2) writeln("branch 16"); 1285 immutable int signal = tokens[i][0] == '+' ? 1 : -1; 1286 ++i; 1287 immutable size_t tokensItemLength = tokens[i].length; 1288 1289 if (tokensItemLength == 4) 1290 { 1291 //-0300 1292 res.tzoffset = to!int(tokens[i][0 .. 2]) * 3600 + to!int(tokens[i][2 .. $]) * 60; 1293 } 1294 else if (i + 1 < tokensLength && tokens[i + 1] == ":") 1295 { 1296 //-03:00 1297 res.tzoffset = to!int(tokens[i]) * 3600 + to!int(tokens[i + 2]) * 60; 1298 i += 2; 1299 } 1300 else if (tokensItemLength <= 2) 1301 { 1302 //-[0]3 1303 res.tzoffset = to!int(tokens[i]) * 3600; 1304 } 1305 else 1306 { 1307 res.badData = true; 1308 return res; 1309 } 1310 ++i; 1311 1312 res.tzoffset *= signal; 1313 1314 //Look for a timezone name between parenthesis 1315 if (i + 3 < tokensLength) 1316 { 1317 immutable notUpperItems = tokens[i + 2] 1318 .byCodeUnit 1319 .filter!(a => !isUpper(a)) 1320 .walkLength(1); 1321 if (info.jump(tokens[i]) && tokens[i + 1] == "(" 1322 && tokens[i + 3] == ")" && 3 <= tokens[i + 2].length 1323 && tokens[i + 2].length <= 5 && notUpperItems == 0) 1324 { 1325 //-0300 (BRST) 1326 res.tzname = tokens[i + 2]; 1327 i += 4; 1328 } 1329 } 1330 continue; 1331 } 1332 1333 //Check jumps 1334 if (!(info.jump(tokens[i]) || fuzzy)) 1335 { 1336 debug(dateparser2) writeln("branch 17"); 1337 res.badData = true; 1338 return res; 1339 } 1340 1341 last_skipped_token_i = i; 1342 ++i; 1343 } 1344 1345 auto ymdResult = ymd.resolveYMD(tokens[], mstridx, yearFirst, dayFirst); 1346 1347 // year 1348 if (ymdResult[0] > -1) 1349 { 1350 res.year = ymdResult[0]; 1351 res.centurySpecified = ymd.centurySpecified; 1352 } 1353 1354 // month 1355 if (ymdResult[1] > 0) 1356 res.month = ymdResult[1]; 1357 1358 // day 1359 if (ymdResult[2] > 0) 1360 res.day = ymdResult[2]; 1361 1362 info.validate(res); 1363 return res; 1364 } 1365 }