Index: src/OFDate.h ================================================================== --- src/OFDate.h +++ src/OFDate.h @@ -64,11 +64,12 @@ * want local time. * * See the manpage for strftime for information on the format. * * @warning The format is currently limited to the following format specifiers: - * %%d, %%e, %%H, %%m, %%M, %%S, %%y, %%Y, %%, %%n and %%t. + * %%a, %%b, %%d, %%e, %%H, %%m, %%M, %%S, %%y, %%Y, %%z, %%, %%n and + * %%t. * * @param string The string describing the date * @param format The format of the string describing the date * @return A new, autoreleased OFDate with the specified date and time */ @@ -80,11 +81,12 @@ * format. * * See the manpage for strftime for information on the format. * * @warning The format is currently limited to the following format specifiers: - * %%d, %%e, %%H, %%m, %%M, %%S, %%y, %%Y, %%, %%n and %%t. + * %%a, %%b, %%d, %%e, %%H, %%m, %%M, %%S, %%y, %%Y, %%z, %%, %%n and + * %%t. * * @param string The string describing the date * @param format The format of the string describing the date * @return A new, autoreleased OFDate with the specified date and time */ Index: src/OFDate.m ================================================================== --- src/OFDate.m +++ src/OFDate.m @@ -142,10 +142,44 @@ 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31, 31 + 28 + 31 + 30 + 31 + 30 + 31 + 31 + 30 + 31 + 30, }; + +static double +tmAndTzToTime(struct tm *tm, int16_t *tz) +{ + double seconds; + + /* Years */ + seconds = (int64_t)(tm->tm_year - 70) * 31536000; + /* Days of leap years, excluding the year to look at */ + seconds += (((tm->tm_year + 1899) / 4) - 492) * 86400; + seconds -= (((tm->tm_year + 1899) / 100) - 19) * 86400; + seconds += (((tm->tm_year + 1899) / 400) - 4) * 86400; + /* Leap day */ + if (tm->tm_mon >= 2 && (((tm->tm_year + 1900) % 4 == 0 && + (tm->tm_year + 1900) % 100 != 0) || + (tm->tm_year + 1900) % 400 == 0)) + seconds += 86400; + /* Months */ + if (tm->tm_mon < 0 || tm->tm_mon > 12) + @throw [OFInvalidFormatException exception]; + seconds += monthToDayOfYear[tm->tm_mon] * 86400; + /* Days */ + seconds += (tm->tm_mday - 1) * 86400; + /* Hours */ + seconds += tm->tm_hour * 3600; + /* Minutes */ + seconds += tm->tm_min * 60; + /* Seconds */ + seconds += tm->tm_sec; + /* Time zone */ + seconds += -(float)*tz * 60; + + return seconds; +} @implementation OFDate #if (!defined(HAVE_GMTIME_R) || !defined(HAVE_LOCALTIME_R)) && \ defined(OF_HAVE_THREADS) + (void)initialize @@ -235,40 +269,19 @@ { self = [super init]; @try { struct tm tm = { 0 }; + int16_t tz = 0; tm.tm_isdst = -1; if (of_strptime([string UTF8String], [format UTF8String], - &tm) == NULL) - @throw [OFInvalidFormatException exception]; - - /* Years */ - _seconds = (int64_t)(tm.tm_year - 70) * 31536000; - /* Days of leap years, excluding the year to look at */ - _seconds += (((tm.tm_year + 1899) / 4) - 492) * 86400; - _seconds -= (((tm.tm_year + 1899) / 100) - 19) * 86400; - _seconds += (((tm.tm_year + 1899) / 400) - 4) * 86400; - /* Leap day */ - if (tm.tm_mon >= 2 && (((tm.tm_year + 1900) % 4 == 0 && - (tm.tm_year + 1900) % 100 != 0) || - (tm.tm_year + 1900) % 400 == 0)) - _seconds += 86400; - /* Months */ - if (tm.tm_mon < 0 || tm.tm_mon > 12) - @throw [OFInvalidFormatException exception]; - _seconds += monthToDayOfYear[tm.tm_mon] * 86400; - /* Days */ - _seconds += (tm.tm_mday - 1) * 86400; - /* Hours */ - _seconds += tm.tm_hour * 3600; - /* Minutes */ - _seconds += tm.tm_min * 60; - /* Seconds */ - _seconds += tm.tm_sec; + &tm, &tz) == NULL) + @throw [OFInvalidFormatException exception]; + + _seconds = tmAndTzToTime(&tm, &tz); } @catch (id e) { [self release]; @throw e; } @@ -280,24 +293,33 @@ { self = [super init]; @try { struct tm tm = { 0 }; + /* + * of_strptime() can never set this to INT16_MAX, no matter + * what is passed to it, so this is a safe way to figure out if + * the date contains a time zone. + */ + int16_t tz = INT16_MAX; tm.tm_isdst = -1; if (of_strptime([string UTF8String], [format UTF8String], - &tm) == NULL) + &tm, &tz) == NULL) @throw [OFInvalidFormatException exception]; + if (tz == INT16_MAX) { #ifndef OF_WINDOWS - if ((_seconds = mktime(&tm)) == -1) - @throw [OFInvalidFormatException exception]; + if ((_seconds = mktime(&tm)) == -1) + @throw [OFInvalidFormatException exception]; #else - if ((_seconds = _mktime64(&tm)) == -1) - @throw [OFInvalidFormatException exception]; + if ((_seconds = _mktime64(&tm)) == -1) + @throw [OFInvalidFormatException exception]; #endif + } else + _seconds = tmAndTzToTime(&tm, &tz); } @catch (id e) { [self release]; @throw e; } Index: src/of_strptime.h ================================================================== --- src/of_strptime.h +++ src/of_strptime.h @@ -28,11 +28,12 @@ OF_ASSUME_NONNULL_BEGIN #ifdef __cplusplus extern "C" { #endif -extern const char* of_strptime(const char*, const char*, struct tm *tm); +extern const char* of_strptime(const char*, const char*, + struct tm *tm, int16_t *tz); #ifdef __cplusplus } #endif OF_ASSUME_NONNULL_END Index: src/of_strptime.m ================================================================== --- src/of_strptime.m +++ src/of_strptime.m @@ -21,24 +21,24 @@ #include #include "macros.h" const char* -of_strptime(const char *buffer, const char *format, struct tm *tm) +of_strptime(const char *buffer, const char *format, struct tm *tm, int16_t *tz) { enum { SEARCH_CONVERSION_SPECIFIER, IN_CONVERSION_SPECIFIER } state = SEARCH_CONVERSION_SPECIFIER; - size_t j, buffer_len, format_len; + size_t j, bufferLen, formatLen; - buffer_len = strlen(buffer); - format_len = strlen(format); + bufferLen = strlen(buffer); + formatLen = strlen(format); j = 0; - for (size_t i = 0; i < format_len; i++) { - if (j >= buffer_len) + for (size_t i = 0; i < formatLen; i++) { + if (j >= bufferLen) return NULL; switch (state) { case SEARCH_CONVERSION_SPECIFIER: if (format[i] == '%') @@ -47,44 +47,103 @@ return NULL; break; case IN_CONVERSION_SPECIFIER:; - int k, max_len, number = 0; + int k, maxLen, number = 0; switch (format[i]) { case 'd': case 'e': case 'H': case 'm': case 'M': case 'S': case 'y': - max_len = 2; + maxLen = 2; break; case 'Y': - max_len = 4; + maxLen = 4; break; case '%': + case 'a': + case 'b': case 'n': case 't': - max_len = 0; + case 'z': + maxLen = 0; break; default: return NULL; } - if (max_len > 0 && (buffer[j] < '0' || buffer[j] > '9')) + if (maxLen > 0 && (buffer[j] < '0' || buffer[j] > '9')) return NULL; - for (k = 0; k < max_len && j < buffer_len && + for (k = 0; k < maxLen && j < bufferLen && buffer[j] >= '0' && buffer[j] <= '9'; k++, j++) { number *= 10; number += buffer[j] - '0'; } switch (format[i]) { + case 'a': + if (bufferLen < j + 3) + return NULL; + + if (memcmp(buffer + j, "Sun", 3) == 0) + tm->tm_wday = 0; + else if (memcmp(buffer + j, "Mon", 3) == 0) + tm->tm_wday = 1; + else if (memcmp(buffer + j, "Tue", 3) == 0) + tm->tm_wday = 2; + else if (memcmp(buffer + j, "Wed", 3) == 0) + tm->tm_wday = 3; + else if (memcmp(buffer + j, "Thu", 3) == 0) + tm->tm_wday = 4; + else if (memcmp(buffer + j, "Fri", 3) == 0) + tm->tm_wday = 5; + else if (memcmp(buffer + j, "Sat", 3) == 0) + tm->tm_wday = 6; + else + return NULL; + + j += 3; + break; + case 'b': + if (bufferLen < j + 3) + return NULL; + + if (memcmp(buffer + j, "Jan", 3) == 0) + tm->tm_mon = 1; + else if (memcmp(buffer + j, "Feb", 3) == 0) + tm->tm_mon = 2; + else if (memcmp(buffer + j, "Mar", 3) == 0) + tm->tm_mon = 3; + else if (memcmp(buffer + j, "Apr", 3) == 0) + tm->tm_mon = 4; + else if (memcmp(buffer + j, "May", 3) == 0) + tm->tm_mon = 5; + else if (memcmp(buffer + j, "Jun", 3) == 0) + tm->tm_mon = 6; + else if (memcmp(buffer + j, "Jul", 3) == 0) + tm->tm_mon = 7; + else if (memcmp(buffer + j, "Aug", 3) == 0) + tm->tm_mon = 8; + else if (memcmp(buffer + j, "Sep", 3) == 0) + tm->tm_mon = 9; + else if (memcmp(buffer + j, "Oct", 3) == 0) + tm->tm_mon = 10; + else if (memcmp(buffer + j, "Nov", 3) == 0) + tm->tm_mon = 11; + else if (memcmp(buffer + j, "Dec", 3) == 0) + tm->tm_mon = 12; + else + return NULL; + + j += 3; + break; case 'd': case 'e': tm->tm_mday = number; break; case 'H': @@ -108,10 +167,48 @@ case 'Y': if (number < 1900) return NULL; tm->tm_year = number - 1900; + break; + case 'z': + if (buffer[j] == '-' || buffer[j] == '+') { + const char *b = buffer + j; + + if (bufferLen < j + 5) + return NULL; + + if (tz == NULL) + break; + + *tz = (((int16_t)b[1] - '0') * 600 + + ((int16_t)b[2] - '0') * 60 + + ((int16_t)b[3] - '0') * 10 + + ((int16_t)b[4] - '0')) * + (b[0] == '-' ? -1 : 1); + + j += 5; + } else if (buffer[j] == 'Z') { + if (tz != NULL) + *tz = 0; + + j++; + } else if (buffer[j] == 'G') { + if (bufferLen < j + 3) + return NULL; + + if (buffer[j + 1] != 'M' || + buffer[j + 2] != 'T') + return NULL; + + if (tz != NULL) + *tz = 0; + + j += 3; + } else + return NULL; + break; case '%': if (buffer[j++] != '%') return NULL; break; Index: tests/OFDateTests.m ================================================================== --- tests/OFDateTests.m +++ tests/OFDateTests.m @@ -13,14 +13,18 @@ * LICENSE.GPLv2 or LICENSE.GPLv3 respectively included in the packaging of this * file. */ #include "config.h" + +#include #import "OFDate.h" #import "OFString.h" #import "OFAutoreleasePool.h" + +#import "of_strptime.h" #import "TestsAppDelegate.h" static OFString *module = @"OFDate"; @@ -28,10 +32,18 @@ - (void)dateTests { OFAutoreleasePool *pool = [[OFAutoreleasePool alloc] init]; OFDate *d1, *d2; + struct tm tm; + int16_t tz; + const char *dstr = "Wed, 09 Jun 2021 +0200x"; + TEST(@"of_strptime()", + of_strptime(dstr, "%a, %d %b %Y %z", &tm, &tz) == dstr + 22 && + tm.tm_wday == 3 && tm.tm_mday == 9 && tm.tm_mon == 6 && + tm.tm_year == 2021 - 1900 && tz == 2 * 60) + TEST(@"+[dateWithTimeIntervalSince1970:]", (d1 = [OFDate dateWithTimeIntervalSince1970: 0])) TEST(@"-[dateByAddingTimeInterval:]", (d2 = [d1 dateByAddingTimeInterval: 3600 * 25 + 5.000002])) @@ -39,13 +51,22 @@ TEST(@"-[description]", [[d1 description] isEqual: @"1970-01-01T00:00:00Z"] && [[d2 description] isEqual: @"1970-01-02T01:00:05Z"]) TEST(@"+[dateWithDateString:format:]", - [[[OFDate dateWithDateString: @"2000-06-20T12:34:56Z" - format: @"%Y-%m-%dT%H:%M:%SZ"] description] - isEqual: @"2000-06-20T12:34:56Z"]); + [[[OFDate dateWithDateString: @"2000-06-20T12:34:56+0200" + format: @"%Y-%m-%dT%H:%M:%S%z"] description] + isEqual: @"2000-06-20T10:34:56Z"]); + + /* + * We can only test local dates that specify a time zone, as the local + * time zone differs between systems. + */ + TEST(@"+[dateWithLocalDateString:format:]", + [[[OFDate dateWithLocalDateString: @"2000-06-20T12:34:56-0200" + format: @"%Y-%m-%dT%H:%M:%S%z"] + description] isEqual: @"2000-06-20T14:34:56Z"]); TEST(@"-[isEqual:]", [d1 isEqual: [OFDate dateWithTimeIntervalSince1970: 0]] && ![d1 isEqual: [OFDate dateWithTimeIntervalSince1970: 0.0000001]])