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 { 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 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 assert(parsed.timezone == brazilTime); 238 239 assert(parse( 240 "2003 10:36:28 BRST 25 Sep Thu", 241 No.ignoreTimezone, 242 timezones 243 ) == SysTime(DateTime(2003, 9, 25, 10, 36, 28))); 244 assert(parse("Thu Sep 25 10:36:28") == SysTime(DateTime(1, 9, 25, 10, 36, 28))); 245 assert(parse("20030925T104941") == SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 246 assert(parse("2003-09-25T10:49:41") == SysTime(DateTime(2003, 9, 25, 10, 49, 41))); 247 assert(parse("10:36:28") == SysTime(DateTime(1, 1, 1, 10, 36, 28))); 248 assert(parse("09-25-2003") == SysTime(DateTime(2003, 9, 25))); 249 } 250 251 /// Apply information on top of `defaultDate` 252 unittest 253 { 254 assert("10:36:28".parse(No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 255 No.fuzzy, SysTime(DateTime(2016, 3, 15))) 256 == SysTime(DateTime(2016, 3, 15, 10, 36, 28))); 257 assert("August 07".parse(No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 258 No.fuzzy, SysTime(DateTime(2016, 1, 1))) 259 == SysTime(Date(2016, 8, 7))); 260 assert("2000".parse(No.ignoreTimezone, null, No.dayFirst, No.yearFirst, 261 No.fuzzy, SysTime(DateTime(2016, 3, 1))) 262 == SysTime(Date(2000, 3, 1))); 263 } 264 265 /// Custom allocators 266 unittest 267 { 268 import std.experimental.allocator.mallocator : Mallocator; 269 270 auto customParser = new Parser!Mallocator(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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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!GCAllocator(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 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 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 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 * Allocator = the allocator type to use 654 * parserInfo = the parser info to reference when parsing 655 */ 656 final class Parser(Allocator) if ( 657 hasMember!(Allocator, "allocate") && hasMember!(Allocator, "deallocate")) 658 { 659 private const ParserInfo info; 660 661 public: 662 /// 663 this(const ParserInfo parserInfo = null) 664 { 665 if (parserInfo is null) 666 { 667 info = new ParserInfo(); 668 } 669 else 670 { 671 info = parserInfo; 672 } 673 } 674 675 /** 676 * This function has the same functionality as the free version of `parse`. 677 * The only difference is this will use your custom `ParserInfo` or allocator 678 * if provided. 679 */ 680 SysTime parse(Range)(Range timeString, 681 Flag!"ignoreTimezone" ignoreTimezone = No.ignoreTimezone, 682 const(TimeZone)[string] timezoneInfos = null, 683 Flag!"dayFirst" dayFirst = No.dayFirst, 684 Flag!"yearFirst" yearFirst = No.yearFirst, 685 Flag!"fuzzy" fuzzy = No.fuzzy, 686 SysTime defaultDate = SysTime(Date(1, 1, 1))) if ( 687 isForwardRange!Range && !isInfinite!Range && isSomeChar!(ElementEncodingType!Range)) 688 { 689 import std.conv : to, ConvException; 690 691 auto res = parseImpl(timeString, dayFirst, yearFirst, fuzzy); 692 693 if (res.badData) 694 throw new ConvException("Unknown string format"); 695 696 if (res.year.isNull() && res.month.isNull() && res.day.isNull() 697 && res.hour.isNull() && res.minute.isNull() 698 && res.second.isNull() && res.weekday.isNull() 699 && res.shortcutResult.isNull() && res.shortcutTimeResult.isNull()) 700 throw new ConvException("String does not contain a date."); 701 702 if (res.shortcutResult.isNull && res.shortcutTimeResult.isNull) 703 { 704 if (!res.year.isNull) 705 defaultDate.year(res.year); 706 707 if (!res.day.isNull) 708 defaultDate.day(res.day); 709 710 if (!res.month.isNull) 711 defaultDate.month(to!Month(res.month)); 712 713 if (!res.hour.isNull) 714 defaultDate.hour(res.hour); 715 716 if (!res.minute.isNull) 717 defaultDate.minute(res.minute); 718 719 if (!res.second.isNull) 720 defaultDate.second(res.second); 721 722 if (!res.microsecond.isNull) 723 defaultDate.fracSecs(usecs(res.microsecond)); 724 725 if (!res.weekday.isNull() && (res.day.isNull || !res.day)) 726 { 727 immutable delta_days = daysToDayOfWeek( 728 defaultDate.dayOfWeek(), 729 to!DayOfWeek(res.weekday) 730 ); 731 defaultDate += dur!"days"(delta_days); 732 } 733 } 734 else if (!res.shortcutTimeResult.isNull) 735 defaultDate = SysTime(DateTime(Date( 736 defaultDate.year, 737 defaultDate.month, 738 defaultDate.day, 739 ), res.shortcutTimeResult.get())); 740 741 if (!ignoreTimezone) 742 { 743 if (res.tzname in timezoneInfos) 744 defaultDate = defaultDate.toOtherTZ( 745 cast(immutable) timezoneInfos[res.tzname] 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); 762 763 if (!res.shortcutResult.isNull) 764 return res.shortcutResult; 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.algorithm.searching : canFind, countUntil; 793 import std.algorithm.iteration : filter; 794 import std.uni : isUpper; 795 import std.ascii : isDigit; 796 import std.utf : byCodeUnit, byChar; 797 import std.conv : to, ConvException; 798 import containers.dynamicarray : DynamicArray; 799 800 ParseResult res; 801 802 DynamicArray!(string, Allocator, true) tokens; 803 804 static if (is(Unqual!(ElementEncodingType!Range) == dchar) || 805 is(Unqual!(ElementEncodingType!Range) == wchar)) 806 { 807 put(tokens, timeString.save.byChar.timeLexer); 808 } 809 else static if (isSomeString!Range && is(Unqual!(ElementEncodingType!Range) == char)) 810 { 811 put(tokens, timeString.save.byCodeUnit.timeLexer); 812 } 813 else 814 { 815 put(tokens, timeString.save.timeLexer); 816 } 817 818 debug(dateparser) writeln("tokens: ", tokens[]); 819 820 //keep up with the last token skipped so we can recombine 821 //consecutively skipped tokens (-2 for when i begins at 0). 822 int last_skipped_token_i = -2; 823 824 //year/month/day list 825 YMD ymd; 826 827 //Index of the month string in ymd 828 ptrdiff_t mstridx = -1; 829 830 immutable size_t tokensLength = tokens.length; 831 debug(dateparser) writeln("tokensLength: ", tokensLength); 832 uint i = 0; 833 while (i < tokensLength) 834 { 835 //Check if it's a number 836 Nullable!(float, float.infinity) value; 837 string value_repr; 838 debug(dateparser) writeln("index: ", i); 839 debug(dateparser) writeln("tokens[i]: ", tokens[i]); 840 841 if (tokens[i][0].isDigit) 842 { 843 value_repr = tokens[i]; 844 debug(dateparser) writeln("value_repr: ", value_repr); 845 value = to!float(value_repr); 846 } 847 848 //Token is a number 849 if (!value.isNull()) 850 { 851 immutable tokensItemLength = tokens[i].length; 852 ++i; 853 854 if (ymd.length == 3 && (tokensItemLength == 2 855 || tokensItemLength == 4) && res.hour.isNull 856 && (i >= tokensLength || (tokens[i] != ":" && info.hms(tokens[i]) == -1))) 857 { 858 debug(dateparser) writeln("branch 1"); 859 //19990101T23[59] 860 auto s = tokens[i - 1]; 861 res.hour = to!int(s[0 .. 2]); 862 863 if (tokensItemLength == 4) 864 { 865 res.minute = to!int(s[2 .. $]); 866 } 867 } 868 else if (tokensItemLength == 6 || (tokensItemLength > 6 869 && tokens[i - 1].countUntil('.') == 6)) 870 { 871 debug(dateparser) writeln("branch 2"); 872 //YYMMDD || HHMMSS[.ss] 873 auto s = tokens[i - 1]; 874 875 if (ymd.length == 0 && !tokens[i - 1].canFind('.')) 876 { 877 ymd.put(s[0 .. 2]); 878 ymd.put(s[2 .. 4]); 879 ymd.put(s[4 .. $]); 880 } 881 else 882 { 883 //19990101T235959[.59] 884 res.hour = to!int(s[0 .. 2]); 885 res.minute = to!int(s[2 .. 4]); 886 auto ms = parseMS(s[4 .. $]); 887 res.second = ms[0]; 888 res.microsecond = ms[1]; 889 } 890 } 891 else if (tokensItemLength == 8 || tokensItemLength == 12 || tokensItemLength == 14) 892 { 893 debug(dateparser) writeln("branch 3"); 894 //YYYYMMDD 895 auto s = tokens[i - 1]; 896 ymd.put(s[0 .. 4]); 897 ymd.put(s[4 .. 6]); 898 ymd.put(s[6 .. 8]); 899 900 if (tokensItemLength > 8) 901 { 902 res.hour = to!int(s[8 .. 10]); 903 res.minute = to!int(s[10 .. 12]); 904 905 if (tokensItemLength > 12) 906 { 907 res.second = to!int(s[12 .. $]); 908 } 909 } 910 } 911 else if ((i < tokensLength && info.hms(tokens[i]) > -1) 912 || (i + 1 < tokensLength && tokens[i] == " " && info.hms(tokens[i + 1]) > -1)) 913 { 914 debug(dateparser) writeln("branch 4"); 915 //HH[ ]h or MM[ ]m or SS[.ss][ ]s 916 if (tokens[i] == " ") 917 { 918 ++i; 919 } 920 921 auto idx = info.hms(tokens[i]); 922 923 while (true) 924 { 925 if (idx == 0) 926 { 927 res.hour = to!int(value.get()); 928 929 if (value % 1) 930 res.minute = to!int(60 * (value % 1)); 931 } 932 else if (idx == 1) 933 { 934 res.minute = to!int(value.get()); 935 936 if (value % 1) 937 res.second = to!int(60 * (value % 1)); 938 } 939 else if (idx == 2) 940 { 941 auto temp = parseMS(value_repr); 942 res.second = temp[0]; 943 res.microsecond = temp[1]; 944 } 945 946 ++i; 947 948 if (i >= tokensLength || idx == 2) 949 break; 950 951 //12h00 952 try 953 { 954 value_repr = tokens[i]; 955 value = to!float(value_repr); 956 } 957 catch (ConvException) 958 { 959 break; 960 } 961 962 ++i; 963 ++idx; 964 965 if (i < tokensLength) 966 { 967 immutable newidx = info.hms(tokens[i]); 968 969 if (newidx > -1) 970 idx = newidx; 971 } 972 } 973 } 974 else if (i == tokensLength && tokensLength > 3 975 && tokens[i - 2] == " " && info.hms(tokens[i - 3]) > -1) 976 { 977 debug(dateparser) writeln("branch 5"); 978 //X h MM or X m SS 979 immutable idx = info.hms(tokens[i - 3]) + 1; 980 981 if (idx == 1) 982 { 983 res.minute = to!int(value.get()); 984 985 if (value % 1) 986 res.second = to!int(60 * (value % 1)); 987 else if (idx == 2) 988 { 989 auto seconds = parseMS(value_repr); 990 res.second = seconds[0]; 991 res.microsecond = seconds[1]; 992 ++i; 993 } 994 } 995 } 996 else if (i + 1 < tokensLength && tokens[i] == ":") 997 { 998 debug(dateparser) writeln("branch 6"); 999 //HH:MM[:SS[.ss]] 1000 static if (isSomeString!Range) 1001 { 1002 if (tokensLength == 5 && info.ampm(tokens[4]) == -1) 1003 { 1004 try 1005 { 1006 res.shortcutTimeResult = TimeOfDay.fromISOExtString(timeString); 1007 return res; 1008 } 1009 catch (DateTimeException) {} 1010 } 1011 } 1012 res.hour = to!int(value.get()); 1013 ++i; 1014 value = to!float(tokens[i]); 1015 res.minute = to!int(value.get()); 1016 1017 if (value % 1) 1018 res.second = to!int(60 * (value % 1)); 1019 1020 ++i; 1021 1022 if (i < tokensLength && tokens[i] == ":") 1023 { 1024 auto temp = parseMS(tokens[i + 1]); 1025 res.second = temp[0]; 1026 res.microsecond = temp[1]; 1027 i += 2; 1028 } 1029 } 1030 else if (i < tokensLength && (tokens[i] == "-" || tokens[i] == "/" 1031 || tokens[i] == ".")) 1032 { 1033 debug(dateparser) writeln("branch 7"); 1034 immutable string separator = tokens[i]; 1035 ymd.put(value_repr); 1036 ++i; 1037 1038 if (i < tokensLength && !info.jump(tokens[i])) 1039 { 1040 if (tokens[i][0].isDigit) 1041 { 1042 //01-01[-01] 1043 static if (isSomeString!Range) 1044 { 1045 if (tokensLength >= 11) 1046 { 1047 try 1048 { 1049 res.shortcutResult = SysTime.fromISOExtString(timeString); 1050 return res; 1051 } 1052 catch (DateTimeException) {} 1053 } 1054 } 1055 1056 ymd.put(tokens[i]); 1057 } 1058 else 1059 { 1060 //01-Jan[-01] 1061 value = info.month(tokens[i]); 1062 1063 if (value > -1) 1064 { 1065 ymd.put(value.get()); 1066 mstridx = cast(ptrdiff_t) (ymd.length == 0 ? 0 : ymd.length - 1); 1067 } 1068 else 1069 { 1070 res.badData = true; 1071 return res; 1072 } 1073 } 1074 1075 ++i; 1076 1077 if (i < tokensLength && tokens[i] == separator) 1078 { 1079 //We have three members 1080 ++i; 1081 value = info.month(tokens[i]); 1082 1083 if (value > -1) 1084 { 1085 ymd.put(value.get()); 1086 mstridx = ymd.length - 1; 1087 } 1088 else 1089 ymd.put(tokens[i]); 1090 1091 ++i; 1092 } 1093 } 1094 } 1095 else if (i >= tokensLength || info.jump(tokens[i])) 1096 { 1097 debug(dateparser) writeln("branch 8"); 1098 if (i + 1 < tokensLength && info.ampm(tokens[i + 1]) > -1) 1099 { 1100 //12 am 1101 res.hour = to!int(value.get()); 1102 1103 if (res.hour < 12 && info.ampm(tokens[i + 1]) == 1) 1104 res.hour += 12; 1105 else if (res.hour == 12 && info.ampm(tokens[i + 1]) == 0) 1106 res.hour = 0; 1107 1108 ++i; 1109 } 1110 else 1111 { 1112 //Year, month or day 1113 ymd.put(value.get()); 1114 } 1115 ++i; 1116 } 1117 else if (info.ampm(tokens[i]) > -1) 1118 { 1119 debug(dateparser) writeln("branch 9"); 1120 //12am 1121 res.hour = to!int(value.get()); 1122 1123 if (res.hour < 12 && info.ampm(tokens[i]) == 1) 1124 res.hour += 12; 1125 else if (res.hour == 12 && info.ampm(tokens[i]) == 0) 1126 res.hour = 0; 1127 1128 ++i; 1129 } 1130 else if (!fuzzy) 1131 { 1132 debug(dateparser) writeln("branch 10"); 1133 res.badData = true; 1134 return res; 1135 } 1136 else 1137 { 1138 debug(dateparser) writeln("branch 11"); 1139 ++i; 1140 } 1141 continue; 1142 } 1143 1144 //Check weekday 1145 value = info.weekday(tokens[i]); 1146 if (value > -1) 1147 { 1148 debug(dateparser) writeln("branch 12"); 1149 res.weekday = to!uint(value.get()); 1150 ++i; 1151 continue; 1152 } 1153 1154 //Check month name 1155 value = info.month(tokens[i]); 1156 if (value > -1) 1157 { 1158 debug(dateparser) writeln("branch 13"); 1159 ymd.put(value.get); 1160 assert(mstridx == -1); 1161 mstridx = ymd.length - 1; 1162 1163 ++i; 1164 if (i < tokensLength) 1165 { 1166 if (tokens[i] == "-" || tokens[i] == "/") 1167 { 1168 //Jan-01[-99] 1169 immutable separator = tokens[i]; 1170 ++i; 1171 ymd.put(tokens[i]); 1172 ++i; 1173 1174 if (i < tokensLength && tokens[i] == separator) 1175 { 1176 //Jan-01-99 1177 ++i; 1178 ymd.put(tokens[i]); 1179 ++i; 1180 } 1181 } 1182 else if (i + 3 < tokensLength && tokens[i] == " " 1183 && tokens[i + 2] == " " && info.pertain(tokens[i + 1])) 1184 { 1185 //Jan of 01 1186 //In this case, 01 is clearly year 1187 try 1188 { 1189 value = to!int(tokens[i + 3]); 1190 //Convert it here to become unambiguous 1191 ymd.put(convertYear(value.get.to!int())); 1192 } 1193 catch (ConvException) {} 1194 i += 4; 1195 } 1196 } 1197 continue; 1198 } 1199 1200 //Check am/pm 1201 value = info.ampm(tokens[i]); 1202 if (value > -1) 1203 { 1204 debug(dateparser) writeln("branch 14"); 1205 //For fuzzy parsing, 'a' or 'am' (both valid English words) 1206 //may erroneously trigger the AM/PM flag. Deal with that 1207 //here. 1208 bool valIsAMPM = true; 1209 1210 //If there's already an AM/PM flag, this one isn't one. 1211 if (fuzzy && !res.ampm.isNull()) 1212 valIsAMPM = false; 1213 1214 //If AM/PM is found and hour is not, raise a ValueError 1215 if (res.hour.isNull) 1216 { 1217 if (fuzzy) 1218 valIsAMPM = false; 1219 else 1220 throw new ConvException("No hour specified with AM or PM flag."); 1221 } 1222 else if (!(0 <= res.hour && res.hour <= 12)) 1223 { 1224 //If AM/PM is found, it's a 12 hour clock, so raise 1225 //an error for invalid range 1226 if (fuzzy) 1227 valIsAMPM = false; 1228 else 1229 throw new ConvException("Invalid hour specified for 12-hour clock."); 1230 } 1231 1232 if (valIsAMPM) 1233 { 1234 if (value == 1 && res.hour < 12) 1235 res.hour += 12; 1236 else if (value == 0 && res.hour == 12) 1237 res.hour = 0; 1238 1239 res.ampm = to!uint(value.get()); 1240 } 1241 1242 ++i; 1243 continue; 1244 } 1245 1246 //Check for a timezone name 1247 immutable upperItems = tokens[i] 1248 .byCodeUnit 1249 .filter!(a => !isUpper(a)) 1250 .walkLength(1); 1251 if (!res.hour.isNull && tokens[i].length <= 5 1252 && res.tzname.length == 0 && res.tzoffset.isNull && upperItems == 0) 1253 { 1254 debug(dateparser) writeln("branch 15"); 1255 res.tzname = tokens[i]; 1256 1257 ++i; 1258 1259 //Check for something like GMT+3, or BRST+3. Notice 1260 //that it doesn't mean "I am 3 hours after GMT", but 1261 //"my time +3 is GMT". If found, we reverse the 1262 //logic so that timezone parsing code will get it 1263 //right. 1264 if (i < tokensLength && (tokens[i][0] == '+' || tokens[i][0] == '-')) 1265 { 1266 tokens[i] = tokens[i][0] == '+' ? "-" : "+"; 1267 res.tzoffset = 0; 1268 if (info.utczone(res.tzname)) 1269 { 1270 //With something like GMT+3, the timezone 1271 //is *not* GMT. 1272 res.tzname = []; 1273 } 1274 } 1275 1276 continue; 1277 } 1278 1279 //Check for a numbered timezone 1280 if (!res.hour.isNull && (tokens[i] == "+" || tokens[i] == "-")) 1281 { 1282 debug(dateparser) writeln("branch 16"); 1283 immutable int signal = tokens[i][0] == '+' ? 1 : -1; 1284 ++i; 1285 immutable size_t tokensItemLength = tokens[i].length; 1286 1287 if (tokensItemLength == 4) 1288 { 1289 //-0300 1290 res.tzoffset = to!int(tokens[i][0 .. 2]) * 3600 + to!int(tokens[i][2 .. $]) * 60; 1291 } 1292 else if (i + 1 < tokensLength && tokens[i + 1] == ":") 1293 { 1294 //-03:00 1295 res.tzoffset = to!int(tokens[i]) * 3600 + to!int(tokens[i + 2]) * 60; 1296 i += 2; 1297 } 1298 else if (tokensItemLength <= 2) 1299 { 1300 //-[0]3 1301 res.tzoffset = to!int(tokens[i]) * 3600; 1302 } 1303 else 1304 { 1305 res.badData = true; 1306 return res; 1307 } 1308 ++i; 1309 1310 res.tzoffset *= signal; 1311 1312 //Look for a timezone name between parenthesis 1313 if (i + 3 < tokensLength) 1314 { 1315 immutable notUpperItems = tokens[i + 2] 1316 .byCodeUnit 1317 .filter!(a => !isUpper(a)) 1318 .walkLength(1); 1319 if (info.jump(tokens[i]) && tokens[i + 1] == "(" 1320 && tokens[i + 3] == ")" && 3 <= tokens[i + 2].length 1321 && tokens[i + 2].length <= 5 && notUpperItems == 0) 1322 { 1323 //-0300 (BRST) 1324 res.tzname = tokens[i + 2]; 1325 i += 4; 1326 } 1327 } 1328 continue; 1329 } 1330 1331 //Check jumps 1332 if (!(info.jump(tokens[i]) || fuzzy)) 1333 { 1334 debug(dateparser) writeln("branch 17"); 1335 res.badData = true; 1336 return res; 1337 } 1338 1339 last_skipped_token_i = i; 1340 ++i; 1341 } 1342 1343 auto ymdResult = ymd.resolveYMD(tokens[], mstridx, yearFirst, dayFirst); 1344 1345 // year 1346 if (ymdResult[0] > -1) 1347 { 1348 res.year = ymdResult[0]; 1349 res.centurySpecified = ymd.centurySpecified; 1350 } 1351 1352 // month 1353 if (ymdResult[1] > 0) 1354 res.month = ymdResult[1]; 1355 1356 // day 1357 if (ymdResult[2] > 0) 1358 res.day = ymdResult[2]; 1359 1360 info.validate(res); 1361 return res; 1362 } 1363 }