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

What is the best way to deal with the NSDateFormatter locale "feature"?

提问人:Hot Licks 提问时间:7/7/2011 最后编辑:halferHot Licks 更新时间:10/5/2022 访问量:48395

问:

似乎有一个“功能”出乎意料地咬了你一口:如果你做一个简单的“固定”格式操作,例如: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];

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

那么,对于宏/重写类/任何东西,有什么聪明的想法可以最大限度地减少更改所有内容的努力,而不会使代码变得模糊?(我的第一反应是用一个可以在 init 方法中设置语言环境的版本覆盖 NSDateFormatter。需要更改两行 -- alloc/init 行和添加的 import。

##Added

这就是我到目前为止想出的——似乎适用于所有场景:

@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

##Update 关于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。这两个结果有明显的区别,我判断类别版本是错误的。请注意,类别版本中的日志正在执行(并且代码中的停止被命中),因此这不仅仅是代码以某种方式未被使用的情况。

##A 好奇的观察

略微修改了类别实现:

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

iOS Objective-C iPhone 语言环境 nsdateformatter

评论

5赞 Hot Licks 7/12/2011
摩西,我不知道你为什么选择编辑标题。“Feechur”是该领域的一个合法术语(已经存在了 30 年左右),指的是某些软件的一个方面或功能,即使作者拒绝承认它,它也被考虑得足够糟糕,可以被视为错误。
1赞 bshirley 7/13/2011
将字符串转换为日期时,字符串必须与格式化程序描述完全匹配 - 这是与您所在地区无关的问题。
0赞 Hot Licks 7/13/2011
各种日期字符串用于测试不同的可能配置,正确和错误。我知道其中一些是无效的,给定格式字符串。
0赞 bshirley 7/13/2011
你有没有尝试过不同的值?- (NSDateFormatterBehavior)formatterBehavior
0赞 Hot Licks 7/13/2011
没有尝试过。该规范对于是否可以在 iOS 中更改是矛盾的。主要描述说“iOS 注意:iOS 仅支持 10.4+ 行为”,而 NSDateFormatterBehavior 部分说这两种模式都可用(但它可能只谈论常量)。

答:

40赞 omz 7/10/2011 #1

您可以创建一个带有附加初始值设定项的类别,该初始值设定项负责分配区域设置,也可能是一个格式字符串,而不是子类化,因此在初始化后立即拥有一个现成的格式化程序。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 决定在未来版本的操作系统中添加此类方法。

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

评论

0赞 Hot Licks 7/10/2011
拥有静态 NSLocale(如我的建议)是否适用于某个类别?
0赞 omz 7/10/2011
是的,这也应该在一个类别中起作用。我省略了它以使示例更简单。
0赞 Hot Licks 7/12/2011
奇怪的是,类别方法不起作用。类别方法被执行,它得到与其他版本完全相同的 Locale(我背靠背执行它们,首先是类别版本)。不知何故,setLocale 显然没有“接受”。
0赞 Hot Licks 7/13/2011
找出为什么这种方法不起作用会很有趣。如果没有人想出更好的东西,我会把赏金奖励给这个明显错误的最佳解释。
0赞 Hot Licks 7/16/2011
好吧,我把赏金奖励给 OMZ,因为他是唯一一个在这方面做出明显努力的人。
68赞 Hot Licks 7/18/2011 #2

咄!!

有时你会有“啊哈!”的时刻,有时更像是“咄!这是后者。在“super”的类别中被编码为 .这初始化了 的 SUPERCLASS,但不初始化对象本身。initWithSafeLocaleinitself = [super init];NSDateFormatterinitNSDateFormatter

显然,当跳过此初始化时,“反弹”,可能是因为对象中缺少一些数据结构。更改 to 会导致初始化发生,并且再次感到高兴。setLocaleinitself = [self init];NSDateFormattersetLocale

以下是该类别 .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

评论

0赞 Agent Chocks. 5/22/2014
“NSString *dateStr = @”2014-04-05T04:00:00.000Z“;”的日期格式化程序是什么?
0赞 Hot Licks 5/22/2014
@Agent - 查找一下:unicode.org/reports/tr35/tr35-31/...
0赞 Hot Licks 3/25/2016
@tbag - 你的问题不应该是关于NSDateFormatter的吗?
0赞 tbag 3/25/2016
@HotLicks是的,我的错。我吃 NSDateFormatter。
0赞 Hot Licks 3/25/2016
@tbag - 规范是怎么说的?
7赞 Daniel 3/10/2015 #3

我可以提出一些完全不同的东西,因为老实说,所有这些都在某种程度上陷入了兔子洞。

您应该使用一个带有 set 和 forced to 的日期(来自服务器/API)。NSDateFormatterdateFormatlocaleen_US_POSIX

然后,您应该为UI使用不同的/属性 - 这样您就不会自己进行显式设置,从而错误地假设将使用该格式。NSDateFormattertimeStyledateStyledateFormat

这意味着 UI 由用户首选项驱动(上午 / 下午 与 24 小时,以及根据用户选择正确格式化的日期字符串 - 从 iOS 设置),而“进入”您的应用程序的日期始终被正确“解析”为供您使用。NSDate

评论

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

这是 swift 版本中该问题的解决方案。在 swift 中,我们可以使用 extension 而不是 category。 因此,在这里,我创建了 DateFormatter 的扩展,并在其中创建了 initWithSafeLocale 返回带有相关 Locale 的 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))")