896 lines
24 KiB
Objective-C
Executable File
896 lines
24 KiB
Objective-C
Executable File
/*ISO8601DateFormatter.m
|
|
*
|
|
*Created by Peter Hosey on 2009-04-11.
|
|
*Copyright 2009 Peter Hosey. All rights reserved.
|
|
*/
|
|
|
|
#import <Foundation/Foundation.h>
|
|
#import "ISO8601DateFormatter.h"
|
|
|
|
#ifndef DEFAULT_TIME_SEPARATOR
|
|
# define DEFAULT_TIME_SEPARATOR ':'
|
|
#endif
|
|
unichar ISO8601DefaultTimeSeparatorCharacter = DEFAULT_TIME_SEPARATOR;
|
|
|
|
//Unicode date formats.
|
|
#define ISO_CALENDAR_DATE_FORMAT @"yyyy-MM-dd"
|
|
//#define ISO_WEEK_DATE_FORMAT @"YYYY-'W'ww-ee" //Doesn't actually work because NSDateComponents counts the weekday starting at 1.
|
|
#define ISO_ORDINAL_DATE_FORMAT @"yyyy-DDD"
|
|
#define ISO_TIME_FORMAT @"HH:mm:ss"
|
|
#define ISO_TIME_WITH_TIMEZONE_FORMAT ISO_TIME_FORMAT @"Z"
|
|
//printf formats.
|
|
#define ISO_TIMEZONE_UTC_FORMAT @"Z"
|
|
#define ISO_TIMEZONE_OFFSET_FORMAT @"%+.2d%.2d"
|
|
|
|
@interface ISO8601DateFormatter(UnparsingPrivate)
|
|
|
|
- (NSString *) replaceColonsInString:(NSString *)timeFormat withTimeSeparator:(unichar)timeSep;
|
|
|
|
- (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat timeZone:(NSTimeZone *)timeZone;
|
|
- (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone;
|
|
|
|
@end
|
|
|
|
static NSMutableDictionary *timeZonesByOffset;
|
|
|
|
@implementation ISO8601DateFormatter
|
|
|
|
+ (void) initialize {
|
|
if (!timeZonesByOffset) {
|
|
timeZonesByOffset = [[NSMutableDictionary alloc] init];
|
|
}
|
|
}
|
|
|
|
+ (void) purgeGlobalCaches {
|
|
NSMutableDictionary *oldCache = timeZonesByOffset;
|
|
timeZonesByOffset = nil;
|
|
[oldCache release];
|
|
}
|
|
|
|
- (NSCalendar *) makeCalendarWithDesiredConfiguration {
|
|
NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
|
|
calendar.firstWeekday = 2; //Monday
|
|
calendar.timeZone = [NSTimeZone defaultTimeZone];
|
|
return calendar;
|
|
}
|
|
|
|
- (id) init {
|
|
if ((self = [super init])) {
|
|
parsingCalendar = [[self makeCalendarWithDesiredConfiguration] retain];
|
|
unparsingCalendar = [[self makeCalendarWithDesiredConfiguration] retain];
|
|
|
|
format = ISO8601DateFormatCalendar;
|
|
timeSeparator = ISO8601DefaultTimeSeparatorCharacter;
|
|
includeTime = NO;
|
|
parsesStrictly = NO;
|
|
}
|
|
return self;
|
|
}
|
|
- (void) dealloc {
|
|
[defaultTimeZone release];
|
|
|
|
[unparsingFormatter release];
|
|
[lastUsedFormatString release];
|
|
[parsingCalendar release];
|
|
[unparsingCalendar release];
|
|
|
|
[super dealloc];
|
|
}
|
|
|
|
@synthesize defaultTimeZone;
|
|
- (void) setDefaultTimeZone:(NSTimeZone *)tz {
|
|
if (defaultTimeZone != tz) {
|
|
[defaultTimeZone release];
|
|
defaultTimeZone = [tz retain];
|
|
|
|
unparsingCalendar.timeZone = defaultTimeZone;
|
|
}
|
|
}
|
|
|
|
//The following properties are only here because GCC doesn't like @synthesize in category implementations.
|
|
|
|
#pragma mark Parsing
|
|
|
|
@synthesize parsesStrictly;
|
|
|
|
static NSUInteger read_segment(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits);
|
|
static NSUInteger read_segment_4digits(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits);
|
|
static NSUInteger read_segment_2digits(const unsigned char *str, const unsigned char **next);
|
|
static double read_double(const unsigned char *str, const unsigned char **next);
|
|
static BOOL is_leap_year(NSUInteger year);
|
|
|
|
/*Valid ISO 8601 date formats:
|
|
*
|
|
*YYYYMMDD
|
|
*YYYY-MM-DD
|
|
*YYYY-MM
|
|
*YYYY
|
|
*YY //century
|
|
* //Implied century: YY is 00-99
|
|
* YYMMDD
|
|
* YY-MM-DD
|
|
* -YYMM
|
|
* -YY-MM
|
|
* -YY
|
|
* //Implied year
|
|
* --MMDD
|
|
* --MM-DD
|
|
* --MM
|
|
* //Implied year and month
|
|
* ---DD
|
|
* //Ordinal dates: DDD is the number of the day in the year (1-366)
|
|
*YYYYDDD
|
|
*YYYY-DDD
|
|
* YYDDD
|
|
* YY-DDD
|
|
* -DDD
|
|
* //Week-based dates: ww is the number of the week, and d is the number (1-7) of the day in the week
|
|
*yyyyWwwd
|
|
*yyyy-Www-d
|
|
*yyyyWww
|
|
*yyyy-Www
|
|
*yyWwwd
|
|
*yy-Www-d
|
|
*yyWww
|
|
*yy-Www
|
|
* //Year of the implied decade
|
|
*-yWwwd
|
|
*-y-Www-d
|
|
*-yWww
|
|
*-y-Www
|
|
* //Week and day of implied year
|
|
* -Wwwd
|
|
* -Www-d
|
|
* //Week only of implied year
|
|
* -Www
|
|
* //Day only of implied week
|
|
* -W-d
|
|
*/
|
|
|
|
- (NSDateComponents *) dateComponentsFromString:(NSString *)string {
|
|
return [self dateComponentsFromString:string timeZone:NULL];
|
|
}
|
|
- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone {
|
|
return [self dateComponentsFromString:string timeZone:outTimeZone range:NULL];
|
|
}
|
|
- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange {
|
|
NSDate *now = [NSDate date];
|
|
|
|
NSDateComponents *components = [[[NSDateComponents alloc] init] autorelease];
|
|
NSDateComponents *nowComponents = [parsingCalendar components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:now];
|
|
|
|
NSUInteger
|
|
//Date
|
|
year,
|
|
month_or_week = 0U,
|
|
day = 0U,
|
|
//Time
|
|
hour = 0U;
|
|
NSTimeInterval
|
|
minute = 0.0,
|
|
second = 0.0;
|
|
//Time zone
|
|
NSInteger tz_hour = 0;
|
|
NSInteger tz_minute = 0;
|
|
|
|
enum {
|
|
monthAndDate,
|
|
week,
|
|
dateOnly
|
|
} dateSpecification = monthAndDate;
|
|
|
|
BOOL strict = self.parsesStrictly;
|
|
unichar timeSep = self.timeSeparator;
|
|
|
|
if (strict) timeSep = ISO8601DefaultTimeSeparatorCharacter;
|
|
NSAssert(timeSep != '\0', @"Time separator must not be NUL.");
|
|
|
|
BOOL isValidDate = ([string length] > 0U);
|
|
NSTimeZone *timeZone = nil;
|
|
|
|
const unsigned char *ch = (const unsigned char *)[string UTF8String];
|
|
|
|
NSRange range = { 0U, 0U };
|
|
const unsigned char *start_of_date = NULL;
|
|
if (strict && isspace(*ch)) {
|
|
range.location = NSNotFound;
|
|
isValidDate = NO;
|
|
} else {
|
|
//Skip leading whitespace.
|
|
NSUInteger i = 0U;
|
|
for(NSUInteger len = strlen((const char *)ch); i < len; ++i) {
|
|
if (!isspace(ch[i]))
|
|
break;
|
|
}
|
|
|
|
range.location = i;
|
|
ch += i;
|
|
start_of_date = ch;
|
|
|
|
NSUInteger segment;
|
|
NSUInteger num_leading_hyphens = 0U, num_digits = 0U;
|
|
|
|
if (*ch == 'T') {
|
|
//There is no date here, only a time. Set the date to now; then we'll parse the time.
|
|
isValidDate = isdigit(*++ch);
|
|
|
|
year = nowComponents.year;
|
|
month_or_week = nowComponents.month;
|
|
day = nowComponents.day;
|
|
} else {
|
|
while(*ch == '-') {
|
|
++num_leading_hyphens;
|
|
++ch;
|
|
}
|
|
|
|
segment = read_segment(ch, &ch, &num_digits);
|
|
switch(num_digits) {
|
|
case 0:
|
|
if (*ch == 'W') {
|
|
if ((ch[1] == '-') && isdigit(ch[2]) && ((num_leading_hyphens == 1U) || ((num_leading_hyphens == 2U) && !strict))) {
|
|
year = nowComponents.year;
|
|
month_or_week = 1U;
|
|
ch += 2;
|
|
goto parseDayAfterWeek;
|
|
} else if (num_leading_hyphens == 1U) {
|
|
year = nowComponents.year;
|
|
goto parseWeekAndDay;
|
|
} else
|
|
isValidDate = NO;
|
|
} else
|
|
isValidDate = NO;
|
|
break;
|
|
|
|
case 8: //YYYY MM DD
|
|
if (num_leading_hyphens > 0U)
|
|
isValidDate = NO;
|
|
else {
|
|
day = segment % 100U;
|
|
segment /= 100U;
|
|
month_or_week = segment % 100U;
|
|
year = segment / 100U;
|
|
}
|
|
break;
|
|
|
|
case 6: //YYMMDD (implicit century)
|
|
if (num_leading_hyphens > 0U)
|
|
isValidDate = NO;
|
|
else {
|
|
day = segment % 100U;
|
|
segment /= 100U;
|
|
month_or_week = segment % 100U;
|
|
year = nowComponents.year;
|
|
year -= (year % 100U);
|
|
year += segment / 100U;
|
|
}
|
|
break;
|
|
|
|
case 4:
|
|
switch(num_leading_hyphens) {
|
|
case 0: //YYYY
|
|
year = segment;
|
|
|
|
if (*ch == '-') ++ch;
|
|
|
|
if (!isdigit(*ch)) {
|
|
if (*ch == 'W')
|
|
goto parseWeekAndDay;
|
|
else
|
|
month_or_week = day = 1U;
|
|
} else {
|
|
segment = read_segment(ch, &ch, &num_digits);
|
|
switch(num_digits) {
|
|
case 4: //MMDD
|
|
day = segment % 100U;
|
|
month_or_week = segment / 100U;
|
|
break;
|
|
|
|
case 2: //MM
|
|
month_or_week = segment;
|
|
|
|
if (*ch == '-') ++ch;
|
|
if (!isdigit(*ch))
|
|
day = 1U;
|
|
else
|
|
day = read_segment(ch, &ch, NULL);
|
|
break;
|
|
|
|
case 3: //DDD
|
|
day = segment % 1000U;
|
|
dateSpecification = dateOnly;
|
|
if (strict && (day > (365U + is_leap_year(year))))
|
|
isValidDate = NO;
|
|
break;
|
|
|
|
default:
|
|
isValidDate = NO;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case 1: //YYMM
|
|
month_or_week = segment % 100U;
|
|
year = segment / 100U;
|
|
|
|
if (*ch == '-') ++ch;
|
|
if (!isdigit(*ch))
|
|
day = 1U;
|
|
else
|
|
day = read_segment(ch, &ch, NULL);
|
|
|
|
break;
|
|
|
|
case 2: //MMDD
|
|
day = segment % 100U;
|
|
month_or_week = segment / 100U;
|
|
year = nowComponents.year;
|
|
|
|
break;
|
|
|
|
default:
|
|
isValidDate = NO;
|
|
} //switch(num_leading_hyphens) (4 digits)
|
|
break;
|
|
|
|
case 1:
|
|
if (strict) {
|
|
//Two digits only - never just one.
|
|
if (num_leading_hyphens == 1U) {
|
|
if (*ch == '-') ++ch;
|
|
if (*++ch == 'W') {
|
|
year = nowComponents.year;
|
|
year -= (year % 10U);
|
|
year += segment;
|
|
goto parseWeekAndDay;
|
|
} else
|
|
isValidDate = NO;
|
|
} else
|
|
isValidDate = NO;
|
|
break;
|
|
}
|
|
case 2:
|
|
switch(num_leading_hyphens) {
|
|
case 0:
|
|
if (*ch == '-') {
|
|
//Implicit century
|
|
year = nowComponents.year;
|
|
year -= (year % 100U);
|
|
year += segment;
|
|
|
|
if (*++ch == 'W')
|
|
goto parseWeekAndDay;
|
|
else if (!isdigit(*ch)) {
|
|
goto centuryOnly;
|
|
} else {
|
|
//Get month and/or date.
|
|
segment = read_segment_4digits(ch, &ch, &num_digits);
|
|
NSLog(@"(%@) parsing month; segment is %lu and ch is %s", string, (unsigned long)segment, ch);
|
|
switch(num_digits) {
|
|
case 4: //YY-MMDD
|
|
day = segment % 100U;
|
|
month_or_week = segment / 100U;
|
|
break;
|
|
|
|
case 1: //YY-M; YY-M-DD (extension)
|
|
if (strict) {
|
|
isValidDate = NO;
|
|
break;
|
|
}
|
|
case 2: //YY-MM; YY-MM-DD
|
|
month_or_week = segment;
|
|
if (*ch == '-') {
|
|
if (isdigit(*++ch))
|
|
day = read_segment_2digits(ch, &ch);
|
|
else
|
|
day = 1U;
|
|
} else
|
|
day = 1U;
|
|
break;
|
|
|
|
case 3: //Ordinal date.
|
|
day = segment;
|
|
dateSpecification = dateOnly;
|
|
break;
|
|
}
|
|
}
|
|
} else if (*ch == 'W') {
|
|
year = nowComponents.year;
|
|
year -= (year % 100U);
|
|
year += segment;
|
|
|
|
parseWeekAndDay: //*ch should be 'W' here.
|
|
if (!isdigit(*++ch)) {
|
|
//Not really a week-based date; just a year followed by '-W'.
|
|
if (strict)
|
|
isValidDate = NO;
|
|
else
|
|
month_or_week = day = 1U;
|
|
} else {
|
|
month_or_week = read_segment_2digits(ch, &ch);
|
|
if (*ch == '-') ++ch;
|
|
parseDayAfterWeek:
|
|
day = isdigit(*ch) ? read_segment_2digits(ch, &ch) : 1U;
|
|
dateSpecification = week;
|
|
}
|
|
} else {
|
|
//Century only. Assume current year.
|
|
centuryOnly:
|
|
year = segment * 100U + nowComponents.year % 100U;
|
|
month_or_week = day = 1U;
|
|
}
|
|
break;
|
|
|
|
case 1:; //-YY; -YY-MM (implicit century)
|
|
NSLog(@"(%@) found %lu digits and one hyphen, so this is either -YY or -YY-MM; segment (year) is %lu", string, (unsigned long)num_digits, (unsigned long)segment);
|
|
NSUInteger current_year = nowComponents.year;
|
|
NSUInteger current_century = (current_year % 100U);
|
|
year = segment + (current_year - current_century);
|
|
if (num_digits == 1U) //implied decade
|
|
year += current_century - (current_year % 10U);
|
|
|
|
if (*ch == '-') {
|
|
++ch;
|
|
month_or_week = read_segment_2digits(ch, &ch);
|
|
NSLog(@"(%@) month is %lu", string, (unsigned long)month_or_week);
|
|
}
|
|
|
|
day = 1U;
|
|
break;
|
|
|
|
case 2: //--MM; --MM-DD
|
|
year = nowComponents.year;
|
|
month_or_week = segment;
|
|
if (*ch == '-') {
|
|
++ch;
|
|
day = read_segment_2digits(ch, &ch);
|
|
}
|
|
break;
|
|
|
|
case 3: //---DD
|
|
year = nowComponents.year;
|
|
month_or_week = nowComponents.month;
|
|
day = segment;
|
|
break;
|
|
|
|
default:
|
|
isValidDate = NO;
|
|
} //switch(num_leading_hyphens) (2 digits)
|
|
break;
|
|
|
|
case 7: //YYYY DDD (ordinal date)
|
|
if (num_leading_hyphens > 0U)
|
|
isValidDate = NO;
|
|
else {
|
|
day = segment % 1000U;
|
|
year = segment / 1000U;
|
|
dateSpecification = dateOnly;
|
|
if (strict && (day > (365U + is_leap_year(year))))
|
|
isValidDate = NO;
|
|
}
|
|
break;
|
|
|
|
case 3: //--DDD (ordinal date, implicit year)
|
|
//Technically, the standard only allows one hyphen. But it says that two hyphens is the logical implementation, and one was dropped for brevity. So I have chosen to allow the missing hyphen.
|
|
if ((num_leading_hyphens < 1U) || ((num_leading_hyphens > 2U) && !strict))
|
|
isValidDate = NO;
|
|
else {
|
|
day = segment;
|
|
year = nowComponents.year;
|
|
dateSpecification = dateOnly;
|
|
if (strict && (day > (365U + is_leap_year(year))))
|
|
isValidDate = NO;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
isValidDate = NO;
|
|
}
|
|
}
|
|
|
|
if (isValidDate) {
|
|
if (isspace(*ch) || (*ch == 'T')) ++ch;
|
|
|
|
if (isdigit(*ch)) {
|
|
hour = read_segment_2digits(ch, &ch);
|
|
if (*ch == timeSep) {
|
|
++ch;
|
|
if ((timeSep == ',') || (timeSep == '.')) {
|
|
//We can't do fractional minutes when '.' is the segment separator.
|
|
//Only allow whole minutes and whole seconds.
|
|
minute = read_segment_2digits(ch, &ch);
|
|
if (*ch == timeSep) {
|
|
++ch;
|
|
second = read_segment_2digits(ch, &ch);
|
|
}
|
|
} else {
|
|
//Allow a fractional minute.
|
|
//If we don't get a fraction, look for a seconds segment.
|
|
//Otherwise, the fraction of a minute is the seconds.
|
|
minute = read_double(ch, &ch);
|
|
second = modf(minute, &minute);
|
|
if (second > DBL_EPSILON)
|
|
second *= 60.0; //Convert fraction (e.g. .5) into seconds (e.g. 30).
|
|
else if (*ch == timeSep) {
|
|
++ch;
|
|
second = read_double(ch, &ch);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!strict) {
|
|
if (isspace(*ch)) ++ch;
|
|
}
|
|
|
|
switch(*ch) {
|
|
case 'Z':
|
|
timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
|
|
break;
|
|
|
|
case '+':
|
|
case '-':;
|
|
BOOL negative = (*ch == '-');
|
|
if (isdigit(*++ch)) {
|
|
//Read hour offset.
|
|
segment = *ch - '0';
|
|
if (isdigit(*++ch)) {
|
|
segment *= 10U;
|
|
segment += *(ch++) - '0';
|
|
}
|
|
tz_hour = (NSInteger)segment;
|
|
if (negative) tz_hour = -tz_hour;
|
|
|
|
//Optional separator.
|
|
if (*ch == timeSep) ++ch;
|
|
|
|
if (isdigit(*ch)) {
|
|
//Read minute offset.
|
|
segment = *ch - '0';
|
|
if (isdigit(*++ch)) {
|
|
segment *= 10U;
|
|
segment += *ch - '0';
|
|
}
|
|
tz_minute = segment;
|
|
if (negative) tz_minute = -tz_minute;
|
|
}
|
|
|
|
NSTimeInterval timeZoneOffset = (tz_hour * 3600) + (tz_minute * 60);
|
|
NSNumber *offsetNum = [NSNumber numberWithDouble:timeZoneOffset];
|
|
timeZone = [timeZonesByOffset objectForKey:offsetNum];
|
|
if (!timeZone) {
|
|
timeZone = [NSTimeZone timeZoneForSecondsFromGMT:timeZoneOffset];
|
|
if (timeZone)
|
|
[timeZonesByOffset setObject:timeZone forKey:offsetNum];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isValidDate) {
|
|
components.year = year;
|
|
components.day = day;
|
|
components.hour = hour;
|
|
components.minute = (NSInteger)minute;
|
|
components.second = (NSInteger)second;
|
|
|
|
switch(dateSpecification) {
|
|
case monthAndDate:
|
|
components.month = month_or_week;
|
|
break;
|
|
|
|
case week:;
|
|
//Adapted from <http://personal.ecu.edu/mccartyr/ISOwdALG.txt>.
|
|
//This works by converting the week date into an ordinal date, then letting the next case handle it.
|
|
NSUInteger prevYear = year - 1U;
|
|
NSUInteger YY = prevYear % 100U;
|
|
NSUInteger C = prevYear - YY;
|
|
NSUInteger G = YY + YY / 4U;
|
|
NSUInteger isLeapYear = (((C / 100U) % 4U) * 5U);
|
|
NSUInteger Jan1Weekday = (isLeapYear + G) % 7U;
|
|
enum { monday, tuesday, wednesday, thursday/*, friday, saturday, sunday*/ };
|
|
components.day = ((8U - Jan1Weekday) + (7U * (Jan1Weekday > thursday))) + (day - 1U) + (7U * (month_or_week - 2));
|
|
|
|
case dateOnly: //An "ordinal date".
|
|
break;
|
|
}
|
|
}
|
|
} //if (!(strict && isdigit(ch[0])))
|
|
|
|
if (outRange) {
|
|
if (isValidDate)
|
|
range.length = ch - start_of_date;
|
|
else
|
|
range.location = NSNotFound;
|
|
|
|
*outRange = range;
|
|
}
|
|
if (outTimeZone) {
|
|
*outTimeZone = timeZone;
|
|
}
|
|
|
|
return components;
|
|
}
|
|
|
|
- (NSDate *) dateFromString:(NSString *)string {
|
|
return [self dateFromString:string timeZone:NULL];
|
|
}
|
|
- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone {
|
|
return [self dateFromString:string timeZone:outTimeZone range:NULL];
|
|
}
|
|
- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange {
|
|
NSTimeZone *timeZone = nil;
|
|
NSDateComponents *components = [self dateComponentsFromString:string timeZone:&timeZone range:outRange];
|
|
if (outTimeZone)
|
|
*outTimeZone = timeZone;
|
|
parsingCalendar.timeZone = timeZone;
|
|
|
|
return [parsingCalendar dateFromComponents:components];
|
|
}
|
|
|
|
- (BOOL)getObjectValue:(id *)outValue forString:(NSString *)string errorDescription:(NSString **)error {
|
|
NSDate *date = [self dateFromString:string];
|
|
if (outValue)
|
|
*outValue = date;
|
|
return (date != nil);
|
|
}
|
|
|
|
#pragma mark Unparsing
|
|
|
|
@synthesize format;
|
|
@synthesize includeTime;
|
|
@synthesize timeSeparator;
|
|
|
|
- (NSString *) replaceColonsInString:(NSString *)timeFormat withTimeSeparator:(unichar)timeSep {
|
|
if (timeSep != ':') {
|
|
NSMutableString *timeFormatMutable = [[timeFormat mutableCopy] autorelease];
|
|
[timeFormatMutable replaceOccurrencesOfString:@":"
|
|
withString:[NSString stringWithCharacters:&timeSep length:1U]
|
|
options:NSBackwardsSearch | NSLiteralSearch
|
|
range:(NSRange){ 0UL, [timeFormat length] }];
|
|
timeFormat = timeFormatMutable;
|
|
}
|
|
return timeFormat;
|
|
}
|
|
|
|
- (NSString *) stringFromDate:(NSDate *)date {
|
|
NSTimeZone *timeZone = self.defaultTimeZone;
|
|
if (!timeZone) timeZone = [NSTimeZone defaultTimeZone];
|
|
return [self stringFromDate:date timeZone:timeZone];
|
|
}
|
|
|
|
- (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone {
|
|
switch (self.format) {
|
|
case ISO8601DateFormatCalendar:
|
|
return [self stringFromDate:date formatString:ISO_CALENDAR_DATE_FORMAT timeZone:timeZone];
|
|
case ISO8601DateFormatWeek:
|
|
return [self weekDateStringForDate:date timeZone:timeZone];
|
|
case ISO8601DateFormatOrdinal:
|
|
return [self stringFromDate:date formatString:ISO_ORDINAL_DATE_FORMAT timeZone:timeZone];
|
|
default:
|
|
[NSException raise:NSInternalInconsistencyException format:@"self.format was %ld, not calendar (%d), week (%d), or ordinal (%d)", self.format, ISO8601DateFormatCalendar, ISO8601DateFormatWeek, ISO8601DateFormatOrdinal];
|
|
return nil;
|
|
}
|
|
}
|
|
|
|
- (NSString *) stringFromDate:(NSDate *)date formatString:(NSString *)dateFormat timeZone:(NSTimeZone *)timeZone {
|
|
if (includeTime)
|
|
dateFormat = [dateFormat stringByAppendingFormat:@"'T'%@", [self replaceColonsInString:ISO_TIME_FORMAT withTimeSeparator:self.timeSeparator]];
|
|
|
|
unparsingCalendar.timeZone = timeZone;
|
|
|
|
if (dateFormat != lastUsedFormatString) {
|
|
[unparsingFormatter release];
|
|
unparsingFormatter = nil;
|
|
|
|
[lastUsedFormatString release];
|
|
lastUsedFormatString = [dateFormat retain];
|
|
}
|
|
|
|
if (!unparsingFormatter) {
|
|
unparsingFormatter = [[NSDateFormatter alloc] init];
|
|
unparsingFormatter.formatterBehavior = NSDateFormatterBehavior10_4;
|
|
unparsingFormatter.dateFormat = dateFormat;
|
|
unparsingFormatter.calendar = unparsingCalendar;
|
|
}
|
|
|
|
NSString *str = [unparsingFormatter stringForObjectValue:date];
|
|
|
|
if (includeTime) {
|
|
NSInteger offset = [timeZone secondsFromGMT];
|
|
offset /= 60; //bring down to minutes
|
|
if (offset == 0)
|
|
str = [str stringByAppendingString:ISO_TIMEZONE_UTC_FORMAT];
|
|
else
|
|
str = [str stringByAppendingFormat:ISO_TIMEZONE_OFFSET_FORMAT, offset / 60, offset % 60];
|
|
}
|
|
|
|
//Undo the change we made earlier
|
|
unparsingCalendar.timeZone = self.defaultTimeZone;
|
|
|
|
return str;
|
|
}
|
|
|
|
- (NSString *) stringForObjectValue:(id)value {
|
|
NSParameterAssert([value isKindOfClass:[NSDate class]]);
|
|
|
|
return [self stringFromDate:(NSDate *)value];
|
|
}
|
|
|
|
/*Adapted from:
|
|
* Algorithm for Converting Gregorian Dates to ISO 8601 Week Date
|
|
* Rick McCarty, 1999
|
|
* http://personal.ecu.edu/mccartyr/ISOwdALG.txt
|
|
*/
|
|
- (NSString *) weekDateStringForDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone {
|
|
unparsingCalendar.timeZone = timeZone;
|
|
NSDateComponents *components = [unparsingCalendar components:NSYearCalendarUnit | NSWeekdayCalendarUnit | NSDayCalendarUnit fromDate:date];
|
|
|
|
//Determine the ordinal date.
|
|
NSDateComponents *startOfYearComponents = [unparsingCalendar components:NSYearCalendarUnit fromDate:date];
|
|
startOfYearComponents.month = 1;
|
|
startOfYearComponents.day = 1;
|
|
NSDateComponents *ordinalComponents = [unparsingCalendar components:NSDayCalendarUnit fromDate:[unparsingCalendar dateFromComponents:startOfYearComponents] toDate:date options:0];
|
|
ordinalComponents.day += 1;
|
|
|
|
enum {
|
|
monday, tuesday, wednesday, thursday, friday, saturday, sunday
|
|
};
|
|
enum {
|
|
january = 1, february, march,
|
|
april, may, june,
|
|
july, august, september,
|
|
october, november, december
|
|
};
|
|
|
|
NSInteger year = components.year;
|
|
NSInteger week = 0;
|
|
//The old unparser added 6 to [calendarDate dayOfWeek], which was zero-based; components.weekday is one-based, so we now add only 5.
|
|
NSInteger dayOfWeek = (components.weekday + 5) % 7;
|
|
NSInteger dayOfYear = ordinalComponents.day;
|
|
|
|
NSInteger prevYear = year - 1;
|
|
|
|
BOOL yearIsLeapYear = is_leap_year(year);
|
|
BOOL prevYearIsLeapYear = is_leap_year(prevYear);
|
|
|
|
NSInteger YY = prevYear % 100;
|
|
NSInteger C = prevYear - YY;
|
|
NSInteger G = YY + YY / 4;
|
|
NSInteger Jan1Weekday = (((((C / 100) % 4) * 5) + G) % 7);
|
|
|
|
NSInteger weekday = ((dayOfYear + Jan1Weekday) - 1) % 7;
|
|
|
|
if((dayOfYear <= (7 - Jan1Weekday)) && (Jan1Weekday > thursday)) {
|
|
week = 52 + ((Jan1Weekday == friday) || ((Jan1Weekday == saturday) && prevYearIsLeapYear));
|
|
--year;
|
|
} else {
|
|
NSInteger lengthOfYear = 365 + yearIsLeapYear;
|
|
if((lengthOfYear - dayOfYear) < (thursday - weekday)) {
|
|
++year;
|
|
week = 1;
|
|
} else {
|
|
NSInteger J = dayOfYear + (sunday - weekday) + Jan1Weekday;
|
|
week = J / 7 - (Jan1Weekday > thursday);
|
|
}
|
|
}
|
|
|
|
NSString *timeString;
|
|
if(includeTime) {
|
|
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
|
|
unichar timeSep = self.timeSeparator;
|
|
if (!timeSep) timeSep = ISO8601DefaultTimeSeparatorCharacter;
|
|
formatter.dateFormat = [self replaceColonsInString:ISO_TIME_WITH_TIMEZONE_FORMAT withTimeSeparator:timeSep];
|
|
|
|
timeString = [formatter stringForObjectValue:date];
|
|
|
|
[formatter release];
|
|
} else
|
|
timeString = @"";
|
|
|
|
return [NSString stringWithFormat:@"%lu-W%02lu-%02lu%@", (unsigned long)year, (unsigned long)week, ((unsigned long)dayOfWeek) + 1U, timeString];
|
|
}
|
|
|
|
@end
|
|
|
|
static NSUInteger read_segment(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits) {
|
|
NSUInteger num_digits = 0U;
|
|
NSUInteger value = 0U;
|
|
|
|
while(isdigit(*str)) {
|
|
value *= 10U;
|
|
value += *str - '0';
|
|
++num_digits;
|
|
++str;
|
|
}
|
|
|
|
if (next) *next = str;
|
|
if (out_num_digits) *out_num_digits = num_digits;
|
|
|
|
return value;
|
|
}
|
|
static NSUInteger read_segment_4digits(const unsigned char *str, const unsigned char **next, NSUInteger *out_num_digits) {
|
|
NSUInteger num_digits = 0U;
|
|
NSUInteger value = 0U;
|
|
|
|
if (isdigit(*str)) {
|
|
value += *(str++) - '0';
|
|
++num_digits;
|
|
}
|
|
|
|
if (isdigit(*str)) {
|
|
value *= 10U;
|
|
value += *(str++) - '0';
|
|
++num_digits;
|
|
}
|
|
|
|
if (isdigit(*str)) {
|
|
value *= 10U;
|
|
value += *(str++) - '0';
|
|
++num_digits;
|
|
}
|
|
|
|
if (isdigit(*str)) {
|
|
value *= 10U;
|
|
value += *(str++) - '0';
|
|
++num_digits;
|
|
}
|
|
|
|
if (next) *next = str;
|
|
if (out_num_digits) *out_num_digits = num_digits;
|
|
|
|
return value;
|
|
}
|
|
static NSUInteger read_segment_2digits(const unsigned char *str, const unsigned char **next) {
|
|
NSUInteger value = 0U;
|
|
|
|
if (isdigit(*str))
|
|
value += *str - '0';
|
|
|
|
if (isdigit(*++str)) {
|
|
value *= 10U;
|
|
value += *(str++) - '0';
|
|
}
|
|
|
|
if (next) *next = str;
|
|
|
|
return value;
|
|
}
|
|
|
|
//strtod doesn't support ',' as a separator. This does.
|
|
static double read_double(const unsigned char *str, const unsigned char **next) {
|
|
double value = 0.0;
|
|
|
|
if (str) {
|
|
NSUInteger int_value = 0;
|
|
|
|
while(isdigit(*str)) {
|
|
int_value *= 10U;
|
|
int_value += (*(str++) - '0');
|
|
}
|
|
value = int_value;
|
|
|
|
if (((*str == ',') || (*str == '.'))) {
|
|
++str;
|
|
|
|
register double multiplier, multiplier_multiplier;
|
|
multiplier = multiplier_multiplier = 0.1;
|
|
|
|
while(isdigit(*str)) {
|
|
value += (*(str++) - '0') * multiplier;
|
|
multiplier *= multiplier_multiplier;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (next) *next = str;
|
|
|
|
return value;
|
|
}
|
|
|
|
static BOOL is_leap_year(NSUInteger year) {
|
|
return \
|
|
((year % 4U) == 0U)
|
|
&& (((year % 100U) != 0U)
|
|
|| ((year % 400U) == 0U));
|
|
}
|