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 }