处理 NSDateFormatter 语言环境“feehur”的最佳方法是啥?

Posted

技术标签:

【中文标题】处理 NSDateFormatter 语言环境“feehur”的最佳方法是啥?【英文标题】:What is the best way to deal with the NSDateFormatter locale "feechur"?处理 NSDateFormatter 语言环境“feehur”的最佳方法是什么? 【发布时间】:2011-07-07 15:30:14 【问题描述】:

看来NSDateFormatter有一个“特点”让你意想不到:如果你做一个简单的“固定”格式操作比如:

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

然后它在美国和大多数地区都可以正常工作,直到...有人将手机设置为 24 小时区域,将设置中的 12/24 小时切换设置为 12。然后上面开始添加“AM”或“ PM" 到结果字符串的末尾。

(参见,例如,NSDateFormatter, am I doing something wrong or is this a bug?)

(见https://developer.apple.com/library/content/qa/qa1480/_index.html

显然,Apple 已经宣布这是“糟糕的”——Broken As Design,他们不会修复它。

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

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

在 onsies-twosies 中还不错,但我正在处理大约十个不同的应用程序,我看到的第一个应用程序有 43 个这种情况的实例。

那么对于宏/重写类/任何巧妙的想法,可以最大限度地减少更改所有内容的工作量,而不会使代码变得晦涩难懂? (我的第一直觉是用一个可以在 init 方法中设置语言环境的版本覆盖 NSDateFormatter。需要更改两行——alloc/init 行和添加的导入。)

添加

这是我迄今为止提出的——似乎适用于所有场景:

@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)

电话 [制作 iPod Touch] 设置为英国,12/24 开关设置为 12。两个结果有明显差异,我判断类别版本错误。请注意,类别版本中的日志正在被执行(并且代码中的停止被命中),所以这不仅仅是代码没有被使用的情况。

赏金更新:

由于我还没有收到任何适用的回复,我会将赏金截止日期再延长一两天。

赏金会在 21 小时后结束——它会发给最努力提供帮助的人,即使答案对我来说并不是真正有用。

一个奇怪的观察

稍微修改了类别实现:

#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 方法有些“奇怪”。

最终答案

请参阅下面的accepted answer。

【问题讨论】:

莫舍,我不知道你为什么选择编辑标题。 “Feechur”是本领域的一个合法术语(并且已经使用了 30 年左右),意思是某些软件的某个方面或功能被认为是错误的,即使作者拒绝承认它也是如此。 将字符串转换为日期时,字符串必须与格式化程序描述完全匹配 - 这与您所在地区的问题无关。 各种日期字符串用于测试不同的可能配置,正确和错误。考虑到格式化字符串,我知道其中一些是无效的。 你尝试过- (NSDateFormatterBehavior)formatterBehavior的不同值吗? 没试过。该规范是否可以在 ios 中更改是矛盾的。主要描述说“iOS 注意:iOS 仅支持 10.4+ 的行为”,而 NSDateFormatterBehavior 部分说两种模式都可用(但它可能只谈论常量)。 【参考方案1】:

呃!!

有时你有一个“啊哈!!”时刻,有时它更像是一个“Duh!!”这是后者。在initWithSafeLocale 的类别中,“超级”init 被编码为self = [super init];。这会初始化 NSDateFormatter 的 SUPERCLASS,但不会 init 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

【讨论】:

"NSString *dateStr = @"2014-04-05T04:00:00.000Z";" 的日期格式化程序是什么? @Agent - 查一下:unicode.org/reports/tr35/tr35-31/… @tbag - 你的问题不应该是关于 NSDateFormatter 的吗? @HotLicks 是的,我的错。我吃 NSDateFormatter。 @tbag - 规范是怎么说的?【参考方案2】:

您可以创建一个 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"];

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

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

【讨论】:

将静态 NSLocale(如我的建议)用于类别吗? 是的,这也应该在一个类别中工作。为了使示例更简单,我省略了它。 奇怪的是,类别方法不起作用。执行 category 方法,它得到与其他版本完全相同的 Locale (我背靠背执行它们,首先是类别版本)。只是不知何故 setLocale 显然没有“接受”。 找出这种方法不起作用的原因会很有趣。如果没有人想出更好的东西,我将奖励对这个明显错误的最佳解释。 好吧,我将赏金授予 OMZ,因为他是唯一一个在这方面做出明显努力的人。【参考方案3】:

我可以建议一些完全不同的东西,因为老实说,所有这些都有点像兔子洞。

您应该使用一个NSDateFormatter 并设置dateFormat 并强制locale 使用en_US_POSIX 来接收日期(来自服务器/API)。

那么您应该为 UI 使用不同的 NSDateFormatter,您将设置 timeStyle/dateStyle 属性 - 这样您就没有自己设置明确的 dateFormat,因此错误地假设将使用格式。

这意味着 UI 是由用户偏好驱动的(上午/下午与 24 小时,日期字符串根据用户的选择正确格式化 - 来自 iOS 设置),而“进入”您的应用程序的日期总是被正确“解析”发送至NSDate 供您使用。

【讨论】:

这个方案有时有效,有时无效。一个危险是您的方法可能需要修改格式化程序的日期格式,并在这样做时更改调用您的代码设置的格式,当时它处于日期格式化操作的中间。在其他情况下,时区必须反复更改。 我不知道为什么改变格式化程序的timeZone 值会妨碍这个方案,你能详细说明一下吗?另外要明确的是,您将避免更改格式。如果您需要这样做,那么这将发生在“导入”格式化程序上,因此需要一个单独的格式化程序。 任何时候改变全局对象的状态都是危险的。很容易忘记其他人也在使用它。【参考方案4】:

这是 swift 版本中该问题的解决方案。在swift中,我们可以使用扩展名而不是类别。 所以,在这里我为 DateFormatter 和 initWithSafeLocale 创建了扩展 返回具有相关区域设置的 DateFormatter,在我们的例子中是 en_US_POSIX,除此之外还提供了几个日期形成方法。

斯威夫特 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
   

使用说明:

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))")

【讨论】:

以上是关于处理 NSDateFormatter 语言环境“feehur”的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

使用 NSDateFormatter 格式化月份和年份返回无效日期

NSDateFormatter:仅当应用程序针对该语言进行本地化时才显示本地化日期

使用 NSDateFormatter 避免脆弱的单元测试

Nsdateformatter日返回我在选择手机的语言。它可以返回我只有英语所有的时间?

Nsdateformatter 以所选电话的语言返回我一天。它可以一直只返回英语吗?

如何根据语言环境仅显示日期的日期和月份?