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.


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

qual será o formatador de data para "NSString * dateStr = @" 2014-04-05T04: 00: 00.000Z ";" ?
— Agente Chocks.


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


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.


À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))")
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.