第11章 中的日期和时区
# 第11章 <chrono>中的日期和时区
C++11引入了<chrono>
库,对持续时间(durations)和时间点(timepoints)提供了基本支持。这使你能够指定和处理不同单位的持续时间以及不同时钟的时间点。然而,它尚未支持像日期(日、月、年)、星期几这类更高级的持续时间和时间点,也不支持处理不同的时区。
C++20对现有的<chrono>
库进行了扩展,增加了对日期、时区以及其他一些特性的支持。本章将介绍这些扩展内容。
# 11.1 通过示例进行概述
在深入探讨细节之前,让我们先看一些具有启发性的示例。
# 11.1.1 每月5号安排会议
假设有一个程序,我们想要遍历一年中的所有月份,并在每个月的5号安排一次会议。该程序可以这样编写:
//lib/chrono1.cpp
#include <chrono>
#include <iostream>
int main()
{
namespace chr = std::chrono; // std::chrono的快捷方式
using namespace std::literals; // 用于h、min、y后缀
// 对于2021年每个月的5号:
chr::year_month_day first = 2021y / 1 / 5;
for (auto d = first; d.year() == first.year(); d += chr::months{1}) {
// 输出日期:
std::cout << d << " :\n ";
// 初始化并输出这些日子的协调世界时(UTC)18:30:
auto tp{chr::sys_days{d} + 18h + 30min};
std::cout << std::format( " We meet on {:%A %D at %R}\n ", tp);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
该程序的输出如下:
2021-01-05:
We meet on Tuesday 01/05/21 at 18:30
2021-02-05:
We meet on Friday 02/05/21 at 18:30
2021-03-05:
We meet on Friday 03/05/21 at 18:30
2021-04-05:
We meet on Monday 04/05/21 at 18:30
2021-05-05:
We meet on Wednesday 05/05/21 at 18:30
...
2021-11-05:
We meet on Friday 11/05/21 at 18:30
2021-12-05:
We meet on Sunday 12/05/21 at 18:30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
让我们逐步分析这个程序。
# 命名空间声明
我们从命名空间和using
声明开始,使<chrono>
库的使用更加便捷:
- 首先,我们引入
chr::
作为<chrono>
库标准命名空间std::chrono
的快捷方式:
namespace chr = std::chrono; // std::chrono的快捷方式
为了使示例更易读,有时我会使用chr::
而不是std::chrono::
。
- 然后,我们确保可以使用诸如
s
、min
、h
和y
(y
是C++20新增的)这样的字面量后缀:
using namespace std::literals; // 用于min、h、y后缀
为了避免任何限定,你也可以使用using
声明:
using namespace std::chrono; // 省略chrono命名空间限定
通常,你应该限制这种using
声明的作用域,以避免不必要的副作用。
# 日历类型year_month_day
数据流从初始化std::chrono::year_month_day
类型的起始日期first
开始:
chr::year_month_day first = 2021y / 1 / 5;
year_month_day
类型是一种日历类型,它为日期的三个字段都提供了属性,使得处理特定日期的年、月、日变得很容易。
因为我们要遍历每个月的5号,所以我们用2021年1月5日初始化该对象,使用/
运算符将年、月、日的值组合起来,具体如下:
- 首先,我们创建一个
std::chrono::year
类型的对象来表示年份。这里,我们使用新的标准字面量y
:
2021y
为了使用这个字面量,我们必须提供以下using
声明之一:
using std::literals; // 启用所有标准字面量
using std::chrono::literals; // 启用所有标准chrono字面量
using namespace std::chrono; // 启用所有标准chrono字面量
using namespace std; // 启用所有标准字面量
2
3
4
如果没有这些字面量,我们可能需要这样写:
std::chrono::year{2021}
- 然后,我们使用
/
运算符将std::chrono::year
与一个整数值组合,创建一个std::chrono::year_month
类型的对象。由于第一个操作数是年份,显然第二个操作数必须是月份,这里不能指定日期。 - 最后,我们再次使用
/
运算符将std::chrono::year_month
对象与一个整数值组合,创建一个std::chrono::year_month_day
对象。
这种对日历类型的初始化是类型安全的,并且只需要指定第一个操作数的类型。
因为运算符已经产生了正确的类型,我们也可以用auto
声明first
。如果没有命名空间声明,我们必须这样写:
auto first = std::chrono::year{2021} / 1 / 5;
有了chrono
字面量后,我们可以简单地写成:
auto first = 2021y/1/5;
# 初始化日历类型的其他方式
还有其他方式可以初始化像year_month_day
这样的日历类型:
auto d1 = std::chrono::years{2021}/1/5; // 2021年1月5日
auto d2 = std::chrono::month{1}/5/2021; // 2021年1月5日
auto d3 = std::chrono::day{5}/1/2021; // 2021年1月5日
2
3
也就是说,/
运算符的第一个参数的类型决定了如何解释其他参数。有了chrono
字面量后,我们可以简单地写成:
using namespace std::literals;
auto d4 = 2021y/1/5; // 2021年1月5日
auto d5 = 5/1/2021; // 2021年1月5日
2
3
没有表示月份的标准后缀,但我们有预定义的标准对象:
auto d6 = std::chrono::January / 5 / 2021; // 2021年1月5日
有了相应的using
声明后,我们甚至可以这样写:
using namespace std::chrono;
auto d6 = January/5/2021; // 2021年1月5日
2
在所有这些情况下,我们都初始化了一个std::chrono::year_month_day
类型的对象。
# 新的持续时间类型
在我们的示例中,然后调用一个循环来遍历一年中的所有月份:
for (auto d = first; d.year() == first.year(); d += chr::months{1}) {
...
}
2
3
std::chrono::months
类型是一种新的持续时间类型,表示一个月。你可以将它用于所有日历类型,来处理日期中的特定月份,就像我们在这里所做的,给日期加上一个月:
std::chrono::year_month_day d = ... ;
d += chr::months{1}; // 加一个月
2
不过要注意,当我们将它用于普通的持续时间和时间点时,months
类型表示一个月的平均持续时间,即30.436875天。因此,在处理普通时间点时,使用months
和years
时要谨慎。
# 所有chrono类型的输出运算符
在循环内部,我们打印当前日期:
std::cout << d << "\n";
从C++20开始,几乎为所有可能的chrono
类型都定义了输出运算符。
std::chrono::year_month_day d = ... ;
std::cout << "d : " << d << "\n";
2
这使得打印任何chrono
值都变得很容易。然而,输出并不总是符合特定需求。对于year_month_day
类型,输出格式是程序员期望的格式:年-月-日。例如:
2021-01-05
其他默认输出格式通常使用斜杠作为分隔符,相关内容在此处有文档说明。
对于用户自定义输出,<chrono>
库也支持新的格式化输出库。我们稍后会用它来输出时间点tp
:
std::cout << std::format( "We meet on {:%D at %R}\n ", tp);
格式化说明符,如%A
表示星期几,%D
表示日期,%R
表示时间(小时和分钟),与C函数strftime()
和POSIX的date
命令的说明符相对应,因此输出可能如下所示:
We meet on 10/05/21 at 18:30
<chrono>
库也支持特定区域设置的输出。chrono
类型格式化输出的详细内容将在后面详细介绍。
# 组合日期和时间
为了初始化tp
,我们将循环中的日期与一天中的特定时间组合起来:
auto tp{sys_days{d} + 18h + 30min};
为了组合日期和时间,我们必须使用时间点和持续时间。像std::chrono::year_month_day
这样的日历类型不是时间点。因此,我们首先将year_month_day
值转换为time_point<>
对象:
std::chrono::year_month_day d = ... ;
std::chrono::sys_days{d} // 转换为time_point
2
std::chrono::sys_days
类型是一个新的快捷方式,表示以天为粒度的系统时间点。它等同于:std::chrono::time_point<std::chrono::system_clock, std::chrono::days>
。通过加上一些持续时间(18小时和30分钟),我们计算出一个新值,就像<chrono>
库中通常的做法一样,这个新值的类型具有适合计算结果的粒度。因为我们将天数与小时数和分钟数组合起来,所以结果类型是以分钟为粒度的系统时间点。不过,我们不必知道具体类型,使用auto
就可以了。为了更明确地指定tp
的类型,我们也可以这样声明:
- 作为未指定粒度的系统时间点:
chr::sys_time tp{chr::sys_days{d} + 18h + 30min};
多亏了类模板参数推导,粒度的模板参数会被推导出来。
- 作为指定粒度的系统时间点:
chr::sys_time<chr::minutes> tp{chr::sys_days{d} + 18h + 30min};
在这种情况下,初始值的粒度不能比指定类型更细,否则你必须使用舍入函数。
- 作为以秒为粒度的便捷系统时间点类型:
chr::sys_seconds tp{chr::sys_days{d} + 18h + 30min};
同样,初始值的粒度不能比指定类型更细,否则你必须使用舍入函数。
在所有这些情况下,默认输出运算符会按照上述指定格式打印时间点。例如:
We meet on 10/05/21 at 18:30
注意,在处理系统时间时,默认输出是协调世界时(UTC)。
粒度更细的时间点也会使用必要的格式输出精确值(如毫秒)。详见后文。
# 11.1.2 每月最后一天安排会议
让我们通过迭代每个月的最后一天来修改第一个示例程序。具体做法如下:
// lib/chrono2.cpp
#include <chrono>
#include <iostream>
int main() {
// std::chrono的别名
namespace chr = std::chrono;
using namespace std::literals;
// 针对2021年每个月的最后一天:
auto first = 2021y / 1 / chr::last;
for (auto d = first; d.year() == first.year(); d += chr::months{1}) {
// 输出日期:
std::cout << d << " :\n " ;
// 初始化并输出这些日子的协调世界时18:30:
auto tp{chr::sys_days{d} + 18h + 30min};
std::cout << std::format( " We meet on {:%A %D at %R}\n " , tp);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们所做的修改仅仅是对first
的初始化。我们使用auto
类型声明它,并将其初始化为std::chrono::last
表示的日期:
auto first = 2021y / 1 / chr::last;
std::chrono::last
类型不仅代表一个月的最后一天,它还会使first
具有不同的类型:std::chrono::year_month_day_last
。这样做的效果是,当增加一个月时,日期中的日会自动调整。实际上,这个类型始终表示一个月的最后一天。在我们输出日期并指定相应的输出格式时,才会将其转换为数字表示的日。
结果,输出变为如下内容:
2021/Jan/last:
We meet on Sunday 01/31/21 at 18:30
2021/Feb/last:
We meet on Sunday 02/28/21 at 18:30
2021/Mar/last:
We meet on Wednesday 03/31/21 at 18:30
2021/Apr/last:
We meet on Friday 04/30/21 at 18:30
2021/May/last:
We meet on Monday 05/31/21 at 18:30
...
2021/Nov/last:
We meet on Tuesday 11/30/21 at 18:30
2021/Dec/last:
We meet on Friday 12/31/21 at 18:30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如你所见,year_month_day_last
的默认输出格式使用last
和斜杠而不是连字符作为分隔符(只有year_month_day
在其默认输出格式中使用连字符)。例如:2021/Jan/last
。
你仍然可以将first
声明为std::chrono::year_month_day
类型:
// 针对2021年每个月的最后一天:
std::chrono::year_month_day first = 2021y / 1 / chr::last;
for (auto d = first; d.year() == first.year(); d += chr::months{1}) {
// 输出日期:
std::cout << d << " :\n " ;
// 初始化并输出这些日子的协调世界时18:30:
auto tp{chr::sys_days{d} + 18h + 30min};
std::cout << std::format( " We meet on {:%D at %R}\n " , tp);
}
2
3
4
5
6
7
8
9
然而,这将导致如下输出:
2021-01-31:
We meet on Sunday 01/31/21 at 18:30
2021-02-31 is not a valid date:
We meet on Wednesday 03/03/21 at 18:30
2021-03-31:
We meet on Wednesday 03/31/21 at 18:30
2021-04-31 is not a valid date:
We meet on Saturday 05/01/21 at 18:30
2021-05-31:
We meet on Monday 05/31/21 at 18:30
...
2
3
4
5
6
7
8
9
10
11
因为first
的类型存储的是日的数值,初始化为1月的最后一天,所以我们会迭代每个月的31日。如果不存在这样的日期,默认输出格式会打印这是一个无效日期,而std::format()
甚至会进行错误的计算。
处理这种情况的一种方法是检查日期是否有效,并确定相应的处理方式。例如:
// lib/chrono3.cpp
#include <chrono>
#include <iostream>
int main() {
// std::chrono的别名
namespace chr = std::chrono;
using namespace std::literals;
// 针对2021年每个月的最后一天:
chr::year_month_day first = 2021y / 1 / 31;
for (auto d = first; d.year() == first.year(); d += chr::months{1}) {
// 输出日期:
if (d.ok()) {
std::cout << d << " :\n " ;
}
else {
// 对于没有31日的月份,使用下一个月的1日:
auto d1 = d.year() / d.month() / 1 + chr::months{1};
std::cout << d << " :\n " ;
}
// 初始化并输出这些日子的协调世界时18:30:
auto tp{chr::sys_days{d} + 18h + 30min};
std::cout << std::format( " We meet on {:%A %D at %R}\n " , tp);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
通过使用成员函数ok()
,我们将无效日期调整为下一个月的第一天。我们得到如下输出:
2021-01-31:
We meet on Sunday 01/31/21 at 18:30
2021-02-31 is not a valid date:
We meet on Wednesday 03/03/21 at 18:30
2021-03-31:
We meet on Wednesday 03/31/21 at 18:30
2021-04-31 is not a valid date:
We meet on Saturday 05/01/21 at 18:30
2021-05-31:
We meet on Monday 05/31/21 at 18:30
...
2021-11-31 is not a valid date:
We meet on Wednesday 12/01/21 at 18:30
2021-12-31:
We meet on Friday 12/31/21 at 18:30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 11.1.3 每月第一个星期一安排会议
类似地,我们可以在每月的第一个星期一安排会议:
// lib/chrono4.cpp
#include <chrono>
#include <iostream>
int main() {
// std::chrono的别名
namespace chr = std::chrono;
using namespace std::literals;
// 针对2021年每个月的第一个星期一:
auto first = 2021y / 1 / chr::Monday[1];
for (auto d = first; d.year() == first.year(); d += chr::months{1}) {
// 输出日期:
std::cout << d << "\n";
// 初始化并输出这些日子的协调世界时18:30:
auto tp{chr::sys_days{d} + 18h + 30min};
std::cout << std::format( " We meet on {:%A %D at %R}\n " , tp);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
同样,first
具有特殊类型std::chrono::year_month_weekday
,它表示一年中某个月的某个星期几。默认的输出格式会以特定的格式显示这一点。不过,格式化后的输出效果良好:
2021/Jan/Mon[1]
We meet on Monday 01/04/21 at 18:30
2021/Feb/Mon[1]
We meet on Monday 02/01/21 at 18:30
2021/Mar/Mon[1]
We meet on Monday 03/01/21 at 18:30
2021/Apr/Mon[1]
We meet on Monday 04/05/21 at 18:30
2021/May/Mon[1]
We meet on Monday 05/03/21 at 18:30
...
2021/Nov/Mon[1]
We meet on Monday 11/01/21 at 18:30
2021/Dec/Mon[1]
We meet on Monday 12/06/21 at 18:30
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 表示星期几的日历类型
这一次,我们将first
初始化为std::chrono::year_month_weekday
日历类型的对象,并将其初始化为2021年1月的第一个星期一:
auto first = 2021y / 1 / chr::Monday[1];
同样,我们使用operator/
来组合不同的日期字段。不过,这次涉及到了表示星期几的类型:
- 首先,我们调用
2021y / 1
,将std::chrono::year
与一个整数值组合,创建一个std::chrono::year_month
。 - 然后,我们对
std::chrono::Monday
(它是std::chrono::weekday
类型的标准对象)调用operator[]
,创建一个std::chrono::weekday_indexed
类型的对象,表示第n
个星期几。 - 最后,使用
operator/
将std::chrono::year_month
与std::chrono::weekday_indexed
类型的对象组合,创建一个std::chrono::year_month_weekday
对象。因此,完整指定的声明如下:
std::chrono::year_month_weekday first = 2021y / 1 / std::chrono::Monday[1];
同样需要注意,year_month_weekday
的默认输出格式使用斜杠而不是连字符作为分隔符(只有year_month_day
在其默认输出格式中使用连字符)。例如:2021/Jan/Mon[1]
。
# 11.1.4 使用不同的时区
让我们修改第一个示例程序,引入时区的概念。实际上,我们希望程序迭代一年中每个月的第一个星期一,并在不同的时区安排会议。
为此,我们需要进行以下修改:
- 迭代当年的所有月份。
- 在当地时间18:30安排会议。
- 使用其他时区打印会议时间。
现在程序可以这样编写:
// lib/chronotz.cpp
#include <chrono>
#include <iostream>
int main() {
// std::chrono的别名
namespace chr = std::chrono;
using namespace std::literals;
try {
// 将今天初始化为当前本地日期:
auto localNow = chr::current_zone()->to_local(chr::system_clock::now());
chr::year_month_day today{chr::floor<chr::days>(localNow)};
std::cout << "today : " << today << "\n";
// 针对当年每个月的第一个星期一:
auto first = today.year() / 1 / chr::Monday[1];
for (auto d = first; d.year() == first.year(); d += chr::months{1}) {
// 输出日期:
std::cout << d << "\n";
// 初始化并输出这些日子的当地时间18:30:
auto tp{chr::local_days{d} + 18h + 30min};
std::cout << " tp : " << tp << "\n";
// 将这个当地时间应用到当前时区:
chr::zoned_time timeLocal{chr::current_zone(), tp};
std::cout << " local : " << timeLocal << "\n";
// 用其他时区输出日期:
chr::zoned_time timeUkraine{"Europe/Kiev " , timeLocal};
chr::zoned_time timeUSWest{"America/Los_Angeles " , timeLocal};
std::cout << " Ukraine : " << timeUkraine << "\n";
std::cout << " Pacific : " << timeUSWest << "\n";
}
}
catch (const std::exception& e) {
std::cerr << "EXCEPTION : " << e.what() << "\n";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
2021年在欧洲运行该程序会得到如下输出:
today: 2021-03-29
2021/Jan/Mon[1]
tp: 2021-01-04 18:30:00
local: 2021-01-04 18:30:00 CET
Ukraine: 2021-01-04 19:30:00 EET
Pacific: 2021-01-04 09:30:00 PST
2021/Feb/Mon[1]
tp: 2021-02-01 18:30:00
local: 2021-02-01 18:30:00 CET
Ukraine: 2021-02-01 19:30:00 EET
Pacific: 2021-02-01 09:30:00 PST
2021/Mar/Mon[1]
tp: 2021-03-01 18:30:00
local: 2021-03-01 18:30:00 CET
Ukraine: 2021-03-01 19:30:00 EET
Pacific: 2021-03-01 09:30:00 PST
2021/Apr/Mon[1]
tp: 2021-04-05 18:30:00
local: 2021-04-05 18:30:00 CEST
Ukraine: 2021-04-05 19:30:00 EEST
Pacific: 2021-04-05 09:30:00 PDT
2021/May/Mon[1]
tp: 2021-05-03 18:30:00
local: 2021-05-03 18:30:00 CEST
Ukraine: 2021-05-03 19:30:00 EEST
Pacific: 2021-05-03 09:30:00 PDT
...
2021/Oct/Mon[1]
tp: 2021-10-04 18:30:00
local: 2021-10-04 18:30:00 CEST
Ukraine: 2021-10-04 19:30:00 EEST
Pacific: 2021-10-04 09:30:00 PDT
2021/Nov/Mon[1]
tp: 2021-11-01 18:30:00
local: 2021-11-01 18:30:00 CET
Ukraine: 2021-11-01 19:30:00 EET
Pacific: 2021-11-01 10:30:00 PDT
2021/Dec/Mon[1]
tp: 2021-12-06 18:30:00
local: 2021-12-06 18:30:00 CET
Ukraine: 2021-12-06 19:30:00 EET
Pacific: 2021-12-06 09:30:00 PST
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
看看10月和11月的输出:在洛杉矶,尽管使用的是相同的太平洋夏令时(PDT),但会议安排在了不同的时间。这是因为会议时间的来源(中欧)从夏令时切换到了冬令时/标准时间。
下面,让我们逐步梳理这个程序的修改内容。
# 处理当前日期
第一条新语句是将today
初始化为year_month_day
类型的对象:
auto localNow = chr::current_zone()->to_local(chr::system_clock::now());
chr::year_month_day today = chr::floor<chr::days>(localNow);
2
从C++11开始就已经提供了对std::chrono::system_clock::now()
的支持,它会返回一个std::chrono::time_point<>
,其粒度取决于系统时钟。这个系统时钟使用协调世界时(UTC)(自C++20起,系统时钟保证使用基于UTC的Unix时间)。因此,我们首先必须将当前的UTC时间和日期调整为当前/本地时区的时间和日期,current_zone()->to_local()
就实现了这一点。否则,我们的本地日期可能与UTC日期不匹配(因为我们所在地区可能已经过了午夜,但UTC时间还未到,或者反之)。
直接使用localNow
初始化year_month_day
值是无法编译的,因为这样会“缩小”该值(丢失其小时、分钟、秒和亚秒部分)。通过使用像floor()
这样的便捷函数(自C++17起可用),我们可以根据所需的粒度向下取整该值。
如果你需要根据UTC获取当前日期,以下代码就足够了:
chr::year_month_day today = chr::floor<chr::days>(chr::system_clock::now());
# 本地日期和时间
同样,我们将迭代的日期与一天中的特定时间相结合。不过,这次我们首先将每一天转换为std::chrono::local_days
类型:
auto tp{chr::local_days{d} + 18h + 30min};
std::chrono::local_days
类型是time_point<local_t, days>
的缩写。这里使用了伪时钟std::chrono::local_t
,这意味着我们有一个本地时间点,即一个尚未关联时区(甚至不是协调世界时,UTC)的时间点。
下一条语句将本地时间点与当前时区相结合,这样我们就得到了一个std::chrono::zoned_time<>
类型的特定时区的时间点:
chr::zoned_time timeLocal{chr::current_zone(), tp}; // 本地时间
请注意,时间点已经与系统时钟相关联,这意味着它已经将时间与UTC相关联。将这样一个时间点与不同的时区相结合,会将时间转换为特定时区的时间。
默认的输出运算符展示了时间点和带时区时间之间的区别:
auto tpLocal{chr::local_days{d} + 18h + 30min}; // 本地时间点
std::cout << "timepoint : " << tpLocal << '\n';
chr::zoned_time timeLocal{chr::current_zone(), tpLocal}; // 应用于本地时区
std::cout << "zonedtime : " << timeLocal << '\n';
2
3
4
这段代码的输出示例如下:
timepoint: 2021-01-04 18:30:00
zonedtime: 2021-01-04 18:30:00 CET
2
可以看到,时间点输出时没有时区,而带时区时间则有时区。不过,由于我们将本地时间点应用于本地时区,所以两者输出的时间是相同的。
实际上,时间点和带时区时间的区别如下:
- 时间点可能与一个定义好的纪元相关联。它可能定义了一个唯一的时间点;然而,它也可能与一个未定义的或伪纪元相关联,在与一个时区相结合之前,其含义并不明确。
- 带时区时间总是与一个时区相关联,因此纪元最终有了明确的含义。它总是代表一个唯一的时间点。
看看当我们使用std::chrono::sys_days
而不是std::chrono::local_days
时会发生什么:
auto tpSys{chr::sys_days{d} + 18h + 30min}; // 系统时间点
std::cout << "timepoint : " << tpSys << '\n';
chr::zoned_time timeSys{chr::current_zone(), tpSys}; // 转换为本地时区
std::cout << "zonedtime : " << timeSys << '\n';
2
3
4
这里我们使用了一个系统时间点,它关联了UTC时区。当本地时间点应用于一个时区时,系统时间点则是转换到一个时区。因此,当我们在与UTC有一小时时差的时区运行这个程序时,会得到以下输出:
timepoint: 2021-01-04 18:30:00
zonedtime: 2021-01-04 19:30:00 CET
2
# 使用其他时区
最后,我们使用不同的时区来打印时间点,一个是乌克兰(俄罗斯发动战争的国家)基辅所在的时区,另一个是北美太平洋时区洛杉矶所在的时区:
chr::zoned_time timeUkraine{"Europe/Kiev", timeLocal}; // 乌克兰时间
chr::zoned_time timeUSWest{"America/Los_Angeles", timeLocal};// 太平洋时间
std::cout << " Ukraine: " << timeUkraine << '\n';
std::cout << " Pacific: " << timeUSWest << '\n';
2
3
4
为了指定一个时区,我们必须使用国际标准时间数据库(IANA timezone database)中的官方时区名称,这些名称通常基于代表该时区的城市。像PST这样的时区缩写可能会随年份变化,或者适用于不同的时区。
使用这些对象的默认输出运算符会添加相应的时区缩写,无论是否处于“冬季时间”:
local: 2021-01-04 18:30:00 CET
Ukraine: 2021-01-04 19:30:00 EET
Pacific: 2021-01-04 09:30:00 PST
2
3
或者是否处于“夏季时间”:
local: 2021-07-05 18:30:00 CEST
Ukraine: 2021-07-05 19:30:00 EEST
Pacific: 2021-07-05 09:30:00 PDT
2
3
有时,一些时区处于夏令时,而另一些则不是。例如,在11月初,美国实行夏令时,而乌克兰则不实行:
local: 2021-11-01 18:30:00 CET
Ukraine: 2021-11-01 19:30:00 EET
Pacific: 2021-11-01 10:30:00 PDT
2
3
# 当时区不被支持时
C++可以在小型系统上使用,甚至在烤面包机上也能发挥作用。在这种情况下,使用IANA时区数据库会消耗过多资源。因此,并不要求必须存在时区数据库。
如果不支持时区数据库,所有与时区相关的调用都会抛出异常。因此,在可移植的C++程序中使用时区时,你应该捕获异常:
try {
// 将今天初始化为当前本地日期:
auto localNow = chr::current_zone()->to_local(chr::system_clock::now());
// ...
}
catch (const std::exception& e) {
std::cerr << "EXCEPTION : " << e.what() << '\n'; // IANA时区数据库缺失
}
2
3
4
5
6
7
8
请注意,对于Visual C++,这也适用于较旧的Windows系统,因为在Windows 10之前的系统中没有所需的操作系统支持。
此外,一些平台可能支持时区API,但并不使用整个IANA时区数据库。例如,在德国的Windows 11系统上,我得到的输出是GMT - 8和GMT - 7,而不是PST和PDT。
# 11.2 基本的chrono概念和术语
chrono库的设计旨在处理不同系统上的定时器和时钟可能不同,以及随着时间推移精度不断提高的情况。为了避免大约每10年就需要引入一种新的时间类型,C++11确立的基本目标是通过将持续时间和时间点分开,提供一种与精度无关的概念。C++20通过增加对日期、时区和其他一些扩展的支持,扩展了这些基本概念。
因此,chrono库的核心由以下类型或概念组成:
- 时间间隔(duration)被定义为在一个时间单位上的特定滴答数。例如,“3分钟”(“分钟”这个时间单位的3次滴答)就是一个时间间隔。其他例子还有“42毫秒”或“86,400秒”,后者表示1天的时长。这种方法还允许指定像“1.5倍三分之一秒”这样的时间间隔,其中1.5是滴答数,“三分之一秒”是所使用的时间单位。
- 时间点(timepoint)被定义为时间间隔和时间起始点(即所谓的纪元,epoch)的组合。 一个典型的例子是系统时间点,它表示2000年12月31日午夜。由于系统纪元被指定为Unix/POSIX诞生的时间,这个时间点将被定义为“自1970年1月1日起946,684,800秒”(如果考虑闰秒,有些时间点会将其计算在内,这个时间点则是946,684,822秒 )。 请注意,纪元可能是未指定的,或者是一个伪纪元。例如,本地时间点与我们拥有的任何本地时间相关联。它仍然需要一个纪元值,但在将这个时间点应用到特定时区之前,它所代表的具体时间并不明确。12月31日午夜就是一个很好的例子:世界上不同地区根据所在时区的不同,庆祝活动和烟花表演开始的时间也不同。
- 时钟(clock)是定义时间点纪元的对象。因此,不同的时钟有不同的纪元。每个时间点都由一个时钟作为参数。
C++11引入了两个基本时钟(
system_clock
用于处理系统时间,steady_clock
用于测量和定时器,不受系统时钟变化的影响)。C++20添加了新的时钟,用于处理UTC时间点(支持闰秒)、GPS时间点、国际原子时(TAI,international atomic time)时间点以及文件系统的时间点。 为了处理本地时间点,C++20还添加了一个伪时钟local_t
,它不与特定的纪元/起始点绑定。 处理多个时间点的操作,如计算两个时间点之间的时间间隔/差值,通常需要使用相同的纪元/时钟。不过,不同时钟之间的转换是可能的。 一个时钟(如果不是本地时钟)提供了一个便捷函数now()
,用于返回当前时间点。 - 日历类型(calendrical type,C++20引入)允许我们使用常见的日、月、年术语来处理日历的属性。这些类型可以用来表示日期的单个属性(日、月、年和星期几)以及它们的组合(如
year_month
或year_month_day
表示完整日期)。 不同的符号,如“Wednesday”(星期三)、“November”(十一月)和“last”(上一个、最后一个),允许我们使用常见术语表示部分日期和完整日期,例如“November的最后一个Wednesday”(十一月的最后一个星期三)。 完全指定的日历日期(包含年、月、日或月份中特定的星期几)可以使用系统时钟或本地时间的伪时钟转换为时间点,或者从时间点转换而来。 - 时区(timezone,C++20引入)用于处理不同时区导致同一事件在不同地区时间不同的情况。如果我们在协调世界时18:30进行线上会议,在亚洲,会议的本地时间会晚很多,而在美国则会早很多。 因此,时区通过将时间点应用或转换为不同的本地时间,赋予了时间点(不同的)意义。
- 带时区时间(zoned time,C++20引入)是时间点和时区的组合。它可以用于将本地时间点应用到特定时区(可能是“当前”时区),或者将时间点转换到不同的时区。 带时区时间可以看作是一个日期和时间对象,它是纪元(时间点的起始点)、时间间隔(与起始点的距离)和时区(用于调整最终时间)的组合。
对于所有这些概念和类型,C++20增加了对输出(甚至格式化输出)和解析的支持。这样,你可以决定是否按以下格式打印日期/时间值:
Nov/24/2011
24.11.2011
2011-11-24 16:30:00 UTC
Thursday, November 11, 2011
2
3
4
# 11.3 C++20中的基本chrono扩展
在详细讨论如何使用chrono库之前,让我们先介绍所有的基本类型和符号。
# 11.3.1 持续时间类型
C++20引入了用于表示日、周、月和年的额外持续时间类型。下表列出了C++目前提供的所有持续时间类型,以及用于创建值的标准字面量后缀和打印值时使用的默认输出后缀。
使用std::chrono::months
和std::chrono::years
时要小心。months
和years
表示一个月或一年的平均时长,这是一个小数天。平均一年的时长是考虑闰年计算得出的:
- 每4年有一个额外的日子(366天而不是365天)。
- 然而,每100年没有这个额外的日子。
- 但是,每400年又会有一个额外的日子。
因此,平均一年的时长值为(400 * 365 + 100 - 4 + 1) / 400 。平均一个月的时长是这个值的十二分之一。
类型 | 定义 | 字面量 | 输出后缀 |
---|---|---|---|
nanoseconds | 1/1,000,000,000秒 | ns | ns |
microseconds | 1,000纳秒 | us | μs 或us |
milliseconds | 1,000微秒 | ms | ms |
seconds | 1,000毫秒 | s | s |
minutes | 60秒 | min | min |
hours | 60分钟 | h | h |
days | 24小时 | d | d |
weeks | 7天 | [604800]s | |
months | 30.436875天 | [2629746]s | |
years | 365.2425天 | y | [31556952]s |
表11.1 C++20起的标准持续时间类型
# 11.3.2 时钟
C++20引入了一些新的时钟(请记住,时钟定义了时间点的起始/纪元)。
表11.2“自C++20起的标准时钟类型”描述了C++标准库目前提供的所有时钟的名称和含义。
类型 | 含义 | 纪元 |
---|---|---|
system_clock | 与系统时钟相关(自C++11起) | 协调世界时(UTC)时间 |
utc_clock | 用于协调世界时(UTC)时间值的时钟 | 协调世界时(UTC)时间 |
gps_clock | 用于全球定位系统(GPS)时间值的时钟 | 全球定位系统(GPS)时间 |
tai_clock | 用于国际原子时(TAI)时间值的时钟 | 国际原子时(TAI)时间 |
file_clock | 用于文件系统库时间点的时钟 | 实现相关 |
local_t | 用于本地时间点的伪时钟 | 未定义 |
steady_clock | 用于测量的时钟(自C++11起) | 实现相关 |
high_resolution_clock | (见正文) |
表11.2 自C++20起的标准时钟类型
“纪元”列指定时钟是否指定了一个唯一的时间点,以便始终有一个特定的协调世界时(UTC)时间定义。这需要一个稳定的指定纪元。对于file_clock
,纪元是系统特定的,但在程序的多次运行中是稳定的。steady_clock
的纪元可能会在应用程序的一次运行到下一次运行之间发生变化(例如,系统重启时)。对于伪时钟local_t
,纪元被解释为“本地时间”,这意味着必须将其与一个时区结合起来,才能知道它代表的是哪个时间点。
自C++11起,C++标准还提供了high_resolution_clock
。然而,在将代码从一个平台移植到另一个平台时,使用它可能会引入一些微妙的问题。实际上,high_resolution_clock
是system_clock
或steady_clock
的别名,这意味着该时钟有时是稳定的,有时不是,并且这个时钟可能支持也可能不支持转换为其他时钟或time_t
。
因为在任何平台上,high_resolution_clock
的精度都不会比steady_clock
更高,所以你应该使用steady_clock
代替它。
我们将在后面讨论时钟的以下细节:
- 时钟之间的详细差异。
- 时钟之间的转换。
- 时钟如何处理闰秒。
# 11.3.3 时间点类型
C++20引入了一些新的时间点类型。基于自C++11起就有的通用定义:
template<typename Clock, typename Duration = typename Clock::duration>
class time_point;
2
这些新类型为在不同时钟下使用时间点提供了更便捷的方式。
表11.3“自C++20起的标准时间点类型”描述了这些便捷时间点类型的名称和含义。
类型 | 含义 | 定义为 |
---|---|---|
local_time<Dur> | 本地时间点 | time_point<LocalTime, Dur> |
local_seconds | 以秒为单位的本地时间点 | time_point<LocalTime, seconds> |
local_days | 以天为单位的本地时间点 | time_point<LocalTime, days> |
sys_time<Dur> | 系统时间点 | time_point<system_clock, Dur> |
sys_seconds | 以秒为单位的系统时间点 | time_point<system_clock, seconds> |
sys_days | 以天为单位的系统时间点 | time_point<system_clock, days> |
utc_time<Dur> | 协调世界时(UTC)时间点 | time_point<utc_clock, Dur> |
utc_seconds | 以秒为单位的协调世界时(UTC)时间点 | time_point<utc_clock, seconds> |
tai_time<Dur> | 国际原子时(TAI)时间点 | time_point<tai_clock, Dur> |
tai_seconds | 以秒为单位的国际原子时(TAI)时间点 | time_point<tai_clock, seconds> |
gps_time<Dur> | 全球定位系统(GPS)时间点 | time_point<gps_clock, Dur> |
gps_seconds | 以秒为单位的全球定位系统(GPS)时间点 | time_point<gps_clock, seconds> |
file_time<Dur> | 文件系统时间点 | time_point<file_clock, Dur> |
表11.3 自C++20起的标准时间点类型
对于每个标准时钟(除了steady_clock
),我们都有一个相应的_time<>
类型,这使我们能够声明表示该时钟的日期或时间点的对象。此外,..._seconds
类型允许我们定义具有秒级粒度的相应类型的日期/时间对象。对于系统时间和本地时间,..._days
类型允许我们定义具有天级粒度的相应类型的日期/时间对象。例如:
std::chrono::sys_days x; // time_point<system_clock, days>
std::chrono::local_seconds y; // time_point<local_t, seconds>
std::chrono::file_time<std::chrono::seconds> z; // time_point<file_clock, seconds>
2
3
请注意,这种类型的对象仍然表示一个时间点,尽管其类型名称不幸采用了复数形式。例如,sys_days
表示定义为系统时间点的某一天(该名称源于“具有天级粒度的系统时间点”)。
# 11.3.4 日历类型
作为时间点类型的扩展,C++20在chrono库中引入了用于民用(格里高利)日历的类型。
时间点是从纪元开始的持续时间来指定的,而日历类型对于年、月、星期几和一个月中的日期有不同的组合类型和值。这两种类型都很有用:
- 只要我们仅使用秒、小时和天进行计算(例如在一年中的每一天执行某些操作),时间点类型就很适用。
- 日历类型在处理日期算术运算时很有用,因为它考虑到了月份和年份的天数不同。此外,你可以处理诸如“本月的第三个星期一”或“本月的最后一天”这样的情况。
表11.4“标准日历类型”列出了新的日历类型及其默认输出格式。
类型 | 含义 | 输出格式 |
---|---|---|
day | 日 | 05 |
month | 月 | Feb |
year | 年 | 1999 |
weekday | 星期几 | Mon |
weekday_indexed | 第n个星期几 | Mon[2] |
weekday_last | 最后一个星期几 | Mon[last] |
month_day | 一个月中的某一天 | Feb/05 |
month_day_last | 一个月的最后一天 | Feb/last |
month_weekday | 一个月中的第n个星期几 | Feb/Mon[2] |
month_weekday_last | 一个月中的最后一个星期几 | Feb/Mon[last] |
year_month | 一年中的某个月 | 1999/Feb |
year_month_day | 完整日期(一年中某个月的某一天) | 1999-02-05 |
year_month_day_last | 一年中某个月的最后一天 | 1999/Feb/last |
year_month_weekday | 一年中某个月的第n个星期几 | 1999/Feb/Mon[2] |
year_month_weekday_last | 一年中某个月的最后一个星期几 | 1999/Feb/Mon[last] |
表11.4 标准日历类型
请记住,这些类型名称并不意味着在初始化或格式化输出时日期元素的传递顺序。例如,std::chrono::year_month_day
类型可以如下使用:
using namespace std::chrono;
year_month_day d = January/31/2021; // 2021年1月31日
std::cout << std::format("{:%D} ", d); // 输出 01/31/21
2
3
像year_month_day
这样的日历类型使我们能够精确计算下个月的同一天的日期:
std::chrono::year_month_day start = ... ; // 2021/2/5
auto end = start + std::chrono::months{1}; // 2021/3/5
2
如果你使用像sys_days
这样的时间点,相应的代码可能无法正确运行,因为它使用的是一个月的平均持续时间:
std::chrono::sys_days start = ... ; // 2021/2/5
auto end = start + std::chrono::months{1}; // 2021/3/7 10:29:06
2
请注意,在这种情况下,end
的类型不同,因为涉及到月份的小数部分天数。
当添加4周或28天时,时间点类型更适用,因为对于它们来说,这是一个简单的算术运算,无需考虑月份或年份的不同长度:
std::chrono::sys_days start = ... ; // 2021/1/5
auto end = start + std::chrono::weeks{4}; // 2021/2/2
2
后面将讨论使用月份和年份的详细内容。
如你所见,有特定的类型用于处理星期几以及一个月中的第n个和最后一个星期几。这使我们能够遍历所有的第二个星期一,或者跳到下个月的最后一天:
std::chrono::year_month_day_last start = ... ; // 2021/2/28
auto end = start + std::chrono::months{1}; // 2021/3/31
2
每种类型都有一个默认的输出格式,是一个固定的英文字符序列。对于其他格式,请使用格式化的chrono输出。请注意,只有year_month_day
在其默认输出格式中使用连字符作为分隔符。其他所有类型默认使用斜杠分隔其组成部分。
为了处理日历类型的值,定义了一些日历常量:
std::chrono::last
:用于指定一个月的最后一天/星期几。该常量的类型为std::chrono::last_spec
。std::chrono::Sunday
、std::chrono::Monday
、...、std::chrono::Saturday
:用于指定星期几(星期日的值为0,星期六的值为6)。这些常量的类型为std::chrono::weekday
。std::chrono::January
、std::chrono::February
、...、std::chrono::December
:用于指定月份(一月的值为1,十二月的值为12)。这些常量的类型为std::chrono::month
。
# 日历类型的受限操作
日历类型旨在在编译时检测出无用或性能不佳的操作。因此,一些“明显”的操作无法编译,如下表“标准日历类型操作”所示:
- 日期和月份的算术运算取决于年份。你不能对没有年份的所有月份类型进行加/减天数/月数或计算差值。
- 对于完全指定的日期进行日期算术运算,需要一些时间来处理月份的不同长度和闰年的情况。你可以对这些类型进行加/减天数或计算差值。
- 由于chrono库没有对一周的第一天做出假设,所以无法确定星期几之间的顺序。在比较包含星期几的类型时,仅支持
operator==
和operator!=
。 | 类型 |++
/--
| 加/减 | -(差值) |==
|<
/<=>
| | ------------------------- | --------- | ---------- | --------- | ---- | --------- | |day
| 是 | 天数 | 是 | 是 | 是 | |month
| 是 | 月数、年数 | 是 | 是 | 是 | |year
| 是 | 年数 | 是 | 是 | 是 | |weekday
| 是 | 天数 | 是 | 是 | 否 | |weekday_indexed
| 否 | - | - | 是 | 否 | |weekday_last
| 否 | - | - | 是 | 否 | |month_day
| 否 | - | - | 是 | 是 | |month_day_last
| 否 | - | - | 是 | 是 | |month_weekday
| 否 | - | - | 是 | 否 | |month_weekday_last
| 否 | - | - | 是 | 否 | |year_month
| 否 | 月数、年数 | 是 | 是 | 是 | |year_month_day
| 否 | 月数、年数 | - | 是 | 是 | |year_month_day_last
| 否 | 月数、年数 | - | 是 | 是 | |year_month_weekday
| 否 | 月数、年数 | - | 是 | 否 | |year_month_weekday_last
| 否 | 月数、年数 | - | 是 | 否 |
表11.5 标准日历类型操作
星期几的算术运算是模7的,这意味着一周的第一天是哪一天并不重要。你可以计算任意两个星期几之间的差值,结果始终是0到6之间的值。将差值加到第一个星期几上,总是会得到第二个星期几。例如:
std::cout << chr::Friday - chr::Tuesday << "\n"; // 3d (从星期二到星期五)
std::cout << chr::Tuesday - chr::Friday << "\n"; // 4d (从星期五到星期二)
auto d1 = chr::February / 25 / 2021;
auto d2 = chr::March / 3 / 2021;
std::cout << chr::sys_days{d1} - chr::sys_days{d2} << "\n"; // -6d (日期差值)
std::cout << chr::weekday(d1) - chr::weekday(d2) << "\n"; // 3d (星期几差值)
2
3
4
5
6
7
这样,你可以轻松计算到“下一个星期一”的差值,即“星期一减去当前星期几”:
d1 = chr::sys_days{d1} + (chr::Monday - chr::weekday(d1)); // 将d1设置为下一个星期一
请注意,如果d1
是一个日历日期类型,你首先必须将其转换为std::chrono::sys_days
类型,以便支持日期算术运算(最好将d1
声明为这种类型)。
同样,月份的算术运算是模12的,这意味着十二月之后的下一个月是一月。如果类型中包含年份,则会相应地进行调整:
auto m = chr::December;
std::cout << m + chr::months{10} << "\n"; // Oct
std::cout << 2021y/m + chr::months{10} << "\n"; // 2022/Oct
2
3
还要注意,chrono日历类型中接受整数值的构造函数是explicit
(显式)的,这意味着使用整数值进行隐式初始化会失败:
std::chrono::day d1{3}; // 正确
std::chrono::day d2 = 3; // 错误
d1 = 3; // 错误
d1 = std::chrono::day{3}; // 正确
passDay(3); // 错误
passDay(std::chrono::day{3}); // 正确
2
3
4
5
6
# 11.3.5 时间类型hh_mm_ss
与日历类型相对应,C++20引入了一种新的时间类型std::chrono::hh_mm_ss
,它将持续时间转换为具有相应时间字段的数据结构。表11.6“std::chrono::hh_mm_ss
成员”描述了hh_mm_ss
成员的名称和含义。
成员 | 含义 |
---|---|
hours() minutes() seconds() subseconds() is_negative() to_duration() precision operator precision() fractional_width | 小时值 分钟值 秒值 具有适当粒度的部分秒值 如果值为负,则为 true 转换回持续时间 部分秒的持续时间类型 转换为具有相应精度的值 部分秒的精度(静态成员) |
表11.6 std::chrono::hh_mm_ss
成员
这种类型对于处理持续时间和时间点的不同属性非常有用。它允许我们将持续时间分解为其属性,并作为格式化的辅助工具。例如,你可以检查特定的小时数,或者将小时和分钟作为整数值传递给另一个函数:
auto dur = measure(); // 处理并产生某个持续时间
std::chrono::hh_mm_ss hms{dur}; // 转换为属性的数据结构
process(hms.hours(), hms.minutes()); // 传递小时和分钟
2
3
如果你有一个时间点,则必须首先将其转换为持续时间。为此,通常只需计算时间点与当天午夜(通过将其向下舍入到天的粒度来计算)之间的差值,例如:
auto tp = getStartTime(); //处理并产生某个时间点
// 将时间转换为属性的数据结构:
std::chrono::hh_mm_ss hms{tp - std::chrono::floor<std::chrono::days>(tp)};
process(hms.hours(), hms.minutes()); //传递小时和分钟
2
3
4
再举个例子,我们可以使用hh_mm_ss
以不同形式打印持续时间的属性:
auto t0 = std::chrono::system_clock::now();
...
auto t1 = std::chrono::system_clock::now();
std::chrono::hh_mm_ss hms{t1 - t0};
std::cout << "minutes : " << hms.hours() + hms.minutes() << "\n";
std::cout << "seconds : " << hms.seconds() << "\n";
std::cout << "subsecs : " << hms.subseconds() << "\n";
2
3
4
5
6
7
可能会输出:
minutes: 63min
seconds: 19s
subsecs: 502998000ns
2
3
无法直接用特定值初始化hh_mm_ss
对象的不同属性。一般来说,应该使用持续时间类型来处理时间:
using namespace std::literals;
...
auto t1 = 18h + 30min; // 18小时30分钟
2
3
hh_mm_ss
最强大的功能在于,它可以接受任意精度的持续时间,并将其转换为通常的小时、分钟和秒的属性及持续时间类型。此外,subseconds()
会以适当的持续时间类型(如毫秒或纳秒)返回剩余的值。即使单位不是10的幂次方(例如三分之一秒),hh_mm_ss
也会将其转换为10的幂次方且最多18位的部分秒值。如果无法精确表示该值,则会使用6位精度。可以使用标准输出运算符将结果作为一个整体打印出来。例如:
std::chrono::duration<int, std::ratio<1,3>> third{1};
auto manysecs = 10000s;
auto dblsecs = 10000.0s;
std::cout << "third : " << third << "\n";
std::cout << " " << std::chrono::hh_mm_ss{third} << "\n";
std::cout << "manysecs : " << manysecs << "\n";
std::cout << " " << std::chrono::hh_mm_ss{manysecs} << "\n";
std::cout << "dblsecs : " << dblsecs << "\n";
std::cout << " " << std::chrono::hh_mm_ss{dblsecs} << "\n";
2
3
4
5
6
7
8
9
这段代码的输出如下:
third: 1[1/3]s
00:00:00.333333
manysecs: 10000s
02:46:40
dblsecs: 10000.000000s
02:46:40.000000
2
3
4
5
6
要输出特定属性,也可以使用带有特定转换说明符的格式化输出:
auto manysecs = 10000s;
std::cout << "manysecs : " << std::format("{:%T} ", manysecs) << "\n";
2
这也会输出:
manysecs: 02:46:40
# 11.3.6 小时相关实用函数
chrono
库现在还提供了一些辅助函数,用于处理12小时制和24小时制格式。“小时相关实用函数”表列出了这些函数。
std::chrono::is_am(h) std::chrono::is_pm(h) std::chrono::make12(h) std::chrono::make24(h, toPM) | 判断h 是否为0到11之间的小时值 判断 h 是否为12到23之间的小时值 返回 h 小时值的12小时制等效值 返回 h 小时值的24小时制等效值 |
---|
表11.7 小时相关实用函数
std::chrono::make12()
和std::chrono::make24()
都要求传入的小时值在0到23之间。std::chrono::make24()
的第二个参数用于指定是否将传入的小时值解释为下午(PM)时间值。如果该参数为true
,并且传入的值在0到11之间,函数会给传入的值加上12小时。
例如:
for (int hourValue : {9, 17}) {
std::chrono::hours h{hourValue};
if (std::chrono::is_am(h)) {
h = std::chrono::make24(h, true); // 假设表示下午的时间
}
std::cout << "Tea at " << std::chrono::make12(h).count() << "pm " << "\n";
}
2
3
4
5
6
7
这段代码的输出如下:
Tea at 9pm
Tea at 5pm
2
# 11.4 chrono
类型的输入输出
C++20为几乎所有chrono
类型的直接输出和解析提供了新的支持。
# 11.4.1 默认输出格式
自C++20起,几乎所有chrono
类型都定义了标准输出运算符。如果适用,它不仅会打印值,还会使用合适的格式和单位。也支持依赖于区域设置(locale)的格式化。
所有日历类型(calendrical types)都会按照“日历类型的输出格式”中列出的方式打印值。
所有时长类型(duration types)都会按照“时长的输出单位”表中列出的单位类型打印值。如果有提供,这与任何字面量运算符都匹配。
单位 | 输出后缀 |
---|---|
atto femto pico nano micro milli centi deci ratio<1> deca hecto kilo mega giga tera peta exa ratio<60> ratio<3600> ratio<86400> ratio<num,1> ratio<num,den> | as fs ps ns s或us ms cs ds s das hs ks Ms Gs Ts Ps Es min h d [num]s [num/den]s |
表11.8 时长的输出单位 |
对于所有标准时间点类型(timepoint types),输出运算符会按以下格式打印日期,时间部分可选:
- 对于隐式可转换为天数的整数粒度单位,格式为
year-month-day
。 - 对于等于或小于天数的整数粒度单位,格式为
year-month-day hour:minutes:seconds
。
如果时间点值的类型(成员rep
)是浮点型,则未定义输出运算符(希望C++23能修复这个问题)。对于时间部分,会使用hh_mm_ss
类型的输出运算符。这与格式化输出的%F %T
转换说明符相对应。例如:
auto tpSys = std::chrono::system_clock::now();
std::cout << tpSys << "\n"; // 2021-04-25 13:37:02.936314000
auto tpL = chr::zoned_time{chr::current_zone(), tpSys}.get_local_time();
std::cout << tpL; // 2021-04-25 15:37:02.936314000
std::cout << chr::floor<chr::milliseconds>(tpL); // 2021-04-25 15:37:02.936
std::cout << chr::floor<chr::seconds>(tpL); // 2021-04-25 15:37:02
std::cout << chr::floor<chr::minutes>(tpL); // 2021-04-25 15:37:00
std::cout << chr::floor<chr::days>(tpL); // 2021-04-25
std::cout << chr::floor<chr::weeks>(tpL); // 2021-04-22
auto tp3 = std::chrono::floor<chr::duration<long long, std::ratio<1, 3>>>(tpSys);
std::cout << tp3 << "\n"; // 2021-04-25 13:37:02.666666
chr::sys_time<chr::duration<double, std::milli>> tpD{tpSys};
std::cout << tpD << "\n"; // 错误:未定义输出运算符
std::chrono::gps_seconds tpGPS;
std::cout << tpGPS << "\n"; // 1980-01-06 00:00:00
auto tpStd = std::chrono::steady_clock::now();
std::cout << "tpStd: " << tpStd; // 错误:未定义输出运算符
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
zoned_time<>
类型的输出类似于时间点类型,并扩展了时区缩写名称。标准时钟使用以下时区缩写:
- 对于
sys_clock
、utc_clock
和file_clock
,使用UTC
。 - 对于
tai_clock
,使用TAI
。 - 对于
gps_clock
,使用GPS
。
最后要注意,sys_info
和local_info
的输出运算符格式未定义,它们仅应用于调试目的。
# 11.4.2 格式化输出
chrono
支持新的格式化输出库。这意味着你可以将日期/时间类型用作std::format()
和std::format_to()
的参数。
例如:
auto t0 = std::chrono::system_clock::now();
...
auto t1 = std::chrono::system_clock::now();
std::cout << std::format("From {} to {}\nit took {}\n", t0, t1, t1 - t0);
2
3
4
这将使用日期/时间类型的默认输出格式。例如,我们可能得到如下输出:
From 2021-04-03 15:21:33.197859000 to 2021-04-03 15:21:34.686544000
it took 1488685000ns
2
为了改善输出效果,你可以限制类型或使用特定的转换说明符。其中大多数(但不是全部)与C函数strftime()
和POSIX的date
命令相对应。例如:
std::cout << std::format("From {:%T} to {:%T} it took {:%S}s\n", t0, t1, t1 - t0);
这样可能会得到如下输出:
From 15:21:34.686544000 to 15:21:34.686544000 it took 01.488685000s
chrono
类型的格式说明符是标准格式说明符语法的一部分(每个说明符都是可选的):
fill align width.prec L spec
fill
、align
、width
和prec
与标准格式说明符的含义相同。spec
指定格式化的通用表示法,以%
开头。L
与往常一样,用于为支持的说明符开启依赖于区域设置的格式化。
“chrono
类型的转换说明符”表列出了所有日期/时间类型格式化输出的转换说明符,并基于2019年6月9日星期日17:33:16和850毫秒给出了示例。
如果不使用特定的转换说明符,将使用默认的输出运算符,它会标记无效日期。而使用特定转换说明符时则不会这样:
std::chrono::year_month_day ymd{2021y/2/31}; // 2021年2月31日
std::cout << std::format("{} ", ymd); //2021-02-31 不是有效的...
std::cout << std::format("{:%F} ", ymd); // 2021-02-31
std::cout << std::format("{:%Y-%m-%d} ", ymd); // 2021-02-31
2
3
4
如果日期/时间值类型没有为转换说明符提供必要的信息,将抛出std::format_error
异常。例如,在以下情况会抛出异常:
- 对
month_day
使用年份说明符。 - 对时长使用星期几说明符。
- 应打印月份或星期几的名称,但值无效。
- 对本地时间点使用时区说明符。
此外,关于转换说明符还要注意以下几点:
- 负的时长或
hh_mm_ss
值会在整个值前面打印负号。例如:
std::cout << std::format("{:%H:%M:%S} ", -10000s); // 输出: -02:46:40
说明符 | 示例 | 含义 |
---|---|---|
%c | Sun Jun 9 17:33:16 2019 | 标准或区域设置的日期和时间表示形式 |
日期:%x %F %D %e %d %b %h %B %m %Y %y %G %g %C | 06/09/19 2019-06-09 06/09/19 9 09 Jun Jun June 06 2019 19 2019 19 20 | 标准或区域设置的日期表示形式 四位和两位数字的年-月-日格式 两位数字的月/日/年格式 如果是一位数,在前面补空格的日期格式 两位数字的日期格式 标准或区域设置的缩写月份名称 同上 标准或区域设置的完整月份名称 两位数字的月份格式 四位数字的年份格式 不带世纪的两位数字年份格式 基于ISO周的四位数字年份(根据 %V 确定的周) 基于ISO周的两位数字年份(根据 %V 确定的周) 两位数字的世纪格式 |
星期和周:%a %A %w %u %W %U %V | Sun Sunday 0 7 22 23 23 | 标准或区域设置的缩写星期几名称 标准或区域设置的完整星期几名称 以十进制数表示的星期几(星期日为0,星期六为6) 以十进制数表示的星期几(星期一为1,星期日为7) 一年中的第几周(00...53,第01周从第一个星期一开始) 一年中的第几周(00...53,第01周从第一个星期日开始) ISO标准的一年中的第几周(01...53,第01周包含1月4日) |
时间:%X %r %T %R %H %I %p %M %S | 17:33:16 05:33:16 PM 17:33:16.850 17:33 17 05 PM 33 16.850 | 标准或区域设置的时间表示形式 标准或区域设置的12小时制时间 小时:分钟:秒(根据区域设置需要包含小数秒) 两位数字表示的小时:分钟格式 24小时制的两位数字小时格式 12小时制的两位数字小时格式 根据12小时制的上午(AM)或下午(PM) 两位数字的分钟格式 以十进制数表示的秒(根据区域设置包含小数秒) |
其他:%Z %z %j %q %Q %n %t %% | CEST +0200 160 ms 63196850 \n \t % | 时区缩写(也可能是UTC、TAI或GPS) 与UTC的偏移量(小时和分钟, %Ez 或%Oz 表示+02:00这种格式) 一年中的第几天,三位数字表示(1月1日为001) 根据时间的时长单位后缀 根据时间的时长值 换行符 制表符 % 字符 |
- 不同的周数和年份格式可能会导致不同的输出值。例如,2023年1月1日星期日会得到:
- 使用
%W
时为第00周(第一个星期一之前的周)。 - 使用
%U
时为第01周(包含第一个星期一的周)。 - 使用
%V
时为第52周(ISO周:第01周的星期一之前的周,第01周包含1月4日)。
- 使用
由于ISO周可能是上一年的最后一周,所以ISO年份(即该周所属的年份)可能会少一年:
- 使用%Y
时为2023年。
- 使用%y
时为23年。
- 使用%G
时为2022年(%V
指定的ISO周的ISO年份,该周是上一个月的最后一周)。
- 使用%g
时为22年(%V
指定的ISO周的ISO年份,该周是上一个月的最后一周)。
- 标准时钟使用以下时区缩写:
- 对于
sys_clock
、utc_clock
和file_clock
,使用UTC
。 - 对于
tai_clock
,使用TAI
。 - 对于
gps_clock
,使用GPS
。
- 对于
除了%q
和%Q
之外,所有转换说明符也可用于格式化解析。
# 11.4.3 依赖于区域设置的输出
如果输出流设置了有自己格式的区域设置,各种类型的默认输出运算符会使用依赖于该区域设置的格式。例如:
using namespace std::literals;
auto dur = 42.2ms;
std::cout << dur << "\n"; // 42.2ms
#ifdef _MSC_VER
std::locale locG("deu_deu.1252");
#else
std::locale locG("de_DE");
#endif
std::cout.imbue(locG); // 切换到德语区域设置
std::cout << dur << "\n"; // 42,2ms
2
3
4
5
6
7
8
9
10
11
12
使用std::format()
进行格式化输出时按如下方式处理(这一行为是对C++20的一个错误修正(见 http://wg21.link/p2372 (opens new window)),这意味着C++20最初的措辞并未规定此行为。):
- 默认情况下,格式化输出使用与区域设置无关的“C”区域设置。
- 通过指定
L
,你可以切换到通过区域设置参数指定的或作为全局区域设置的依赖于区域设置的输出。
这意味着,要使用依赖于区域设置的表示法,你必须使用L
说明符,并且要么将区域设置作为第一个参数传递给std::format()
,要么在调用它之前设置全局区域设置。例如:
using namespace std::literals;
auto dur = 42.2ms; // 要打印的时长
#ifdef _MSC_VER
std::locale locG("deu_deu.1252");
#else
std::locale locG("de_DE");
#endif
std::string s1 = std::format("{:%S} ", dur); // "00.042s"(未本地化)
std::string s3 = std::format(locG, "{:%S} ", dur); // "00.042s"(未本地化)
std::string s2 = std::format(locG, "{:L%S} ", dur); // "00,042s"(本地化)
std::locale::global(locG); // 全局设置德语区域设置
std::string s4 = std::format("{:L%S} ", dur); // "00,042s"(本地化)
2
3
4
5
6
7
8
9
10
11
12
13
14
在许多情况下,你甚至可以根据strftime()
和ISO 8601:2004使用替代区域设置的表示法,在转换说明符前加上O
或E
即可指定:
E
可以在c
、C
、x
、X
、y
、Y
和z
前用作区域设置的替代表示法。O
可以在d
、e
、H
、I
、m
、M
、S
、u
、U
、V
、w
、W
、y
和z
前用作区域设置的替代数字符号。
# 11.4.4 格式化输入
chrono
库也支持格式化输入。有两种方式:
- 某些日期/时间类型提供了独立函数
std::chrono::from_stream()
,用于根据传入的格式字符串读取特定值。 - 操纵符
std::chrono::parse()
允许我们将from_stream()
作为使用输入运算符>>
进行更大规模解析的一部分。
# 使用from_stream()
下面的代码展示了如何通过解析完整的时间点来使用from_stream()
:
std::chrono::sys_seconds tp;
std::istringstream sstrm{ "2021-2-28 17:30:00 "};
std::chrono::from_stream(sstrm, "%F %T ", tp);
if (sstrm) {
std::cout << "tp: " << tp << "\n";
} else {
std::cerr << "reading into tp failed\n";
}
2
3
4
5
6
7
8
这段代码的输出如下:
tp: 2021-02-28 17:30:00
再举个例子,你可以从指定完整月份名称和年份等信息的字符序列中解析出year_month
,如下所示:
std::chrono::year_month m;
std::istringstream sstrm{ "Monday, April 5, 2021 "};
std::chrono::from_stream(sstrm, "%A, %B %d, %Y ", m);
if (sstrm) {
std::cout << "month: " << m << "\n"; // 输出: month: 2021/Apr
}
2
3
4
5
6
格式字符串接受除%q
和%Q
之外的所有格式化输出转换说明符,且具有更高的灵活性。例如:
%d
表示用一到两个字符指定日期,使用%4d
时,你可以指定最多解析四个字符。%n
表示恰好一个空白字符。%t
表示零个或一个空白字符。- 像空格这样的空白字符表示任意数量的空白(包括零个空白)。
from_stream()
适用于以下类型:
- 任意类型的
duration<>
。 - 任意时长的
sys_time<>
、utc_time<>
、gps_time<>
、tai_time<>
、local_time<>
或file_time<>
。 day
、month
或year
。year_month
、month_day
或year_month_day
。weekday
。
格式必须是const char*
类型的C字符串,它必须与输入流中的字符以及要解析的值相匹配。如果出现以下情况,解析会失败:
- 输入的字符序列与所需格式不匹配。
- 格式没有为值提供足够的信息。
- 解析出的日期无效。
在这种情况下,流的failbit
会被设置,你可以通过调用fail()
或把流当作布尔值来测试。
# 解析日期/时间的通用函数
在实际应用中,日期和时间很少被硬编码。然而,在测试代码时,你通常需要一种简单的方式来指定日期/时间值。
下面是我用于测试本书示例的一个小辅助函数:
// lib/chronoparse.hpp
#include <chrono>
#include <string>
#include <sstream>
#include <cassert>
// 解析年-月-日,可选小时:分钟和可选:秒
// - 返回传入时钟(默认:system_clock)的time_point<>,以秒为单位
template<typename Clock = std::chrono::system_clock>
auto parseDateTime(const std::string& s)
{
// 返回值:
std::chrono::time_point<Clock, std::chrono::seconds> tp;
// 用于读取的字符串流:
std::istringstream sstrm{s}; // 不支持string_view
auto posColon = s.find(" :");
if (posColon != std::string::npos) {
if (posColon != s.rfind(" :")) {
// 多个冒号:
std::chrono::from_stream(sstrm, "%F %T ", tp);
} else {
// 一个冒号:
std::chrono::from_stream(sstrm, "%F %R ", tp);
}
} else {
// 没有冒号:
std::chrono::from_stream(sstrm, "%F", tp);
}
// 处理无效格式:
assert((!sstrm.fail()));
return tp;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
你可以如下使用parseDateTime()
:
// lib/chronoparse.cpp
#include "chronoparse.hpp"
#include <iostream>
int main() {
auto tp1 = parseDateTime("2021-1-1");
std::cout << std::format("{:%F %T %Z}\n", tp1);
auto tp2 = parseDateTime<std::chrono::local_t>("2021-1-1");
std::cout << std::format("{:%F %T}\n", tp2);
auto tp3 = parseDateTime<std::chrono::utc_clock>("2015-6-30 23:59:60");
std::cout << std::format("{:%F %T %Z}\n", tp3);
auto tp4 = parseDateTime<std::chrono::gps_clock>("2021-1-1 18:30");
std::cout << std::format("{:%F %T %Z}\n", tp4);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
该程序的输出如下:
2021-01-01 00:00:00 UTC
2021-01-01 00:00:00
2015-06-30 23:59:60 UTC
2021-01-01 18:30:00 GPS
2
3
4
注意,对于本地时间点,不能使用%Z
打印其时区(这样做会抛出异常)。
# 使用parse()
操纵符
你可以调用:
sstrm >> std::chrono::parse("%F %T ", tp);
来替代调用from_stream()
:
std::chrono::from_stream(sstrm, "%F %T ", tp);
请注意,原始的C++20标准并未正式允许直接将格式作为字符串字面量传递,这意味着你必须调用:
sstrm >> std::chrono::parse(std::string{"%F %T "}, tp);
不过,这个问题应该会通过http://wg21.link/lwg3554 (opens new window)得到修复。
std::chrono::parse()
是一个输入输出流操纵符。它允许你在从输入流读取的一条语句中解析多个值。此外,得益于移动语义,你甚至可以传递一个临时输入流。例如:
chr::sys_days tp;
chr::hours h;
chr::minutes m;
// 将日期解析到tp,小时解析到h,分钟解析到m:
std::istringstream{"12/24/21 18:00"} >> chr::parse("%D ", tp) >> chr::parse(" %H ", h)
>> chr::parse(":%M", m);
std::cout << tp << " at " << h << " " << m << "\n";
2
3
4
5
6
7
这段代码的输出为:
2021-12-24 at 18h 0min
再次注意,你可能需要将字符串字面量"%D"
、" %H"
和":%M"
显式转换为字符串。
# 解析时区
解析时区有点棘手,因为时区缩写并不唯一。为了提供帮助,from_stream()
有以下几种格式:
istream from_stream(istream , format , value)
istream from_stream(istream , format , value , abbrevPtr)
istream from_stream(istream , format , value , abbrevPtr , offsetPtr)
2
3
如你所见,你可以选择传递一个std::string
的地址,将解析出的时区缩写存储到该字符串中,还可以传递一个std::chrono::minutes
对象的地址,将解析出的时区偏移量存储到该对象中。在这两种情况下,都可以传递nullptr
。
不过,你仍需小心:
- 以下代码可以正常工作:
chr::sys_seconds tp;
std::istringstream sstrm{ "2021-4-13 12:00 UTC "};
chr::from_stream(sstrm, "%F %R %Z ", tp);
std::cout << std::format("{:%F %R %Z}\n", tp); // 2021-04-13 12:00 UTC
2
3
4
但它能正常工作只是因为系统时间点无论如何都使用UTC。
- 以下代码无法正常工作,因为它忽略了时区:
chr::sys_seconds tp;
std::istringstream sstrm{ "2021-4-13 12:00 MST "};
chr::from_stream(sstrm, "%F %R %Z ", tp);
std::cout << std::format("{:%F %R %Z} ", tp); // 2021-04-13 12:00 UTC
2
3
4
%Z
用于解析MST
,但没有参数来存储该值。
- 以下代码看似可以正常工作:
chr::sys_seconds tp;
std::string tzAbbrev;
std::istringstream sstrm{ "2021-4-13 12:00 MST "};
chr::from_stream(sstrm, "%F %R %Z ", tp, &tzAbbrev);
std::cout << tp << "\n"; // 2021-04-13 12:00
std::cout << tzAbbrev << "\n"; // MST
2
3
4
5
6
7
然而,如果你计算区域时间,会发现你是在将UTC时间转换为不同的时区:
chr::zoned_time zt{tzAbbrev, tp}; // 没问题:MST存在
std::cout << zt << "\n"; // 2021-04-13 05:00:00 MST
2
- 以下代码看起来确实可以正常工作:
chr::local_seconds tp; // 本地时间
std::string tzAbbrev;
std::istringstream sstrm{ "2021-4-13 12:00 MST "};
chr::from_stream(sstrm, "%F %R %Z ", tp, &tzAbbrev);
std::cout << tp << "\n"; // 2021-04-13 12:00
std::cout << tzAbbrev << "\n"; // MST
chr::zoned_time zt{tzAbbrev, tp}; // 没问题:MST存在
std::cout << zt << "\n"; // 2021-04-13 12:00:00 MST
2
3
4
5
6
7
8
9
但这只是因为MST
是时区数据库中少数几个作为已弃用条目存在的缩写之一。一旦你使用CEST
或CST
运行这段代码,在初始化zoned_time
时就会抛出异常。
- 因此,要么只使用
tzAbbrev
而不是zoned_time
和%Z
:
chr::local_seconds tp; // 本地时间
std::string tzAbbrev;
std::istringstream sstrm{ "2021-4-13 12:00 CST "};
chr::from_stream(sstrm, "%F %R %Z ", tp, &tzAbbrev);
std::cout << std::format("{:%F %R} {}", tp, tzAbbrev); // 2021-04-13 12:00 CST
2
3
4
5
要么你必须编写代码将时区缩写映射到时区。注意,%Z
无法解析伪时区GPS
和TAI
。
# 11.5 实际使用chrono扩展
既然你已经了解了chrono库的新特性和类型,本节将讨论如何在实际中使用它们。
另请参阅http://github.com/HowardHinnant/date/wiki/Examples-and-Recipes (opens new window),获取更多示例和方法。
# 11.5.1 无效日期
日历类型的值可能无效。这可能以两种方式发生:
- 通过使用无效值进行初始化。例如:
std::chrono::day d{0}; // 无效日期
std::chrono::year_month ym{2021y/13}; // 无效的年-月
std::chrono::year_month_day ymd{2021y/2/31}; // 无效的年-月-日
2
3
- 通过计算得到无效日期。例如:
auto ymd1 = std::chrono::year{2021}/1/31; // 2021年1月31日
ymd1 += std::chrono::months{1}; // 2021年2月31日(无效)
auto ymd0 = std::chrono::year{2020}/2/29; // 2020年2月29日
ymd1 += std::chrono::years{1}; // 2021年2月29日(无效)
2
3
4
表11.10“标准日期属性的有效值”列出了日期不同属性的内部类型和可能的值。
属性 | 内部类型 | 有效值 |
---|---|---|
日 | unsigned char | 1到31 |
月 | unsigned char | 1到12 |
年 | short | -32767到32767 |
星期几 | unsigned char | 0(星期日)到6(星期六) 以及7(同样表示星期日,会转换为0) |
星期几索引 | unsigned char | 1到5 |
表11.10 标准日期属性的有效值
如果各个组件有效,则所有组合类型都是有效的(例如,一个有效的month_weekday
要求月份和星期几都有效)。不过,可能还需要进行额外检查:
- 类型为
year_month_day
的完整日期必须是存在的,这意味着它要考虑闰年的情况。例如:
2020y/2/29; // 有效(2020年有2月29日)
2021y/2/29; // 无效(2021年没有2月29日)
2
- 仅当日期在该月份中可能有效时,
month_day
才有效。对于二月,29日是有效的,但30日无效。例如:
February/29; // 有效(二月可能有29天)
February/30; // 无效(二月不可能有30天)
2
- 仅当指定年份的指定月份中存在该星期几索引时,
year_month_weekday
才有效。例如:
2020y/1/Thursday[5]; // 有效(2020年1月有第五个星期四)
2020y/1/Sunday[5]; // 无效(2020年1月没有第五个星期日)
2
每个日历类型都提供了一个ok()
成员函数,用于检查值是否有效。默认的输出运算符会标记无效日期。
处理无效日期的方式取决于你的编程逻辑。对于创建的日期超过了月份中最大天数这种常见情况,你有以下几种选择:
- 向下舍入到该月的最后一天:
auto ymd = std::chrono::year{2021}/1/31;
ymd += std::chrono::months{1};
if (!ymd.ok()) {
ymd = ymd.year()/ymd.month()/std::chrono::last; // 2021年2月28日
}
2
3
4
5
注意,右侧的表达式创建了一个year_month_last
,然后会转换为year_month_day
类型。
- 向上舍入到下个月的第一天:
auto ymd = std::chrono::year{2021}/1/31;
ymd += std::chrono::months{1};
if (!ymd.ok()) {
ymd = ymd.year()/ymd.month()/1 + std::chrono::months{1}; // 2021年3月1日
}
2
3
4
5
不要只是将月份加1,因为对于12月,这样会创建一个无效的月份。
- 根据溢出的天数向上舍入日期:
auto ymd = std::chrono::year{2021}/1/31;
ymd += std::chrono::months{1}; // 2021年3月3日
if (!ymd.ok()) {
ymd = std::chrono::sys_days(ymd);
}
2
3
4
5
这利用了year_month_day
转换的一个特殊功能,即所有溢出的天数会在逻辑上添加到下个月。不过,这种方法只适用于少数天数(不能用这种方式添加1000天)。
如果日期不是有效值,默认输出格式会用“is not a valid type”来标记。例如:
std::chrono::day d{0}; // 无效日期
std::chrono::year_month_day ymd{2021y/2/31}; // 无效的年-月-日
std::cout << "day : " << d << "\n";
std::cout << "ymd : " << ymd << "\n";
2
3
4
这段代码将输出:
day: 00 is not a valid day
ymd: 2021-02-31 is not a valid year_month_day
2
使用格式化输出的默认格式化(仅使用{}
)时也会如此。通过使用特定的转换说明符,可以禁用“is not a valid”输出(在银行或季度处理软件中,有时甚至会使用像6月31日这样的日期):
std::chrono::year_month_day ymd{2021y/2/31};
std::cout << ymd << "\n"; // “2021-02-31 is not a valid year_month_day ”
std::cout << std::format("{:%F}\n", ymd); // “2021-02-31 ”
std::cout << std::format("{:%Y-%m-%d}\n", ymd); // “2021-02-31 ”
2
3
4
# 11.5.2 处理月份和年份
由于std::chrono::months
和std::chrono::years
不是天数的整数倍,在使用它们时必须小心。
- 对于那些有自己年份值的标准类型(如
month
、year
、year_month
、year_month_day
、year_month_weekday_last
等),在给日期添加特定数量的月份或年份时,它们的表现正常。 - 对于将日期作为一个整体来表示的标准时间点类型(如
time_point
、sys_time
、sys_seconds
和sys_days
),它们会给日期加上相应的平均小数周期,这可能不会得到你期望的日期。
例如,让我们看看给2020年12月31日分别加上4个月或4年的不同结果:
- 处理
year_month_day
时:
chr::year_month_day ymd0 = chr::year{2020}/12/31;
auto ymd1 = ymd0 + chr::months{4}; // 糟糕:2021年4月31日
auto ymd2 = ymd0 + chr::years{4}; // 正确:2024年12月31日
std::cout << "ymd: " << ymd0 << "\n";// 2020-12-31
std::cout << " +4months: " << ymd1 << "\n";// 2021-04-31 is not a valid ...
std::cout << " +4years: " << ymd2 << "\n";// 2024-12-31
2
3
4
5
6
- 处理
year_month_day_last
时:
chr::year_month_day_last yml0 = chr::year{2020}/12/chr::last;
auto yml1 = yml0 + chr::months{4}; // 正确:2021年4月的最后一天
auto yml2 = yml0 + chr::years{4}; // 正确:2024年12月的最后一天
std::cout << "yml : " << yml0 << "\n"; // 2020/Dec/last
std::cout << " +4months: " << yml1 << "\n"; // 2021/Apr/last
std::cout << " as date : " << chr::sys_days{yml1} << "\n"; // 2021-04-30
std::cout << " +4years: " << yml2 << "\n"; // 2024/Dec/last
2
3
4
5
6
7
- 处理
sys_days
时:
chr::sys_days day0 = chr::year{2020}/12/31;
auto day1 = day0 + chr::months{4}; // 糟糕:2021年5月1日17:56:24
auto day2 = day0 + chr::years{4}; // 糟糕:2024年12月30日23:16:48
std::cout << "day : " << day0 << "\n"; // 2020-12-31
std::cout << " with time : " << chr::sys_seconds{day0} << "\n";// 2020-12-31 00:00:00
std::cout << " +4months: " << day1 << "\n"; // 2021-05-01 17:56:24
std::cout << " +4years: " << day2 << "\n"; // 2024-12-30 23:16:48
2
3
4
5
6
7
这个特性仅用于支持对一些不关心人类日历复杂细节的物理或生物过程进行建模(如天气、妊娠期等),不应用于其他目的。
注意,输出值和默认输出格式都有所不同。这清楚地表明使用了不同的类型:
- 当给日历类型添加月份或年份时,这些类型会处理正确的逻辑日期(即同一天,或者再次是下个月或下一年的最后一天)。请注意,这可能会导致像4月31日这样的无效日期,默认输出运算符甚至会在输出中标记如下:
2021-04-31 is not a valid year_month_day
- 当给
std::chrono::sys_days
类型的日期添加月份或年份时,结果不是sys_days
类型。与chrono库中通常的情况一样,结果是能够表示任何可能结果的最佳类型:- 添加月份会得到一个单位为54秒的类型。
- 添加年份会得到一个单位为216秒的类型。
这两种单位都是秒的倍数,这意味着它们可以用作
std::chrono::sys_seconds
。默认情况下,相应的输出运算符会以秒为单位打印日期和时间,正如你所看到的,这与之后月份或年份的同一天和时间并不相同。两个时间点都不再是一个月的31日或最后一天,时间也不再是午夜:
2021-05-01 17:56:24
2024-12-30 23:16:48
2
这两种情况都可能有用。不过,将月份和年份与时间点一起使用通常仅在计算许多个月和 / 或年后的大致日期时才有用。
由于一些不一致性,无效日期的确切格式可能与C++20标准不匹配,标准中此处指定的是“is not a valid date”。
# 11.5.3 解析时间点和持续时间
如果你有一个时间点或持续时间,可以通过以下程序演示的方式访问不同的字段:
// lib/chronoattr.cpp
#include <chrono>
#include <iostream>
int main() {
// 类型为sys_time<>
// 类型为sys_days
auto now = std::chrono::system_clock::now();
auto today = std::chrono::floor<std::chrono::days>(now);
std::chrono::year_month_day ymd{today};
std::chrono::hh_mm_ss hms{now - today};
std::chrono::weekday wd{today};
std::cout << "now : " << now << "\n";
std::cout << "today : " << today << "\n";
std::cout << "ymd : " << ymd << "\n";
std::cout << "hms : " << hms << "\n";
std::cout << "year : " << ymd.year() << "\n";
std::cout << "month : " << ymd.month() << "\n";
std::cout << "day : " << ymd.day() << "\n";
std::cout << "hours : " << hms.hours() << "\n";
std::cout << "minutes : " << hms.minutes() << "\n";
std::cout << "seconds : " << hms.seconds() << "\n";
std::cout << "subsecs : " << hms.subseconds() << "\n";
std::cout << "weekday : " << wd << "\n";
try {
std::chrono::sys_info info{std::chrono::current_zone()->get_info(now)};
std::cout << "timezone : " << info.abbrev << "\n";
}
catch (const std::exception& e) {
std::cerr << "no timezone database : ( " << e.what() << ")\n";
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
该程序的输出可能如下:
now: 2021-04-02 13:37:34.059858000
today: 2021-04-02
ymd: 2021-04-02
hms: 13:37:34.059858000
year: 2021
month: Apr
day: 02
hours: 13h
minutes: 37min
seconds: 34s
subsecs: 59858000ns
weekday: Fri
timezone: CEST
2
3
4
5
6
7
8
9
10
11
12
13
now()
函数返回一个具有系统时钟粒度的时间点:
auto now = std::chrono::system_clock::now();
结果的类型是std::chrono::sys_time<>
,其分辨率由特定实现的持续时间类型决定。
如果你想处理本地时间,必须实现以下内容:
auto tpLoc = std::chrono::zoned_time{std::chrono::current_zone(),
std::chrono::system_clock::now()
}.get_local_time();
2
3
要处理时间点的日期部分,我们需要以天为粒度,这可以通过以下方式获得:
auto today = std::chrono::floor<std::chrono::days>(now);
初始化后的变量today
的类型是std::chrono::sys_days
。你可以将其赋值给std::chrono::year_month_day
类型的对象,这样就可以通过相应的成员函数访问年、月和日:
std::chrono::year_month_day ymd{today};
std::cout << "year : " << ymd.year() << "\n";
std::cout << "month : " << ymd.month() << "\n";
std::cout << "day : " << ymd.day() << "\n";
2
3
4
要处理时间点的时间部分,我们需要一个持续时间,这可以通过计算原始时间点与当天午夜时间点的差值得到。我们使用这个持续时间来初始化一个hh_mm_ss
对象:
std::chrono::hh_mm_ss hms{now - today};
从这个对象中,你可以直接获取小时、分钟、秒和部分秒:
std::cout << "hours : " << hms.hours() << "\n";
std::cout << "minutes : " << hms.minutes() << "\n";
std::cout << "seconds : " << hms.seconds() << "\n";
std::cout << "subsecs : " << hms.subseconds() << "\n";
2
3
4
对于部分秒,hh_mm_ss
会确定所需的粒度并使用适当的单位。在我们的例子中,它以纳秒为单位打印:
hms: 13:37:34 .059858000
hours: 13h
minutes: 37min
seconds: 34s
subsecs: 59858000ns
2
3
4
5
对于星期几,你只需用天粒度的类型来初始化它。这对sys_days
、local_days
和year_month_day
(因为后者会隐式转换为std::sys_days
)都适用:
std::chrono::weekday wd{today}; // 正确(today具有天粒度)
std::chrono::weekday wd{ymd}; // 由于隐式转换为sys_days,所以正确
2
对于时区方面的信息,你必须将时间点(这里是now
)与时区(这里是当前时区)结合起来。得到的std::chrono::sys_info
对象包含诸如时区缩写名称等信息:
std::chrono::sys_info info{std::chrono::current_zone()->get_info(now)};
std::cout << "timezone : " << info.abbrev << "\n";
2
注意,并非每个C++平台都支持时区数据库。因此,在某些系统上,程序的这部分内容可能会抛出异常。
# 11.6 时区
在幅员辽阔的国家或国际交流中,仅仅说 “我们中午见面” 是不够的,因为不同地区存在时区差异。新的chrono库通过提供处理地球上不同时区的API来解决这一问题,包括处理标准时间(“冬季时间”)和夏令时(“夏季时间”)。
# 11.6.1 时区的特点
处理时区问题有点棘手,因为这个话题可能会相当复杂。例如,你必须考虑以下几点:
- 时区差异不一定是整小时数。实际上,我们也有30分钟甚至15/45分钟的差异。例如,澳大利亚的很大一部分地区(北领地和阿德莱德所在的南澳大利亚州)的标准时区是UTC+9:30,而尼泊尔的时区是UTC+5:45。两个时区也可能相差0分钟(比如北美和南美的一些时区)。
- 时区缩写可能指代不同的时区。例如,CST可能代表中部标准时间(Central Standard Time,芝加哥、墨西哥城和哥斯达黎加的标准时区)、中国标准时间(China Standard Time,北京和上海所在时区的国际名称)或古巴标准时间(Cuba Standard Time,哈瓦那的标准时区)。
- 时区会发生变化。例如,当国家决定更改时区或夏令时开始时,就会出现这种情况。因此,在处理时区问题时,你可能需要考虑每年多次的变更。
# 11.6.2 IANA时区数据库
为了处理时区问题,C++标准库使用了IANA时区数据库,可在http://www.iana.org/time-zones (opens new window)上获取。再次注意,并非所有平台都提供该时区数据库。
时区数据库的关键条目是时区名称,通常有以下两种:
- 代表某个地区或国家时区的城市名称。例如:America/Chicago(美国/芝加哥)、Asia/Hong_Kong(亚洲/香港)、Europe/Berlin(欧洲/柏林)、Pacific/Honolulu(太平洋/檀香山)。
- 带有与UTC时间偏移量的GMT条目(偏移量取反)。例如:Etc/GMT、Etc/GMT+6(代表UTC-6)或Etc/GMT-8(代表UTC+8)。是的,UTC时区偏移量在GMT条目中故意取反;你无法搜索类似UTC+6或UTC+5:45的内容。
此外,还支持一些额外的规范名称和别名(例如,UTC或GMT),也有一些已弃用的条目(例如,PST8PDT、US/Hawaii、Canada/Central或Japan)。但是,你不能搜索单个时区缩写条目,如CST或PST。我们稍后将讨论如何处理时区缩写。
有关时区名称列表,另请参阅http://en.wikipedia.org/wiki/List_of_tz_database_time_zones (opens new window)。
# 访问时区数据库
IANA时区数据库每年会进行多次更新,以确保在时区发生变化时能够保持最新,从而使程序能够做出相应反应。
系统处理时区数据库的方式因实现而异。操作系统必须决定如何提供必要的数据以及如何保持其更新。时区数据库的更新通常作为操作系统更新的一部分进行。此类更新通常需要重启机器,因此在更新期间没有C++应用程序可以运行。
C++20标准为该数据库提供了底层支持,处理时区的高级函数会使用这些支持:
std::chrono::get_tzdb_list()
返回对时区数据库的引用,它是std::chrono::tzdb_list
类型的单例。它是一个时区数据库条目的列表,以便能够并行支持多个版本。std::chrono::get_tzdb()
返回对当前时区数据库条目的引用,类型为std::chrono::tzdb
。该类型有多个成员,如下所示:version
,一个表示数据库条目版本的字符串。zones
,一个std::chrono::time_zone
类型的时区信息向量。
处理时区的标准函数(例如,std::chrono::current_zone()
或std::chrono::zoned_time
类型的构造函数)在内部会使用这些调用。
例如,std::chrono::current_zone()
是std::chrono::get_tzdb().current_zone()
的快捷方式。
当平台不提供时区数据库时,这些函数将抛出std::runtime_error
类型的异常。
对于支持在不重启的情况下更新IANA时区数据库的系统,提供了std::chrono::reload_tzdb()
函数。更新不会删除旧数据库中的内存,因为应用程序可能仍然持有指向其中(time_zone
)的指针。相反,新数据库会原子性地添加到时区数据库列表的开头。
你可以使用get_tzdb()
返回的tzdb
类型的version
字符串成员来检查当前时区数据库的版本。例如:
std::cout << "tzdb version : " << chr::get_tzdb().version << '\n';
通常,输出是年份和该年份更新的递增字母字符(例如,“2021b”)。remote_version()
提供最新可用时区数据库的版本,你可以使用它来决定是否调用reload_tzdb()
。
如果一个长时间运行的程序使用chrono时区数据库但从不调用reload_tzdb()
,那么该程序将不会感知到数据库的任何更新。它将继续使用程序首次访问时的数据库版本。
有关为长时间运行的程序重新加载IANA时区数据库的更多详细信息和示例,请参阅http://github.com/HowardHinnant/date/wiki/Examples-and-Recipes#tzdb_manage (opens new window)。
# 11.6.3 使用时区
在处理时区时,有两种类型起着基本作用:
std::chrono::time_zone
,一种表示特定时区的类型。std::chrono::zoned_time
,一种表示与特定时区相关联的特定时间点的类型。
让我们详细了解一下它们。
# time_zone类型
所有可能的时区值都由IANA时区数据库预先定义。因此,你不能仅仅通过声明来创建一个time_zone
对象。这些值来自时区数据库,你通常处理的是指向这些对象的指针:
std::chrono::current_zone()
返回指向当前时区的指针。std::chrono::locate_zone(name)
返回指向指定时区名称的指针。std::chrono::get_tzdb()
返回的时区数据库包含所有时区条目的非指针集合:zones
成员包含所有规范条目。links
成员包含所有别名条目及其链接。
你可以使用这些来根据缩写名称等特征查找时区。例如:
auto tzHere = std::chrono::current_zone(); // 类型为const time_zone*
auto tzUTC = std::chrono::locate_zone("UTC"); // 类型为const time_zone*
...
std::cout << tzHere->name() << '\n';`<br>`std::cout << tzUTC->name() << '\n';
2
3
4
输出结果取决于你当前所在的时区。对于我在德国的情况,输出如下:
Europe/Berlin
Etc/UTC
2
如你所见,你可以在时区数据库中搜索任何条目(例如,“UTC”),但你得到的是其规范条目。例如,“UTC”只是指向“Etc/UTC”的时区链接。如果locate_zone()
找不到具有该名称的相应条目,将抛出std::runtime_error
异常。
你对time_zone
能做的操作有限。最重要的是将它与系统时间点或本地时间点相结合。
如果你输出一个time_zone
,只会得到一些用于调试目的的特定于实现的输出:
std::cout << *tzHere << '\n'; // 一些特定于实现的调试输出
chrono库还允许你定义和使用自定义时区类型。
# zoned_time类型
std::chrono::zoned_time
类型的对象将时间点应用于时区。你有两种进行这种转换的方式:
- 将系统时间点(属于系统时钟的时间点)应用于一个时区。在这种情况下,我们将一个事件的时间点转换为与另一个时区的本地时间同时发生的时间。
- 将本地时间点(属于伪时钟
local_t
的时间点)应用于一个时区。在这种情况下,我们将一个时间点作为本地时间应用到另一个时区。
此外,你可以通过使用另一个zoned_time
对象初始化一个新的zoned_time
对象,将一个zoned_time
的时间点转换到不同的时区。
例如,让我们安排在2021年9月底,每个办公室在当地时间18:00举办一场本地派对,同时在多个时区举办一场公司派对:
auto day = 2021y/9/chr::Friday[chr::last]; // 该月的最后一个星期五
chr::local_seconds tpOfficeParty{chr::local_days{day} - 6h};// 前一天的18:00
chr::sys_seconds tpCompanyParty{chr::sys_days{day} + 17h}; // 当天的17:00
std::cout << "Berlin Office and Company Party:\n";
std::cout << " " << chr::zoned_time{"Europe/Berlin", tpOfficeParty} << '\n';
std::cout << " " << chr::zoned_time{"Europe/Berlin", tpCompanyParty} << '\n';
std::cout << "New York Office and Company Party:\n";
std::cout << " " << chr::zoned_time{"America/New_York", tpOfficeParty} << '\n';
std::cout << " " << chr::zoned_time{"America/New_York", tpCompanyParty} << '\n';
2
3
4
5
6
7
8
9
10
11
这段代码的输出如下:
Berlin Office and Company Party:
2021-09-23 18:00:00 CEST
2021-09-24 19:00:00 CEST
New York Office and Company Party:
2021-09-23 18:00:00 EDT
2021-09-24 13:00:00 EDT
2
3
4
5
6
在组合时间点和时区时,细节很重要。例如,考虑以下代码:
auto sysTp = chr::floor<chr::seconds>(chr::system_clock::now()); //系统时间点
auto locTime = chr::zoned_time{chr::current_zone(), sysTp}; // 本地时间
...
std::cout << "sysTp : " << sysTp << '\n';
std::cout << "locTime : " << locTime << '\n';
2
3
4
5
首先,我们将sysTp
初始化为当前系统时间点(以秒为单位),并将这个时间点与当前时区相结合。输出显示了同一时间点的系统时间和本地时间:
sysTp: 2021-04-13 13:40:02
locTime: 2021-04-13 15:40:02 CEST
2
现在让我们初始化一个本地时间点。一种方法是将系统时间点转换为本地时间点。为此,我们需要一个时区。如果我们使用当前时区,本地时间将转换为UTC:
auto sysTp = chr::floor<chr::seconds>(chr::system_clock::now()); //系统时间点
auto curTp = chr::current_zone()->to_local(sysTp); // 本地时间点
std::cout << "sysTp : " << sysTp << '\n';
std::cout << "locTp : " << locTp << '\n';
2
3
4
相应的输出如下:
sysTp: 2021-04-13 13:40:02
curTp: 2021-04-13 13:40:02
2
然而,如果我们使用UTC作为时区,本地时间将作为没有关联时区的本地时间使用:
auto sysTp = chr::floor<chr::seconds>(chr::system_clock::now()); //系统时间点
auto locTp = std::chrono::locate_zone("UTC")->to_local(sysTp); // 按原样使用本地时间
std::cout << "sysTp : " << sysTp << '\n';
std::cout << "locTp : " << locTp << '\n';
2
3
4
根据输出,两个时间点看起来相同:
sysTp: 2021-04-13 13:40:02
locTp: 2021-04-13 13:40:02
2
然而,它们并不相同。sysTp
关联了UTC纪元,而locTp
没有。如果我们现在将本地时间点应用于一个时区,我们不是进行转换;而是指定缺失的时区,保持时间不变:
auto timeFromSys = chr::zoned_time{chr::current_zone(), sysTp}; // 转换后的时间
auto timeFromLoc = chr::zoned_time{chr::current_zone(), locTp}; // 应用后的时间
std::cout << "timeFromSys : " << timeFromSys << '\n';
std::cout << "timeFromLoc : " << timeFromLoc << '\n';
2
3
4
因此,输出如下:
timeFromSys: 2021-04-13 15:40:02 CEST
timeFromLoc: 2021-04-13 13:40:02 CEST
2
现在让我们将所有四个对象与纽约的时区相结合:
std::cout << "NY sysTp : "
<< std::chrono::zoned_time{"America/New_York", sysTp} << '\n';
std::cout << "NY locTP : "
<< std::chrono::zoned_time{"America/New_York", locTp} << '\n';
std::cout << "NY timeFromSys : "
<< std::chrono::zoned_time{"America/New_York", timeFromSys} << '\n';
std::cout << "NY timeFromLoc : "
<< std::chrono::zoned_time{"America/New_York", timeFromLoc} << '\n';
2
3
4
5
6
7
8
输出如下:
NY sysTp: 2021-04-13 09:40:02 EDT
NY locTP: 2021-04-13 13:40:02 EDT
NY timeFromSys: 2021-04-13 09:40:02 EDT
NY timeFromLoc: 2021-04-13 07:40:02 EDT
2
3
4
系统时间点和从它派生的本地时间都将当前时间转换到纽约的时区。通常,本地时间点应用于纽约,所以我们现在得到的是去除时区后的原始时间值。timeFromLoc
是中欧最初的本地时间13:40:02应用到纽约时区后的时间。
# 11.6.4 处理时区缩写
由于时区缩写可能指代不同的时区,你无法通过缩写定义唯一的时区。相反,你必须将缩写映射到多个IANA时区条目中的一个。
以下程序展示了如何处理时区缩写CST:
// lib/chronocst.cpp
#include <iostream>
#include <chrono>
using namespace std::literals;
int main(int argc, char** argv) {
auto abbrev = argc > 1 ? argv[1] : "CST";
auto day = std::chrono::sys_days{2021y/1/1};
auto& db = std::chrono::get_tzdb();
// 打印所有具有该缩写的时区的时间和名称:
std::cout << std::chrono::zoned_time{"UTC", day}
<< " 映射到这些 ’" << abbrev << "’ 条目:\n";
// 遍历所有时区条目:
for (const auto& z : db.zones) {
// 并映射到使用我传入(或默认)缩写的条目:
if (z.get_info(day).abbrev == abbrev) {
std::chrono::zoned_time zt{&z, day};
std::cout << " " << zt << " " << z.name() << '\n';
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
该程序在不传递命令行参数或传递“CST”时,可能会有如下输出:
2021-01-01 00:00:00 UTC maps these ’CST’ entries:
2020-12-31 18:00:00 CST America/Bahia_Banderas
2020-12-31 18:00:00 CST America/Belize
2020-12-31 18:00:00 CST America/Chicago
2020-12-31 18:00:00 CST America/Costa_Rica
2020-12-31 18:00:00 CST America/El_Salvador
2020-12-31 18:00:00 CST America/Guatemala
2020-12-31 19:00:00 CST America/Havana
2020-12-31 18:00:00 CST America/Indiana/Knox
2020-12-31 18:00:00 CST America/Indiana/Tell_City
2020-12-31 18:00:00 CST America/Managua
2020-12-31 18:00:00 CST America/Matamoros
2020-12-31 18:00:00 CST America/Menominee
2020-12-31 18:00:00 CST America/Merida
2020-12-31 18:00:00 CST America/Mexico_City
...
2020-12-31 18:00:00 CST America/Winnipeg
2021-01-01 08:00:00 CST Asia/Macau
2021-01-01 08:00:00 CST Asia/Shanghai
2021-01-01 08:00:00 CST Asia/Taipei
2020-12-31 18:00:00 CST CST6CDT
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
由于CST可能代表中部标准时间、中国标准时间或古巴标准时间,你可以看到美国的大多数条目和中国的条目之间有14个小时的时差。此外,古巴的哈瓦那与这些条目有1或13个小时的时差。
请注意,当我们在夏季的某一天搜索“CST”时,输出会明显减少,因为美国的条目和古巴的条目那时会切换到“CDT”(相应的夏令时)。然而,我们仍然会有一些条目,例如,中国和哥斯达黎加没有夏令时。
还要注意,对于CST,你可能根本找不到条目,因为时区数据库不可用,或者使用类似GMT - 6的表示而不是CST。
# 11.6.5 自定义时区
chrono
库允许使用自定义时区。一个常见的例子是需要一个直到运行时才知道与协调世界时(UTC)偏移量的时区。
下面是一个示例,它提供了一个自定义时区OffsetZone
,可以精确到分钟来表示与UTC的偏移量:
// lib/offsetzone.hpp
#include <chrono>
#include <iostream>
#include <type_traits>
class OffsetZone {
// 感谢霍华德·欣南特(Howard Hinnant)提供此示例。
private:
std::chrono::minutes offset; // UTC偏移量
public:
explicit OffsetZone(std::chrono::minutes offs) : offset{offs} {
}
template<typename Duration>
auto to_local(std::chrono::sys_time<Duration> tp) const {
// 定义本地时间的辅助类型:
using LT = std::chrono::local_time<std::common_type_t<Duration,
std::chrono::minutes>>;
// 转换为本地时间:
return LT{(tp + offset).time_since_epoch()};
}
template<typename Duration>
auto to_sys(std::chrono::local_time<Duration> tp) const {
// 定义系统时间的辅助类型:
using ST = std::chrono::sys_time<std::common_type_t<Duration,
std::chrono::minutes>>;
// 转换为系统时间:
return ST{(tp - offset).time_since_epoch()};
}
template<typename Duration>
auto get_info(const std::chrono::sys_time<Duration>& tp) const {
return std::chrono::sys_info{};
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
如你所见,你只需要定义本地时间和系统时间之间的转换。你可以像使用任何其他时区指针一样使用这个时区:
// lib/offsetzone.cpp
#include "offsetzone.hpp"
#include <iostream>
int main() {
using namespace std::literals; // 用于h和min后缀
// 偏移3小时45分钟的时区:
OffsetZone p3_45{3h + 45min};
// 将当前时间转换到该偏移时区:
auto now = std::chrono::system_clock::now();
std::chrono::zoned_time<decltype(now)::duration, OffsetZone*> zt{&p3_45, now};
std::cout << "UTC : " << zt.get_sys_time() << "\n";
std::cout << "+3:45: " << zt.get_local_time() << "\n";
std::cout << zt << "\n";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
该程序可能会有如下输出:
UTC: 2021-05-31 13:01:19.0938339
+3:45: 2021-05-31 16:46:19.0938339
2
# 11.7 时钟详解
C++20现在支持多种时钟。本节将讨论它们之间的差异以及如何使用特殊时钟。
# 11.7.1 具有指定纪元的时钟
C++20现在提供了以下与纪元相关联的时钟(以便它们定义一个唯一的时间点):
- 系统时钟是操作系统的时钟。自C++20起,它被指定为Unix时间,从1970年1月1日00:00:00 UTC这个纪元开始计时。
闰秒的处理方式使得某些秒可能会稍微长一点。因此,永远不会出现有61秒的小时,并且所有365天的年份都有相同的31,536,000秒。
- UTC时钟表示协调世界时,通常称为格林威治标准时间(GMT,Greenwich Mean Time)或祖鲁时间(Zulu time)。本地时间与UTC的差异在于你所在时区的UTC偏移量。
它与系统时钟使用相同的纪元(1970年1月1日00:00:00 UTC)。
闰秒的处理方式使得某些分钟可能会有61秒。例如,存在时间点1972-06-30 23:59:60,因为在1972年6月的最后一分钟添加了一个闰秒。因此,365天的年份有时可能有31,536,001秒甚至31,536,002秒。
- GPS时钟使用全球定位系统的时间,这是由GPS地面控制站和卫星中的原子钟实现的原子时标。GPS时间从1980年1月6日00:00:00 UTC这个纪元开始。
每分钟有60秒,但GPS通过提前切换到下一小时来考虑闰秒。结果是,GPS时间比UTC越来越超前(或者在1980年之前越来越落后)。例如,时间点2021-01-01 00:00:00 UTC在GPS时间中表示为2021-01-01 00:00:18 GPS。在撰写本书的2021年,GPS时间点超前18秒。
所有365天的GPS年份(两个GPS日期午夜之间的差值)都有相同的31,536,000秒,但可能比“实际”年份短1到2秒。
- TAI时钟使用国际原子时(International Atomic Time),这是基于国际单位制秒(SI second)的连续计数的国际原子时标。TAI时间从1958年1月1日00:00:00 UTC这个纪元开始。
与GPS时间一样,每分钟有60秒,并且通过提前切换到下一小时来考虑闰秒。结果是,TAI比UTC越来越超前,但与GPS始终有19秒的固定偏移量。例如,时间点2021-01-01 00:00:00 UTC在TAI时间中表示为2021-01-01 00:00:37 TAI。在撰写本书的2021年,TAI时间点超前37秒。
关于Unix时间的详细信息,例如可查看http://en.wikipedia.org/wiki/Unix_time。
# 11.7.2 伪时钟local_t
如前所述,有一种特殊的时钟类型std::chrono::local_t
。这个时钟允许我们指定本地时间点,它还没有关联时区(甚至不是UTC)。它的纪元被解释为“本地时间”,这意味着你必须将它与一个时区结合起来,才能知道它代表的是哪个时间点。
local_t
是一个“伪时钟”,因为它并不满足时钟的所有要求。实际上,它没有提供成员函数now()
:
auto now1 = std::chrono::local_t::now(); // 错误:未提供now()
相反,你需要一个系统时钟时间点和一个时区。然后,你可以使用当前时区或UTC等时区将该时间点转换为本地时间点:
auto sysNow = chr::system_clock::now();// NOW作为UTC时间点
...
chr::local_time now2
= chr::current_zone()->to_local(sysNow); // NOW作为本地时间点
chr::local_time now3
= chr::locate_zone( "Asia/Tokyo ")->to_local(sysNow); // NOW作为东京时间点
2
3
4
5
6
另一种获得相同结果的方法是对一个带时区的时间(一个关联了时区的时间点)调用get_local_time()
:
chr::local_time now4 = chr::zoned_time{chr::current_zone(),
sysNow}.get_local_time();
2
还有一种不同的方法是将字符串解析为本地时间点:
chr::local_seconds tp; // time_point<local_t, seconds>
std::istringstream{ "2021-1-1 18:30 "} >> chr::parse(std::string{ "%F %R "}, tp);
2
记住本地时间点与其他时间点之间的细微差别:
- 系统/UTC/GPS/TAI时间点代表一个特定的时间点。将其应用于一个时区会转换它所代表的时间值。
- 本地时间点代表本地时间。一旦它与一个时区结合,其全球时间点就明确了。
例如:
auto now = chr::current_zone()->to_local(chr::system_clock::now());
std::cout << now << "\n";
std::cout << "Berlin : " << chr::zoned_time( "Europe/Berlin " , now) << "\n";
std::cout << "Sydney : " << chr::zoned_time( "Australia/Sydney " , now) << "\n";
std::cout << "Cairo : " << chr::zoned_time( "Africa/Cairo " , now) << "\n";
2
3
4
5
这将当前的本地时间应用于三个不同的时区:
2021-04-14 08:59:31.640004000
Berlin: 2021-04-14 08:59:31.640004000 CEST
Sydney: 2021-04-14 08:59:31.640004000 AEST
Cairo: 2021-04-14 08:59:31.640004000 EET
2
3
4
注意,在使用本地时间点时,你不能使用时区的转换说明符:
chr::local_seconds tp; // time_point<local_t, seconds>
...
std::cout << std::format( "{:%F %T %Z}\n " , tp); // 错误:无效格式
std::cout << std::format( "{:%F %T}\n " , tp); // 正确
2
3
4
# 11.7.3 处理闰秒
前面关于具有指定纪元的时钟的讨论已经介绍了处理闰秒的基本方面。
为了更清楚地说明闰秒的处理,让我们使用不同的时钟来迭代一个闰秒内的时间点:
// lib/chronoclocks.cpp
#include <iostream>
#include <chrono>
int main() {
using namespace std::literals;
namespace chr = std::chrono;
auto tpUtc = chr::clock_cast<chr::utc_clock>(chr::sys_days{2017y/1/1} - 1000ms);
for (auto end = tpUtc + 2500ms; tpUtc <= end; tpUtc += 200ms) {
auto tpSys = chr::clock_cast<chr::system_clock>(tpUtc);
auto tpGps = chr::clock_cast<chr::gps_clock>(tpUtc);
auto tpTai = chr::clock_cast<chr::tai_clock>(tpUtc);
std::cout << std::format( "{:%F %T} SYS " , tpSys);
std::cout << std::format( "{:%F %T %Z} " , tpUtc);
std::cout << std::format( "{:%F %T %Z} " , tpGps);
std::cout << std::format( "{:%F %T %Z}\n " , tpTai);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
该程序有如下输出:
2016-12-31 | 23:59:59.000 | SYS | 2016-12-31 | 23:59:59.000 | UTC | 2017-01-01 | 00:00:16.000 | GPS | 2017-01-01 | 00:00:35.000 | TAI |
---|---|---|---|---|---|---|---|---|---|---|---|
2016-12-31 | 23:59:59.200 | SYS | 2016-12-31 | 23:59:59.200 | UTC | 2017-01-01 | 00:00:16.200 | GPS | 2017-01-01 | 00:00:35.200 | TAI |
2016-12-31 | 23:59:59.400 | SYS | 2016-12-31 | 23:59:59.400 | UTC | 2017-01-01 | 00:00:16.400 | GPS | 2017-01-01 | 00:00:35.400 | TAI |
2016-12-31 | 23:59:59.600 | SYS | 2016-12-31 | 23:59:59.600 | UTC | 2017-01-01 | 00:00:16.600 | GPS | 2017-01-01 | 00:00:35.600 | TAI |
2016-12-31 | 23:59:59.800 | SYS | 2016-12-31 | 23:59:59.800 | UTC | 2017-01-01 | 00:00:16.800 | GPS | 2017-01-01 | 00:00:35.800 | TAI |
2016-12-31 | 23:59:59.999 | SYS | 2016-12-31 | 23:59:60.000 | UTC | 2017-01-01 | 00:00:17.000 | GPS | 2017-01-01 | 00:00:36.000 | TAI |
2016-12-31 | 23:59:59.999 | SYS | 2016-12-31 | 23:59:60.200 | UTC | 2017-01-01 | 00:00:17.200 | GPS | 2017-01-01 | 00:00:36.200 | TAI |
2016-12-31 | 23:59:59.999 | SYS | 2016-12-31 | 23:59:60.400 | UTC | 2017-01-01 | 00:00:17.400 | GPS | 2017-01-01 | 00:00:36.400 | TAI |
2016-12-31 | 23:59:59.999 | SYS | 2016-12-31 | 23:59:60.600 | UTC | 2017-01-01 | 00:00:17.600 | GPS | 2017-01-01 | 00:00:36.600 | TAI |
2016-12-31 | 23:59:59.999 | SYS | 2016-12-31 | 23:59:60.800 | UTC | 2017-01-01 | 00:00:17.800 | GPS | 2017-01-01 | 00:00:36.800 | TAI |
2017-01-01 | 00:00:00.000 | SYS | 2017-01-01 | 00:00:00.000 | UTC | 2017-01-01 | 00:00:18.000 | GPS | 2017-01-01 | 00:00:37.000 | TAI |
2017-01-01 | 00:00:00.200 | SYS | 2017-01-01 | 00:00:00.200 | UTC | 2017-01-01 | 00:00:18.200 | GPS | 2017-01-01 | 00:00:37.200 | TAI |
2017-01-01 | 00:00:00.400 | SYS | 2017-01-01 | 00:00:00.400 | UTC | 2017-01-01 | 00:00:18.400 | GPS | 2017-01-01 | 00:00:37.400 | TAI |
我们在这里查看的闰秒是本书撰写时的最后一个闰秒(我们无法提前知道未来何时会出现闰秒)。我们打印出时间点及其相应的时区(对于系统时间点,我们打印“SYS”而不是其默认的UTC时区)。你可以观察到以下几点:
- 在闰秒期间:
- UTC时间的秒数使用值60。
- 系统时钟使用插入闰秒之前
sys_time
的最后一个可表示值。这种行为是由C++标准保证的。
- 在闰秒之前:
- GPS时间比UTC时间提前17秒。
- TAI时间比UTC时间提前36秒(一如既往,比GPS时间提前19秒)。
- 在闰秒之后:
- GPS时间比UTC时间提前18秒。
- TAI时间比UTC时间提前37秒(仍然比GPS时间提前19秒)。
# 11.7.4 时钟间的转换
在转换合理的情况下,你可以在不同时钟的时间点之间进行转换。为此,<chrono>
库提供了clock_cast<>
。它的定义方式确保你只能在具有指定稳定纪元的时钟(sys_time<>
、utc_time<>
、gps_time<>
、tai_time<>
)的时间点以及文件系统时间点之间进行转换。
转换需要目标时钟,并且你可以选择传递不同的持续时间。
以下程序将一个协调世界时(UTC)闰秒的时间点转换为其他几个时钟的时间点并输出:
//lib/chronoconv.cpp
#include <iostream>
#include <sstream>
#include <chrono>
int main() {
namespace chr = std::chrono;
// 用一个闰秒初始化一个utc_time<>:
chr::utc_time<chr::utc_clock::duration> tp;
std::istringstream{ "2015-6-30 23:59:60 "}
>> chr::parse(std::string{ "%F %T "}, tp);
// 将其转换为其他时钟的时间点并输出:
auto tpUtc = chr::clock_cast<chr::utc_clock>(tp);
std::cout << "utc_time : " << std::format( "{:%F %T %Z} ", tpUtc) << "\n";
auto tpSys = chr::clock_cast<chr::system_clock>(tp);
std::cout << "sys_time : " << std::format( "{:%F %T %Z} ", tpSys) << "\n";
auto tpGps = chr::clock_cast<chr::gps_clock>(tp);
std::cout << "gps_time : " << std::format( "{:%F %T %Z} ", tpGps) << "\n";
auto tpTai = chr::clock_cast<chr::tai_clock>(tp);
std::cout << "tai_time : " << std::format( "{:%F %T %Z} ", tpTai) << "\n";
auto tpFile = chr::clock_cast<chr::file_clock>(tp);
std::cout << "file_time : " << std::format( "{:%F %T %Z} ", tpFile) << "\n";
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
该程序的输出如下:
utc_time: 2015-06-30 23:59:60.0000000 UTC
sys_time: 2015-06-30 23:59:59.9999999 UTC
gps_time: 2015-07-01 00:00:16.0000000 GPS
tai_time: 2015-07-01 00:00:35.0000000 TAI
file_time: 2015-06-30 23:59:59.9999999 UTC
2
3
4
5
请注意,对于所有这些时钟,我们在格式化输出时使用的是伪时区。转换规则如下:
- 从本地时间点进行的任何转换都只是添加纪元,时间值保持不变。
- UTC、GPS和TAI时间点之间的转换会加上或减去必要的偏移量。
- UTC和系统时间之间的转换不会改变时间值,但对于UTC闰秒的时间点,会使用前一个时间点作为系统时间。
从本地时间点到其他时钟的转换也受支持。不过,要转换为本地时间点,你必须先转换为系统时间点,然后使用to_local()
。不支持与steady_clock
的时间点进行相互转换。
# 时钟转换的内部机制
在底层,clock_cast<>
是一个“双中心辐射式系统” 。两个中心是system_clock
和utc_clock
。每个可转换的时钟都必须与其中一个中心(而非两个都)进行相互转换。与中心的转换是通过时钟的to_sys()
或to_utc()
静态成员函数完成的。从中心进行转换时,则提供from_sys()
或from_utc()
函数。clock_cast<>
将这些成员函数串联起来,实现从任意一个时钟到另一个时钟的转换。
无法处理闰秒的时钟应与system_clock
进行转换。例如,utc_clock
和local_t
提供了to_sys()
函数。
能够以某种方式处理闰秒(这并不一定意味着它们具有闰秒值)的时钟应与utc_clock
进行转换。这适用于gps_clock
和tai_clock
,因为即使GPS和TAI没有闰秒,但它们与UTC闰秒有唯一的双向映射关系。
对于file_clock
,是否提供与system_clock
或utc_clock
的转换取决于具体实现。
# 11.7.5 处理文件时钟
std::chrono::file_clock
时钟是文件系统库用于文件系统条目(文件、目录等)时间点的时钟。它是一种与实现相关的时钟类型,反映了文件系统时间值的分辨率和范围。
例如,你可以使用文件时钟来更新文件的最后访问时间,如下所示:
// 触碰路径为p的文件(更新文件的最后写入访问时间):
std::filesystem::last_write_time(p,
std::chrono::file_clock::now());
2
3
在C++17中,你也可以通过使用std::filesystem::file_time_type
类型来使用文件系统条目的时钟:
std::filesystem::last_write_time(p,
std::filesystem::file_time_type::clock::now());
2
从C++20开始,文件系统类型名file_time_type
定义如下:
namespace std::filesystem {
using file_time_type = chrono::time_point<chrono::file_clock>;
}
2
3
在C++17中,你只能使用未指定的普通时钟。
对于文件系统的时间点,现在也定义了file_time
类型:
namespace std::chrono {
template<typename Duration>
using file_time = time_point<file_clock, Duration>;
}
2
3
4
像file_seconds
(其他时钟有类似类型)这样的类型并没有定义。
现在file_time
类型的新定义使程序员能够可移植地将文件系统时间点转换为系统时间点。例如,你可以按如下方式打印传递文件的最后访问时间:
void printFileAccess(const std::filesystem::path& p) {
std::cout << "\"" << p.string() << "\":\n ";
auto tpFile = std::filesystem::last_write_time(p);
std::cout << std::format( " Last write access : {0:%F} {0:%X}\n ", tpFile);
auto diff = std::chrono::file_clock::now() - tpFile;
auto diffSecs = std::chrono::round<std::chrono::seconds>(diff);
std::cout << std::format( " It is {} old\n ", diffSecs);
}
2
3
4
5
6
7
8
这段代码可能输出:
"chronoclocks.cpp":
Last write access: 2021-07-12 16:50:08
It is 18s old
2
3
如果你使用时间点的默认输出运算符,它会根据文件时钟的粒度打印亚秒部分:
std::cout << " Last write access : " << diffSecs << "\n";
输出可能如下:
Last write access: 2021-07-12 16:50:08.3680536
要将文件访问时间作为系统时间或本地时间处理,你必须使用clock_cast<>()
(在内部,它可能会调用file_clock
的静态成员函数to_sys()
或to_utc()
)。例如:
auto tpFile = std::filesystem::last_write_time(p);
auto tpSys = std::chrono::file_clock::to_sys(tpFile);
auto tpSys = std::chrono::clock_cast<std::chrono::system_clock>(tpFile);
2
3
# 11.8 其他新的chrono特性
除了我目前描述的内容之外,新的<chrono>
库还添加了以下特性:
- 为了检查一个类型是否是时钟,提供了一个新的类型特征
std::chrono::is_clock<>
以及其对应的变量模板std::chrono::is_clock_v<>
。例如:
std::chrono::is_clock_v<std::chrono::system_clock> // true
std::chrono::is_clock_v<std::chrono::local_t> // false
2
伪时钟local_t
在这里返回false
,因为它没有提供成员函数now()
。
- 为持续时间和时间点定义了
operator<=>
。
# 11.9 补充说明
<chrono>
库由Howard Hinnant开发。当C++11将持续时间和时间点的基本部分标准化时,就已经计划对其进行扩展,增加对日期、日历和时区的支持。
C++20对<chrono>
库的扩展最初由Howard Hinnant在http://wg21.link/p0355r0 (opens new window)中提出。该扩展最终被接受的表述由Howard Hinnant和Tomasz Kaminski在http://wg21.link/p0355r7 (opens new window)中制定。
Howard Hinnant在http://wg21.link/p1466r3 (opens new window)和Tomasz Kaminski在http://wg21.link/p1650r0 (opens new window)中添加了一些小的修正。<chrono>
库与格式化库集成的最终被接受的表述由Victor Zverovich、Daniela Engert和Howard Hinnant在http://wg21.link/p1361r2 (opens new window)中制定。
在完成C++20之后,对C++20标准中<chrono>
库的扩展进行了一些修正:
- http://wg21.link/p2372 (opens new window)阐明了与区域设置相关的格式化输出的行为。
- http://wg21.link/lwg3554 (opens new window)确保你可以将字符串字面量作为格式传递给
parse()
。