跳转至

时间、日期与 locale — 第一个项目与国际化

这一章讲什么: 两件事缠在一起。第一,方法论——原书用它招牌的 「研究真命令 → 仿写简化版」流程做了第一个完整项目: date 命令的仿写版 spl_date,从只能打印当前时间一路加到支持自定义格式、时间偏移、多语言输出。 第二,知识——Unix 怎么表示时间(Epoch 秒数、拆解结构体)、 时区与 locale 怎么让同一份代码说出世界各地的话。 读完你会拿到:全书共享代码库的组织方式、Unix 时间 API 的全景、 以及国际化的完整链路。

1. 顶层全景:一个项目的一生

  ① 挑一个现成命令(date)→ apropos 找到它的 man 页
  ② 读 man 页:它用了什么系统资源?——系统时钟
  ③ 顺藤摸瓜:time(7) 概览 → time()/localtime()/strftime() 的 man 页
  ④ 仿写 spl_date1:默认格式
  ⑤ 加功能:自定义格式串(spl_date2)→ 时间偏移 -d(spl_date3)
  ⑥ 国际化:setlocale() 一行,输出丹麦语/德语/日语(spl_date4)
  ⑦ 顺藤摸到的另一半:locale 是什么、12 个类别、时区文件、
     locale 定义文件、查询接口(localeconv/nl_langinfo)

图说:左边是动作,右边是每一动碰到的概念。这一章的骨架就是 「一次完整的项目循环」,之后的每一章都会再循环一次。

主线例子当然是 spl_date 的四次演进——每一步都对应一个机制: time() 取数、localtime() 拆解、strftime() 格式化、 mktime() 归一化、setlocale() 换语言。

2. 先建地基:全书的公共代码库

动手前,原书先做了一件之后每天都在受益的事:把多个项目要共用的代码收进 一个公共库。书的配套源码仓(GitHub 上的 stewartweiss/intro-linux-sys-prog) 顶层有 include/(头文件)和 lib/(库),所有项目共享的文件叫 common_hdrs.h——它再包含系统头文件合集 sys_hdrs.h 和自建的 get_nums.h、error_exits.h1。

sys_hdrs.h 里就是那几张熟脸:<sys/types.h>、<stdlib.h>、 <stdio.h>、<string.h>、<limits.h>、<unistd.h>(系统调用原型)、 <errno.h>2。每个头文件都用头文件保护(header guard)包起来:

#ifndef COMMON_HDRS_H
#define COMMON_HDRS_H
--snip--
#endif

图说:没有保护,嵌套的 #include 会把同一份定义复制多份,编译器报重定义错误; 有了保护,第二次 include 时 #ifndef 不成立,整段被跳过。 机制是预处理器 的宏开关,每个头文件都该有3。

get_long()/get_int():把 strtol 的四查法收进函数

第 2 章的 strtol 四查法每次重写太啰嗦,公共库把它固化成两个函数: get_long(字符串, 标志, &值, &错误消息) 返回 0(VALID_NUMBER)或负错误码。 标志位按位或: NO_TRAILING(有尾随字符报错)、NON_NEG_ONLY、 POS_ONLY、ONLY_DIGITS(没数字报错);错误码从 -1(FATAL_ERROR)到 -5(NEG_NUM_FOUND)4。get_int() 更薄:调 get_long() 再查 INT_MAX/INT_MIN 范围5。行为完全由调用方的标志决定:

$ ./test_get_long 32kjk        # 带 NO_TRAILING
return: TRAILING_CHARS_FOUND.  message: Trailing characters follow the number: "kjk"
$ ./test_get_long 32kjk        # 不带
return: VALID_NUMBER           ← 同一个输入,宽容模式照单全收
$ ./test_get_int -28734898798798798798879879
return: FATAL_ERROR            message: Numerical result out of range

图说:标志位是「调用方声明这条输入多干净」的契约。 需要严格的地方 (比如解析端口号)传 ONLY_DIGITS|NO_TRAILING,宽容的地方传 0。

三个错误处理函数与文件组织

error_exits.h 里三个函数,语义从名字就能读出来6:

函数 干什么 何时用
error_mssge(errno值, 消息) 打错误到 stderr,不死 错误可恢复
fatal_error(errno值, 消息) 打完 exit(EXIT_FAILURE) 不可恢复
usage_error(用法串) 打用法到 stderr 并退出 用户敲错命令行

errno 值传正数就用 strerror 翻译成系统消息,传非正数就直接打印自带消息7。 这些公共代码编译打包成静态库 libspl.a(用 GNU ar,附录 A 教怎么做), 各章节目录取其所需8。

判断(我们的,不是书里的): 这个三函数错误接口的设计很Unix—— 「用法错误」「致命错误」「可恢复错误」三分,正好对应系统程序面对的三类 失败;而「errno 正数走 strerror,非正数走自定义消息」的双通道,让项目 自定义错误码和系统错误码共存于同一个参数位。这套接口之后 17 章里会 出现几百次,值得现在就看懂为什么这样切。 如果错,会错在: 多线程程序里这三个函数都没考虑线程局部错误报告 (strerror 非线程安全,原书第 2 章自己也提过);第 16 章线程同步之后, 这套「打印即退出」的风格也要让位于更仔细的错误传播。

3. 方法论:man 页驱动的五步研究流程

第一个项目从研究真命令开始。目标是仿写 date,流程是原书全书的模板9:

  1. apropos 找命令:apropos -s1 -e date(不加 -e 会匹配 64 行噪音)
  2. 读 man 页:POSIX 版(date(1posix))是「规范」,Linux 版(date(1))是
     「实际实现」——两版对照着读,差别本身就是知识点
  3. 跑真命令、提问题:它怎么知道星期几?时区哪来的?取时间是系统调用吗?
  4. apropos -s7 -e time 找概览页——第 7 节的概览页是研究任何新主题的第一站
  5. 从概览顺藤到具体函数的 man 页

图说:第 ① 步的 -e 教训很典型——关键词搜索默认子串匹配, apropos -s1 date 连 AppArmor 的文档都捞出来;先宽搜再收窄是常态。

date 命令本身先露了几手:-d 'next Thu'、-d '5 years ago' 这种 自然语言时间表达式(GNU 扩展,作者把完整文档塞进了 Info);输出格式由 +"FORMAT" 控制,%m 是两位月、%a 是缩写星期几、%D 是 mm/dd/yy; 不传格式时的默认格式等价于 +"%a %b %e %H:%M:%S %Z %Y",而且是 "取决于 LC_TIME locale 类别"的——这一句把 locale 提前拽进了视野10。

4. Unix 怎么表示时间:三种时间,两层结构

time(7) 概览页把 Unix 的时间分成两类:进程时间(进程耗在 CPU 上的时间, 第 4 章讲怎么取)和实时(real time,从某个参照点起算的流逝时间)。 参照点固定的实时叫日历时间(calendar time)——参照点就是 Epoch (纪元):1970 年 1 月 1 日 00:00:00 UTC,大致是第一台 Unix 系统发布的日子。 Unix 的日历时间 = 从 Epoch 起流逝的秒数11。UTC(Coordinated Universal Time,协调世界时,旧称格林威治时间)的字母顺序是当年国际协商 的妥协,顺序没错12。

日历秒数 ↔ 拆解结构:time_t 与 struct tm

秒数适合存储和计算,人不认。于是有了拆解时间(broken-down time): 把秒数拆成年、月、日、时、分、秒、星期、年积日……装进一个结构体 struct tm(定义在 <time.h>)13:

struct tm {
    int tm_sec;   /* 秒 0–60(60 是给闰秒留的)      */
    int tm_min;   /* 分 0–59                          */
    int tm_hour;  /* 时 0–23                          */
    int tm_mday;  /* 月内日 1–31                      */
    int tm_mon;   /* 月 0–11 ← 从 0 开始,著名的坑    */
    int tm_year;  /* 年 − 1900 ← 著名的坑            */
    int tm_wday;  /* 星期 0–6,周日=0                 */
    int tm_yday;  /* 年积日 0–365,1 月 1 日=0        */
    int tm_isdst; /* 夏令时:>0 生效,=0 不生效,<0 未知 */
};

图说:九个字段里埋着两个「著名坑」——tm_mon 从 0 数、tm_year 要 加 1900。 引一段原书放在这里的话:人类的时间单位「如此复杂、不对称、 互不衔接,以至于连贯的心算几乎不可能」——这正是要有拆解结构的动机14。

取当前时间:time()、clock_gettime()、gettimeofday()

候选有三个,原书做了实测对比15:

函数 返回 精度 备注
time(NULL) time_t(Epoch 秒数) 秒 参数传 NULL 时不会失败,连错误处理都省了
clock_gettime(CLOCK_REALTIME, &ts) timespec{秒, 纳秒} 纳秒 更通用(第 9 章用它拿单调时钟);要 _POSIX_C_SOURCE >= 199309L
gettimeofday(&tv, NULL) timeval{秒, 微秒} 微秒 POSIX.1-2008 起已废弃(obsolete),时区参数更早废弃

性能实测(各调用 1000 万次,x86-64、Linux 5.15.0): time() 用 0.032 秒,clock_gettime() 用 0.171 秒——快 5 倍多, 所以 spl_date 选 time()16。这个对比顺带教了一招:man 页 SEE ALSO 里 推荐的「更好的替代品」常常有性能代价,选哪个要实测,不看名头。

转换函数族:一张表全收

man ctime 一页装下了转换全家,原书归纳成一张完美的表17:

函数 输入 → 输出 说明
gmtime(&t) time_t → struct tm* 按 UTC 拆
localtime(&t) time_t → struct tm* 按用户时区拆(TZ 环境变量)
mktime(&tm) struct tm → time_t 反向;顺带把越界字段归一化(「10 月 40 日」变「11 月 9 日」),还会重算星期和年积日
asctime(&tm) struct tm → 字符串 固定格式
ctime(&t) time_t → 字符串 固定格式
strftime(buf, max, 格式, &tm) struct tm + 格式串 → 字符串 格式化主力,格式符和 date 命令几乎一样
strptime(字符串, 格式, &tm) 反向解析 —

两个淘汰注记:asctime()/ctime() 已被标 obsolete,用 strftime() 替代18;gettimeofday() 同理被 clock_gettime() 替代15。 学到一个通用判据:文档里标 deprecated/obsolete 的东西别碰——维护方 自己都说要淘汰了19。

5. spl_date 的四次演进(主线例子)

spl_date1:三通最小可用版

  time(NULL)          localtime()            strftime(buf, n, "%c", tm)      printf
      │                    │                         │                            │
  Epoch 秒数 ────────► struct tm ───── 按 "%c" 格式化 ─────► 字符串 ──► 屏幕
  (time_t)            (本地时区)        (locale 的日期时间)

图说:三步转换就是 spl_date1 的全部骨骼。 程序逻辑三行:取时间、 拆解、格式化打印20。%c 是「locale 的日期时间」格式符; glibc 不支持 man 页里提到的 %+,所以用它近似 date 的默认输出21。

spl_date2:加自定义格式

第二版只加了一个 getformat() 函数:命令行最后一个参数若以 + 开头 就把它(去掉 +)当作格式串;太长(超过 MAXLEN)或不是 + 开头就报用法 错误22。运行记录:

$ ./spl_date2
Wed Mar 26 15:07:25 2025
$ ./spl_date2 today
spl_date2: format should be +"format-string"
$ ./spl_date2 +"Today is day %e of %B. It is now %r"
Today is day 26 of March. It is now 03:08:49 PM

图说:格式串校验(长度、开头)在解析层做完,strftime 只管格式化。 strftime 返回 0 也算错误(它表示「转换失败或产出空串」)23。

spl_date3:加时间偏移 -d "2 years 4 months"

第三版加了两个能力:选项解析(getopt,选项串 ":d:h"——注意开头的 :, 让「缺参数」和「无效选项」两种错误可区分)和时间偏移解析。

用户接口的设计本身就是教材:时间偏移串接受 1 year 4 months -2 days 这种「数字+单位」的序列,数字必须阿拉伯数字(不接受 "two years"), 负数只作用于紧邻的单位(-4 hours 5 minutes 是 −4 小时再 +5 分钟), 允许重复单位,禁止小数和逗号。原书写出了完整文法24:

time-adjustment = <num> <time-unit> [<num> <time-unit> ...]
num             = [+|-]<integer>
time-unit       = year[s] month[s] week[s] day[s] hour[s] minute[s] second[s]

模糊时间的坑值得单独讲:「7 月 31 日减一个月」是几号?没有 6 月 31 日。 GNU date 的规则是:月数直接加减,日期不合法时归到最近的合法日期 (Dec 30 +2 months → Mar 2)25。恰好 mktime() 的归一化语义 和这个规则一致——把偏移加进 struct tm 再调 mktime(),它自动把 「10 月 40 日」改成「11 月 9 日」并重算星期/年积日,这个难题整包交给 库函数26。

解析器用 strtok(串, " \t") 切词(空格和制表符当分隔符),每轮循环取两个 token——数字用公共库的 get_int() 收(带 ONLY_DIGITS|NO_TRAILING), 单位用 strstr() 匹配子串。整型加法累积进一个全零 struct tm 的各个 字段,最后 adjust_time() 把它加进当前时间并调 mktime() 归一化27。

性能与设计的权衡(原书框):解析器其实可以一遍扫描手写,但「跑得不多的 代码不需要快」——用久经测试的库函数、两遍扫描也无所谓;要又快又久经考验, 只有热点代码才值得抠28。

spl_date4:国际化只需一行

if ( setlocale(LC_TIME, "") == NULL )
    fatal_error(LOCALE_ERROR, "setlocale() could not set the given locale");

图说:在调任何库函数之前调 setlocale(LC_ALL, "") 或按需传类别, 是所有 locale 感知函数改变行为的总开关。 不传这个调用,程序永远活在 默认的 "C" locale(美式英语、ASCII 排序)29。效果立竿见影:

$ LC_TIME=ja_JP.utf8 ./spl_date4 "+%A, %d %B %Y"
水曜日, 26 3月 2025
$ LC_TIME=fi_FI.utf8 ./spl_date4
ke 26. maaliskuuta 2025 15.56.07
$ LC_TIME=da_DK.utf8 ./spl_date4
ons 26 mar 2025 15:50:40 EDT

同一个程序、同一份代码,环境变量一换,输出就说起了日语、芬兰语、 丹麦语——前提是 strftime 这类函数本来就是 locale 感知的。 glibc 的哲学:程序员不该为国际化背负重担,setlocale 一开, 该变的行为自动变30。

6. locale 机制全解:从环境变量到程序接口

locale 是什么:一组类别

locale(7) 页的定义:locale 是一组语言和文化规则,覆盖消息语言、 字符集、排序习惯等31。它由类别(category)组成,每个类别管一类 文化设定,名字都以 LC_ 开头。POSIX 定义 6 个,glibc 2.2 起又扩了 6 个, 共 12 个32:

类别 归属 管什么
LC_COLLATE POSIX 排序顺序
LC_CTYPE POSIX 字符分类与大小写转换
LC_MESSAGES POSIX 消息与诊断格式
LC_MONETARY POSIX 货币格式
LC_NUMERIC POSIX 非货币数字格式
LC_TIME POSIX 日期时间格式
LC_ADDRESS glibc 地址/地理格式
LC_IDENTIFICATION glibc locale 自身的元数据
LC_MEASUREMENT glibc 度量衡(公制/美制)
LC_NAME glibc 称呼用语
LC_PAPER glibc 纸张尺寸(A4/Letter)
LC_TELEPHONE glibc 电话格式

图说:12 个格子,每个格子是一组「这类事在该文化里怎么做」的答案。 类别名同时是宏(给程序用)和环境变量(给用户用)——同一个名字两种身份。

管 locale 的环境变量与优先级

除 12 个 LC_* 类别外,还有几个总管变量33:

变量 作用
LC_ALL 凌驾一切:非空时覆盖所有类别
LANG 兜底:没有任何 LC_* 时用它
LANGUAGE glibc 的 gettext 翻译用
TZ 时区(见下)
NLSPATH / LOCPATH 消息目录 / locale 数据文件的查找路径

setlocale(类别, "") 的查值顺序就是这三步:*LC_ALL → 与类别同名的 LC_ 变量 → LANG**34。bash 里可以临时改:TZ=Spain LC_ALL=es_ES.utf8 date +"%c" 只对这一条命令生效35。

时区:TZ 与 zoneinfo

时区信息存在 /usr/share/zoneinfo/ 下,按 RFC 8536 的标准二进制格式 (含夏令时规则)。里面大多是目录:America/、Europe/……每个目录下是 以城市/地区命名的文件,也有符号链接(如 Greenwich)36。 TZ 的值形如 :America/New_York,冒号开头表示按 zoneinfo 路径解析37。

关键省心事实:除了 gmtime() 这种明说不理时区的函数,所有时间函数 都自动正确处理时区——C 库全包了,程序什么都不用做38。 用户没设 TZ 时,默认用系统时区(/etc/timezone 或其软链所指)。

用户级命令:locale

locale 不带参数打印当前各类别的值(环境里的直接显示,推导来的加引号); -a 列出系统可用 locale;-av 连详情(标题、语言、地区、代码集)一起列; -k LC_TIME 列出某类别的全部关键字值;-ck date_fmt 查单个关键字39。

locale 名字的语法:language[_territory][.codeset][@modifier],如 en_US.UTF-8——ISO 639 语言码 + ISO 3166 国家码 + 字符集40。 codeset(代码集)是「图形字符 → 数字」的映射;数字叫码位(code point)。 ASCII 是 7 位早期代码集;UTF-8 是变长代码集,1 到 4 字节表示任意 Unicode 码位;Unicode 则是几乎所有已知文字的数值编码(附录 B 有完整故事)41。

三个预定义 locale 要认得:C / POSIX / C.utf8。POSIX.1-2024 要求 每个系统都有 POSIX locale,且它是所有 C 程序的默认 locale——所以 不调用 setlocale 的程序活在一个讲英语、按 ASCII 排序的世界里42。 想生成新 locale:改 /etc/locale.gen 取消注释,跑 locale-gen(要 root); 系统支持的全部名单在 /usr/share/i18n/SUPPORTED——i18n 这个目录名本身 就是 internationalization 的缩写(i 和 n 中间 18 个字母)43。

locale 定义文件与 localedef

locale 数据从哪来?第 5 节 man 页(locale(5))讲了格式:locale 定义文件 是一种类似 XML 的标记语言,由 localedef 命令编译成压缩二进制; 程序读的是二进制,不是定义文件44。看一个最小的真实例子 (en_US 的 LC_NUMERIC 类别)45:

LC_NUMERIC
decimal_point   "<period>"
thousands_sep   "<comma>"
grouping        "3;0"
END LC_NUMERIC

三行关键字:小数点是点、千分位是逗号、每 3 位一组——美国习惯。 对照欧洲国家,decimal_point 和 thousands_sep 恰好互换(第 2 章那个 歧义例子的根源就在这里)。LC_CTYPE 的定义文件大得多(字符分类表、 大小写映射表),但格式相同46。

程序接口:setlocale 与「谁是 locale 感知的」

setlocale(int category, const char *locale) 是总开关: 第二参数是具体名字(如 "en_US.UTF-8")就设定;是 "" 就按环境变量查; 是 NULL 就只查询不改动47。glibc 的省心哲学:程序只管 setlocale, 哪些函数感知 locale、用哪个类别的数据,是库的事30。

原书给了约 40 个 locale 感知函数的清单(fprintf、strerror、perror、 strftime、strcoll、字符分类全家 isalpha/isdigit/……、宽字符 与多字节转换 mbstowcs 等),来源是 POSIX 的系统接口卷48。查某个函数 用不用 locale:在它的 man 页里搜 LC_,或在 setlocale 的 Info 页 / POSIX 网站查49。

strcoll 实验:排序是文化的

strcmp 按字节比(ASCII 序),strcoll 按 locale 的 LC_COLLATE 比。 原书的对比演示一击致命50:

$ LC_COLLATE=C        ./strcoll_demo Zebra lion camel ape   → Zebra
$ LC_COLLATE=en_US.utf8 ./strcoll_demo Zebra lion camel ape → ape

C locale 里所有大写字母排在小写前面(Zebra 的 Z 赢了);en_US.utf8 的 排序忽略大小写(ape 的 a 最小)。换成 strcmp,两种 locale 下都输出 Zebra——字节序不等于文化序。任何「给字符串排序」的程序要想在别的 国家正确,必须用 strcoll()(或先 strxfrm() 变换再 strcmp)。

直接查 locale 数据:localeconv vs nl_langinfo

有时没有现成的 locale 感知函数,要直接读数据。两个查询接口51:

函数 返回 取舍
localeconv() 一个大结构体 struct lconv,只有 LC_MONETARY 和 LC_NUMERIC 的数据 更可移植,但慢、不可扩展
nl_langinfo(关键字) 单个关键字的值 glibc 称之「对 locale 的点对点访问」;还能查 LC_TIME 和 LC_MESSAGES

nl_langinfo 的例子:传 DAY_1…DAY_7(在 <langinfo.h> 里是连续 整数,可以循环)拿到当前 locale 的星期名52:

$ LC_ALL=es_ES.utf8 ./nl_langinfo_demo1   → domingo lunes martes miércoles …
$ LC_ALL=da_DK.utf8 ./nl_langinfo_demo1   → søndag mandag tirsdag onsdag …

locale 对象:线程各说各的话(进阶)

setlocale 是进程级的总开关——多线程时代,一个线程想按西班牙格式算钱、 另一个按日本格式显示日期,怎么办?POSIX 2008 加了 locale 对象: newlocale(类别掩码, 名字, base) 造一个 locale_t 对象 (base 传 0 表示新建,否则在已有对象上改);uselocale(loc) 让当前线程 换用它;用完 freelocale() 释放53。示例把西班牙的数字格式和日本的 时间格式拼进一个对象:

$ ./newlocale_demo es_ES.utf8 ja_JP.utf8
With numeric settings of es_ES.utf8, number is: 123.456.789,12
With time settings of ja_JP.utf8, date/time is: 2023年09月25日 09時55分36秒

还有一批名字以 _l 结尾的函数(isalpha_l、strcoll_l……), 把 locale 对象作为最后一个参数显式传入——不同线程可以各用各的对象, 互不干扰54。释放顺序有讲究:先 uselocale(LC_GLOBAL_LOCALE) 换回来, 再 freelocale(),因为不能释放正在使用的对象55。

7. 作者的判断与证据

有明证的:

  • time() 比 clock_gettime() 快约 5 倍(0.032s vs 0.171s / 1000 万次)是 实测数据,附带了系统和内核版本16。
  • mktime() 的归一化语义直接引自 man 页原文26。
  • locale 类别的清单、环境变量优先级、setlocale 的三步查值,都是 POSIX 规定的事实3447。
  • strcoll/strcmp 的行为差异有对照实验50。

作者的判断:

  • 「解析器用库函数两遍扫、不手写一遍扫」——三条取舍原则(非热点不求快、 用久经测试的代码、长寿命代码要好维护)是经验之谈,不是定理28。
  • 「时间偏移不接受 'two years' 这类自然语言」是教学取舍,明确说 「不是生产软件」24。
  • glibc「程序员不该背国际化的重担」的哲学转述,是 GNU 的设计立场, 不是所有库都这么省心30。

8. 边界与局限

  • 宽字符与多字节(wchar、UTF-8 逐字符处理)只列了函数名没展开—— 真正的 Unicode 编程要读附录 B 加自己的修炼。
  • locale 对象的线程级用法只给了入门演示;_l 系列函数「并非所有系统 都支持」(POSIX 2008 才加入)54。
  • 消息翻译(gettext 体系:把 "Hello" 换成 "Hola" 的那套)只字未提—— 国际化的另一半,本书范围之外。
  • 夏令时规则、闰秒的处理全交给 C 库,本书不深究 zoneinfo 二进制格式36。
  • spl_date 系列没有实现 date 的读时钟设时区之外的系统调用面(如 date MMDDhhmm 设置系统时间需要特权,教学版略去)。

9. 可带走的

主线例子,一行复述: time(NULL) 拿 Epoch 秒 → localtime() 按 TZ 拆成 struct tm → strftime(buf,n,"%c",tm) 按 locale 格式化 → 加偏移就是 tm 字段加法 + mktime() 归一化 → 国际化就是开头加一句 setlocale(LC_TIME, "")。

  1. Unix 时间 = Epoch(1970-01-01 00:00:00 UTC)起的秒数;time_t 存它, struct tm 是给人看的拆解形式(记住 tm_mon 从 0、tm_year 要加 1900)。
  2. 取当前时间用 time(NULL)——不会失败、比 clock_gettime 快约 5 倍; 纳秒精度才用 clock_gettime();gettimeofday() 已废弃别碰。
  3. 转换链:gmtime/localtime 拆、mktime 合(自带归一化,模糊日期 整包交给它)、strftime 格式化。asctime/ctime 已废弃。
  4. 时间偏移的语义先定文法再写解析器;「7 月 31 日减一个月」这类模糊 问题,GNU 的规则是归一到最近合法日期,mktime 同款。
  5. 非热点代码别抠性能:用测试过的库函数,哪怕多扫一遍。
  6. *locale = 12 个类别(LC_);优先级 LC_ALL > LC_类别 > LANG; 默认 C locale 说英语按 ASCII 排序。
  7. 国际化的第一步永远是 setlocale(LC_ALL, "");不传就永远活在 C locale。
  8. 排序要文化正确用 strcoll,别用 strcmp;查 locale 数据用 nl_langinfo(关键字)(点对点)或 localeconv()(只货币和数字)。
  9. 时区全自动:TZ=:Europe/Paris,数据在 /usr/share/zoneinfo, 时间函数除 gmtime 都自动处理。
  10. locale 定义文件是源码,localedef 编译成二进制,程序读二进制; locale -k LC_TIME 能直接看任何类别的数据。

10. 原文地图

主题 原书章 原文位置
公共代码组织、sys_hdrs.h Chapter 3: Time, Dates, and Locales text/09-ch03-chapter-3-time-dates-and-locales.txt:87(搜「include and lib」) · :96(搜「sys/types.h」)
头文件保护 同上 :174(搜「include guard」)
get_long/get_int 与标志位 同上 :217(搜「NO_TRAILING」) · :355(搜「get_int」)
三个错误处理函数 同上 :420(搜「error_mssge」) · :451(搜「strerror」)
libspl.a 与目录结构 同上 :480(搜「libspl.a」)
man 页驱动研究流程 同上 :513(搜「apropos -s1 date」) · :521(搜「64 lines」)
date 的格式串与默认格式 同上 :649(搜「LC_TIME locale category」) · :611(搜「two-digit month」)
Epoch 与日历时间定义 同上 :701(搜「Epoch」) · :703(搜「Coordinated Universal Time」)
struct tm 九个字段 同上 :738(搜「struct tm」)
三个取时函数与性能实测 同上 :771(搜「clockid_t clockid」) · :825(搜「gettimeofday」) · :843(搜「obsolete」) · :856(搜「0.032 seconds」)
转换函数族与废弃注记 同上 :867(搜「asctime」) · :896(搜「obsolete」) · :839(搜「deprecated」)
spl_date1 程序流程 同上 :966(搜「time(NULL)」) · :917(搜「%+」)
spl_date2 格式串处理 同上 :1008(搜「getformat」) · :1070(搜「exceeds the size」)
时间偏移文法与模糊日期 同上 :1124(搜「time-adjustment =」) · :1154(搜「nearest date that's valid」)
mktime 归一化 同上 :1185(搜「encapsulates the corrections」)
性能设计三原则 同上 :1287(搜「doesn't need to be fast」)
strtok 解析器与 adjust_time 同上 :1409(搜「strtok」) · :1471(搜「mktime」)
locale 定义与 12 类别 同上 :1647(搜「set of language」) · :1665(搜「Categories」)
总管环境变量与优先级 同上 :1709(搜「LC_ALL」) · :2082(搜「non-NULL environment variable LC_ALL」)
时区 zoneinfo 与 TZ 同上 :1743(搜「RFC 8536」) · :1741(搜「America/New York」) · :1760(搜「C Library does all of the work」)
locale 命令族与 locale 名 同上 :1788(搜「displays information」) · :1827(搜「language[territory]」) · :1838(搜「UTF-8」)
预定义 locale 与生成 同上 :1862(搜「requires systems to have」) · :1905(搜「locale-gen」)
localedef 与定义文件 同上 :1944(搜「localedef」) · :1955(搜「decimal_point」)
setlocale 与 C locale 默认 同上 :2088(搜「must call setlocale」) · :2073(搜「doesn't change the locale」)
spl_date4 国际化 同上 :2102(搜「TZ and LC_TIME」) · :2149(搜「da_DK.utf8」)
locale 感知函数清单 同上 :2194(搜「fprintf」) · :2184(搜「freed as much as possible」)
strcoll 对照实验 同上 :2253(搜「Zebra lion camel ape」) · :2258(搜「ASCII ordering」)
localeconv vs nl_langinfo 同上 :2284(搜「very large data structure」) · :2288(搜「more portable」)
locale 对象与 _l 函数 同上 :2348(搜「locale_t」) · :2401(搜「LC_NUMERIC_MASK」) · :2440(搜「end in」)

  1. 出处:「Chapter 3: Time, Dates, and Locales」第 81–92 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:87,搜「include and lib」)。 ↩

  2. 出处:「Chapter 3: Time, Dates, and Locales」第 93–103 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:96,搜「sys/types.h」)。 ↩

  3. 出处:「Chapter 3: Time, Dates, and Locales」第 142–191 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:174,搜「include guard」)。 ↩

  4. 出处:「Chapter 3: Time, Dates, and Locales」第 211–248 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:217,搜「NO_TRAILING」)。 ↩

  5. 出处:「Chapter 3: Time, Dates, and Locales」第 351–375 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:355,搜「get_int」)。 ↩

  6. 出处:「Chapter 3: Time, Dates, and Locales」第 411–434 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:420,搜「error_mssge」)。 ↩

  7. 出处:「Chapter 3: Time, Dates, and Locales」第 438–445 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:451,搜「strerror」)。 ↩

  8. 出处:「Chapter 3: Time, Dates, and Locales」第 474–494 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:480,搜「libspl.a」)。 ↩

  9. 出处:「Chapter 3: Time, Dates, and Locales」第 496–560 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:513,搜「apropos -s1 date」)。 ↩

  10. 出处:「Chapter 3: Time, Dates, and Locales」第 604–654 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:649,搜「LC_TIME locale category」)。 ↩

  11. 出处:「Chapter 3: Time, Dates, and Locales」第 692–709 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:701,搜「Epoch」)。 ↩

  12. 出处:「Chapter 3: Time, Dates, and Locales」第 702–707 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:703,搜「Coordinated Universal Time」)。 ↩

  13. 出处:「Chapter 3: Time, Dates, and Locales」第 736–748 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:738,搜「struct tm」)。 ↩

  14. 出处:「Chapter 3: Time, Dates, and Locales」第 711–726 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:728,搜「single data structure」)。引文出自 Robert Grudin 的 Time and the Art of Living。 ↩

  15. 出处:「Chapter 3: Time, Dates, and Locales」第 762–844 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:771,搜「clockid_t clockid」) · :825(搜「gettimeofday」) · :843(搜「obsolete」)。 ↩↩

  16. 出处:「Chapter 3: Time, Dates, and Locales」第 845–859 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:856,搜「0.032 seconds」)。 ↩↩

  17. 出处:「Chapter 3: Time, Dates, and Locales」第 860–883 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:867,搜「asctime」)。 ↩

  18. 出处:「Chapter 3: Time, Dates, and Locales」第 895–897 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:896,搜「obsolete」)。 ↩

  19. 出处:「Chapter 3: Time, Dates, and Locales」第 839–841 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:839,搜「deprecated」)。 ↩

  20. 出处:「Chapter 3: Time, Dates, and Locales」第 948–982 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:966,搜「time(NULL)」)。 ↩

  21. 出处:「Chapter 3: Time, Dates, and Locales」第 916–924 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:917,搜「%+」)。 ↩

  22. 出处:「Chapter 3: Time, Dates, and Locales」第 989–1056 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1008,搜「getformat」)。 ↩

  23. 出处:「Chapter 3: Time, Dates, and Locales」第 1058–1072 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1070,搜「exceeds the size」)。 ↩

  24. 出处:「Chapter 3: Time, Dates, and Locales」第 1122–1131 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1124,搜「time-adjustment =」)。 ↩↩

  25. 出处:「Chapter 3: Time, Dates, and Locales」第 1145–1172 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1154,搜「nearest date that's valid」)。 ↩

  26. 出处:「Chapter 3: Time, Dates, and Locales」第 1176–1189 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1185,搜「encapsulates the corrections」)。 ↩↩

  27. 出处:「Chapter 3: Time, Dates, and Locales」第 1401–1478 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1409,搜「strtok」) · :1471(搜「mktime」)。 ↩

  28. 出处:「Chapter 3: Time, Dates, and Locales」第 1281–1296 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1287,搜「doesn't need to be fast」)。 ↩↩

  29. 出处:「Chapter 3: Time, Dates, and Locales」第 2051–2095 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:2088,搜「must call setlocale」)。 ↩

  30. 出处:「Chapter 3: Time, Dates, and Locales」第 2183–2188 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:2184,搜「freed as much as possible」)。 ↩↩↩

  31. 出处:「Chapter 3: Time, Dates, and Locales」第 1638–1653 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1647,搜「set of language」)。 ↩

  32. 出处:「Chapter 3: Time, Dates, and Locales」第 1664–1679 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1674,搜「defines six categories」)与第 1680–1702 段的类别表。 ↩

  33. 出处:「Chapter 3: Time, Dates, and Locales」第 1703–1726 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1709,搜「LC_ALL」)。 ↩

  34. 出处:「Chapter 3: Time, Dates, and Locales」第 2079–2087 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:2082,搜「non-NULL environment variable LC_ALL」)。 ↩↩

  35. 出处:「Chapter 3: Time, Dates, and Locales」第 1916–1934 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1918,搜「variable assignments」)。 ↩

  36. 出处:「Chapter 3: Time, Dates, and Locales」第 1735–1749 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1743,搜「RFC 8536」)。 ↩↩

  37. 出处:「Chapter 3: Time, Dates, and Locales」第 1740–1742 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1741,搜「America/New York」)。 ↩

  38. 出处:「Chapter 3: Time, Dates, and Locales」第 1760–1762 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1760,搜「C Library does all of the work」)。 ↩

  39. 出处:「Chapter 3: Time, Dates, and Locales」第 1763–1797 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1788,搜「displays information」)与第 2015–2049 段的 -k/-ck 示例。 ↩

  40. 出处:「Chapter 3: Time, Dates, and Locales」第 1825–1832 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1827,搜「language[territory]」)。 ↩

  41. 出处:「Chapter 3: Time, Dates, and Locales」第 1833–1842 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1838,搜「UTF-8」)。 ↩

  42. 出处:「Chapter 3: Time, Dates, and Locales」第 1861–1865 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1862,搜「requires systems to have」)。 ↩

  43. 出处:「Chapter 3: Time, Dates, and Locales」第 1903–1914 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1905,搜「locale-gen」) · :1912(搜「i18n」)。 ↩

  44. 出处:「Chapter 3: Time, Dates, and Locales」第 1936–1946 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1944,搜「localedef」)。 ↩

  45. 出处:「Chapter 3: Time, Dates, and Locales」第 1947–1967 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1955,搜「decimal_point」)。 ↩

  46. 出处:「Chapter 3: Time, Dates, and Locales」第 1968–2014 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:1968,搜「LC_CTYPE」)。 ↩

  47. 出处:「Chapter 3: Time, Dates, and Locales」第 2071–2078 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:2073,搜「doesn't change the locale」)。 ↩↩

  48. 出处:「Chapter 3: Time, Dates, and Locales」第 2193–2208 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:2194,搜「fprintf」)。清单约 40 个函数,横跨字符分类、格式化 I/O、字符串与宽字符处理。 ↩

  49. 出处:「Chapter 3: Time, Dates, and Locales」第 2202–2212 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:2208,搜「/LC_」)。 ↩

  50. 出处:「Chapter 3: Time, Dates, and Locales」第 2250–2262 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:2253,搜「Zebra lion camel ape」) · :2258(搜「ASCII ordering」)。 ↩↩

  51. 出处:「Chapter 3: Time, Dates, and Locales」第 2280–2292 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:2284,搜「very large data structure」) · :2288(搜「more portable」)。 ↩

  52. 出处:「Chapter 3: Time, Dates, and Locales」第 2293–2343 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:2312,搜「DAY_1」)。 ↩

  53. 出处:「Chapter 3: Time, Dates, and Locales」第 2345–2431 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:2348,搜「locale_t」) · :2401(搜「LC_NUMERIC_MASK」)。 ↩

  54. 出处:「Chapter 3: Time, Dates, and Locales」第 2439–2449 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:2440,搜「end in」)。 ↩↩

  55. 出处:「Chapter 3: Time, Dates, and Locales」第 2424–2431 段(text/09-ch03-chapter-3-time-dates-and-locales.txt:2420,搜「freelocale」)。 ↩