Handling Birthdays, and Other Anniversaries

How old are you? Simple question, yes? Perhaps. But the answer could change depending on the granularity of the question, and on your perspective.

To answer "how many minutes old are you?", we need a lot more information than might be readily available:

  • Your date and exact time of birth
  • The time zone (or location) of your birth
  • If you happened to be born during a daylight saving time fall-back transition, when (in some time zones) clocks moved backwards - for example, ticking from 2:59:59 AM back to 2:00:00 AM.  If you were born at 2:30, you'll need to know which instance of 2:30.
  • The current universal time, expressed in UTC.

Most people do not have all of this information on a regular basis, nor do we usually care.  When someone asks how long one has been alive, dead, married, employed, or any other similar question, they are usually looking for a _calendrical _answer like "30 years", or "6 months" or sometimes "2 years, 4 months and 5 days".

A calendrical unit of measure is a humanized expression of a passage of time.  This is quite different than an elapsed duration of time that computer's typically measure, which often creates confusion for developers.  For example, one of the first questions to be asked on Stack Overflow was "How do I calculate someone's age in C#", which has many hundreds of votes and over 70 different answers (not all of them accurate).

Here's what you need to realize, which is often missing from the discussion and the algorithm:

  • We need to compute a calendric answer, not an elapsed duration of time.
  • That implies the use of a calendar, and we usually care about the Gregorian calendar in modern times.
  • We usually have only a date on that calendar to use as a reference.  We don't usually have a time of day or a time zone.  Even if we did, it wouldn't be relevant for the desired output.
  • Time zones control how each of our views of this calendar are aligned to the universal instantaneous timeline.
  • Our answer will vary not by the time zone of our birth, but by the time zone of where we are physically located when we ask the question!

That last part has some really interesting implications.  Want to stay younger longer?  Travel west!  As anyone who's flown on a plane going westbound near sunset can tell you - your perception of the "day" will elongate if you can keep up with the rotation of the earth.  Of course, that idea has it's limitations.

The best approach for calculating calendrical periods in C#  (or any .Net language) is by using the Period class in the Noda Time library:

using NodaTime;

Period CalculateAgeFrom(int year, int month, int day, string targetTimeZone,  
                        PeriodUnits units = PeriodUnits.YearMonthDay)
{
    Instant now = SystemClock.Instance.Now;
    LocalDate today = now.InZone(DateTimeZoneProviders.Tzdb[targetTimeZone])
                         .Date;
    LocalDate referenceDate = new LocalDate(year, month, day);
    return Period.Between(referenceDate, today, units);
}

This can be used in a variety of different ways. For example:

Period age = CalculateAgeFrom(1976, 8, 27, "America/New_York");  
Console.WriteLine("Today, I am: {0} years, {1} month(s), and {2} day(s) old.",  
                   age.Years, age.Months, age.Days);

Note that the time zone of "America/NewYork" I'm passing here is because as I'm writing this, I'm _physically located in the US Eastern Time zone. I was born in Arizona, and tomorrow I'll be in Seattle, but that does not matter for this question.

Sometimes it might not be possible to know the current physical whereabouts of a person. In that case, you should use the time zone of the person who is asking the question. At least the response will be accurate from their point of view.

If you'd like a simpler form of the above code, consider helper methods such as:

int CalculateAgeInYearsFrom(int year, int month, int day, string targetTimeZone)  
{
    Period age = CalculateAgeFrom(year, month, day, targetTimeZone,
                                  PeriodUnits.Years);
    return (int) age.Years;
}

int CalculateAgeInDaysFrom(int year, int month, int day, string targetTimeZone)  
{
    Period age = CalculateAgeFrom(year, month, day, targetTimeZone,
                                  PeriodUnits.Days);
    return (int) age.Days;
}

If you're a Java developer, a similar approach can be taken with the Period class found in Java.Time (Java 8), or the one from Joda Time.  I'm sure there are options in other languages also.

Can you calculate calendric periods in .Net without using Noda Time?  Sure, but some calculations are much harder than others, and the subtleties are more important than you think. I don't recommend trying.