The <chrono>
library allows dates to silently fall into a state of !ok(). For example:
#include <chrono>
#include <iostream>
int
main()
{
using namespace std;
using namespace chrono;
auto date = 2023y/October/31;
cout << date.ok() << 'n';
date += months{1};
cout << date.ok() << 'n';
}
Output:
1
0
I get that Oct 31 is a valid date and that Nov 31 is not. But why isn’t Nov 31 an error (assert or throw)? Or why does it not snap back to Nov 30, or roll over into Dec 1 as other date libraries do?
Is it not error prone to just let Nov 31 silently exist?
8
1 Answer
The question, in part, almost answers itself:
Or why does it not snap back to Nov 30, or roll over into Dec 1 as other date libraries do?
Because there isn’t any consistent practice on what should happen, <chrono>
leaves it up to the client for what should happen. Therefore literally anything the programmer can dream up and implement can happen.
For example, here is how to snap back to Nov 30:
auto date = 2023y/October/31;
date += months{1};
if (!date.ok())
date = date.year()/date.month()/last;
And here is how to roll into December:
auto date = 2023y/October/31;
date += months{1};
if (!date.ok())
date = sys_days{date};
The former is quite obvious what it is doing in the code. But the latter deserves a little more explanation: Conversion to sys_days
is just converting a {year, month, day}
data structure into a {count_of_days}
data structure. And that conversion is allowed to happen even if the day field is overflowed. This is just like the C timing API between tm
and time_t
.
And just like there is no invalid time_t
, there is no invalid sys_days
. It is just a count of days since (or before) 1970-01-01. And when you convert that count back to {year, month, day}
, it results in a valid (.ok() == true
) year_month_day
.
Obviously the programmer could also declare an error:
auto date = 2023y/October/31;
date += months{1};
if (!date.ok())
throw "oops!";
One cool aspect of this behavior (prior to corrections added by the programmer) is that the date arithmetic follows normal arithmetic rules:
z = x + y;
assert(x == z - y);
I.e., if you add a month, and then subtract a month, you are guaranteed to get back the same date:
auto date = 2023y/October/31;
date += months{1};
date -= months{1};
assert(date == 2023y/October/31);
And sometimes the correct action is none of flag-error, roll-over or snap-back. Sometimes the correct action is to ignore the invalid date!
Consider this problem:
I want to find all dates for some year y
which are the 5th Friday of the month (because that is party day or whatever). Here is a very efficient function which collects all of the 5th Fridays of a year:
#include <array>
#include <chrono>
#include <utility>
#include <iostream>
std::pair<std::array<std::chrono::year_month_day, 5>, unsigned>
fifth_friday(std::chrono::year y)
{
using namespace std::chrono;
constexpr auto nan = 0y/0/0;
std::array<year_month_day, 5> dates{nan, nan, nan, nan, nan};
unsigned n = 0;
for (auto d = Friday[5]/January/y; d.year() == y; d += months{1})
{
if (d.ok())
{
dates[n] = year_month_day{d};
++n;
}
}
return {dates, n};
}
int
main()
{
using namespace std::chrono;
auto current_year = year_month_day{floor<days>(system_clock::now())}.year();
auto dates = fifth_friday(current_year);
std::cout << "Fifth Friday dates for " << current_year << " are:n";
for (auto i = 0u; i < dates.second; ++i)
std::cout << dates.first[i] << 'n';
}
Example output:
Fifth Friday dates for 2023 are:
2023-03-31
2023-06-30
2023-09-29
2023-12-29
It turns out that it is an invariant that every year will have either 4 or 5 months which will have 5 Fridays. So we can efficiently return the results as a pair<array<year_month_day, 5>, unsigned>, where the second member of the pair will always be either 4 or 5.
The first job is just to initialize the array with a bunch of year_month_day
s. I’ve arbitrarily chosen 0y/0/0
as a good initialization value. What do I like about this value? One of the things I like is that it is !ok()
! If I accidentally access .first[4]
when .second == 4
, an extra bit of safety is that the resultant year_month_day
is !ok()
. So being able to construct these !ok()
values without an assert or exception is important just for that reason (like a nan). The cost? Nothing. These are compile-time constants.
Next I iterate over each month for the year y
. The first thing to do is construct the 5th Friday for January. And then increment the loop by 1 month until the year changes.
Now since not every month has a 5th Friday, this may not result in a valid date. But in this function the proper response to constructing an invalid date is not an assert nor an exception, nor a snap-back or roll-over. The proper response is to ignore the date and iterate on to the next month. If it is a valid date, then it pushed on to the result.
Many invalid dates were computed during the execution of this program. And none of them represented errors. And invalid dates were even used within computations (adding a month). But because the arithmetic is regular, everything just works.
So in summary, it is really best to leave the behavior of invalid dates up to the clever programmer. Because clever programmers can create all kinds of ingenious solutions to their problems, given the flexibility to do so.
9
-
Will gcc 14 libstdc++ have
chrono
fully implemented? (if you know). I check the status page, but it is so piece-meal, it is hard to tell.– David C. Rankinyesterday
-
Do you then consider a
class ok_date
that is always ok? You have already provided the conversion method; why not make a full fledged class?– Red.Waveyesterday
-
@DavidC.Rankin. Yes, to the best of my knowledge, gcc 14 has the entire C++20 chrono package.
– Howard Hinnantyesterday
-
1
@Red.Wave. I think it would be fine (and easy) for clients to create their own
class ok_date
. I do not think it would be a good idea forstd::chrono
to supply it because there is no clear one right way to design it.– Howard Hinnantyesterday
-
2
I once debugged a major application outage that was due to a datetime library being fussy about "nonexistent" times when there was no need for it to be. Especially for a stdlib feature it definitely makes sense to let the caller decide what sort of handling they want.
– hobbs17 hours ago
It's a bit inconsistent that you can add a month and get a !ok() date. But you can't add days at all and get a compiler error. Well, it's C++. You get used to all it's §$%& design choices. wandbox.org/permlink/BMr5uWSRzxPwjCrl
17 hours ago
Here is a FAQ on that issue: github.com/HowardHinnant/date/wiki/FAQ#day_arithmetic. But perhaps that would make a good SO Q/A as well. Thank you for the suggestion. And here is your example with working syntax: wandbox.org/permlink/Fyxa6J2MEARHg4ZG
17 hours ago
I don't see a different in adding months instead of days. So, for the same reason we should get a compiler error when adding months. Because for doing it right, we need conversions via sys_days() as shown in the answer. One thing is to hide or not hide cost. The other thing is to be consistent.
17 hours ago
2000 divided by 4 and divided by 5 is not 100, but the 5th of April 2000. What a mess. Only Americans can understand. It should really be 2000 minus 04 minus 05, which is not 1991 but also the 5th of April 2000. Only ISO can understand. For me, as a teacher, chrono is broken since the beginning. You can't teach all that. It's too much. 100 exceptional cases here, another 200 there and 500 core guidelines as well. They should ask the users how they want the API be designed. I only care about performance when I have a performance issue. Until then, I want useful high level APIs.
17 hours ago
vector and list and deque are separate classes. They don't need the same interface. Nobody expects a stack to have a push_back or push_front method. It just has push and pop. As expected. And that's the point. What do I expect? I expect that I can add days to a year_month_day. Why not? It's totally reasonable. Even small children can do it. But not C++. It violates the principle of least astonishment. Same for the division operator. It's astonishing. Unexpected. You can't intuitively find this, you need to learn it the hard way. As a teacher, I don't like learning the hard way.
16 hours ago