第 20 章 / 共 58 章 卷四 容器与文本

time 与日期时间

一个 #2024-01-05# 字面量背后是一条 67 个函数的完整时间线工具链——构造、解析、分量、运算、比较、时区、格式化、相对时间。本章把它们按功能分组全部实测一遍,卷四就此收官。

本章导读

第 3 章见过 time 的基本面貌,第 19 章用过 time.format/formatPattern。本章补齐整条时间线:#...# 字面量三形态、time 命名空间 67 个函数按功能组逐一实测——当前时间、构造解析、分量、add/diff 运算、比较区间、时间戳与 UTC、月周年周边界、格式化与相对时间,以计时和日志排序两个实战收尾。实测发现三个 RC0 缺陷(月末钳制、var 转换、字典数组字面量),如实记录并给出规避方案。

本章学习目标

掌握 #...# 三种字面量形态;分清 now/today/clock/nowUTC/timer 五个"当前时间";会用 fromDate 构造、tryParse 安全解析;背下 weekday 周日为 0 等约定;掌握 add/diff 八族的方向与截断;会用 inRange/rangeOverlap/approx 做区间判断;理解 toUnix 的 UTC 口径与时区转换链;会用 relative 输出中文相对时间、timer 测耗时、运算符给时间排序。

20.1 time 类型与三种字面量

第 3 章介绍过:time 是一等基础类型,字面量用 #...# 包裹,三种形态的运行时表现:

literal.xl
auto d = #2024-01-05#               // 仅日期
auto t = #12:30:59#                 // 仅时间
auto dt = #2024-01-05 12:30:59#    // 日期时间
println(type(d))    // time
println(d)          // 2024-01-05 00:00:00:日期形态时分秒补零
println(t)          // 0-01-01 12:30:59:时间形态日期部分是"第 0 天"
println(dt)         // 2024-01-05 12:30:59
println(int(dt))    // 63871677059:time 本质是秒数序列
println(int(t))     // 45059 = 12×3600 + 30×60 + 59

三形态共享一个内部表示:从第 0 天起算的秒数(int() 可见)。日期形态 = 整天秒数,时间形态 = 当天内秒数,日期时间 = 两者相加——int(dt) == int(d) + int(t) 精确成立。0-01-01 的怪异显示也由此而来:纯时间的日期部分是 0,被格式化器翻译成"第 0 年第 1 天"。规则:# 到下一处 #,中间不能换行。

20.2 获取当前时间

"现在"有五种口径,语义分明(输出为实测当时的真实值):

now.xl
println(time.now())        // 2026-07-19 00:54:35:本地当前日期时间
println(time.today())      // 2026-07-19 00:00:00:今天零点(日期部分)
println(time.clock())      // 0-01-01 00:54:35:当前时刻(纯时间部分)
println(time.nowUTC())     // 2026-07-18 16:54:35:UTC 口径(本机 UTC+8,差 8 小时)
println(time.timer())      // 1234.567...:高精度计时器读数(float 秒,不是时钟!)

记忆法:now 管"现在"、today 管"今天"、clock 管"几点"、nowUTC 管"伦敦几点"、timer 管"跑了多久"。timer 的绝对值无意义,两次取差才是耗时(20.10 实战)。today 与 clock 互补:一个把时间清零,一个把日期清零。

20.3 构造与解析

字面量写死在源码里,运行期的日期来自两个入口:按字段构造与字符串解析:

build.xl
println(time.fromDate(2024, 2, 29))              // 2024-02-29 00:00:00
println(time.fromTime(8, 5, 3))                 // 0-01-01 08:05:03
println(time.fromDateTime(2024, 1, 5))          // 时分秒有默认值 0
println(time.fromDateTime(2024, 1, 5, 12, 30))  // 2024-01-05 12:30:00
println(time.fromDateTime(2024, 1, 5, 12, 30, 59))

解析宽容——连字符、斜杠、点、甚至中文年月日都认:

parse.xl
println(time.parse("2024-01-05"))           // 2024-01-05 00:00:00
println(time.parse("2024/01/05"))           // 2024-01-05 00:00:00
println(time.parse("2024年1月5日"))          // 2024-01-05 00:00:00
println(time.parse("2024-01-05 12:30:59"))  // 2024-01-05 12:30:59
println(time.parse("2024-13-40"))           // 0-01-01 00:00:00:失败静默返回零值!
auto ok, v = time.tryParse("2024-13-40")
println(f"{ok} {v}")   // false 0-01-01 00:00:00:元组带回成功标志
auto ok2, v2 = time.tryParse("2024-01-05")
println(f"{ok2} {v2}") // true 2024-01-05 00:00:00
println(time.parsePattern("05-01-2024", "dd-mm-yyyy"))  // 按自定义模式解析

关键纪律:parse 失败不报错、静默返回 time(0),外部输入必须走 tryParse / tryParsePattern——返回 (bool, time) 元组(第 9 章多返回值),用标志位判成败。parsePattern 的模式字母规则同 formatPattern(第 19 章)。

20.4 分量提取

拿到 time 值后拆字段:

parts.xl
auto dt = #2024-01-05 12:30:59#
println(time.year(dt), " ", time.month(dt), " ", time.day(dt))      // 2024 1 5
println(time.hour(dt), " ", time.minute(dt), " ", time.second(dt))   // 12 30 59
println(time.weekday(#2024-01-07#))    // 0:周日是 0!周一 1 … 周六 6
println(time.weekday(dt))              // 5:2024-01-05 是周五
println(time.dayOfYear(dt))            // 5:年内第几天
println(time.quarter(#2024-11-15#))    // 4:季度 1~4
println(time.isLeapYear(2024), " ", time.isLeapYear(1900))  // true false(百年不闰)
println(time.daysInMonth(2024, 2))     // 29:闰年二月
println(time.daysInYear(2024))         // 366
println(time.datePart(dt))             // 2024-01-05 00:00:00:时间清零
println(time.timePart(dt))             // 0-01-01 12:30:59:日期清零

三个易错约定:weekday 周日为 0(不是 7,也不是周一为 0);isLeapYear 收年份 int;datePart/timePart 返回 time 类型,对应部分清零。

20.5 日期运算 add 与 diff

time 不支持 +/- 运算符(坑 1),运算全部函数化。add 八族:amount 传负数即减:

add.xl
println(time.addDays(#2024-01-31#, 1))      // 2024-02-01:自动进月
println(time.addDays(#2024-01-31#, -31))    // 2023-12-31:负数倒退跨年
println(time.addWeeks(#2024-01-05#, 2))     // 2024-01-19
println(time.addQuarters(#2024-01-05#, 1))  // 2024-04-05
println(time.addMonths(#2024-01-15#, 1))    // 2024-02-15
println(time.addYears(#2024-01-15#, -4))    // 2020-01-15
println(time.addHours(#2024-01-05 12:00:00#, 13))    // 2024-01-06 01:00:00:跨天进位
println(time.addMinutes(#2024-01-05 12:00:00#, 90))  // 2024-01-05 13:30:00
println(time.addSeconds(#2024-01-05 12:00:00#, 3601)) // 2024-01-05 13:00:01

diff 八族回答"从左到右隔多远",方向敏感:右晚左早为正。两套语义并存——秒到天是流逝量换算,周到年是日历字段差:

diff.xl
println(time.diffDays(#2024-01-05#, #2024-01-15#))    // 10:左→右为正
println(time.diffDays(#2024-01-15#, #2024-01-05#))    // -10:反向为负
println(time.diffHours(#2024-01-05 00:00:00#, #2024-01-06 12:00:00#))  // 36
println(time.diffMinutes(#2024-01-05 00:00:00#, #2024-01-05 02:30:00#)) // 150
println(time.diffWeeks(#2024-01-01#, #2024-01-20#))   // 2:19 天 ÷ 7 截断
println(time.diffMonths(#2024-01-31#, #2024-03-01#))  // 2:月份字段差,不看"日"
println(time.diffYears(#2020-06-15#, #2024-03-01#))   // 4:年份字段差(实际只过了 3 年 8 个月)

用 diffYears 算年龄会得到"虚岁"——精确年龄需减一后用月日比较修正。addMonths/addYears 的月末钳制缺陷见坑 3。

20.6 比较与区间

比较运算符直接可用,语义是秒数的精确比较——时分秒参与运算:

cmp.xl
auto a = #2024-01-05#           // 午夜零点
auto b = #2024-01-05 12:00:00#  // 同日正午
auto c = #2024-01-10#
println(a < c, " ", a == #2024-01-05#, " ", a != c)  // true true true
println(a == b)  // false:同一天不同时刻不相等!
println(time.isSameDay(a, b))         // true:只比日期部分
println(time.isSameMonth(a, c))       // true
println(time.isSameYear(a, #2024-12-31#))  // true
println(time.inRange(b, a, c))        // true:闭区间,两端含
println(time.inRange(#2024-01-11#, a, c))  // false
println(time.rangeOverlap(a, b, b, c)) // true:端点相触算重叠
println(time.rangeOverlap(a, a, c, c)) // false:真正不相交
println(time.approx(a, time.addSeconds(a, 10)))  // true:容差 10 秒
println(time.approx(a, time.addSeconds(a, 30)))  // false

分工:== 精确到秒;isSameDay/Month/Year 按日历粒度;inRange 闭区间点查;rangeOverlap 判区间相交(含端点相触);approx 判"差不多同时"——容差实测 10 秒,适合对时钟抖动宽容的场景。

20.7 时间戳与 UTC

与外部系统交换时间用 Unix 时间戳。实测口径:toUnix 把 time 当 UTC 墙钟解读,fromUnix 同样返回 UTC 墙钟:

unix.xl
println(time.toUnix(#2024-01-05 00:00:00#))  // 1704412800:恰是该时刻的 UTC 时间戳
println(time.fromUnix(0))                   // 1970-01-01 00:00:00:UTC 纪元
println(time.fromUnix(1704412800))          // 2024-01-05 00:00:00:往返一致
auto now = time.now()
auto u = time.localToUTC(now)
println(int(now) - int(u))        // 28800:本地与 UTC 差 8 小时的秒数
println(time.utcToLocal(u) == now) // true:转换链往返还原
println(time.timezoneOffset())    // 28800:本机时区偏移(秒)

本地时间转标准时间戳的完整链:toUnix(localToUTC(本地时间));还原:utcToLocal(fromUnix(时间戳))。timezoneOffset 单位是秒(UTC+8 = 28800),读本机设置,跨时区程序别假设恒定。

20.8 月周年周边界

报表类高频操作:"本月初""本年末""本周一",六个边界函数一次给全(返回值的时分秒清零):

bounds.xl
auto d = #2024-01-05 12:30:59#
println(time.firstDayOfMonth(d))  // 2024-01-01 00:00:00
println(time.lastDayOfMonth(d))   // 2024-01-31 00:00:00
println(time.lastDayOfMonth(#2024-02-15#))  // 2024-02-29:闰年认得
println(time.firstDayOfYear(d))   // 2024-01-01 00:00:00
println(time.lastDayOfYear(d))    // 2024-12-31 00:00:00
println(time.firstDayOfWeek(d))   // 2024-01-01:默认周一(startDay=1)起算
println(time.lastDayOfWeek(d))    // 2024-01-07:周日收尾
println(time.firstDayOfWeek(d, 0))  // 2023-12-31:startDay=0 改为周日起算
println(time.weekOfYear(#2024-12-31#))   // 53:年内第几周
println(time.weekOfMonth(#2024-01-29#))   // 5:月内第几周

firstDayOfWeek/lastDayOfWeek 的 startDay 沿用 weekday 编号(周日 0、周一 1),默认 1 即周一起算——周报直接用,美式周日历传 0。

20.9 格式化与相对时间

格式化第 19 章已详解(format 三形态、formatPattern 模式字母小写、mm 语境二义),此处补两个便捷别名与新函数 relative:

rel.xl
auto dt = #2024-01-05 12:30:59#
println(time.dateString(dt))  // 2024-01-05:等价 format(dt, 1)
println(time.timeString(dt))  // 12:30:59:等价 format(dt, 2)
auto base = #2024-01-05 12:00:00#
println(time.relative(time.addSeconds(base, 30), base))   // 30秒后
println(time.relative(time.addMinutes(base, -5), base))  // 5分钟前
println(time.relative(time.addHours(base, 3), base))      // 3小时后
println(time.relative(time.addDays(base, 2), base))       // 2天后
println(time.relative(time.addDays(base, 30), base))      // 1个月后
println(time.relative(time.addDays(base, 400), base))     // 1年后
println(time.relative(time.addMinutes(base, 90), base))   // 1小时后:截断,不四舍五入

relative(value, base) 输出中文相对时间:单位按秒→分→时→天→月→年自动升档,数值向零截断,方向用"前/后"表达。社交 Feed 的"3 分钟前"一行搞定。

20.10 实战:计时与日志排序

计时:timer 取差测耗时,sleep 暂停毫秒:

timing.xl
auto t0 = time.timer()
time.sleep(120)                 // 暂停 120 毫秒
println(time.timer() - t0)     // 0.1202...:实测耗时(float 秒,略高于 0.12 是调度开销)

排序:给带时间戳的日志按时间重排。数组没有内建 sort(第 16 章),手写冒泡;time 直接用 > 比较:

logsort.xl
auto ts = [#2024-01-05 09:00:12#, #2024-01-05 08:59:47#, #2024-01-05 09:00:03#]
auto msgs = ["服务启动", "磁盘告警", "用户登录"]
for i in range(ts.length)
  for j in range(ts.length - 1 - i)
    if ts[j] > ts[j + 1] then
      auto tt = ts[j]
      ts[j] = ts[j + 1]
      ts[j + 1] = tt
      auto tm = msgs[j]
      msgs[j] = msgs[j + 1]
      msgs[j + 1] = tm
    end
  end
end
for i in range(ts.length)
  println(time.format(ts[i], 0), " ", msgs[i])
end
// 2024-01-05 08:59:47   磁盘告警
// 2024-01-05 09:00:03   用户登录
// 2024-01-05 09:00:12   服务启动

这里用平行数组(ts 与 msgs 同步交换)而非字典数组,是实测后的选择——字典字面量数组与 var 装 time 各有一个 RC0 缺陷(坑 4、坑 5)。

深入细节

拾遗 1:time 编译成了什么——秒数与 long long 证据

用 xl cgen 观察 auto d = #2024-01-05# 与 time.addDays(d, 30),生成的核心代码是:

__xl6_user_global_d = xrtStrToTime("2024-01-05", 10); __xl6_user_global_d2 = __xl_std_time_add_days(__xl6_user_global_d, 30LL); // addDays 本体:return (long long)xrtDateAdd(3LL, amount, (long long)value);

字面量在模块初始化时被 xrtStrToTime 解析一次,之后就是 long long 秒数;time 在 C 层不带盒子和引用计数,比较、加减都是原生整数运算——这是 time 比 var 快得多的根本原因(对照第 15 章拾遗)。20.1 的 int() 实测与此互为印证。

拾遗 2:0-01-01 从哪里来——日期时间合一的表示

纯时间字面量显示为 0-01-01 12:30:59,不是 bug 是表示法:内部只有一个秒数,日期部分为 0 时被忠实翻译成"第 0 年第 1 天"。推论:判断纯时间看 int(t) < 86400;把纯时间贴到某日期上,用 fromDateTime 按字段重组(time 没有 + 运算符)。

拾遗 3:diff 的两套语义——流逝量 vs 日历字段

diff 八族看似整齐,实则两套逻辑:diffSeconds~diffDays 是流逝量换算(相差秒数逐级除,截断取整);diffWeeks~diffYears 是日历字段差(2020→2024 恒为 4,不管是否真过了四个整年)。混用会踩坑:算工龄用 diffYears 会虚高,正确姿势是减一后按"今年纪念日是否已过"修正。add 族同理:addMonths 是"月份字段 +1"而非"加 30 天",这正是月末钳制问题(坑 3)的来源。

⚠️ 常见错误

以下输出全部来自真实编译器与运行时(文件路径已缩短)。

坑 1:给 time 用 +/- 运算符
# bad.xl 内容: # auto t2 = #2024-01-05 12:00:00# + 3600 > xl run bad.xl bad.xl:1:33: [sem] numeric operator operands must be numeric

原因:time 不是数值类型,运算一律走函数。正确做法:加用 add 八族(addSeconds(t, 3600)),减用 diff 八族(diffDays(t1, t2))。

坑 2:parse 失败不报错
# bad.xl 内容: # println(time.parse("2024-13-40")) > xl run bad.xl 0-01-01 00:00:00(静默零值,无任何报错!)

原因:parse 失败静默返回 time(0),脏数据会带着"第 0 年"流入后续逻辑。正确做法:外部输入一律 auto ok, t = time.tryParse(s),判 ok 再用 t。

坑 3:addMonths/addYears 跨月末得到零值(RC0 缺陷)
# bad.xl 内容: # println(time.addMonths(#2024-01-31#, 1)) # println(time.addYears(#2024-02-29#, 1)) > xl run bad.xl 0-01-01 00:00:00 0-01-01 00:00:00(目标日不存在时静默零值,不钳制到月末)

原因:1 月 31 日 +1 月 = 2 月 31 日(不存在),闰日 +1 年同理;RC0 不钳制也不报错。正确做法:锚日固定在 1~28 号;或先 firstDayOfMonth 取月初运算;用前自查 month 分量。

坑 4:var 装 time 再 as time 得到零值(RC0 缺陷)
# bad.xl 内容: # var v = #2024-01-05 09:00:12# # println(v as time) > xl run bad.xl 0-01-01 00:00:00(type(v) 明明是 time,值却丢了)

原因:var→静态 time 转换路径有缺陷,静默产出零值;var 间的比较、println 直打却是好的。正确做法:time 待在静态世界——静态数组、dict<time>(取值直接是 time);必须装箱时存 int(v) 秒数,用时 time(...) 还原。

坑 5:字典字面量数组编译失败(RC0 缺陷)
# bad.xl 内容: # auto logs = [{"k": 1}, {"k": 2}] > xl run bad.xl tcc: tcc: error: unresolved reference to 'xl6_runtime_type_desc_2281444BE1B287C8'

原因:字典字面量直接组成数组字面量时类型描述符未生成,tcc 链接失败。正确做法:先 auto logs = [] 再逐个 push(实测可用);或用 20.10 的平行数组。

本章小结

主题规则要点
字面量#yyyy-mm-dd# / #hh:mm:ss# / 合体内部是秒数;纯时间显示 0-01-01 开头
当前时间now / today / clock / nowUTC / timertimer 是秒表取差用,其余是时钟
构造解析fromDate/Time/DateTime;parse/tryParse/Patternparse 失败静默零值;外部输入走 tryParse 元组
分量year…second / weekday / dayOfYear / quarterweekday 周日=0;isLeapYear 收 int 年份
运算add/diff 各八族,负数即减diff 左→右为正;秒~天流逝量、周~年字段差;月末钳制有缺陷
比较区间运算符精确到秒;isSame* / inRange / rangeOverlap / approxinRange 闭区间;端点相触算重叠;approx 容差 10 秒
时间戳toUnix/fromUnix 按 UTC 解读localToUTC/utcToLocal 搭桥;timezoneOffset 单位是秒
边界firstDay/lastDay × 月/周/年;weekOf*周默认周一起算,startDay=0 改周日
格式化format/formatPattern(19 章);dateString/timeString;relativerelative 输出中文"N 单位前/后",数值截断
工具sleep(毫秒);timer() 取差time 无 +/- 运算符;字典数组字面量、var as time 有缺陷

复习题

✍️ 先遮住答案想一想
  1. println(#12:30:59#) 输出什么?为什么是 0-01-01 开头?
  2. now/today/clock/nowUTC/timer 各自回答什么问题?
  3. time.parse("2024-13-40") 的结果?处理用户输入的正确姿势?
  4. time.weekday(#2024-01-07#) 返回几?编号约定是什么?
  5. diffYears(#2020-06-15#, #2024-03-01#) 返回 4,但实际只过了约 3.7 年——为什么?算精确年龄怎么修正?
  6. var 里的 time 值直接 v as time 格式化会发生什么?两条可行路径?
参考答案
  1. 0-01-01 12:30:59。内部是"第 0 天起算的秒数",纯时间日期部分为 0,被翻译成第 0 年第 1 天。
  2. now=本地日期时间;today=今天零点;clock=当前时刻(纯时间);nowUTC=UTC 口径;timer=高精度计时器读数(float 秒,两次取差才是耗时)。
  3. 静默返回 time(0)(显示 0-01-01 00:00:00),不报错。外部输入必须用 tryParse/tryParsePattern,用元组的 bool 标志判成败。
  4. 0。约定周日=0、周一=1……周六=6(与美式 strftime 一致,不是 ISO 的周一=1)。
  5. diffYears 是"年份字段差"(2024-2020),不是流逝整年数。精确年龄:diffYears(birth, today) 的基础上,今年生日还没到则减 1(用 month/day 分量判断)。
  6. 静默得到 time(0)(0-01-01 00:00:00)——var→静态 time 转换有 RC0 缺陷。路径一:dict<time> 或静态数组让 time 留在静态类型;路径二:装箱时存 int(v) 秒数,用时还原。

编程练习

练习 1:生日倒计时

编写 birthday.xl:给定生日月日(如 8 月 15 日),计算今天到下一个生日还有多少天;今年已过则算到明年,输出"距你的生日还有 N 天"。

参考思路:time.year(time.today()) 取今年,fromDate(y, m, d) 造今年生日;与 today 用运算符比较;已过则 fromDate(y + 1, m, d)——避开 addYears 的钳制缺陷;最后 diffDays 取天数。

练习 2:耗时测量器

编写 bench.xl:用 timer 测量"一百万次空循环"的耗时并打印;再 sleep(200) 后用 timer 验证暂停精度,打印两次测量与理论值的偏差。

参考思路:auto t0 = time.timer() … time.timer() - t0;sleep 参数是毫秒;实测略高于理论值是正常的调度开销(本章实测 120ms 得 0.1202s)。

练习 3:本周日期范围报告

编写 week.xl:打印本周日期范围与已过天数,形如"本周:01 月 01 日 ~ 01 月 07 日,已过 5 天"。再用 startDay=0 与默认值各打印一次,体会两种周历差别。

参考思路:firstDayOfWeek/lastDayOfWeek 取边界;formatPattern(t, "mm 月 dd 日") 格式化(mm 在日期语境为月,第 19 章规则);已过天数 diffDays(周一, today) + 1;周日历版本传 startDay=0。