Как лучше всего работать с локалью NSDateFormatter «feechur»?

168

Кажется, у 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 объявила, что это «ПЛОХО» - Broken As Designed, и они не собираются это исправить.

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

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

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

Итак, есть ли какие-нибудь умные идеи для макро / переопределенного класса / чего бы то ни было, чтобы минимизировать усилия по изменению всего, не делая код неясным? (Мой первый инстинкт - переопределить NSDateFormatter версией, которая установит языковой стандарт в методе init. Требуется изменить две строки - строку 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

Bounty!

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

Обновить

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

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

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

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

Горячие лижет
источник
5
Моше, я не знаю, почему ты решил отредактировать название. «Feechur» - это законный термин в данной области техники (и существующий уже около 30 лет), означающий аспект или особенность некоторого программного обеспечения, которое является достаточно непродуманным, чтобы считаться ошибкой, даже если авторы отказываются признавать это.
Горячая Ликс
1
при преобразовании строки в дату строка должна точно соответствовать описанию средства форматирования - это является касательной проблемой для вашей местности.
bshirley
Различные строки даты предназначены для проверки возможных конфигураций, правильных и ошибочных. Я знаю, что некоторые из них недопустимы, учитывая строку форматирования.
Hot Licks
Вы экспериментировали с разными значениями - (NSDateFormatterBehavior)formatterBehavior?
bshirley
Не экспериментировал с этим. Спецификация противоречива о том, может ли она быть изменена в iOS. Основное описание гласит: «Примечание для iOS: iOS поддерживает только поведение 10.4+», а в разделе NSDateFormatterBehavior говорится, что оба режима доступны (но речь может идти только о константах).
Hot Licks

Ответы:

67

Duh !!

Иногда у вас есть "Ага !!" момент, иногда это больше "Дух !!" Это последнее. В категории для initWithSafeLocale«супер» initбыло закодировано как self = [super init];. Это inits суперкласс , 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
Горячие лижет
источник
каким будет форматер даты для "NSString * dateStr = @" 2014-04-05T04: 00: 00.000Z ";" ?
Агент Чокс.
@Agent - посмотрите: unicode.org/reports/tr35/tr35-31/…
Hot Licks
@ tbag - Разве ваш вопрос не должен быть о NSDateFormatter?
Hot Licks
@ Хотликс да мой плохой. Я мясо NSDateFormatter.
tbag
@tbag - Что говорит спецификация?
Hot Licks
41

Вместо создания подклассов вы можете создать 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 (как в моем предложении) работать в категории?
Hot Licks
Да, это также должно работать в категории. Я оставил это, чтобы сделать пример проще.
OMZ
Любопытно, что подход категории не работает. Метод category выполняется, и он получает ту же локаль, что и другая версия (я выполняю их вплотную, сначала версию категории). Просто так или иначе setLocale явно не "берет".
Hot Licks
Было бы интересно узнать, почему этот подход не работает. Если никто не придумает что-то лучшее, я назначу награду за лучшее объяснение этой очевидной ошибки.
Hot Licks
Что ж, я присуждаю награду ОМЗ, поскольку он единственный, кто приложил к этому явные усилия.
Hot Licks
7

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

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

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

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

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

Вот решение этой проблемы в быстрой версии. В 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))")
Технология
источник