4

Inspired by this SO Answer, which has a question in it:

I just wish chrono could let me do something like this:

std::chrono::time_point<std::chrono::system_clock> xmas = std::chrono::datetime("2023-12-25");

E.g. let me get a fixed timepoint based on a date as specified by ISO 8601. If the date is wrong somehow, either raise exception or otherwise set timepoint to epoch with whatever error handling is deemed appropriate. Non-ISO 8601 dates will not be supported, although it should be noted other standards could also be implemented.

Optionally, you can use XXXX for current year e.g. XXXX-01-01 becomes Jan. first of this year and XXXX-12-25 becomes dec. 25th but now I am getting really out there on the wish list :)

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577

2 Answers2

8

Think of C++20 std::chrono as a set of building blocks for date & time code. There's really nothing you can't easily build out of these fundamental building blocks while avoiding most of the trickery associated with time, time zones and calendars.

For example here is a function that is only a couple of dozen lines long that allows you do to exactly what you want, including getting everything on your wish list. Line-by-line explanation follows the code:

#include <chrono>
#include <sstream>
#include <stdexcept>

namespace my
{

std::chrono::system_clock::time_point
datetime(std::string const& s)
{
    std::istringstream in{s};
    std::chrono::year y;
    std::chrono::month_day md;
    if (in.peek() == 'X')
    {
        in >> std::chrono::parse("XXXX-%m-%d", md);
        if (in.fail())
            throw std::runtime_error(
                "Unable to parse a date of the form XXXX-mm-dd out of \"" + s + '"');
        y = std::chrono::year_month_day{
                std::chrono::floor<std::chrono::days>(
                    std::chrono::system_clock::now())}.year();
    }
    else
    {
        in >> std::chrono::parse("%Y-", y) >> std::chrono::parse("%m-%d", md);
        if (in.fail())
            throw std::runtime_error(
                "Unable to parse a date of the form yyyy-mm-dd out of \"" + s + '"');
    }
    auto date = y/md;
    if (!date.ok())
        throw std::runtime_error("Parsed invalid date out of \"" + s + '"');
    return std::chrono::sys_days{date};
}

}  // namespace my
  • The first thing to do is to find out if the string is of the form XXXX-mm-dd or yyyy-mm-dd. This is easily accomplished by peeking at the first character of the string. If it is X then it must be XXXX-mm-dd, else it must be yyyy-mm-dd, else it is an error that we flag by throwing an exception with a detailed error message.

  • If the string looks like it is of the form XXXX-mm-dd, then parse a chrono::month_day with the format string "XXXX-%m-%d". If there are any parsing errors, or if the parsed month_day could not possibly be valid, the parse will fail.

  • If the parse failed, throw an exception with a helpful error message.

  • If the parse succeeded, compute the current year (UTC) and assign that to y. If the local year is desired, or the year in any IANA time zone is desired, that is only a couple more lines of code.

  • Otherwise the string must be of the form yyyy-mm-dd. Parse into a chrono::year and a chrono::month_day separately.

  • If any parse failed, throw an exception with a helpful error message.

  • Finally combine the year and the month_day into a year_month_day (called date in this demo code).

  • Check for the possibility that the year is valid, and the month_day is valid, but the combination of these two is not valid. This will catch things like February 29 on a non-leap-year. If found, throw an exception with a helpful error message.

  • Convert the parsed date to a system_clock::time_point by first converting to a sys_days, and then letting the implicit conversion refine the precision to system_clock::time_point.

This can be exercised like this:

#include <iostream>

int
main()
{
    auto xmas = my::datetime("2023-12-25");
    std::cout << xmas << '\n';
    xmas = {};
    xmas = my::datetime("XXXX-12-25");
    std::cout << xmas << '\n';
    try
    {
        xmas = my::datetime("XXXX-25-12");
    }
    catch (std::exception const& e)
    {
        std::cout << e.what() << '\n';
    }
}

Which outputs:

2023-12-25 00:00:00.000000
2023-12-25 00:00:00.000000
Unable to parse a date of the form XXXX-mm-dd out of "XXXX-25-12"

Note that our code caught the error of correct syntax, but the invalid date of month 25 day 12.

Also note that other date formats could be supported with more checking and branching if desired.

Howard Hinnant
  • 206,506
  • 52
  • 449
  • 577
  • `g++ (GCC) 13.1.1 20230429` results in `error: ‘parse’ is not a member of ‘std::chrono’` for `in >> std::chrono::parse("XXXX-%m-%d", md);` and the other two. Is this a gcc failure to include some dependent header for the object manip discussed at [std::chrono::parse](https://en.cppreference.com/w/cpp/chrono/parse) which lists only `` as required? Using `g++ -Wall -Wextra -pedantic -Wshadow -std=c++20 -Ofast -o test test.cpp` – David C. Rankin Jul 09 '23 at 07:38
  • Oops - never mind, found you at [Solved-from_stream not a member of std::chrono-C++](https://www.appsloveworld.com/cplus/100/855/from-stream-not-a-member-of-stdchrono) with link to [Date](https://github.com/HowardHinnant/date) FYI `clang++ 15.07` has same issue. – David C. Rankin Jul 09 '23 at 07:42
  • Unfortunately MSVC has the only complete implementation at this time. My services have been, and continue to be available to help the other implementors along. gcc is getting very close. – Howard Hinnant Jul 09 '23 at 13:10
  • Thanks Howard. I've followed the development and adoption of `chrono` and it is an amazing piece of work. Your Q/A like this one help the pieces fit together. The building blocks metaphor is perfect. I've still got a way to go digesting it all, but we are getting there. When gcc gets its act together things will be much easier. Biggest drawback with bandaiding it together until then is *Template Spew* -- the bane of C++... 1000 lines of spaghetti to tell you `error: no matching function for call to ‘parse(const char [11], std::chrono::month_day&)’`.... – David C. Rankin Jul 10 '23 at 04:56
2

So, the answer given is almost correct but ISO 6801 dates are a little more complex than that. In essence, an ISO 6801 string consists of three distinct parts, date, time, and timezone. These are all valid representations of the same date and time:

2023-07-10T10:11:12Z
2023W281T9:11:12-1
2023190T114112+1:30

More specifically, the standard specifies three distinct parts that all may or may not exist. First, the date may be written as one of:

  • CC (1st of Jan, CC00)
  • YYYY (1st of Jan, YYYY)
  • YYYY-MM (1st of MM, YYYY)
  • YYYYMMDD or YYYY-MM-DD (DD of MM, YYYY)
  • YYYYWww or YYYY-Www (Monday, Week ww, YYYY)
  • YYYYWwwD or YYYY-Www-D (day D of Week ww, YYYY)
  • YYYYDDD or YYYY-DDD (Day DDD of YYYY)

All of the above are valid dates. Optionally, one may also supply a time with the T prefix. Note millihours, which is 3600 milliseconds per millihour, and milliminutes, which is 60 milliseconds per milliminute.

  • Thh (hh:00:00)
  • Thhmm or Thh:mm (hh:mm:00)
  • Thhmmss or Thh:mm:ss (hh:mm:ss)
  • Thh.hhh (hh:00:00 + hhh millihours)
  • Thhmm.mmm or Thh:mm.mmm (hh:mm:00 + mmm milliminutes)
  • Thhmmss.sss or Thh:mm:ss.sss (hh:mm:ss + sss milliseconds)

And of course, in order to complicate things further, the world also has time zones. This is added with either a Z (UTC time), or +, or - corrections. So, again, here are the valid extensions:

  • Z (UTC time)
  • +hh (Offset by hh hours before UTC time)
  • -hh (Offset by hh hours after UTC time)
  • +hhmm or +hh:mm (Offset by hh hours and mm minutes before UTC time)
  • -hhmm or -hh:mm (Offset by hh hours and mm minutes after UTC time)

It sure would be convenient to have a function handy that would validate these specific expressions and nothing else for a proper time output. To my knowledge no proper STL implementation exists of this standard, though I do make the argument there should be one that then outputs a proper std::chrono timepoint. That does not solve the current problem though, so for now... Use std::chrono as the backend and make it yourself! :)