Как лучше всего справиться с feechur локали 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)

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

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

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

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

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

Добавлен

Это то, что я придумал до сих пор - похоже, работает во всех сценариях:

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

#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. Есть явная разница в двух результатах, и я считаю версию категории неправильной. Обратите внимание, что журнал в версии категории IS запускается (и срабатывают остановки, помещенные в код), так что это не просто случай, когда код каким-то образом не используется.

Обновление баунти:

Поскольку я еще не получил подходящих ответов, я продлю крайний срок награждения еще на день или два.

Баунти заканчивается через 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. Похоже, что в методе инициализации в категории есть что-то «странное».

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

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


person Hot Licks    schedule 07.07.2011    source источник
comment
Моше, я не знаю, почему ты решил изменить заголовок. Feechur - это законный термин в данной области техники (который использовался в течение 30 лет или около того), означающий аспект или особенность некоторого программного обеспечения, которое недостаточно продумано, чтобы считаться ошибкой, даже если авторы отказываются признать это.   -  person Hot Licks    schedule 12.07.2011
comment
при преобразовании строки в дату, строка должна точно соответствовать описанию средства форматирования - это проблема, касающаяся вашего местоположения.   -  person bshirley    schedule 12.07.2011
comment
Различные строки даты предназначены для проверки различных возможных конфигураций, правильных и ошибочных. Я знаю, что некоторые из них недействительны, учитывая строку форматирования.   -  person Hot Licks    schedule 12.07.2011
comment
вы экспериментировали с разными значениями - (NSDateFormatterBehavior)formatterBehavior?   -  person bshirley    schedule 12.07.2011
comment
Не экспериментировал с этим. Спецификации противоречивы относительно того, можно ли это вообще изменить в iOS. В основном описании указано iOS Note: iOS поддерживает только поведение 10.4+, в то время как в разделе NSDateFormatterBehavior говорится, что доступны оба режима (но, возможно, речь идет только о константах).   -  person Hot Licks    schedule 12.07.2011
comment
меня не удивит, если доступно только поведение 10.4+   -  person bshirley    schedule 13.07.2011
comment
@HotLicks Я немного запутался, эта проблема все еще сохраняется в iOS 6? Или яблоко это решило ..?   -  person Johnson Mathew    schedule 09.05.2013
comment
@JohnsonMathew - Насколько мне известно, Apple заявила, что их путь верен, поэтому они не будут его менять. Если только они не передумают.   -  person Hot Licks    schedule 09.05.2013
comment
является ли NSDateFormatter потокобезопасным для вызова этого из нескольких потоков?   -  person tbag    schedule 25.03.2016


Ответы (4)


Да !!

Иногда бывает ага !! момент, иногда это больше похоже на Ду !! Это последнее. В категории для initWithSafeLocale супер init был закодирован как self = [super init];. Это входит в СУПЕРКЛАСС 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
person Hot Licks    schedule 18.07.2011
comment
какой будет форматер даты для NSString * dateStr = @ 2014-04-05T04: 00: 00.000Z; ? - person Agent Chocks.; 22.05.2014
comment
@Agent - Посмотрите: unicode.org/reports / tr35 / tr35-31 / - person Hot Licks; 22.05.2014
comment
@tbag - Разве ваш вопрос не должен быть о NSDateFormatter? - person Hot Licks; 25.03.2016
comment
@HotLicks, да, мне плохо. Я мясо NSDateFormatter. - person tbag; 25.03.2016
comment
@tbag - Что написано в спецификации? - person Hot Licks; 25.03.2016
comment
@HotLicks Я читал спецификации, где сказано, что это не так, но я запутался, поэтому прошу пояснить. Мне было интересно, следует ли использовать dispatch_once для инициализации NSLocale для обеспечения безопасности потоков. Извините, если вопрос слишком простой. - person tbag; 25.03.2016
comment
@tbag - Можно утверждать, что в приведенном выше коде следует использовать протокол блокировки, такой как dispatch_once, чтобы гарантировать создание только одного экземпляра NSLocale. На практике не возникает проблем, если создается более одного экземпляра из-за одновременного выполнения кода, но добавить синхронизацию безвредно. Не имеет ничего общего с тем, является ли NSLocale потокобезопасным, ни с NSDateFormatter (который является потокобезопасным для iOS, если вы читаете спецификацию). - person Hot Licks; 25.03.2016
comment
О, хорошо, спасибо за разъяснения @HotLicks. Я проверил страницу developer.apple.com/ library / ios / documentation / Cocoa / Conceptual / и увидел NSDateFormatter, перечисленный в потоках небезопасных классов, поэтому я подумал, что это не потокобезопасный. - person tbag; 25.03.2016
comment
@tbag - некоторые разновидности класса для OSx небезопасны, и то же самое верно для очень старых разновидностей под iOS. Но обычная iOS вполне подойдет. - person Hot Licks; 25.03.2016
comment
@hotLicks Поддерживает ли это решение случай, если у пользователя есть буддийский календарь в настройках устройства? - person toxicsun; 16.05.2017

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

person omz    schedule 09.07.2011
comment
Будет ли статический NSLocale (как в моем предложении) работать в категории? - person Hot Licks; 09.07.2011
comment
Да, это тоже должно работать в категории. Я оставил это, чтобы упростить пример. - person omz; 10.07.2011
comment
Любопытно, что категориальный подход не работает. Метод категории выполняется, и он получает тот же языковой стандарт, что и другая версия (я выполняю их последовательно, сначала версия категории). Просто как-то setLocale видимо не берет. - person Hot Licks; 12.07.2011
comment
Было бы интересно узнать, почему такой подход не работает. Если никто не придумает что-то получше, я награжу награду за лучшее объяснение этой очевидной ошибки. - person Hot Licks; 12.07.2011
comment
Ну, я награждаю ОМЗ, потому что он единственный, кто приложил для этого явные усилия. - person Hot Licks; 16.07.2011
comment
@omz спасибо за решение. Поддерживает ли это решение буддийский календарь из настроек iphone? - person toxicsun; 16.05.2017
comment
Начиная с iOS 7, NSDateFormatters и NSNumberFormatters являются потокобезопасными. - person user1105951; 06.11.2018

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

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

Затем вы должны использовать другой NSDateFormatter для пользовательского интерфейса, для которого вы зададите свойства _6 _ / _ 7_ - таким образом у вас не будет явного dateFormat, установленного вами, что приведет к ложному предположению, что этот формат будет использоваться.

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

person Daniel    schedule 10.03.2015
comment
Иногда эта схема работает, иногда нет. Одна опасность заключается в том, что вашему методу может потребоваться изменить формат даты средства форматирования и при этом изменить формат, установленный кодом, который вызвал вас, когда он находился в середине операций форматирования даты. Существуют и другие сценарии, в которых часовой пояс необходимо постоянно менять. - person Hot Licks; 10.03.2015
comment
Я не знаю, почему изменение значения timeZone средства форматирования может помешать этой схеме, не могли бы вы уточнить? Также для ясности вы воздержитесь от изменения формата. Если вам нужно это сделать, то это произойдет в модуле форматирования импорта, поэтому в отдельном модуле форматирования. - person Daniel; 10.03.2015
comment
Каждый раз, когда вы меняете состояние глобального объекта, это опасно. Легко забыть, что его используют и другие. - person Hot Licks; 10.03.2015

Вот решение этой проблемы в быстрой версии. В 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))")
    
person Tech    schedule 25.12.2017