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