Quelle est la meilleure façon de gérer la locale «feechur» de NSDateFormatter?

168

Il semble que cela NSDateFormattera une "fonctionnalité" qui vous mord de manière inattendue: 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 régions JUSQU'À ce que ... quelqu'un avec son téléphone réglé sur une région de 24 heures règle le commutateur 12/24 heures dans les paramètres sur 12. Ensuite, ce qui précède commence à clouer "AM" ou "PM" sur la fin de la chaîne résultante.

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

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

Apparemment, Apple a déclaré que c'était "MAUVAIS" - Cassé comme conçu, et ils ne vont pas le réparer.

Le contournement consiste apparemment à définir les paramètres régionaux 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];

Pas trop mal dans onsies-twosies, mais j'ai affaire à une dizaine d'applications différentes, et la première que je regarde contient 43 instances de ce scénario.

Alors, des idées intelligentes pour une macro / classe remplacée / quoi que ce soit pour minimiser l'effort de tout changer, sans rendre le code obscur? (Mon premier instinct est de remplacer NSDateFormatter par une version qui définirait les paramètres régionaux dans la méthode init. Nécessite de changer deux lignes - la ligne alloc / init et l'importation ajoutée.)

Ajoutée

C'est ce que j'ai proposé jusqu'à présent - semble fonctionner dans tous les scénarios:

@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

Prime!

Je vais attribuer la prime à la meilleure suggestion / critique (légitime) que je vois d'ici mardi midi. [Voir ci-dessous - délai prolongé.]

Mettre à jour

Concernant la proposition d'OMZ, voici ce que je trouve -

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 [faites qu'un iPod Touch] est réglé sur la Grande-Bretagne, avec le commutateur 12/24 réglé sur 12. Il y a une nette différence entre les deux résultats, et je juge que la version de la catégorie est fausse. Notez que le journal dans la version de catégorie EST en cours d'exécution (et les arrêts placés dans le code sont frappés), donc ce n'est pas simplement 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 applicables, je prolongerai la date limite de prime d'un jour ou deux.

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

Une observation curieuse

Modifié légèrement l'implémentation de la catégorie:

#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

Fondamentalement, il suffit de changer le nom de la variable locale statique (au cas où il y aurait un conflit avec le statique déclaré dans la sous-classe) et d'ajouter le NSLog supplémentaire. Mais regardez ce que 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 voir, le setLocale ne l'a tout simplement pas. Les paramètres régionaux du formateur sont 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 la réponse acceptée ci-dessous.

Hot Licks
la source
5
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 30 ans environ), signifiant un aspect ou une caractéristique de certains logiciels qui est suffisamment mal conçu pour être considéré comme un bogue, même si les auteurs refusent de l'admettre.
Hot Licks
1
lors de la conversion d'une chaîne en date, la chaîne doit correspondre exactement à la description du formateur - il s'agit d'un problème tangentiel à celui de votre localité.
bshirley
Les différentes chaînes de date sont là pour tester les différentes configurations possibles, correctes et erronées. Je sais que certains d'entre eux sont invalides, compte tenu de la chaîne de formatage.
Hot Licks
avez-vous expérimenté différentes valeurs de - (NSDateFormatterBehavior)formatterBehavior?
bshirley
Je n'en ai pas fait l'expérience. La spécification est contradictoire quant à savoir si elle peut même être modifiée dans iOS. La description principale dit "iOS Note: iOS ne prend en charge que le comportement 10.4+", tandis que la section NSDateFormatterBehavior indique que les deux modes sont disponibles (mais il se peut que cela ne parle que des constantes).
Hot Licks

Réponses:

67

Duh !!

Parfois, vous avez un "Aha !!" moment, parfois c'est plus un "Duh !!" C'est le dernier. Dans la catégorie pour initWithSafeLocalele «super» a initété codé comme self = [super init];. Cela fait partie de la SUPERCLASS de l' objet lui-même, NSDateFormattermais pas de initcelui NSDateFormatter-ci.

Apparemment, lorsque cette initialisation est ignorée, setLocale"rebondit", probablement à cause d'une structure de données manquante dans l'objet. Changer le initen self = [self init];provoque l' NSDateFormatterinitialisation et setLocaleest à 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
Hot Licks
la source
quel sera le formateur de date pour "NSString * dateStr = @" 2014-04-05T04: 00: 00.000Z ";" ?
Agent Chocks.
@Agent - Cherchez-le: unicode.org/reports/tr35/tr35-31/…
Hot Licks
@tbag - Votre question ne devrait-elle pas porter sur NSDateFormatter?
Hot Licks
@HotLicks oui mon mauvais. Je viande NSDateFormatter.
tbag
@tbag - Que dit la spécification?
Hot Licks
41

Au lieu de sous-classer, vous pouvez créer une NSDateFormattercatégorie avec un initialiseur supplémentaire qui s'occupe d'attribuer les paramètres régionaux et éventuellement aussi une chaîne de format, de sorte que vous ayez un formateur prêt à l'emploi juste après l'initialisation.

@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

Ensuite, vous pouvez utiliser NSDateFormattern'importe où dans votre code avec juste:

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

Vous voudrez peut-être 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 de singleton avec certaines configurations (quelque chose comme +sharedRFC3339DateFormatter). Sachez cependant que ce NSDateFormattern'est pas thread-safe et que vous devez utiliser des verrous ou@synchronized blocs lorsque vous utilisez la même instance à partir de plusieurs threads.

omz
la source
Un NSLocale statique (comme dans ma suggestion) fonctionnerait-il dans une catégorie?
Hot Licks le
Oui, cela devrait également fonctionner dans une catégorie. Je l'ai laissé de côté pour simplifier l'exemple.
omz
Curieusement, l'approche par catégorie ne fonctionne pas. La méthode de catégorie est exécutée et obtient exactement le même Locale que l'autre version (je les exécute dos à dos, la version de catégorie en premier). En quelque sorte, le setLocale ne "prend" apparemment pas.
Hot Licks du
Il serait intéressant de savoir pourquoi cette approche ne fonctionne pas. Si personne ne propose quelque chose de mieux, j'accorderai la prime à la meilleure explication de ce bug apparent.
Hot Licks
Eh bien, j'attribue la prime à OMZ, car il est le seul à avoir fait un effort apparent à ce sujet.
Hot Licks
7

Puis-je suggérer quelque chose de totalement différent parce que, pour être honnête, tout cela coule un peu dans un terrier de lapin.

Vous devriez en utiliser un NSDateFormatteravec dateFormatset et localeforcé à en_US_POSIXpour recevoir les dates (des serveurs / API).

Ensuite, vous devriez utiliser une NSDateFormatterinterface différente pour l'interface utilisateur dont vous définirez les propriétés timeStyle/ dateStyle- de cette façon, vous n'avez pas de dateFormatjeu explicite par vous-même, supposant ainsi à tort que le format sera utilisé.

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

Daniel
la source
Parfois, ce schéma fonctionne, parfois non. Un risque est que votre méthode peut avoir besoin de modifier le format de date du formateur et, ce faisant, de modifier le format défini par le code qui vous a appelé, alors qu'il était au milieu des opérations de formatage de date. Il existe d'autres scénarios dans lesquels le fuseau horaire doit être modifié à plusieurs reprises.
Hot Licks
Je ne sais pas pourquoi changer la timeZonevaleur du formateur entraverait ce schéma, pourriez-vous élaborer? Aussi pour être clair, vous vous abstiendrez de changer le format. Si vous avez besoin de le faire, cela se produira sur un formateur «importé», donc un formateur séparé.
Daniel
Chaque fois que vous modifiez l'état d'un objet global, c'est dangereux. Facile d'oublier que d'autres l'utilisent aussi.
Hot Licks
3

Voici la solution à ce problème dans la version swift. Dans swift, nous pouvons utiliser l'extension au lieu de la catégorie. Donc, ici, j'ai créé l'extension pour le DateFormatter et à l'intérieur que initWithSafeLocale renvoie le DateFormatter avec le Locale pertinent, Ici, dans notre cas, c'est en_US_POSIX, en dehors de cela également 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))")
Technologie
la source