tags:

views:

958

answers:

5

I'm trying to store today's date in a sqlite table as a string in a 24-hour time format regardless of the user's locale. Because NSDate honours the user's locale, this code returns different results depending on the user's location:

NSLog(@"The date is %@", [NSDate date]);

If their locale is set to the United Kingdom, where 24-hour time is the default, the above returns 'The date is 2009-09-05 11:17:35', whereas if they're in the United States, where 12-hour time is the default, it returns 'The date is 2009-09-05 11:17:35 AM'.

Is there a way to automatically detect and convert 12-hour time to 24-hour time before committing it to the database? I'm using SQLite Persistent Objects, so I need to provide the date as an NSDate and not NSString.

A: 

You could use the sqlite DATETIME type, and actually store the date instead of a string. You can use NSDateFormatter to define how the date is displayed to the user.

Graham Lee
Thanks very much for the reply, Graham. Right now I'm committed to using SQLite Persistent Objects, which stores the date as a string, but otherwise datetime would be a good option.
NickD
A: 

You might consider swizzling the [NSDate date] call to always apply the US locale date formatter.

Alex Reynolds
Thanks, Alex. I didn't know about this -- I'll take a closer look. Thanks for the reply.
NickD
A: 

This looks like a bug in SQLite Persistent Objects. The library takes your NSDate instance and serialises it as a string in the format @"yyyy-MM-dd HH:mm:ss.SSSS", which means that the timezone information is actually being thrown out. When the date string is converted back into an NSDate, the timezone is assumed to be the local time zone. This works only in the case that all your NSDate instances are in the local time zone.

The easiest way of fixing this is to patch the NSDate(SqlPersistence) category defined by SQLite Persistent Objects in NSDate-SQLitePersistence.m to include the timezone in the format string (e.g. @"yyyy-MM-dd HH:mm:ss.SSSS Z") during serialisation and deserialisation. However, the proper fix would be to serialise the NSDate using the a DATETIME column rather than a char-based column.

When you're displaying the dates in your UI, you can then use your own NSDateFormatter to define your own string date format.

Nathan de Vries
Thanks for the suggestion, Nathan -- I'm actually getting around the timezone issue by doing time comparisons using sqlite's 'localtime' option, like this:todayTasks = [Task findByCriteria: @"WHERE date(due) BETWEEN '1980-01-01' AND date('now','localtime') ORDER BY due"];This works great when the time is stored in 24-hour format (e.g. 2009-09-05 13:18:48.3970), but it breaks when using SQLitePO and storing time in 12-Hour format (e.g. 2009-09-05 01:19:23 PM.1400), which leaves me stumped.
NickD
SQLite Persistent Objects will always store your NSDate properties as strings in 24 hour format (that's what the "HH" is in the "yyyy-MM-dd HH:mm:ss.SSSS" date format it uses -- "hh" would be 12 hour time).To query, you should be using datetime() rather than date() to include the time component. The datetime() method understands epoch seconds, so you should use `[NSString stringWithFormat:@"datetime(%d)", [yourNSDate timeIntervalSince1970]]` in the query string being passed to -findByCriteria:.
Nathan de Vries
Thanks, Nathan. Really appreciate your input. I'm using date() because I don't need to compare the time itself - just the day; I think that's the simplest way to do it, anyway.I think you might be right in stating that it's a bug with sqlitepo -- I've grabbed the db file from my iPhone, and 12-Hour time isn't being converted to 24-Hour time. 12-Hour time locales get stored as 2009-09-06 01:09:11 PM.8550, whereas 24-Hour time is correctly written as 2009-09-06 13:09:11.8550.
NickD
These comments have added much more context to the problem and show that I might have misunderstood your question/problem. I've posted a new answer which should help.
Nathan de Vries
+1  A: 

In my previous answer I thought you were having issues with SQLite Persistent Objects, but that's not the case. I think you've simply misunderstood what the following code is actually doing:

NSLog(@"The date is %@", [NSDate date]);

The reason why you're getting different log output under different locales is not because of NSDate, it's because of the NSDateFormatter being used under the hood to insert the string representation of [NSDate date] into your log string using %@. In fact, NSDate has no concept of "12-hour" or "24-hour" locales -- it's just a representation of a point in time.

Locales come into play when you turn your NSDate into a string, such as in an NSLog statement or as part of an SQL query string. When you want to do that, you should specify your own explicit formatter, like so:

NSDateFormatter *formatter = [[NSDateFormatter alloc] initWithDateFormat:@"yyyy-MM-dd HH:mm:ss" allowNaturalLanguage:NO];
NSLog(@"The date is %s", [formatter stringFromDate:[NSDate date]]);
[formatter release];

Using the above, you should get identical strings regardless of the users' locale settings.

As for your SQL queries, am I right in thinking that you're also using %@ to insert the date into the query string? If so, you should do something like this instead:

NSString* criteriaTemplate = @"WHERE date(due) BETWEEN date(%d) AND date('now', 'localtime')";
NSString* criteria = [NSString stringWithFormat: criteriaTemplate, [myNSDate timeIntervalSince1970]];
NSArray* todayTasks = [Task findByCriteria:criteria];

Hope this helps.

Nathan de Vries
Thanks, Nathan -- that's helped me understand it much better. It led me onto looking at how setDateFormat works, which helped me find the NSLocale override I've posted about separately. Much appreciated.
NickD
+1  A: 

Update: in the end I chose to override the user's locale before manipulating the date and storing it, by using NSLocale. So, instead of this:

NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; 
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSSS"];  
NSString *formattedDateString = [dateFormatter stringFromDate:self];

I did this:

NSLocale *POSIXLocale = [[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"] autorelease];
NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:POSIXLocale];  
[dateFormatter setDateFormat:@"yyyy-MM-dd HH:mm:ss.SSSS"];  

NSString *formattedDateString = [dateFormatter stringFromDate:self];

This has the effect of standardising the date format before storing it. For those using sqlitepo who encounter the same issue, the relevant code to amend can be found in NSDate-SQLitePersistence.m on lines 33 and 46.

Thanks to all who chipped in.

NickD
@NickD: The original code and the code you've replaced it with *should* produce identical strings as per the Unicode TR35-4 standard. Looks like you might have run into a bug -- http://stackoverflow.com/questions/143075/nsdateformatter-am-i-doing-something-wrong-or-is-this-a-bug
Nathan de Vries
Well spotted. I've searched Apple's iPhone Developer Forums and found that someone's already submitted the issue as bug #7037950: https://devforums.apple.com/message/93981#93981
NickD