Monday, December 31, 2007

Wrong Dates in iCal Birthday Calendar

To keep track of people's birthdays, I use Mac OS X's Birthday Calendar feature of Address Book/iCal. I was going through my calendar the other day, and I noticed that a birthday which I knew was sometime in January wasn't showing up. It was on the corresponding Address Book contact, though. I deleted the birthday from this contact and reentered it, which fixed that entry, but on the suspicion that more birthdays might be missing, I flipped through my calendar and found:

Address Book says Mar 23, iCal says Mar 21

The Address Book birthday field has the misfeature that it forces a year to be specified. What a rude thing for Address Book to be asking! Anyway, I'd arbitrarily picked year 1 for the year for any contacts whose birth years I didn't know. Maybe, I thought, the Gregorian reform was throwing things off. However, changing the year to 1900 didn't help matters, and in fact made them worse:

Address Book says Mar 23, iCal says June 23

Turning the birthday calendar off (which wipes out iCal's backing store for the calendar) and on didn't help matters. A web search turned up some other people having the same problem, but the only useful solution they came up with was deleting and recreating entire contacts by hand.

I wanted to see if the raw data was wrong in Address Book's database. Address Book uses Core Data in a way that makes the database difficult to work with at the SQLite command-line level, so instead I hacked /Developer/Examples/Python/PyObjC/AddressBook/Scripts/exportBook.py to emit the birthday field by adding ('Birthday', AddressBook.kABBirthdayProperty) to FIELD_NAMES and the following to encodeField:

    elif isinstance(value, AppKit.NSCalendarDate):
        return value.descriptionWithCalendarFormat_("%Y-%m-%d")

It turns out that a number of entries had negative years, e.g. -1900-03-23 instead of 1900-03-23. I'm not sure how this happened, but here's a script (which you can download) to fix it:

#!/usr/bin/python
"""
Fix negative birthday years in Address Book.
This work is hereby released into the Public Domain.
"""
import AddressBook
import AppKit

def personName(person):
    return "%s %s" % (
        person.valueForProperty_(AddressBook.kABFirstNameProperty),
        person.valueForProperty_(AddressBook.kABLastNameProperty)
        )

def formatDate(date):
    return date.descriptionWithCalendarFormat_("%Y-%m-%d")

def fixBirthday(birthday):
    year = int(birthday.descriptionWithCalendarFormat_("%Y"))
    if year < 0:
        return birthday.dateByAddingYears_months_days_hours_minutes_seconds_(
            -year * 2, 0, 0, 0, 0, 0)
    else:
        return None

def fixPersonBirthday(person):
    birthdayProp = AddressBook.kABBirthdayProperty

    birthday = person.valueForProperty_(birthdayProp)
    if birthday == None: return

    fixedBirthday = fixBirthday(birthday)
    if fixedBirthday != None:
        print "Fixing up %s: %s -> %s" % (
            personName(person),
            formatDate(birthday),
            formatDate(fixedBirthday)
            )        
        person.setValue_forProperty_(fixedBirthday, birthdayProp) 

book = AddressBook.ABAddressBook.sharedAddressBook()

for person in book.people():
    fixPersonBirthday(person)

book.save()