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 :)
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.
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! :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With