Как лучше всего работать с локалью NSDateFormatter "feechur"? - PullRequest
163 голосов
/ 07 июля 2011

Кажется, что NSDateFormatter имеет «особенность», которая неожиданно вас кусает: если вы выполняете простую операцию «фиксированного» формата, такую ​​как:

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

, тогда она отлично работает в США и большинстве стран.locales UNTIL ... кто-то, чей телефон настроен на 24-часовой регион, устанавливает переключатель 12/24 часа в настройках на 12. Затем вышеприведенное начинает приставлять «AM» или «PM» к концу полученной строки.

(см., Например, NSDateFormatter, я что-то не так делаю или это ошибка? )

(И см. https://developer.apple.com/library/content/qa/qa1480/_index.html)

Очевидно, Apple имеетобъявил, что это «ПЛОХО» - Broken As Designed, и они не собираются это исправлять.

Обходное решение заключается в том, чтобы установить языковой формат форматера даты для определенного региона, обычно США,но это немного грязно:

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

Не так уж и плохо в первых двух, но я имею дело примерно с десятью различными приложениями, и первое, на которое я смотрю, имеет 43 экземпляра этого сценария.

Так что любые умные идеи для макроса / переопределенного класса / что угодно мСодействовать усилиям по изменению всего, не делая код неясным?(Мой первый инстинкт - переопределить NSDateFormatter версией, которая установит языковой стандарт в методе init. Требуется изменить две строки - строку alloc / init и добавленный импорт.)

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

Щедрость!

Я буду назначать награду лучшим (законным)предложение / критика я вижу к полудню вторника.[См. Ниже - срок продлен.]

Обновление

По предложению ОМЗ, вот что я нахожу -

Вот версия категории - h file:

#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 в категории есть что-то «странное».

Окончательный ответ

См. принятый ответ ниже.

Ответы [ 4 ]

63 голосов
/ 18 июля 2011

Дух !!

Иногда у вас появляется "Ага !!"момент, иногда это больше "Дух !!"Это последнее.В категории для initWithSafeLocale «супер» init было закодировано как self = [super init];.Это присваивает SUPERCLASS NSDateFormatter, но не 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
38 голосов
/ 09 июля 2011

Вместо создания подклассов вы можете создать категорию 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 блоки при использовании одного и того же экземпляра из нескольких потоков.

6 голосов
/ 10 марта 2015

Могу я предложить что-то совершенно иное, потому что, честно говоря, все это в некоторой степени бежит по кроличьей норе.

Вы должны использовать один NSDateFormatter с установленным dateFormat и locale принудительным значением en_US_POSIX для получения дат (с серверов / API).

Тогда вы должны использовать другой NSDateFormatter для пользовательского интерфейса, для которого вы будете устанавливать свойства timeStyle / dateStyle - таким образом, вы не будете устанавливать явный dateFormat самостоятельно, таким образом, ошибочно полагая, что формат будет использоваться.

Это означает, что пользовательский интерфейс определяется пользовательскими настройками (am / pm против 24 часов и строки даты, правильно отформатированные по выбору пользователя - из настроек iOS), тогда как даты, которые «входят» в ваше приложение, всегда «правильно анализируются» на NSDate для использования.

3 голосов
/ 25 декабря 2017

Вот решение этой проблемы в быстрой версии.В Swift мы можем использовать расширение вместо категории.Итак, здесь я создал расширение для 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
    }   }
    
  • описание использования:

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