Archive Project
This commit is contained in:
79
ISO8601DateFormatter/ISO8601DateFormatter.h
Executable file
79
ISO8601DateFormatter/ISO8601DateFormatter.h
Executable file
@@ -0,0 +1,79 @@
|
||||
/*ISO8601DateFormatter.h
|
||||
*
|
||||
*Created by Peter Hosey on 2009-04-11.
|
||||
*Copyright 2009 Peter Hosey. All rights reserved.
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/*This class converts dates to and from ISO 8601 strings. A good introduction to ISO 8601: <http://www.cl.cam.ac.uk/~mgk25/iso-time.html>
|
||||
*
|
||||
*Parsing can be done strictly, or not. When you parse loosely, leading whitespace is ignored, as is anything after the date.
|
||||
*The loose parser will return an NSDate for this string: @" \t\r\n\f\t 2006-03-02!!!"
|
||||
*Leading non-whitespace will not be ignored; the string will be rejected, and nil returned. See the README that came with this addition.
|
||||
*
|
||||
*The strict parser will only accept a string if the date is the entire string. The above string would be rejected immediately, solely on these grounds.
|
||||
*Also, the loose parser provides some extensions that the strict parser doesn't.
|
||||
*For example, the standard says for "-DDD" (an ordinal date in the implied year) that the logical representation (meaning, hierarchically) would be "--DDD", but because that extra hyphen is "superfluous", it was omitted.
|
||||
*The loose parser will accept the extra hyphen; the strict parser will not.
|
||||
*A full list of these extensions is in the README file.
|
||||
*/
|
||||
|
||||
/*The format to either expect or produce.
|
||||
*Calendar format is YYYY-MM-DD.
|
||||
*Ordinal format is YYYY-DDD, where DDD ranges from 1 to 366; for example, 2009-32 is 2009-02-01.
|
||||
*Week format is YYYY-Www-D, where ww ranges from 1 to 53 (the 'W' is literal) and D ranges from 1 to 7; for example, 2009-W05-07.
|
||||
*/
|
||||
enum {
|
||||
ISO8601DateFormatCalendar,
|
||||
ISO8601DateFormatOrdinal,
|
||||
ISO8601DateFormatWeek,
|
||||
};
|
||||
typedef NSUInteger ISO8601DateFormat;
|
||||
|
||||
//The default separator for time values. Currently, this is ':'.
|
||||
extern unichar ISO8601DefaultTimeSeparatorCharacter;
|
||||
|
||||
@interface ISO8601DateFormatter: NSFormatter
|
||||
{
|
||||
NSString *lastUsedFormatString;
|
||||
NSDateFormatter *unparsingFormatter;
|
||||
|
||||
NSCalendar *parsingCalendar, *unparsingCalendar;
|
||||
|
||||
NSTimeZone *defaultTimeZone;
|
||||
ISO8601DateFormat format;
|
||||
unichar timeSeparator;
|
||||
BOOL includeTime;
|
||||
BOOL parsesStrictly;
|
||||
}
|
||||
|
||||
//Call this if you get a memory warning.
|
||||
+ (void) purgeGlobalCaches;
|
||||
|
||||
@property(nonatomic, retain) NSTimeZone *defaultTimeZone;
|
||||
|
||||
#pragma mark Parsing
|
||||
|
||||
//As a formatter, this object converts strings to dates.
|
||||
|
||||
@property BOOL parsesStrictly;
|
||||
|
||||
- (NSDateComponents *) dateComponentsFromString:(NSString *)string;
|
||||
- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone;
|
||||
- (NSDateComponents *) dateComponentsFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange;
|
||||
|
||||
- (NSDate *) dateFromString:(NSString *)string;
|
||||
- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone;
|
||||
- (NSDate *) dateFromString:(NSString *)string timeZone:(out NSTimeZone **)outTimeZone range:(out NSRange *)outRange;
|
||||
|
||||
#pragma mark Unparsing
|
||||
|
||||
@property ISO8601DateFormat format;
|
||||
@property BOOL includeTime;
|
||||
@property unichar timeSeparator;
|
||||
|
||||
- (NSString *) stringFromDate:(NSDate *)date;
|
||||
- (NSString *) stringFromDate:(NSDate *)date timeZone:(NSTimeZone *)timeZone;
|
||||
|
||||
@end
|
||||
895
ISO8601DateFormatter/ISO8601DateFormatter.m
Executable file
895
ISO8601DateFormatter/ISO8601DateFormatter.m
Executable file
@@ -0,0 +1,895 @@
|
||||
/*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));
|
||||
}
|
||||
Reference in New Issue
Block a user