当前位置: 首页 > news >正文

CppCon 2015 学习:A C++14 Approach to Dates and Times

Big Picture — 日期库简介

  • 扩展 标准库
    这个库是对 C++ 标准库中 <chrono> 的自然延伸,专注于处理“日历”相关的功能(比如年月日、闰年、节假日等),而不仅仅是时间点和时长。
  • 极简设计
    它是**单头文件(header-only)**库,轻量且方便集成。
  • 功能有限,但基础扎实
    这个库不会帮你完成所有日期相关的复杂需求,但提供了高效且可靠的基础模块,你可以基于它自由构建自己的日期和日历功能。
    换句话说,这个库不是“全功能的日期解决方案”,而是给程序员提供了“积木块”,用来灵活地实现各种自定义的日期处理。

这部分是描述日期时间库(如 date.htz.h)在整个时间处理系统中的位置。以下是详细理解:

图示结构(从底层到上层)

hardware↑
OS↑
<chrono>   ← C++ 标准时间库(时长、时间点等)↑
"date.h"   ← 扩展 <chrono> 的日期处理(如年/月/日)
"tz.h"     ← 时区支持,使用 IANA tz 数据库↑
IANA tz database ← 提供全世界时区规则数据
NTP Server       ← 用于同步当前精确时间

各部分说明

部件作用
hardware提供系统时钟的硬件层
OS操作系统控制时间访问和管理
<chrono>C++ 标准时间库,处理时间点/持续时间
date.h更高层的日期处理(年/月/日等),对 <chrono> 的扩展
tz.h基于 IANA 时区数据库的时区支持
IANA tz database世界标准的时区定义数据库(例如夏令时规则)
NTP Server网络时间协议,用于获取当前标准时间

本讲内容集中在哪里?

  • 重点在 date.htz.h
    如何在 C++ 中用现代、高效、跨平台的方式处理日期和时区问题
    这为跨时区处理、日历计算、时间比较等功能提供了基础。

这一部分说明了这个日期库(如 date.h)在用户代码(client code)中的位置与使用方式。它表达的是这个库的架构理念和接口层次。理解如下:

分层结构

Client Code↑
Cute API         ← 简洁易用的高级接口(例如:"2025-06-03")↑
Type-Safe Objects ← 严格类型封装的日期/时间对象↑
Date Algorithms   ← 内部封装的日期计算逻辑

各层职责说明

层级描述
Client Code应用层开发者写的业务代码
Cute API高层语法糖接口,简洁好读(如 "2025y/6/3"
Type-Safe Objects比如 year, month, day, year_month_day 等强类型对象,避免误用
Date Algorithms底层封装的算法,比如判断闰年、月天数、日期加减等逻辑

核心理念

  • 你可以选择用:
    • 简洁、表达力强的 “Cute API” 来提高代码可读性
    • 或直接操作 类型安全对象(type-safe objects) 获得精细控制
  • 所有的算法实现都隐藏在类型安全对象里,比如 year_month_day 内部处理所有的闰年/月份/天数验证等,不需要你操心

举例说明

#include "date/date.h"
using namespace date;
using namespace std::chrono;
year_month_day ymd = 2025_y/6/3;     // Cute API
auto sys_days = sys_days(ymd);       // 转为系统时间
auto ymd2 = year_month_day{sys_days}; // 类型安全地再转回来

你既可以用 "2025_y/6/3" 这样的简洁表达,也可以用 year, month, day 构造更底层对象,这两种方式之间是可互换的。

这段是对日期库中一个核心日期算法的解析

constexpr int days_from_civil(int y, unsigned m, unsigned d) noexcept {y -= m <= 2;const Int era = (y >= 0 ? y : y - 399) / 400;const unsigned yoe = static_cast<unsigned>(y - era * 400);const unsigned doy = (153 * (m + (m > 2 ? -3 : 9)) + 2) / 5 + d - 1;const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;return era * 146097 + static_cast<Int>(doe) - 719468;
}

{year, month, day} 转换成“距某一固定日期的天数”(序列化天数)。这个过程叫做**“civil date → days”** 转换。

理解核心函数:days_from_civil(...)

函数作用:

将公历日期 {年, 月, 日} 转换为一个整数(表示相对某个“起点”的总天数)。
这个“起点”通常是:1970-01-01(UNIX epoch) 或 0000-03-01 或其他定义

参数说明:

constexpr int days_from_civil(int y, unsigned m, unsigned d)
  • y:年份
  • m:月份(1-12)
  • d:日(1-31)

主要计算过程(逐行解释):

y -= m <= 2;
  • 如果是1月或2月,就把年份减1(因为算法以3月为起始月,有助于统一闰年处理)
const int era = (y >= 0 ? y : y - 399) / 400;
  • 把年份分成以400年为周期的“纪元”,以处理格里历周期性(400年是格里历的一个周期)
const unsigned yoe = static_cast<unsigned>(y - era * 400);
  • yoe: year of era,即当前纪元内的年数(范围:0~399)
const unsigned doy = (153*(m + (m > 2 ? -3 : 9)) + 2)/5 + d - 1;
  • 计算年内的天数 day-of-year (doy),基于3月为起点的公式
  • 这样处理2月(闰年)的问题更简单、统一
const unsigned doe = yoe * 365 + yoe/4 - yoe/100 + doy;
  • doe: day-of-era,计算当前纪元开始以来的总天数
    • 包括平年天数 365 × yoe
    • 加上闰年天数 + yoe/4 - yoe/100(每4年1闰,百年不闰)
return era * 146097 + static_cast<Int>(doe) - 719468;
  • 结合“纪元”和“纪元内天数”计算出总天数
  • 146097 是 400 年内总天数:365×400 + 97(闰年)
  • 719468 是一个偏移量,用于将基准点转为 Unix epoch 或其他系统的起始点

应用场景

  • 快速比较两个日期之间的天数差
  • 实现 operator<operator- 之类的日期操作
  • std::chrono::sys_days 等类型背后的基础

你现在看到的是上一函数的“反操作”版本,也就是将序列化天数(自某固定日期起算)转换为 {year, month, day} 三元组的算法。这个函数名为:

constexpr std::tuple<int, unsigned, unsigned> civil_from_days(int z) noexcept {z += 719468;const Int era = (z >= 0 ? z : z - 146096) / 146097;const unsigned doe = static_cast<unsigned>(z - era * 146097);const unsigned yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;const Int y = static_cast<Int>(yoe) + era * 400;const unsigned doy = doe - (365 * yoe + yoe / 4 - yoe / 100);const unsigned mp = (5 * doy + 2) / 153;const unsigned d = doy - (153 * mp + 2) / 5 + 1;const unsigned m = mp + (mp < 10 ? 3 : -9);return std::tuple<Int, unsigned, unsigned>(y + (m <= 2), m, d);
}

civil_from_days(int z)

作用:

给定一个整数 z,表示自公历参考点(如 1970-01-01 或 0000-03-01)以来的天数,返回其对应的 {年, 月, 日}

这是 days_from_civil 的逆函数,两个函数可以互相还原。

参数:

  • z:序列化天数(整数)。例如,0 可能表示 1970-01-01(取决于基准)。

逐行解释:

z += 719468;
  • 把天数偏移到公历系统的某个统一参考点(通常对应于 0000-03-01
const Int era = (z >= 0 ? z : z - 146096) / 146097;
  • 计算是哪一个 400 年纪元(每纪元是 146097 天 = 365×400 + 97 闰年天)
const unsigned doe = static_cast<unsigned>(z - era * 146097);
  • 纪元内的天数 doe = day-of-era
const unsigned yoe = (doe - doe/1460 + doe/36524 - doe/146096) / 365;
  • 计算纪元内的年份 yoe = year-of-era(范围 0~399),包含闰年修正
    这是“逆推”的关键计算:从天数恢复年份
const Int y = static_cast<Int>(yoe) + era * 400;
  • 总年份 y 是当前纪元的年份加上纪元前的年份
const unsigned doy = doe - (365*yoe + yoe/4 - yoe/100);
  • 计算该年内的第几天(day-of-year)
const unsigned mp = (5*doy + 2)/153;
  • 将 day-of-year 映射为月段索引 mp,这里仍以3月为起点(使2月变成年末)
const unsigned d = doy - (153*mp+2)/5 + 1;
  • mp 反推具体的“日”
const unsigned m = mp + (mp < 10 ? 3 : -9);
  • mp 反推具体的“月”(注意它是基于3月起算的)
return std::tuple<Int, unsigned, unsigned>(y + (m <= 2), m, d);
  • 如果是1月或2月,年份要补回刚开始减掉的1年
  • 返回 {year, month, day}

days_from_civil 配套使用:

int days = days_from_civil(2025, 6, 3);
auto [y, m, d] = civil_from_days(days);  // 还原回 2025, 6, 3

应用场景:

  • 构造日历
  • 日期加减、间隔计算
  • std::chrono::sys_days → date 显示格式转换

日期算法的鲁棒性和覆盖范围非常强:

  • 所有提供的日期算法(如 days_from_civil()civil_from_days() 等)
    都经过了严格的单元测试
  • 测试范围覆盖了从现在往前和往后各一百万年,即总共约 200 万年的日期。

为什么这很重要?

  • 日历系统的规则(如闰年、纪元、历法转换)是复杂的;
  • 即使在极端日期范围(比如公元前999,999年),算法也仍然工作正确;
  • 表明这些算法非常可靠、健壮,甚至远超实际工程需求(实际我们只需几百年范围)。

实际意义:

  • 你可以放心地使用这些算法,不必担心日期出错;
  • 它们适用于历史研究、天文计算、未来预测等;
  • 对于需要精度和长期一致性的系统(如日历服务、时间戳系统等),特别有价值。

这部分讲的是 “类型安全的日期对象(Type-Safe Objects)”,是现代 C++ 日期库(如 date.h)中提供的一种 安全、清晰、结构化表示日期的方式

核心概念:类型安全的日期对象

这些类 不是简单的整数或字符串,而是具备语义的结构体,让日期处理更直观、安全、易于组合。

1. year_month_day

  • 典型的日期结构,表示一个明确的年月日。
  • 示例:
    year_month_day ymd{2025y, June, 3d}; // 2025-06-03
    

2. year_month_day_last

  • 表示某月的最后一天(无需手动判断是28/30/31日)。
  • 示例:
    year_month_day_last ymdl{2025y, month_day_last{June}};
    

3. year_month_weekday

  • 表示类似“2025年6月的第1个星期一”。
  • 示例:
    year_month_weekday{2025y, June, weekday_indexed{Monday, 1}};
    

4. year_month_weekday_last

  • 表示类似“2025年6月的最后一个星期五”。
  • 示例:
    year_month_weekday_last{2025y, June, weekday_last{Friday}};
    

5. weekday

  • 表示一周中的哪一天(Monday、Tuesday 等)。
  • 示例:
    weekday wd{2025, 6, 3}; // 会计算出星期几
    

6. day_point(或 sys_days

  • 时间轴上的绝对日期点(通常是自1970年1月1日起的某天数)。
  • 它是所有转换和运算的核心 —— 可以看作“中枢神经”。
  • 示例:
    sys_days dp = 2025y/6/3; // day_point,类似 time_point
    

总结:为什么这很重要?

  • intstring 表示日期更安全(不易混淆年月日顺序)
  • 支持丰富的日期计算与转换
  • chrono 完美集成,可参与时间运算
  • 代码更易读、维护性强

选对数据结构,比写对算法更重要

引用 Stepanov(STL 设计者)的一句话:

“选择不合适的数据结构,几乎是导致性能问题的最常见原因。”

在这个日期库中的应用:

这个日期库提供了 类型安全的数据结构(Type-Safe Objects),用于更清晰、更高效地处理日期和时间。

year_month_day

  • 表示一个具体的日历日期。
  • 数据结构如下:
struct year_month_day {year  y;month m;day   d;
};
  • 示例用法:
year_month_day ymd{2025y, June, 3d}; // 表示 2025-06-03

day_point(也叫 sys_days

  • 表示自某个固定日期(通常是1970-01-01)以来的天数。
  • 是日期计算的“底层格式”,适合数学运算。
  • 数据结构如下:
struct day_point {days count; // days 是 chrono 中的类型,表示“经过了多少天”
};
  • 示例:
sys_days dp = 2025y/6/3;  // 自动转换为 day_point(类似时间戳)

为什么要这样设计?

  • year_month_day语义清晰,易于展示、读写
  • day_point高效、适合计算(如相减得出间隔天数)
  • 二者可以互相转换 → 灵活组合,性能优良

鼓励你自己构建数据结构

这个库提供了基础组件,比如 yearmonthday,你也可以根据需要组合:

  • 构建 academic_calendar_date
  • 支持农历、节假日标记等
  • 构建用于金融系统的“交易日”结构

这部分讲的是 类型安全对象(Type-Safe Objects) 与日期算法(Date Algorithms)之间的关系,特别强调了:

类型转换 = 自动执行日期算法

内容总结:

“Type conversions execute date algorithms.”

—— 意思是说,在类型之间转换时,系统自动运行相应的日期算法,你不需要手动调用。

示例对象:year_month_dayday_point

year_month_day ymd{2025y, June, 3d};     // 日历格式
day_point dp = ymd;                      // 自动转换为 day_point(执行算法)
year_month_day back = dp;               // 再转回来(再次执行算法)
背后发生了什么?
  • ymd -> day_point:调用 days_from_civil(...)
  • day_point -> ymd:调用 civil_from_days(...)

所以关键点是:

  • 这些转换 看似简单,但背后是精准的算法(已经过百万年范围的单元测试验证)
  • C++ 的类型系统和重载机制 隐藏了算法的复杂性,给你一个“Cute API” —— 既好用又强大

Cute API 的好处

  • 不需要用户理解底层算法
  • 只需操作对象,就完成了日期推导与转换
  • 代码表达力强,错误率低

小结

对象类型说明
year_month_day人类友好格式,适合输入输出
day_point机器友好格式,适合运算存储
类型转换自动执行算法,结果可靠精确

这部分内容是在介绍 “Cute API” 如何结合 类型安全对象(Type-Safe Objects)重载运算符 实现简洁、直观的日期构造方式。

理解内容核心:

/ 运算符拼接字段(如 year、month、day)来构建完整日期对象。

这是一种 操作符重载(operator overloading) 技巧,让代码看起来更像人类写的日期,而不是传统结构体初始化。

具体结构

year / month / day   -->   year_month_day 类型

例如:

auto date = 2025y / June / 3d;  // 创建一个 year_month_day 对象
  • 2025yyear 类型的对象
  • June 是枚举类型 month
  • 3dday 类型的对象

上述表达式会被解释为:

year_month_day{year{2025}, month{6}, day{3}};

实现机制:

背后是对 / 运算符的重载,例如:

constexpr year_month operator/(const year& y, const month& m);
constexpr year_month_day operator/(const year_month& ym, const day& d);

所以 / 是组合构建器,逐步合并:

  1. year / monthyear_month
  2. year_month / dayyear_month_day

为什么称它为 “Cute API”?

  • 阅读性强:像写自然语言
  • 函数式风格:无副作用,组合清晰
  • 类型安全:编译器能检测非法组合(比如 year / day 不合法)

示例完整代码:

#include <date/date.h>
#include <iostream>
using namespace date;
int main() {auto d = 2025y / June / 3d;std::cout << d << '\n';  // 输出: 2025-06-03
}

总结:

概念说明
Cute API/ 运算符拼接字段创建日期对象
类型安全对象year, month, day, year_month_day 等
好处清晰、直观、强类型、安全、易读
背后机制operator/ 重载实现类型组合和构建

这部分讲的是 Cute API 如何与 类型安全对象(Type-Safe Objects)日期常量(如星期、月份、日) 一起工作,构建更丰富的日期表示,特别是像“某年3月的最后一个星期天”这样的复杂日期。

核心理解:

使用 运算符重载(/字面量(如 sun, last, mar, 2015y,构造一个表示特殊日期结构的对象:

sun[last] / mar / 2015

→ year_month_weekday_last

关键概念解释:

1. 类型 year_month_weekday_last

用于表示 “某年某月的最后一个星期几”,比如:

  • 2015年3月的最后一个星期天
    对应结构:
year_month_weekday_last{year{2015},month{3},weekday_last{Sunday}
}

2. 表达式解释:

sun[last] / mar / 2015y

等价于:

year_month_weekday_last{year{2015},month{3},weekday_last{weekday{0}}  // Sunday is typically 0
}

3. 运算符重载过程:

表达式由一连串 / 运算拼接组成:

步骤表达式结果类型
1sun[last]weekday_last
2weekday_last / marmonth_weekday_last
3month_weekday_last / 2015yyear_month_weekday_last

这些操作都依赖于重载的 / 运算符,像链式拼接一样。

示例代码:

#include <date/date.h>
#include <iostream>
using namespace date;
int main() {auto special_date = sun[last] / March / 2015y;std::cout << special_date << '\n';  // 输出: 2015-Mar-last-Sun
}

应用场景:

  • 日历系统中找“每月最后一个星期五”
  • 排程与提醒(如 DST 调整日、支付日)
  • 法律/金融文档中日期计算

总结:

项目描述
Cute API利用 / 运算符重载组合字段
year_month_weekday_last 类型表示“某年某月的最后一个星期几”
字面量:sun, last, mar, 2015y用于构造类型安全、编译时常量的表达式
表达式:sun[last]/mar/2015y转换为 year_month_weekday_last

这段内容对比了用 Cute API 和传统构造函数来创建日期对象的两种方式,重点说明了 Cute API 通过运算符重载使代码更简洁、易读,同时传统构造函数仍然可用。

核心理解:

1. Cute API(运算符重载组合字段)

auto d = sun[last] / mar / 2015;
  • 通过重载的 / 运算符,将各个部分(星期、月份、年份)组合成一个 year_month_weekday_last 类型。
  • 看起来像自然语言,更直观。
  • 这里的 sun[last] 是“最后一个星期天”的表达。

2. 传统构造函数语法

year_month_weekday_last(year(2015),month(3),weekday_last(weekday(0))  // Sunday 是 weekday 0
);
  • 直接用构造函数传入具体的类型和值。
  • 更显式但写起来更啰嗦。
  • 可用于不支持运算符重载的环境,或者需要更清晰的类型控制时。

3. 作用对比

方式优点备注
Cute API代码简洁,语义清晰,易读依赖运算符重载和字面量
传统构造函数明确、标准,兼容性好代码较繁琐

4. 总结

主要点
Cute API 是对传统构造函数的“语法糖”,让代码更漂亮
两者最终生成的是相同的 year_month_weekday_last 类型实例
开发者可以根据偏好和场景选择使用哪种方式

这部分内容讲的是 year_month_day 这个类型,它是日期库里的一个字段类型(field type),用来表示具体的日期(年、月、日)。重点是展示了:

核心理解:

1. 构造方式(Cute API)

auto ymd = 2015_y / sep / 25;
  • 利用运算符 / 重载,把年(2015_y)、月(sep)、日(25)组合成一个 year_month_day 类型实例。
  • 这种写法很自然,就像写日期一样。

2. 访问字段

assert(ymd.year() == 2015_y);
assert(ymd.month() == sep);
assert(ymd.day() == 25_d);
  • 可以通过 year()month()day() 方法分别访问对应的日期字段。
  • 这里的 2015_ysep25_d 是类型安全的字面量,保证类型和单位正确。

具体说明

元素说明
year_month_day表示年-月-日的类型安全日期结构
2015_yC++ 用户定义字面量,表示年份2015
sep表示九月(September),预定义的月份常量
25整数日数
25_d25天,带有类型的“日”字面量

总结

  • year_month_day 是日期的核心类型,代表具体的年月日。
  • Cute API 用 / 运算符拼接字面量生成实例,代码直观易读。
  • 可以通过 .year().month().day() 方法访问各个部分。

这段话强调了 year_month_day 类型的几个重要特性:

理解要点

1. 构造

constexpr auto ymd = 2015_y / sep / 25;
  • 使用了 constexpr,表示这个 year_month_day 对象在编译时就能被计算和确定。
  • 用 Cute API 语法构造日期,语义清晰。

2. 访问字段(编译期检查)

static_assert(ymd.year() == 2015_y, "");
static_assert(ymd.month() == sep, "");
static_assert(ymd.day() == 25_d, "");
  • static_assert 是编译时断言,保证 ymd 中的年、月、日字段值在编译时就正确。
  • 说明所有字段访问都支持 constexpr,即可以在编译期完成运算和验证。

3. 总结

  • year_month_day 支持完全的编译期操作,提高效率且能在编译阶段捕获错误。
  • 这种设计对需要高性能和安全性的程序特别有用。
  • 既能在编译期用作常量,也能在运行时使用,非常灵活。

这组内容详细介绍了 year_month_day 类型的用法和灵活性,重点讲了构造、算术运算、输入输出以及类型安全等。下面是详细的理解总结:

1. 构造 & 读写操作

constexpr auto ymd = 2015_y / sep / 25;
  • 利用重载的 / 运算符,可以轻松构造日期。
  • 支持 constexpr,编译时就能确定日期值。
cout << ymd << '\n';  // 输出 2015-09-25
  • 支持直接打印,格式化为常见的年月日形式。

2. 年月算术运算

auto next_month = ymd + months{1};
auto last_year  = ymd - years{1};
  • 支持用 months{}years{} 进行日期加减操作。
  • 这种算术是基于年月日的“字段”加减。
  • 如果要对天数进行操作,应该使用 day_point 类型,专门处理天数的加减。

3. 字段的灵活输入输出(I/O)

  • 每个字段(年、月、日)都能与整型互相转换,方便自定义输入输出。
  • 例如:
int y, m, d;
cin >> y >> m >> d;
auto ymd = year(y) / month(m) / day(d);
  • 只有第一个字段必须显式转换,后续字段可直接用整型值(比如 md):
auto ymd = year(y) / m / d;

4. 字段顺序灵活

  • 可以用多种顺序构造日期:
auto ymd1 = day(d) / m / y;
auto ymd2 = month(m) / d / y;
  • 只要第一个字段是明确的类型yearmonthday),其余字段为整数,解析就无歧义。

5. 类型安全编译期检查

auto ymd = y / month(m) / d; // 编译错误!
  • 不能混淆字段类型,比如整型 y 不能直接与 month 类型做 / 运算。
  • 这种错误会在编译阶段被捕获,保证类型安全。

总结

  • year_month_day 提供了灵活、类型安全的日期表示和操作。
  • 方便用多种方式输入日期,同时编译器能帮你防止字段顺序或类型的错误。
  • 支持方便的日期加减运算,支持打印输出。
  • 设计上既保证了易用性,也保证了安全性和正确性。

这个部分讲的是日期加减时遇到“无效日期”的处理问题,以及这套库是如何设计的。总结如下:

1. 无效日期问题示例

  • 比如计算:
auto ymd = 2015_y / jan / 31;
ymd += months{1};  // 2015-02-31 是无效日期
  • 这是个无效日期,因为2015年2月没有31号。
  • 有多种常见处理方案:
    • 自动调整到有效日期(如2015-02-28)
    • 抛出异常
    • 返回下一个有效日期(如2015-03-03)
  • 不同库、程序有不同的选择。

2. 本库的设计理念

  • 库本身不做“自动修正”或“自动抛异常”,它允许你创建无效日期。
  • 你必须自己选择何时检测和如何处理无效日期。
  • 这带来更高性能,因为库不做额外检查,给你自由和灵活性。
  • 你可以选择:
    • 使用 assert() 检测有效性。
    • 使用 ok() 函数检测日期是否有效。
    • 自己抛异常或者做其他逻辑处理。

3. 具体示例

断言检测:

ymd += months{1};           // ymd 变成了 2015-02-31(无效)
assert(ymd.ok());           // 如果无效,assert 会触发

抛异常示例:

auto result = ymd + months{1};
if (!result.ok()) {std::ostringstream os;os << ymd << " + " << months{1}.count() << " months results in " << result;throw std::domain_error(os.str());
}

自动调整到月底:

ymd += months{1};
if (!ymd.ok())ymd = ymd.year() / ymd.month() / last;  // 修正到该月最后一天,例如 2015-02-28

转换为 day_point 处理(不自动修正):

ymd += months{1};
if (!ymd.ok())ymd = day_point{ymd};  // 转成 day_point(天数计数),保持原值

4. 总结

  • 库只负责创建日期和提供 ok() 判断函数,不自动纠错或抛异常。
  • 这让你能写出更高效、可控的代码,决定在哪些地方要做有效性检测。
  • 编译期会捕获一些无效字段顺序等错误,运行时的日期有效性由你自己检测和处理。
  • 库本身不抛异常,但你的代码可以自由抛异常。
  • 这种设计更适合需要底层高性能和灵活性的场景。

这部分讲的是日期类型中的特殊标记 last,总结理解如下:

1. 什么是 last

  • last 是一个特殊的“日期”标记,表示某个月或某年某月的最后一天。
  • 在任何可以写“日(day)”的地方,都可以写 last
    例如:
auto ymd1 = 2015_y / feb / last;  // 2015年2月的最后一天,即2015-02-28
auto ymd2 = feb / last / 2015_y;  // 顺序不同,但语义相同
auto ymd3 = last / feb / 2015_y;  // 依然合法

2. 类型和API

  • 表达式结果是 year_month_day_last 类型,而不是普通的 year_month_day
  • year_month_day_last 的API几乎与 year_month_day 相同。
  • 它也可以隐式转换成 year_month_day(即转成具体某一天的日期)。

3. 使用场景示例

当日期无效时,可以用 last 修正到当月最后一天:

ymd += months{1};     // 例如 ymd 是 2015-01-31,加一个月变成 2015-02-31(无效)
if (!ymd.ok()) {ymd = ymd.year() / ymd.month() / last;  // 转成当月最后一天 2015-02-28
}

4. 总结

  • last 是库提供的一个语法糖,用于方便表达“某月的最后一天”。
  • 它简化了对无效日期的处理(比如月份天数不同)。
  • 让代码更直观,也方便避免无效日期错误。

这段内容介绍了“Indexed weekdays(带索引的星期几)”的用法,理解如下:

1. Indexed Weekdays 是什么?

  • 在任何能写“日(day)”的地方,也可以写“带索引的星期几”(比如“第4个星期五”)。
  • 语法形式是:weekday[index],例如 fri[4] 表示“第4个星期五”。

2. 示例

auto ymwd1 = 2015_y / sep / fri[4];   // 2015年9月的第4个星期五
auto ymwd2 = sep / fri[4] / 2015_y;   // 顺序不同,但含义相同
auto ymwd3 = fri[4] / sep / 2015_y;   // 依然合法
  • 这些表达式的类型是 year_month_weekday

3. 转换为具体日期(year_month_day)

  • 可以将 year_month_weekday 转成普通日期 year_month_day
year_month_day ymd{ymwd1};
cout << ymwd1 << '\n';  // 输出类似:2015/Sep/Fri[4]
cout << ymd << '\n';    // 输出具体日期:2015-09-25

4. 带索引的“最后一个星期几”

  • 也可以使用 last 作为索引,表示“某月最后一个指定星期几”:
auto ymwd_last = 2015_y / sep / fri[last];  // 2015年9月最后一个星期五
  • 类型为 year_month_weekday_last
  • 同样可以转换为普通日期:
year_month_day ymd{ymwd_last};
cout << ymwd_last << '\n'; // 输出 2015/Sep/Fri[last]
cout << ymd << '\n';       // 输出具体日期,比如 2015-09-25

5. 总结

  • Indexed weekdays 让你能方便表达“某月第N个星期几”或“某月最后一个星期几”。
  • 类型安全且语法灵活,支持多种字段顺序。
  • 可以轻松转换成具体的日期。

day_point 概念

  • day_point 是基于天的时间点(time_point),分辨率为“天”。
  • 它是 std::chrono::time_point 的一个类型别名,时间单位是天(days)。
  • 具体定义是:
using day_point = std::chrono::time_point<std::chrono::system_clock, days>;
  • 它表示自 Unix 纪元时间(1970-01-01)以来经过的天数。

示例说明

day_point dp = sep/25/2015;  // 将日期转换成 day_point 类型
cout << dp.time_since_epoch().count() << '\n';

输出:

16703
  • 表示自 1970-01-01 至 2015年9月25日,共经过了 16,703 天。

day_point 的优势

  • 支持高效的“基于天”的日期算术:
dp += days{2};  // 加2天
cout << dp.time_since_epoch().count() << '\n';  // 变成 16705
  • 这样操作比操作具体年月日字段更高效,也避免了日期边界问题。

总结

  • day_point 是以天为单位的时间点类型,基于 <chrono> 标准库。
  • 它是这套日期库的核心,负责处理日期的底层计数和计算。
  • 你可以用它做高效的日期加减等算术运算。

system_clock::time_point 转换为 day_point

  • day_point 是一个时间点,时间单位是天,属于 coarse duration(粗粒度时间)。
  • 要将 system_clock::time_point 转成 day_point,可以用:

1. 使用 time_point_cast

day_point dp = std::chrono::time_point_cast<days>(std::chrono::system_clock::now());
  • 缺点time_point_cast 是向零舍入(truncate toward zero),
  • 这对 1970 年之前的日期(负时间点) 会产生意料之外的错误结果。

2. 使用 floor

day_point dp = floor<days>(std::chrono::system_clock::now());
  • floor 类似 time_point_cast,但是是向负无穷舍入(round down),
  • 对负时间点(1970 年之前的时间)表现更正确。

示例

day_point dp = floor<days>(std::chrono::system_clock::now());
std::cout << dp.time_since_epoch().count() << '\n';

输出:

16703
  • 表示从 1970-01-01 到现在(示例日期)过去了 16703 天。

总结

  • 转换时推荐用 floor<days>(),避免负时间点舍入错误。
  • 这是标准库 <chrono> 里的时间点转换好方法。

day_point 和时间点(time_point)结合的日期时间操作

  • day_point 本质上是一个以天为单位的 std::chrono::time_point

示例说明

auto tp = day_point{jan/3/1970};
assert(tp.time_since_epoch() == days{2});
  • jan/3/1970 转为 day_point,是从1970-01-01起的第2天(因为1970-01-01是第0天)。

可以给 day_point 加上小时、分钟、秒

auto tp = day_point{jan/3/1970} + 7h;
assert(tp.time_since_epoch() == 55h);
auto tp2 = day_point{jan/3/1970} + 7h + 33min;
assert(tp2.time_since_epoch() == 3333min);
auto tp3 = day_point{jan/3/1970} + 7h + 33min + 20s;
assert(tp3.time_since_epoch() == 200000s);
  • 这里时间单位变得更细(小时、分钟、秒),time_since_epoch() 会以相应单位表示。

从带有时分秒的 time_point 中恢复日期(day_point)

auto dp = floor<days>(tp3);
assert(dp.time_since_epoch() == days{2});
  • 使用 floor<days> 截断时分秒,得到对应的日期(只到天的精度)。

获取当天时间(时分秒)

auto s = tp3 - dp;
assert(s == 27200s);  // 7h33m20s 转换成秒数
  • tp3 减去日期部分 dp,得到当天的时间段。

将秒数分解为时分秒字段

auto time = make_time(s);
assert(time.hours() == 7h);
assert(time.minutes() == 33min);
assert(time.seconds() == 20s);
  • 使用 make_time 将时间段转换成小时、分钟、秒。

day_point 转成年月日类型

auto ymd = year_month_day{dp};
assert(ymd.year() == 1970_y);
assert(ymd.month() == jan);
assert(ymd.day() == 3_d);
  • 方便地得到具体日期信息。

总结

  • 这个库完美衔接了 C++14 <chrono> 的时间点和日历日期。
  • day_point 既能做日期算术,也能做时间加减(时分秒),并且能方便地转换为年月日等类型。
  • 这让日期时间处理既高效又直观。

性能成本分析 — 这个库的开销到底有多大?

背景

  • 作者没有跑性能测试,而是直接对比了用“date.h”中复杂类型生成代码的汇编,与简单的C结构体写法生成的汇编代码。
  • 结果显示,这些高级封装并没有增加任何额外的运行时成本!

示例1:构造年月日结构体

date.h版本(year_month_day)

date::year_month_day make_year_month_day(int y, int m, int d) {using namespace date;return year{y}/m/d;
}

对应的汇编大致是位操作组合三个字段。
传统C-like版本

struct YMD_4 {std::int16_t year;std::uint8_t month;std::uint8_t day;
};
YMD_4 make_YMD_4(int y, int m, int d) {return {static_cast<std::int16_t>(y),static_cast<std::uint8_t>(m),static_cast<std::uint8_t>(d)};
}

生成的汇编与上面几乎相同。

结论1:

  • “date.h”的“花哨”API在编译后生成的代码和传统C结构体构造函数几乎一样快。
  • 即“Cute API”零空间和时间开销!

示例2:时间点偏移(将epoch从2000-01-01切换到1970-01-01)

date.h版本

using time_point = std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>;
time_point shift_epoch(time_point t) {using namespace date;return t + (day_point{jan/1/2000} - day_point{jan/1/1970});
}

C-like版本

long shift_epoch(long t) {return t + 946684800; // 秒数常量
}

对应汇编几乎完全一样:

leaq 946684800(%rdi), %rax

表示将常数偏移加到传入参数。

结论2:

  • 用“date.h”封装的高层时间点算术,不会导致额外运行时负担。
  • 编译器优化后,与裸指针加常数一样高效。

总结

  • 该日期库设计巧妙,利用C++的类型系统和表达能力,完全没有牺牲性能。
  • 你写的代码看起来简洁、类型安全、功能丰富,底层机器码同样简洁高效。
  • 不用担心抽象导致运行慢,放心用!

“date.h” 的时间点偏移函数性能解析

代码示例对比

// date.h 版本,使用类型安全的时间点和日期
using time_point = std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>;
time_point shift_epoch(time_point t) {using namespace date;return t + (day_point{jan/1/2000} - day_point{jan/1/1970});
}

vs

// 传统 C-like 版本,直接用整数秒数
long shift_epoch(long t) {return t + 946684800;
}

关键点

  • day_point{jan/1/2000}day_point{jan/1/1970} 其实是两个日期的序列号(自1970年1月1日起的天数)。
  • 这两个日期相差 10,957 天(2000年1月1日距离1970年1月1日的天数)。
  • 将天数转成秒数时,等于 10,957 × 86400 = 946,684,800 秒。
  • 这些计算全部在编译期完成(constexpr),运行时只做简单加法。

运行时效果

  • 编译器生成的机器码与直接加一个常量的C-like代码完全一样高效。
  • 这保证了使用“date.h”库的类型安全和表达力,不会带来任何运行时开销

总结

  • 库利用了现代C++的编译期计算(constexpr)机制。
  • 高层抽象代码在底层被优化成极简机器码。
  • 类型安全和性能兼得,使用该库既安全又高效。

这段内容在对比几个C++日期时间库处理“每月第5个星期五”这类较复杂日期计算时的易用性、性能和代码规模,我帮你总结和拆解一下:

Inter-library Comparison — 不同库处理“每月第5个星期五”活动

任务背景:

  • 要找到**每年中所有“有第5个星期五的月份”**的具体日期。
  • 这类情况每年发生4~5次。
  • 目标:
    • 代码实现的简易性
    • 运行时的性能消耗(执行时间)
    • 库本身的代码规模

统一的接口设计(测试基准)

struct ymd {std::int16_t y;std::uint8_t m;std::uint8_t d;
};
std::pair<std::array<ymd, 5>, std::uint32_t> fifth_friday(int y);
  • 返回值包含最多5个日期和实际找到的数量。

测试环境

  • 编译开启 -O3 优化。
  • 测试机器:4核MacBook Pro。
  • 测量:平均多次运行时间(微秒级)。
  • 输出示例:
2015-1-30
2015-5-29
2015-7-31
2015-10-30
<运行时间纳秒>

Bloomberg bdlt

  • 通过静态查表来快速得出结果。
  • 查表大小固定,有14条预定义模式。
  • 速度快(平均几微秒内完成)。
  • 代码大小中等(几十KB范围)。
  • 方案本质上是查表代替计算,非常高效。

Boost date_time(传统版本)

  • 通过循环12个月,调用库函数计算每月第5个星期五。
  • 计算量中等,且调用外部函数。
  • 代码相对较大(更多模板和函数调用)。
  • 速度比Bloomberg略慢,但仍在微秒范围。

Boost date_time v2(新版)

  • 类似Boost date_time,接口稍改进。
  • 功能同样是按月循环调用计算。
  • 性能和代码规模接近传统Boost。

Howard Hinnant’s date.h 库(你提到的date)

  • 使用表达式式的语法,结合类型安全日期和时间点。
  • 允许生成无效日期(例如不存在的第5个星期五),然后用.ok()检测。
  • 代码最简洁,调用表达式清晰。
  • 代码规模很小(几十KB以下)。
  • 性能在所有库中表现优秀,甚至更快一点。
  • 灵活性好,允许无效日期操作,简化实现。

重要对比点

代码规模执行速度易用性特点
Bloomberg bdlt中等 (~几十KB)极快 (<5μs)需要查表维护静态查表,极致优化
Boost date_time较大 (~百KB)快 (~5-8μs)复杂,依赖Boost计算驱动,代码量大
Boost date_time v2较大类似传统类似传统新接口,未来可期
date.h (Howard)小 (~几KB)非常快简洁易用类型安全,灵活,支持无效日期

总结

  • date.h 兼具简洁易用高性能,且代码体积小。
  • 它允许“无效日期”先生成,后验证,有利于写更自然的代码,减少分支判断。
  • Bloomberg 的查表方案性能最好,但灵活性较低,需要维护表数据。
  • Boost 方案更传统,但代码庞大且运行时成本稍高。
  • 这体现了现代C++库设计趋向于类型安全和表达力强,同时保持高效。

1)用C标准库 <time.h> 判断2001年7月4日是星期几

#include <stdio.h>
#include <time.h>
static const char *const wday[] = {"星期日", "星期一", "星期二", "星期三","星期四", "星期五", "星期六", "-未知-"
};
int main() {struct tm time_str = {0};time_str.tm_year = 2001 - 1900;  // 年份从1900算起time_str.tm_mon = 7 - 1;         // 月份0开始计数time_str.tm_mday = 4;time_str.tm_isdst = -1;          // 让系统自动判断夏令时if (mktime(&time_str) == (time_t)(-1)) {time_str.tm_wday = 7; // 计算失败,标记为未知}printf("%s\n", wday[time_str.tm_wday]);return 0;
}

运行后输出:

星期三
  • 这里用mktime把日期转换成时间戳,会自动填充tm_wday字段,代表星期几(0是星期日,6是星期六)。

2)用现代C++库date.h(Howard Hinnant写的)

#include "date.h"
#include <iostream>
int main() {using namespace date;std::cout << weekday{2001_y/jul/4} << '\n';  // 输出 Wed
}
  • 代码更简洁、易读。
  • 输出同样是星期三(Wed)。

3)编译期判断(date.h支持)

#include "date.h"
int main() {using namespace date;static_assert(weekday{2001_y/jul/4} == wed);
}
  • 这是在编译阶段就判断2001年7月4日确实是星期三,编译不通过就报错。
  • 很适合做编译期的日期校验。

4)时区处理 tz.h

默认之前的方法都在UTC时区下计算,实际工作中经常需要用本地时间:

#include "date/tz.h"
#include <iostream>
int main() {using namespace date;using namespace std::chrono;auto zone = locate_zone("America/Los_Angeles");  // 选择时区auto now = floor<milliseconds>(system_clock::now());auto local = zone->to_local(now);std::cout << now << " UTC\n";std::cout << local.first << " " << local.second << "\n";  // 本地时间和时区缩写
}

示例输出:

2015-09-25 16:50:06.123 UTC
2015-09-25 09:50:06.123 PDT
  • 支持完整的IANA时区数据库,包括历史变动和夏令时切换。
  • 还支持闰秒计算。

总结表格

方法结果(2001年7月4日星期几)特点
C标准库星期三运行时判断,跨平台
date.h星期三现代C++,代码简洁
date.h 编译期星期三编译期校验,安全
tz.h + date.h本地时区时间支持完整时区和夏令时处理
http://www.xdnf.cn/news/868447.html

相关文章:

  • Vue 3 弹出式计算器组件(源码 + 教程)
  • SOC-ESP32S3部分:30-I2S音频-麦克风扬声器驱动
  • Go语言学习-->go的跨平台编译
  • 基于C++实现(WinForm) LAN 的即时通信软件
  • 【笔记】PyCharm 使用问题反馈与官方进展速览
  • 开源模型应用落地-OpenAI Agents SDK-集成Qwen3-8B-function_tool(二)
  • IDEA中微服务指定端口启动
  • java31
  • Spring Boot 从Socket 到Netty网络编程(下):Netty基本开发与改进【心跳、粘包与拆包、闲置连接】
  • React组件基础
  • Python 2.7 退役始末:代码架构缺陷与社区演进路线图
  • Linux-linux和windows创建新进程的区别以及posix_spawn
  • 爬虫学习记录day1
  • Git Github Gitee GitLab
  • [特殊字符] 深度剖析 n8n 与 Dify:使用场景、优劣势及技术选型建议
  • 常用的Docker命令
  • 得物GO面试题及参考答案
  • Quick UI 组件加载到 Axure
  • [杰理]蓝牙状态机设计与实现详解
  • Android 3D球形水平圆形旋转,旋转动态更换图片
  • (2025)Windows修改JupyterNotebook的字体,使用JetBrains Mono
  • 【计算机网络】第3章:传输层—TCP 拥塞控制
  • MaskSearch:提升智能体搜索能力的新框架
  • Qwen3与MCP协议:重塑大气科学的智能研究范式
  • 文献分析指令
  • SSM spring Bean基础配置
  • 代理ip的原理,代理ip的类型有哪些?
  • Vue全局事件总线
  • 【Cursor】开发chrome插件,实现网页tab根据域名分组插件
  • 区块链+AI融合实战:智能合约如何结合机器学习优化DeFi风控?