Search code examples
c++mfccoledatetime

What is the right way to add a year to COledateTime taking into account leap years?


I have this code:

COleDateTime datStart = COleDateTime::GetCurrentTime(), datEnd;

// Update end date (one year later)
datEnd.SetDateTime(datStart.GetYear() + 1,
    datStart.GetMonth(),
    datStart.GetDay(),
    datStart.GetHour(),
    datStart.GetMinute(),
    datStart.GetSecond());

The above code failed yesterday (Feb 29, 2020) because datEnd resulted in Feb 29, 2021 which is not valid.

What is the right way to safely add a year to datStart taking into account leap years?

C# has:

DateTime theDate = DateTime.Now;
DateTime yearInTheFuture = theDate.AddYears(1);

What is the equivalent for MFC/C++?

One possibility is:

COleDateTimeSpan spnYear;
spnYear.SetDateTimeSpan(365, 0, 0, 0);

datEnd = datStart + spnYear;

But it is still potentially flawed due to leap years having 366 days. So what is the right way?

I see this similar question:

C++ add 1 year to date

It implies using boost. I do have boost in my project although never used it for date manipulation. Wonder if this can be used with a COleDateTime object?

Using boost:

boost::gregorian::date dStart{ datStart.GetYear(), datStart.GetMonth(), datStart.GetDay() };
boost::gregorian::date dEnd = dStart + boost::gregorian::years(1);

datEnd.SetDateTime(
    dEnd.year(),
    dEnd.month(),
    dEnd.day(),
    datStart.GetMonth(),
    datStart.GetMinute(),
    datStart.GetSecond());

Doesn't compile.

4>D:\My Programs\2019\MeetSchedAssist\Meeting Schedule Assistant\PublishersDatabaseDlg.cpp(354,49): error C2398: Element '1': conversion from 'int' to 'boost::gregorian::date::year_type' requires a narrowing conversion
4>D:\My Programs\2019\MeetSchedAssist\Meeting Schedule Assistant\PublishersDatabaseDlg.cpp(354,70): error C2398: Element '2': conversion from 'int' to 'boost::gregorian::date::month_type' requires a narrowing conversion
4>D:\My Programs\2019\MeetSchedAssist\Meeting Schedule Assistant\PublishersDatabaseDlg.cpp(354,89): error C2398: Element '3': conversion from 'int' to 'boost::gregorian::date::day_type' requires a narrowing conversion

Solution

  • COleDateTime is a wrapper around the (hideously surprising) DATE type. Internally it stores a 64-bit floating point value, where whole numbers represent the days since 30th of December, 1899 (midnight). The fractional part represents the time of day (e.g. 0.25 means 6 A.M., and 0.5 is noon). Every floating point value is a legal value and represents a unique1 point in time.

    Adding a year to a COleDateTime value amounts to adding the value 365.0 (or whichever value suits your needs) to its internal representation. This can be done by accessing the m_dt member directly

    COleDateTime datEnd{ datStart.m_dt + 365.0 };
    

    or by using a COleDateTimeSpan

    datEnd = datStart + COleDateTimeSpan{ 365.0 };
    // or
    datEnd = datStart + COleDateTimeSpan{ 365, 0, 0, 0 };
    

    This operation is always well defined, and will produce a valid time point regardless of leap years. Under the hood, it's just adding floating point values, that have no notion of calendrical properties. The following code

    COleDateTime datStart{ 2020, 2, 29, 0, 0, 0 };
    COleDateTime datEnd{ datStart + COleDateTimeSpan{ 365.0 } };
    

    will produce a time point that represents February 28th, 2021 (note that if you add 366.0 days, this will return March 1st, 2021).


    The initial solution that attempts to construct a COleDateTime by doing arithmetic on the individual date components fails during construction. The constructor validates its input. When passed (2021, 2, 29, 0, 0, 0) it determines an invalid date, and sets m_status to invalid.


    1 That's not entirely correct. Values in the range (-1 .. 0] map to the same time point as their absolute values. Note, that e.g. COleDateTime{-0.25} == COleDateTime{0.25} evaluates to false, even though they represent the same point in time.