Computing the interval between two moments in time


Computing the interval between two moments in time is easy: It's just subtraction, but subtraction may not be what you want.

If you are displaying time units on the order of months and years, then you run into the problem that a month is of variable length. some people just take the value relative to a base date of January 1 and extract the year and month counts.

Unfortunately, this results in somewhat non-intuitive results. Let's illustrate with some examples. I'm going to write this in C# because it lets me focus on the algorithm instead of getting distracted by "oh dear how do I convert between SYSTEMTIME and FILETIME?" issues, and because it hightlights some new issues.

// Remember, code in italics is wrong
using System;
using SC = System.Console;

class Program {
 static void PrintAge(DateTime bday, DateTime asof)
 {
  TimeSpan span = asof - bday;
  SC.WriteLine(span);
 }
 public static void Main(string[] args) {
  DateTime bday = DateTime.Parse(args[0]);
  DateTime asof = DateTime.Parse(args[1]);
  if (bday > asof) { SC.WriteLine("not born yet"); return; }
  PrintAge(bday, asof);
 }
}

The two parameters to the program are the victim's birthday and the date as of which you want to compute the victim's age.

Here's a sample run:

> howold 1/1/2001 1/1/2002
365.00:00:00

Observe that the TimeSpan structure does not attempt to produce results in any unit larger than a day, since the authors of TimeSpan realized that months and years are variable-length.

A naive implementation might go like this:

static void PrintAge(DateTime bday, DateTime asof)
{
 TimeSpan span = asof - bday;
 DateTime dt = (new DateTime(1900, 1, 1)).Add(span);
 SC.WriteLine("{0} years, {1} months, {2} days",
              dt.Year - 1900, dt.Month - 1, dt.Day - 1);
}

Try it with some command lines and see what happens:

> howold 1/1/2001 1/1/2002
1 years, 0 months, 0 days // good
> howold 1/1/2001 3/1/2001
0 years, 2 months, 0 days // good
> howold 1/1/2000 1/1/2001
1 years, 0 months, 1 days // wrong
> howold 9/1/2000 11/1/2000
0 years, 2 months, 2 days // wrong

Why does it say that a person born on January 1, 2000 is one year and one day old on January 1, 2001? The person is clearly exactly one year old on that day. Similarly, it thinks that November first is two months and two days after September first, when it is clearly two months exactly.

The reason is that months and years are variable-length, but our algorithm assumes that they are constant. Specifically, months and years are context-sensitive but the algorithm assumes that they are translation-invariant. The lengths of months and years depend which month and year you're talking about. Leap years are longer than non-leap years. Months have all different lengths.

How do you fix this? Well, first you have to figure out how human beings compute the difference between dates when variable-length units are involved. The most common algorithm is to declare that one year has elapsed when the same month and day have arrived in the year following the starting point. Similarly, a month has elapsed when the same numerical date has arrived in the month following the starting point.

Mentally, you add years until you can't add years any more without overshooting. Then you add as many months as fit, and then finish off with days. (Some people subtract, but the result is the same.)

Now you get to mimic this algorithm in code.

static void PrintAge(DateTime bday, DateTime asof)
{
 // Accumulate years without going over.
 int years = asof.Year - bday.Year;
 DateTime t = bday.AddYears(years);
 if (t > asof) { years--; t = bday.AddYears(years); }

 // Accumulate months without going over.
 int months = asof.Month - bday.Month; // fixed 10pm
 if (asof.Day < bday.Day) months--;
 months = (months + 12) % 12;
 t = t.AddMonths(months);

 // Days are constant-length, woo-hoo!
 int days = (asof - t).Days;

 SC.WriteLine("{0} years, {1} months, {2} days",
              years, months, days);
}

Notice that this algorithm agrees with the common belief that people born on February 29th have birthdays only once every four years.

Exercise: Explain what goes wrong if you change the line

 if (t > asof) { years--; t = bday.AddYears(years); }

to

 if (t > asof) { years--; t = t.AddYears(-1); }
Comments (20)
  1. hobbes78 says:

    This reminds me of a bug I solved last week. To know if a date entered by the user is valid, the program checked if the day was greater than the number of days that month had. To calculate this, the program determined the the first day of the following month and subtracted one day.

    But this failed to confirm 2002/Mar/31 was valid.

    And what was the root to this problem? Well, that day was the last Sunday of March, thus daylight savings change day. Therefore this day had 23 hours. If you have CTime(2002, 4, 1, 0, 0, 0, 0) and you subtract CTimeSpan(1, 0, 0, 0), you’ll end up on CTime(2002, 3, 30, 23, 0, 0). So the program assumed 2002/Mar had 30 days :-)

  2. Raymond

    I disagree with your assertion that "a month has elapsed when the same numerical date has arrived in the month following the starting point." How many months have elapsed between 31-Mar and 30-Apr? Personally, I’d say one month, and not 30 days, which is what your algorithm would produce.

    I discussed an algorithm for determining the difference in months/days between two dates on my web site at http://www.boyet.com/Articles/PublishedArticles/Calculatingthenumberofmon.html. (Actually one of the most popular pages on my site.)

    Cheers, Julian

  3. Peter Hession says:

    Should the mod 12 be performed after the check if (asof.Day < bday.Day)…?

    Try it for 14/04/2005 & 13/04/2006

  4. Doug says:

    Anything to do with time is non-trivial.

    Time on a computer does not monatomically increase. 2 queries to current time can return the same, or even backwards. (Someone changed the clock, for example.)

    It has annoyed me for a long time that the various OS’s don’t step up to the time issue and provide a decent set of functions for time storage and calculation. Record everything in UTC, and provide various easy to use manipulation functions.

    This book describes all the different calendars out there. Very good.

    http://www.amazon.com/exec/obidos/tg/detail/-/0521564743/103-3519196-5205412?v=glance

  5. Bramster says:

    Many years ago, writing a timesheet program, I used the "number of seconds since Jan 1, 1970" approach, and did the appropriate math.

    It made me think about the implicatons of 2^31, which, if I’m correct will hit in 2038. Yes, that’s a long time away, but so was Y2K. And there were damn few embedded systems written in Cobol.

    fade out, fade in. . .

    UTC definitly simplifies a lot of calculations. I wear two watches — one to local time, one two UTC. Two cheap Timexes on one strap equals the functionality of most or the expensive Aviator’s watches — with no buttons to push — just a twist of the wrist!

  6. Eric Lippert says:

    The most common algorithm is to declare that one year has elapsed when the same month and day have arrived in the year following the starting point.

    Though this is a common algorithm, it is NOT the algorithm that VB/VBScript’s DateDiff uses. With datediff, the difference in years is "subtract the year". So the difference between 31Dec2001 and 1Jan2002 is one year. Similarly for months.

    This is a good illustration of the difference between the "MIT" and "Berkeley" design philosophies. The MIT design philosophy favours complex, hard-to-explain implementations that always do the "correct" thing. The Berkeley design philosophy favours designs that may get the corner cases wrong, but can actually be explained to mortals without drawing flowcharts.

  7. I dealt with this while trying to write some javascript to determine how pregnant my wife was to display on a web page. This evolved to how old someone is in general. The code is at this URL in the qryHowOld function if you want to check it out.

    http://www.willrickards.net/library/dates.js

  8. // Accumulate months without going over.

    int months = (asof.Month – bday.Month + 12) % 12;

    if (asof.Day < bday.Day) months–;

    t = t.AddMonths(months);

    So the difference between 1/15/2001 and 1/25/2000 would be 0 years -1 months …

  9. grouse says:

    "[T]he TimeSpan structure does not attempt to produce results in any unit larger than a day, since the authors of TimeSpan realized that months and years are variable-length."

    So are minutes (they can have leap seconds).

    For that matter, weeks are not any more variable length than days are.

  10. Kelsey says:

    Interestingly we have had to solve this exact issue. There’s another problem that comes up (well, it did in Java anyway) – Timezones.

    If the date that represents my birthday has any timezone information, we have to explicitly throw that information away.

    I was born on November 23, in New Zealand. That was November 22 in USA, but I always continue to use the 23rd even when I go there.

  11. Kevin says:

    It’s funny you mentioned timezones. My wife and I moved from Milwaukee to Seattle. All of my Outlook calendar items for Birthdays were shifted back two hours for the duration of a day.

    a birthday appt at 12:00am 10/20/2004 for the whole day became a 11:00pm 10/19/2004 -> 11:00pm 10/20/1977

    I had to go in and update all of them to fix it. That was a bit annoying!

  12. Michael Dwyer says:

    Time on a well-managed machine *does* increase monotonically and in UTC. NTP works by slowing the clock down and speeding it up so that there are never discontinuities in time.

    The problem — like in the Y2K problem — is the human element. Lets say I schedule something to run at 2:30am. Should it have run during the changeover from MST to MDT? During the daylight saving time switchover, 2:30am doesn’t exist. It goes from 1:59am to 3:00am.

    It gets even worse when you switch from MDT back to MST — should your process run twice?

    On the other hand, if you unplug your human need to have local time, you would schedule your event for a certain time UTC, and everything would work the way it was supposed to… until a human noticed that it was happening an hour too early…

    Ugh. Sometimes, you humans drive me insane.

    (MDT/MST is -0700 and -0600 timezones, I think, for those who are non USAsians)

  13. Ray Trent says:

    I would hope that this is incredibly obvious, but it seems like the answer to the "problem" entirely depends on what you’re planning on *doing* with the result.

    An off the cuff human-readable timespan like "one year" has very different connotations than a timespan measured in hours, which is different from one measured in millenia, or milliseconds.

    Here’s a very controversial related proposition:

    This is all a matter of ignoring significant digits. On the scale of a year, a day is insignificant, because the error bars on what you mean by a "year" are a day wide.

    Reporting 1/1/2000 <-> 1/1/2001 as "one year, zero months, one day" is as ridiculous as reporting the difference in 2 people’s heights in microns.

    It should be "one year, 0 months". Maybe ", 0 weeks" if you want to be a little more accurate without lying.

    At the *very* worst, you should say: "1 year, 0 months, 0 days plus or minus 1 day".

    As a side note, I would argue that the convention of people with 2/29 birthdays having a b-day every four years isn’t commonly held as anything more serious than a joke.

  14. SQL DATEDIFF of course works differently again by just counting the number of boundaries of the specified type between the two dates. So DATEDIFF( m, ‘1900-01-31’, ‘1900-02-01’ ) returns 1 because 1 month boundary has been crossed.

  15. Universalis says:

    Timezones are a nuisance. My web site gives information related to today’s date, but there is no way to know the date on which the client has requested a web page.

  16. Dave says:

    "Calculations and comparisons of DateTime instances are only meaningful when the instances are created in the same time zone. For that reason, it is assumed that the developer has some external mechanism, such as an explicit variable or policy, to know in which time zone a DateTime was created."

    http://msdn.microsoft.com/library/en-us/cpref/html/frlrfSystemDateTimeClassTopic.asp

  17. Combining two tricks into one big trick.

  18. Steve says:

    Never mind all the discussions about dates and times – surely the important thing about this post is that ** Raymond is writing in C# ** !!!

  19. Norman Diamond says:

    Friday, April 15, 2005 2:42 AM by Steve

    > surely the important thing about this post

    > is that ** Raymond is writing in C# ** !!!

    "not actually a .NET blog"

    Obviously he’s using ROTOR ^_^

Comments are closed.

Skip to main content