什么时候可以用 C++20 计时日期的 !ok() ?

When is it ok to be !ok() with C++20 chrono dates?

提问人:Howard Hinnant 提问时间:11/14/2023 最后编辑:Howard Hinnant 更新时间:11/17/2023 访问量:2848

问:

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

#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

演示。

我知道 10 月 31 日是有效日期,而 11 月 31 日不是。但是,为什么 11 月 31 日不是错误(断言或抛出)呢?或者为什么它没有像其他日期库那样回弹到 11 月 30 日,或者滚动到 12 月 1 日?

难道就这样让 11 月 31 日默默存在不容易出错吗?

C ++20 C ++-Chrono

评论

4赞 Thomas Weller 11/15/2023
您可以添加一个月并获得 !ok() 日期有点不一致。但是你根本不能添加天数并得到编译器错误。嗯,它是 C++。你已经习惯了它的所有§$%&设计选择。wandbox.org/permlink/BMr5uWSRzxPwjCrl
2赞 Howard Hinnant 11/15/2023
以下是有关该问题的常见问题解答:github.com/HowardHinnant/date/wiki/FAQ#day_arithmetic。但也许这也是一个很好的 SO Q/A。谢谢你的建议。这是您的工作语法示例:wandbox.org/permlink/Fyxa6J2MEARHg4ZG
0赞 Howard Hinnant 11/17/2023
感谢您提出一个新标题。我更喜欢我原来的标题,所以我把它改回来了。
1赞 bolov 11/17/2023
@user3840170标题完全没问题。请不要破坏这个问题。这是一个文字游戏,它很好地表达了内容,而且它比你的建议做得更好,因为这篇文章不是专门增加月份,而是解决了选择这种处理无效日期机制的基本理念和考虑因素。
0赞 user3840170 11/19/2023
我不是在破坏这个问题,我希望我的评论不会在没有实际解决而不是被驳回的情况下被删除。我坚持我的编辑;片名基本和《记忆与结构先生》一样糟糕;模糊到无用的地步。

答:

39赞 Howard Hinnant 11/14/2023 #1

在某种程度上,这个问题几乎不言自明:

或者为什么它没有像其他日期库那样回弹到 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};

前者在代码中的作用非常明显。但后者值得进一步解释:转换为只是将数据结构转换为数据结构。即使 day 字段溢出,也允许进行这种转换。这就像 和 之间的 C 计时 API 一样。sys_days{year, month, day}{count_of_days}tmtime_t

就像没有无效一样,也没有无效。这只是自 1970-01-01 以来(或之前)的天数。当您将该计数转换回 时,它会产生一个有效的 () 。time_tsys_days{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);

演示。

有时,正确的操作不是标志错误、翻转或回弹。有时正确的做法是忽略无效日期!

考虑这个问题:

我想找到某个年份的所有日期,即每月的第 5 个星期五(因为那是派对日或其他什么)。这是一个非常有效的函数,它收集了一年中所有 5星期五:y

#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

演示。

事实证明,每年都有 4 个月或 5 个月,其中有 5 个星期五,这是一个不变的。因此,我们可以有效地将结果返回为对<数组<year_month_day,5>,无符号>,其中对的第二个成员将始终是 4 或 5。

第一项工作只是用一堆 s 初始化数组。我任意选择了一个良好的初始化值。我喜欢这个值的哪些方面?我喜欢的一件事是它是!如果我不小心访问了 时,额外的安全是结果是 .因此,能够在没有断言或异常的情况下构造这些值非常重要(就像 nan 一样)。成本?无。这些是编译时常量。year_month_day0y/0/0!ok().first[4].second == 4year_month_day!ok()!ok()

接下来,我将迭代一年中的每个月。首先要做的是构建 1 月份的第 5 个星期五。然后将循环增加 1 个月,直到年份发生变化。y

现在,由于不是每个月都有第 5星期五,因此这可能不会产生有效日期。但是在这个函数中,对构造无效日期的正确响应既不是断言也不是异常,也不是回弹或滚动。正确的反应是忽略日期并迭代到下个月。如果是有效日期,则继续推送结果。

在执行此程序期间计算了许多无效日期。它们都不代表错误。甚至在计算中使用了无效日期(增加一个月)。但是因为算术是有规律的,所以一切都有效。

因此,总而言之,最好将无效日期的行为留给聪明的程序员。因为聪明的程序员可以为他们的问题创建各种巧妙的解决方案,只要这样做的灵活性。

评论

3赞 Howard Hinnant 11/14/2023
@Red.波浪。我认为客户创建自己的.我认为提供它不是一个好主意,因为没有明确的正确设计方法。class ok_datestd::chrono
2赞 Howard Hinnant 11/14/2023
我相信我已经在上面的回答中介绍了 C++20 计时日期设计的基本原理和优势。如果你不喜欢它,我仍然很高兴你用它来构建一个你喜欢的约会类。如果你觉得它根本没有用,你仍然可以自由地构建自己的库,或者使用几十年来开发的其他日期库之一。如果您正在构建自己的算法,这里有一些高质量的基本日期算法可以帮助您入门: howardhinnant.github.io/date_algorithms.html
1赞 Thomas Weller 11/15/2023
恕我直言,第一个演示还应该看到,不仅可以加减一个月,而且一旦出现,它也不会保持“不正确”的状态:wandbox.org/permlink/jjes0b0Y9nzflaG9assert(date.ok())
4赞 hobbs 11/15/2023
我曾经调试过一次重大的应用程序中断,这是由于日期时间库对“不存在”的时间大惊小怪,而不需要它。特别是对于 stdlib 功能,让调用者决定他们想要什么样的处理绝对是有意义的。
1赞 Caleth 11/16/2023
虽然不是数字(),但它不是更重要的不是日期()吗?0y/0/0nannad