Кой е най-добрият начин за справяне с локала на NSDateFormatter feechur?

Изглежда, че 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];

Не е много лошо в onsies-twosies, но имам работа с около десет различни приложения и първото, което гледам, има 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. Има ясна разлика в двата резултата и смятам, че версията на категорията е грешна. Обърнете внимание, че регистрационният файл във версията на категорията СЕ изпълнява (и спиранията, поставени в кода, са ударени), така че това не е просто случай, когато кодът по някакъв начин не се използва.

Актуализация на наградата:

Тъй като все още не съм получил никакви приложими отговори, ще удължа крайния срок за наградите с още ден или два.

Баунти приключва след 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 метод в категория.

Окончателен отговор

Вижте приетия отговор по-долу.


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 Бележка: iOS поддържа само поведението 10.4+, докато разделът NSDateFormatterBehavior казва, че и двата режима са налични (но може да се говори само за константите).   -  person Hot Licks    schedule 12.07.2011
comment
няма да ме изненада, ако е налично само поведението 10.4+   -  person bshirley    schedule 13.07.2011
comment
@HotLicks Малко съм объркан, този проблем все още ли съществува в iOS 6? Или Apple реши това..?   -  person Johnson Mathew    schedule 09.05.2013
comment
@JohnsonMathew -- AFAIK, 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
Е, присъждам наградата на OMZ, тъй като той е единственият, който положи видими усилия за това. - 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 за потребителския интерфейс, на който ще зададете свойствата timeStyle/dateStyle - по този начин няма да имате изричен 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 версията. В swift можем да използваме разширение вместо категория. И така, тук създадох разширението за DateFormatter и вътре в него initWithSafeLocale връща DateFormatter със съответния локал, тук в нашия случай това е en_US_POSIX, освен това също предоставих няколко метода за формиране на дата.

  • Суифт 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