Qual è il modo migliore per gestire le impostazioni internazionali "feechur" di NSDateFormatter?


168

Sembra che NSDateFormatterabbia una "caratteristica" che ti morde inaspettatamente: se esegui una semplice operazione di formattazione "fissa" come:

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

Quindi funziona bene negli Stati Uniti e nella maggior parte dei locali FINO A ... qualcuno con il telefono impostato su una regione di 24 ore imposta l'interruttore di 12/24 ore nelle impostazioni su 12. Quindi quanto sopra inizia a virare su "AM" o "PM" su la fine della stringa risultante.

(Vedi, ad esempio, NSDateFormatter, sto facendo qualcosa di sbagliato o è un bug? )

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

Apparentemente Apple ha dichiarato che questo è "BAD" - Broken As Designed, e non lo risolveranno.

L'elusione è apparentemente quella di impostare le impostazioni locali del formatter data per una regione specifica, generalmente gli Stati Uniti, ma questo è un po 'disordinato:

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

Non male in onsies-twosies, ma ho a che fare con una decina di app diverse e la prima che guardo ha 43 istanze di questo scenario.

Quindi qualche idea intelligente per una macro / classe ignorata / qualunque cosa per minimizzare lo sforzo di cambiare tutto, senza rendere il codice oscuro? (Il mio primo istinto è quello di sovrascrivere NSDateFormatter con una versione che imposti le impostazioni internazionali nel metodo init. Richiede la modifica di due righe: la riga alloc / init e l'importazione aggiunta.)

aggiunto

Questo è quello che ho escogitato finora - sembra funzionare in tutti gli scenari:

@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

Bounty!

Assegnerò la generosità alla migliore (legittima) proposta / critica che vedrò martedì a metà giornata. [Vedi sotto - scadenza estesa.]

Aggiornare

Per quanto riguarda la proposta di OMZ, ecco cosa sto trovando -

Ecco la versione della categoria - file h:

#import <Foundation/Foundation.h>


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

File categoria 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

Il codice:

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

Il risultato:

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)

Il telefono [fa che un iPod Touch] sia impostato sulla Gran Bretagna, con l'interruttore 12/24 impostato su 12. C'è una chiara differenza nei due risultati e ritengo che la versione della categoria sia errata. Si noti che il registro nella versione della categoria viene eseguito (e vengono bloccati gli stop inseriti nel codice), quindi non è semplicemente un caso in cui il codice in qualche modo non viene utilizzato.

Aggiornamento di Bounty:

Dal momento che non ho ancora ricevuto alcuna risposta applicabile, prorogherò la scadenza per un altro giorno o due.

Bounty termina tra 21 ore - andrà a chiunque faccia il massimo sforzo per aiutare, anche se la risposta non è davvero utile nel mio caso.

Un'osservazione curiosa

Modificata leggermente l'implementazione della categoria:

#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

Fondamentalmente ha appena cambiato il nome della variabile locale statica (nel caso ci fosse qualche conflitto con la statica dichiarata nella sottoclasse) e ha aggiunto il NSLog extra. Ma guarda cosa stampa 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

Come puoi vedere, setLocale semplicemente no. La locale del formatter è ancora en_GB. Sembra che ci sia qualcosa di "strano" in un metodo init in una categoria.

Risposta finale

Vedi la risposta accettata di seguito.


5
Moshe, non so perché hai scelto di modificare il titolo. "Feechur" è un termine legittimo nell'arte (ed era stato per circa 30 anni), che significa un aspetto o una caratteristica di alcuni software che è sufficientemente mal concepito per essere considerato un bug, anche se gli autori si rifiutano di ammetterlo.
Hot Licks,

1
quando converti una stringa fino ad oggi, la stringa deve corrispondere esattamente alla descrizione del formatter - questo è un problema tangenziale a quello della tua località.
bshirley,

Le varie stringhe di date sono lì per testare le diverse possibili configurazioni, corrette ed errate. So che alcuni di essi non sono validi, data la stringa di formattazione.
Hot Licks,

hai sperimentato diversi valori di - (NSDateFormatterBehavior)formatterBehavior?
bshirley,

Non l'ho provato. Le specifiche sono contraddittorie sul fatto che possano essere modificate in iOS. La descrizione principale dice "Nota iOS: iOS supporta solo il comportamento 10.4+", mentre la sezione NSDateFormatterBehavior dice che entrambe le modalità sono disponibili (ma potrebbe solo parlare delle costanti).
Hot Licks,

Risposte:


67

Duh !!

A volte hai un "Aha !!" momento, a volte è più un "Duh !!" Questo è il secondo. Nella categoria per initWithSafeLocaleil "super" è initstato codificato come self = [super init];. Questo entra nella SUPERCLASS NSDateFormatterma non initl' NSDateFormatteroggetto stesso.

Apparentemente quando questa inizializzazione viene ignorata, setLocale"rimbalza", presumibilmente a causa di una struttura di dati mancante nell'oggetto. La modifica di initto self = [self init];provoca l' NSDateFormatterinizializzazione ed setLocaleè di nuovo felice.

Ecco la fonte "finale" per il .m della categoria:

#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

quale sarà il formattatore della data per "NSString * dateStr = @" 2014-04-05T04: 00: 00.000Z ";" ?
Agente cunei.


@tbag - La tua domanda non dovrebbe riguardare NSDateFormatter?
Hot Licks

@HotLicks sì mio male. Carnei NSDateFormatter.
tbag

@tbag - Cosa dice la specifica?
Hot Licks

41

Invece di eseguire la sottoclasse, è possibile creare una NSDateFormattercategoria con un inizializzatore aggiuntivo che si occupa di assegnare la locale e possibilmente anche una stringa di formato, in modo da avere un formattatore pronto all'uso subito dopo l'inizializzazione.

@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

Quindi puoi usare NSDateFormatterovunque nel tuo codice con solo:

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

Potresti voler aggiungere il prefisso al tuo metodo di categoria in qualche modo per evitare conflitti di nomi, nel caso in cui Apple decida di aggiungere un tale metodo in una versione futura del sistema operativo.

Nel caso in cui utilizzi sempre gli stessi formati di data, potresti anche aggiungere metodi di categoria che restituiscono istanze singleton con determinate configurazioni (qualcosa del genere +sharedRFC3339DateFormatter). Tieni presente, tuttavia, che NSDateFormatternon è thread-safe e devi usare i lucchetti o@synchronized blocchi quando usi la stessa istanza da più thread.


Avere un NSLocale statico (come nel mio suggerimento) funzionerebbe in una categoria?
Hot Licks

Sì, dovrebbe funzionare anche in una categoria. L'ho lasciato fuori per rendere l'esempio più semplice.
omz,

Curiosamente, l'approccio di categoria non funziona. Il metodo di categoria viene eseguito e sta ottenendo esattamente le stesse impostazioni locali dell'altra versione (le eseguo di volta in volta, prima la versione della categoria). Solo in qualche modo apparentemente il setLocale non "prende".
Hot Licks,

Sarebbe interessante scoprire perché questo approccio non funziona. Se nessuno esce con qualcosa di meglio assegnerò la generosità alla migliore spiegazione di questo apparente errore.
Hot Licks,

Bene, sto assegnando la generosità a OMZ, dal momento che è l'unico che ha fatto uno sforzo evidente su questo.
Hot Licks,

7

Vorrei suggerire qualcosa di completamente diverso perché, a dire il vero, tutto questo sta in qualche modo correndo in una tana di coniglio.

Dovresti usarne uno NSDateFormattercon dateFormatset e localecostretto a farloen_US_POSIX per ricevere le date (dai server / API).

Quindi dovresti usare un diverso NSDateFormatterper l'interfaccia utente che imposterai le proprietà timeStyle/ dateStyle- in questo modo non hai un esplicitodateFormat set da solo, quindi assumendo falsamente che verrà utilizzato quel formato.

Ciò significa che l'interfaccia utente è guidata dalle preferenze dell'utente (am / pm vs 24 ore e stringhe di date formattate correttamente in base alla scelta dell'utente - dalle impostazioni di iOS), mentre le date che "entrano" nella tua app vengono sempre "analizzate" correttamente in un NSDatefor tu da usare.


A volte questo schema funziona, a volte no. Un pericolo è che il tuo metodo potrebbe dover modificare il formato della data del formattatore e, nel fare ciò, alterare il formato impostato dal codice che ti ha chiamato, quando era nel mezzo delle operazioni di formattazione della data. Esistono altri scenari in cui il fuso orario deve essere modificato ripetutamente.
Hot Licks

Non so perché cambiare il timeZonevalore del formatter potrebbe ostacolare questo schema, potresti approfondire? Inoltre, per essere chiari, ti asteresti dal cambiare il formato. Se è necessario farlo, ciò accadrà su un formattatore "import", quindi un formattatore separato.
Daniel

Ogni volta che stai cambiando lo stato di un oggetto globale è pericoloso. Facile dimenticare che anche altri lo stanno usando.
Hot Licks

3

Ecco la soluzione per questo problema nella versione rapida. In breve tempo possiamo usare l'estensione invece della categoria. Quindi, qui ho creato l'estensione per DateFormatter e all'interno di initWithSafeLocale restituisce DateFormatter con le impostazioni internazionali pertinenti, qui nel nostro caso che è en_US_POSIX, oltre a quello fornito anche un paio di metodi di formazione della data.

  • 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
    }   }
  • descrizione dell'uso:

    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))")
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.