Commits

Anonymous committed a1eff57

Massive rewrite. The parser and unparser are now together in a single NSFormatter subclass, and are now based on NSCalendar and NSDateComponents instead of NSCalendarDate.

The parser remains more or less unchanged. Two-thirds of the unparser is now based on NSDateFormatter, which handles that portion of ISO 8601 string-formatting more or less for free; the part not handled for free is week dates (because NSCalendar has a different idea of when the week starts), and that unparser also remains more or less unchanged.

Comments (0)

Files changed (12)

ISO8601DateFormatter.h

+/*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
+{
+	NSTimeZone *defaultTimeZone;
+	ISO8601DateFormat format;
+	unichar timeSeparator;
+	BOOL includeTime;
+	BOOL parsesStrictly;
+}
+
+@property(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

ISO8601DateFormatter.m

+/*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 @"%+02d%02d"
+
+@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
+
+@implementation ISO8601DateFormatter
+
+- (id) init {
+	if ((self = [super init])) {
+		format = ISO8601DateFormatCalendar;
+		timeSeparator = ISO8601DefaultTimeSeparatorCharacter;
+		includeTime = NO;
+		parsesStrictly = NO;
+	}
+	return self;
+}
+- (void) dealloc {
+	[defaultTimeZone release];
+	[super dealloc];
+}
+
+@synthesize defaultTimeZone;
+
+//The following properties are only here because GCC doesn't like @synthesize in category implementations.
+
+#pragma mark Parsing
+
+@synthesize parsesStrictly;
+
+static unsigned read_segment(const unsigned char *str, const unsigned char **next, unsigned *out_num_digits);
+static unsigned read_segment_4digits(const unsigned char *str, const unsigned char **next, unsigned *out_num_digits);
+static unsigned 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(unsigned 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 {
+	NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
+	calendar.firstWeekday = 2; //Monday
+	NSDate *now = [NSDate date];
+
+	NSDateComponents *components = [[[NSDateComponents alloc] init] autorelease];
+	NSDateComponents *nowComponents = [calendar components:(NSYearCalendarUnit | NSMonthCalendarUnit | NSDayCalendarUnit) fromDate:now];
+
+	unsigned
+		//Date
+		year,
+		month_or_week,
+		day,
+		//Time
+		hour = 0U;
+	NSTimeInterval
+		minute = 0.0,
+		second = 0.0;
+	//Time zone
+	signed tz_hour = 0;
+	signed 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;
+	if (strict && isspace(*ch)) {
+		range.location = NSNotFound;
+		isValidDate = NO;
+	} else {
+		//Skip leading whitespace.
+		unsigned i = 0U;
+		for(unsigned len = strlen((const char *)ch); i < len; ++i) {
+			if (!isspace(ch[i]))
+				break;
+		}
+
+		range.location = i;
+		ch += i;
+		start_of_date = ch;
+
+		unsigned segment;
+		unsigned 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 {
+			segment = 0U;
+
+			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 %u and ch is %s", string, 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 %u digits and one hyphen, so this is either -YY or -YY-MM; segment (year) is %u", string, num_digits, segment);
+							unsigned current_year = nowComponents.year;
+							unsigned century = (current_year % 100U);
+							year = segment + (current_year - century);
+							if (num_digits == 1U) //implied decade
+								year += century - (current_year % 10U);
+
+							if (*ch == '-') {
+								++ch;
+								month_or_week = read_segment_2digits(ch, &ch);
+								NSLog(@"(%@) month is %u", string, 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);
+						}
+					}
+				}
+
+				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 = (signed)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;
+							}
+
+							timeZone = [NSTimeZone timeZoneForSecondsFromGMT:(tz_hour * 3600) + (tz_minute * 60)];
+						}
+				}
+			}
+		}
+
+		if (isValidDate) {
+			components.year = year;
+			components.day = day;
+			components.hour = hour;
+			components.minute = minute;
+			components.second = 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.
+					unsigned prevYear = year - 1U;
+					unsigned YY = prevYear % 100U;
+					unsigned C = prevYear - YY;
+					unsigned G = YY + YY / 4U;
+					unsigned isLeapYear = (((C / 100U) % 4U) * 5U);
+					unsigned 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 {
+	NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
+	calendar.firstWeekday = 2; //Monday
+
+	NSTimeZone *timeZone = nil;
+	NSDateComponents *components = [self dateComponentsFromString:string timeZone:&timeZone range:outRange];
+	if (outTimeZone)
+		*outTimeZone = timeZone;
+	calendar.timeZone = timeZone;
+
+	return [calendar 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 %d, 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]];
+
+	NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
+	calendar.firstWeekday = 2; //Monday
+
+	NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
+	formatter.formatterBehavior = NSDateFormatterBehavior10_4;
+	formatter.dateFormat = dateFormat;
+	formatter.calendar = calendar;
+
+	NSString *str = [formatter stringForObjectValue:date];
+
+	[formatter release];
+
+	if (includeTime) {
+		int 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];
+	}
+	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 {
+	NSCalendar *calendar = [[[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar] autorelease];
+	calendar.timeZone = timeZone;
+	NSDateComponents *components = [calendar components:NSYearCalendarUnit | NSWeekdayCalendarUnit | NSDayCalendarUnit fromDate:date];
+
+	//Determine the ordinal date.
+	NSDateComponents *startOfYearComponents = [calendar components:NSYearCalendarUnit fromDate:date];
+	startOfYearComponents.month = 1;
+	startOfYearComponents.day = 1;
+	NSDateComponents *ordinalComponents = [calendar components:NSDayCalendarUnit fromDate:[calendar dateFromComponents:startOfYearComponents] toDate:date options:0];
+	ordinalComponents.day += 1;
+
+	enum {
+		monday, tuesday, wednesday, thursday, friday, saturday, sunday
+	};
+	enum {
+		january = 1U, february, march,
+		april, may, june,
+		july, august, september,
+		october, november, december
+	};
+
+	unsigned year = components.year;
+	unsigned week = 0U;
+	//The old unparser added 6 to [calendarDate dayOfWeek], which was zero-based; components.weekday is one-based, so we now add only 5.
+	unsigned dayOfWeek = (components.weekday + 5U) % 7U;
+	unsigned dayOfYear = ordinalComponents.day;
+
+	unsigned prevYear = year - 1U;
+
+	BOOL yearIsLeapYear = is_leap_year(year);
+	BOOL prevYearIsLeapYear = is_leap_year(prevYear);
+
+	unsigned YY = prevYear % 100U;
+	unsigned C = prevYear - YY;
+	unsigned G = YY + YY / 4U;
+	unsigned Jan1Weekday = (((((C / 100U) % 4U) * 5U) + G) % 7U);
+
+	unsigned weekday = ((dayOfYear + Jan1Weekday) - 1U) % 7U;
+
+	if((dayOfYear <= (7U - Jan1Weekday)) && (Jan1Weekday > thursday)) {
+		week = 52U + ((Jan1Weekday == friday) || ((Jan1Weekday == saturday) && prevYearIsLeapYear));
+		--year;
+	} else {
+		unsigned lengthOfYear = 365U + yearIsLeapYear;
+		if((lengthOfYear - dayOfYear) < (thursday - weekday)) {
+			++year;
+			week = 1U;
+		} else {
+			unsigned J = dayOfYear + (sunday - weekday) + Jan1Weekday;
+			week = J / 7U - (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:self];
+
+		[formatter release];
+	} else
+		timeString = @"";
+
+	return [NSString stringWithFormat:@"%u-W%02u-%02u%@", year, week, dayOfWeek + 1U, timeString];
+}
+
+@end
+
+static unsigned read_segment(const unsigned char *str, const unsigned char **next, unsigned *out_num_digits) {
+	unsigned num_digits = 0U;
+	unsigned 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 unsigned read_segment_4digits(const unsigned char *str, const unsigned char **next, unsigned *out_num_digits) {
+	unsigned num_digits = 0U;
+	unsigned 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 unsigned read_segment_2digits(const unsigned char *str, const unsigned char **next) {
+	unsigned 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) {
+		unsigned 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(unsigned year) {
+	return \
+	    ((year %   4U) == 0U)
+	&& (((year % 100U) != 0U)
+	||  ((year % 400U) == 0U));
+}
 	diff -qs test_files/testunparser-expected.out testunparser.out
 .PHONY: all test parser-test unparser-test
 
-testparser: testparser.o NSCalendarDate+ISO8601Parsing.o
+testparser: testparser.o ISO8601DateFormatter.o
 
 testparser.sh: testparser.sh.in
 	python testparser.sh.py
 
-unparse-weekdate: unparse-weekdate.o NSCalendarDate+ISO8601Parsing.o NSCalendarDate+ISO8601Unparsing.o
-unparse-ordinaldate: unparse-ordinaldate.o NSCalendarDate+ISO8601Parsing.o NSCalendarDate+ISO8601Unparsing.o
-unparse-date: unparse-date.o NSCalendarDate+ISO8601Parsing.o NSCalendarDate+ISO8601Unparsing.o
+unparse-weekdate: unparse-weekdate.o ISO8601DateFormatter.o ISO8601DateFormatter.o
+unparse-ordinaldate: unparse-ordinaldate.o ISO8601DateFormatter.o ISO8601DateFormatter.o
+unparse-date: unparse-date.o ISO8601DateFormatter.o ISO8601DateFormatter.o

NSCalendarDate+ISO8601Parsing.h

-/*NSCalendarDate+ISO8601Parsing.h
- *
- *Created by Peter Hosey on 2006-02-20.
- *Copyright 2006 Peter Hosey. All rights reserved.
- */
-
-#import <Foundation/Foundation.h>
-
-/*This addition parses ISO 8601 dates. A good introduction: <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 NSCalendarDate 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 default separator for time values. Currently, this is ':'.
-extern unichar ISO8601ParserDefaultTimeSeparatorCharacter;
-
-@interface NSCalendarDate(ISO8601Parsing)
-
-//This method is the one that does all the work. All the others are convenience methods.
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str strictly:(BOOL)strict getRange:(out NSRange *)outRange;
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str strictly:(BOOL)strict;
-
-//Strictly: NO.
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str timeSeparator:(unichar)timeSep getRange:(out NSRange *)outRange;
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str timeSeparator:(unichar)timeSep;
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str getRange:(out NSRange *)outRange;
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str;
-
-@end

NSCalendarDate+ISO8601Parsing.m

-/*NSCalendarDate+ISO8601Parsing.m
- *
- *Created by Peter Hosey on 2006-02-20.
- *Copyright 2006 Peter Hosey. All rights reserved.
- */
-
-#include <ctype.h>
-#include <string.h>
-
-#import "NSCalendarDate+ISO8601Parsing.h"
-
-#ifndef DEFAULT_TIME_SEPARATOR
-#	define DEFAULT_TIME_SEPARATOR ':'
-#endif
-unichar ISO8601ParserDefaultTimeSeparatorCharacter = DEFAULT_TIME_SEPARATOR;
-
-static unsigned read_segment(const unsigned char *str, const unsigned char **next, unsigned *out_num_digits) {
-	unsigned num_digits = 0U;
-	unsigned 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 unsigned read_segment_4digits(const unsigned char *str, const unsigned char **next, unsigned *out_num_digits) {
-	unsigned num_digits = 0U;
-	unsigned 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 unsigned read_segment_2digits(const unsigned char *str, const unsigned char **next) {
-	unsigned 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) {
-		unsigned 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(unsigned year) {
-	return \
-	    ((year %   4U) == 0U)
-	&& (((year % 100U) != 0U)
-	||  ((year % 400U) == 0U));
-}
-
-@implementation NSCalendarDate(ISO8601Parsing)
-
-/*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
- */
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str strictly:(BOOL)strict timeSeparator:(unichar)timeSep getRange:(out NSRange *)outRange {
-	NSCalendarDate *now = [NSCalendarDate calendarDate];
-	unsigned
-		//Date
-		year,
-		month_or_week,
-		day,
-		//Time
-		hour = 0U;
-	NSTimeInterval
-		minute = 0.0,
-		second = 0.0;
-	//Time zone
-	signed tz_hour = 0;
-	signed tz_minute = 0;
-
-	enum {
-		monthAndDate,
-		week,
-		dateOnly
-	} dateSpecification = monthAndDate;
-
-	if(strict) timeSep = ISO8601ParserDefaultTimeSeparatorCharacter;
-	NSAssert(timeSep != '\0', @"Time separator must not be NUL.");
-
-	BOOL isValidDate = ([str length] > 0U);
-	NSTimeZone *timeZone = nil;
-	NSCalendarDate *date = nil;
-
-	const unsigned char *ch = (const unsigned char *)[str UTF8String];
-
-	NSRange range = { 0U, 0U };
-	const unsigned char *start_of_date;
-	if(strict && isspace(*ch)) {
-		range.location = NSNotFound;
-		isValidDate = NO;
-	} else {
-		//Skip leading whitespace.
-		unsigned i = 0U;
-		for(unsigned len = strlen((const char *)ch); i < len; ++i) {
-			if(!isspace(ch[i]))
-				break;
-		}
-
-		range.location = i;
-		ch += i;
-		start_of_date = ch;
-
-		unsigned segment;
-		unsigned 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 = [now yearOfCommonEra];
-			month_or_week = [now monthOfYear];
-			day = [now dayOfMonth];
-		} else {
-			segment = 0U;
-
-			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 = [now yearOfCommonEra];
-							month_or_week = 1U;
-							ch += 2;
-							goto parseDayAfterWeek;
-						} else if(num_leading_hyphens == 1U) {
-							year = [now yearOfCommonEra];
-							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  = [now yearOfCommonEra];
-						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 = [now yearOfCommonEra];
-
-							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  = [now yearOfCommonEra];
-								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  = [now yearOfCommonEra];
-								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 %u and ch is %s", str, 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  = [now yearOfCommonEra];
-								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 + [now yearOfCommonEra] % 100U;
-								month_or_week = day = 1U;
-							}
-							break;
-
-						case 1:; //-YY; -YY-MM (implicit century)
-							NSLog(@"(%@) found %u digits and one hyphen, so this is either -YY or -YY-MM; segment (year) is %u", str, num_digits, segment);
-							unsigned current_year = [now yearOfCommonEra];
-							unsigned century = (current_year % 100U);
-							year = segment + (current_year - century);
-							if(num_digits == 1U) //implied decade
-								year += century - (current_year % 10U);
-
-							if(*ch == '-') {
-								++ch;
-								month_or_week = read_segment_2digits(ch, &ch);
-								NSLog(@"(%@) month is %u", str, month_or_week);
-							}
-
-							day = 1U;
-							break;
-
-						case 2: //--MM; --MM-DD
-							year = [now yearOfCommonEra];
-							month_or_week = segment;
-							if(*ch == '-') {
-								++ch;
-								day = read_segment_2digits(ch, &ch);
-							}
-							break;
-
-						case 3: //---DD
-							year = [now yearOfCommonEra];
-							month_or_week = [now monthOfYear];
-							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 = [now yearOfCommonEra];
-						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);
-						}
-					}
-				}
-
-				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 = (signed)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;
-							}
-
-							timeZone = [NSTimeZone timeZoneForSecondsFromGMT:(tz_hour * 3600) + (tz_minute * 60)];
-						}
-				}
-			}
-		}
-
-		if(isValidDate) {
-			switch(dateSpecification) {
-				case monthAndDate:
-					date = [NSCalendarDate dateWithYear:year
-												  month:month_or_week
-													day:day
-												   hour:hour
-												 minute:minute
-												 second:second
-											   timeZone:timeZone];
-					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.
-					unsigned prevYear = year - 1U;
-					unsigned YY = prevYear % 100U;
-					unsigned C = prevYear - YY;
-					unsigned G = YY + YY / 4U;
-					unsigned isLeapYear = (((C / 100U) % 4U) * 5U);
-					unsigned Jan1Weekday = (isLeapYear + G) % 7U;
-					enum { monday, tuesday, wednesday, thursday/*, friday, saturday, sunday*/ };
-					day = ((8U - Jan1Weekday) + (7U * (Jan1Weekday > thursday))) + (day - 1U) + (7U * (month_or_week - 2));
-
-				case dateOnly: //An "ordinal date".
-					date = [NSCalendarDate dateWithYear:year
-												  month:1
-													day:1
-												   hour:hour
-												 minute:minute
-												 second:second
-											   timeZone:timeZone];
-					date = [date dateByAddingYears:0
-											months:0
-											  days:(day - 1)
-											 hours:0
-										   minutes:0
-										   seconds:0];
-					break;
-			}
-		}
-	} //if(!(strict && isdigit(ch[0])))
-
-	if(outRange) {
-		if(isValidDate)
-			range.length = ch - start_of_date;
-		else
-			range.location = NSNotFound;
-
-		*outRange = range;
-	}
-	return date;
-}
-
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str {
-	return [self calendarDateWithString:str strictly:NO getRange:NULL];
-}
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str strictly:(BOOL)strict {
-	return [self calendarDateWithString:str strictly:strict getRange:NULL];
-}
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str strictly:(BOOL)strict getRange:(out NSRange *)outRange {
-	return [self calendarDateWithString:str strictly:strict timeSeparator:ISO8601ParserDefaultTimeSeparatorCharacter getRange:NULL];
-}
-
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str timeSeparator:(unichar)timeSep getRange:(out NSRange *)outRange {
-	return [self calendarDateWithString:str strictly:NO timeSeparator:timeSep getRange:outRange];
-}
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str timeSeparator:(unichar)timeSep {
-	return [self calendarDateWithString:str strictly:NO timeSeparator:timeSep getRange:NULL];
-}
-+ (NSCalendarDate *)calendarDateWithString:(NSString *)str getRange:(out NSRange *)outRange {
-	return [self calendarDateWithString:str strictly:NO timeSeparator:ISO8601ParserDefaultTimeSeparatorCharacter getRange:outRange];
-}
-
-@end

NSCalendarDate+ISO8601Unparsing.h

-/*NSCalendarDate+ISO8601Unparsing.h
- *
- *Created by Peter Hosey on 2006-05-29.
- *Copyright 2006 Peter Hosey. All rights reserved.
- */
-
-#import <Foundation/Foundation.h>
-
-/*This addition unparses dates to ISO 8601 strings. A good introduction to ISO 8601: <http://www.cl.cam.ac.uk/~mgk25/iso-time.html>
- */
-
-//The default separator for time values. Currently, this is ':'.
-extern unichar ISO8601UnparserDefaultTimeSeparatorCharacter;
-
-@interface NSCalendarDate(ISO8601Unparsing)
-
-- (NSString *)ISO8601DateStringWithTime:(BOOL)includeTime timeSeparator:(unichar)timeSep;
-- (NSString *)ISO8601WeekDateStringWithTime:(BOOL)includeTime timeSeparator:(unichar)timeSep;
-- (NSString *)ISO8601OrdinalDateStringWithTime:(BOOL)includeTime timeSeparator:(unichar)timeSep;
-
-- (NSString *)ISO8601DateStringWithTime:(BOOL)includeTime;
-- (NSString *)ISO8601WeekDateStringWithTime:(BOOL)includeTime;
-- (NSString *)ISO8601OrdinalDateStringWithTime:(BOOL)includeTime;
-
-//includeTime: YES.
-- (NSString *)ISO8601DateStringWithTimeSeparator:(unichar)timeSep;
-- (NSString *)ISO8601WeekDateStringWithTimeSeparator:(unichar)timeSep;
-- (NSString *)ISO8601OrdinalDateStringWithTimeSeparator:(unichar)timeSep;
-
-//includeTime: YES.
-- (NSString *)ISO8601DateString;
-- (NSString *)ISO8601WeekDateString;
-- (NSString *)ISO8601OrdinalDateString;
-
-@end
-

NSCalendarDate+ISO8601Unparsing.m

-/*NSCalendarDate+ISO8601Unparsing.m
- *
- *Created by Peter Hosey on 2006-05-29.
- *Copyright 2006 Peter Hosey. All rights reserved.
- */
-
-#import <Foundation/Foundation.h>
-
-#ifndef DEFAULT_TIME_SEPARATOR
-#	define DEFAULT_TIME_SEPARATOR ':'
-#endif
-unichar ISO8601UnparserDefaultTimeSeparatorCharacter = DEFAULT_TIME_SEPARATOR;
-
-static BOOL is_leap_year(unsigned year) {
-	return \
-	    ((year %   4U) == 0U)
-	&& (((year % 100U) != 0U)
-	||  ((year % 400U) == 0U));
-}
-
-@interface NSString(ISO8601Unparsing)
-
-//Replace all occurrences of ':' with timeSep.
-- (NSString *)prepareDateFormatWithTimeSeparator:(unichar)timeSep;
-
-@end
-
-@implementation NSCalendarDate(ISO8601Unparsing)
-
-#pragma mark Public methods
-
-- (NSString *)ISO8601DateStringWithTime:(BOOL)includeTime timeSeparator:(unichar)timeSep {
-	NSString *dateFormat = [(includeTime ? @"%Y-%m-%dT%H:%M:%S" : @"%Y-%m-%d") prepareDateFormatWithTimeSeparator:timeSep];
-	NSDateFormatter *formatter = [[NSDateFormatter alloc] initWithDateFormat:dateFormat allowNaturalLanguage:NO];
-	NSString *str = [formatter stringForObjectValue:self];
-	[formatter release];
-	if(includeTime) {
-		int offset = [[self timeZone] secondsFromGMT];
-		offset /= 60;  //bring down to minutes
-		if(offset == 0)
-			str = [str stringByAppendingString:@"Z"];
-		if(offset < 0)
-			str = [str stringByAppendingFormat:@"-%02d%02d", -offset / 60, offset % 60];
-		else
-			str = [str stringByAppendingFormat:@"+%02d%02d", offset / 60, offset % 60];
-	}
-	return str;
-}
-/*Adapted from:
- *	Algorithm for Converting Gregorian Dates to ISO 8601 Week Date
- *	Rick McCarty, 1999
- *	http://personal.ecu.edu/mccartyr/ISOwdALG.txt
- */
-- (NSString *)ISO8601WeekDateStringWithTime:(BOOL)includeTime timeSeparator:(unichar)timeSep {
-	enum {
-		monday, tuesday, wednesday, thursday, friday, saturday, sunday
-	};
-	enum {
-		january = 1U, february, march,
-		april, may, june,
-		july, august, september,
-		october, november, december
-	};
-
-	unsigned year = [self yearOfCommonEra];
-	unsigned week = 0U;
-	unsigned dayOfWeek = ([self dayOfWeek] + 6U) % 7U;
-	unsigned dayOfYear = [self dayOfYear];
-
-	unsigned prevYear = year - 1U;
-
-	BOOL yearIsLeapYear = is_leap_year(year);
-	BOOL prevYearIsLeapYear = is_leap_year(prevYear);
-
-	unsigned YY = prevYear % 100U;
-	unsigned C = prevYear - YY;
-	unsigned G = YY + YY / 4U;
-	unsigned Jan1Weekday = (((((C / 100U) % 4U) * 5U) + G) % 7U);
-
-	unsigned weekday = ((dayOfYear + Jan1Weekday) - 1U) % 7U;
-
-	if((dayOfYear <= (7U - Jan1Weekday)) && (Jan1Weekday > thursday)) {
-		week = 52U + ((Jan1Weekday == friday) || ((Jan1Weekday == saturday) && prevYearIsLeapYear));
-		--year;
-	} else {
-		unsigned lengthOfYear = 365U + yearIsLeapYear;
-		if((lengthOfYear - dayOfYear) < (thursday - weekday)) {
-			++year;
-			week = 1U;
-		} else {
-			unsigned J = dayOfYear + (sunday - weekday) + Jan1Weekday;
-			week = J / 7U - (Jan1Weekday > thursday);
-		}
-	}
-
-	NSString *timeString;
-	if(includeTime) {
-		NSDateFormatter *formatter = [[NSDateFormatter alloc] initWithDateFormat:[@"T%H:%M:%S%z" prepareDateFormatWithTimeSeparator:timeSep] allowNaturalLanguage:NO];
-		timeString = [formatter stringForObjectValue:self];
-		[formatter release];
-	} else
-		timeString = @"";
-
-	return [NSString stringWithFormat:@"%u-W%02u-%02u%@", year, week, dayOfWeek + 1U, timeString];
-}
-- (NSString *)ISO8601OrdinalDateStringWithTime:(BOOL)includeTime timeSeparator:(unichar)timeSep {
-	NSString *timeString;
-	if(includeTime) {
-		NSDateFormatter *formatter = [[NSDateFormatter alloc] initWithDateFormat:[@"T%H:%M:%S%z" prepareDateFormatWithTimeSeparator:timeSep] allowNaturalLanguage:NO];
-		timeString = [formatter stringForObjectValue:self];
-		[formatter release];
-	} else
-		timeString = @"";
-
-	return [NSString stringWithFormat:@"%u-%03u%@", [self yearOfCommonEra], [self dayOfYear], timeString];
-}
-
-#pragma mark -
-
-- (NSString *)ISO8601DateStringWithTime:(BOOL)includeTime {
-	return [self ISO8601DateStringWithTime:includeTime timeSeparator:ISO8601UnparserDefaultTimeSeparatorCharacter];
-}
-- (NSString *)ISO8601WeekDateStringWithTime:(BOOL)includeTime {
-	return [self ISO8601WeekDateStringWithTime:includeTime timeSeparator:ISO8601UnparserDefaultTimeSeparatorCharacter];
-}
-- (NSString *)ISO8601OrdinalDateStringWithTime:(BOOL)includeTime {
-	return [self ISO8601OrdinalDateStringWithTime:includeTime timeSeparator:ISO8601UnparserDefaultTimeSeparatorCharacter];
-}
-
-#pragma mark -
-
-- (NSString *)ISO8601DateStringWithTimeSeparator:(unichar)timeSep {
-	return [self ISO8601DateStringWithTime:YES timeSeparator:timeSep];
-}
-- (NSString *)ISO8601WeekDateStringWithTimeSeparator:(unichar)timeSep {
-	return [self ISO8601WeekDateStringWithTime:YES timeSeparator:timeSep];
-}
-- (NSString *)ISO8601OrdinalDateStringWithTimeSeparator:(unichar)timeSep {
-	return [self ISO8601OrdinalDateStringWithTime:YES timeSeparator:timeSep];
-}
-
-#pragma mark -
-
-- (NSString *)ISO8601DateString {
-	return [self ISO8601DateStringWithTime:YES timeSeparator:ISO8601UnparserDefaultTimeSeparatorCharacter];
-}
-- (NSString *)ISO8601WeekDateString {
-	return [self ISO8601WeekDateStringWithTime:YES timeSeparator:ISO8601UnparserDefaultTimeSeparatorCharacter];
-}
-- (NSString *)ISO8601OrdinalDateString {
-	return [self ISO8601OrdinalDateStringWithTime:YES timeSeparator:ISO8601UnparserDefaultTimeSeparatorCharacter];
-}
-
-@end
-
-@implementation NSString(ISO8601Unparsing)
-
-//Replace all occurrences of ':' with timeSep.
-- (NSString *)prepareDateFormatWithTimeSeparator:(unichar)timeSep {
-	NSString *dateFormat = self;
-	if(timeSep != ':') {
-		NSMutableString *dateFormatMutable = [[dateFormat mutableCopy] autorelease];
-		[dateFormatMutable replaceOccurrencesOfString:@":"
-		                               	   withString:[NSString stringWithCharacters:&timeSep length:1U]
-	                                      	  options:NSBackwardsSearch | NSLiteralSearch
-	                                        	range:(NSRange){ 0U, [dateFormat length] }];
-		dateFormat = dateFormatMutable;
-	}
-	return dateFormat;
-}
-
-@end
 Parsing
 -------
 
-Call +[NSCalendarDate calendarDateWithString:myString]. The method will return either an NSCalendarDate or nil.
+Create an ISO 8601 date formatter, then call [formatter dateFromString:myString]. The method will return either an NSDate or nil.
 
-There are a total of four parser methods. The one that contains the actual parser is +[NSCalendarDate calendarDateWithString:strictly:getRange:]. The other three are based on this one.
+There are a total of six parser methods. The one that contains the actual parser is -[ISO8601DateFormatter dateComponentsFromString:timeZone:range:]. The other five are based on this one.
 
-The "strict" option, when set to YES, enforces sanity checks on the string; without it, or when set to NO, the parser will afford you quite a bit of leeway.
+The "outTimeZone" parameter, when not set to NULL, is a pointer to an NSTimeZone *variable. If the string specified a time zone, you'll receive the time zone object in that variable. If the string didn't specify a time zone, you'll receive nil.
 
 The "outRange" parameter, when not set to NULL, is a pointer to NSRange storage. You will receive the range of the parsed substring in that storage.
 
 Unparsing
 ---------
 
-When you want to unparse a calendar date to ISO 8601 date format, call [myDate ISO8601DateString].
+Create an ISO 8601 date formatter, then call [formatter stringFromDate:myDate]. The method will return a string.
 
-When you want to unparse a calendar date to ISO 8601 week-date format, call [myDate ISO8601WeekDateString].
+The formatter has several properties that control its behavior:
 
-When you want to unparse a calendar date to ISO 8601 ordinal-date format, call [myDate ISO8601OrdinalDateString].
-
-All three methods give you the time as well as the date. All three versions also come in versions that let you not get the time; for example, ISO8601DateStringWithTime:. Pass NO to not get the time.
+* You can set the format of the resulting strings. By default, the formatter will generate calendar-date strings; your other options are week dates and ordinal dates.
+* You can set a default time zone; by default, it will use [NSTimeZone defaultTimeZone].
+* You can enable a strict mode, wherein the formatter enforces sanity checks on the string. By default, the parser will afford you quite a bit of leeway.
+* You can set whether to include the time in the string, and if so, what hour-minute separator to use (default ':').
 
 How to test that this code works
 ================================
 Parsing
 -------
 
-* This method won't extract a date from just anywhere in a string, only immediately after the start of the string (or any leading whitespace). There are two solutions: either require that -calendarDateValue be invoked on a string that is only an ISO 8601 date, with nothing before or after (bad for parsing purposes), or find an ISO 8601 date as a substring. I won't do the first one, and barring a patch, I probably won't do the second one either.
+* This method won't extract a date from just anywhere in a string, only immediately after the start of the string (or any leading whitespace). There are two solutions: either require you to invoke the parser on a string that is only an ISO 8601 date, with nothing before or after (bad for parsing purposes), or make the parser able to find an ISO 8601 date as a substring. I won't do the first one, and barring a patch, I probably won't do the second one either.
 
 * Date ranges (also specified by ISO 8601) are not supported; this method will only return one date. To handle ranges would require at least one more method.
 
 #import <Foundation/Foundation.h>
-#import "NSCalendarDate+ISO8601Parsing.h"
+#import "ISO8601DateFormatter.h"
 
 int main(int argc, const char **argv) {
 	NSAutoreleasePool *pool = [NSAutoreleasePool new];
 		parseStrictly = YES;
 	}
 
+	ISO8601DateFormatter *formatter = [[[ISO8601DateFormatter alloc] init] autorelease];
+	formatter.parsesStrictly = parseStrictly;
+
 	while(--argc) {
 		NSString *str = [NSString stringWithUTF8String:*++argv];
 		NSLog(@"Parsing strictly: %hhi", parseStrictly);
-		NSDate *date = [NSCalendarDate calendarDateWithString:str strictly:parseStrictly];
+		NSDate *date = [formatter dateFromString:str];
 		fputs([[NSString stringWithFormat:@"%@ %C %@\n", str, 0x2192, date] UTF8String], stdout);
 	}
 
-#import "NSCalendarDate+ISO8601Parsing.h"
-#import "NSCalendarDate+ISO8601Unparsing.h"
+#import "ISO8601DateFormatter.h"
 
 int main(int argc, const char **argv) {
 	NSAutoreleasePool *pool = [NSAutoreleasePool new];
 
+	ISO8601DateFormatter *formatter = [[[ISO8601DateFormatter alloc] init] autorelease];
+	formatter.format = ISO8601DateFormatCalendar;
+
 	while(--argc) {
 		NSString *arg = [NSString stringWithUTF8String:*++argv];
-		printf("%s\n", [[NSString stringWithFormat:@"%@:\t%@", arg, [[NSCalendarDate calendarDateWithString:arg] ISO8601DateStringWithTime:NO]] UTF8String]);
+		NSTimeZone *timeZone = nil;
+		printf("%s\n", [[NSString stringWithFormat:@"%@:\t%@", arg, [formatter stringFromDate:[formatter dateFromString:arg timeZone:&timeZone] timeZone:timeZone]] UTF8String]);
 	}
 
 	[pool release];

unparse-ordinaldate.m

-#import "NSCalendarDate+ISO8601Parsing.h"
-#import "NSCalendarDate+ISO8601Unparsing.h"
+#import "ISO8601DateFormatter.h"
 
 int main(int argc, const char **argv) {
 	NSAutoreleasePool *pool = [NSAutoreleasePool new];
 
+	ISO8601DateFormatter *formatter = [[[ISO8601DateFormatter alloc] init] autorelease];
+	formatter.format = ISO8601DateFormatOrdinal;
+
 	while(--argc) {
 		NSString *arg = [NSString stringWithUTF8String:*++argv];
-		printf("%s\n", [[NSString stringWithFormat:@"%@:\t%@", arg, [[NSCalendarDate calendarDateWithString:arg] ISO8601OrdinalDateStringWithTime:NO]] UTF8String]);
+		NSTimeZone *timeZone = nil;
+		printf("%s\n", [[NSString stringWithFormat:@"%@:\t%@", arg, [formatter stringFromDate:[formatter dateFromString:arg timeZone:&timeZone] timeZone:timeZone]] UTF8String]);
 	}
 
 	[pool release];

unparse-weekdate.m

-#import "NSCalendarDate+ISO8601Parsing.h"
-#import "NSCalendarDate+ISO8601Unparsing.h"
+#import "ISO8601DateFormatter.h"
 
 int main(int argc, const char **argv) {
 	NSAutoreleasePool *pool = [NSAutoreleasePool new];
 
+	ISO8601DateFormatter *formatter = [[[ISO8601DateFormatter alloc] init] autorelease];
+	formatter.format = ISO8601DateFormatWeek;
+
 	while(--argc) {
 		NSString *arg = [NSString stringWithUTF8String:*++argv];
-		printf("%s\n", [[NSString stringWithFormat:@"%@:\t%@", arg, [[NSCalendarDate calendarDateWithString:arg] ISO8601WeekDateStringWithTime:NO]] UTF8String]);
+		NSTimeZone *timeZone = nil;
+		printf("%s\n", [[NSString stringWithFormat:@"%@:\t%@", arg, [formatter stringFromDate:[formatter dateFromString:arg timeZone:&timeZone] timeZone:timeZone]] UTF8String]);
 	}
 
 	[pool release];
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.