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.


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

jaki będzie program formatujący datę dla „NSString * dateStr = @" 2014-04-05T04: 00: 00.000Z ";" ?
— Agent Chocks.


@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.


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.


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))")
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.