1 /*******************************************************************************
2 
3     Module that provides a method to convert from a date time formatted as a
4     string to a UNIX timestamp value.
5 
6     Copyright:
7         Copyright (c) 2009-2016 dunnhumby Germany GmbH.
8         All rights reserved.
9 
10     License:
11         Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
12         Alternatively, this file may be distributed under the terms of the Tango
13         3-Clause BSD License (see LICENSE_BSD.txt for details).
14 
15 *******************************************************************************/
16 
17 module ocean.text.convert.DateTime;
18 
19 import ocean.core.Array : contains;
20 import ocean.core.Verify;
21 import ocean.meta.types.Qualifiers;
22 import ocean.text.Unicode;
23 import ocean.time.chrono.Gregorian;
24 
25 import core.stdc.stdio : sscanf;
26 import core.sys.posix.sys.stat;
27 import core.sys.posix.time;
28 
29 
30 /*******************************************************************************
31 
32     Type of date conversion that has been applied
33 
34 *******************************************************************************/
35 
36 public enum DateConversion : uint
37 {
38     None,
39     DateTime,               // 2010-08-14 17:29:06
40     DateTimeT,              // 2010-08-14T17:29:06
41     YearMonthDay,           // 20100814
42     YearMonthDayWithHyphen, // 2010-08-14
43     YearMonth,              // 201008
44     YearMonthWithHyphen,    // 2010-08
45     InternetMsgFmtDateTime  // Sat, 14 Aug 2010 17:29:06 UTC
46 }
47 
48 
49 /*******************************************************************************
50 
51     Given a timestamp, if it belongs to a set of supported formats, return the
52     equivalent unix timestamp. Refer to the 'DateConversion' enum for the list
53     of supported formats.
54 
55     To call C functions used for the conversion safely, the input string needs
56     to be null-terminated. Fortunately the format is clearly defined, so we can
57     just copy the input string into a fixed-length buffer.
58 
59     Params:
60         str = string containing the timestamp to be converted
61         time = a time_t that is filled with the result of the conversion
62         conversion_type = if the timestamp was converted from one of the
63             supported formats, then set the type of conversion used
64 
65     Returns:
66         true if the timestamp was successfully converted
67 
68 *******************************************************************************/
69 
70 public bool timeToUnixTime ( cstring str, ref time_t time,
71     out DateConversion conversion_type )
72 {
73     tm datetime;
74 
75     // Create a static buffer of length one more than the longest supported
76     // format
77     char [("Sat, 14 Aug 2010 17:29:06 UTC".length + 1)] buff;
78 
79     auto len = (str.length < buff.length) ? str.length : (buff.length - 1);
80 
81     buff[0 .. len] = str[0 .. len];
82     buff[len] = '\0';
83 
84     if ( str.length && !isDigit(str[0]) )
85     {
86         if ( ! getDateTimeForInternetMsgFormat(buff[0 .. len + 1], datetime) )
87         {
88             return false;
89         }
90 
91         conversion_type = DateConversion.InternetMsgFmtDateTime;
92     }
93     else
94     {
95         if ( ! getDateTimeForISO8601Format(buff[0 .. len + 1], str.length,
96             datetime, conversion_type) )
97         {
98             return false;
99         }
100     }
101 
102     time = timegm(&datetime);
103 
104     return true;
105 }
106 
107 
108 /*******************************************************************************
109 
110     Gets a struct containing details of the date and time corresponding to the
111     given string. This function supports some (but not all) of the formats
112     supported by ISO 8601.
113 
114     This method makes use of the C function 'sscanf()', and to call 'sscanf()'
115     safely, the input string must be null-terminated.
116 
117     Params:
118         buff = null-terminated string containing the timestamp
119         orig_str_len = length of the original string containing the timestamp
120         datetime = output struct to be filled with details of the date and time
121             corresponding to the timestamp
122         conversion_type = if the timestamp was converted from one of the
123             supported formats, then set the type of conversion used
124 
125     Returns:
126         true if the timestamp was successfully converted
127 
128 *******************************************************************************/
129 
130 private bool getDateTimeForISO8601Format ( cstring buff, size_t orig_str_len,
131     ref tm datetime, ref DateConversion conversion_type )
132 {
133     verify(buff[$ - 1] == '\0', "Input string must be null-terminated");
134     char separator;
135 
136     // Initialise the time and the day of the month to 0 and 1 respectively
137     datetime.tm_hour = datetime.tm_min = datetime.tm_sec = 0;
138     datetime.tm_mday = 1;
139 
140     // try the date format with 2010-08-14T17:29:06 or 2010-08-14 17:29:06
141     int num_matched = sscanf(buff.ptr, "%d-%d-%d%c%d:%d:%d".ptr,
142         &datetime.tm_year, &datetime.tm_mon, &datetime.tm_mday, &separator,
143         &datetime.tm_hour, &datetime.tm_min, &datetime.tm_sec);
144 
145     Converted: switch ( num_matched )
146     {
147         case 1:
148             if ( validateCharacters(buff) )
149             {
150                 switch ( orig_str_len )
151                 {
152                     case 6: // 201008
153                         sscanf(buff.ptr, "%04d%02d".ptr, &datetime.tm_year,
154                             &datetime.tm_mon);
155                         conversion_type = DateConversion.YearMonth;
156                         break Converted;
157 
158                     case 8: // 20100814
159                         sscanf(buff.ptr, "%04d%02d%02d".ptr, &datetime.tm_year,
160                             &datetime.tm_mon, &datetime.tm_mday);
161                         conversion_type = DateConversion.YearMonthDay;
162                         break Converted;
163 
164                     default:
165                         return false;
166                 }
167             }
168             return false;
169 
170         case 2:
171             if ( validateCharacters(buff, "-") ) // 2010-08
172             {
173                 conversion_type = DateConversion.YearMonthWithHyphen;
174                 break;
175             }
176             return false;
177 
178         case 3: // 2013-10-01
179             if ( validateCharacters(buff, "-") )
180             {
181                 conversion_type = DateConversion.YearMonthDayWithHyphen;
182                 break;
183             }
184             return false;
185 
186         case 7:
187             switch ( separator )
188             {
189                 case 'T': // 2010-08-14T17:29:06
190                     conversion_type = DateConversion.DateTimeT;
191                     break Converted;
192 
193                 case ' ': // 2010-08-14 17:29:06
194                     conversion_type = DateConversion.DateTime;
195                     break Converted;
196 
197                 default:
198                     return false;
199             }
200             assert(false);
201 
202         default:
203             return false;
204     }
205 
206     if ( !validateDate(datetime.tm_mday, datetime.tm_mon, datetime.tm_year) )
207     {
208         return false;
209     }
210 
211     if ( !validateTime(datetime.tm_hour, datetime.tm_min, datetime.tm_sec) )
212     {
213         return false;
214     }
215 
216     datetime.tm_year -= 1900;
217     datetime.tm_mon--;
218     datetime.tm_isdst = false;
219 
220     return true;
221 }
222 
223 
224 /*******************************************************************************
225 
226     Gets a struct containing details of the date and time corresponding to the
227     given string. This function only supports the Internet message format as
228     defined in RFC 5322.
229 
230     This method makes use of the C function 'strptime()', and to call
231     'strptime()' safely, the input string must be null-terminated.
232 
233     Params:
234         buff = null-terminated string containing the timestamp
235         datetime = output struct to be filled with details of the date and time
236             corresponding to the timestamp
237 
238     Returns:
239         true if the timestamp was successfully converted
240 
241 *******************************************************************************/
242 
243 private bool getDateTimeForInternetMsgFormat ( cstring buff, ref tm datetime )
244 {
245     verify(buff[$ - 1] == '\0', "Input string must be null-terminated");
246     return
247         strptime(buff.ptr, "%A, %d %b %Y %H:%M:%S %Z".ptr, &datetime) !is null;
248 }
249 
250 
251 /*******************************************************************************
252 
253     Validates the characters in the given string (until the first '\0' or the
254     end of string is hit). Valid characters are digits and any characters in the
255     extra parameter.
256 
257     Params:
258         str = string to check for valid characters
259         extra = string containing characters other than digits that are valid
260             (defaults to an empty string)
261 
262     Returns:
263         true if the string only contains valid characters
264 
265 *******************************************************************************/
266 
267 private bool validateCharacters ( cstring str, cstring extra = "" )
268 {
269     foreach ( chr; str )
270     {
271         if ( chr == '\0')
272         {
273             break;
274         }
275 
276         if ( !isDigit(chr) && !extra.contains(chr) )
277         {
278             return false;
279         }
280     }
281     return true;
282 }
283 
284 
285 /*******************************************************************************
286 
287     Check that the date has valid values for days, months, and years.
288 
289     Params:
290         day = the day of the month to check
291         month = the month of the year to check
292         year = the year to check
293 
294     Returns:
295         true if the date is valid
296 
297 *******************************************************************************/
298 
299 public bool validateDate ( uint day, uint month, uint year )
300 {
301     if ( year < 1900 )
302     {
303         return false;
304     }
305     if ( month < 1 || month > 12 )
306     {
307         return false;
308     }
309     if ( day < 1 || day > Gregorian.generic.
310         getDaysInMonth(year, month, Gregorian.AD_ERA) )
311     {
312         return false;
313     }
314     return true;
315 }
316 
317 
318 /*******************************************************************************
319 
320     Check that the time has valid values for hour, minute, and second.
321 
322     Params:
323         hour = the hour of the day to check
324         minute = the minute of the hour to check
325         second = the second to check
326 
327     Returns:
328         true if the time is valid
329 
330 *******************************************************************************/
331 
332 public bool validateTime ( int hour, int minute, int second )
333 {
334     if ( hour < 0 || hour > 23 )
335     {
336         return false;
337     }
338     if ( minute < 0 || minute > 59 )
339     {
340         return false;
341     }
342     if ( second < 0 || second > 59 )
343     {
344         return false;
345     }
346     return true;
347 }
348 
349 
350 /*******************************************************************************
351 
352     unittest for the date conversion
353 
354 *******************************************************************************/
355 
356 version (unittest)
357 {
358     import ocean.core.Test;
359     import ocean.text.convert.Formatter;
360 }
361 
362 unittest
363 {
364     void testConversion ( cstring datetime, time_t expected_time,
365         DateConversion expected_conversion, bool should_pass = true,
366         typeof(__LINE__) line_num = __LINE__ )
367     {
368         time_t timestamp;
369         auto conversion_type = DateConversion.None;
370 
371         auto t = new NamedTest(format("Date conversion test (line {})",
372             line_num));
373 
374         // check the conversion works if it should or fails if it should not
375         auto success = timeToUnixTime(datetime, timestamp, conversion_type);
376         t.test!("==")(should_pass, success);
377 
378         // only check the datetime and type if the initial test passes
379         if ( should_pass )
380         {
381             t.test!("==")(timestamp, expected_time);
382             t.test!("==")(conversion_type, expected_conversion);
383         }
384     }
385 
386     testConversion("2013-09-05 14:44:01", 1378392241, DateConversion.DateTime);
387 
388     testConversion("2013-09-05T14:55:17", 1378392917, DateConversion.DateTimeT);
389 
390     testConversion("20130930", 1380499200, DateConversion.YearMonthDay);
391 
392     testConversion("2013-03-13", 1363132800,
393         DateConversion.YearMonthDayWithHyphen);
394 
395     testConversion("201309", 1377993600, DateConversion.YearMonth);
396 
397     testConversion("2013-03", 1362096000, DateConversion.YearMonthWithHyphen);
398 
399     testConversion("10000101", 0, DateConversion.None, false);
400 
401     testConversion("2013-09-31 14:44:01", 0, DateConversion.None, false);
402 
403     testConversion("2013-11-32", 0, DateConversion.None, false);
404 
405     testConversion("2013-13", 0, DateConversion.None, false);
406 
407     testConversion("2013-12-01-", 0, DateConversion.None, false);
408 
409     testConversion("2013-09-05 24:44:01", 0, DateConversion.DateTime, false);
410 
411     testConversion("2013-09-05T14:61:17", 0, DateConversion.DateTimeT, false);
412 
413     testConversion("2013-09-05 24:44:80", 0, DateConversion.DateTime, false);
414 
415     testConversion("a_really_long_dummy_string", 0, DateConversion.None, false);
416 
417     testConversion("Sun, 09 Sep 2001 01:46:40 UTC", 1000000000,
418         DateConversion.InternetMsgFmtDateTime);
419 }
420 
421 unittest
422 {
423     // test validateDate
424     test!("==")(validateDate(12, 12, 1899), false);
425     test!("==")(validateDate(12, -1, 1899), false);
426     test!("==")(validateDate(12, 13, 2017), false);
427     test!("==")(validateDate(-1, 12, 2017), false);
428     test!("==")(validateDate(32, 12, 2017), false);
429     test!("==")(validateDate(12, 12, 2017), true);
430 
431     // // test validateTime
432     test!("==")(validateTime(-1, 12, 12), false);
433     test!("==")(validateTime(25, 12, 12), false);
434     test!("==")(validateTime(12, -1, 12), false);
435     test!("==")(validateTime(12, 62, 12), false);
436     test!("==")(validateTime(12, 12, -1), false);
437     test!("==")(validateTime(12, 12, 62), false);
438     test!("==")(validateTime(12, 12, 12), true);
439 }