使用 C++20 <chrono> 计算基于 ISO 周的日期

How*_*ant 3 c++ c++-chrono c++20

我知道我可以使用基于 ISO 周的日历(%G%V%u)来格式化计时日期,但我如何使用 自己计算这些值<chrono>

How*_*ant 5

我建议创建一个类型来保存基于 ISO 周的年份、周数和工作日,并为该类型提供与std::chrono::sys_days. 这将满足您的要求(转换为 ISO 基于周的日历)以及 ISO 基于周的日历。它使您能够与任何其他具有sys_days.

该数据结构的初稿可能如下所示:

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;
    constexpr iso_week_date(std::chrono::sys_days tp) noexcept;
    constexpr operator std::chrono::sys_days() const noexcept;
};
Run Code Online (Sandbox Code Playgroud)

为了演示的目的,我已使其尽可能简单。人们可以很容易地看出,可以为此添加功能,例如年份算术,使数据成员私有并为它们提供 getter 和 setter,添加流和解析运算符等。

采用 a 的构造函数sys_days是问题中要求的转换函数。是operator sys_days逆向转换。这些可以轻松完成noexceptconstexpr启用这些操作的编译时计算。

让我们从sys_days构造函数开始:

关于 ISO 基于周的日历,首先要了解的是,它将一年精确地划分为 52 或 53 周,每周从星期一开始。每个星期都完全在 ISO 年的单个年份内。因此,ISO 年份并不总是与民用年份相同,尽管通常是相同的。在民用年开始或结束时,ISO 年可以是民用年的其中一个年份。例如,当 12 月 31 日是星期一时,该星期一与下一个星期四为同一年份,比该星期一的民用年份大一年。

对于一周中的每一天(周四除外),民用年份可以比 ISO 年份晚一年或早一年。但对于星期四,民用年份和 ISO 年份始终相同。再加上一周(周一到周日)始终在同一年这一事实,对于计算从sys_days到 的转换非常重要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;
}
Run Code Online (Sandbox Code Playgroud)

辅助closest_thursday函数接受 asys_days并将其映射到最近的星期四。也就是说,它在星期一、星期二或星期三加上几天,并从星期五、星期六或星期日减去几天,以产生星期四。这用于获取所tp引用的 ISO 周的正确 ISO 年份,无论一周中的哪一天tp

计算出正确的 ISO 年份后,ISO 年的开始时间为该年第一个星期四之前的星期一。该表达式(Thursday - Monday)只是一种详细的书写方式days{3},旨在表明该表达式在第一个星期四之前的星期一查找。days{3}如果需要可以替换。这不会影响代码的正确性或性能。

接下来,填写剩余的iso_week_date数据成员就很简单了。请注意,周计数从 1 开始,而不是从 0 开始,因此+ 1.

反向转换(从iso_week_datesys_days)甚至更容易:

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);
}
Run Code Online (Sandbox Code Playgroud)

首先计算 ISO 年的开始时间(第一个星期四之前的星期一)。然后添加正确的周数以及星期一之后的天数。

可以这样练习:

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';
}
Run Code Online (Sandbox Code Playgroud)

这会将 a 转换year_month_dayiso_week_date. 请注意,代码中没有任何地方iso_week_date知道该类型year_month_day。然后它将 a 转换year_weekday_lastiso_week_date. 这恰好与前面的示例是同一日期。并且注意的iso_week_date是不知道类型year_weekday_last。转换是在sys_days引擎盖下进行的。

请注意,如果其他人创建了另一个具有相互转换的日历sys_days(儒略历、希伯来历、玛雅历、伊斯兰历、不同的基于周的日历......),那么也iso_week_date将能够在这些日历之间进行转换。

最后通过逆向转换找出3天后的日期。

输出:

2024-W13 Fri
2024-W13 Fri
2024-04-01
Run Code Online (Sandbox Code Playgroud)

演示。