Qual é a melhor maneira de lidar com o locale do NSDateFormatter "feechur"?

168

Parece que NSDateFormatterpossui um "recurso" que o morde inesperadamente: se você fizer uma operação simples de formato "fixo", como:

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

Funciona bem nos EUA e na maioria das localidades, ATÉ QUE ... alguém com o telefone definido para uma região de 24 horas define o interruptor de 12/24 horas nas configurações para 12. Em seguida, o item acima começa a colocar "AM" ou "PM" em o fim da sequência resultante.

(Veja, por exemplo, NSDateFormatter, estou fazendo algo errado ou isso é um bug? )

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

Aparentemente, a Apple declarou que isso é "MAU" - Quebrado como projetado, e eles não vão corrigi-lo.

Aparentemente, a evasão define o local do formatador de datas para uma região específica, geralmente os EUA, mas isso é um pouco confuso:

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

Não é tão ruim em twosies customizáveis, mas estou lidando com cerca de dez aplicativos diferentes, e o primeiro que eu olho tem 43 instâncias desse cenário.

Então, alguma idéia inteligente para uma classe macro / substituída / o que quer que seja para minimizar o esforço de mudar tudo, sem tornar o código obscuro? (Meu primeiro instinto é substituir o NSDateFormatter por uma versão que defina a localidade no método init. Requer a alteração de duas linhas - a linha de alocação / inicialização e a importação adicionada.)

Adicionado

Isto é o que eu criei até agora - parece funcionar em todos os cenários:

@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

Recompensa!

Atribuirei a recompensa à melhor sugestão / crítica (legítima) que vejo até o meio-dia de terça-feira. [Veja abaixo - prazo estendido.]

Atualizar

Re proposta da OMZ, aqui está o que eu estou encontrando -

Aqui está a versão da categoria - arquivo h:

#import <Foundation/Foundation.h>


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

Arquivo da 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

O código:

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

O resultado:

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)

O telefone [faz com que o iPod Touch] esteja definido para a Grã-Bretanha, com o interruptor 12/24 definido como 12. Há uma clara diferença nos dois resultados, e considero a versão da categoria errada. Observe que o log na versão da categoria está sendo executado (e as paradas colocadas no código são atingidas); portanto, não é apenas um caso do código que de alguma forma não está sendo usado.

Atualização de recompensa:

Como ainda não recebi nenhuma resposta aplicável, estenderei o prazo para mais um ou dois dias.

A recompensa termina em 21 horas - vai para quem fizer mais esforço para ajudar, mesmo que a resposta não seja realmente útil no meu caso.

Uma observação curiosa

Modificou a implementação da categoria ligeiramente:

#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

Basicamente, apenas alterei o nome da variável de localidade estática (caso houvesse algum conflito com a estática declarada na subclasse) e adicionei o NSLog extra. Mas veja o que o NSLog imprime:

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

Como você pode ver, o setLocale simplesmente não. O código do idioma do formatador ainda é en_GB. Parece que há algo "estranho" em um método init em uma categoria.

Resposta final

Veja a resposta aceita abaixo.

Hot Licks
fonte
5
Moshe, não sei por que você escolheu editar o título. "Feechur" é um termo legítimo na arte (e existia há 30 anos), significando um aspecto ou recurso de algum software que é suficientemente mal concebido para ser considerado um bug, mesmo que os autores se recusem a admiti-lo.
Hot Licks
1
ao converter uma string em data, ela deve corresponder exatamente à descrição do formatador - esse é um problema tangencial ao da sua localidade.
22411 bshirley
As várias seqüências de datas estão lá para testar as diferentes configurações possíveis, corretas e erradas. Eu sei que alguns deles são inválidos, dada a seqüência de formatação.
Hot Licks
você já experimentou diferentes valores de - (NSDateFormatterBehavior)formatterBehavior?
22411 bshirley
Ainda não experimentei. A especificação é contraditória se ela pode ser alterada no iOS. A descrição principal diz "iOS Nota: o iOS suporta apenas o comportamento 10.4 ou superior", enquanto a seção NSDateFormatterBehavior diz que ambos os modos estão disponíveis (mas pode estar falando apenas das constantes).
Hot Licks

Respostas:

67

Duh !!

Às vezes você tem um "Aha !!" momento, às vezes é mais um "Duh !!" Este é o último. Na categoria para initWithSafeLocaleo "super" initfoi codificado como self = [super init];. Isso inicia a SUPERCLASS NSDateFormattermas não inito NSDateFormatterobjeto em si.

Aparentemente, quando essa inicialização é ignorada, setLocale"retorna", presumivelmente por causa de alguma estrutura de dados ausente no objeto. Alterar o initpara self = [self init];faz com que a NSDateFormatterinicialização ocorra e setLocalefica feliz novamente.

Aqui está a fonte "final" para o .m da 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
Hot Licks
fonte
qual será o formatador de data para "NSString * dateStr = @" 2014-04-05T04: 00: 00.000Z ";" ?
Agente Chocks.
@Agent - Procure: unicode.org/reports/tr35/tr35-31/…
Hot Licks
@tbag - Sua pergunta não deveria ser sobre NSDateFormatter?
Hot Licks
@HotLicks sim meu mal. Eu carne NSDateFormatter.
tbag
@tbag - O que dizem as especificações?
Hot Licks
41

Em vez de subclassificar, você pode criar uma NSDateFormattercategoria com um inicializador adicional que se encarrega de atribuir a localidade e, possivelmente, também uma string de formato, para que você tenha um formatador pronto para uso logo após inicializá-lo.

@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

Então você pode usar NSDateFormatterqualquer lugar no seu código com apenas:

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

Você pode prefixar seu método de categoria de alguma forma para evitar conflitos de nome, caso a Apple decida adicionar esse método em uma versão futura do sistema operacional.

Caso você esteja sempre usando o (s) mesmo (s) formato (s) de data, você também pode adicionar métodos de categoria que retornam instâncias singleton com determinadas configurações (algo como +sharedRFC3339DateFormatter). Esteja ciente, no entanto, de que NSDateFormatternão é seguro para threads e você precisa usar bloqueios ou @synchronizedblocos quando estiver usando a mesma instância de vários threads.

omz
fonte
Ter um NSLocale estático (como na minha sugestão) funcionaria em uma categoria?
Hot Licks
Sim, isso também deve funcionar em uma categoria. Deixei de fora para tornar o exemplo mais simples.
omz 10/07/11
Curiosamente, a abordagem por categoria não funciona. O método da categoria é executado e está obtendo exatamente o mesmo código de idioma que a outra versão (eu os executo novamente, a versão da categoria primeiro). De alguma forma, o setLocale aparentemente não "aceita".
Hot Licks
Seria interessante descobrir por que essa abordagem não funciona. Se ninguém encontrar algo melhor, atribuirei a recompensa à melhor explicação desse bug aparente.
Hot Licks
Bem, estou concedendo a recompensa à OMZ, já que ele é o único que fez um aparente esforço nisso.
Hot Licks
7

Posso sugerir algo totalmente diferente, porque, para ser sincero, tudo isso está correndo pela toca de um coelho.

Você deve usar um NSDateFormattercom dateFormatset e localeforçado a en_US_POSIXreceber datas (de servidores / APIs).

Então você deve usar um diferente NSDateFormatterpara a interface do usuário que definirá as propriedades timeStyle/ dateStyle- dessa forma, você não terá um dateFormatconjunto explícito , assumindo falsamente que o formato será usado.

Isto significa UI é impulsionado por preferências do usuário (am / pm vs 24 horas, e cordas data formatada corretamente a escolha do usuário - a partir das configurações do iOS), enquanto datas que são "que entram em" seu aplicativo estão sempre sendo "analisado" corretamente a um NSDatepara você usar.

Daniel
fonte
Às vezes, esse esquema funciona, às vezes não. Um perigo é que seu método pode precisar modificar o formato da data do formatador e, ao fazê-lo, alterar o formato definido pelo código que o chamou quando estava no meio das operações de formatação da data. Existem outros cenários em que o fuso horário deve ser alterado repetidamente.
Hot Licks
Não sei por que a alteração do timeZonevalor do formatador atrapalharia esse esquema, você poderia elaborar? Também para ficar claro, você se absteria de alterar o formato. Se você precisar fazer isso, isso aconteceria em um formatador "import", portanto, em um formatador separado.
Daniel
Sempre que você altera o estado de um objeto global, é perigoso. É fácil esquecer que outras pessoas também o estão usando.
Hot Licks
3

Aqui está a solução para esse problema na versão rápida. No swift, podemos usar extensão em vez de categoria. Então, aqui eu criei a extensão para o DateFormatter e, dentro dele, initWithSafeLocale retorna o DateFormatter com o local relevante.

  • 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
    }   }
  • descrição de 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))")
Tecnologia
fonte