Jaki jest najlepszy sposób radzenia sobie z ustawieniami lokalnymi NSDateFormatter „feechur”?

168

Wygląda na to, że NSDateFormatterma „funkcję”, która nieoczekiwanie cię gryzie: jeśli wykonasz prostą operację „ustalonego” formatu, taką jak:

NSDateFormatter* fmt = [[NSDateFormatter alloc] init];
[fmt setDateFormat:@"yyyyMMddHHmmss"];
NSString* dateStr = [fmt stringFromDate:someDate];
[fmt release];

Wtedy działa dobrze w Stanach Zjednoczonych i większości lokalizacji DO ... ktoś z telefonem ustawionym na region 24-godzinny ustawi przełącznik 12/24 godzinny w ustawieniach na 12. Następnie powyższe zaczyna przypisywać „AM” lub „PM” do koniec powstałego ciągu.

(Zobacz np. NSDateFormatter, czy robię coś źle, czy to jest błąd? )

(Zobacz też https://developer.apple.com/library/content/qa/qa1480/_index.html )

Najwyraźniej Apple oświadczył, że jest to „ZŁE” - zepsute zgodnie z projektem i nie zamierzają tego naprawić.

Obejście polega najwyraźniej na ustawieniu ustawień regionalnych programu formatującego datę dla określonego regionu, ogólnie Stanów Zjednoczonych, ale jest to trochę niechlujne:

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

Nieźle jak na dwójki, ale mam do czynienia z około dziesięcioma różnymi aplikacjami, a pierwsza, na którą patrzę, ma 43 przypadki tego scenariusza.

Czy są jakieś sprytne pomysły na makro / klasę nadpisaną / cokolwiek, aby zminimalizować wysiłek związany ze zmianą wszystkiego, bez utrudniania zaciemniania kodu? (Moim pierwszym odruchem jest nadpisanie NSDateFormatter wersją, która ustawiałaby ustawienia regionalne w metodzie init. Wymaga zmiany dwóch wierszy - linii przydziel / init i dodanego importu).

Dodany

Oto, co do tej pory wymyśliłem - wydaje się działać we wszystkich scenariuszach:

@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

Hojność!

Przyznam nagrodę za najlepszą (uzasadnioną) sugestię / krytykę, jaką zobaczę do południa we wtorek. [Patrz poniżej - termin przedłużony.]

Aktualizacja

Ponownie propozycja OMZ, oto co znajduję -

Oto wersja kategorii - plik h:

#import <Foundation/Foundation.h>


@interface NSDateFormatter (Locale)
- (id)initWithSafeLocale;
@end

Plik kategorii 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

Kod:

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];

Wynik:

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)

Telefon [sprawia, że ​​iPod Touch] jest ustawiony na Wielką Brytanię, z przełącznikiem 12/24 ustawionym na 12. Jest wyraźna różnica w tych dwóch wynikach i oceniam wersję kategorii jako błędną. Zauważ, że log w wersji kategorii JEST wykonywany (i zatrzymywane w kodzie są trafiane), więc nie jest to po prostu przypadek, że kod w jakiś sposób nie jest używany.

Aktualizacja zleceń:

Ponieważ nie otrzymałem jeszcze żadnych odpowiednich odpowiedzi, przedłużę termin bounty o kolejny dzień lub dwa.

Bounty kończy się za 21 godzin - trafi do tego, kto podejmie najwięcej starań, aby pomóc, nawet jeśli odpowiedź nie jest w moim przypadku przydatna.

Ciekawa obserwacja

Nieznacznie zmodyfikowano implementację kategorii:

#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

Po prostu zmieniłem nazwę statycznej zmiennej locale (na wypadek, gdyby był jakiś konflikt ze statyczną zadeklarowaną w podklasie) i dodałem dodatkowy dziennik NSLog. Ale spójrz, co drukuje 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

Jak widać, setLocale po prostu nie. Ustawienia regionalne programu formatującego to nadal en_GB. Wygląda na to, że jest coś „dziwnego” w metodzie init w kategorii.

Ostatnia odpowiedź

Zobacz zaakceptowaną odpowiedź poniżej.

Hot Licks
źródło
5
Mosze, nie wiem, dlaczego zdecydowałeś się edytować tytuł. „Feechur” jest słusznym terminem w sztuce (i istniało od około 30 lat), oznaczającym aspekt lub cechę oprogramowania, które jest wystarczająco nieprzemyślane, aby można je było uznać za błąd, mimo że autorzy odmawiają przyznania tego.
Hot Licks
1
podczas konwertowania łańcucha do daty, ciąg musi dokładnie odpowiadać opisowi programu formatującego - jest to kwestia styczna do Twojej lokalizacji.
bshirley
Różne ciągi dat służą do testowania różnych możliwych konfiguracji, poprawnych i błędnych. Wiem, że niektóre z nich są nieprawidłowe, biorąc pod uwagę ciąg formatujący.
Hot Licks,
czy eksperymentowałeś z różnymi wartościami - (NSDateFormatterBehavior)formatterBehavior?
bshirley
Nie eksperymentowałem z tym. Specyfikacja jest sprzeczna, czy można ją nawet zmienić w iOS. Główny opis mówi „Uwaga iOS: iOS obsługuje tylko zachowanie 10.4+”, podczas gdy sekcja NSDateFormatterBehavior mówi, że oba tryby są dostępne (ale może być mowa tylko o stałych).
Hot Licks

Odpowiedzi:

67

Duh !!

Czasami masz „Aha !!” moment, czasami jest to bardziej "Duh !!" To jest to drugie. W kategorii za initWithSafeLocale„super” initzostał zakodowany jako self = [super init];. Ten inits nadklasą NSDateFormatterale nieinitNSDateFormatter sam obiekt.

Najwyraźniej kiedy ta inicjalizacja jest pomijana, setLocale"odbija się", prawdopodobnie z powodu jakiejś brakującej struktury danych w obiekcie. Zmiana initTO self = [self init];powoduje NSDateFormatterinicjalizacji występuje, isetLocale chętnie ponownie.

Oto „ostateczne” źródło .m kategorii:

#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
Hot Licks
źródło
jaki będzie program formatujący datę dla „NSString * dateStr = @" 2014-04-05T04: 00: 00.000Z ";" ?
Agent Chocks.
@Agent - Look it up: unicode.org/reports/tr35/tr35-31/…
Hot Licks
@tbag - Czy Twoje pytanie nie powinno dotyczyć NSDateFormatter?
Hot Licks
@HotLicks yes my bad. I mięso NSDateFormatter.
torebka
@tbag - Co mówi specyfikacja?
Hot Licks
41

Zamiast tworzenia podklas, możesz utworzyć NSDateFormatterkategorię z dodatkowym inicjatorem, który zajmie się przypisaniem ustawień regionalnych i prawdopodobnie również ciągu formatującego, dzięki czemu będziesz mieć gotowy do użycia program formatujący zaraz po jego zainicjowaniu.

@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

Następnie możesz użyć NSDateFormatterdowolnego miejsca w kodzie za pomocą tylko:

NSDateFormatter* fmt = [[NSDateFormatter alloc] initWithPOSIXLocaleAndFormat:@"yyyyMMddHHmmss"];

Możesz w jakiś sposób chcieć poprzedzić metodę kategorii, aby uniknąć konfliktów nazw, na wypadek gdyby Apple zdecydował się dodać taką metodę w przyszłej wersji systemu operacyjnego.

W przypadku, gdy zawsze używasz tych samych formatów daty, możesz również dodać metody kategorii, które zwracają pojedyncze instancje z określonymi konfiguracjami (na przykład +sharedRFC3339DateFormatter). Należy jednak pamiętać, że NSDateFormatternie jest to bezpieczne dla wątków i musisz używać blokad lub @synchronizedbloków, gdy używasz tego samego wystąpienia z wielu wątków.

omz
źródło
Czy posiadanie statycznego NSLocale (jak w mojej sugestii) działałoby w kategorii?
Hot Licks
Tak, to powinno również działać w kategorii. Pominąłem to, żeby przykład był prostszy.
omz,
Co ciekawe, podejście do kategorii nie działa. Metoda category jest wykonywana i pobiera dokładnie to samo ustawienie regionalne, co w innej wersji (wykonuję je z powrotem do tyłu, najpierw wersja kategorii). Po prostu w jakiś sposób setLocale najwyraźniej nie "przyjmuje".
Hot Licks
Byłoby interesujące dowiedzieć się, dlaczego to podejście nie działa. Jeśli nikt nie wymyśli czegoś lepszego, przyznam nagrodę za najlepsze wyjaśnienie tego widocznego błędu.
Hot Licks,
Cóż, przyznam nagrodę dla OMZ, ponieważ jest jedynym, który poczynił w tej sprawie wyraźny wysiłek.
Hot Licks,
7

Mogę zasugerować coś zupełnie innego, ponieważ szczerze mówiąc wszystko to jest trochę spływaniem do króliczej nory.

Powinieneś używać jednego NSDateFormatterz dateFormatustawionym i localewymuszonym do en_US_POSIXodbierania dat (z serwerów / API).

Następnie powinieneś użyć innego NSDateFormatterdla interfejsu użytkownika, który ustawisz timeStyle/ dateStyleproperties - w ten sposób nie masz dateFormatsamodzielnie jawnego zestawu, więc błędnie zakładasz, że format będzie używany.

Oznacza to, że interfejs użytkownika jest sterowany przez preferencje użytkownika (am / pm vs 24-godzinny oraz ciągi dat sformatowane prawidłowo do wyboru użytkownika - z ustawień iOS), podczas gdy daty, które „pojawiają się” w Twojej aplikacji, są zawsze poprawnie „analizowane” NSDatena do użycia.

Daniel
źródło
Czasami ten schemat działa, czasami nie. Jedno z zagrożeń polega na tym, że metoda może wymagać zmodyfikowania formatu daty programu formatującego, a tym samym zmiany formatu ustawionego przez kod, który Cię wywołał, gdy był w środku operacji formatowania daty. Istnieją inne scenariusze, w których strefa czasowa musi być wielokrotnie zmieniana.
Hot Licks
Nie wiem, dlaczego zmiana timeZonewartości formatera miałaby przeszkadzać w tym schemacie, czy mógłbyś to rozwinąć? Aby było jasne, powstrzymałbyś się od zmiany formatu. Jeśli musisz to zrobić, będzie to miało miejsce w programie formatującym „importującym”, a więc na osobnym programie formatującym.
Daniel
Za każdym razem, gdy zmieniasz stan obiektu globalnego, jest to niebezpieczne. Łatwo zapomnieć, że inni też go używają.
Hot Licks
3

Oto rozwiązanie tego problemu w wersji szybkiej. Szybko możemy użyć rozszerzenia zamiast kategorii. Więc tutaj stworzyłem rozszerzenie dla DateFormatter i wewnątrz tego initWithSafeLocale zwraca DateFormatter z odpowiednim ustawieniem regionalnym, Tutaj w naszym przypadku jest to en_US_POSIX, Oprócz tego dostarczyłem również kilka metod tworzenia daty.

  • Szybki 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
    }   }
  • opis użytkowania:

    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))")
Tech
źródło