Який найкращий спосіб впоратися з локальним словом “feechur” NSDateFormatter?


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 заявила, що це "BAD" - 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

Баунті!

Я присуджую нагороду за найкращу (законну) пропозицію / критику, яку я бачу до середини дня вівторка. [Дивіться нижче - термін продовжений.]

Оновлення

Перегляньте пропозицію 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. Існує чітка різниця в двох результатах, і я вважаю, що версія категорії є неправильною. Зауважте, що журнал у версії категорії 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. Здається, що у методі init у категорії є щось "дивне".

Остаточна відповідь

Дивіться прийняту відповідь нижче.


5
Моше, я не знаю, чому ти вирішив редагувати заголовок. "Feechur" - це законний термін у цій галузі (і він існував вже 30 років), що означає аспект або особливості певного програмного забезпечення, що є недостатньо продуманим, щоб вважати помилкою, навіть якщо автори відмовляються визнати це.
— Гарячі лизання

1
при перетворенні рядка в дату, рядок повинен точно відповідати опису форматора - це дотична проблема для вашого населеного пункту.
— bshirley

Різні рядки дати існують для перевірки різних можливих конфігурацій, правильних та помилкових. Я знаю, що деякі з них недійсні, враховуючи рядок форматування.
— Гарячі лизання

Ви експериментували з різними значеннями - (NSDateFormatterBehavior)formatterBehavior?
— bshirley

Не експериментував з цим. Специфікація суперечить, чи можна її навіть змінити в iOS. Основний опис говорить "Примітка iOS: iOS підтримує лише поведінку 10.4+", тоді як в розділі NSDateFormatterBehavior обидва режими доступні (але це може говорити лише про константи).
— Гарячі лизання

Відповіді:


67

Дух !!

Іноді у вас є "Ага !!" Момент, іноді це більше "Дух !!" Це останнє. У категорії 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

яким буде формат дат для "NSString * dateStr = @" 2014-04-05T04: 00: 00.000Z ";" ?
— Агент Чокс.

@Agent - Подивіться: unicode.org/reports/tr35/tr35-31/…
— Hot Licks

@tbag - Чи не має Вашого запитання про NSDateFormatter?
— Гарячі лизання

@HotLicks так мені погано. Я м'ясо NSDateFormatter.
— tbag

@tbag - Що говорить специфікація?
— Гарячі лизання

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 (як, на мою пропозицію) працює в категорії?
— Гарячі лизання

Так, це також повинно працювати в категорії. Я залишив це, щоб зробити приклад більш простим.
— omz

Цікаво, що категорійний підхід не працює. Метод категорії виконується, і він отримує той самий Locale, як і інша версія (я виконую їх назад до спини, перша версія версії). Просто якимось чином setLocale, мабуть, не "бере".
— Гарячі лизання

Було б цікаво з'ясувати, чому такий підхід не працює. Якщо ніхто не придумає чогось кращого, я присуджую нагороду найкращому поясненню цієї явної помилки.
— Гарячі лизання

Що ж, я присуджую нагороду ОМЗ, оскільки він єдиний, хто зробив це очевидно.
— Гарячі лизи

7

Я можу запропонувати щось зовсім інше, тому що чесно кажучи, все це дещо біжить по кролячій норі.

Ви повинні використовувати один NSDateFormatterіз dateFormatвстановленим і localeпримусовим до en_US_POSIXприйому дат (із серверів / API).

Тоді вам слід використовувати інший NSDateFormatterдля інтерфейсу користувача, який ви встановите timeStyle/ dateStyleвластивості - таким чином у вас немає явного dateFormatнабору самих, таким чином, помилково припускаючи, що формат буде використаний.

Це означає, що користувальницький інтерфейс керується налаштуваннями користувачів (am / pm vs 24 години, а рядки дат відформатовані правильно за вибором користувача - з налаштувань iOS), тоді як дати, які "надходять" у ваш додаток, завжди "правильно розбираються" NSDateна вам користуватися.


Іноді ця схема працює, іноді ні. Одна з небезпек полягає в тому, що вашому методу може знадобитися змінити формат дати форматера і, тим самим, змінити формат, встановлений кодом, який викликав вас, коли він був посеред операцій із форматування дати. Є й інші сценарії, коли часовий пояс потрібно неодноразово змінювати.
— Гарячі лизання

Я не знаю, чому зміна timeZoneзначення форматера перешкоджатиме цій схемі, ви могли б розробити? Крім того, щоб було зрозуміло, ви б утрималися від зміни формату. Якщо вам потрібно зробити це, то це станеться на "імпортному" форматері, тобто окремому форматорі.
— Даніель

Щоразу, коли ви змінюєте стан глобального об'єкта, це небезпечно. Легко забути, що і інші користуються ним.
— Гарячі лизання

3

Ось вирішення цієї проблеми у швидкій версії. У swift ми можемо використовувати розширення замість категорії. Отже, тут я створив розширення для DateFormatter і всередині цього initWithSafeLocale повертає DateFormatter з відповідним Locale.

  • Швидкий 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))")
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.