Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I add a number of days to a date in C++20 chrono?

Given the new <chrono> facilities in C++20, how do I add some number of days (say n) to a date?

When I try this I get a compile-time error:

auto d = July/4/2020;
auto d2 = d + days{5};
          ~ ^ ~~~~~~~
error: invalid operands to binary expression
   ('std::chrono::year_month_day' and 'std::chrono::days')
like image 327
Howard Hinnant Avatar asked Dec 23 '22 17:12

Howard Hinnant


1 Answers

A "date" in <chrono> is just a time point with days-precision. And a "date-time" is just a chrono::time_point (usually based on system_clock) with precision finer than days. And the canonical "date type" in <chrono> is:

chrono::time_point<chrono::system_clock, chrono::days>

This is nothing but a days-precision time_point based on system_clock. This particular time_point also has a convenience type alias: sys_days.

using sys_days = time_point<system_clock, days>;

So to add a number of days to sys_days one just does:

sys_days tp = ...;
tp += days{n};
// or
auto tp2 = tp + days{n};

If it is just one day, you can also just:

++tp;

This is very efficient because under the hood it is just adding two ints.

If your time_point has precision finer than days, it is still the same procedure to add days to it:

auto tp = system_clock::now() + days{n};

When I try this I get a compile-time error:

auto d = July/4/2020;
auto d2 = d + days{5};
          ~ ^ ~~~~~~~
error: invalid operands to binary expression
   ('std::chrono::year_month_day' and 'std::chrono::days')

The variable d above has type year_month_day. Even though year_month_day is semantically equivalent to sys_days, it does not directly support days-precision arithmetic. year_month_day is a days-precision time point (but not a chrono::time_point). Instead it is a {year, month, day} data structure. You can easily perform days-precision arithmetic by converting it to sys_days first:

auto d = July/4/2020;
auto d2 = sys_days{d} + days{5};

In this case, the type of the result d2 is sys_days. If you would like the result to have year_month_day, then just convert it back:

year_month_day d2 = sys_days{d} + days{5};

Rationale for this design:

Efficiency. The conversion from year_month_day to sys_days (and back) takes a bit of number crunching. It isn't a huge amount. But it is large compared to a single integral addition.

If the <chrono> library provided days-precision arithmetic for year_month_day, the algorithm would be to convert the year_month_day to sys_days, do the arithmetic, and then convert the result back to year_month_day. If you have a lot of days-precision arithmetic to do, it is better to just traffic in sys_days, and convert to year_month_day only when necessary (i.e. when you need to get the year, month or day fields out of it).

If the library provided the day-precision arithmetic for year_month_day, clients would blindly use it, not realizing that year_month_day is the wrong data structure for their application. It would be akin to giving std::list an indexing operator (which would be quite easy to do):

template <class T, class A>
T&
list<T, A>::operator[](size_type i)
{
    return *advance(begin(), i);
}

Providing such an API just makes it too easy to write inefficient code. sys_days is the data structure of choice for doing days precision arithmetic.

It still doesn't work for me:

auto d = July/4/2020;
auto d2 = sys_days{d} + day{5};
          ~~~~~~~~~~~ ^ ~~~~~~
error: invalid operands to binary expression
   ('std::chrono::sys_days' and 'std::chrono::day')

You need to use days, not day. day is a calendrical specifier for the day of a month in the civil calendar. days is a chrono::duration. See this stack overflow Q/A for a more in-depth discussion about the distinction between these two concepts.

If I have a year_month_day and I want to add a few days to it that I know won't reach the end of the month, can I do that without converting to sys_days and thus gain efficiency?

Yes.

auto d = July/4/2020;
auto d2 = d.year()/d.month()/(d.day() + days{5});

The above simply adds 5 to the day field of d. There is no checking to see if the result goes past the last day of the month. If it does go past the last day of the month, that result will be stored in the day field (up to day 255), and d2.ok() will return false.


year_month_day is good for retrieving the year, month or day fields from a date. It is also good for years and months precision calendrical arithmetic. sys_days is good for days precision arithmetic, and for converting to a finer precision (a date-time).

year_month_day and sys_days can convert to one another with no information loss. Use whichever data structure makes the most sense. And if you forget, the compiler will remind you, just like it does for vector (no push_front), list (no indexing operator), and forward_list (no size).

like image 64
Howard Hinnant Avatar answered Jan 08 '23 15:01

Howard Hinnant