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 }