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