日期格式化器 <- 数据格式化指南
你可以使用dateFromString:方法来创建一个代表日期的字符串,你也可以使用stringFromDate:方法把字符串解析为一个日期对象。你还可以使用getObjectValue:forString:range:error:方法对解析的字符串的范围有更多控制,
日期格式化器中有很多可读写的属性。当你要向用户显示信息的时候,你通常只需要使用NSDateFormatter样式常量即可。该常量预定义了可以决定如何格式化显示日期的属性。但是,如果你想要生成一个精确格式的日期,你应该使用格式字符串(format string)。
如果你需要解析日期字符串,你采用的方法取决于你想要完成的任务。如果你想要解析用户的输入,你通常使用样式常量以便匹配他们的期望。如果你想解析从数据库或网络服务器得到的日期,你应该使用格式字符串。
在所有的情况中,你都应该考虑到格式化器使用用户区域(currentLocale)在用户偏好设置中叠加默认值。如果你想使用用户的区域,但却没有它们独特的设置时,你可以通过当前用户的区域(localIdentifier)来获取一个区域id,并用它来只做一个新的“标准”区域,然后把该标准区域设置为格式化器的locale。
使用格式化器样式来呈现用户偏好的日期和时间
NSDateFormatter可以让你很容易的使用系统偏好的“国际偏好”面板中的设置来格式化日期。NSDateFormatter的样式常量(NSDateFormatter style constants—NSDateFormatterNoStyle, NSDateFormatterShortStyle, NSDateFormatterMediumStyle, NSDateFormatterLongStyle, 和 NSDateFormatterFullStyle)指定一系列属性,这些属性根据用户的偏好决定如何显示日期。
你要分别使用setDateStyle:和setTimeStyle:方法为日期格式化器的组建指定日期和时间的格式。代码清单 1展示了你如何使用格式化器样式格式化一个日期。注意,使用NSDateFormatterNoStyle会抑制时间组件,并产生只包含日期的字符串。
代码清单 1 使用格式化器样式格式化一个日期
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateStyle:NSDateFormatterMediumStyle];
[dateFormatter setTimeStyle:NSDateFormatterNoStyle];
NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:162000];
NSString *formattedDateString = [dateFormatter stringFromDate:date];
NSLog(@"formattedDateString: %@", formattedDateString);
// Output for locale en_US: "formattedDateString: Jan 2, 2001".
使用格式字符串来指定自定义格式
一般来说,有两种情况你需要使用自定义格式:
- 对于固定格式字符串,例如网络日期。
- 对于和任何现有样式都不匹配的用户可见的元素。
固定格式
想要为日期格式化器指定一个自定义的固定格式,你要使用setDateFormat:。格式字符串使用来自Unicode Technical Standard #35的格式模式。不同的操作系统版本使用不同标准的版本:
- OS X v10.9 和 iOS 7 使用 version tr35-31.
- OS X v10.8 和 iOS 6 使用 version tr35-25.
- iOS 5 使用 version tr35-19.
- OS X v10.7 和 iOS 4.3 使用 version tr35-17.
- iOS 4.0, iOS 4.1, 和 iOS 4.2 使用 version tr35-15.
- iOS 3.2 使用 version tr35-12.
- OS X v10.6, iOS 3.0, 和 iOS 3.1 使用 version tr35-10.
- OS X v10.5 使用 version tr35-6.
- OS X v10.4 使用 version tr35-4.
虽然原则上一个格式字符串指定一个固定格式,但是默认情况下,NSDateFormatter让人会考虑用户的偏好(包括区域设置)。当使用格式字符串的时候,你必须考虑下面几点:
- NSDateFormatter会以用户选中的日历的方式处理你所解析的字符串中的数字。例如,如果用户选中了Buddhist日历,那么Gregorian日历的1467会被解析生成为2010的NSDate对象。(更多关于不同日历系统和如何使用它们的信息,参见Date and Time Programming Guide。)
- 在iOS中,用户可以重写默认的AM/PM与24小时的时间设置。这可能导致你需要重写你设置的格式字符串。
注意Unicode格式字符串的格式,你应该在格式字符串中的字面量放在两个撇号之间('')。
下面的例子说明了使用格式字符串生成一个字符串:
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:@"yyyy-MM-dd 'at' HH:mm"];
NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:162000];
NSString *formattedDateString = [dateFormatter stringFromDate:date];
NSLog(@"formattedDateString: %@", formattedDateString);
// For US English, the output may be:
// formattedDateString: 2001-01-02 at 13:00
这个例子要注意两点:
- 它使用yyyy来指定年分组件。一个常见的错误是使用YYYY。yyyy值得年是日历年,而YYYY指的年是ISO的年-周(year-week)日历。d大多数情况下,yyyy和YYYY产生同样的结果,但是它们也可能会不同。通常,你应该使用日历年。
- 时间的表示法可能是13:00。但是在iOS中,用户可能把24小时制关闭,那么时间的现实可能是1:00 pm。
显示给用户的日期自定义格式
想要显示一个包含特定元素设置的日期,要使用dateFormatFromTemplate:options:locale:方法。该方法生成你想使用的日期组件的格式字符串,但是要使用正确的标点和恰当的顺序(也就是,针对用户的区域和偏好定制)。然后你使用格式字符串创建格式化器。
例如,想要使用当前的区域创建格式化器来显示今天的星期、日、以及月,你可以这样写:
NSString *formatString = [NSDateFormatter dateFormatFromTemplate:@"EdMMM" options:0 locale:[NSLocale currentLocale]];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setDateFormat:formatString];
NSString *todayString = [dateFormatter stringFromDate:[NSDate date]];
NSLog(@"todayString: %@", todayString);
想要理解这种需要,你要考虑要在何处显示星期、日、以及月。你不能使用格式化器样式(没有可以忽略年的样式)来创建日期的这种表达。但是,使用格式字符串可以方便的始终如一的创建正确的表示法。虽然一开始它看上去比较简单,但是也有复杂的地方:来自美国的用户通常期望的日期格式是“Mon, Jan 3”,然而来自英国的用户通常期望的日期格式是“Mon 31 Jan”。
下面这个例子说明了这一点:
NSLocale *usLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
NSString *usFormatString = [NSDateFormatter dateFormatFromTemplate:@"EdMMM" options:0 locale:usLocale];
NSLog(@"usFormatterString: %@", usFormatString);
// Output: usFormatterString: EEE, MMM d.
NSLocale *gbLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"];
NSString *gbFormatString = [NSDateFormatter dateFormatFromTemplate:@"EdMMM" options:0 locale:gbLocale];
NSLog(@"gbFormatterString: %@", gbFormatString);
// Output: gbFormatterString: EEE d MMM.
解析日期字符串
除了继承于NSFormatter的方法(例如getObjectValue:forString:errorDescription:)之外,NSDateFormatter添加了dateFromString: 和 getObjectValue:forString:range:error:方法。这两个方法可以让你很方便的在代码中直接使用NSDateFormatter对象,并且可以比NSString格式化更复杂更方便的方式将日期格式化成字符串。
getObjectValue:forString:range:error:方法允许你指定字符串的子串来进行解析,它返回被真实解析了的子串(在错误的情况下,它会指出发生错误的区域)。它还返回一个NSError对象,该对象包含比getObjectValue:forString:errorDescription:(继承子NSFormatter)的错误字符串更丰富的信息。
如果你是用固定格式日期,你应该首先要设置日期格式化器的locale属性,用以匹配你的固定格式。大多数情况下,locale最好选择en_US_POSIX,它专门设计用来产出美国英语结果,无论用户和系统偏好如何。en_US_POSIX还不可变(如果美国在未来改变了格式日期的方式,en_US会做相应改变,但是en_US_POSIX不会),且跨平台(en_US_POSIX在iOS、OS X、以及其他平台上的表现是一样的)。
一旦你把en_US_POSIX作为日期格式化器的locale,你就可以设置格式字符串,日期格式化器为用户提供一致的行为。
代码清单 2 展示了如何使用NSDateFormatter解决上述两个任务。首先创建en_US_POSIX日期格式化器来解析RFC 3339日期字符串,使用固定日期格式字符串和UTC时区。然后,它创建一个标准的日期格式化器来,已将日期转化为字符串呈现给用户。
代码清单 2 解析RFC 3339日期时间
- (NSString *)userVisibleDateTimeStringForRFC3339DateTimeString:(NSString *)rfc3339DateTimeString {
/*
Returns a user-visible date time string that corresponds to the specified
RFC 3339 date time string. Note that this does not handle all possible
RFC 3339 date time strings, just one of the most common styles.
*/
NSDateFormatter *rfc3339DateFormatter = [[NSDateFormatter alloc] init];
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
[rfc3339DateFormatter setLocale:enUSPOSIXLocale];
[rfc3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
[rfc3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
// Convert the RFC 3339 date time string to an NSDate.
NSDate *date = [rfc3339DateFormatter dateFromString:rfc3339DateTimeString];
NSString *userVisibleDateTimeString;
if (date != nil) {
// Convert the date object to a user-visible date string.
NSDateFormatter *userVisibleDateFormatter = [[NSDateFormatter alloc] init];
assert(userVisibleDateFormatter != nil);
[userVisibleDateFormatter setDateStyle:NSDateFormatterShortStyle];
[userVisibleDateFormatter setTimeStyle:NSDateFormatterShortStyle];
userVisibleDateTimeString = [userVisibleDateFormatter stringFromDate:date];
}
return userVisibleDateTimeString;
}
为了效率缓存格式化器
创建日期格式化器的操作会耗费一定资源。如果你频繁使用格式化器,通常缓存一个单例要比创建和处理多个实例要更有效率。其中一种方式是使用static变量。
代码清单 3 重新实现了在代码清单 2的方法,用以持有日期格式化器方便以后重用。
代码清单 3 使用缓存的格式化器来解析RFC 3339日期时间
static NSDateFormatter *sUserVisibleDateFormatter = nil;
- (NSString *)userVisibleDateTimeStringForRFC3339DateTimeString:(NSString *)rfc3339DateTimeString {
/*
Returns a user-visible date time string that corresponds to the specified
RFC 3339 date time string. Note that this does not handle all possible
RFC 3339 date time strings, just one of the most common styles.
*/
// If the date formatters aren't already set up, create them and cache them for reuse.
static NSDateFormatter *sRFC3339DateFormatter = nil;
if (sRFC3339DateFormatter == nil) {
sRFC3339DateFormatter = [[NSDateFormatter alloc] init];
NSLocale *enUSPOSIXLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
[sRFC3339DateFormatter setLocale:enUSPOSIXLocale];
[sRFC3339DateFormatter setDateFormat:@"yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"];
[sRFC3339DateFormatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:0]];
}
// Convert the RFC 3339 date time string to an NSDate.
NSDate *date = [rfc3339DateFormatter dateFromString:rfc3339DateTimeString];
NSString *userVisibleDateTimeString;
if (date != nil) {
if (sUserVisibleDateFormatter == nil) {
sUserVisibleDateFormatter = [[NSDateFormatter alloc] init];
[sUserVisibleDateFormatter setDateStyle:NSDateFormatterShortStyle];
[sUserVisibleDateFormatter setTimeStyle:NSDateFormatterShortStyle];
}
// Convert the date object to a user-visible date string.
userVisibleDateTimeString = [sUserVisibleDateFormatter stringFromDate:date];
}
return userVisibleDateTimeString;
}
如果你缓存了日期格式化器(或者其他任何基于用户当前区域的对象),你应该订阅NSCurrentLocaleDidChangeNotification通知,并在当前区域改变的时候更新你的缓存对象。代码清单 3中的代码在方法之外定义了sUserVisibleDateFormatter,以便其他代码(未显示)可以在必要时更新它。相反,sRFC3339DateFormatterDateFormatter在方法内被定义,根据设计,它不依赖于用户的区域设置。
注意:理论上,你可以使用自动更新区域(autoupdatingCurrentLocale)来创建区域,该区域会根据用户的区域设置改变而自动改变。在实践中,它当前还不用于日期格式化器。
考虑固定格式化和非本地化日期的Unix函数
对于在固定的、非本地化格式中的日期和时间,它们总是可以使用相同的日历,有时使用标准C库函数strptime_1 和 strftime_1或许更容易也更有效率。
要注意,C库也有当前区域的概念。要想保证固定日期格式,你应该给这些程序的loc参数传递NULL。这会让它们使用POSIX区域(也被称为C区域),这与Cocoa的en_US_POSIX是等价的。下例说明了这一点。
struct tm sometime;
const char *formatString = "%Y-%m-%d %H:%M:%S %z";
(void) strptime_l("2005-07-01 12:00:00 -0700", formatString, &sometime, NULL);
NSLog(@"NSDate is %@", [NSDate dateWithTimeIntervalSince1970: mktime(&sometime)]);
// Output: NSDate is 2005-07-01 12:00:00 -0700