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