【仓颉语言入门 · 第二篇 第 2 课】
【仓颉语言入门 · 第二篇 第 2 课】时间与日期:管好每一秒(std.time)
上一课我们用正则从日志里"挖"出了时间字符串——但它到手里还只是一串文本。
"2026-10-09 08:31:02"比大小要靠字符串比较,加一小时要手工进位,想问"这两条日志隔了几秒"更是无从下手。这一课的std.time就是给"时刻"和"时长"安上真正的类型:DateTime表达一个时刻,Duration表达一段时长——构造、加减、比较、格式化、解析、时区换算、时间戳互转,全套齐活。最后我们把上一课日志里的时间字符串真正解析成时间对象,让日志巡检工具再进化一步。本文所有代码、输出与报错文案均在仓颉 SDK 1.2.0 下逐行实测编译运行。
目录(第二篇导航)
第一篇《仓颉语言入门》共 30 课已完结(环境搭建 → 语法 → 函数 → 类型系统 → 工程化 → 并发 → 两场实战)。第二篇在更高的起点上继续,共 17 课、4 个模块:
| 模块 | 课次 | 内容 |
|---|---|---|
| 一、补遗篇 | 01~06 | 正则、时间日期、命令行参数、随机数、加密哈希、二进制处理 |
| 二、数据库篇 | 07~10 | std.database.sql 入门与 CRUD、预编译与事务、手写 DAO、综合小实战 |
| 三、进阶硬核篇 | 11~14 | 反射、宏与 deriving、C 互操作 FFI、并发容器与对象池 |
| 四、实战项目篇 | 15~17 | HTTP 服务端、多线程日志分析器、简易爬虫 |
- 正则表达式(std.regex)
- 时间日期处理(本文)
- 命令行参数解析(std.argopt)
- 随机数与概率(std.random)
- 加密与哈希(std.crypto)
- 二进制数据处理(std.binary)
- 数据库入门:std.database.sql 与 CRUD
- 预编译、事务与防注入
- 手写 DAO 数据访问层
- 数据库综合小实战
- 反射:在运行时打量类型
- 宏与 deriving:让编译器帮你写代码
- 与 C 互操作:FFI
- 并发容器与对象池
- 实战三:HTTP 服务端
- 实战四:多线程日志分析器
- 实战五:简易爬虫
一、为什么要专门一个时间库
先看三个"不用时间库"的惨案:
- 比时间先后:字符串比较
"2026-10-09 08:31:02" < "2026-10-09 08:31:45"碰巧能对,但一换成2026-9-5(没补零)就全乱。 - 加一段时间:
08:45:00加 30 分钟,秒、分、时、日、月、年逐级进位,还要管 2 月 28 天、闰年 366 天——手写必错。 - 算两时刻差:
23:50:00到次日00:10:00隔了多久?跨天、跨月、跨年,手工计算是灾难。
std.time 把这三件事全包了:DateTime 会自己处理进位和闰年,Duration 表达"一段时长",两者相减就是间隔。和 std.regex 一样,它属于核心标准库,随 SDK 安装、开箱即用,不需要 stdx。
本课的所有示例都在下面这个最普通的工程里完成:
s2probe/ ← 工程根目录(cjpm init --name s2probe)
├── cjpm.toml
├── data/
│ └── app.log ← 第十一节实战用的数据文件
└── src/
└── main.cj ← 所有代码都在这里
二、两个主角:DateTime 与 Duration
std.time 的核心就是两个类型,先认清分工:
DateTime:一个时刻。"2026 年 10 月 10 日 8 点 30 分 15 秒"是一个时刻。Duration:一段时长。"90 秒"是一段时长,它不关心从几点到几点。
时刻 ± 时长 = 新时刻;时刻 − 时刻 = 时长。这两个类型配合,就能表达绝大多数时间计算。
先 import std.time.*。但注意:Duration 其实定义在 std.core(基础库,默认可见),就算不写任何 import,Duration.second 也能直接用——实测把 import std.time.* 删掉后 Duration 的代码照样编译通过。DateTime、TimeZone 这些才真正需要 import std.time.*。
三、DateTime:表达一个时刻
3.1 拿到"现在"
import std.time.*
main(): Int64 {
println("UTC 时间:${DateTime.nowUTC()}")
println("本地时间:${DateTime.now(timeZone: TimeZone.Local)}")
0
}
在我这台 UTC+8 的机器上运行(你的输出会随运行时间变化):
UTC 时间:2026-10-10T14:35:13.6185812Z
本地时间:2026-10-10T22:35:13.6186243+08:00
nowUTC() 返回世界协调时(UTC,字符串以 Z 结尾);now(timeZone: TimeZone.Local) 返回本地时区的时间(+08:00 结尾)。now 必须带命名参数 timeZone:,直接写 DateTime.now() 会编译报错:
error: missing argument prefix 'timeZone:' for named parameter
3.2 默认格式:ISO 8601
上面输出的 2026-10-10T22:35:13.6186243+08:00 就是 DateTime 的默认打印格式(ISO 8601):日期 T 时间 时区。这是国际标准格式,排序友好、机器可读,但人看着累——人看的格式化第六节用 format 解决。
3.3 构造指定时刻:DateTime.of
要表达"2026 年 10 月 10 日 8 点 30 分 15 秒",用 DateTime.of:
import std.time.*
main(): Int64 {
let dt = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15)
println(dt)
0
}
2026-10-10T08:30:15+08:00
这里有个和以往 API 很不一样的规矩,本课第一坑:of 的参数全是命名参数,必须写 year:、month:、dayOfMonth:……一个都不能漏写前缀,按位置传会报:
error: missing argument prefix 'year:' or 'month:' or 'dayOfMonth:' or 'hour:' or 'minute:' or 'second:' or 'nanosecond:' or 'timeZone:' for named parameter
参数清单与默认值(实测:只写前 6 个参数完全合法):
| 参数 | 类型 | 说明 | 默认值 |
|---|---|---|---|
year: |
Int64 |
年 | 必填 |
month: |
Int64 或 Month |
月,10 或 Month.October 都行 |
必填 |
dayOfMonth: |
Int64 |
日 | 必填 |
hour: / minute: / second: |
Int64 |
时分秒 | 必填 |
nanosecond: |
Int64 |
纳秒(10⁻⁹ 秒) | 0 |
timeZone: |
TimeZone |
时区 | 本地时区 |
不带 timeZone: 时构造出来就是本地时区的时刻(+08:00);要 UTC 就传 timeZone: TimeZone.UTC,还有个等价的快捷构造 DateTime.ofUTC(...),两者实测输出一致(都是 Z 结尾):
import std.time.*
main(): Int64 {
let a = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15, timeZone: TimeZone.UTC)
let b = DateTime.ofUTC(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15)
println("a=${a}")
println("b=${b}")
0
}
a=2026-10-10T08:30:15Z
b=2026-10-10T08:30:15Z
3.4 拆开看:字段属性
DateTime 的字段属性一 table 打尽(用 2026-10-10 08:30:15.123456789 +08:00 这个时刻实测):
import std.time.*
main(): Int64 {
let dt = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15, nanosecond: 123456789)
println("year=${dt.year}")
println("month=${dt.month}")
println("dayOfMonth=${dt.dayOfMonth}")
println("dayOfWeek=${dt.dayOfWeek}")
println("dayOfYear=${dt.dayOfYear}")
println("hour=${dt.hour} minute=${dt.minute} second=${dt.second} nanosecond=${dt.nanosecond}")
println("zoneId=${dt.zoneId}")
println("zoneOffset=${dt.zoneOffset}")
0
}
year=2026
month=October
dayOfMonth=10
dayOfWeek=Saturday
dayOfYear=283
hour=8 minute=30 second=15 nanosecond=123456789
zoneId=UTC+08:00
zoneOffset=8h
三个要点:
month返回Month枚举(打印英文全名October)、dayOfWeek返回DayOfWeek枚举(Saturday)、dayOfYear是年内的第几天(2026-10-10 是第 283 天)。zoneId是时区 ID 字符串,zoneOffset是相对 UTC 的偏移量——类型是Duration,所以打印成8h(Duration 的格式,下一节讲)。- 还有三个元组属性:
isoWeek(年 + ISO 周数)、date(年、月、日)、time(时、分、秒)。元组不能直接塞进字符串插值,要先解构:
import std.time.*
main(): Int64 {
let dt = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15)
let (y, m, d) = dt.date
let (h, mi, s) = dt.time
println("date=(${y}, ${m}, ${d})")
println("time=(${h}, ${mi}, ${s})")
let (iw, w) = dt.isoWeek
println("isoWeek=第 ${iw} 年第 ${w} 周")
0
}
date=(2026, October, 10)
time=(8, 30, 15)
isoWeek=第 2026 年第 41 周
小提示:直接写
"${dt.date}"编译就过不了,报the type 'Tuple<Int64, Enum-Month, Int64>' should implement interface 'ToString' in the 'core' package——元组没实现ToString。解构(如上)或用下标dt.date[0]都行。另外老资料里还能见到
monthValue属性(返回 1~12 的数字),它已标记废弃,编译会有 warning,用month配合第十四节的Month枚举方法即可。
四、Duration:表达一段时长
4.1 常量与打印格式
Duration 提供了一组现成常量,打印格式是"数字 + 单位缩写":
main(): Int64 {
println("Zero=${Duration.Zero}")
println("nanosecond=${Duration.nanosecond}")
println("microsecond=${Duration.microsecond}")
println("millisecond=${Duration.millisecond}")
println("second=${Duration.second}")
println("minute=${Duration.minute}")
println("hour=${Duration.hour}")
println("day=${Duration.day}")
0
}
Zero=0s
nanosecond=1ns
microsecond=1us
millisecond=1ms
second=1s
minute=1m
hour=1h
day=1d
单位从纳秒(ns)到天(d)共八档。注意这个 main 连 import 都没写——Duration 在 std.core,天生可见。
4.2 拼出任意时长:常量组合运算
Duration 的构造没有"指定秒数"的普通方法,标准手法是常量 + 运算组合:
main(): Int64 {
let d1 = Duration.minute * 2 + Duration.second * 30
println("d1=${d1}")
let d2 = Duration.hour + Duration.millisecond * 500
println("d2=${d2}")
let d3 = Duration.day * 3
println("d3=${d3}")
0
}
d1=2m30s
d2=1h500ms
d3=3d
打印时自动挑合适的单位档位拼接:2m30s、1h500ms。负时长也支持(d = Duration.Zero - Duration.second 打印 -1s)——但注意一个实测的坑:Duration 不支持一元负号,写 -Duration.second 直接编译报错:
error: invalid unary operator '-' on type 'Struct-Duration'
# note: you may want to implement 'operator func -()' for type 'Struct-Duration'
要表达负时长,用 Duration.Zero - x 或者乘 -1(Duration.second * -1,实测输出 -1s)。
4.3 四则运算
main(): Int64 {
let a = Duration.minute + Duration.second * 30 // 加
println("a=${a}")
let b = Duration.minute - Duration.second * 90 // 减(出负数)
println("b=${b}")
let c = Duration.hour * 3 // 乘整数
println("c=${c}")
let d = Duration.day / 8 // 除整数
println("d=${d}")
let e = Duration.day / 2.5 // 除小数
println("e=${e}")
0
}
a=1m30s
b=-30s
c=3h
d=3h
e=9h36m
乘除都行:乘 Int64、除 Int64 或 Float64。Duration.day / 2.5 = 9.6h,打印成 9h36m(打印按档位取整显示,数值本身不失精度)。
4.4 换单位与比较
一组 toXxx() 方法把时长换算成指定单位(向下截断成整数):
main(): Int64 {
let d = Duration.hour * 25 + Duration.second * 61 + Duration.nanosecond * 500000000
println("d=${d}")
println("toDays=${d.toDays()}")
println("toHours=${d.toHours()}")
println("toMinutes=${d.toMinutes()}")
println("toSeconds=${d.toSeconds()}")
println("toMilliseconds=${d.toMilliseconds()}")
println("toNanoseconds=${d.toNanoseconds()}")
0
}
d=1d1h1m1s500ms
toDays=1
toHours=25
toMinutes=1501
toSeconds=90061
toMilliseconds=90061500
toNanoseconds=90061500000000
toSeconds 把 1 天 1 小时 1 分 1.5 秒折成 90061 秒(截掉半秒);toMilliseconds 折到毫秒级 90061500。比较运算和数学里一样:
main(): Int64 {
println("相等:${Duration.minute == Duration.second * 60}")
println("小于:${Duration.second < Duration.minute}")
println("compare:${Duration.minute.compare(Duration.second * 60)}")
println("绝对值:${(Duration.minute - Duration.second * 90).abs()}")
0
}
相等:true
小于:true
compare:Ordering.EQ
绝对值:30s
compare 返回 Ordering 枚举(LT/EQ/GT,第 18 课的老朋友);abs() 取绝对值。
五、时间计算:加加减减才见真章
5.1 add 系列:按"日历单位"推进
DateTime 提供八个 addXxx 方法,按年/月/周/日/时/分/秒/纳秒推进:
import std.time.*
main(): Int64 {
let dt = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15)
println("加 3 天:${dt.addDays(3)}")
println("加 2 月:${dt.addMonths(2)}")
println("加 1 年:${dt.addYears(1)}")
println("加 2 周:${dt.addWeeks(2)}")
println("加 5 时:${dt.addHours(5)}")
println("加 45 分:${dt.addMinutes(45)}")
println("加 50 秒:${dt.addSeconds(50)}")
println("加 1.5 秒(纳秒):${dt.addNanoseconds(1500000000)}")
println("链式:${dt.addDays(1).addHours(2).addMinutes(3)}")
0
}
加 3 天:2026-10-13T08:30:15+08:00
加 2 月:2026-12-10T08:30:15+08:00
加 1 年:2027-10-10T08:30:15+08:00
加 2 周:2026-10-24T08:30:15+08:00
加 5 时:2026-10-10T13:30:15+08:00
加 45 分:2026-10-10T09:15:15+08:00
加 50 秒:2026-10-10T08:31:05+08:00
加 1.5 秒(纳秒):2026-10-10T08:30:16.5+08:00
链式:2026-10-11T10:33:15+08:00
两个观察:
add系列的参数是普通位置参数(addDays(3)),和of的命名参数风格不同——这不是笔误,是实测结论,两套 API 各有各的规矩。addNanoseconds(1500000000)就是 1.5 秒,打印成08:30:16.5——默认格式自动省略纳秒尾零(123456789纳秒打.123456789,500000000纳秒打.5)。
add 系列默认接收负数就是回退:dt.addDays(-1) 等价"减一天"。
还有一个必须知道的月末陷阱:1 月 31 日加 1 个月是几号?2 月没有 31 号,实测它截断到当月最后一天:
import std.time.*
main(): Int64 {
let jan31 = DateTime.of(year: 2026, month: 1, dayOfMonth: 31)
println(jan31.addMonths(1))
0
}
2026-02-28T00:00:00+08:00
2026 年不是闰年,2 月只有 28 天,结果落在 2-28。做"会员到期日""账单日"这类功能时,这个行为要想清楚是不是你要的。
5.2 与 Duration 混合运算
DateTime 和 Duration 可以直接加减——"时刻 + 时长 = 新时刻":
import std.time.*
main(): Int64 {
let dt = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15)
println("开会后 2 小时:${dt + Duration.hour * 2}")
println("提前 90 分钟:${dt - Duration.minute * 90}")
0
}
开会后 2 小时:2026-10-10T10:30:15+08:00
提前 90 分钟:2026-10-10T07:00:15+08:00
5.3 两时刻相减 = Duration;比大小直接用运算符
DateTime 减 DateTime 得到 Duration,比较运算符 <、>、== 全部可用:
import std.time.*
main(): Int64 {
let a = DateTime.of(year: 2026, month: 10, dayOfMonth: 9, hour: 8, minute: 0, second: 0)
let b = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15)
println("a 在 b 之前:${a < b}")
println("间隔:${b - a}")
println("间隔秒数:${(b - a).toSeconds()}")
0
}
a 在 b 之前:true
间隔:1d30m15s
间隔秒数:88215
== 比的是"是不是同一瞬间",与时区无关:把一个时刻用第八节的 inUTC() 换到 UTC 再比,实测仍然相等(它们的 Unix 时间戳一致)。
六、格式化输出:format 与格式符全表
给"机器看的"是默认 ISO 格式,给"人看的"要用 format。它接收一个模式串,模式串里混合"格式符"和"普通字符":
import std.time.*
main(): Int64 {
let dt = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15, nanosecond: 123456789)
println("${dt.format("yyyy-MM-dd HH:mm:ss")}")
println("${dt.format("yyyy/MM/dd HH:mm:ss.SSS")}")
println("${dt.format("hh:mm a")}")
println("${dt.format("yyyy年MM月dd日 HH时mm分")}")
0
}
2026-10-10 08:30:15
2026/10/10 08:30:15.123456789
08:30 AM
2026年10月10日 08时30分
-、/、空格、甚至中文,凡不是格式符的字符原样输出。格式符全表(对 2026-10-10 08:30:15.123456789 +08:00、星期六实测):
| 格式符 | 输出 | 说明 |
|---|---|---|
yyyy / y |
2026 |
4 位年 |
yy |
26 |
2 位年 |
M / MM |
10 |
月(MM 补零到 2 位) |
MMM |
Oct |
英文月缩写 |
MMMM |
October |
英文月全名 |
d / dd |
10 |
日 |
H |
8 |
时(24 小时制,不补零) |
HH |
08 |
时(24 小时制,补零) |
h / hh |
8 / 08 |
时(12 小时制) |
m / mm |
30 |
分 |
s / ss |
15 |
秒 |
S |
123 |
毫秒(固定 3 位) |
SS |
123456 |
微秒(固定 6 位) |
SSS |
123456789 |
纳秒(固定 9 位) |
a |
AM |
上午/下午 |
D / DD |
283 |
年内第几天 |
w / ww |
6 / 06 |
星期几(周日=0,周六=6) |
www |
Sat |
英文星期缩写 |
wwww |
Saturday |
英文星期全名 |
O |
+08 |
时区偏移(小时) |
OO |
+08:00 |
时区偏移(时:分) |
OOO |
+08:00:00 |
时区偏移(时:分:秒) |
OOOO |
+08:00 |
同 OO |
四个实测的"反直觉"点,写进你的备忘录:
E不是星期格式符(Java 背景的同学最容易中招)。写"E"原样输出字母E。要星期几用w/www/wwww,或者拿dayOfWeek属性自己转中文(见 6.2)。S/SS/SSS不是"不足补零的任意位小数",而是固定毫秒/微秒/纳秒。写 4 个S直接抛异常:The number of character "S" can not exceed 3.- 同理
D最多 2 个、w最多 4 个,超了分别报The number of character "D" can not exceed 2.和The number of character "w" can not exceed 4. X、x也不是时区格式符,原样输出。
6.1 内置的四个预设
DateTimeFormat 有四个 RFC 标准格式的字符串常量(注意:它们本身就是 String,直接传给 format 即可):
import std.time.*
main(): Int64 {
let dt = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15)
println("RFC3339=${dt.format(DateTimeFormat.RFC3339)}")
println("RFC1123=${dt.format(DateTimeFormat.RFC1123)}")
println("RFC822=${dt.format(DateTimeFormat.RFC822)}")
println("RFC850=${dt.format(DateTimeFormat.RFC850)}")
0
}
RFC3339=2026-10-10T08:30:15+08:00
RFC1123=Sat, 10 Oct 2026 08:30:15 UTC+08:00
RFC822=06 10 Oct 26 08:30:15 UTC+08:00
RFC850=Saturday, 10-Oct-26 08:30:15 UTC+08:00
RFC3339 就是 HTTP 头、JSON 里最常见的那种时间格式。RFC822 输出开头的 06 是 ww(星期六=6),不是日期——这是 RFC822 模式串 ww dd MMM yy ... 的原样行为,别被它吓到。
6.2 实战套路:输出中文星期
格式符只出英文星期,要"星期六"三个字,自己用 dayOfWeek 映射:
import std.time.*
func weekCn(d: DayOfWeek): String {
match (d) {
case Sunday => "日"
case Monday => "一"
case Tuesday => "二"
case Wednesday => "三"
case Thursday => "四"
case Friday => "五"
case Saturday => "六"
}
}
main(): Int64 {
let dt = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15)
println("2026-10-10 是星期${weekCn(dt.dayOfWeek)}")
0
}
2026-10-10 是星期六
七、解析:从字符串到 DateTime
格式化的逆过程。parse 有两种用法,各自的规矩都不少。
7.1 默认格式解析:必须带时区
DateTime.parse(字符串) 只认 ISO 8601 格式,而且时区部分必须写:
import std.time.*
func tryP(s: String): Unit {
try {
println("解析成功:${DateTime.parse(s)}")
} catch (e: Exception) {
println("解析失败:${e.message}")
}
}
main(): Int64 {
tryP("2026-10-10T08:30:15+08:00")
tryP("2026-10-10T00:30:15Z")
tryP("2026-10-10T08:30:15")
tryP("2026-10-10 08:30:15")
0
}
解析成功:2026-10-10T08:30:15+08:00
解析成功:2026-10-10T00:30:15Z
解析失败:Illegal zone offset value.
解析失败:Fail to parse at index 10: " " should be "T".
- 带偏移
+08:00或带Z都能解析;不带时区的"2026-10-10T08:30:15"抛Illegal zone offset value.——这是很多人第一次用parse翻车的地方。 - 日期和时间之间必须是
T,写空格报Fail to parse at index 10: " " should be "T".(index 10 直接告诉你在第几个字符翻的车)。
那"不带时区"的串怎么解?用模式串。
7.2 模式串解析:自己声明格式
parse 的第二个重载接收模式串,格式符与第六节的表格完全一致:
import std.time.*
main(): Int64 {
let dt = DateTime.parse("2026/10/10 08:30", "yyyy/MM/dd HH:mm")
println(dt)
println("格式化回去:${dt.format("yyyy年MM月dd日 HH时mm分")}")
0
}
2026-10-10T08:30:00+08:00
格式化回去:2026年10月10日 08时30分
没写的字段补零值(时分秒补 0),时区默认本地。日志里最常见的 "2026-10-09 08:31:02"(中间是空格、无时区)就得走这条路:DateTime.parse(t, "yyyy-MM-dd HH:mm:ss")——第十一节实战就这么干。
解析失败抛的是 TimeParseException(Exception 的子类),try-catch 住即可;另一个实测文案给留个印象——串里第一个字符就不是数字时报:
Fail to parse at index 0: "垃" should be integer.
7.3 tryParse:不想抛异常就用它
tryParse 是 parse 的温和版:成功返回 Some(dt),失败返回 None,绝不抛异常:
import std.time.*
func show(s: String): Unit {
match (DateTime.tryParse(s)) {
case Some(dt) => println("[${s}] -> ${dt}")
case None => println("[${s}] -> 不是合法时间")
}
}
main(): Int64 {
show("2026-10-10T08:30:15+08:00")
show("2026-10-10T08:30:15")
show("明天早上八点")
0
}
[2026-10-10T08:30:15+08:00] -> 2026-10-10T08:30:15+08:00
[2026-10-10T08:30:15] -> 不是合法时间
[明天早上八点] -> 不是合法时间
注意第二条:tryParse 与 parse 的默认格式同样要求时区,不带时区照样失败。来源不可信的字符串(用户输入、外部文件)优先 tryParse;格式明确的(自家日志)直接 parse + 模式串。
八、时区:TimeZone
8.1 两个常量与三个转换
TimeZone.UTC 和 TimeZone.Local 覆盖绝大多数需求,DateTime 上有三个转换方法:
import std.time.*
main(): Int64 {
let dt = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15)
println("原(本地):${dt}")
println("转 UTC:${dt.inUTC()}")
println("转本地:${dt.inLocal()}")
let tz9 = TimeZone("UTC+9", Duration.hour * 9)
println("转东 9 区:${dt.inTimeZone(tz9)}")
0
}
原(本地):2026-10-10T08:30:15+08:00
转 UTC:2026-10-10T00:30:15Z
转本地:2026-10-10T08:30:15+08:00
转东 9 区:2026-10-10T09:30:15+09:00
看清语义:转换不改瞬间,只改显示。8:30(北京)和 0:30(UTC)是同一瞬间——这在 5.3 节验证过 == 相等。
8.2 自建时区:固定偏移构造
TimeZone("UTC+9", Duration.hour * 9) 的第一个参数是自定义 ID(随便起,打印 zoneId 时用),第二个参数是与 UTC 的偏移。Windows 上想表达"北京时间",这是标准写法——因为按名字加载时区的 TimeZone.load("Asia/Shanghai") 在 Windows 上实测直接抛异常:
No valid timezone file is found.
Windows 没有随 SDK 附带 IANA 时区数据库文件,load 无从加载。记住这个替代写法:
import std.time.*
main(): Int64 {
let bj = TimeZone("UTC+8", Duration.hour * 8)
let dt = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15)
println("北京时间:${dt.inTimeZone(bj)}")
println("时区 ID:${bj.id}")
0
}
北京时间:2026-10-10T08:30:15+08:00
时区 ID:UTC+8
九、Unix 时间戳:机器间交换时间的通用语
Unix 时间戳 = 从 1970-01-01 00:00:00 UTC 起经过的秒数。数据库、接口、日志系统之间传时间,十有八九传的就是它。
import std.time.*
main(): Int64 {
// 时刻 -> 时间戳
let dt = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 8, minute: 30, second: 15)
let ts = dt.toUnixTimeStamp()
println("时间戳(Duration):${ts}")
println("时间戳(秒):${ts.toSeconds()}")
// 时间戳 -> 时刻
let back = DateTime.fromUnixTimeStamp(ts)
println("转回来:${back}")
println("往返一致:${back == dt}")
// 起点
println("纪元:${DateTime.UnixEpoch}")
println("ofEpoch(0, 0):${DateTime.ofEpoch(second: 0, nanosecond: 0)}")
0
}
时间戳(Duration):20736d30m15s
时间戳(秒):1791592215
转回来:2026-10-10T00:30:15Z
往返一致:true
纪元:1970-01-01T00:00:00Z
ofEpoch(0, 0):1970-01-01T00:00:00Z
要点:
toUnixTimeStamp()返回Duration(距纪元的时长),要整数秒就链一个.toSeconds()。打印20736d30m15s是 Duration 的显示格式(20736 天 + 30 分 + 15 秒),别被它迷惑,数值是对的。fromUnixTimeStamp(Duration)逆向转换。注意转回来打印的是 UTC(Z)——时间戳本身不带时区,fromUnixTimeStamp统一按 UTC 表达。DateTime.ofEpoch(second:, nanosecond:)(命名参数)从秒 + 纳秒构造,负数也合法:ofEpoch(second: -86400, nanosecond: 0)得到1969-12-31T00:00:00Z,1970 年前的时刻同样能表达。DateTime.UnixEpoch就是纪元时刻本身。
十、MonoTime:给代码掐表
想知道"这段代码跑了多久",用 MonoTime——单调时钟,只进不退,不受系统改时间的影响,是测耗时的正确工具:
import std.time.*
main(): Int64 {
let start = MonoTime.now()
var sum = 0
for (i in 0..1000000) {
sum += i
}
let elapsed = MonoTime.now() - start
println("sum=${sum}")
println("耗时:${elapsed}")
println("耗时(毫秒):${elapsed.toMilliseconds()}")
0
}
我机器上的一次运行(你的数值会不同):
sum=499999500000
耗时:2ms429us200ns
耗时(毫秒):2
MonoTime.now() - MonoTime.now() 直接得到 Duration,配合第四节的 toXxx() 想看什么单位都行。<、> 比较也支持(时间越往后值越大)。
小提示:为什么不用
DateTime.now()相减测耗时?因为系统时间可能被 NTP 校准、被手动调整,两次取值之间时钟"倒退"就会算出负数;MonoTime的读数单调递增,专门为"测间隔"设计。
十一、CIDE 实操:日志巡检工具再进化
兑现上一课的预告:把日志里的时间字符串真正解析成时间对象。在 S2-01 的日志巡检工具上加三个新能力:
- 把每条日志的时间串
parse成DateTime; - 打印每条日志距上一条的间隔秒数(时刻相减);
- 统计整批日志的时间跨度。
11.1 数据文件
沿用上一课的 data/app.log(六行服务日志):
2026-10-09 08:30:15 [INFO] auth - 用户 user01 从 192.168.1.23 登录
2026-10-09 08:31:02 [ERROR] db - 连接超时: 192.168.1.10:5432
2026-10-09 08:31:45 [WARN] auth - 密码重试过多: user07
2026-10-09 08:32:20 [INFO] api - 请求 /api/orders 耗时 42ms
2026-10-09 08:33:05 [ERROR] api - 调用支付网关失败: 10.0.0.8
2026-10-09 08:34:10 [INFO] auth - 用户 13812345678 修改了手机号 13998887766
11.2 完整代码
src/main.cj 全文:
package s2probe
import std.time.*
import std.regex.*
import std.fs.*
import std.unicode.*
main(): Int64 {
// 1. 读入日志文本(Windows 文件是 \r\n 换行,逐行后 trim 掉行尾的 \r)
let raw = String.fromUtf8(File.readFrom(Path("data/app.log")))
let lines = raw.split("\n")
.map({ l: String => l.trim() })
.filter({ l: String => l != "" })
.toArray()
// 2. 逐行抽时间串,解析成 DateTime
let lineRe = Regex("^(\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}) \\[(\\w+)\\] (\\w+) - (.+)$")
var prev: Option<DateTime> = None
var first: Option<DateTime> = None
var last: Option<DateTime> = None
for (line in lines) {
match (lineRe.find(line, group: true)) {
case Some(m) =>
let tStr = m.matchString(1)
let level = m.matchString(2)
let dt = DateTime.parse(tStr, "yyyy-MM-dd HH:mm:ss")
match (prev) {
case Some(p) =>
let gap = dt - p
println("${dt.format("HH:mm:ss")} [${level}] 距上条 ${gap.toSeconds()} 秒")
case None =>
println("${dt.format("HH:mm:ss")} [${level}] 第一条")
}
if (first.isNone()) { first = Some(dt) }
last = Some(dt)
prev = Some(dt)
case None => println("格式不认识的行:${line}")
}
}
// 3. 时间跨度
match ((first, last)) {
case (Some(f), Some(l)) =>
println("时间跨度:${(l - f).toSeconds()} 秒")
case _ => println("没有可解析的日志")
}
0
}
11.3 逐段看懂
读文件与拆行是第 22 课和 S2-01 的老三样:File.readFrom → String.fromUtf8 → 按 \n 拆行 → trim 去掉行尾 \r。链式 lambda 的参数类型标注 { l: String => ... } 不能省(S2-01 实战踩过的坑)。
正则 + parse 的接力是本课的核心:lineRe 的 1 号捕获组把 2026-10-09 08:31:02 原样取出,交给 DateTime.parse(tStr, "yyyy-MM-dd HH:mm:ss")。为什么必须用模式串? 回看 7.1:默认格式要求 T 分隔 + 必带时区,日志串两样都不满足,直接 parse 必抛 TimeParseException。
var prev: Option<DateTime> 记住上一条的时间——第一条没有"上条",用 Option 表达"可能还没有",正是第 9 课 Option 的本职。dt - p 时刻相减得 Duration,.toSeconds() 拿到整数秒。
元组模式匹配 match ((first, last)) 把两个 Option 一次性解构——case (Some(f), Some(l)) 同时命中"首尾都有值"才算出跨度。这比嵌套两层 match 简洁得多。
11.4 运行与验证
08:30:15 [INFO] 第一条
08:31:02 [ERROR] 距上条 47 秒
08:31:45 [WARN] 距上条 43 秒
08:32:20 [INFO] 距上条 35 秒
08:33:05 [ERROR] 距上条 45 秒
08:34:10 [INFO] 距上条 65 秒
时间跨度:235 秒
上一课我们从日志里"看见"了时间,这一课时间真正"可计算"了:间隔、跨度都是一行表达式的事。把这套能力记熟,第 16 课的多线程日志分析器里还会再用。
十二、常用 API 速查
DateTime(需 import std.time.*)
| 类别 | API | 说明 |
|---|---|---|
| 当前 | nowUTC() / now(timeZone:) |
UTC 时刻 / 指定时区时刻(timeZone: 是命名参数) |
| 构造 | of(year:, month:, dayOfMonth:, hour:, minute:, second:[, nanosecond:][, timeZone:]) |
全命名参数;month 收 Int64 或 Month;纳秒默认 0、时区默认本地 |
| 构造 | ofUTC(year:, ...) / ofEpoch(second:, nanosecond:) |
UTC 版 / 时间戳版(支持负数) |
| 时间戳 | fromUnixTimeStamp(Duration) / toUnixTimeStamp(): Duration / UnixEpoch |
互转与纪元 |
| 字段 | year / month / dayOfMonth / dayOfWeek / dayOfYear / hour / minute / second / nanosecond / zoneId / zoneOffset |
单值属性 |
| 字段 | date / time / isoWeek |
元组属性(不可直接插值,先解构) |
| 推进 | addYears / addMonths / addWeeks / addDays / addHours / addMinutes / addSeconds / addNanoseconds(n) |
位置参数;负数即回退;月末截断 |
| 运算 | dt + Duration、dt - Duration、dt1 - dt2、< > ==、compare |
与 Duration 混算、差值、比较 |
| 转时区 | inUTC() / inLocal() / inTimeZone(tz) |
只改显示不改瞬间 |
| 格式化 | format(模式串) |
预设 DateTimeFormat.RFC3339 等也是 String,直接传 |
| 解析 | parse(串) / parse(串, 模式串) / tryParse(串): Option<DateTime> |
默认格式必须带时区;失败抛 TimeParseException |
Duration(std.core,无需 import)
| 类别 | API | 说明 |
|---|---|---|
| 常量 | Zero / nanosecond / microsecond / millisecond / second / minute / hour / day |
打印 0s 1ns ... 1d |
| 构造 | 常量组合:Duration.minute * 2 + Duration.second * 30 |
1.2.0 的 Duration.of 不可从用户代码调用,别用 |
| 运算 | + -(两个 Duration)、* Int64、/ Int64、/ Float64 |
一元负号不支持;负值用 Zero - x 或 * -1 |
| 换算 | toDays / toHours / toMinutes / toSeconds / toMilliseconds / toMicroseconds / toNanoseconds |
向下截断成整数 |
| 比较 | == < > <= >=、compare、abs() |
TimeZone 与 MonoTime
| API | 说明 |
|---|---|
TimeZone.UTC / TimeZone.Local |
两个常量 |
TimeZone(id, Duration) |
固定偏移构造(Windows 表达"北京时间"的标准写法) |
TimeZone.load(id) |
按名加载;Windows 上实测抛 No valid timezone file is found. |
MonoTime.now() |
单调时钟取值;相减得 Duration,测耗时专用 |
十三、常见问题 FAQ
Q1:DateTime.of 一堆参数到底哪个能省?
只有 nanosecond:(默认 0)和 timeZone:(默认本地)可省,前六个(年月日时分秒)必填且必须带参数名。位置传参报 missing argument prefix 'year:' ...。
Q2:DateTime.now() 为什么不能无参调用?
报 missing argument prefix 'timeZone:' for named parameter。库的设计意图是逼你显式表态"要哪个时区的时间":要 UTC 用 nowUTC(),要本地用 now(timeZone: TimeZone.Local)。
Q3:Duration.of(...) 为什么编译不过?
1.2.0 里它不可从用户代码调用(no matching function declaration for function call 'of')。用常量组合代替:Duration.of(sec: 90, ns: 5) 的意图写成 Duration.minute + Duration.second * 30 + Duration.nanosecond * 5。
Q4:-Duration.second 报错,负时长怎么表达?
Duration 没实现一元负号(invalid unary operator '-')。用 Duration.Zero - Duration.second 或 Duration.second * -1。
Q5:parse("2026-10-10T08:30:15") 抛 Illegal zone offset value.?
默认格式必须带时区:+08:00 或 Z。不带时区的串用模式串解析:DateTime.parse("2026-10-10 08:30:15", "yyyy-MM-dd HH:mm:ss")。
Q6:格式符 "E" 怎么原样输出了?
E 不是本库的星期格式符(Java 老习惯别带过来)。星期用 w(数字,周日=0)、www(缩写)、wwww(全名),中文星期见 6.2 的映射写法。
Q7:TimeZone.load("Asia/Shanghai") 抛异常?
Windows 平台没有时区数据库文件,load 实测抛 No valid timezone file is found.。用固定偏移构造:TimeZone("UTC+8", Duration.hour * 8)。
Q8:"${dt.date}" 编译报错?
元组属性没实现 ToString(报 should implement interface 'ToString')。解构 let (y, m, d) = dt.date 后再拼,或用下标 dt.date[0]。
十四、课后练习
练习原则照旧:每题只考本课知识点,题面给全提示,先想清楚 API 再动手。前 4 题必做,第 5 题选做。
1.(必做)构造与格式化。 用 DateTime.of 构造"2026 年 10 月 1 日 9 点 0 分 0 秒",用 format 打印成 2026年10月01日 09:00:00。提示:模式串 yyyy年MM月dd日 HH:mm:ss(dd/HH 补零)。
import std.time.*
func ex1(): Unit {
let dt = DateTime.of(year: 2026, month: 10, dayOfMonth: 1, hour: 9, minute: 0, second: 0)
println(dt.format("yyyy年MM月dd日 HH:mm:ss"))
}
期望输出:
2026年10月01日 09:00:00
2.(必做)月末陷阱观察。 构造 2026-01-31,打印 addMonths(1) 与 addMonths(2) 的结果(println 默认格式即可),并打印 2 月 28 日是星期几。提示:dayOfWeek 返回 DayOfWeek 枚举,直接插值打印英文名。
import std.time.*
func ex2(): Unit {
let d = DateTime.of(year: 2026, month: 1, dayOfMonth: 31)
println(d.addMonths(1))
println(d.addMonths(2))
println(d.addMonths(1).dayOfWeek)
}
期望输出:
2026-02-28T00:00:00+08:00
2026-03-31T00:00:00+08:00
Saturday
3.(必做)模式串解析与时间戳。 用模式串解析 "2026/12/31 23:59:59",打印解析结果与它的 Unix 时间戳整数秒。提示:DateTime.parse(s, "yyyy/MM/dd HH:mm:ss");toUnixTimeStamp() 返回 Duration,链 .toSeconds()。
import std.time.*
func ex3(): Unit {
let dt = DateTime.parse("2026/12/31 23:59:59", "yyyy/MM/dd HH:mm:ss")
println(dt)
println(dt.toUnixTimeStamp().toSeconds())
}
期望输出:
2026-12-31T23:59:59+08:00
1798732799
4.(必做)MonoTime 掐表。 用 MonoTime 测量"对 0..500000 求和"这段循环的耗时,打印毫秒数(toMilliseconds())。提示:let start = MonoTime.now(),循环 var sum = 0; for (i in 0..500000) { sum += i },MonoTime.now() - start 得 Duration。运行数值因机器而异,打印格式为纯数字即可。
import std.time.*
func ex4(): Unit {
let start = MonoTime.now()
var sum = 0
for (i in 0..500000) {
sum += i
}
let elapsed = MonoTime.now() - start
println("sum=${sum}")
println("耗时毫秒:${elapsed.toMilliseconds()}")
}
期望输出(耗时数值因机器而异):
sum=124999750000
耗时毫秒:1
5.(选做)电影散场时刻。 一部电影时长 2 小时 15 分 30 秒,20:45:00 开场。用 Duration 表达片长(常量组合),时刻加时长得到散场时刻,按 HH:mm:ss 格式化打印。提示:Duration.hour * 2 + Duration.minute * 15 + Duration.second * 30;dt + 片长。
import std.time.*
func ex5(): Unit {
let begin = DateTime.of(year: 2026, month: 10, dayOfMonth: 10, hour: 20, minute: 45, second: 0)
let movie = Duration.hour * 2 + Duration.minute * 15 + Duration.second * 30
println("散场:${(begin + movie).format("HH:mm:ss")}")
}
期望输出:
散场:23:00:30
下节预告
我们的程序一直是"哑巴"——所有输入要么写死在代码里,要么靠改文件。真实世界的工具第一件事就是听懂命令行:tool.exe -i input.log -o out.txt --verbose 这种参数串怎么优雅地接住?下一课 命令行参数解析(std.argopt) 带你把"短选项、长选项、带值选项、位置参数"一次理清,顺手给本课的日志工具装上"指定日志路径"的命令行入口。
💬 遇到问题?扫码联系作者
跟着课程练习时,如果在 SDK 安装、环境变量配置、编译报错或调试上卡住,欢迎扫码加作者企业微信直接咨询(请备注"仓颉课程"):

离线环境下图片可能加载不出来,也可以在 CIDE 菜单 Help ▸ 联系作者 / Contact 中查看同一张二维码(应用内置兜底图,无需联网)。
📥 工具下载
本系列全程使用的仓颉 IDE —— CIDE(免费开源、社区版):
- GitCode 仓库 / 安装包下载:https://gitcode.com/wp_upala/cide
- 打开页面后进入 发行版(Releases),两种包任选其一:
- 安装版:下载
CIDE-<版本>-x64-Setup.exe,双击安装,适合日常长期使用; - 免安装版(Portable):下载
CIDE-<版本>-x64-Portable.zip,解压到任意目录即用,不写注册表、不留安装痕迹,拷到 U 盘也能在别的电脑直接运行(包内附《使用说明.txt》)。适合先试用、或在受限电脑上学习本系列课程。
- 安装版:下载
- 仓颉 SDK 请前往仓颉编程语言官网下载:https://cangjie-lang.cn

浙公网安备 33010602011771号