Was ist der beste Weg, um mit dem Gebietsschema „feechur“ von NSDateFormatter umzugehen?

167

Es scheint, dass NSDateFormatteres eine "Funktion" gibt, die Sie unerwartet beißt: Wenn Sie eine einfache "feste" Formatoperation ausführen, wie z.

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

Dann funktioniert es in den USA und den meisten Regionen gut, BIS ... jemand, dessen Telefon auf eine 24-Stunden-Region eingestellt ist, den 12/24-Stunden-Schalter in den Einstellungen auf 12 stellt. Dann beginnt das oben Gesagte, "AM" oder "PM" anzuheften das Ende der resultierenden Zeichenfolge.

(Siehe z. B. NSDateFormatter, mache ich etwas falsch oder ist das ein Fehler? )

(Siehe https://developer.apple.com/library/content/qa/qa1480/_index.html )

Anscheinend hat Apple dies als "SCHLECHT" deklariert - Broken As Designed, und sie werden es nicht beheben.

Die Umgehung besteht anscheinend darin, das Gebietsschema des Datumsformatierers für eine bestimmte Region, im Allgemeinen die USA, festzulegen, aber dies ist etwas chaotisch:

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

Nicht schlecht in Onsies-Twosies, aber ich habe es mit ungefähr zehn verschiedenen Apps zu tun, und die erste, die ich mir ansehe, enthält 43 Instanzen dieses Szenarios.

Also irgendwelche cleveren Ideen für ein Makro / eine überschriebene Klasse / was auch immer, um den Aufwand zu minimieren, alles zu ändern, ohne den Code zu verschleiern? (Mein erster Instinkt ist, NSDateFormatter mit einer Version zu überschreiben, die das Gebietsschema in der init-Methode festlegt. Erfordert das Ändern von zwei Zeilen - der Alloc / Init-Zeile und dem hinzugefügten Import.)

Hinzugefügt

Das habe ich mir bisher ausgedacht - scheint in allen Szenarien zu funktionieren:

@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

Kopfgeld!

Ich werde das Kopfgeld an den besten (legitimen) Vorschlag / die beste Kritik vergeben, die ich bis Dienstagmittag sehe. [Siehe unten - Frist verlängert.]

Aktualisieren

Zu OMZs Vorschlag, hier ist was ich finde -

Hier ist die Kategorie Version - h Datei:

#import <Foundation/Foundation.h>


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

Datei der Kategorie 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

Der Code:

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

Das Ergebnis:

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)

Das Telefon [machen Sie einen iPod Touch] ist auf Großbritannien eingestellt, der 12/24-Schalter auf 12. Es gibt einen deutlichen Unterschied zwischen den beiden Ergebnissen, und ich beurteile die Kategorieversion als falsch. Beachten Sie, dass das Protokoll in der Kategorieversion ausgeführt wird (und Stopps im Code getroffen werden), sodass der Code nicht einfach nicht verwendet wird.

Kopfgeld-Update:

Da ich noch keine zutreffenden Antworten erhalten habe, werde ich die Kopfgeldfrist um ein oder zwei Tage verlängern.

Das Kopfgeld endet in 21 Stunden - es geht an jeden, der sich am meisten bemüht, zu helfen, auch wenn die Antwort in meinem Fall nicht wirklich nützlich ist.

Eine merkwürdige Beobachtung

Die Implementierung der Kategorie wurde geringfügig geändert:

#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

Grundsätzlich wurde nur der Name der statischen Gebietsschemavariablen geändert (falls es einen Konflikt mit der in der Unterklasse deklarierten statischen Variable gab) und das zusätzliche NSLog hinzugefügt. Aber schauen Sie, was dieser NSLog druckt:

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

Wie Sie sehen können, hat die setLocale dies einfach nicht getan. Das Gebietsschema des Formatierers ist immer noch en_GB. Es scheint, dass eine Init-Methode in einer Kategorie etwas "Seltsames" hat.

Endgültige Antwort

Siehe die akzeptierte Antwort unten.

Hot Licks
quelle
5
Moshe, ich weiß nicht, warum du den Titel bearbeitet hast. "Feechur" ist ein legitimer Begriff auf dem Fachgebiet (und das schon seit ungefähr 30 Jahren), der einen Aspekt oder eine Funktion einer Software bedeutet, die so schlecht konzipiert ist, dass sie als Fehler angesehen werden kann, obwohl die Autoren dies nicht zugeben.
Hot Licks
1
Wenn Sie eine Zeichenfolge in ein Datum konvertieren, muss die Zeichenfolge genau mit der Formatierungsbeschreibung übereinstimmen. Dies ist ein tangentiales Problem für Ihre lokale Zeichenfolge.
Bshirley
Die verschiedenen Datumszeichenfolgen dienen dazu, die verschiedenen möglichen Konfigurationen zu testen, korrekt und fehlerhaft. Ich weiß, dass einige von ihnen aufgrund der Formatierungszeichenfolge ungültig sind.
Hot Licks
Haben Sie mit verschiedenen Werten von experimentiert - (NSDateFormatterBehavior)formatterBehavior?
Bshirley
Ich habe nicht damit experimentiert. Die Spezifikation ist widersprüchlich, ob sie überhaupt in iOS geändert werden kann. In der Hauptbeschreibung heißt es "iOS Hinweis: iOS unterstützt nur das Verhalten 10.4+", während im Abschnitt NSDateFormatterBehavior angegeben ist, dass beide Modi verfügbar sind (es werden jedoch möglicherweise nur die Konstanten behandelt).
Hot Licks

Antworten:

67

Duh !!

Manchmal hast du ein "Aha !!" Moment, manchmal ist es eher ein "Duh !!" Dies ist das letztere. In der Kategorie für initWithSafeLocaledas "Super" initwurde als codiert self = [super init];. Dies ist die SUPERCLASS von NSDateFormatter, aber nicht initdas NSDateFormatterObjekt selbst.

Wenn diese Initialisierung übersprungen wird, setLocale"prallt" sie offenbar ab, vermutlich aufgrund einer fehlenden Datenstruktur im Objekt. Durch Ändern von initto self = [self init];wird die NSDateFormatterInitialisierung ausgeführt und setLocaleist wieder zufrieden.

Hier ist die "endgültige" Quelle für die .m der Kategorie:

#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
quelle
Was ist der Datumsformatierer für "NSString * dateStr = @" 2014-04-05T04: 00: 00.000Z ";" ?
Agent Chocks.
@ Agent - Nachschlagen: unicode.org/reports/tr35/tr35-31/…
Hot Licks
@tbag - Sollte Ihre Frage nicht NSDateFormatter sein?
Hot Licks
@ HotLicks ja mein schlechtes. Ich fleische NSDateFormatter.
tbag
@tbag - Was sagt die Spezifikation?
Hot Licks
41

Anstelle einer Unterklasse können Sie eine NSDateFormatterKategorie mit einem zusätzlichen Initialisierer erstellen , der sich um die Zuweisung des Gebietsschemas und möglicherweise auch einer Formatzeichenfolge kümmert, sodass Sie direkt nach der Initialisierung einen gebrauchsfertigen Formatierer haben.

@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

Dann könnten Sie NSDateFormatterüberall in Ihrem Code mit nur:

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

Möglicherweise möchten Sie Ihrer Kategoriemethode ein Präfix voranstellen, um Namenskonflikte zu vermeiden, falls Apple beschließt, eine solche Methode in einer zukünftigen Version des Betriebssystems hinzuzufügen.

Wenn Sie immer dieselben Datumsformate verwenden, können Sie auch Kategoriemethoden hinzufügen, die Singleton-Instanzen mit bestimmten Konfigurationen zurückgeben (z. B. +sharedRFC3339DateFormatter). Beachten Sie jedoch, dass dies NSDateFormatternicht threadsicher ist und Sie Sperren oder @synchronizedBlöcke verwenden müssen, wenn Sie dieselbe Instanz aus mehreren Threads verwenden.

omz
quelle
Würde eine statische NSLocale (wie in meinem Vorschlag) in einer Kategorie funktionieren?
Hot Licks
Ja, das sollte auch in einer Kategorie funktionieren. Ich habe es weggelassen, um das Beispiel zu vereinfachen.
Omz
Seltsamerweise funktioniert der Kategorieansatz nicht. Die Kategorie-Methode wird ausgeführt und erhält genau das gleiche Gebietsschema wie die andere Version (ich führe sie hintereinander aus, zuerst die Kategorie-Version). Nur irgendwie "nimmt" die setLocale anscheinend nicht.
Hot Licks
Es wäre interessant herauszufinden, warum dieser Ansatz nicht funktioniert. Wenn sich niemand etwas Besseres einfallen lässt, werde ich das Kopfgeld für die beste Erklärung dieses offensichtlichen Fehlers vergeben.
Hot Licks
Nun, ich vergebe das Kopfgeld an OMZ, da er der einzige ist, der sich offensichtlich darum bemüht hat.
Hot Licks
7

Darf ich etwas völlig anderes vorschlagen, denn um ehrlich zu sein, läuft all dies etwas durch ein Kaninchenloch.

Sie sollten eine NSDateFormattermit dateFormatset verwenden und localegezwungen sein, en_US_POSIXDaten zu empfangen (von Servern / APIs).

Dann sollten Sie NSDateFormatterfür die Benutzeroberfläche eine andere verwenden, für die Sie die timeStyle/ dateStyle-Eigenschaften festlegen. Auf diese Weise haben Sie keine explizite dateFormatFestlegung selbst und gehen fälschlicherweise davon aus, dass das Format verwendet wird.

Dies bedeutet, dass die Benutzeroberfläche von den Benutzereinstellungen abhängt (morgens / abends gegenüber 24 Stunden und Datumszeichenfolgen, die gemäß den iOS-Einstellungen korrekt nach Benutzerauswahl formatiert wurden), während Datumsangaben, die in Ihre App "eingehen", immer korrekt auf ein NSDatefür "analysiert" werden Sie zu verwenden.

Daniel
quelle
Manchmal funktioniert dieses Schema, manchmal nicht. Eine Gefahr besteht darin, dass Ihre Methode möglicherweise das Datumsformat des Formatierers ändern muss und dabei das Format ändern muss, das von dem Code festgelegt wurde, der Sie aufgerufen hat, als es sich mitten in Datumsformatierungsvorgängen befand. Es gibt andere Szenarien, in denen die Zeitzone wiederholt geändert werden muss.
Hot Licks
Ich weiß nicht, warum eine Änderung des timeZoneWertes des Formatierers diesem Schema im Wege stehen würde. Könnten Sie das näher erläutern? Um ganz klar zu sein, würden Sie auch auf eine Änderung des Formats verzichten. Wenn Sie dies tun müssen, geschieht dies bei einem "Import" -Formatierer, also einem separaten Formatierer.
Daniel
Jedes Mal, wenn Sie den Status eines globalen Objekts ändern, ist dies gefährlich. Leicht zu vergessen, dass andere es auch benutzen.
Hot Licks
3

Hier ist die Lösung für dieses Problem in der schnellen Version. In Swift können wir Erweiterung anstelle von Kategorie verwenden. Also, hier habe ich die Erweiterung für den DateFormatter erstellt und darin gibt initWithSafeLocale den DateFormatter mit dem entsprechenden Gebietsschema zurück. Hier in unserem Fall en_US_POSIX. Abgesehen davon wurden auch einige Methoden zur Datumsbildung bereitgestellt.

  • Swift 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
    }   }
  • Verwendungsbeschreibung:

    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))")
Technik
quelle