Why can adding a month to a valid date result in an invalid date instead of adjusting the result to be valid?

Why can adding a month to a valid date result in an invalid date instead of adjusting the result to be valid?


16

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

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

8

  • 1

    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

    – Thomas Weller

    17 hours ago


  • 1

    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

    – Howard Hinnant

    17 hours ago

  • 1

    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.

    – Thomas Weller

    17 hours ago

  • 1

    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.

    – Thomas Weller

    17 hours ago

  • 1

    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.

    – Thomas Weller

    16 hours ago

1 Answer
1


26

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

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.

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

    yesterday


  • 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

    yesterday

  • @DavidC.Rankin. Yes, to the best of my knowledge, gcc 14 has the entire C++20 chrono package.

    – Howard Hinnant

    yesterday

  • 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 for std::chrono to supply it because there is no clear one right way to design it.

    – Howard Hinnant

    yesterday

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

    – hobbs

    17 hours ago



Leave a Reply

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