When is it ok to be !ok() with C++20 chrono dates?

When is it ok to be !ok() with C++20 chrono dates?


10

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

Demo.

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 libs do?

Is it not error prone to just let Nov 31 silently exist?

1 Answer
1


15

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 libs do?

Because there is no 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 Decmeber:

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 arithemtic 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);

Demo.

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

Demo.

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_days. 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.

2

  • 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. Rankin

    9 hours ago


  • 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.Wave

    1 hour ago



Leave a Reply

Your email address will not be published. Required fields are marked *