It seems that NSDateFormatter
has a "feature" that bites you unexpectedly: If you do a simple "fixed" format operation such as:
NSDateFormatter* fmt = [[NSDateFormatter alloc] init];
[fmt setDateFormat:@"yyyyMMddHHmmss"];
NSString* dateStr = [fmt stringFromDate:someDate];
[fmt release];
Then it works fine in the US and most locales UNTIL ... someone with their phone set to a 24-hour region sets the 12/24 hour switch in settings to 12. Then the above starts tacking "AM" or "PM" onto the end of the resulting string.
(See, eg, NSDateFormatter, am I doing something wrong or is this a bug?)
(And see https://developer.apple.com/library/content/qa/qa1480/_index.html)
Apparently Apple has declared this to be "BAD" -- Broken As Designed, and they aren't going to fix it.
The circumvention is apparently to set the locale of the date formatter for a specific region, generally the US, but this is a bit messy:
NSLocale *loc = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"];
[df setLocale: loc];
[loc release];
Not too bad in onsies-twosies, but I'm dealing with about ten different apps, and the first one I look at has 43 instances of this scenario.
So any clever ideas for a macro/overridden class/whatever to minimize the effort to change everything, without making the code to obscure? (My first instinct is to override NSDateFormatter with a version that would set the locale in the init method. Requires changing two lines -- the alloc/init line and the added import.)
Added
This is what I've come up with so far -- seems to work in all scenarios:
@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!
I'll award the bounty to the best (legitimate) suggestion/critique I see by mid-day Tuesday. [See below -- deadline extended.]
Update
Re OMZ's proposal, here is what I'm finding --
Here is the category version -- h file:
#import <Foundation/Foundation.h>
@interface NSDateFormatter (Locale)
- (id)initWithSafeLocale;
@end
Category m file:
#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
The 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];
The result:
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)
The phone [make that an iPod Touch] is set to Great Britain, with the 12/24 switch set to 12. There's a clear difference in the two results, and I judge the category version to be wrong. Note that the log in the category version IS getting executed (and stops placed in the code are hit), so it's not simply a case of the code somehow not getting used.
Bounty update:
Since I haven't gotten any applicable replies yet I'll extend the bounty deadline for another day or two.
Bounty ends in 21 hours -- it'll go to whoever makes the most effort to help, even if the answer isn't really useful in my case.
A curious observation
Modified the category implementation slightly:
#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
Basically just changed the name of the static locale variable (in case there was some conflict with the static declared in the subclass) and added the extra NSLog. But look what that NSLog prints:
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
As you can see, the setLocale simply didn't. The locale of the formatter is still en_GB. It appears that there is something "strange" about an init method in a category.
Final answer
See the accepted answer below.
Try This one....
Instead of subclassing, you could create an
NSDateFormatter
category with an additional initializer that takes care of assigning the locale and possibly also a format string, so you'd have a ready-to-use formatter right after initializing it.Then you could use
NSDateFormatter
anywhere in your code with just:You might want to prefix your category method somehow to avoid name conflicts, just in case Apple decides to add such a method in a future version of the OS.
In case you're always using the same date format(s), you could also add category methods that return singleton instances with certain configurations (something like
+sharedRFC3339DateFormatter
). Be aware however thatNSDateFormatter
is not thread-safe and you have to use locks or@synchronized
blocks when you're using the same instance from multiple threads.Here is the solution for that problem in the swift version. In swift we can use extension instead of category. So, Here I have created the extension for the DateFormatter and inside that initWithSafeLocale returns the DateFormatter with the relevant Locale, Here in our case that is en_US_POSIX, Apart from that also provided couple of date formation methods.
Swift 4
usage description:
Duh!!
Sometimes you have an "Aha!!" moment, sometimes it's more of a "Duh!!" This is the latter. In the category for
initWithSafeLocale
the "super"init
was coded asself = [super init];
. This inits the SUPERCLASS ofNSDateFormatter
but does notinit
theNSDateFormatter
object itself.Apparently when this initialization is skipped,
setLocale
"bounces off", presumably because of some missing data structure in the object. Changing theinit
toself = [self init];
causes theNSDateFormatter
initialization to occur, andsetLocale
is happy again.Here is the "final" source for the category's .m:
May I suggest something totally different because to be honest all of this is somewhat running down a rabbit hole.
You should be using one
NSDateFormatter
withdateFormat
set andlocale
forced toen_US_POSIX
for receiving dates (from servers/APIs).Then you should be using a different
NSDateFormatter
for the UI which you will set thetimeStyle
/dateStyle
properties - this way you're not having an explicitdateFormat
set by yourself, thus falsely assuming that format will be used.This means UI is driven by user preferences (am/pm vs 24 hour, and date strings formatted correctly to user choice - from iOS settings), whereas dates that are "coming into" your app are always being "parsed" correctly to an
NSDate
for you to use.