1 /**
2  * Time parsing functions.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.utils.time.parse;
15 
16 import core.time : minutes, seconds, dur;
17 
18 import std.exception : enforce;
19 import std.conv : to;
20 import std.ascii : isDigit, isWhite;
21 import std.datetime;
22 import std.typecons : Rebindable;
23 import std.string : strip, startsWith;
24 
25 import ae.utils.time.common;
26 
27 private struct ParseContext(Char, bool checked)
28 {
29 	int year=0, month=1, day=1, hour=0, minute=0, second=0, usecs=0;
30 	int hour12 = 0; bool pm;
31 	Rebindable!(immutable(TimeZone)) tz;
32 	int dow = -1;
33 	Char[] t;
34 	bool escaping;
35 
36 	void need(size_t n)()
37 	{
38 		static if (checked)
39 			enforce(t.length >= n, "Not enough characters in date string");
40 	}
41 
42 	auto take(size_t n)()
43 	{
44 		need!n();
45 		auto result = t[0..n];
46 		t = t[n..$];
47 		return result;
48 	}
49 
50 	char takeOne()
51 	{
52 		need!1();
53 		auto result = t[0];
54 		t = t[1..$];
55 		return result;
56 	}
57 
58 	R takeNumber(size_t n, sizediff_t maxP = -1, R = int)()
59 	{
60 		enum max = maxP == -1 ? n : maxP;
61 		need!n();
62 		foreach (i, c; t[0..n])
63 			enforce((i==0 && c=='-') || isDigit(c) || isWhite(c), "Number expected");
64 		static if (n == max)
65 			enum i = n;
66 		else
67 		{
68 			auto i = n;
69 			while (i < max && (checked ? i < t.length : true) && isDigit(t[i]))
70 				i++;
71 		}
72 		auto s = t[0..i];
73 		t = t[i..$];
74 		return s.strip().to!R();
75 	}
76 
77 	int takeWord(in string[] words, string name)
78 	{
79 		foreach (idx, string word; words)
80 		{
81 			static if (checked)
82 				bool b = t.startsWith(word);
83 			else
84 				bool b = t[0..word.length] == word;
85 			if (b)
86 			{
87 				t = t[word.length..$];
88 				return cast(int)idx;
89 			}
90 		}
91 		throw new Exception(name ~ " expected");
92 	}
93 
94 	char peek()
95 	{
96 		need!1();
97 		return *t.ptr;
98 	}
99 }
100 
101 private void parseToken(alias c, alias context)()
102 {
103 	with (context)
104 	{
105 		// TODO: check if the compiler optimizes this check away
106 		// in the compile-time version. If not, "escaping" needs to
107 		// be moved into an alias parameter.
108 		if (escaping)
109 		{
110 			enforce(takeOne() == c, c ~ " expected");
111 			escaping = false;
112 			return;
113 		}
114 
115 		switch (c)
116 		{
117 			// Day
118 			case 'd':
119 				day = takeNumber!(2)();
120 				break;
121 			case 'D':
122 				dow = takeWord(WeekdayShortNames, "Weekday");
123 				break;
124 			case 'j':
125 				day = takeNumber!(1, 2);
126 				break;
127 			case 'l':
128 				dow = takeWord(WeekdayLongNames, "Weekday");
129 				break;
130 			case 'N':
131 				dow = takeNumber!1 % 7;
132 				break;
133 			case 'S': // ordinal suffix
134 				take!2;
135 				break;
136 			case 'w':
137 				dow = takeNumber!1;
138 				break;
139 			//case 'z': TODO
140 
141 			// Week
142 			//case 'W': TODO
143 
144 			// Month
145 			case 'F':
146 				month = takeWord(MonthLongNames, "Month") + 1;
147 				break;
148 			case 'm':
149 				month = takeNumber!2;
150 				break;
151 			case 'M':
152 				month = takeWord(MonthShortNames, "Month") + 1;
153 				break;
154 			case 'n':
155 				month = takeNumber!(1, 2);
156 				break;
157 			case 't':
158 				takeNumber!(1, 2); // TODO: validate DIM?
159 				break;
160 
161 			// Year
162 			case 'L':
163 				takeNumber!1; // TODO: validate leapness?
164 				break;
165 			// case 'o': TODO (ISO 8601 year number)
166 			case 'Y':
167 				year = takeNumber!4;
168 				break;
169 			case 'y':
170 				year = takeNumber!2;
171 				if (year > 50) // TODO: find correct logic for this
172 					year += 1900;
173 				else
174 					year += 2000;
175 				break;
176 
177 			// Time
178 			case 'a':
179 				pm = takeWord(["am", "pm"], "am/pm")==1;
180 				break;
181 			case 'A':
182 				pm = takeWord(["AM", "PM"], "AM/PM")==1;
183 				break;
184 			// case 'B': TODO (Swatch Internet time)
185 			case 'g':
186 				hour12 = takeNumber!(1, 2);
187 				break;
188 			case 'G':
189 				hour = takeNumber!(1, 2);
190 				break;
191 			case 'h':
192 				hour12 = takeNumber!2;
193 				break;
194 			case 'H':
195 				hour = takeNumber!2;
196 				break;
197 			case 'i':
198 				minute = takeNumber!2;
199 				break;
200 			case 's':
201 				second = takeNumber!2;
202 				break;
203 			case 'u':
204 				usecs = takeNumber!6;
205 				break;
206 			case 'E': // not standard
207 				usecs = 1000 * takeNumber!3;
208 				break;
209 
210 			// Timezone
211 			// case 'e': ???
212 			case 'I':
213 				takeNumber!1;
214 				break;
215 			case 'O':
216 			{
217 				if (peek() == 'Z')
218 				{
219 					t = t[1..$];
220 					tz = UTC();
221 				}
222 				else
223 				if (peek() == 'G')
224 				{
225 					enforce(take!3() == "GMT", "GMT expected");
226 					tz = UTC();
227 				}
228 				else
229 				{
230 					auto tzStr = take!5();
231 					enforce(tzStr[0]=='-' || tzStr[0]=='+', "- / + expected");
232 					auto n = (to!int(tzStr[1..3]) * 60 + to!int(tzStr[3..5])) * (tzStr[0]=='-' ? -1 : 1);
233 					tz = new immutable(SimpleTimeZone)(minutes(n));
234 				}
235 				break;
236 			}
237 			case 'P':
238 			{
239 				auto tzStr = take!6();
240 				enforce(tzStr[0]=='-' || tzStr[0]=='+', "- / + expected");
241 				enforce(tzStr[3]==':', ": expected");
242 				auto n = (to!int(tzStr[1..3]) * 60 + to!int(tzStr[4..6])) * (tzStr[0]=='-' ? -1 : 1);
243 				tz = new immutable(SimpleTimeZone)(minutes(n));
244 				break;
245 			}
246 			case 'T':
247 				version(Posix)
248 					tz = PosixTimeZone.getTimeZone(t);
249 				else
250 				version(Windows)
251 					tz = WindowsTimeZone.getTimeZone(t);
252 
253 				t = null;
254 				break;
255 			case 'Z':
256 			{
257 				// TODO: is this correct?
258 				auto n = takeNumber!(1, 6);
259 				tz = new immutable(SimpleTimeZone)(seconds(n));
260 				break;
261 			}
262 
263 			// Full date/time
264 			//case 'c': TODO
265 			//case 'r': TODO
266 			//case 'U': TODO
267 
268 			// Escape next character
269 			case '\\':
270 				escaping = true;
271 				break;
272 
273 			// Other characters (whitespace, delimiters)
274 			default:
275 			{
276 				enforce(t.length && t[0]==c, c~ " expected or unsupported format character");
277 				t = t[1..$];
278 			}
279 		}
280 	}
281 }
282 
283 import ae.utils.meta;
284 
285 private SysTime parseTimeImpl(alias fmt, bool checked, C)(C[] t, immutable TimeZone defaultTZ = null)
286 {
287 	ParseContext!(C, checked) context;
288 	context.t = t;
289 	context.tz = defaultTZ;
290 
291 	foreach (c; CTIterate!fmt)
292 		parseToken!(c, context)();
293 
294 	enforce(context.t.length == 0, "Left-over characters: " ~ context.t);
295 
296 	SysTime result;
297 
298 	with (context)
299 	{
300 		if (hour12)
301 			hour = hour12%12 + (pm ? 12 : 0);
302 
303 		// Compatibility with both <=2.066 and >=2.067
304 		static if (__traits(hasMember, SysTime, "fracSecs"))
305 			auto frac = dur!"usecs"(usecs);
306 		else
307 			auto frac = FracSec.from!"usecs"(usecs);
308 
309 		result = SysTime(
310 			DateTime(year, month, day, hour, minute, second),
311 			frac,
312 			tz);
313 
314 		if (dow >= 0)
315 			enforce(result.dayOfWeek == dow, "Mismatching weekday");
316 	}
317 
318 	return result;
319 }
320 
321 /// Parse the given string into a SysTime, using the format spec fmt.
322 /// This version generates specialized code for the given fmt.
323 SysTime parseTime(string fmt, C)(C[] t, immutable TimeZone tz = null)
324 {
325 	// Omit length checks if we know the input string is long enough
326 	enum maxLength = timeFormatSize(fmt);
327 	if (t.length < maxLength)
328 		return parseTimeImpl!(fmt, true )(t, tz);
329 	else
330 		return parseTimeImpl!(fmt, false)(t, tz);
331 }
332 
333 /// Parse the given string into a SysTime, using the format spec fmt.
334 /// This version parses fmt at runtime.
335 SysTime parseTimeUsing(C)(C[] t, in char[] fmt)
336 {
337 	return parseTimeImpl!(fmt, true)(t);
338 }
339 
340 deprecated SysTime parseTime(C)(const(char)[] fmt, C[] t)
341 {
342 	return t.parseTimeUsing(fmt);
343 }
344 
345 version(unittest) import ae.utils.time.format;
346 
347 unittest
348 {
349 	const s0 = "Tue Jun 07 13:23:19 GMT+0100 2011";
350 	//enum t = s0.parseTime!(TimeFormats.STD_DATE); // https://d.puremagic.com/issues/show_bug.cgi?id=12042
351 	auto t = s0.parseTime!(TimeFormats.STD_DATE);
352 	auto s1 = t.formatTime(TimeFormats.STD_DATE);
353 	assert(s0 == s1, s0 ~ "/" ~ s1);
354 	auto t1 = s0.parseTimeUsing(TimeFormats.STD_DATE);
355 	assert(t == t1);
356 }
357 
358 unittest
359 {
360 	"Tue, 21 Nov 2006 21:19:46 +0000".parseTime!(TimeFormats.RFC2822);
361 	"Tue, 21 Nov 2006 21:19:46 +0000".parseTimeUsing(TimeFormats.RFC2822);
362 }
363