时间与日历
Turbo time 库包含了用于表示时间值的抽象,包括 绝对时间 和 民用时间。该时间库由以下组件组成:
time.h:包含绝对时间(及持续时间)的抽象,以及用于构造、解析和转换这些类型的辅助函数。此外,该头文件还支持时区,使你可以在绝对时间和民用时间之间进行映射。civil_time.h:包含民用时间的抽象,以及用于构造、解析和转换这些类型的辅助函数。clock.h:包含使用系统时钟创建时间对象的工具函数。
这些抽象和工具函数在下文中进行了说明。
基本概念
表示时间有两种方式:绝对时间 和 民用时间。绝对时间可以唯一且全局地表示某一特定时刻。每个事件都发生在特定的绝对时间上,世界上的每个人都同意事件发生的绝对时间。time_t 是一个众所周知的绝对时间类型。想象尼尔·阿姆斯特朗首次踏上月球的瞬间。他只发生过一次。一个小时后,他不会再为西部时区的观众重复此刻。世界上所有人都同意该事件发生的 绝对时间。
另一方面,并非每个人都同意这个“民用时间”。民用时间表示为“六个独立字段”,分别表示年、月、日、小时、分钟和秒(即 YMDHMS)。这六个字段表示某些地方政府定义的时间。你的民用时间与挂在 Dilbert 墙上的时钟和你桌上的日历显示的值一致。你住在全国另一端的朋友,在同一时刻看到的民用时间可能不同。
例如,如果你住在纽约,你在 1969 年 7 月 20 日晚上 10:56 目睹了尼尔·阿姆斯特朗的小步,而你的旧金山朋友在同一时刻看到的是下午 7:56,你的悉尼笔友看到的是 1969 年 7 月 21 日中午 12:56。你们都同意事件的绝对时间,但民用时间不同。
时区是一个地缘政治区域,在该区域内,绝对时间和民用时间之间的转换规则是共享的。时区的地理特征可以从其标识符中看出,例如 America/New_York、America/Los_Angeles 和 Australia/Sydney。时区规则包括该区域相对于 UTC 标准的偏移、夏令时调整以及缩写字符串。由于这些规则可能随地方政府的决定而变化,历史上时区在特定时期可能有不同规则。时区非常复杂,因此应始终让时间库为你执行时区计算。
时区定义了绝对时间与民用时间之间的关系。给定一个绝对时间或民用时间以及时区,你可以计算出另一个,如下所示及图 1。
民用时间 = F(绝对时间, 时区)
绝对时间 = F'(民用时间, 时区)
到目前为止描述的概念——绝对时间、民用时间和时区——是通用概念,适用于 所有编程语言,因为它们描述的是现实世界中的时间。不同编程语言和库可能使用不同的类甚至不同的名称来建模这些概念,但这些基本概念和关系保持不变。
turbo::Time 构造
Turbo 时间库包含几个核心类,它们映射到上述概念:
- 绝对时间由
turbo::Time类表示。(见 time.h)这是一个小型的类整数类型,应按值传递,并使用算术运算符和描述性函数进行计算。turbo::Time可以通过转换函数与其他绝对时间表示进行相互转换,并在turbo::TimeZone的帮助下与民用时间表示进行相互转换。 - 民用时间由六个独立整数表示,指定
turbo::Civil*对象的年、月、日、小时、分钟和秒,应按值传递。(见 civil_time.h)这些整数值可以作为民用时间构造函数(如turbo::CivilYear或turbo::CivilSecond)的参数,或从格式化时间字符串中解析。 - 时区由
turbo::TimeZone类表示。(见 time.h)这是一个大部分不透明的值类型,通过值传递给其他 Turbo 时间函数,这些函数随后执行必要的绝对时间与民用时间转换。Turbo 时间库本身会处理所有时区运算,从而几乎消除了代码中的偏移计算错误。
一个尚未讨论的重要辅助概念是固定长度的时间间隔,由 turbo::Duration 类表示。这是一个小型的类整数类型,应按值传递,并使用普通整数算术运算符进行计算。Turbo 时间库包含多个函数用于与其他持续时间类型相互转换以及执行浮点运算。持续时间是单位安全的,非常适合用于接受超时或其他固定长度时间间隔的接口。
图 2 展示了主要的 Turbo Time 类型,以及它们如何映射到前面描述的基本时间概念。
 图 2
使用 turbo::Time 的绝对时间
turbo::Time 表示某一特定时刻,以某个起点(纪元)为基础的某种粒度(分辨率)下的时钟计数。turbo::Time 应按值传递,而非按 const 引用传递。提供了算术运算符,以便自然地表达时间计算。
它适用于大多数可用的时间尺度(即精度至少为一纳秒,范围为 +/- 1000 亿年)。时间尺度之间的转换通过向下取整(向负无穷方向)到最接近的可表示点进行。
turbo::Time 假设一分钟为 60 秒,这意味着底层时间尺度必须“平滑”以处理闰秒。详见 https://developers.google.com/time/smear。
构造 turbo::Time
可以直接构造 turbo::Time 实例:
// 使用系统时钟构造 turbo::Time。(见 clock.h)
turbo::Time t1 = turbo::Time::current_time();
// 默认构造产生 UNIX 纪元的绝对时间
turbo::Time t2 = turbo::Time();
// 为了清晰,建议直接构造此类时间
turbo::Time t3 = turbo::Time::unix_epoch();
turbo::Time 值通常从其他值类型创建。Turbo 时间库包含众多 turbo::Time::from_*() 工厂函数,可接受其他时间表示。
// 从 time_t 构造 turbo::Time。time() 返回自 UNIX 纪元以来的秒数。
time_t tt = time(NULL);
turbo::Time t1 = turbo::Time::from_time_t(tt);
// 从 std::chrono 时间构造 turbo::Time
auto tp = std::chrono::system_clock::from_time_t(123);
turbo::Time t2 = turbo::Time::from_chrono(tp);
// 使用转换函数构造 turbo::Time。(假设 MyCustomTime() 返回自 UNIX 纪元以来的微秒数)
int64_t unix_micros = MyCustomTime();
turbo::Time t3 = turbo::Time::from_unix_micros(unix_micros);
// 从民用时间和时区构造 turbo::Time
// (见下文这些类型的说明)
turbo::TimeZone nyc;
// TimeZone::load 可能失败,因此最好检查是否成功
if (!turbo::TimeZone::load("America/New_York", &nyc)) {
// 处理错误
}
// 我的航班在 2017 年 1 月 2 日 03:04:05 从 NYC 出发
turbo::CivilSecond ct(2017, 1, 2, 3, 4, 5);
turbo::Time takeoff = turbo::Time::from_civil(ct, nyc);
turbo::Time 可以获取未来或过去的时间。Turbo 时间库包含众多 turbo::Time::future_*() 工厂函数,可接受其他时间表示。
static constexpr Time future_infinite();
static Time future_time(Duration d);
static int64_t future_nanoseconds(int64_t ns);
static int64_t future_microseconds(int64_t us);
static int64_t future_milliseconds(int64_t ms);
static int64_t future_seconds(int64_t s);
static constexpr Time past_infinite();
static Time past_time(Duration d);
static int64_t past_nanoseconds(int64_t ns);
static int64_t past_microseconds(int64_t us);
static int64_t past_milliseconds(int64_t ms);
static int64_t past_seconds(int64_t s);
通过 turbo::Time 获取当前的纳秒、微秒:
static Time current_time();
static int64_t current_nanoseconds();
static T current_microseconds();
static T current_milliseconds();
static T current_seconds();
格式化 turbo::Time
提供了格式化和解析函数,用于在字符串与时间之间进行转换。FormatTime() 可以接受一个绝对时间和一个时区,并返回表示该时间的字符串。(见下文 时区 说明。)
// 使用系统时钟构造 turbo::Time。
turbo::Time t1 = turbo::Time::current_time();
// 格式化时间时,应传入一个时区。
turbo::TimeZone utc = turbo::UTCTimeZone();
std::cout << turbo::Time::format_time(t1, utc);
// 输出示例: "2018-08-06T23:35:32.637472794+00:00"
// turbo::Time 的流操作符使用 turbo::Time::format_time(),默认使用
// turbo::TimeZone::local()。建议直接使用显式指定时区的 turbo::Time::format_time(),如上所示。
std::cout << t1 << "\n";
// 输出示例: "2018-08-06T17:35:32.637472794-04:00"
时间持续量
turbo::Duration 表示有符号的固定长度时间间隔。Duration 可以通过单位特定的工厂函数生成,或者由两个 turbo::Time 相减得到。
持续量行为类似单位安全的整数,支持所有自然的整数算术运算。算术溢出时将饱和到 +/- 无限大。Duration 应按值传递,而非按 const 引用。
工厂函数 Duration::nanoseconds()、Duration::microseconds()、Duration::milliseconds()、Duration::seconds()、Duration::minutes()、Duration::hours() 和 Duration::infinite_duration() 可用于创建 constexpr 的 Duration 值。
constexpr turbo::Duration ten_ns = turbo::Duration::nanoseconds(10);
constexpr turbo::Duration min = turbo::Duration::minutes(1);
constexpr turbo::Duration hour = turbo::Duration::hours(1);
turbo::Duration dur = 60 * min; // dur == hour
turbo::Duration half_sec = turbo::Duration::milliseconds(500);
turbo::Duration quarter_sec = 0.25 * turbo::Duration::seconds(1);
Duration 值可以通过除法运算轻松转换为整数单位。
constexpr turbo::Duration dur = turbo::Duration::milliseconds(1500);
int64_t ns = dur / turbo::Duration::nanoseconds(1); // ns == 1500000000
int64_t ms = dur / turbo::Duration::milliseconds(1); // ms == 1500
int64_t sec = dur / turbo::Duration::seconds(1); // sec == 1 (小数部分被截断)
int64_t min = dur / turbo::Duration::minutes(1); // min == 0
此外,Turbo 时间库提供了将持续量值转换为 int64_t 或 double 的辅助函数:
Duration::to_int64_nanoseconds()和Duration::to_double_nanoseconds()Duration::to_int64_microseconds()和Duration::to_double_microseconds()Duration::to_int64_milliseconds()和Duration::to_double_milliseconds()Duration::to_int64_seconds()和Duration::to_double_seconds()Duration::to_int64_minutes()和Duration::to_double_minutes()Duration::to_int64_hours()和Duration::to_double_hours()
turbo::Duration d = turbo::Duration::milliseconds(1500);
int64_t isec = turbo::Duration::to_int64_seconds(d); // isec == 1
turbo::Duration d = turbo::Duration::milliseconds(1500);
double dsec = turbo::Duration::to_double_seconds(d); // dsec == 1.5
民用时间
“民用时间”指法律上认可的人类尺度时间,由六个字段表示:“YYYY-MM-DD hh:mm:ss”。 “日期”可能是民用时间最常见的例子。现代民用时间遵循公历,并且与时区无关:例如,“2016-06-01 12:00:00”的民用时间不依赖于时区。换句话说,民用时间不会映射到唯一的绝对时间;民用时间必须通过时区映射到绝对时间。
由于民用时间是大多数人理解的“时间”,绝对时间通常会映射到民用时间以供用户显示。Turbo 时间库允许从绝对时间构造民用时间;相关函数见 time.h。
该库提供了六个类用于构造民用时间对象,同时提供若干辅助函数用于四舍五入、迭代及在民用时间对象上执行算术运算,同时避免夏令时(DST)等复杂性:
turbo::CivilSecondturbo::CivilMinuteturbo::CivilHourturbo::CivilDayturbo::CivilMonthturbo::CivilYear
建议按值传递这些 turbo::Civil* 类型,而非按 const 引用。
每种民用时间类型都是简单的值类型,具有相同的构造接口和六个字段的访问器(年、月、日、小时、分钟、秒,即 YMDHMS)。 各类仅在对齐方式上不同,对齐方式由类型名指示,并指定算术操作作用的字段。
民用时间构造
每种民用时间类型可以通过两种方式构造:
- 直接向构造函数传入最多六个整数,表示 YMDHMS 字段;
- 从对齐方式不同的民用时间类型复制 YMDHMS 字段。
省略的字段将被赋予最小有效值。小时、分钟、秒设为 0,月和日设为 1。由于没有最小年份,默认为 1970。
turbo::CivilDay default_value; // 1970-01-01 00:00:00
// 构造特定日期的民用时间对象
turbo::CivilDay a(2015, 2, 3); // 2015-02-03 00:00:00
turbo::CivilDay b(2015, 2, 3, 4, 5, 6); // 2015-02-03 04:05:06
turbo::CivilDay c(2015); // 2015-01-01 00:00:00
// 构造特定秒的民用时间对象
turbo::CivilSecond ss(2015, 2, 3, 4, 5, 6); // 2015-02-03 04:05:06
turbo::CivilMinute mm(ss); // 2015-02-03 04:05:00
turbo::CivilHour hh(mm); // 2015-02-03 04:00:00
turbo::CivilDay d(hh); // 2015-02-03 00:00:00
turbo::CivilMonth m(d); // 2015-02-01 00:00:00
turbo::CivilYear y(m); // 2015-01-01 00:00:00
m = turbo::CivilMonth(y); // 2015-01-01 00:00:00
d = turbo::CivilDay(m); // 2015-01-01 00:00:00
hh = turbo::CivilHour(d); // 2015-01-01 00:00:00
mm = turbo::CivilMinute(hh); // 2015-01-01 00:00:00
ss = turbo::CivilSecond(mm); // 2015-01-01 00:00:00
超出范围的字段会被归一化(例如 10 月 32 日 -> 11 月 1 日),确保所有民用时间对象表示有效值。详情见 归一化。
民用时间对齐
每个民用时间类根据类名中指示的字段进行对齐。对齐通过将所有下属字段设为最小有效值完成(如上所述)。 下例展示六种类型如何对齐 2015 年 11 月 22 日 12:34:56 的时间字段。(注:这里的字符串格式仅作为 YMDHMS 字段的简写示例。)
turbo::CivilSecond : 2015-11-22 12:34:56
turbo::CivilMinute : 2015-11-22 12:34:00
turbo::CivilHour : 2015-11-22 12:00:00
turbo::CivilDay : 2015-11-22 00:00:00
turbo::CivilMonth : 2015-11-01 00:00:00
turbo::CivilYear : 2015-01-01 00:00:00
每种民用时间类型对其指定的字段执行算术运算。这意味着给 turbo::CivilDay 加 1 会增加天字段(必要时归一化),而从 turbo::CivilMonth 减去 7 会操作月字段(必要时归一化)。所有算术运算均产生有效的民用时间。
差值运算符接受两个对齐方式相似的民用时间对象,并返回按对象对齐单位的标量结果。例如,两个 turbo::CivilHour 对象的差值将以民用小时为单位。
民用时间转换
民用时间对象的对齐方式不可更改,但可以用于构造新的不同对齐的对象。
这称为“重新对齐”。当重新对齐到相同或更高精度的类型(如 turbo::CivilDay -> turbo::CivilSecond)时,可以隐式转换,因为不会丢失信息。然而,如果可能丢失信息(如 turbo::CivilSecond -> turbo::CivilDay),则必须在调用处显式转换。
void UseDay(turbo::CivilDay day);
turbo::CivilSecond cs;
UseDay(cs); // 编译失败,可能丢失数据
UseDay(turbo::CivilDay(cs)); // OK: 显式转换
turbo::CivilDay cd;
UseDay(cd); // OK: 无需转换
turbo::CivilMonth cm;
UseDay(cm); // OK: 隐式转换为 turbo::CivilDay
民用时间归一化
归一化会将无效值调整为有效值。在民用时间库中,传递给 Civil* 构造函数的整数参数可能超出范围,这时会通过进位到更粗粒度的字段生成有效的民用时间对象。归一化允许对构造函数参数进行自然算术运算,而无需担心字段范围。
// 超出范围;归一化为 2016-11-01
turbo::CivilDay d(2016, 10, 32);
// 超出范围,负数;归一化为 2016-10-30T23
turbo::CivilHour h1(2016, 10, 31, -1);
// 归一化是累积的;归一化为 2016-10-30T23
turbo::CivilHour h2(2016, 10, 32, -25);
民用时间比较
民用时间对象的比较会考虑所有六个 YMDHMS 字段,而不管类型的对齐方式如何。不同对齐方式的民用时间类型之间也允许比较。
turbo::CivilDay feb_3(2015, 2, 3); // 2015-02-03 00:00:00
turbo::CivilDay mar_4(2015, 3, 4); // 2015-03-04 00:00:00
// feb_3 < mar_4
// turbo::CivilYear(feb_3) == turbo::CivilYear(mar_4)
turbo::CivilSecond feb_3_noon(2015, 2, 3, 12, 0, 0); // 2015-02-03 12:00:00
// feb_3 < feb_3_noon
// feb_3 == turbo::CivilDay(feb_3_noon)
// 遍历 2015 年 2 月的所有天
for (turbo::CivilDay d(2015, 2, 1); d < turbo::CivilMonth(2015, 3); ++d) {
// ...
}
民用时间算术
民用时间类型支持自然算术运算符,如加法、减法和求差。算术运算在类型名指示的民用时间字段上进行。差值运算符要求参数具有相同对齐方式,返回值单位为该对齐字段单位。
turbo::CivilDay a(2015, 2, 3);
++a; // 2015-02-04 00:00:00
--a; // 2015-02-03 00:00:00
turbo::CivilDay b = a + 1; // 2015-02-04 00:00:00
turbo::CivilDay c = 1 + b; // 2015-02-05 00:00:00
int n = c - a; // n = 2 (民用天数)
int m = c - turbo::CivilMonth(c); // 编译失败:类型不同
民用时间访问器
每个民用时间类型都提供访问所有六个字段的方法:
year()返回civil_year_tmonth()返回intday()返回inthour()返回intminute()返回intsecond()返回int
注意,低于类型对齐字段的值会被设为最小有效值。
turbo::CivilDay d(2015, 6, 28);
// d.year() == 2015
// d.month() == 6
// d.day() == 28
// d.hour() == 0
// d.minute() == 0
// d.second() == 0
时区
turbo::TimeZone 是一个小型值类型,表示地缘政治区域,其中使用特定规则在绝对时间和民用时间之间转换。
turbo::TimeZone 值使用 IANA 时区数据库中的 TZ 标识符命名,如 "America/Los_Angeles" 或 "Australia/Sydney",可通过 turbo::TimeZone::load() 加载。
turbo::TimeZone lax;
// 加载时区可能失败,建议检查返回值
if (!turbo::TimeZone::load("America/Los_Angeles", &lax)) {
// 处理失败
}
建议按值传递 turbo::TimeZone 而非按 const 引用。
Turbo 时间库还提供几个便捷函数用于构造时区:
turbo::UTCTimeZone()turbo::FixedTimeZone(offset)以 UTC 偏移构造时区(尽量避免,适用于旧 API)。turbo::LocalTimeZone()构造本地时区(未知时默认为 UTC)。建议使用显式时区名。
turbo::TimeZone utc = turbo::TimeZone::utc();
turbo::TimeZone pst = turbo::FixedTimeZone(-8 * 60 * 60);
turbo::TimeZone loc = turbo::LocalTimeZone();
绝对时间与民用时间转换
turbo::TimeZone 用于在 turbo::Time 与民用时间之间转换。
可以使用 turbo::TimeZone::At() 进行转换,也有辅助函数直接转换。
绝对时间 -> 民用时间
turbo::Time t = ...;
turbo::TimeZone tz = ...;
const turbo::CivilDay cd = turbo::ToCivilDay(t, tz);
TimeZone::at(Time) 提供更多转换信息,包括子秒和 UTC 偏移。如果时间是无限值,会返回最大或最小的民用秒,并且子秒为无限。
turbo::TimeZone lax;
turbo::TimeZone::load("America/Los_Angeles", &lax);
const turbo::TimeZone::CivilInfo epoch_info = lax.at(turbo::Time::unix_epoch());
// epoch_info.cs == 1969-12-31 16:00:00
// epoch_info.subsecond == turbo::ZeroDuration()
// epoch_info.offset == -28800
// epoch_info.is_dst == false
// epoch_info.abbr == "PST"
民用时间 -> 绝对时间
转换民用时间到绝对时间时需小心,因为某些民用时间可能不存在或不唯一(如夏令时跳过或重复的时间)。
-
FromCivil()将任何对齐的民用时间转换为绝对时间: -
唯一时间 -> 返回该时间
-
重复时间 -> 返回较早时间
-
跳过时间 -> 返回过渡时间
turbo::CivilSecond cs = ...;
turbo::TimeZone tz = ...;
turbo::Time t = turbo::FromCivil(cs, tz);
TimeZone::at(CivilSecond) 提供完整转换信息:
pre:过渡前时间post:过渡后时间trans:过渡时刻kind:转换类型,值为UNIQUE、REPEATED或SKIPPED
示例:
// 唯一时间
auto jan01 = lax.At(turbo::CivilSecond(2011, 1, 1, 0, 0, 0));
// jan01.kind == UNIQUE
// 被跳过的时间(DST spring forward)
auto mar13 = lax.At(turbo::CivilSecond(2011, 3, 13, 2, 15, 0));
// mar13.kind == SKIPPED
// 被重复的时间(DST fall back)
auto nov06 = lax.At(turbo::CivilSecond(2011, 11, 6, 1, 15, 0));
// nov06.kind == REPEATED
正确的时间使用习惯
- 在系统内部及接口中尽量使用 Turbo 时间类型。
- 仅在系统边界进行类型转换。
- 不要手动使用时区偏移进行计算,让 Turbo 时间库处理。
- 使用显式时区名,避免“本地时间”假设。
- 尽量让代码与时区无关,如无法避免,推荐使用 UTC。
- 时间字符串包含偏移
%z,推荐 RFC3339/ISO8601 格式(如turbo::RFC3339_full)。 - 使用
turbo::Time::format_time格式化时间,turbo::Time::parse_time解析时间。