Home > Software design >  How do I calculate the difference in days, months and years between two dates with leap years?
How do I calculate the difference in days, months and years between two dates with leap years?

Time:02-18

I'm requesting the user to enter two dates (day, month and year).

Using the time.h header, I'm storing it in a struct tm variable like following (let's say date1):

date1.tm_mday = day;
date1.tm_mon = month;
date1.tm_year = year - 1900;
date1.tm_hour = 0;
date1.tm_min = 0;
date1.tm_sec = 0;
date1.tm_isdst = 0;

The second date (date2) is the same above but with "date2" instead of "date1" (two structures).

Next, I did the mktime of date1 and date2 to calculate the seconds of both dates:

date1TimeT = mktime(&date1);
date2TimeT = mktime(&date2);

Following, I checked whether the date1 is bigger than date2, so I placed:

if(date1TimeT > date2TimeT)
    finalDateTimeT = date1TimeT - date2TimeT;
else
    finalDateTimeT = date2TimeT - date1TimeT;

Now to calculate the days, months and years that have passed through this two dates:

totalDays = (int) finalDateTimeT / 86400;
remainingYears = totalDays / 365;
remainingMonths = (totalDays - remainingYears * 365) / 30;
remainingDays = (totalDays - remainingYears * 365 - remainingMonths * 30);

However, this is not 100% accurate, as this does not take care of the leap years (those who are divisible by 4).

Is there a safer way to calculate this? Keeping in mind the leap years.

CodePudding user response:

The idea to go through the delta days is a bad one. Once you pass the month boundary you are in trouble. Each month has 28..31 days. In your original post you assume every month to have 30 days.

In order not to loose relationship with the month's lengths we have to work directly with day/month/year. We can combine month year in one variable as every year has 12 months, like the expression month year*12. Here my code:

#include <time.h>
#include <stdio.h>
void PrintDelta (int StartYear, int StartMonth, int StartDay, int EndYear, int EndMonth, int EndDay)
{
    //Month and Day are 1-based
    int Start_MonthYear = StartYear * 12   StartMonth-1;
    int   End_MonthYear =   EndYear * 12     EndMonth-1;
    int Delta_MonthYear = End_MonthYear-Start_MonthYear;
    int Delta_Days      = EndDay-StartDay;

    if (Delta_Days < 0)
    {
        //Like Jan 31st --> Feb 1st
        //remove one month from Delta_MonthYear and add one month to Delta_Days
        static const int MonthDays[2][12]=
        {
            //MonthDays[][0] is the number of days in the month BEFORE January!!
            //12 01 02 03 04 05 06 07 08 09 10 11 <--- month
            { 31,31,28,31,30,31,30,31,31,30,31,30}, //non-leap year
            { 31,31,29,31,30,31,30,31,31,30,31,30}  //leap year
        };
        Delta_MonthYear--;
        Delta_Days  = MonthDays[(EndYear & 3) ? 0 : 1][EndMonth-1];
    }
    printf("Timespan from %d-d-d to %d-d-d is %d year(s), %d month(s), %d day(s)\n",
           StartYear, StartMonth, StartDay,
           EndYear,     EndMonth,   EndDay,
           Delta_MonthYear/12, Delta_MonthYear, Delta_Days
          );
}
int main(int argc, char **argv)
{
    PrintDelta(2000,2,28,  2001,3,1);  //1yr 1day, including Feb 29th
    PrintDelta(2001,2,28,  2002,3,1);  //1yr 1day, no Feb 29th
    PrintDelta(2022,1,11,  2022,1,21); //10 days
    PrintDelta(2022,1,31,  2022,2,1);  //one day
}

CodePudding user response:

How do I calculate the difference in days, months and years between two dates?

There are two totally different ways to do this. The more popular one is the way you've already tried: First convert both dates to some common, monotonic timebase, such as a Unix-style time_t value. Then subtract. This gives you a simple difference in, in this case, seconds. You can divide by 86400 to get days, but it's hard to see how to convert to months or years.

The reason it's hard is that our familiar year/month/day/hour/minute/second system of date/timekeeping is a mixed base scheme, and it's worse than that, in that different months have different numbers of days. There's no single constant days-per-month value you can divide by, the way there is for, say, inches-per-foot (or even hours-per-day).

And then time zones, daylight saving time, leap years, and leap seconds can make things even more complicated.

But in fact, those complications are one reason that the convert-to-monotonic-timebase technique is so popular, because it cuts through most of that complexity rather nicely. Assuming that library functions like mktime() and localtime() have been implemented properly, they take care of all the nasty details of time zones, daylight saving time, and leap years. Just about the only thing left for you to do is to make sure you use them correctly.

Using them correctly involves:

  • making sure to encode the tm_year and tm_mon fields of struct tm correctly (this is easy to get wrong, and in fact your posted code makes one of the common mistakes, forgetting that tm_mon is 0-based)
  • making sure to set tm_isdst to -1 if you want mktime to figure out whether DST applied for the date/time in question or not
  • setting tm_hour to 12 (not 0) if you're only interested in dates, not times (this helps to avoid certain anomalies that can crop up near DST transitions, although this probably wouldn't matter for your problem), and
  • when converting to days, remembering to round, not truncate, when dividing by 86400, since you may see an effective 23- or 25-hour day length if you're straddling a DST transition.

Leap years are generally not a problem when using this technique, and I'm not sure what you meant when you said that it was "not 100% accurate" because it "does not take care of the leap years". mktime and localtime understand leap years perfectly, and will allow you to compute a proper difference (in seconds) between two dates which straddle the February/March transition, in both leap and non-leap years. It's true that you won't get an accurate number of years if you just divide by the constant 365, but that's no worse than the way you'll often get the wrong number of months if you divide by a constant 30.

So if you've converted to a monotonic number of seconds, the answer to the question of how you convert to years, months, and days is: you don't. It's an impossible, ill-defined problem.

Now, just abut everyone would agree that, informally at least, the difference between, say, February 5 and March 7 is "one month and two days". Notice that this is equally true in leap and non-leap years. If you want to implement this more "informal" or "human-centric" algorithm in a C program, you certainly can, although it's not going to involve mktime or localtime. Instead, it's going to be an implementation of mixed-base arithmetic, with a complication due to the fact that the number of days per month is not constant.

Suppose we want to find the difference between April 5, 2021 and June 10, 2022. That will look like

  year    month   day
  2022    6       10
- 2021    4       5
  ----    --      --
     
     1    2       5

The answer is "One year, two months, and five days". This problem was simple, because we didn't have to borrow.

Here's a harder one: what's the difference between January 25, 2020 and March 5, 2020? That looks like

  year    month   day
  2020    3       5
- 2020    1       25
  ----    --      --
  ????    ??      ??

What are we supposed to do with this? The first date is clearly greater than the second, but of course 5 is less than 20. Informally, from January 25 to February 25 is one month, and then it's a few more days until March 5, but that "few more days" hits the worst case, because it straddles the end of February, so we have to worry about whether it's a leap year or not.

The way to work the problem is probably like this:

  year    month   day
  2020    2       34
- 2020    1       25
  ----    --      --
  0       1       9

Here I have borrowed 1 from the month's column, and added it in to the day's column. Since 2020 was a leap year, there were 29 days in February, so the adjusted days number after the borrow is 5 29=34.

You might have to implement a similar borrow from the year's to the month's column, although that's easier, because the number of months per year is always 12. (That is, I'm pretty sure you will not have to worry about leap years at that point in the algorithm.)

I'm out of time for this too-long answer, so I'm not going to write any C code to do this mixed-base arithmetic, but with this prose outline I think you should be able to implement it straightforwardly enough.

Footnote 1: The algorithm I've presented isn't quite complete. It will not handle, for example, the interval from January 31 to March 1 properly. I'm not honestly sure just now how to handle that rather problematic case.

Footnote 2: Remember that in our Gregorian calendar, the leap-year determination is not a simple test for divisibility by 4.

Footnote 3: For completeness and for anyone who might be curious, I'll say a little more about leap seconds. Unlike leap years, leap seconds are not just automatically taken care of for you by mktime and localtime. In fact, handling leap seconds properly is more or less impossible under the conventional mktime/localtime scheme, for the simple reason that the Unix time_t representation denies the existence of leap seconds altogether. Due to the presence of leap seconds, UTC is not the uniform, continuous, monotonic time scale that time_t assumes. Handling leap seconds properly requires thinking pretty carefully about how you want them to affect your addition and subtraction algorithms, and it requires using some data type other than time_t.

  •  Tags:  
  • c
  • Related