I know that I can format a chrono date with the ISO week-based calendar (%G
, %V
and %u
) but how can I compute these values myself using <chrono>
?
I recommend creating a type that holds the ISO week-based year, week number and weekday and giving that type conversions functions to and from std::chrono::sys_days
. This gives you what you're asking for (conversions to the ISO week-based calendar) and from the ISO week-based calendar. And it sets you up to interoperate with any other calendrical system that has conversions to and from sys_days
.
A first draft of this data structure might look like:
struct iso_week_date
{
std::int16_t year;
std::uint8_t weeknum;
std::chrono::weekday dow;
constexpr iso_week_date(int y, unsigned wn, std::chrono::weekday wd) noexcept : year{ y }, weeknum{ wn }, dow{ wd } {}
constexpr iso_week_date(std::chrono::sys_days tp) noexcept;
constexpr operator std::chrono::sys_days() const noexcept;
};
I've made this as simple as possible for the purpose of demonstration. One can easily see though that one could add functionality to this such as year arithmetic, make the data members private and give them getters and setters, add streaming and parsing operators, etc.
The constructor that takes a sys_days
is the conversion function asked for in the question. The operator sys_days
is the reverse conversion. And these can trivially be made noexcept
and constexpr
enabling compile-time computation of these operations.
Let's start with the sys_days
constructor:
The first thing to understand about the ISO week-based calendar is that it divides the year up into exactly 52 or 53 weeks, with the week starting on Monday. Every week is fully within a single year ISO year. Thus the ISO year is not always the same as the civil year, though it usually is. Near the beginning or end of the civil year, the ISO year can be one off of the civil year. For example when December 31 falls on a Monday, that Monday shares the same year as the following Thursday, one greater than the civil year for that Monday.
For every day of the week except Thursday, the civil year can be one behind or one ahead of the ISO year. But for Thursday, the civil year and the ISO year are always the same. That combined with the fact that a week (Monday thru Sunday) is always in the same year, is important in computing the conversion from sys_days
to iso_week_date
.
constexpr
iso_week_date::iso_week_date(std::chrono::sys_days tp) noexcept
: iso_week_date{0, 0, std::chrono::weekday{}}
{
using std::chrono::sys_days;
using std::chrono::year_month_day;
using std::chrono::Monday;
using std::chrono::Thursday;
using std::chrono::weeks;
using std::chrono::weekday;
using std::chrono::days;
using std::chrono::floor;
dow = weekday{tp};
auto closest_thursday = [this](sys_days tp)
{
auto i = static_cast<int>(dow.iso_encoding());
tp += days{4-i};
return tp;
};
auto y = year_month_day{closest_thursday(tp)}.year();
auto start = sys_days{y/1/Thursday[1]} - (Thursday - Monday);
year = int{y};
weeknum = floor<weeks>(tp - start) / weeks{1} + 1;
}
The closest_thursday
helper function takes a sys_days
and maps it to the nearest Thursday. That is, it adds a few days to Monday, Tuesday or Wednesday, and subtracts a few days from Friday, Saturday or Sunday in order to produce a Thursday. This is used to get the correct ISO year for the ISO week to which tp
refers, no matter what day of the week tp
has.
Once the correct ISO year is computed, the start of the ISO year is the Monday before the first Thursday of the year. The expression (Thursday - Monday)
is simply a verbose way of writing days{3}
, and is meant to indicate that the expression is finding the Monday prior to the first Thursday. Substitute days{3}
if desired. That will not impact the correctness or performance of the code.
Next it is straightforward to fill out the remaining iso_week_date
data members. Note that the week count starts at 1, not 0, thus the + 1
.
The reverse conversion (from iso_week_date
to sys_days
) is even easier:
constexpr
iso_week_date::operator std::chrono::sys_days() const noexcept
{
using std::chrono::sys_days;
using std::chrono::Monday;
using std::chrono::Thursday;
using std::chrono::weeks;
auto start = sys_days{std::chrono::year{year}/1/Thursday[1]} - (Thursday - Monday);
return start + weeks{weeknum-1} + (dow - Monday);
}
First the start of the ISO year is computed (the Monday prior to the first Thursday). And then to that is added the correct number of weeks, and the number of days after Monday.
This can be exercised like this:
int
main()
{
using namespace std::chrono;
iso_week_date d1{2024y/March/29};
std::cout << d1.year << "-W" << unsigned{d1.weeknum} << " " << d1.dow << '\n';
iso_week_date d2{Friday[last]/March/2024};
std::cout << d2.year << "-W" << unsigned{d2.weeknum} << " " << d2.dow << '\n';
sys_days d3 = sys_days{d2} + days{3};
std::cout << d3 << '\n';
}
This converts a year_month_day
to iso_week_date
. Note that nowhere in the code is iso_week_date
aware of the type year_month_day
. Then it converts a year_weekday_last
to iso_week_date
. This happens to be the same date as the previous example. And note that iso_week_date
is not aware of the type year_weekday_last
. The conversion is bouncing off of sys_days
under the hood.
Note that if someone else creates yet another calendar that has conversions to and from sys_days
(Julian, Hebrew, Mayan, Islamic, a different week-based calendar ...) that iso_week_date
will be able to convert to and from those calendars as well.
Finally the reverse conversion is used to find out the date 3 days later.
Output:
2024-W13 Fri
2024-W13 Fri
2024-04-01