Який найкращий спосіб впоратися з локальним словом “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.