C++20: Seemingly Unexpected Behavior with Date Arithmetic
C++20 has added support for calendars, dates, and time zones to the C++ Standard Library. This also allows you to perform arithmetic with dates. However, certain arithmetic might give seemingly the wrong results.
Let’s look at a simple example that works as expected:
using namespace std::chrono;
auto timestamp1 = sys_days{ 2022y / January / 19d } + 8h + 39min + 42s;
auto timestamp2 = timestamp1 + days{ 3 }; // Add 3 days
std::cout << timestamp1 << '\n' << timestamp2;
The output is as expected:
2022-01-19 08:39:42
2022-01-22 08:39:42
Now let’s try to add 1 year to timestamp1:
auto timestamp3 = timestamp1 + years{ 1 }; // Add 1 year
std::cout << timestamp1 << '\n' << timestamp3;
The output now is:
2022-01-19 08:39:42
2023-01-19 14:28:54
The date part is correctly incremented with 1 year, but the timestamp looks wrong on first sight. However, this is correct according to the C++ standard. The reason why it is seemingly off has to do with support for leap years. The C++ standard states that adding 1 year to a date must add 1 average year to keep leap years into account. So, while you could expect adding 1 year adds 86,400 * 365 = 31,536,000 seconds (86,400 = number of seconds in a day), it doesn’t. Instead, it adds 86,400 * ((365 * 400) + 97) / 400) = 31,556,952 seconds.
The reason why it is behaving like this is that the type of timestamp1, 2, and 3 is std::chrono::time_point which is a so-called serial type, i.e., it represents a date as a single number relative to a certain epoch (= clock origin). If you don’t want this behavior you can perform the arithmetic with a field-based type. The serial-based types above can be converted to a field-based representation as follows:
// Split timestamp1 into "days" and "remaining seconds".
sys_days timestamp1_days = time_point_cast<days>(timestamp1);
seconds timestamp1_seconds = timestamp1 - timestamp1_days;
// Convert the timestamp1_days serial type to a field-based year_month_day.
year_month_day ymd2 = timestamp1_days;
// Add 1 year.
year_month_day ymd3 = ymd2 + years{ 1 };
// Convert the result back to a serial type.
auto timestamp4 = sys_days{ ymd3 } + timestamp1_seconds;
std::cout << timestamp1 << '\n' << timestamp4;
The output now is:
2022-01-19 08:39:42
2023-01-19 08:39:42