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