news 2026/10/7 2:21:33

Pendulum 时间差计算与人性化展示:diff()、Interval 与 diff_for_humans() 实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Pendulum 时间差计算与人性化展示:diff()、Interval 与 diff_for_humans() 实战指南
  • 后端

【免费下载链接】pendulum

Python datetimes made easy

项目地址:https://gitcode.com/gh_mirrors/pe/pendulum
点击查看免费下载

本指南围绕 Pendulum 的差值计算体系展开,深入讲解diff()如何返回表示两个时刻总时长的Interval对象、如何通过in_*系列方法按指定单位(年/月/周/天/时/分/秒)表达截断后的差值,以及diff_for_humans()如何输出 "1 day ago"、"3 weeks from now" 等人类可读的文案并支持多语言本地化。读完本文,你将掌握 Pendulum 中日期差值计算的完整 API、正负号与绝对值的控制方式、截断(不四舍五入)的边界行为,以及本地化定制的方法,并能直接迁移到评论时间、内容时效等真实业务场景。

一、diff():两个时间点之间的总时长

diff()是 Pendulum 计算两个时刻差值的入口方法。它返回一个 Interval 实例,该实例表示两个DateTime之间的总时长。例如:

>>> import pendulum >>> dt_ottawa = pendulum.datetime(2000, 1, 1, tz='America/Toronto') >>> dt_vancouver = pendulum.datetime(2000, 1, 1, tz='America/Vancouver') >>> dt_ottawa.diff(dt_vancouver).in_hours() 3

两个城市同刻跨时区比较,diff()返回的是真实的 3 小时时差,而不是简单的本地时间相减。

1.1 参数签名与语义

从源码看,DateTime.diff()的定义位于 src/pendulum/datetime.py:

def diff(self, dt: datetime.datetime | None = None, abs: bool = True) -> Interval[datetime.datetime]: if dt is None: dt = self.now(self.tz) return Interval(self, dt, absolute=abs)

两个参数的完整语义如下:

参数默认值说明
dtNone要与之比较的DateTime实例;传入None时使用now()(并沿用当前实例的时区self.tz)
absTrue是否返回绝对值。True时始终返回正数;False时返回带正负号的相对值,若传入的日期早于当前实例则带-号

1.2 正负号:absolute 参数的实战效果

abs参数决定了差值的符号方向,文档示例清楚地展示了这一点:

>>> dt_ottawa.diff(dt_vancouver).in_hours() 3 >>> dt_ottawa.diff(dt_vancouver, False).in_hours() 3 >>> dt_vancouver.diff(dt_ottawa, False).in_hours() -3

当abs=True(默认)时,无论谁先谁后,结果都是正数 3;当abs=False时,以调用者为基准,dt_ottawa.diff(dt_vancouver, False)表示"渥太华相对温哥华"(Ottawa 更早,但二者相减符号随内部方向),而dt_vancouver.diff(dt_ottawa, False)返回-3,明确表达出"温哥华在渥太华之前 3 小时"这一方向性信息。

从 src/pendulum/interval.py 的实现可以看到,Interval.__new__在absolute=True且start > end时会自动交换起止点(end, start = start, end),从而保证绝对值的语义;同时Interval.__init__中通过self._invert = start > end记录方向,供后续格式化使用。

1.3 返回值类型:Interval

diff()返回的不是普通的timedelta,而是Interval。Interval继承自Duration(见 src/pendulum/interval.py),并额外携带start、end两个端点属性,支持in_words()、range()、迭代与成员判断(in运算符)等高级能力。关于 Interval 的更多用法可参考 Interval 文档。

二、in_* 方法:以指定单位表达总差值

diff()返回的 Interval 可以通过一系列in_*方法,把总时长折算为任意单位。这些方法永远返回"以指定时间单位表达的完整总差值",所有值都是截断(truncate)而非四舍五入(round)。

2.1 可用方法一览

Interval(及父类 Duration)提供的单位方法定义在 src/pendulum/duration.py:

def in_weeks(self) -> int: return int(self.total_weeks()) def in_days(self) -> int: return int(self.total_days()) def in_hours(self) -> int: return int(self.total_hours()) def in_minutes(self) -> int: return int(self.total_minutes()) def in_seconds(self) -> int: return int(self.total_seconds())

Interval 还在 src/pendulum/interval.py 中补充了in_years()(返回整年数)、in_months()(years * 12 + months的完整月数)、in_weeks()与in_days()等语义。所有方法都以int(...)截断小数部分,因此 59 秒的差值用in_minutes()表达就是 0,而 60 秒就是 1。

2.2 截断语义的文档示例

文档用一组精确的例子验证了"截断而非四舍五入"的规则:

>>> dt = pendulum.datetime(2012, 1, 31, 0) >>> dt.diff(dt.add(months=1)).in_days() 29 >>> dt.diff(dt.subtract(months=1), False).in_days() -31 >>> dt = pendulum.datetime(2012, 4, 30, 0) >>> dt.diff(dt.add(months=1)).in_days() 30 >>> dt.diff(dt.add(weeks=1)).in_days() 7 >>> dt = pendulum.datetime(2012, 1, 1, 0) >>> dt.diff(dt.add(seconds=59)).in_minutes() 0 >>> dt.diff(dt.add(seconds=60)).in_minutes() 1 >>> dt.diff(dt.add(seconds=119)).in_minutes() 1 >>> dt.diff(dt.add(seconds=120)).in_minutes() 2

这些示例蕴含两个关键事实:

  • 月份长度按真实日历计算:1 月 31 日加一个月到达 2 月 29 日(2012 为闰年),差值 29 天;4 月 30 日加一个月到达 5 月 30 日,差值 30 天。diff()的月差基于精确的日历日期差,而非固定 30 天。
  • 严格向下截断:119 秒 = 1 分 59 秒,in_minutes()返回 1;120 秒 = 2 分钟,返回 2。任何小于整单位的部分都被丢弃。

2.3 底层精确差值计算

Interval 内部的精确差值由precise_diff()完成(src/pendulum/interval.py 中self._delta: PreciseDiff = precise_diff(_start, _end))。precise_diff在 src/pendulum/helpers.py 中按环境动态加载:当PENDULUM_EXTENSIONS环境变量为"1"(默认)且平台为 64 位时,优先使用 Rust 扩展实现(pendulum._pendulum.precise_diff),否则回退到 Python 实现(pendulum._helpers.precise_diff)。这也是 Interval 能精确拆解出years / months / weeks / days / hours / minutes / seconds / remaining_*等结构化字段的原因。

2.4 测试用例佐证

仓库测试 tests/datetime/test_diff.py 覆盖了所有单位的正负、跨年、跨时区场景,例如:

  • dt.diff(dt.subtract(years=1), False).in_years() == -1(相对值带符号)
  • dt.diff(dt.add(days=1).add(hours=13)).in_days() == 1(截断)
  • dt.diff(dt.add(seconds=1.9)).in_seconds() == 1(小数秒截断)
  • dt_ottawa.diff(dt_vancouver).in_seconds() == 3 * 60 * 60(跨时区精确换算)

三、diff_for_humans():人性化差值文案

diff_for_humans()在差值数值后附加一个短语,把"天数"翻译成自然语言表达。它根据比较对象是否为 now以及方向(过去/未来),共有 4 种输出模式:

比较场景输出模式示例
过去的值 vs 默认 nowago1 hour ago、5 months ago
未来的值 vs 默认 nowfrom now1 hour from now、5 months from now
过去的值 vs 另一个值before1 hour before、5 months before
未来的值 vs 另一个值after1 hour after、5 months after

3.1 基本用法示例

>>> import pendulum # 最典型的场景:评论时间对比当前 now() >>> pendulum.now().subtract(days=1).diff_for_humans() '1 day ago' >>> pendulum.now().diff_for_humans(pendulum.now().subtract(years=1)) '1 year after' >>> dt = pendulum.datetime(2011, 8, 1) >>> dt.diff_for_humans(dt.add(months=1)) '1 month before' >>> dt.diff_for_humans(dt.subtract(months=1)) '1 month after' >>> pendulum.now().add(seconds=5).diff_for_humans() '5 seconds from now' >>> pendulum.now().subtract(days=24).diff_for_humans() '3 weeks ago'

注意最后一行:24 天被智能地折叠为"3 weeks"而非"24 days"——这体现了diff_for_humans()的近似取整策略(见下文 4.2 节)。

3.2 absolute 参数:去掉修饰语

传入True作为第 2 个参数,可以移除ago、from now、before、after等修饰语,只保留纯数值:

>>> pendulum.now().subtract(days=24).diff_for_humans(absolute=True) '3 weeks'

3.3 源码调用链

DateTime.diff_for_humans()的实现位于 src/pendulum/datetime.py:

def diff_for_humans(self, other=None, absolute=False, locale=None) -> str: is_now = other is None if is_now: other = self.now() diff = self.diff(other) return pendulum.format_diff(diff, is_now, absolute, locale)

核心逻辑委托给pendulum.format_diff(),最终由DifferenceFormatter(src/pendulum/formatting/difference_formatter.py)完成文案生成。is_now标志决定使用ago / from now还是before / after分支;方向由 Interval 的invert属性(差值为负时为真)决定使用未来(future)还是过去(past)的翻译键。

Date与Time类型同样提供diff()与diff_for_humans(),实现分别位于 src/pendulum/date.py 与 src/pendulum/time.py,未传参时默认与today()/ 当前时间比较。

四、本地化:全局与单次调用

4.1 两种设置方式

差值文案的本地化支持两种方式:全局设置(在调用diff_for_humans()之前调用pendulum.set_locale('fr'))或单次调用传参(通过locale关键字参数)。后者优先级更高,且不会影响全局状态:

>>> import pendulum >>> pendulum.set_locale('de') >>> pendulum.now().add(years=1).diff_for_humans() 'in 1 Jahr' >>> pendulum.now().add(years=1).diff_for_humans(locale='fr') 'dans 1 an'

示例中全局 locale 为德语(de),但单次调用指定locale='fr'后输出法语,二者互不干扰。完整的本地化机制与可用语言列表可参考 Localization 文档;仓库中 src/pendulum/locales 目录下按语言(de、fr、zh、ja等)组织 locale 数据,DifferenceFormatter通过Locale.load(locale)加载对应语言包,并利用 CLDR(Unicode 通用语言环境数据仓库)中的translations.relative与translations.units数据进行翻译与复数规则匹配。

4.2 近似取整规则(从源码看文案逻辑)

diff_for_humans()的输出不是简单的单位换算,而是遵循DifferenceFormatter.format()中定义的阈值近似规则(src/pendulum/formatting/difference_formatter.py):

阈值常量值含义
DAYS_THRESHOLD_FOR_HALF_WEEK3剩余天数 > 3 天时,周数 +1(约半周)
DAYS_THRESHOLD_FOR_HALF_MONTH15剩余天数 > 15 天时,月数 +1
MONTHS_THRESHOLD_FOR_HALF_YEAR6剩余月数 > 6 个月时,年数 +1
HOURS_IN_NEARLY_A_DAY22小时 ≥ 22 时,天数 +1(接近一天)
DAYS_IN_NEARLY_A_MONTH27折算天数 ≥ 27 时,月数 +1(接近一月)
MONTHS_IN_NEARLY_A_YEAR1111 个月 + 足够天数时进位为 1 年
FEW_SECONDS_MAX10剩余秒数 ≤ 10 时优先使用 "a few seconds" 等自定义单位

这解释了"24 天 → 3 weeks":24 天 = 3 周 + 3 天剩余,未超过半周阈值(3 天),因此保持 3 周。格式化同时会依据 locale 的复数规则(locale.plural(count))选择单复数形式,例如英语的 "1 day ago" 与 "2 days ago"。

五、实际应用与注意事项

5.1 典型业务场景

  • 评论/帖子时间戳:pendulum.now().subtract(days=1).diff_for_humans()输出 "1 day ago",是社交产品最典型的用法;
  • 事件方向提示:dt.diff_for_humans(other)输出 "before / after" 指明先后关系;
  • 无修饰的纯时长:diff_for_humans(absolute=True)用于表格、标签等不强调方向的界面;
  • 精确计算:需要精确时长时使用diff().in_*(),它严格截断,适合倒计时、统计等场景。

5.2 边界与陷阱

  • 时区参与计算:diff()对带时区的实例执行的是真实时刻差,跨时区比较会自动换算(如本文开篇渥太华与温哥华的 3 小时示例);Interval.__new__还会对 offset-naive 与 offset-aware 混用的情况抛出TypeError(见 src/pendulum/interval.py)。
  • 截断非取整:in_minutes()对 119 秒返回 1、in_days()对 1 天 13 小时返回 1,这是文档与测试共同确认的行为,做倒计时逻辑时需自行处理余数。
  • 月份差异依赖日历:add(months=1)后的实际天数随起止月份变化(1 月末加 1 月是 29 天,4 月末加 1 月是 30 天),不要假设固定 30 天/月。
  • 人性化文案是近似值:diff_for_humans()的输出经过阈值四舍五入式的折叠(如 24 天显示为 3 周),适合展示而不适合精确计算。

仓库中 tests/datetime/test_diff.py 与 tests/date/test_diff.py 提供了大量可直接对照的断言用例,可作为理解截断、符号与文案规则的"活文档"。

六、小结

Pendulum 的差值体系由三层构成:diff()负责产出携带端点与方向的Interval;in_*系列负责把总时长严格截断到目标单位;diff_for_humans()负责把差值折叠为符合人类阅读习惯、且支持复数规则与多语言本地化的自然语言文案。掌握abs(正负号)、absolute(去修饰语)与locale(本地化)三个开关,以及"截断不取整""近似折叠"两条核心规则,即可在真实业务中正确、优雅地处理所有时间差需求。

  • 后端

【免费下载链接】pendulum

Python datetimes made easy

项目地址:https://gitcode.com/gh_mirrors/pe/pendulum
点击查看免费下载
上一篇:Switch终极音乐播放方案:TriPlayer完整使用教程与技巧
下一篇:TIDAL无损音乐下载终极指南:24-bit/192kHz母带级音质免费保存

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 2:15:15

DeepSeek本地部署全指南:显存计算、量化选型与推理框架调优

简介:一份DeepSeek大语言模型本地部署教程,面向具有一定计算机基础、希望实现数据本地化处理或模型二次开发的技术人员。教程完整覆盖安装前准备、部署方案选择、可视化界面配置、验证与测试、常见问题与解决方案、安全与性能优化建议等环节:…

作者头像 李华
网站建设 2026/10/7 2:13:25

排序全解析:算法、工程与应用的三个核心层面

如果只看标题“排序------3”,你可能会觉得这是一个随手记的草稿:不知道“3”是第几版,也不知道为什么要用三个横杠隔开。但恰恰是这种模糊的标题,反而把一个被大多数人当成“理所当然”的技术话题重新推到了台前。排序这件事&…

作者头像 李华
网站建设 2026/10/7 2:12:55

代理记账许可证编号怎么查?DLJZ 编号含义与查验方法

代理记账许可证编号怎么查?DLJZ 编号含义与查验方法 一分钟看答案 正规代理记账机构的《代理记账许可证书》编号以 DLJZ 开头(DL代理,JZ记账),后面是地区行政区划码、核发年份和流水号。查验只要三步: 要编…

作者头像 李华