Archive for August, 2005

How old are you, really?

Sunday, August 7th, 2005

When a man sits with a pretty girl for an hour, it seems like a minute. But let him sit on a hot stove for a minute–and it’s longer than any hour. That’s relativity.

-Albert Einstein

Reckoning time has always been a problem for humans, it seems. We have argued over which calendar to use for quote a long time. Even worse is trying to figure out how long ago something happened.

The answers to many “how long ago” questions can be answered with a certain degree of slop. For example, “how long ago was Jesus of Nazareth born?” could be answered, “about 2000 years ago”. “When was peace declared at the end of World War II?”, “60 years ago”. But what a question to which the answer should be more specific, such as “how long ago was I born?”. I want to know the years, months, and days for that figure, and here’s why.

As part of my continuing work with The Center for Promotion of Child Development Through Primary Care, I have to be able to display ages for patients that our doctors will be treating. More often than not, these patients are young, so we’re talking about newborns through adolescencts. For the newborns, the number of months and days is very important, while the ages of adolescent patients are okay to round-off to years and months, and maybe just years.

It turns out that it’s somewhat difficult to answer the question “how old are you?”. It doesn’t really seem all that hard, until you actually try to do it. The problem is that people disagree about a lot of things. For example, you won’t get much argument that there are 10 days separating 2000-01-01 and 2000-01-11, or that there is 1 month separating 2000-01-01 and 2000-02-01. But what about the date difference between 2000-01-31 and 2000-02-30? Is that 30 days or is it 1 month?

Julian Bucknall is a guy who studies algorithms, at least as a hobby. He has a discussion of time reckoning in software including a sample implementation in C#. Although I appreciate his discussion (and created a few new unit tests based upon some of the problematic date ranges he presents), I don’t entirely agree with how he did his implementation. I happen to be using Java for my purposes, but I did my own implementation because I needed to, not because I’m just a Java wonk.

Before I start, those without a programming background have to realize that most programming languages have very poor tools for handling dates. Mostly they center around counting milliseconds since a certain date (usually 1970-01-01). This is great for quick calculations of numbers of days between events, since a day has a fixed number of milliseconds (1000 ms/sec * 60 sec/min * 60 min/hr * 24 hr/day = 86400000 ms/day).

For those of you who are too smart for your own good, I’m going to be ignoring leap seconds and things like that for the time being, since computers generally don’t handle those, anyway. If you want your computer’s time to be correct to the nearest leap-second mandated by the IEOS, you should just manually adjust your clock whenever it’s convenient… no date library is going to worry about keeping a list of all leap-seconds ever added to civil time.

So, back to dates in software. Since the number of milliseconds in a day is fixed, and computers often represent dates as a number of milliseconds from a fixed date (generally known as the epoch), it’s very easy to calculate the difference between two dates as a number of days. For example, I was born on 1977-10-27. That means that I am 10146 days old (wow, that doesn’t seem like a lot…). But how many years, months, and days old am I?

Fortunately, for discussion purposes, I’m writing this entry on 2005-08-07, which has both the day-of-month, as well as the month itself, less than the same numbers in my birth date (that is, 8 is less than 10, and 7 is less than 27). That’s good because it makes the math harder. If I had been born on 1977-08-01, then you could count on your fingers that I am 28 years, 0 months, and 6 days old. Since I was born later in the month and later in the year, there are all kinds of fun things that have to happen.

If you were to perform these calculations on your fingers, you’d probably start with the birth date and keep adding years until you couldn’t add them anymore without going over. You’d easily get to 27 and stop (if you had that many fingers). But then, you have to figure out what the differences are between the months and days. Exactly 27 years after my birth would be 2004-10-27. In order to get yourself to 2005-08-07, you need to add a bunch of months. If you add 10 months, you’ll get 2005-08-27, which is too much. So, you have to add 9 months instead, and then figure the days. Exactly 27 years and 9 months after my birth would be 2005-07-27. In order to get to today, you have to add days. If you add 11 days, you’ll get to 2005-08-07. Ta-da!

Now, that didn’t seem too bad, did it? Actually, an implementation which basically follows this on-your-fingers calculation is the one proposed by Julian Bucknall as well as many others on the web. I don’t like this implementation because is it computational overkill (you have to do lots of looping, and most Date object implementations that exist out there will re-calculate a bunch of stuff whenever you update a single field, such as the year or month). I actually wrote mine before I read his article, and I don’t have a C# compiler handy to run his algorithm through my test cases, so I can’t be sure that they yield the same results. At any rate, I have an implementation that should be a little more efficient and meets my needs.

Oh, one last note: we had been using a Java library called BigDate to do our date calculations. I knew it was going to be a pain in the neck to write our own, so we found a library that would do it for us. Unfortunately, it fails with Java Date objects representing dates before 1970-01-01. The author claims that his library handles dates prior to 1970 in contrast to Java’s Date, but it appears that he is wrong on two counts: Java’s Date class does, in fact, handle dates before 1970, and his library trips over them. I was able to use his library by passing-in the year, month, and date separately, but that required me to use deprecated methods in the Date API, and I was already starting to look down my nose at it, slightly. Just for the heck of it, I tried to use BigDate to calculate the date delta between a BCE date and today, and BigDate ignored the era, so I got the wrong answers there, too.

So, I wrote my own implementation (in Java) that quickly calculates deltas for all three fields (I’m not concerned with time, just the date), possibly ajdusts them for BCE dates, and then runs a fairly simple algorithm to move the date, then month and year to their correct values. We use a class called DiffDate which just stores a year, month, and date as a return value. I have one method that accepts a pair of Date objects, and one that accepts a pair of Calendars. Use of the Calendar avoids deprecation warnings during compilation, and offers two methods for client code, making it easier to use in situations that call for either Dates or Calendars.

    //
    // Copyright and licence notice: I intend for this code to be freely copied, edited, improved, etc.
    // Please give me (Chris Schultz, http://www.christopherschultz.net/) credit as the source of
    // this code, and let me know if you find ways to improve it.
    //
    public static DiffDate diffDates(Date earlier, Date later)
    {
      Calendar c_e = Calendar.getInstance();
      c_e.setTime(earlier);
      Calendar c_l = Calendar.getInstance();
      c_l.setTime(later);
      return diff(c_e, c_l);
    }

    public static DiffDate diff(Calendar earlier, Calendar later)
    {
      int y1 = earlier.get(Calendar.YEAR);
      int m1 = earlier.get(Calendar.MONTH);
      int d1 = earlier.get(Calendar.DATE);
      int y2 = later.get(Calendar.YEAR);
      int m2 = later.get(Calendar.MONTH);
      int d2 = later.get(Calendar.DATE);

      // Adjust years across eras (BC dates should be negative, here).
      if(java.util.GregorianCalendar.BC == earlier.get(Calendar.ERA))
        y1 = -y1;
      if(java.util.GregorianCalendar.BC == later.get(Calendar.ERA))
        y2 = -y2;

      int d_y = y2 - y1;
      int d_m = m2 - m1;
      int d_d;

      // Now that we've got deltas, start with the days and work backward
      // changing any negatives into positives, and rippling up to larger
      // fields.
      if(d2 >= d1)
      {
        d_d = d2 - d1; // Easy
      }
      else
      {
        // To determine how big the months are.
        Calendar work = (Calendar)later.clone();
        while(d1 > d2)
        {
          // Move backward through the months, adding a whole month 
          // until we have enough days to cover the deficit.
          --m2;
          // To track our progress through the month
          --d_m;
          // Now, there's one less month between dates
          if(0 > m2)
          {
            --d_y;
            work.set(Calendar.YEAR, work.get(Calendar.YEAR) - 1);
            m2 = Calendar.DECEMBER;
          }

          work.set(Calendar.MONTH, m2);
          d2 += work.getActualMaximum(Calendar.DAY_OF_MONTH);
        }

        d_d = d2 - d1;
      }

      // Adjust the months and years
      while(0 > d_m)
      {
        d_m += 12;
        d_y -= 1;
      }

      return new DiffDate(d_y, d_m, d_d);
    }

The whole thing is very straightforward, with the notable exception of the big “else” block in the middle of the code. It is here where we handle cases when the earlier date has a day-of-month that is later in the month than the later date. In that case, we need to count backwards, enlisting the help of a Calendar object to give me the lengths of various months. That ‘work’ calendar actually exists only to help me with leap-year determination. I suppose I would have used the old “years evenly divisible by 4, except every 100, except every 400”, but that would have complicated my code even further, and, I think, been inaccurate for old dates because of changes to the calendar. Then again, I think that GregorianCalendar (the default calendar in my locale) had those same rules, so I’d get the same results in both cases. If you want to calculate dates in October of 1582, you’re on your own.

You may have noticed, but this implementation does not handle time zones in any way. The reason is that this is intended to be for age calculation. If you were born in Sydney on 2000-01-01, then it might still have been 1999-12-31 in New York. However, you’re certainly not going to maintain your birthday to be 1999-12-31 when you’re in the US and 2000-01-01 when you’re in Sydney. Or, at least, we won’t ;)

It occurs to be that I’d like to write an entirely new Date implementation for Java, to handle things like bizarre missing dates (like October 1582) and a few other things that bother me about the Date class, but it’s just not going to happen. There are too many APIs that already use Date (or Calendar) and they’re not likely to change. Also, one of the things that I haven’t liked about the APIs is that they were able to neither calculate nor store delta dates. I have solved both with a delta date implementation and a simple delta date class.

So, how old are you, exactly? My code says that I’m 27 years, 9 months, and 11 days old. But I feel much younger than that.