¿Cuál es la mejor manera de lidiar con la configuración regional “feechur” de NSDateFormatter?

168

Parece que NSDateFormatter tiene una "característica" que te muerde inesperadamente: si haces una operación de formato "fijo" simple como:

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

Entonces funciona bien en los EE. UU. Y en la mayoría de los lugares HASTA ... alguien con su teléfono configurado en una región de 24 horas establece el interruptor de 12/24 horas en la configuración a 12. Luego lo anterior comienza a agregar "AM" o "PM" en El final de la cadena resultante.

(Ver, por ejemplo, NSDateFormatter, ¿estoy haciendo algo mal o es un error? )

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

Aparentemente, Apple ha declarado que esto es "MALO", roto como está diseñado, y no lo van a arreglar.

Aparentemente, la elusión es establecer la configuración regional del formateador de fecha para una región específica, generalmente en los Estados Unidos, pero esto es un poco desordenado:

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

No está mal en onsies-twosies, pero estoy lidiando con unas diez aplicaciones diferentes, y la primera que miro tiene 43 instancias de este escenario.

Entonces, ¿alguna idea inteligente para una macro / clase anulada / lo que sea para minimizar el esfuerzo de cambiar todo, sin hacer que el código se oscurezca? (Mi primer instinto es anular NSDateFormatter con una versión que establezca la configuración regional en el método init. Requiere cambiar dos líneas: la línea alloc / init y la importación agregada).

Adicional

Esto es lo que se me ocurrió hasta ahora: parece funcionar en todos los escenarios:

@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

¡Generosidad!

Otorgaré la recompensa a la mejor sugerencia / crítica (legítima) que vea hasta el mediodía del martes. [Ver abajo - plazo extendido]

Actualizar

Re propuesta de OMZ, esto es lo que estoy encontrando:

Aquí está la versión de categoría - archivo h:

#import <Foundation/Foundation.h>


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

Archivo de categoría 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

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

El 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)

El teléfono [hace que un iPod Touch] esté configurado en Gran Bretaña, con el interruptor 12/24 configurado en 12. Hay una clara diferencia en los dos resultados, y considero que la versión de categoría es incorrecta. Tenga en cuenta que el registro en la versión de categoría SE está ejecutando (y las paradas colocadas en el código se golpean), por lo que no se trata simplemente de que el código de alguna manera no se esté utilizando.

Actualización de recompensa:

Como aún no he recibido ninguna respuesta aplicable, extenderé el plazo de recompensa por otro día o dos.

Bounty finaliza en 21 horas: irá a quien haga el mayor esfuerzo para ayudar, incluso si la respuesta no es realmente útil en mi caso.

Una curiosa observación

Se modificó ligeramente la implementación de la categoría:

#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

Básicamente, solo cambié el nombre de la variable de entorno local estático (en caso de que hubiera algún conflicto con el estático declarado en la subclase) y agregó el NSLog adicional. Pero mira lo que imprime 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

Como puede ver, el setLocale simplemente no lo hizo. La configuración regional del formateador sigue siendo en_GB. Parece que hay algo "extraño" sobre un método init en una categoría.

Respuesta final

Vea la respuesta aceptada a continuación.

Hot Licks
fuente
55
Moshe, no sé por qué elegiste editar el título. "Feechur" es un término legítimo en el arte (y lo ha sido durante 30 años más o menos), lo que significa un aspecto o característica de algún software que está suficientemente mal concebido para ser considerado un error, a pesar de que los autores se niegan a admitirlo.
Hot Licks
1
al convertir una cadena a la fecha, la cadena debe coincidir exactamente con la descripción del formateador; este es un problema tangencial para su localidad.
bshirley
Las diversas cadenas de fecha están allí para probar las diferentes configuraciones posibles, correctas y erróneas. Sé que algunos de ellos no son válidos, dada la cadena de formato.
Hot Licks
¿Has experimentado con diferentes valores de - (NSDateFormatterBehavior)formatterBehavior?
bshirley
No he experimentado con eso. La especificación es contradictoria sobre si incluso se puede cambiar en iOS. La descripción principal dice "Nota de iOS: iOS solo admite el comportamiento 10.4+", mientras que la sección NSDateFormatterBehavior dice que ambos modos están disponibles (pero puede que solo esté hablando de las constantes).
Hot Licks

Respuestas:

67

Duh !!

A veces tienes un "¡Ajá!" momento, a veces es más un "¡Duh!" Este es el último. En la categoría para initWithSafeLocaleel "super" initse codificó como self = [super init];. Esto inicia la SUPERCLASE de NSDateFormatterpero no initelNSDateFormatter objeto en sí.

Aparentemente, cuando se omite esta inicialización, setLocale"rebota", presumiblemente debido a la falta de una estructura de datos en el objeto. Al cambiar inita, se self = [self init];produce la NSDateFormatterinicialización ysetLocale es feliz nuevamente.

Aquí está la fuente "final" para el .m de la categoría:

#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
fuente
cuál será el formateador de fecha para "NSString * dateStr = @" 2014-04-05T04: 00: 00.000Z ";" ?
Agente de cuñas.
@Agent - Búscalo: unicode.org/reports/tr35/tr35-31/…
Hot Licks
@tbag - ¿No debería ser tu pregunta sobre NSDateFormatter?
Hot Licks
@ HotLicks sí, mi mal. Yo carne NSDateFormatter.
tbag
@tbag - ¿Qué dice la especificación?
Hot Licks
41

En lugar de subclasificar, podría crear una NSDateFormattercategoría con un inicializador adicional que se encargue de asignar la configuración regional y posiblemente también una cadena de formato, para que tenga un formateador listo para usar justo después de inicializarlo.

@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

Entonces podría usar NSDateFormattercualquier parte de su código con solo:

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

Es posible que desee prefijar su método de categoría de alguna manera para evitar conflictos de nombres, en caso de que Apple decida agregar dicho método en una versión futura del sistema operativo.

En caso de que siempre use los mismos formatos de fecha, también puede agregar métodos de categoría que devuelvan instancias de singleton con ciertas configuraciones (algo así como +sharedRFC3339DateFormatter). Sin embargo, tenga en cuenta que NSDateFormatterno es seguro para subprocesos y que debe usar bloqueos o @synchronizedbloqueos cuando usa la misma instancia de múltiples subprocesos.

omz
fuente
¿Tener un NSLocale estático (como en mi sugerencia) funcionaría en una categoría?
Hot Licks
Sí, eso también debería funcionar en una categoría. Lo dejé fuera para simplificar el ejemplo.
omz
Curiosamente, el enfoque de categoría no funciona. El método de categoría se ejecuta y está obteniendo exactamente la misma configuración regional que la otra versión (los ejecuto de forma consecutiva, primero la versión de categoría). De alguna manera, el setLocale aparentemente no "toma".
Hot Licks
Sería interesante descubrir por qué este enfoque no funciona. Si a nadie se le ocurre algo mejor, otorgaré la recompensa a la mejor explicación de este aparente error.
Hot Licks
Bueno, estoy otorgando la recompensa a OMZ, ya que él es el único que hizo un esfuerzo aparente en esto.
Hot Licks
7

¿Puedo sugerir algo totalmente diferente porque para ser honesto, todo esto está corriendo por un agujero de conejo?

Debería usar uno NSDateFormattercon dateFormatset y localeforzado a en_US_POSIXrecibir fechas (de servidores / API).

Entonces, debería usar una NSDateFormatterinterfaz de usuario diferente para la que establecerá las propiedades timeStyle/ dateStyle, de esta manera no tendrá un dateFormatconjunto explícito usted mismo, por lo tanto, suponiendo falsamente que se usará ese formato.

Esto significa que la interfaz de usuario depende de las preferencias del usuario (am / pm frente a 24 horas, y cadenas de fecha formateadas correctamente a elección del usuario, desde la configuración de iOS), mientras que las fechas que están "entrando" en su aplicación siempre se están "analizando" correctamente NSDatepara usted para usar

Daniel
fuente
A veces este esquema funciona, a veces no. Un peligro es que su método puede necesitar modificar el formato de fecha del formateador y, al hacerlo, alterar el formato establecido por el código que lo llamó, cuando estaba en el medio de las operaciones de formateo de fecha. Hay otros escenarios en los que la zona horaria debe cambiarse repetidamente.
Hot Licks
No sé por qué cambiar el timeZonevalor del formateador se interpondría en este esquema, ¿podría explicarlo? También para ser claro, se abstendrá de cambiar el formato. Si necesita hacerlo, esto sucedería en un formateador "importado", por lo tanto, en un formateador separado.
Daniel
Cada vez que cambia el estado de un objeto global es peligroso. Es fácil olvidar que otros también lo están usando.
Hot Licks
3

Aquí está la solución para ese problema en la versión rápida. En swift podemos usar extensión en lugar de categoría. Entonces, aquí he creado la extensión para DateFormatter y dentro de ese initWithSafeLocale devuelve DateFormatter con la configuración regional relevante, aquí en nuestro caso es en_US_POSIX, aparte de eso también proporcionó un par de métodos de formación de fecha.

  • 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
    }   }
  • descripción 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))")
Tecnología
fuente