In the previous post we discussed elements of the <chrono> library which are already available as of C++11 or C++14. This is our present. What about the future? Enter the date and timezone libraries. We will talk about the date library today. date will be, most likely, available in C++20. The paper currently voted into C++20 can be found here.
But what about the people who want to play with it now? Well, Howard Hinnant has implemented a prototype of the library and has graciously made it available to everybody. date.h is provided as an header-only library so using it in your favorite build system/IDE should be trivial. Even better, if you use Vcpkg the library is available on this package manager. So if you are a Vcpkg user, obtaining the library is a simple as writing:
vcpkg.exe install date:x64-windows
or
vcpkg.exe install date:x86-windows
Here we will summarize some key ideas of the header date.h. What we discuss today will be applicable to the date library in C++20 and, hopefully, the difference will be minimal.
For a more in-depth look, sit down, relax, allocate 1 hour of your time and watch this:
The talk is extremely entertaining and Howard goes in the nitty-gritty details of the architecture of the library and discuss its performance. We will discuss the library focusing on its use cases and main types, leaving the rest to the video for anybody who’s interested.
Counting the days
First, we should say that date deals only with UTC (like system_clock), so all the dates we will see are UTC, for non-UTC dates we will need the timezone library.
I feel like a broken record, but date is built following the same principles of chrono: if it builds it is correct, enforcing constraints and performing conversions is achieved through the type system. The core type of date is date::sys_days (day_point in the video):
using sys_days =
std::chrono::time_point<std::chrono::system_clock, date::days>
sys_days represents the number of days from the epoch of the system_clock. The epoch is 00:00:00 UTC, 01/01/1970 (standard in C++20, de-facto standard before).
We can already start doing some tests:
#include <iostream>
#include <chrono>
#include "date/date.h"
int main(){
auto now = std::chrono::system_clock::now();
date::sys_days nowInDays =
std::chrono::floor<date::days>(now);
std::cout << "Days since epoch "
<< nowInDays.time_since_epoch().count() << '\n';
}
Sure enough the program prints 18136 days, using this side you’ll be able to find out when this article was actually written.
We saw before that chrono allowed us to lift the hood of its compile-time machinery and extended it. date does the same.
Story time: during my first “real” job I had to work in a codebase where dates were stored as days since epoch in double. Debatable idea, but when in Rome… Let’s see if we can handle a double number of days. We first create our custom duration having as underlying type a double.
using perH = std::chrono::hours::period;
using hoursInADay = std::ratio_multiply<std::ratio<24>, perH>;
using daysD = std::chrono::duration<double, hoursInADay>;
using sys_days_d =
std::chrono::time_point<std::chrono::system_clock, daysD>;
Then, we assign the value of now expressed in the duration of system_clock to an instance of sys_days_d. And, wait, no cast? Yes, no cast, the cast is needed only when introducing truncation error, not rounding error.
auto now = std::chrono::system_clock::now();
//No casts here
sys_days_d nowInDoubleDays = now;
std::cout << "Days since epoch (double) "
<< nowInDoubleDays.time_since_epoch().count() << '\n';
Now, we transform the difference between the number of days in double and the number of days in integer in seconds: here we have truncation, (double to integer) so we need a cast.
std::chrono::seconds diff =
std::chrono::floor<std::chrono::seconds>
(nowInDoubleDays - nowInDays);
std::cout << date::time_of_day<decltype(diff)>{ diff } << '\n';
time_of_day is a type that breaks a duration from midnight in hours, minutes and seconds.
Note: in older material one can find the factory function date::make_time. While still available in the library, it was substituted in the standard by direct calls to the constructor of time_of_day with appropriate deduction guides, hence we will be able to write the following in C++20:
std::cout << time_of_day{ diff } << '\n';
Have you noticed that we can call directly operator<< ? The types in date.h (and corresponding C++20 proposal) have the operator and C++20 introduces operator<< for the other types in chrono.
Counting months, years, etc.
Counting days since the epoch is nice and good, but we would like to do something more useful. Like knowing today’s date. Let’s use date::year_month_day:
date::year_month_day ymd{ nowInDays };
std::cout <<"Today is " << ymd << '\n';
Or we would like to know the number of days between epoch and a particular date. We could try:
date::year_month_day ymd_does_not_compile{ 2019, 5, 8 };
std::cout << "Number of days between epoch and 8 May 2019"
<< date::sys_days{ymd2}.time_since_epoch().count()
<< '\n';
And, it doesn’t compile, why? Well, again, type safety, implicit conversions between numbers and types in date are not allowed, but this will work
date::year_month_day ymd2{date::year{2019},
date::month{5},
date::day{8} };
std::cout << "Number of days since epoch "
<< date::sys_days{ymd2}.time_since_epoch().count()
<< '\n';
or, using the cute API we can have even more readable code:
using namespace date::literals;
date::year_month_day ymd2 = 2019_y/may/8_d;
std::cout << "Number of days since epoch "
<< date::sys_days{ymd2}.time_since_epoch().count()
<< '\n';
Using the cute API one can create year_month_day instances in three different ways:
date::year_month_day ymd3 = 2019_y / may / 8_d;
date::year_month_day ymd4 = 8_d/ may / 2019_y;
date::year_month_day ymd5 = may / 8_d / 2019_y;;
Note: as of now, the C++20 version of the library will have the same literals with some differences, for example there will be d instead of _d or May instead of may, so you will have to write 2019y/May/8d.
We have seen also that we can convert date::sys_days to date::year_month_day and vice versa. Now, what if I want to know what day of the week is today? Sure enough we have a type for it.
date::year_month_weekday ymwd{ nowInDays };
std::cout << ymwd << '\n';
Done! So, what about finding out the final day of the current month? Yet another type.
date::year_month_day_last ymd_last = ymwd.year()/ymwd.month()/last;
std::cout << ymd_last.day() << '\n';
And finally, we have year_month_weekday_last which allows us to get the last Sunday of the current month:
date::year_month_weekday_last
ymwd_last = ymwd.year()/ ymwd.month()/date::Sunday[last];
std::cout << date::year_month_day{ ymwd_last } << '\n';
Lastly, we should notice that all these types provide arithmetical operators and they have a consistent algebra. If you cannot find the operator you need, then, most likely its use would have required an expensive conversion. In this case you are responsible to perform the appropriate conversion, execute the operation and convert back the date if needed. It might look clumsy but in this way nothing is hidden and you are responsible to make the smart choices.
Invalid dates
The library seems to work well when dealing with normal dates. What if the date does not exist? In this case the September 31st:
date::year_month_day ymd2020 = 2020_y / 8 / last;
ymd2020 += date::months{1};
std::cout << ymd2020 << " "<< ymd2020.ok() << std::endl;
The types of the library implement the member function bool ok() that returns false if the value is invalid. With this knowledge we can choose what to do, the library impose no choice on us.
For example we can decide to snap to the end of the month: we can disassemble the invalid date and use year, month and date::last to construct a new date that snaps to the last valid day of the current month.
year_month_day snapToEndOfMonth(const year_month_day& date) {
if (date.ok()) return date;
return date.year() / date.month() / date::last;
}
But we can also decide to overflow in the next month, here we can see that the date, while invalid, can still be converted to sys_days (number of days from the epoch). Then the number of days can be converted back to a valid date, as we saw in one of the first examples.
year_month_day overflowIntoNextMonth(const year_month_day& date) {
if (date.ok()) return date;
sys_days d{ date };
return year_month_day{ d };
}
constexpr
constexpr declares that it is possible to evaluate the function at compile time. Looking at the date library we can see that a slew of functions are constexpr, meaning that they can be calculated at compile time. So the compiler can decide to push at compile time the calculations for which it knows already the inputs:
constexpr year_month_day dyw_last = 2019_y / 8 / last;
constexpr auto has_31_days = dyw_last.day()==date::day{31};
static_assert(has_31_days, "No");
Conclusion
I hope that this non exhaustive tour showed you how powerful, clean and simple this library is and the kind of facilities C++20 will have for date calculations. More information regarding all the types, operators and conversions can be found in the part of the proposal detailing the proposed wording for the standard.
