Fetching contributors…
Cannot retrieve contributors at this time
242 lines (223 sloc) 12.6 KB
/*ISO8601DateFormatter.h
*
*Created by Peter Hosey on 2009-04-11.
*Copyright 2009–2016 Peter Hosey. All rights reserved.
*/
#import <Foundation/Foundation.h>
#if __has_feature(nullability)
#define ISO8601_NULLABLE _Nullable
#define ISO8601_NONNULL _Nonnull
#else
#define ISO8601_NULLABLE /*can be NULL*/
#define ISO8601_NONNULL /*never NULL*/
#endif
///Which of ISO 8601's three date formats the formatter should produce.
typedef NS_ENUM(NSUInteger, ISO8601DateFormat) {
///YYYY-MM-DD.
ISO8601DateFormatCalendar,
///YYYY-DDD, where DDD ranges from 1 to 366; for example, 2009-32 is 2009-02-01.
ISO8601DateFormatOrdinal,
///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.
ISO8601DateFormatWeek,
};
///The default separator for time values. Currently, this is ':'.
extern const unichar ISO8601DefaultTimeSeparatorCharacter;
/*!
* @brief This class converts dates to and from ISO 8601 strings.
*
* @details TL;DR: You want to use ISO 8601 for any and all dates you send or receive over the internet, unless the spec for the protocol or format you're working with specifically tells you otherwise. See http://xkcd.com/1179/ .
*
* ISO 8601 is most recognizable as “year-month-date” strings, such as “2013-09-08T15:06:11-0800”. Of course, as you might expect of a formal standard, it's more sophisticated (some might say complicated) than that.
*
* For one thing, ISO 8601 actually defines *three* different date formats. The most common one, shown above, is called the calendar date format. The other two are week dates, where the month is replaced by a week of the year and the day is a day-of-the-week (1 being Monday) rather than day-of-month, and ordinal dates, where the middle segment is dispensed with entirely and the day component is day-of-year (1–366).
*
* The week format is the most bizarre of them, since 7 × 52 ≠ 365. The start and end of the year for purposes of week dates usually don't line up with the start and end of the calendar year. As a result, the first and/or last day of a year in the week-date “calendar” more often than not is on a different year from the first and/or last day of that year on the actual Gregorian calendar.
*
* In practice, almost all ISO 8601 dates you see in the wild will be in the calendar format.
*
* At any rate, this formatter can both parse and unparse dates in all three formats. (By “unparse”, I mean “produce a string from”—the reverse of parsing.)
*
* For a good and more detailed introduction to ISO 8601, see [“A summary of the international standard date and time notation” by Markus Kuhn](http://www.cl.cam.ac.uk/~mgk25/iso-time.html). The actual standard itself can be found in PDF format online with a well-crafted web search.
*/
@interface ISO8601DateFormatter: NSFormatter
{
NSString *lastUsedFormatString;
NSDateFormatter *unparsingFormatter;
NSCalendar *parsingCalendar, *unparsingCalendar;
NSTimeZone *defaultTimeZone;
ISO8601DateFormat format;
unichar timeSeparator;
unichar timeZoneSeparator;
BOOL includeTime;
BOOL useMillisecondPrecision;
BOOL parsesStrictly;
}
@property(nonatomic, retain) NSTimeZone *ISO8601_NULLABLE defaultTimeZone;
#pragma mark Parsing
/*!
* @name Parsing
*/
//As a formatter, this object converts strings to dates.
/*!
* @brief Disables various leniencies in how the formatter parses strings.
*
* @details By default, the parser allows these extensions to the ISO 8601 standard:
*
* - Whitespace is allowed before the date.
* - Numbers don't have to be within range for a component. For example, you can have a string that refers to the 56th day of the hundredth month.
* - Since 0.6, allows a single whitespace character before the time-zone specification. This extension provides compatibility with NSDate output (`description`) and input (`dateWithString:`).
* - “Superfluous” hyphens in date specifications such as “`--DDD`” (where DDD is an ordinal day-of-year number and the year is implied) are allowed. (The standard recommends writing such a date as “`-DDD`”, with only a single hyphen. This is consistent with ordinal dates having only two components: the year and the day-of-year, separated by one hyphen.)
* - The same goes for week dates such as “`--Www-DD`”. Again, the extra hyphen really is superfluous, but is allowed as an extension.
* - Calendar dates with no separator between month and day-of-month are allowed, at least when they total four digits. (For example, 2013-0908, which would be interpreted as 2013-09-08.)
* - Single-digit components are allowed. (If you wish to specify a date in a single-digit year with the strict parser, pad it with zeroes.)
* - “YYYY-W” (without a week number after the 'W') is allowed, interpreted as “YYYY-W01-01”.
*
* Setting this property to `YES` will disable all of those extensions.
*
* These extensions are intended to help you process degenerate input (received from programs and services that use broken date-formatting libraries); whenever *you* create ISO 8601 strings, you should generate strictly-conforming ones.
*
* This property does not affect unparsing. The formatter always creates valid ISO 8601 strings. Any invalid string (loosely, any string that would require turning this property off to re-parse) should be considered a bug; please report it.
*/
@property BOOL parsesStrictly;
/*!
* @brief Parse a string into individual date components.
*
* @param string The string to parse. Must represent a date in one of the ISO 8601 formats.
* @result An NSDateComponents object containing most of the information parsed from the string, aside from the fraction of second and time zone (which are lost).
* @sa dateComponentsFromString:timeZone:
* @sa dateComponentsFromString:timeZone:range:fractionOfSecond:
*/
- (NSDateComponents *ISO8601_NULLABLE) dateComponentsFromString:(NSString *ISO8601_NONNULL)string;
/*!
* @brief Parse a string into individual date components.
*
* @param string The string to parse. Must represent a date in one of the ISO 8601 formats.
* @param outTimeZone If non-`NULL`, an NSTimeZone object or `nil` will be stored here, depending on whether the string specified a time zone.
* @result An NSDateComponents object containing most of the information parsed from the string, aside from the fraction of second (which is lost) and time zone.
* @sa dateComponentsFromString:
* @sa dateComponentsFromString:timeZone:range:fractionOfSecond:
*/
- (NSDateComponents *ISO8601_NULLABLE) dateComponentsFromString:(NSString *ISO8601_NONNULL)string timeZone:(out NSTimeZone *ISO8601_NULLABLE *ISO8601_NULLABLE)outTimeZone;
/*!
* @brief Parse a string into individual date components.
*
* @param string The string to parse. Must represent a date in one of the ISO 8601 formats.
* @param outTimeZone If non-`NULL`, an NSTimeZone object or `nil` will be stored here, depending on whether the string specified a time zone.
* @param outRange If non-`NULL`, an NSRange structure will be stored here, identifying the substring of `string` that specified the date.
* @param outFractionOfSecond If non-`NULL`, an NSTimeInterval value will be stored here, containing the fraction of a second, if the string specified one. If it didn't, this will be set to zero.
* @result An NSDateComponents object containing most of the information parsed from the string, aside from the fraction of second and time zone.
* @sa dateComponentsFromString:
* @sa dateComponentsFromString:timeZone:
*/
- (NSDateComponents *ISO8601_NULLABLE) dateComponentsFromString:(NSString *ISO8601_NONNULL)string timeZone:(out NSTimeZone *ISO8601_NULLABLE *ISO8601_NULLABLE)outTimeZone range:(out NSRange *ISO8601_NULLABLE)outRange fractionOfSecond:(NSTimeInterval *ISO8601_NULLABLE)outFractionOfSecond;
/*!
* @brief Parse a string.
*
* @param string The string to parse. Must represent a date in one of the ISO 8601 formats.
* @result An NSDate object containing most of the information parsed from the string, aside from the time zone (which is lost).
* @details This method will use self.defaultTimeZone to resolve the components to a date if the string does not specify a time zone.
* @sa dateComponentsFromString:
* @sa dateFromString:timeZone:
* @sa dateFromString:timeZone:range:
*/
- (NSDate *ISO8601_NULLABLE) dateFromString:(NSString *ISO8601_NONNULL)string;
/*!
* @brief Parse a string.
*
* @param string The string to parse. Must represent a date in one of the ISO 8601 formats.
* @param outTimeZone If non-`NULL`, an NSTimeZone object or `nil` will be stored here, depending on whether the string specified a time zone.
* @result An NSDate object containing most of the information parsed from the string, aside from the time zone.
* @details This method will use self.defaultTimeZone to resolve the components to a date if the string does not specify a time zone.
* @sa dateComponentsFromString:timeZone:
* @sa dateFromString:
* @sa dateFromString:timeZone:range:
*/
- (NSDate *ISO8601_NULLABLE) dateFromString:(NSString *ISO8601_NONNULL)string timeZone:(out NSTimeZone *ISO8601_NULLABLE *ISO8601_NULLABLE)outTimeZone;
/*!
* @brief Parse a string into a single date, identified by an NSDate object.
*
* @param string The string to parse. Must represent a date in one of the ISO 8601 formats.
* @param outTimeZone If non-`NULL`, an NSTimeZone object or `nil` will be stored here, depending on whether the string specified a time zone.
* @param outRange If non-`NULL`, an NSRange structure will be stored here, identifying the substring of `string` that specified the date.
* @result An NSDate object containing most of the information parsed from the string, aside from the time zone.
* @details This method will use self.defaultTimeZone to resolve the components to a date if the string does not specify a time zone.
* @sa dateComponentsFromString:timeZone:range:fractionOfSecond:
* @sa dateFromString:
* @sa dateFromString:timeZone:
*/
- (NSDate *ISO8601_NULLABLE) dateFromString:(NSString *ISO8601_NONNULL)string timeZone:(out NSTimeZone *ISO8601_NULLABLE *ISO8601_NULLABLE)outTimeZone range:(out NSRange *ISO8601_NULLABLE)outRange;
#pragma mark Unparsing
/*!
* @name Unparsing
*/
/*!
* @brief Which ISO 8601 format to format dates in.
*
* @details See ISO8601DateFormat for possible values.
*/
@property ISO8601DateFormat format;
/*!
* @brief Whether strings should include time of day.
*
* @details If `NO`, strings include only the date, nothing after it.
*
* @sa timeSeparator
* @sa timeZoneSeparator
*/
@property BOOL includeTime;
/*!
* @brief Whether strings should include millisecond precision time.
*
* @details If `YES`, strings include three millisecond digits. Only has an effect if `includeTime` is `YES`
*
*/
@property BOOL useMillisecondPrecision;
/*!
* @brief The character to use to separate components of the time of day.
*
* @details This is used in both parsing and unparsing.
*
* The default value is ISO8601DefaultTimeSeparatorCharacter.
*
* When parsesStrictly is set to `YES`, this property is ignored. Otherwise, the parser will raise an exception if this is set to zero.
*
* @sa includeTime
* @sa timeZoneSeparator
*/
@property unichar timeSeparator;
/*!
* @brief The character to use to separate the hour and minute in a time zone specification.
*
* @details This is used in both parsing and unparsing.
*
* If zero, no separator is inserted into time zone specifications.
*
* The default value is zero (no separator).
*
* @sa includeTime
* @sa timeSeparator
*/
@property unichar timeZoneSeparator;
/*!
* @brief Produce a string that represents a date in UTC.
*
* @param date The string to parse. Must represent a date in one of the ISO 8601 formats.
* @result A string that represents the date in UTC. Can be `nil` in some cases; see `stringFromDate:timeZone:` for details.
* @sa stringFromDate:timeZone:
*/
- (NSString *ISO8601_NULLABLE) stringFromDate:(NSDate *ISO8601_NONNULL)date;
/*!
* @brief Produce a string that represents a date.
*
* @param date The string to parse. Must represent a date in one of the ISO 8601 formats.
* @param timeZone An NSTimeZone object identifying the time zone in which to specify the date.
* @result A string that represents the date in the requested time zone, if possible.
*
* @details Not all dates are representable in all time zones (because of historical calendar changes, such as transitions from the Julian to the Gregorian calendar).
* For an example, see http://stackoverflow.com/questions/18663407/date-formatter-returns-nil-for-june .
* This method *should* return `nil` in such cases.
*
* @sa stringFromDate:
*/
- (NSString *ISO8601_NULLABLE) stringFromDate:(NSDate *ISO8601_NONNULL)date timeZone:(NSTimeZone *ISO8601_NONNULL)timeZone;
@end