HumanizeDuration 跨端三方库仓颉适配全流程

本文记录 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 与仓颉的运行时模型不同,以下限制需要在接入时提前了解:

  1. 当前仓颉实现内置 en、zh_CN、zh_TW、ja、ko、es、fr、de 八种语言;原 JavaScript 版本还包含更多语言,未迁移语言不会被错误地宣称为已支持。
  2. 原库的自定义 languages 语言表没有在仓颉中开放;语言文本由当前源码中的固定表提供,未知语言只能通过 fallbacks 回退到内置语言。
  3. 原库的 unitMeasures 自定义换算表和数字替换表没有迁移。仓颉版本使用与上游默认值一致的固定换算表,且不支持阿拉伯数字替换等特殊语言元数据。
  4. JavaScript 的数字强制转换、任意对象 options 和动态函数值没有直接仓颉等价物;调用方需要使用 Int64/Float64 和 HumanizeOptions 的静态字段。
  5. units 应按从大到小传入,且必须使用 y、mo、w、d、h、m、s、ms 中的值。未知单位会抛出异常,错误顺序不会自动修正。
  6. 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 抓取运行日志;
  • 优先复现最小用例,确认是否为平台差异或时序问题。

十、参考资料

相关推荐
传奇开心果编程3 小时前
【Flutter踩坑排雷】第3课:const 误用坑|const 错误使用引发 UI 异常与性能损耗
flutter·移动开发·跨平台·dart·编程语言理论·编程框架·声明式ui
风骏时光牛马4 小时前
后端服务接口开发与性能优化
前端
kyriewen4 小时前
我让 4 个 AI 互相 review 代码,最温柔的那个下手最狠
前端·程序员·ai编程
计算机魔术师4 小时前
493家公司白砸AI钱?Apollo这组数据戳破了幻觉
前端
IT_陈寒5 小时前
Java静态变量初始化坑得我头皮发麻
前端·人工智能·后端
天外飞雨道沧桑5 小时前
我在浏览器里练了一个 792 参数的大模型,它学会了补全 JavaScript
开发语言·javascript·ai·ecmascript·ai编程
柳杉5 小时前
用 Codex 完成智慧高速 3D 可视化大屏,从 18 张行业设计稿到可交互的运行监测中心
前端·ai编程·数据可视化
review445435 小时前
拿到 codex 第一步
前端·agent·vibecoding
鶴哥只手遮天5 小时前
jQuery 4.0 源码架构与工程实践-第 01 章 · 全景与工程结构:一个 20 岁库的自省
前端·javascript·jquery