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