本文记录 humanize-duration 3.34.0 适配仓颉(Cangjie)的完整过程。适配目标是得到一个可以被仓颉项目直接依赖的跨端三方库,并在终端中用真实示例验证毫秒时长转换、单位拆分、舍入、语言、回退和格式化选项等能力。
项目地址: AtomGit/nutpi_cj/HumanizeDuration.js
上游仓库: EvanHahn/HumanizeDuration.js.git
开发工具: 华为云码道
一、背景
1.1 为什么适配 humanize-duration
humanize-duration 是一个把毫秒数转换为可读时长文本的 JavaScript 工具库。它可以把 12000 转换为 12 seconds,也可以将较长时长拆分为 1 day, 3 hours, 2 minutes,并支持语言、单位集合、最大单位数量、舍入、连接词、分隔符和小数位数等选项。
JavaScript 版本接收动态的 number 和配置对象,并通过语言表和运行时选项决定最终文本。仓颉强调静态类型、显式参数标签和编译期检查,因此直接逐行翻译 JavaScript 并不能得到清晰的仓颉 API。适配的重点是重新确定时长输入、单位配置、格式化选项和语言回退的类型边界。
本项目将 humanize-duration 的核心时长计算和格式化能力映射为 humanizeDuration、HumanizeOptions、defaultUnits 和 getSupportedLanguages 等仓颉入口,同时保留上游使用的 y、mo、w、d、h、m、s、ms 单位名称,降低迁移成本。
1.2 跨端三方库的适配边界
当前项目提供的是仓颉跨端库,不绑定某一个 UI 框架或操作系统。适配边界可以概括为:
| 边界 | 仓颉实现 |
|---|---|
| 包管理 | cjpm.toml、cjpm.lock、cjpm build 和静态库输出 |
| 时长输入 | Int64 与 Float64 毫秒重载,负数取绝对值 |
| 单位计算 | 年、月、周、日、时、分、秒和毫秒的固定换算表 |
| 格式选项 | HumanizeOptions 的单位、舍入、连接词、分隔符和小数配置 |
| 语言支持 | en、zh_CN、zh_TW、ja、ko、es、fr、de |
| 交付形态 | 仓颉静态库 libhumanize_duration_cangjie.a 和接口文件 humanize_duration_cangjie.cjo |
| 验证方式 | test/main.cj 断言测试和 example/example.cj 终端验收程序 |
这里没有 HAP 构建、ArkTS 桥接和真机系统能力验收。只要目标平台提供兼容版本的仓颉 SDK,就可以按同一套源码和 cjpm 配置构建并消费这个库。原始 JavaScript 文件、npm 配置和原有测试继续保留在项目中,不参与仓颉包的源码扫描。
1.3 适配后的能力
当前仓颉实现导出以下入口:
- 默认配置:
defaultUnits返回y、mo、w、d、h、m、s七个默认单位; - 核心函数:
humanizeDuration(Int64)和humanizeDuration(Float64)两种毫秒输入重载; - 配置对象:
HumanizeOptions支持language、fallbacks、units、largest、round、delimiter、spacer、decimal、conjunction、serialComma和maxDecimalPoints; - 语言查询:
getSupportedLanguages返回当前适配内置的 8 种语言; - 单位格式化:支持固定单位顺序、最大输出单位数、最小单位小数和四舍五入进位;
- 错误处理:未知语言和未知单位通过
IllegalArgumentException明确报告。
这些入口覆盖当前仓颉适配承诺的完整能力范围。原 JavaScript 版本中的自定义 languages 对象、unitMeasures、数字替换表和部分语言的特殊语法没有伪装成仓颉接口,兼容边界在第七节说明。
二、实现路线图
text
第 1 阶段:盘点 API ── 对照 humanize-duration 的时长和格式化选项,确定仓颉函数和目录边界
第 2 阶段:建立 cjpm 工程 ── 固定包名、版本、源码目录和静态库输出
第 3 阶段:类型映射 ── 用 Int64、Float64、Array<String> 和 HumanizeOptions 表达动态配置
第 4 阶段:实现格式化逻辑 ── 完成单位拆分、最大单位数、舍入、语言选择和文本拼接
第 5 阶段:测试与核对 ── 编译测试入口,执行数值、选项、语言和错误行为断言
第 6 阶段:终端演示 ── 构建 example,直接输出真实仓颉调用结果和效果图
每个阶段都有可检查的产物:工程阶段得到 cjpm.toml,实现阶段得到 target/release/humanize_duration_cangjie,验证阶段得到 ALL HUMANIZE-DURATION CANGJIE TESTS PASS,最终示例则输出 ALL HUMANIZE-DURATION CANGJIE EXAMPLE CHECKS PASS。
三、逐步实现过程
第 1 阶段:盘点原库 API 和仓颉工程边界
1.1 识别动态 API
humanize-duration 的 JavaScript 入口接收毫秒数和可选配置。配置可以替换语言、单位数组、分隔符、连接词和舍入方式,语言表还允许一个单位根据数量选择单复数形式。仓颉没有 JavaScript 配置对象那样的任意字段,因此本适配先把稳定的公共选项收敛为 HumanizeOptions 结构体,再把单位和语言限定为可编译检查的字符串值。
原库默认使用年、月、周、日、时、分、秒七个单位,其中一年按 365.25 天、一个月按 30.4375 天计算。仓颉实现沿用这组换算值,保证常见输出与 JavaScript 版本一致。
1.2 固定仓颉项目结构
以下目录树按照当前 HumanizeDuration.js 项目的仓颉适配文件整理,只展示仓颉相关文件;原始 JavaScript 源码、测试和构建目录保持原位置,不在此处重复列出:
text
HumanizeDuration.js/
├── cjpm.toml
├── cjpm.lock
├── README.Cangjie.md
├── README.Cangjie_CN.md
├── src/
│ └── humanize_duration.cj
├── test/
│ └── main.cj
└── example/
├── cjpm.toml
├── cjpm.lock
└── example.cj
实现集中在 src/humanize_duration.cj,测试入口和示例工程独立于库源码。示例使用 src-dir = "." 时,将构建输出放到项目外部的 .cjpm-target/humanize_duration_example,避免 cjpm 把构建目录再次作为源码目录扫描。
第 2 阶段:创建 cjpm 工程
根目录 cjpm.toml 使用静态库输出类型,并将源码目录、包名和版本固定下来:
toml
[package]
cjc-version = "1.1.3"
name = "humanize_duration_cangjie"
version = "3.34.0"
output-type = "static"
src-dir = "src"
target-dir = "target"
包版本沿用当前 humanize-duration 版本,包名使用 humanize_duration_cangjie,避免连字符和 JavaScript 包名对仓颉模块命名造成歧义。构建成功后,主要产物是:
text
target/release/humanize_duration_cangjie/libhumanize_duration_cangjie.a
target/release/humanize_duration_cangjie/humanize_duration_cangjie.cjo
其中 .a 用于链接,.cjo 提供编译器需要的接口信息。独立示例使用本地路径依赖:
toml
[dependencies]
humanize_duration_cangjie = { path = ".." }
根项目没有第三方仓颉依赖,因此 cjpm.lock 保持空依赖锁定。示例项目的构建目录位于仓库外部,运行 cjpm run 时不会产生 target 源码扫描警告。
第 3 阶段:把 JavaScript 动态值映射为仓颉类型
3.1 用数值类型表达毫秒输入
JavaScript 版本可以接收可转换为数字的值,仓颉版本则提供两个显式重载:
cangjie
public func humanizeDuration(milliseconds: Int64, options!: HumanizeOptions = HumanizeOptions()): String
public func humanizeDuration(milliseconds: Float64, options!: HumanizeOptions = HumanizeOptions()): String
整数输入适合常规毫秒时间戳,小数输入适合需要保留亚毫秒精度的场景。两个重载都先取绝对值,再按配置单位拆分。零值会使用最小配置单位输出,例如默认配置下为 0 seconds。
3.2 用结构体表达格式化选项
仓颉通过 HumanizeOptions 替换 JavaScript 的动态 options 对象:
cangjie
let options = HumanizeOptions(
language: "zh_CN",
largest: 2,
round: true,
conjunction: " 和 "
)
let text = humanizeDuration(97320000, options: options)
参数标签保留了调用意图,并让语言、单位、最大单位数和分隔符等字段在编译期有明确类型。largest 使用非正数表示不限制输出单位数,maxDecimalPoints 使用负数表示不截断小数。
3.3 用数组和字符串保留单位语义
上游单位名称是短字符串而不是枚举,因此仓颉接口继续接受 Array<String>:
cangjie
let text = humanizeDuration(
69000,
options: HumanizeOptions(units: ["h", "m", "s"])
)
每个单位都会先经过固定换算表校验。未知单位抛出 IllegalArgumentException,避免拼写错误被静默当成普通文本。语言选择也会先检查内置表,再依次尝试 fallbacks。
第 4 阶段:分层实现源码
4.1 核心文件 src/humanize_duration.cj
核心文件按"单位换算、语言选择、数字格式化、时长拆分、文本拼接"组织实现。piecesFor 先从大到小计算每个单位的数量,再按 largest 限制输出数量;round 开启后会把剩余较小单位合并到最后一个有效单位并执行进位。
cangjie
let text = humanizeDuration(
22141000,
options: HumanizeOptions(conjunction: " and ")
)
// 6 hours, 9 minutes, and 1 second
文本拼接根据片段数量区分单片段、两片段和三片段以上的连接方式。serialComma 只影响三个及以上片段的最终逗号;delimiter 负责普通片段之间的分隔;spacer 负责数字和单位名称之间的文本。
4.2 语言和单复数格式化
语言表通过 unitLabel 按语言和数量选择单位文本。英文、德文、法文和西班牙文会根据数量选择单数或复数;中文、日文和韩文使用固定单位词。语言不在内置表中时,适配会按 fallbacks 从左到右选择第一个可用语言。
cangjie
let chinese = humanizeDuration(
3000,
options: HumanizeOptions(language: "zh_CN")
)
// 3 秒
getSupportedLanguages 返回与语言表同步的代码列表,示例会同时检查列表长度和每种语言的实际输出,确保文档中的语言范围不是静态写死的说明。
4.3 数字和小数格式化
formatNumber 保留整数的简洁表示,并在最小单位产生小数时应用 maxDecimalPoints 和 decimal。例如:
cangjie
let precise = humanizeDuration(8123.456789)
let limited = humanizeDuration(
8123.456789,
options: HumanizeOptions(maxDecimalPoints: 3)
)
// 8.123456789 seconds
// 8.123 seconds
仓颉实现对输入值和选项使用显式类型,不复刻 JavaScript 中字符串数字自动转换、任意对象赋值或动态属性合并等运行时行为。
第 5 阶段:建立自动化测试
test/main.cj 是直接链接仓颉静态库的测试入口,覆盖以下行为:
| 测试组 | 示例 |
|---|---|
| 输入类型 | Int64、Float64、负数和零值 |
| 单位拆分 | 默认单位、毫秒单位、自定义单位和多片段输出 |
| 数值选项 | largest、round、maxDecimalPoints |
| 文本选项 | decimal、spacer、delimiter、conjunction、serialComma |
| 语言 | 英文、简体中文、繁体中文、日文、韩文、西班牙文、法文、德文 |
| 回退和异常 | fallbacks、未知语言和未知单位 |
断言失败会抛出异常并以非零状态退出;全部通过时输出固定标记:
text
ALL HUMANIZE-DURATION CANGJIE TESTS PASS
示例工程的 example/example.cj 使用同样的断言方式,但作为独立 cjpm 项目通过本地依赖消费库。它会检查当前适配的每个公开选项,最后输出:
text
ALL HUMANIZE-DURATION CANGJIE EXAMPLE CHECKS PASS
第 6 阶段:核对 humanize-duration 导出名称
当前 src/humanize_duration.cj 的公共入口核对结果为:
text
defaultUnits 1 个函数
HumanizeOptions 1 个配置结构体
humanizeDuration(Int64) 1 个重载
humanizeDuration(Float64) 1 个重载
getSupportedLanguages 1 个函数
这些入口覆盖当前仓颉适配明确承诺的能力。原 JavaScript 版本的语言表和动态 options 范围更大,尚未迁移的能力在第七节明确列出,而不是用名称覆盖掩盖行为差异。
四、完整验证步骤
以下命令均以仓库根目录 HumanizeDuration.js/ 为当前目录。先加载已经安装在本机的仓颉 SDK:
bash
source ~/.zshrc
echo "$CANGJIE_HOME"
cjc --version
cjpm --version
检查源码和依赖关系:
bash
cjpm check
构建静态库:
bash
cjpm build
编译并运行测试入口:
bash
cjc test/main.cj \
--import-path target/release \
-L target/release/humanize_duration_cangjie \
-l humanize_duration_cangjie \
-o /tmp/humanize-duration-test
/tmp/humanize-duration-test
在 macOS 的 CJNative 环境中,如果运行时需要显式指定仓颉动态库目录,可以使用:
bash
DYLD_LIBRARY_PATH="$CANGJIE_HOME/runtime/lib/darwin_aarch64_cjnative" \
/tmp/humanize-duration-test
再构建并运行独立的终端示例工程:
bash
cd example
cjpm run
示例的 target-dir 位于 example 源码目录之外,因此运行时不会出现 target 被再次扫描的警告。
五、运行效果
下面的效果图来自 example/example.cj 的实际终端运行,包含仓颉三方库本地依赖、完整断言和最终通过标记。

这张图可以作为文章或项目说明中的验收证据:终端中的 ALL HUMANIZE-DURATION CANGJIE EXAMPLE CHECKS PASS 是程序执行全部断言后输出的结果,不是预先写死的展示文本。示例覆盖数值输入、单位选择、舍入、语言、格式化和异常路径。
六、关键决策说明
6.1 为什么不用无类型 Any
humanize-duration 的 JavaScript 配置对象可以在运行时自由增加字段,但仓颉适配如果退化为无类型容器,就会失去单位数组、布尔选项和数字选项的编译期检查。因此项目用 HumanizeOptions 明确列出当前支持的字段,并用 Array<String> 表达单位顺序。
6.2 为什么保留 humanize-duration 的单位名称和函数名
保留 humanizeDuration、defaultUnits、getSupportedLanguages 以及 y、mo、w、d、h、m、s、ms 等名称,可以降低迁移成本。熟悉 JavaScript 版本的开发者只需要适应仓颉的参数标签、数值重载和结构体配置,不必重新记忆另一套时长单位。
6.3 为什么把测试和示例分开
测试程序负责断言,适合自动化执行;示例工程负责作为真实依赖运行,适合在终端中观察调用路径和截图。两者都使用同一个 humanize_duration_cangjie 包,因此效果图来自库的实际运行输出,而不是复制一组预先计算的字符串。
七、兼容边界和遗留问题
JavaScript humanize-duration 与仓颉的运行时模型不同,以下限制需要在接入时提前了解:
- 当前仓颉实现内置
en、zh_CN、zh_TW、ja、ko、es、fr、de八种语言;原 JavaScript 版本还包含更多语言,未迁移语言不会被错误地宣称为已支持。 - 原库的自定义
languages语言表没有在仓颉中开放;语言文本由当前源码中的固定表提供,未知语言只能通过fallbacks回退到内置语言。 - 原库的
unitMeasures自定义换算表和数字替换表没有迁移。仓颉版本使用与上游默认值一致的固定换算表,且不支持阿拉伯数字替换等特殊语言元数据。 - JavaScript 的数字强制转换、任意对象 options 和动态函数值没有直接仓颉等价物;调用方需要使用
Int64/Float64和HumanizeOptions的静态字段。 units应按从大到小传入,且必须使用y、mo、w、d、h、m、s、ms中的值。未知单位会抛出异常,错误顺序不会自动修正。Float64的显示结果受仓颉浮点精度影响;需要固定显示位数时,应使用maxDecimalPoints,需要改变小数分隔文本时使用decimal。
这些是明确的适配边界,不是构建错误。只要 cjpm check、cjpm build、测试入口和示例断言都成功,库就完成了当前范围内的本地编译和行为验收。
八、接入方式
在另一个仓颉项目中,可以将本仓库作为本地依赖或发布包使用。最直接的本地接入方式是在调用方的 cjpm.toml 中加入:
toml
[dependencies]
humanize_duration_cangjie = { path = "../HumanizeDuration.js" }
调用代码导入仓颉包并传入毫秒数:
cangjie
import humanize_duration_cangjie.*
main(): Int64 {
let basic = humanizeDuration(12000)
let detailed = humanizeDuration(
97320000,
options: HumanizeOptions(largest: 3)
)
println(basic)
println(detailed)
return 0
}
如果使用 cjc 直接链接静态库,则需要将 target/release 加入导入路径,并将 target/release/humanize_duration_cangjie 加入库搜索路径。跨端项目可以复用相同的时长格式化 API 和测试思路,具体的 UI、线程、定时器或文件系统能力由宿主平台负责。
九、总结
本次适配完成了 humanize-duration 到仓颉静态类型模型的迁移:毫秒输入使用 Int64 和 Float64 重载,格式化选项使用 HumanizeOptions,单位使用上游短名称,语言使用内置表和显式回退,输出片段通过固定换算表和舍入逻辑生成。
验证链路包括 cjpm check、静态库构建、测试入口、示例工程和终端效果图。最终效果图来自真实的仓颉可执行程序,能够直观看到所有当前公开选项的断言通过。对于尚未一比一映射 JavaScript 动态语言表和自定义换算选项的部分,项目通过静态签名和文档边界保持行为可解释,方便后续继续扩展。
适配过程 FAQ
以下问题来自仓颉工程创建、
cjpm依赖、与 HarmonyOS 设备联调的实际流程。
Q1:cjpm 依赖解析失败或拉取超时?
- 检查
cjpm.toml中的依赖坐标与版本号; - 配置可用的仓颉包仓库镜像或代理;
- 删除
target、cjpm.lock后重新执行cjpm update/cjpm build。
Q2:编译报找不到仓颉 SDK 或 cjc 版本不匹配?
- 设置
CANGJIE_HOME指向仓颉 SDK 根目录; - 确认
cjc --version、cjpm --version与工程要求的版本一致; - 清理构建缓存后重新生成。
Q3:与 HarmonyOS 设备联调时安装失败?
- 用
hdc list targets确认设备在线; - 确认 HAP 已签名,
bundleName与签名 profile 一致; - 设备需加入调试 profile 的
deviceIds。
Q4:动态库或 FFI 符号找不到?
- 检查导出符号名称、ABI 与打包路径;
- 确认
.so已随 HAP 一起打包到libs/<abi>/; - 用
llvm-readelf -d检查依赖库是否齐全。
Q5:测试用例在设备上失败,但本机通过?
- 对比设备与主机的架构、SDK 版本和环境变量;
- 用
hdc hilog抓取运行日志; - 优先复现最小用例,确认是否为平台差异或时序问题。