处理 NSDateFormatter 语言环境“特性”的最佳方法是什么?

看起来 NSDateFormatter有一个出乎意料的“特性”: 如果你做一个简单的“固定”格式的操作,比如:

NSDateFormatter* fmt = [[NSDateFormatter alloc] init];
[fmt setDateFormat:@"yyyyMMddHHmmss"];
NSString* dateStr = [fmt stringFromDate:someDate];
[fmt release];

然后,它在美国和大多数地区工作良好,直到... 有人与他们的手机设置为24小时区域设置12/24小时开关设置为12。然后,上面的代码开始在结果字符串的末尾添加“ AM”或“ PM”。

(见 NSDateFormatter,是我做错了什么,还是这是一个错误?)

(请参阅 https://developer.apple.com/library/content/qa/qa1480/_index.html)

显然,苹果公司已经宣布这是“坏的”-破碎的设计,他们不会去修复它。

这种规避显然是为特定地区(通常是美国)设置日期格式化程序的位置,但这有点混乱:

NSLocale *loc = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
[df setLocale: loc];
[loc release];

穿连体衣还不错,但我正在处理大约10个不同的应用程序,我看到的第一个应用程序有43个这种情况的实例。

那么,对于宏/重写类/其他类,有没有什么聪明的想法可以减少更改所有内容的工作量,同时又不会使代码变得模糊不清呢?(我的第一反应是用一个在 init 方法中设置区域设置的版本覆盖 NSDateFormatter。需要更改两行—— alloc/init 行和添加的 import。)

加上

这是我到目前为止想到的——似乎在所有情况下都有效:

@implementation BNSDateFormatter


-(id)init {
static NSLocale* en_US_POSIX = nil;
NSDateFormatter* me = [super init];
if (en_US_POSIX == nil) {
en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
[me setLocale:en_US_POSIX];
return me;
}


@end

更新 关于 OMZ 的建议,以下是我的发现——

下面是类别版本—— h 文件:

#import <Foundation/Foundation.h>




@interface NSDateFormatter (Locale)
- (id)initWithSafeLocale;
@end

类别 m 档案:

#import "NSDateFormatter+Locale.h"




@implementation NSDateFormatter (Locale)


- (id)initWithSafeLocale {
static NSLocale* en_US_POSIX = nil;
self = [super init];
if (en_US_POSIX == nil) {
en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
NSLog(@"Category's locale: %@ %@", en_US_POSIX.description, [en_US_POSIX localeIdentifier]);
[self setLocale:en_US_POSIX];
return self;
}


@end

密码:

NSDateFormatter* fmt;
NSString* dateString;
NSDate* date1;
NSDate* date2;
NSDate* date3;
NSDate* date4;


fmt = [[NSDateFormatter alloc] initWithSafeLocale];
[fmt setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
dateString = [fmt stringFromDate:[NSDate date]];
NSLog(@"dateString = %@", dateString);
date1 = [fmt dateFromString:@"2001-05-05 12:34:56"];
NSLog(@"date1 = %@", date1.description);
date2 = [fmt dateFromString:@"2001-05-05 22:34:56"];
NSLog(@"date2 = %@", date2.description);
date3 = [fmt dateFromString:@"2001-05-05 12:34:56PM"];
NSLog(@"date3 = %@", date3.description);
date4 = [fmt dateFromString:@"2001-05-05 12:34:56 PM"];
NSLog(@"date4 = %@", date4.description);
[fmt release];


fmt = [[BNSDateFormatter alloc] init];
[fmt setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
dateString = [fmt stringFromDate:[NSDate date]];
NSLog(@"dateString = %@", dateString);
date1 = [fmt dateFromString:@"2001-05-05 12:34:56"];
NSLog(@"date1 = %@", date1.description);
date2 = [fmt dateFromString:@"2001-05-05 22:34:56"];
NSLog(@"date2 = %@", date2.description);
date3 = [fmt dateFromString:@"2001-05-05 12:34:56PM"];
NSLog(@"date3 = %@", date3.description);
date4 = [fmt dateFromString:@"2001-05-05 12:34:56 PM"];
NSLog(@"date4 = %@", date4.description);
[fmt release];

结果是:

2011-07-11 17:44:43.243 DemoApp[160:307] Category's locale: <__NSCFLocale: 0x11a820> en_US_POSIX
2011-07-11 17:44:43.257 DemoApp[160:307] dateString = 2011-07-11 05:44:43 PM
2011-07-11 17:44:43.264 DemoApp[160:307] date1 = (null)
2011-07-11 17:44:43.272 DemoApp[160:307] date2 = (null)
2011-07-11 17:44:43.280 DemoApp[160:307] date3 = (null)
2011-07-11 17:44:43.298 DemoApp[160:307] date4 = 2001-05-05 05:34:56 PM +0000
2011-07-11 17:44:43.311 DemoApp[160:307] Extended class's locale: <__NSCFLocale: 0x11a820> en_US_POSIX
2011-07-11 17:44:43.336 DemoApp[160:307] dateString = 2011-07-11 17:44:43
2011-07-11 17:44:43.352 DemoApp[160:307] date1 = 2001-05-05 05:34:56 PM +0000
2011-07-11 17:44:43.369 DemoApp[160:307] date2 = 2001-05-06 03:34:56 AM +0000
2011-07-11 17:44:43.380 DemoApp[160:307] date3 = (null)
2011-07-11 17:44:43.392 DemoApp[160:307] date4 = (null)

这款手机设置为大不列颠,12/24开关设置为12。这两个结果有明显的不同,我认为分类版本是错误的。请注意,类别版本中的日志正在执行(并且代码中的停止被命中) ,所以这不仅仅是代码以某种方式没有得到使用的情况。

奇怪的观察

略微修改了类别的实施:

#import "NSDateFormatter+Locale.h"


@implementation NSDateFormatter (Locale)


- (id)initWithSafeLocale {
static NSLocale* en_US_POSIX2 = nil;
self = [super init];
if (en_US_POSIX2 == nil) {
en_US_POSIX2 = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
NSLog(@"Category's locale: %@ %@", en_US_POSIX2.description, [en_US_POSIX2 localeIdentifier]);
[self setLocale:en_US_POSIX2];
NSLog(@"Category's object: %@ and object's locale: %@ %@", self.description, self.locale.description, [self.locale localeIdentifier]);
return self;
}


@end

基本上只是更改了静态语言环境变量的名称(以防与子类中声明的静态发生冲突) ,并添加了额外的 NSLog。但看看 NSLog 记录了什么:

2011-07-15 16:35:24.322 DemoApp[214:307] Category's locale: <__NSCFLocale: 0x160550> en_US_POSIX
2011-07-15 16:35:24.338 DemoApp[214:307] Category's object: <NSDateFormatter: 0x160d90> and object's locale: <__NSCFLocale: 0x12be70> en_GB
2011-07-15 16:35:24.345 DemoApp[214:307] dateString = 2011-07-15 04:35:24 PM
2011-07-15 16:35:24.370 DemoApp[214:307] date1 = (null)
2011-07-15 16:35:24.378 DemoApp[214:307] date2 = (null)
2011-07-15 16:35:24.390 DemoApp[214:307] date3 = (null)
2011-07-15 16:35:24.404 DemoApp[214:307] date4 = 2001-05-05 05:34:56 PM +0000

正如您所看到的,setLocale 并没有这样做。格式化程序的区域设置仍然是 en _ GB。似乎类别中的 init 方法有些“奇怪”。

47332 次浏览

您可以使用一个附加的初始化器创建一个 NSDateFormatter类别,而不是子类化,这个初始化器负责分配语言环境,可能还有一个格式字符串,这样在初始化之后就有了一个现成可用的格式化程序。

@interface NSDateFormatter (LocaleAdditions)


- (id)initWithPOSIXLocaleAndFormat:(NSString *)formatString;


@end


@implementation NSDateFormatter (LocaleAdditions)


- (id)initWithPOSIXLocaleAndFormat:(NSString *)formatString {
self = [super init];
if (self) {
NSLocale *locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
[self setLocale:locale];
[locale release];
[self setFormat:formatString];
}
return self;
}


@end

然后你可以在代码的任何地方使用 NSDateFormatter,只需:

NSDateFormatter* fmt = [[NSDateFormatter alloc] initWithPOSIXLocaleAndFormat:@"yyyyMMddHHmmss"];

您可能希望以某种方式为分类方法添加前缀,以避免名称冲突,以防苹果决定在未来版本的操作系统中添加这样的方法。

如果您总是使用相同的日期格式,您还可以添加类别方法来返回具有某些配置(比如 +sharedRFC3339DateFormatter)的单例实例。但是请注意,NSDateFormatter不是线程安全的,当您从多个线程使用相同的实例时,必须使用锁或 @synchronized块。

当然!

有时候你会发出“啊哈! !”的声音那一刻,有时候更像是一声“切! !”这是后者。在 initWithSafeLocale的类别中,“超级”init被编码为 self = [super init];。这将初始化 NSDateFormatter的 SUPERClass,但不会初始化 NSDateFormatter对象本身。

显然,当跳过这个初始化时,setLocale会“弹出”,可能是因为对象中缺少了一些数据结构。将 init更改为 self = [self init];将导致 NSDateFormatter初始化发生,并且 setLocale再次高兴起来。

下面是这个类别的. m 的“最终”来源:

#import "NSDateFormatter+Locale.h"


@implementation NSDateFormatter (Locale)


- (id)initWithSafeLocale {
static NSLocale* en_US_POSIX = nil;
self = [self init];
if (en_US_POSIX == nil) {
en_US_POSIX = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
}
[self setLocale:en_US_POSIX];
return self;
}


@end

请允许我提出一些完全不同的建议,因为说实话,所有这些都有点像是掉进了兔子洞。

您应该使用一个 NSDateFormatterdateFormat集和 locale强制 en_US_POSIX接收日期(从服务器/API)。

然后你应该使用一个不同的 NSDateFormatter作为界面,你将设置 timeStyle/dateStyle属性-这样你就没有一个明确的 dateFormat自己设置,从而错误地假设该格式将被使用。

这意味着用户界面是由用户偏好驱动的(上午/下午 vs 24小时,日期字符串格式正确到用户选择-从 iOS 设置) ,而日期是“进入”你的应用程序总是正确地被“解析”到一个 NSDate供你使用。

这里有一个针对这个问题的快速版本的解决方案。在快速版本中,我们可以使用扩展而不是分类。 因此,这里我已经为 DateFormatter 创建了扩展,并在 initWithSafeLocale 中创建了扩展 返回带有相关语言环境的 DateFormatter,在我们的示例中是 en _ US _ POSIX,除此之外还提供了几个日期形成方法。

  • Swift 4

    extension DateFormatter {
    
    
    private static var dateFormatter = DateFormatter()
    
    
    class func initWithSafeLocale(withDateFormat dateFormat: String? = nil) -> DateFormatter {
    
    
    dateFormatter = DateFormatter()
    
    
    var en_US_POSIX: Locale? = nil;
    
    
    if (en_US_POSIX == nil) {
    en_US_POSIX = Locale.init(identifier: "en_US_POSIX")
    }
    dateFormatter.locale = en_US_POSIX
    
    
    if dateFormat != nil, let format = dateFormat {
    dateFormatter.dateFormat = format
    }else{
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    }
    return dateFormatter
    }
    
    
    // ------------------------------------------------------------------------------------------
    
    
    class func getDateFromString(string: String, fromFormat dateFormat: String? = nil) -> Date? {
    
    
    if dateFormat != nil, let format = dateFormat {
    dateFormatter = DateFormatter.initWithSafeLocale(withDateFormat: format)
    }else{
    dateFormatter = DateFormatter.initWithSafeLocale()
    }
    guard let date = dateFormatter.date(from: string) else {
    return nil
    }
    return date
    }
    
    
    // ------------------------------------------------------------------------------------------
    
    
    class func getStringFromDate(date: Date, fromDateFormat dateFormat: String? = nil)-> String {
    
    
    if dateFormat != nil, let format = dateFormat {
    dateFormatter = DateFormatter.initWithSafeLocale(withDateFormat: format)
    }else{
    dateFormatter = DateFormatter.initWithSafeLocale()
    }
    
    
    let string = dateFormatter.string(from: date)
    
    
    return string
    }   }
    
  • usage description:

    let date = DateFormatter.getDateFromString(string: "11-07-2001”, fromFormat: "dd-MM-yyyy")
    print("custom date : \(date)")
    let dateFormatter = DateFormatter.initWithSafeLocale(withDateFormat: "yyyy-MM-dd HH:mm:ss")
    let dt = DateFormatter.getDateFromString(string: "2001-05-05 12:34:56")
    print("base date = \(dt)")
    dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
    let dateString = dateFormatter.string(from: Date())
    print("dateString = " + dateString)
    let date1 = dateFormatter.date(from: "2001-05-05 12:34:56")
    print("date1 = \(String(describing: date1))")
    let date2 = dateFormatter.date(from: "2001-05-05 22:34:56")
    print("date2 = \(String(describing: date2))")
    let date3 = dateFormatter.date(from: "2001-05-05 12:34:56PM")
    print("date3 = \(String(describing: date3))")
    let date4 = dateFormatter.date(from: "2001-05-05 12:34:56 PM")
    print("date4 = \(String(describing: date4))")