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