什么时候可以使用C++20 chrono date!ok()?

h43kikqp  于 11个月前  发布在  其他
关注(0)|答案(1)|浏览(99)

<chrono>库允许日期静默地进入!ok()状态。例如:

#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';
}

字符串
输出量:

1
0


Demo.
我知道10月31日是一个有效的日期,而11月31日不是。但是为什么11月31日不是一个错误(Assert或抛出)?或者为什么它不像其他日期库那样快速返回到11月30日,或者滚动到12月1日?
难道让11月31日静静地存在不容易出错吗?

ldxq2e6h

ldxq2e6h1#

这个问题,在某种程度上,几乎可以自己回答:
或者为什么它不像其他日期库那样快速回到11月30日,或者滚动到12月1日?
因为没有任何一致的实践来决定应该发生什么,<chrono>把应该发生什么留给了客户端。因此,程序员可以想象和实现的任何事情都可以发生。
例如,以下是如何快速返回到11月30日:

auto date = 2023y/October/31;
date += months{1};
if (!date.ok())
    date = date.year()/date.month()/last;

字符串
以下是如何进入12月:

auto date = 2023y/October/31;
date += months{1};
if (!date.ok())
    date = sys_days{date};


前者在代码中的作用是显而易见的。但后者需要更多的解释:转换到sys_days只是将{year, month, day}数据结构转换为{count_of_days}数据结构。即使day字段溢出,也允许进行转换。这就像tmtime_t之间的C计时API。
就像没有无效的time_t一样,也没有无效的sys_days。它只是自1970-01-01以来(或之前)的天数计数。当您将该计数转换回{year, month, day}时,它会导致有效的(.ok() == trueyear_month_day
显然,程序员也可以声明一个错误:

auto date = 2023y/October/31;
date += months{1};
if (!date.ok())
    throw "oops!";


这种行为的一个很酷的方面(在程序员添加更正之前)是日期算术遵循正常的算术规则:

z = x + y;
 assert(x == z - y);


也就是说,如果你加上一个月,然后减去一个月,你保证会得到相同的日期:

auto date = 2023y/October/31;
assert( date.ok());
date += months{1};
assert(!date.ok());
date -= months{1};
assert( date.ok());
assert(date == 2023y/October/31);


Demo.
有时正确的操作不是flag-error,roll-over或者snap-back,有时正确的操作是 * 忽略 * 无效的日期!
考虑这个问题:
我想找到某年y的所有日期,这些日期是该月的第5个星期五(因为那是聚会日或其他什么)。下面是一个非常有效的函数,它收集了一年中所有的第5个星期五:

#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';
}


输出示例:

Fifth Friday dates for 2023 are:
2023-03-31
2023-06-30
2023-09-29
2023-12-29


Demo.
事实证明,这是一个不变量,每年将有4个月或5个月,其中将有5个星期五。所以我们可以有效地返回结果作为一对<array<year_month_day,5>,unsigned>,其中第二个成员总是4或5。
第一个任务就是用一堆year_month_day初始化数组。我随意选择了0y/0/0作为一个好的初始化值。我喜欢这个值的什么?我喜欢的一点是它是!ok()!如果我在.second == 4时意外访问了.first[4],一个额外的安全性是,结果year_month_day!ok()。因此,能够在没有Assert或异常的情况下构造这些!ok()值非常重要只是因为这个原因(比如nan)。成本?没有。这些是编译时常量。
接下来我遍历y年的每个月。首先要做的是构造1月的第5个星期五。然后将循环递增1个月,直到年份改变。
现在,因为不是每个月都有第5个星期五,所以这可能不会导致有效的日期。但是在这个函数中,构造无效日期的正确响应不是Assert,也不是异常,也不是快速返回或滚动。正确的响应是忽略日期并返回到下一个月。如果它是有效日期,那么它会推到结果。
在这个程序的执行过程中,计算出了许多无效的日期。它们中没有一个代表错误。甚至在计算中使用了无效的日期(增加一个月)。但是因为算法是规则的,所以一切都正常。
所以总而言之,最好还是让聪明的程序员来处理无效日期的行为,因为聪明的程序员可以为他们的问题创建各种巧妙的解决方案,只要有足够的灵活性。

相关问题