NetNewsWire/Frameworks/RSParser/Utilities/RSDateParser.m
2017-10-04 13:28:48 -07:00

436 lines
14 KiB
Objective-C
Executable File

//
// RSDateParser.m
// RSParser
//
// Created by Brent Simmons on 3/25/15.
// Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
//
#import <time.h>
#import <RSParser/RSDateParser.h>
typedef struct {
const char *abbreviation;
const NSInteger offsetHours;
const NSInteger offsetMinutes;
} RSTimeZoneAbbreviationAndOffset;
#define kNumberOfTimeZones 96
static const RSTimeZoneAbbreviationAndOffset timeZoneTable[kNumberOfTimeZones] = {
{"GMT", 0, 0}, //Most common at top, for performance
{"PDT", -7, 0}, {"PST", -8, 0}, {"EST", -5, 0}, {"EDT", -4, 0},
{"MDT", -6, 0}, {"MST", -7, 0}, {"CST", -6, 0}, {"CDT", -5, 0},
{"ACT", -8, 0}, {"AFT", 4, 30}, {"AMT", 4, 0}, {"ART", -3, 0},
{"AST", 3, 0}, {"AZT", 4, 0}, {"BIT", -12, 0}, {"BDT", 8, 0},
{"ACST", 9, 30}, {"AEST", 10, 0}, {"AKST", -9, 0}, {"AMST", 5, 0},
{"AWST", 8, 0}, {"AZOST", -1, 0}, {"BIOT", 6, 0}, {"BRT", -3, 0},
{"BST", 6, 0}, {"BTT", 6, 0}, {"CAT", 2, 0}, {"CCT", 6, 30},
{"CET", 1, 0}, {"CEST", 2, 0}, {"CHAST", 12, 45}, {"ChST", 10, 0},
{"CIST", -8, 0}, {"CKT", -10, 0}, {"CLT", -4, 0}, {"CLST", -3, 0},
{"COT", -5, 0}, {"COST", -4, 0}, {"CVT", -1, 0}, {"CXT", 7, 0},
{"EAST", -6, 0}, {"EAT", 3, 0}, {"ECT", -4, 0}, {"EEST", 3, 0},
{"EET", 2, 0}, {"FJT", 12, 0}, {"FKST", -4, 0}, {"GALT", -6, 0},
{"GET", 4, 0}, {"GFT", -3, 0}, {"GILT", 7, 0}, {"GIT", -9, 0},
{"GST", -2, 0}, {"GYT", -4, 0}, {"HAST", -10, 0}, {"HKT", 8, 0},
{"HMT", 5, 0}, {"IRKT", 8, 0}, {"IRST", 3, 30}, {"IST", 2, 0},
{"JST", 9, 0}, {"KRAT", 7, 0}, {"KST", 9, 0}, {"LHST", 10, 30},
{"LINT", 14, 0}, {"MAGT", 11, 0}, {"MIT", -9, 30}, {"MSK", 3, 0},
{"MUT", 4, 0}, {"NDT", -2, 30}, {"NFT", 11, 30}, {"NPT", 5, 45},
{"NT", -3, 30}, {"OMST", 6, 0}, {"PETT", 12, 0}, {"PHOT", 13, 0},
{"PKT", 5, 0}, {"RET", 4, 0}, {"SAMT", 4, 0}, {"SAST", 2, 0},
{"SBT", 11, 0}, {"SCT", 4, 0}, {"SLT", 5, 30}, {"SST", 8, 0},
{"TAHT", -10, 0}, {"THA", 7, 0}, {"UYT", -3, 0}, {"UYST", -2, 0},
{"VET", -4, 30}, {"VLAT", 10, 0}, {"WAT", 1, 0}, {"WET", 0, 0},
{"WEST", 1, 0}, {"YAKT", 9, 0}, {"YEKT", 5, 0}
}; /*See http://en.wikipedia.org/wiki/List_of_time_zone_abbreviations for list*/
#pragma mark - Parser
enum {
RSJanuary = 1,
RSFebruary,
RSMarch,
RSApril,
RSMay,
RSJune,
RSJuly,
RSAugust,
RSSeptember,
RSOctober,
RSNovember,
RSDecember
};
static NSInteger nextMonthValue(const char *bytes, NSUInteger numberOfBytes, NSUInteger startingIndex, NSUInteger *finalIndex) {
/*Months are 1-based -- January is 1, Dec is 12.
Lots of short-circuits here. Not strict. GIGO.*/
NSUInteger i;// = startingIndex;
NSUInteger numberOfAlphaCharactersFound = 0;
char monthCharacters[3] = {0, 0, 0};
for (i = startingIndex; i < numberOfBytes; i++) {
*finalIndex = i;
char character = bytes[i];
BOOL isAlphaCharacter = (BOOL)isalpha(character);
if (!isAlphaCharacter && numberOfAlphaCharactersFound < 1)
continue;
if (!isAlphaCharacter && numberOfAlphaCharactersFound > 0)
break;
numberOfAlphaCharactersFound++;
if (numberOfAlphaCharactersFound == 1) {
if (character == 'F' || character == 'f')
return RSFebruary;
if (character == 'S' || character == 's')
return RSSeptember;
if (character == 'O' || character == 'o')
return RSOctober;
if (character == 'N' || character == 'n')
return RSNovember;
if (character == 'D' || character == 'd')
return RSDecember;
}
monthCharacters[numberOfAlphaCharactersFound - 1] = character;
if (numberOfAlphaCharactersFound >=3)
break;
}
if (numberOfAlphaCharactersFound < 2)
return NSNotFound;
if (monthCharacters[0] == 'J' || monthCharacters[0] == 'j') { //Jan, Jun, Jul
if (monthCharacters[1] == 'a' || monthCharacters[i] == 'A')
return RSJanuary;
if (monthCharacters[1] == 'u' || monthCharacters[1] == 'U') {
if (monthCharacters[2] == 'n' || monthCharacters[2] == 'N')
return RSJune;
return RSJuly;
}
return RSJanuary;
}
if (monthCharacters[0] == 'M' || monthCharacters[0] == 'm') { //March, May
if (monthCharacters[2] == 'y' || monthCharacters[2] == 'Y')
return RSMay;
return RSMarch;
}
if (monthCharacters[0] == 'A' || monthCharacters[0] == 'a') { //April, August
if (monthCharacters[1] == 'u' || monthCharacters[1] == 'U')
return RSAugust;
return RSApril;
}
return RSJanuary; //should never get here
}
static NSInteger nextNumericValue(const char *bytes, NSUInteger numberOfBytes, NSUInteger startingIndex, NSUInteger maximumNumberOfDigits, NSUInteger *finalIndex) {
/*maximumNumberOfDigits has a maximum limit of 4 (for time zone offsets and years).
*finalIndex will be the index of the last character looked at.*/
if (maximumNumberOfDigits > 4)
maximumNumberOfDigits = 4;
NSUInteger i = 0;
NSUInteger numberOfDigitsFound = 0;
NSInteger digits[4] = {0, 0, 0, 0};
for (i = startingIndex; i < numberOfBytes; i++) {
*finalIndex = i;
BOOL isDigit = (BOOL)isdigit(bytes[i]);
if (!isDigit && numberOfDigitsFound < 1)
continue;
if (!isDigit && numberOfDigitsFound > 0)
break;
digits[numberOfDigitsFound] = bytes[i] - 48; // '0' is 48
numberOfDigitsFound++;
if (numberOfDigitsFound >= maximumNumberOfDigits)
break;
}
if (numberOfDigitsFound < 1)
return NSNotFound;
if (numberOfDigitsFound == 1)
return digits[0];
if (numberOfDigitsFound == 2)
return (digits[0] * 10) + digits[1];
if (numberOfDigitsFound == 3)
return (digits[0] * 100) + (digits[1] * 10) + digits[2];
return (digits[0] * 1000) + (digits[1] * 100) + (digits[2] * 10) + digits[3];
}
static BOOL hasAtLeastOneAlphaCharacter(const char *s) {
NSUInteger length = strlen(s);
NSUInteger i = 0;
for (i = 0; i < length; i++) {
if (isalpha(s[i]))
return YES;
}
return NO;
}
#pragma mark - Time Zones and offsets
static NSInteger offsetInSecondsForTimeZoneAbbreviation(const char *abbreviation) {
/*Linear search should be fine. It's a C array, and short (under 100 items).
Most common time zones are at the beginning of the array. (We can tweak this as needed.)*/
NSUInteger i;
for (i = 0; i < kNumberOfTimeZones; i++) {
RSTimeZoneAbbreviationAndOffset zone = timeZoneTable[i];
if (strcmp(abbreviation, zone.abbreviation) == 0) {
if (zone.offsetHours < 0)
return (zone.offsetHours * 60 * 60) - (zone.offsetMinutes * 60);
return (zone.offsetHours * 60 * 60) + (zone.offsetMinutes * 60);
}
}
return 0;
}
static NSInteger offsetInSecondsForOffsetCharacters(const char *timeZoneCharacters) {
BOOL isPlus = timeZoneCharacters[0] == '+';
NSUInteger finalIndex = 0;
NSInteger hours = nextNumericValue(timeZoneCharacters, strlen(timeZoneCharacters), 0, 2, &finalIndex);
NSInteger minutes = nextNumericValue(timeZoneCharacters, strlen(timeZoneCharacters), finalIndex + 1, 2, &finalIndex);
if (hours == NSNotFound)
hours = 0;
if (minutes == NSNotFound)
minutes = 0;
if (hours == 0 && minutes == 0)
return 0;
NSInteger seconds = (hours * 60 * 60) + (minutes * 60);
if (!isPlus)
seconds = 0 - seconds;
return seconds;
}
static const char *rs_GMT = "GMT";
static const char *rs_UTC = "UTC";
static NSInteger parsedTimeZoneOffset(const char *bytes, NSUInteger numberOfBytes, NSUInteger startingIndex) {
/*Examples: GMT Z +0000 -0000 +07:00 -0700 PDT EST
Parse into char[5] -- drop any colon characters. If numeric, calculate seconds from GMT.
If alpha, special-case GMT and Z, otherwise look up in time zone list to get offset.*/
char timeZoneCharacters[6] = {0, 0, 0, 0, 0, 0}; //nil-terminated last character
NSUInteger i = 0;
NSUInteger numberOfCharactersFound = 0;
for (i = startingIndex; i < numberOfBytes; i++) {
char ch = bytes[i];
if (ch == ':' || ch == ' ')
continue;
if (isdigit(ch) || isalpha(ch) || ch == '+' || ch == '-') {
numberOfCharactersFound++;
timeZoneCharacters[numberOfCharactersFound - 1] = ch;
}
if (numberOfCharactersFound >= 5)
break;
}
if (numberOfCharactersFound < 1 || timeZoneCharacters[0] == 'Z' || timeZoneCharacters[0] == 'z')
return 0;
if (strcasestr(timeZoneCharacters, rs_GMT) != nil || strcasestr(timeZoneCharacters, rs_UTC))
return 0;
if (hasAtLeastOneAlphaCharacter(timeZoneCharacters))
return offsetInSecondsForTimeZoneAbbreviation(timeZoneCharacters);
return offsetInSecondsForOffsetCharacters(timeZoneCharacters);
}
#pragma mark - Date Creation
static NSDate *dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset(NSInteger year, NSInteger month, NSInteger day, NSInteger hour, NSInteger minute, NSInteger second, NSInteger milliseconds, NSInteger timeZoneOffset) {
struct tm timeInfo;
timeInfo.tm_sec = (int)second;
timeInfo.tm_min = (int)minute;
timeInfo.tm_hour = (int)hour;
timeInfo.tm_mday = (int)day;
timeInfo.tm_mon = (int)(month - 1); //It's 1-based coming in
timeInfo.tm_year = (int)(year - 1900); //see time.h -- it's years since 1900
timeInfo.tm_wday = -1;
timeInfo.tm_yday = -1;
timeInfo.tm_isdst = -1;
timeInfo.tm_gmtoff = 0;//[timeZone secondsFromGMT];
timeInfo.tm_zone = nil;
NSTimeInterval rawTime = (NSTimeInterval)(timegm(&timeInfo) - timeZoneOffset); //timegm instead of mktime (which uses local time zone)
if (rawTime == (time_t)ULONG_MAX) {
/*NSCalendar is super-amazingly-slow (which is partly why RSDateParser exists), so this is used only when the date is far enough in the future (19 January 2038 03:14:08Z on 32-bit systems) that timegm fails. If profiling says that this is a performance issue, then you've got a weird app that needs to work with dates far in the future.*/
NSDateComponents *dateComponents = [NSDateComponents new];
dateComponents.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:timeZoneOffset];
dateComponents.year = year;
dateComponents.month = month;
dateComponents.day = day;
dateComponents.hour = hour;
dateComponents.minute = minute;
dateComponents.second = second + (milliseconds / 1000);
return [[NSCalendar autoupdatingCurrentCalendar] dateFromComponents:dateComponents];
}
if (milliseconds > 0) {
rawTime += ((float)milliseconds / 1000.0f);
}
return [NSDate dateWithTimeIntervalSince1970:rawTime];
}
#pragma mark - Standard Formats
static NSDate *RSParsePubDateWithBytes(const char *bytes, NSUInteger numberOfBytes) {
/*@"EEE',' dd MMM yyyy HH':'mm':'ss ZZZ"
@"EEE, dd MMM yyyy HH:mm:ss zzz"
@"dd MMM yyyy HH:mm zzz"
@"dd MMM yyyy HH:mm ZZZ"
@"EEE, dd MMM yyyy"
@"EEE, dd MMM yyyy HH:mm zzz"
etc.*/
NSUInteger finalIndex = 0;
NSInteger day = 1;
NSInteger month = RSJanuary;
NSInteger year = 1970;
NSInteger hour = 0;
NSInteger minute = 0;
NSInteger second = 0;
NSInteger timeZoneOffset = 0;
day = nextNumericValue(bytes, numberOfBytes, 0, 2, &finalIndex);
if (day < 1 || day == NSNotFound)
day = 1;
month = nextMonthValue(bytes, numberOfBytes, finalIndex + 1, &finalIndex);
year = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 4, &finalIndex);
hour = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
if (hour == NSNotFound)
hour = 0;
minute = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
if (minute == NSNotFound)
minute = 0;
NSUInteger currentIndex = finalIndex + 1;
BOOL hasSeconds = (currentIndex < numberOfBytes) && (bytes[currentIndex] == ':');
if (hasSeconds)
second = nextNumericValue(bytes, numberOfBytes, currentIndex, 2, &finalIndex);
currentIndex = finalIndex + 1;
BOOL hasTimeZone = (currentIndex < numberOfBytes) && (bytes[currentIndex] == ' ');
if (hasTimeZone)
timeZoneOffset = parsedTimeZoneOffset(bytes, numberOfBytes, currentIndex);
return dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset(year, month, day, hour, minute, second, 0, timeZoneOffset);
}
static NSDate *RSParseW3CWithBytes(const char *bytes, NSUInteger numberOfBytes) {
/*@"yyyy'-'MM'-'dd'T'HH':'mm':'ss"
@"yyyy-MM-dd'T'HH:mm:sszzz"
@"yyyy-MM-dd'T'HH:mm:ss'.'SSSzzz"
etc.*/
NSUInteger finalIndex = 0;
NSInteger day = 1;
NSInteger month = RSJanuary;
NSInteger year = 1970;
NSInteger hour = 0;
NSInteger minute = 0;
NSInteger second = 0;
NSInteger milliseconds = 0;
NSInteger timeZoneOffset = 0;
year = nextNumericValue(bytes, numberOfBytes, 0, 4, &finalIndex);
month = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
day = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
hour = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
minute = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
second = nextNumericValue(bytes, numberOfBytes, finalIndex + 1, 2, &finalIndex);
NSUInteger currentIndex = finalIndex + 1;
BOOL hasMilliseconds = (currentIndex < numberOfBytes) && (bytes[currentIndex] == '.');
if (hasMilliseconds) {
milliseconds = nextNumericValue(bytes, numberOfBytes, currentIndex, 3, &finalIndex);
currentIndex = finalIndex + 1;
}
timeZoneOffset = parsedTimeZoneOffset(bytes, numberOfBytes, currentIndex);
return dateWithYearMonthDayHourMinuteSecondAndTimeZoneOffset(year, month, day, hour, minute, second, milliseconds, timeZoneOffset);
}
static BOOL dateIsPubDate(const char *bytes, NSUInteger numberOfBytes) {
NSUInteger i = 0;
for (i = 0; i < numberOfBytes; i++) {
if (bytes[i] == ' ' || bytes[i] == ',')
return YES;
}
return NO;
}
static BOOL numberOfBytesIsOutsideReasonableRange(NSUInteger numberOfBytes) {
return numberOfBytes < 6 || numberOfBytes > 50;
}
#pragma mark - API
NSDate *RSDateWithBytes(const char *bytes, NSUInteger numberOfBytes) {
if (numberOfBytesIsOutsideReasonableRange(numberOfBytes))
return nil;
if (dateIsPubDate(bytes, numberOfBytes))
return RSParsePubDateWithBytes(bytes, numberOfBytes);
return RSParseW3CWithBytes(bytes, numberOfBytes);
}
NSDate *RSDateWithString(NSString *dateString) {
const char *utf8String = [dateString UTF8String];
return RSDateWithBytes(utf8String, strlen(utf8String));
}