The Curious UIDatePicker

I’ve started learning how to program the iPhone.  It’s a very interesting beast.

My first application uses the built-in UIDatePicker object (that’s the slot machine with wheels for picking a date).  I want my app to handle historical times, so I scrolled the wheel for the year back a couple of centuries, and noticed some very strange behavior.  When you ask the date picker for the selected date, you get back an NSDate object.  If you print that out, then you’ll find that the date you get back sometimes does NOT match the date on the wheels.  This is of course a disaster for any application.  Try it yourself: spin the wheels around to May 15, 1152, and check the NSDate returned by the picker object: you’ll get May 8, 1153.  One year and 7 days wrong.

And unfortunately, as you move around you’ll find the days continue to shift: May 15 in the year 852 reports as May 11, 853.  Now one year and 4 days wrong.  It’s a problem, but we can fix it. 

Why is this happening?  I think it’s a combination of politics and programming.  The Gregorian calendar (which we use) has gone through a lot of changes since it was adopted.  Many of these were to compensate for the fact that the Earth takes a fraction of a day more than 365 days to get all the way around the sun.  Back in the 16th century, the calendar had fallen so far out of sync with the Earth’s orbit that 10 days were simply dropped out of the middle of October, 1582.  And before that, people added extra days here and there.

The Date Picker returns the selected date as an NSDate object, which I had expected would take these well-known historical events into consideration.  I figured if I spun the wheels back to October 1582 I’d find a few missing dates.  Instead, the whole year of 1582 is missing!  The year wheel simply jumps from 1583 to 1581.  And that’s where the trouble begins.  The dates that are reported by the date picker start to slosh around at that point, moving further out of phase the first year of roughly every century going back (though not consistently - there’s no slippage in the 4th, 5th, 6th, 7th, or 11th centuries, for example).  Essentially, the date that shows on the wheels does not match the date that the picker says is showing on the wheels.

If you keep scrolling back, you’ll find eventually that the year 300 repeats twice!  The wheel goes from 302 to 301 to 300 to 300 to 299 and so on.

The bottom line is that for anything prior to 1582, the odds are that the day the user dials and the day that the DatePicker reports will be different by more than a year.  Trying to fix this in code would be a real mess.  Happily, before I tackled that I tried running the picker’s date through the NSCalendar attached to the picker.  The result is that the days always agree, but the year is still off by one.  But still, that’s much better!

To work with the date that the user scrolls in, you need only adjust the year by subtracting one if the year is anything from 301 to 1582.  There are two downsides.  The first is that the user simply cannot enter a date in 1582.  But that’s not our fault: it doesn’t exist on the picker!  The second is that the user might be confused by the two occurrences of the year 300 (the NSCalendar year reports the first 300 as 301, and the second as 300).

year shown on the wheel: 1584 1583 1581 1580 1579 … 0302 0301 0300 0300 0209 0208
year reported by NSDate: 1584 1583 1582 1581 1580 … 0303 0302 0301 0300 0209 0208

The whole situation is very strange.  It’s almost as if rather than creating the values on the year wheel algorithmically, someone typed them in by hand, and accidentally skipped 1582 and then accidentally doubled 300.  In any case, if you want to use the date picker for historical dates, don’t use the NSDate object it returns.  Pass it through the picker’s own calendar, and manually fix the year if it’s in the problem range.  Here’s a snippet:

NSDate *date = [datePicker date];          // don't use this date directly!
NSCalendar *cal = [datePicker calendar];
unsigned unitFlags = NSYearCalendarUnit | NSMonthCalendarUnit |  NSDayCalendarUnit;
NSDateComponents *comps = [cal components:unitFlags fromDate:date];
int day = [comps day];
int month = [comps month];
int year = [comps year];
if ((year >= 301) && (year <= 1582)) year -= 1;  // correct for UIDatePicker bug
// now work with (day, month, year) and it will match the value on the wheels

One sad thing is that if you convert these components back into an NSDate (using dateFromComponents), it goes wrong again. For example, dial in August 10, 1390. The picker will tell you you’ve chosen August 2, 1391. Convert it to components and subtract a year, and you have the right date. Now hand those components back to NSDate (with the corrected year!) and you get back August 2 again, but in 1390. The year is now right, but that’s still not the date that’s on the wheels.

I know that calendars are complex and wonky things with tons of weirdness, and in some ways it doesn’t really make sense to talk about these ancient dates in Gregorian terms, but I wish that the Apple objects were consistent.  I wish the date reported by the picker matched the date that shows on the wheels, and if not, that the date decoded by the calendar did.  As it stands, *nothing* matches the user interface between 300 and 1582.

If the NSCalendar object is the the thing that understands how the Gregorian calendar works, then I wish that NSDate didn’t have a built-in display that disagrees with the calendar.  As they say, the man with one watch knows what time it is, but the man with two is never sure. Having to constantly juggle between dates and components and calendars and such is not just a hassle, but an invitation to messing something up.

Leave a comment

You must be logged in to post a comment.