174 votes

Quelle est la meilleure façon de traiter la locale "feechur" de NSDateFormatter ?

Il semble que NSDateFormatter a une "fonction" qui vous mord à l'improviste : Si vous effectuez une simple opération de format "fixe" telle que :

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

Ensuite, cela fonctionne bien aux États-Unis et dans la plupart des pays, JUSQU'À ce que quelqu'un dont le téléphone est réglé sur une région de 24 heures mette le commutateur 12/24 heures sur 12 dans les paramètres. Le système commence alors à ajouter "AM" ou "PM" à la fin de la chaîne de caractères résultante.

(Voir par exemple NSDateFormatter, est-ce que je fais quelque chose de mal ou est-ce que c'est un bug ? )

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

Apparemment, Apple a déclaré qu'il s'agissait d'un problème "BAD" (Broken As Designed) et n'a pas l'intention de le résoudre.

La solution consiste apparemment à définir la locale du formateur de date pour une région spécifique, généralement les États-Unis, mais c'est un peu compliqué :

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

Ce n'est pas si mal, mais j'ai affaire à une dizaine d'applications différentes, et la première que je consulte contient 43 cas de ce scénario.

Alors, des idées astucieuses pour une macro/classe surchargée/quoi que ce soit pour minimiser l'effort de tout changer, sans rendre le code trop obscur ? (Mon premier réflexe est de surcharger NSDateFormatter avec une version qui définirait la locale dans la méthode init. Il faut changer deux lignes - la ligne alloc/init et l'import ajouté).

Ajouté

Voici ce que j'ai trouvé jusqu'à présent - cela semble fonctionner dans tous les cas de figure :

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

J'attribuerai la prime à la meilleure suggestion/critique (légitime) que je verrai d'ici mardi à la mi-journée. (Voir ci-dessous -- délai prolongé).

Mise à jour

Concernant la proposition de l'OMZ, voici ce que je constate

Voici la version de la catégorie -- fichier h :

#import <Foundation/Foundation.h>

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

Fichier de catégorie 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

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

Le résultat :

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)

Le téléphone (ou plutôt l'iPod Touch) est réglé sur la Grande-Bretagne et le commutateur 12/24 sur 12. Il y a une nette différence entre les deux résultats, et je considère que la version de la catégorie est erronée. Notez que le journal de la version de la catégorie EST exécuté (et que les arrêts placés dans le code sont atteints), de sorte qu'il ne s'agit pas simplement d'un cas où le code n'est pas utilisé d'une manière ou d'une autre.

Mise à jour de la prime :

Comme je n'ai pas encore reçu de réponses pertinentes, je prolonge le délai de la prime d'un jour ou deux.

La prime se termine dans 21 heures - elle ira à celui qui fera le plus d'efforts pour aider, même si la réponse n'est pas vraiment utile dans mon cas.

Une observation curieuse

La mise en œuvre de la catégorie a été légèrement modifiée :

#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

J'ai simplement changé le nom de la variable locale statique (au cas où il y aurait un conflit avec la variable statique déclarée dans la sous-classe) et j'ai ajouté le NSLog supplémentaire. Mais regardez ce que ce 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

Comme vous pouvez le constater, le setLocale n'a tout simplement pas fonctionné. La locale du formateur est toujours en_GB. Il semble qu'il y ait quelque chose d'étrange à propos d'une méthode init dans une catégorie.

Réponse finale

Voir le réponse acceptée ci-dessous.

5 votes

Moshe, je ne sais pas pourquoi vous avez choisi de modifier le titre. "Feechur" est un terme légitime dans l'art (et ce depuis une trentaine d'années), qui désigne un aspect ou une caractéristique d'un logiciel qui est suffisamment mal conçu pour être considéré comme un bogue, même si les auteurs refusent de l'admettre.

1 votes

Lors de la conversion d'une chaîne en date, la chaîne doit correspondre exactement à la description du formateur - il s'agit d'une question qui n'a rien à voir avec celle de la localité.

0 votes

Les différentes chaînes de dates sont là pour tester les différentes configurations possibles, correctes et erronées. Je sais que certaines d'entre elles sont invalides, compte tenu de la chaîne de formatage.

68voto

Hot Licks Points 25075

Duh !!

Parfois, vous avez un moment "Aha !", parfois c'est plutôt "Duh !". C'est ce dernier cas qui s'est présenté. Dans la catégorie des initWithSafeLocale le "super" init a été codée comme self = [super init]; . Il s'agit de la SUPERCLASSE de NSDateFormatter mais ne le fait pas init les NSDateFormatter lui-même.

Apparemment, lorsque cette initialisation est ignorée, setLocale "rebondit", probablement à cause d'une structure de données manquante dans l'objet. La modification de la init a self = [self init]; provoque la NSDateFormatter l'initialisation doit avoir lieu, et setLocale est à nouveau heureux.

Voici la source "finale" du .m de la catégorie :

#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

0 votes

Quel sera le formatage de la date pour "NSString *dateStr = @"2014-04-05T04:00:00.000Z" ;" ?

0 votes

@Agent - Cherchez : unicode.org/reports/tr35/tr35-31/

0 votes

@tbag - Votre question ne devrait-elle pas porter sur NSDateFormatter ?

40voto

omz Points 38947

Au lieu de créer une sous-classe, vous pourriez créer un fichier NSDateFormatter avec un initialisateur supplémentaire qui se charge d'assigner la locale et éventuellement une chaîne de format, afin d'avoir un formateur prêt à l'emploi juste après l'avoir initialisé.

@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

Vous pouvez alors utiliser NSDateFormatter n'importe où dans votre code avec seulement :

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

Vous pouvez préfixer votre méthode de catégorie d'une manière ou d'une autre pour éviter les conflits de noms, au cas où Apple déciderait d'ajouter une telle méthode dans une future version du système d'exploitation.

Si vous utilisez toujours le(s) même(s) format(s) de date, vous pouvez également ajouter des méthodes de catégorie qui renvoient des instances singleton avec certaines configurations (quelque chose comme +sharedRFC3339DateFormatter ). Sachez toutefois que NSDateFormatter n'est pas à l'épreuve des threads et vous devez utiliser des verrous ou des @synchronized lorsque vous utilisez la même instance à partir de plusieurs threads.

0 votes

Est-ce que le fait d'avoir un NSLocale statique (comme dans ma suggestion) fonctionnerait dans une catégorie ?

0 votes

Oui, cela devrait également fonctionner dans une catégorie. Je l'ai laissé de côté pour simplifier l'exemple.

0 votes

Curieusement, l'approche par catégorie ne fonctionne pas. La méthode de la catégorie est exécutée, et elle obtient exactement le même Locale que l'autre version (je les exécute l'une après l'autre, la version de la catégorie en premier). C'est juste que le setLocale ne prend apparemment pas.

7voto

Daniel Points 14150

Puis-je suggérer quelque chose de totalement différent parce que, pour être honnête, tout ceci est un peu en train de s'enfoncer dans un trou de lapin.

Vous devriez en utiliser un NSDateFormatter avec dateFormat et locale contraint à en_US_POSIX pour la réception de dates (provenant de serveurs/API).

Dans ce cas, vous devez utiliser un autre NSDateFormatter pour l'interface utilisateur dans laquelle vous définirez le timeStyle / dateStyle de cette façon, vous n'avez pas à utiliser une propriété explicite de dateFormat défini par vous-même, ce qui permet de supposer à tort que ce format sera utilisé.

Cela signifie que l'interface utilisateur est pilotée par les préférences de l'utilisateur (am/pm vs 24 heures, et chaînes de dates formatées correctement selon le choix de l'utilisateur - à partir des paramètres iOS), tandis que les dates qui "entrent" dans votre application sont toujours "analysées" correctement pour un NSDate à utiliser.

3voto

Tech Points 341

Voici la solution à ce problème dans la version swift. En swift, nous pouvons utiliser l'extension au lieu de la catégorie. J'ai donc créé une extension pour le DateFormatter et à l'intérieur de celle-ci, initWithSafeLocale renvoie le DateFormatter avec le Locale approprié, ici dans notre cas c'est en_US_POSIX, à part cela, j'ai aussi fourni quelques méthodes de formation de date.

  • 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
    }   }
  • description de l'utilisation :

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X