仓颉三方库开发实战:人性化格式转换库 humanize 的设计与实现
仓颉三方库开发实战:人性化格式转换库 humanize 的设计与实现
本文详细介绍了基于仓颉语言(Cangjie)开发的人性化格式转换库
humanize的完整开发过程,从需求分析、架构设计、核心算法实现到工程实践,为仓颉三方库开发者提供一套可参考的实践方案。
目录
项目背景
需求来源
在实际开发中,将数据转换为人类可读的友好格式是一个常见的需求场景:
- 文件大小显示:将字节数转换为
83 MB或79 MiB,而不是82854982 bytes - 时间显示:显示
1 hour ago而不是-3600 seconds - 数字格式化:显示
1,000,000而不是1000000 - 统计信息:显示
193rd而不是193 - 科学计算:将小数字格式化为
2.23 pF而不是2.2345e-12 F
虽然这些格式化功能看似简单,但每个功能都有其复杂性和细节需要处理。开发一个功能完整、易于使用的格式化库,对于提高开发效率和改善用户体验具有重要意义。
设计目标
基于实际需求,我们确定了以下设计目标:
- 易用性优先:提供简洁直观的 API 接口,一行代码完成格式化
- 功能完整:覆盖字节、时间、数字、序数词等常见格式化场景
- 标准兼容:遵循 SI 和 IEC 等国际标准
- 模块化设计:每个功能模块独立,可按需导入使用
- 性能优化:保证格式化操作的效率,适合高频调用场景
技术选型
为什么选择仓颉语言?
仓颉语言作为华为推出的全栈编程语言,具有以下特点:
1. 静态类型系统
// 函数签名清晰,类型安全
public func bytes(s: UInt64): String
public func parseBytes(s: String): UInt64
静态类型系统提供了编译期类型检查,减少了运行时错误,提高了代码的可靠性。
2. 现代化语法
// 支持模式匹配,优雅处理可选值
match (result) {
case Some(value) => println("解析成功: ${value}")
case None => println("解析失败")
}
3. 强大的标准库
仓颉提供了丰富的标准库支持:
std.math- 数学函数(pow, round, log10 等)std.collection- 提供 Array、HashMap 等集合类型std.unittest- 单元测试框架
4. 良好的性能
作为编译型语言,仓颉具有接近 C/C++ 的执行效率,同时保持了较高的开发效率。
开发环境配置
# cjpm.toml
[package]
name = "humanize"
version = "1.0.0"
cjc-version = "1.0.3"
output-type = "executable"
description = "人性化格式转换库"
架构设计
整体架构
采用模块化设计,每个功能模块独立实现:
┌─────────────────────────────────────┐
│ 应用层 (Application Layer) │
│ 开发者调用接口 │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 功能模块层 (Module Layer) │
│ ┌────────┐ ┌──────┐ ┌─────────┐ │
│ │ bytes │ │ time │ │ ordinal │ │
│ └────────┘ └──────┘ └─────────┘ │
│ ┌────────┐ ┌──────┐ ┌─────────┐ │
│ │ comma │ │ ftoa │ │ si │ │
│ └────────┘ └──────┘ └─────────┘ │
│ ┌────────┐ ┌──────────┐ │
│ │ number │ │ english │ │
│ └────────┘ └──────────┘ │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ 标准库层 (Stdlib Layer) │
│ std.math | std.collection | ... │
└─────────────────────────────────────┘
模块划分
1. 字节格式化模块(bytes)
职责:处理字节数的格式化,支持 SI 和 IEC 两种标准。
package humanize.bytes
import std.math.pow
import std.math.round
// SI 标准单位
let SI_UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB"]
// IEC 标准单位
let IEC_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]
public func bytes(s: UInt64): String {
return formatBytes(s, 1000u64, SI_UNITS)
}
public func iBytes(s: UInt64): String {
return formatBytes(s, 1024u64, IEC_UNITS)
}
设计亮点:
- 使用常量数组存储单位,便于维护和扩展
- 统一的格式化函数处理两种标准,避免代码重复
- 支持精度控制和字符串解析
2. 时间格式化模块(time)
职责:将时间戳转换为相对时间描述。
package humanize.time
public func time(seconds: Int64): String {
return relTime(seconds, "ago", "from now")
}
public func relTime(seconds: Int64, past: String, future: String): String {
let absSeconds = if (seconds < 0) { -seconds } else { seconds }
if (absSeconds < 60) {
return formatTimeUnit(absSeconds, "second", "seconds", if (seconds < 0) { past } else { future })
} else if (absSeconds < 3600) {
// 分钟处理
} else if (absSeconds < 86400) {
// 小时处理
}
// ...
}
设计亮点:
- 支持自定义标签(支持中英文)
- 可配置的时间单位阈值
- 灵活的自定义格式化接口
3. 序数词转换模块(ordinal)
职责:将数字转换为英文序数词。
package humanize.ordinal
public func ordinal(n: Int64): String {
// 特殊情况:11, 12, 13 使用 "th"
let lastTwo = abs(n) % 100
if (lastTwo >= 11 && lastTwo <= 13) {
return "${n}th"
}
// 根据最后一位判断后缀
let lastDigit = abs(n) % 10
if (lastDigit == 1) {
return "${n}st"
} else if (lastDigit == 2) {
return "${n}nd"
} else if (lastDigit == 3) {
return "${n}rd"
}
return "${n}th"
}
设计亮点:
- 处理英文序数词的特殊规则
- 支持负数处理
依赖关系
humanize.bytes
↓
std.math (pow, round)
humanize.time
↓
std.math (abs)
humanize.comma
↓
std.convert (数字转字符串)
所有模块
↓
std.unittest (测试框架)
核心实现
1. 字节格式化算法
1.1 单位选择算法
字节格式化的核心是选择合适的单位,算法如下:
private func findUnit(bytes: Float64, base: Float64, units: Array<String>): (Float64, String) {
if (bytes < base) {
return (bytes, units[0]) // 小于基数,使用 B
}
// 通过循环找到最合适的单位
var exp = 1
var divisor = base
let maxExp = units.size - 1
var i = 2
while (i <= maxExp) {
let nextDivisor = pow(base, Float64(i))
if (bytes >= nextDivisor) {
exp = i
divisor = nextDivisor
i = i + 1
} else {
break
}
}
let unit = units[exp]
let value = bytes / divisor
return (value, unit)
}
算法说明:
- 从最小的单位开始,逐步向上查找
- 找到最大的不超过 bytes 值的单位
- 计算对应的数值和单位
1.2 精度控制算法
bytesN 函数使用 n 参数表示总数字位数(包括整数和小数部分):
private func formatBytesWithPrecision(
bytes: UInt64,
base: UInt64,
units: Array<String>,
minDigits: Int
): String {
let (value, unit) = findUnit(Float64(bytes), Float64(base), units)
// 计算整数位数
let intDigits = countDigitsFloat(value)
// 计算小数位数 = 总位数 - 整数位数
var decimalDigits = minDigits - intDigits
// 如果小数位数小于0,则不显示小数
if (decimalDigits < 0) {
decimalDigits = 0
}
// 四舍五入到指定小数位数
let roundedValue = roundTo(value, decimalDigits)
// 格式化输出
// ...
}
示例:
bytesN(1500000000, 3)→1.50 GB(1位整数 + 2位小数 = 3位)bytesN(123456789, 3)→123 MB(3位整数 + 0位小数 = 3位)
1.3 浮点数精度问题处理
在检查 UInt64 最大值时,18446744073709551615.0 无法在 Float64 中精确表示:
// 检查溢出(最大UInt64值:18446744073709551615)
// 注意:由于浮点数精度限制,使用科学记数法
let maxUInt64Approx = 1.8446744073709552e19
if (result < 0.0 || result > maxUInt64Approx) {
return 0u64
}
2. 时间格式化算法
时间格式化的核心是选择合适的单位(秒/分/时/天/周/月/年):
public func relTime(seconds: Int64, past: String, future: String): String {
let absSeconds = if (seconds < 0) { -seconds } else { seconds }
let label = if (seconds < 0) { past } else { future }
// 阈值数组
let thresholds = [31536000i64, 2592000i64, 604800i64, 86400i64, 3600i64, 60i64]
let units = ["year", "month", "week", "day", "hour", "minute"]
let divisors = [31536000i64, 2592000i64, 604800i64, 86400i64, 3600i64, 60i64]
for (i in 0..thresholds.size) {
if (absSeconds >= thresholds[i]) {
let count = absSeconds / divisors[i]
let unit = if (count == 1) { units[i] } else { units[i] + "s" }
return "${count} ${unit} ${label}"
}
}
// 小于1分钟,使用秒
let unit = if (absSeconds == 1) { "second" } else { "seconds" }
return "${absSeconds} ${unit} ${label}"
}
3. 序数词转换算法
序数词转换需要处理英文的特殊规则:
public func ordinal(n: Int64): String {
// 特殊情况:11, 12, 13 使用 "th"(不是 "11st", "12nd", "13rd")
let lastTwo = abs(n) % 100
if (lastTwo >= 11 && lastTwo <= 13) {
return "${n}th"
}
// 根据最后一位数字判断后缀
let lastDigit = abs(n) % 10
if (lastDigit == 1) {
return "${n}st"
} else if (lastDigit == 2) {
return "${n}nd"
} else if (lastDigit == 3) {
return "${n}rd"
}
return "${n}th"
}
4. 千分位格式化算法
千分位格式化需要从右到左每三位添加一个逗号:
public func comma(n: Int64): String {
if (n == 0) {
return "0"
}
var num = n
var isNegative = false
if (num < 0) {
isNegative = true
num = -num
}
var result = ""
var count = 0
while (num > 0) {
if (count > 0 && count % 3 == 0) {
result = "," + result
}
let digit = num % 10
result = String(Rune(48u8 + UInt8(digit))) + result
num = num / 10
count = count + 1
}
return if (isNegative) { "-" + result } else { result }
}
工程实践
1. 项目结构设计
humanize/
├── src/
│ ├── main.cj # 示例程序入口
│ ├── bytes/ # 字节格式化模块
│ │ ├── bytes.cj
│ │ └── byte_test.cj
│ ├── time/ # 时间格式化模块
│ │ ├── time.cj
│ │ └── time_test.cj
│ ├── ordinal/ # 序数词模块
│ │ ├── ordinal.cj
│ │ └── ordinal_test.cj
│ ├── comma/ # 千分位模块
│ │ ├── comma.cj
│ │ └── comma_test.cj
│ ├── ftoa/ # 浮点数模块
│ │ ├── ftoa.cj
│ │ └── ftoa_test.cj
│ ├── si/ # SI单位模块
│ │ ├── si.cj
│ │ └── si_test.cj
│ ├── number/ # 数字格式化模块
│ │ ├── number.cj
│ │ └── number_test.cj
│ └── english/ # 英文工具模块
│ ├── words.cj
│ └── words_test.cj
├── cjpm.toml # 项目配置文件
├── README.md # 项目说明
└── LICENSE # 开源协议
设计原则:
- 模块独立:每个功能模块独立,可单独导入使用
- 测试伴随:每个模块都有对应的测试文件
- 示例清晰:main.cj 提供完整的使用示例
2. 单元测试实践
使用仓颉的 std.unittest 框架编写测试:
package humanize.bytes
import std.unittest.*
import std.unittest.testmacro.*
@Test
public class BytesTests {
// 测试 bytes 函数 - 零值
@TestCase
func testBytesZero(): Unit {
@Assert(bytes(0u64) == "0 B")
}
// 测试 bytes 函数 - 基本功能
@TestCase
func testBytesOneKB(): Unit {
@Assert(bytes(1000u64) == "1 kB")
}
// 参数化测试
@TestCase[
byteValue in [0u64, 999u64, 1000u64, 1234u64, 82854982u64, 1500000000u64]
]
func testBytesVariousValues(byteValue: UInt64): Unit {
let result = bytes(byteValue)
@Assert(result.size > 0)
}
// 往返测试:格式化 -> 解析
@TestCase[
byteValue in [42u64, 1000u64, 1500u64, 1000000u64, 1500000000u64]
]
func testRoundTrip(byteValue: UInt64): Unit {
let formatted = bytes(byteValue)
let parsed = parseBytes(formatted)
// 允许一定的误差(因为格式化会四舍五入)
let diff = if (parsed > byteValue) { parsed - byteValue } else { byteValue - parsed }
let tolerance = if (byteValue > 0u64) { byteValue / 100u64 } else { 0u64 }
@Assert(diff <= tolerance)
}
}
测试策略:
- 边界条件测试:零值、最小值、最大值
- 典型场景测试:正常情况下的格式化
- 参数化测试:测试多个输入值
- 往返测试:验证格式化和解析的一致性
3. 代码规范实践
3.1 函数拆分
将长函数拆分为多个小函数,每个函数职责单一:
// 原来的长函数(96行)
private func formatBigBytesInternal(...): String {
// 太多逻辑
}
// 拆分为多个小函数
private func tryParseUInt64(num: String): (Bool, UInt64)
private func calculateSIUnitIndex(numLen: Int): Int
private func calculateIECUnitIndex(numLen: Int): Int
private func formatDisplayResult(num: String, unitIndex: Int, units: Array<String>): String
private func formatBigBytesInternal(...): String {
// 调用上述小函数
}
3.2 变量作用域最小化
// 改进前:全局常量
let BIG_SI_UNITS = ["B", "kB", "MB", ...]
// 改进后:局部作用域
public func bigBytes(numStr: String): String {
let BIG_SI_UNITS = ["B", "kB", "MB", ...] // 只在需要时定义
return formatBigBytesInternal(numStr, 1000i64, BIG_SI_UNITS)
}
3.3 不可变优先
// 改进前
var num = removeLeadingZeros(numStr)
// 改进后(如果不需要修改)
let num = removeLeadingZeros(numStr)
4. 错误处理实践
使用仓颉的 Option 类型处理可能失败的情况:
public func parseBytes(s: String): UInt64 {
let numStart = findNumberStart(s)
if (numStart < 0) {
return 0u64 // 解析失败,返回默认值
}
// ...
}
性能优化
1. 算法优化
1.1 单位查找优化
使用循环查找而不是递归,减少函数调用开销:
// 高效:循环查找
var i = 2
while (i <= maxExp) {
let nextDivisor = pow(base, Float64(i))
if (bytes >= nextDivisor) {
exp = i
divisor = nextDivisor
i = i + 1
} else {
break
}
}
1.2 字符串拼接优化
在循环中减少字符串拼接操作:
// 改进:一次性构建字符串,而不是多次拼接
var result = ""
var count = 0
while (num > 0) {
if (count > 0 && count % 3 == 0) {
result = "," + result
}
let digit = num % 10
result = String(Rune(48u8 + UInt8(digit))) + result
num = num / 10
count = count + 1
}
2. 编译期优化
使用编译期常量:
// 编译期常量,无运行时开销
let SI_UNITS = ["B", "kB", "MB", "GB", "TB", "PB", "EB"]
let IEC_UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"]
3. 内存优化
避免不必要的中间对象创建:
// 直接计算,避免创建临时变量
let value = bytes / divisor // 而不是:let temp = bytes; let value = temp / divisor
性能测试结果
| 操作 | 平均耗时 | 说明 |
|---|---|---|
| bytes() 单次调用 | ~3μs | 包含完整格式化流程 |
| parseBytes() 单次调用 | ~2μs | 字符串解析 |
| comma() 单次调用 | ~1μs | 千分位格式化 |
| ordinal() 单次调用 | ~0.5μs | 序数词转换 |
经验总结
1. 模块化设计的重要性
经验:将项目拆分为多个独立模块,带来了以下好处:
- 职责清晰:每个模块只关注自己的核心功能
- 易于测试:可以针对每个模块编写独立的单元测试
- 便于维护:修改某个模块不会影响其他模块
- 可扩展性强:新增功能只需添加新模块
反思:如果一开始就把所有代码写在一起,随着功能增加会变得难以维护。
2. 测试驱动开发
经验:在编写实现代码之前,先编写测试用例:
// 1. 先写测试
@TestCase
func testBytesZero(): Unit {
@Assert(bytes(0u64) == "0 B")
}
// 2. 再写实现
public func bytes(s: UInt64): String {
// 实现代码
}
好处:
- 明确功能需求
- 保证代码质量
- 重构时有安全网
3. 代码规范的重要性
经验:严格遵守 linter 规范,虽然初期可能麻烦,但长期来看收益巨大:
- 可读性提升:代码更易读易理解
- 维护成本降低:统一的代码风格
- 团队协作顺畅:减少代码审查的争议
4. 性能优化要有依据
经验:不要盲目优化,先测量再优化:
- 先实现功能:确保代码正确
- 测量性能:使用性能测试工具
- 找到瓶颈:分析哪里是性能热点
- 针对性优化:只优化关键路径
5. 文档的价值
经验:完善的文档能大大降低使用门槛:
- README.md:快速上手,5 分钟学会使用
- 代码注释:解释复杂的算法逻辑
- 示例代码:展示实际使用场景
挑战与解决方案
挑战 1:精度控制算法
问题:bytesN 函数需要根据总数字位数动态计算小数位数,算法比较复杂。
解决方案:
- 先计算整数位数
- 再计算小数位数 = 总位数 - 整数位数
- 处理边界情况(小数位数不能为负)
挑战 2:浮点数精度问题
问题:UInt64 最大值无法在 Float64 中精确表示。
解决方案:
- 使用科学记数法表示近似值
- 添加注释说明精度限制
- 在关键位置使用 UInt64 进行比较
挑战 3:函数长度限制
问题:linter 要求函数不超过 50 行,但某些函数逻辑复杂。
解决方案:
- 将长函数拆分为多个小函数
- 每个函数职责单一
- 通过函数组合实现复杂逻辑
挑战 4:测试覆盖率
问题:如何确保所有功能都被测试覆盖?
解决方案:
- 使用参数化测试覆盖多种输入
- 编写边界条件测试
- 编写往返测试验证一致性
未来展望
1. 功能扩展
- 国际化支持:支持更多语言的本地化
- 自定义单位:允许用户定义自定义单位
- 格式化选项:更多的格式化选项和配置
2. 性能优化
- SIMD 加速:使用 SIMD 指令加速字符串处理
- 缓存优化:缓存常用格式化结果
- 编译期优化:使用宏在编译期优化代码
3. 生态建设
- 发布到 CJPM 仓库:让更多开发者方便使用
- 与其他库集成:如日志库、Web 框架
- 提供工具:如 CLI 工具、IDE 插件
4. 社区贡献
- 贡献指南:编写详细的贡献文档
- Issue 模板:规范问题反馈流程
- CI/CD:自动化测试和发布
结语
通过开发 humanize 人性化格式转换库,我深刻体会到:
- 模块化设计是项目成功的基石,合理的模块划分让项目易于维护和扩展
- 测试驱动开发保证了代码质量,让重构变得安全
- 代码规范的重要性不容忽视,规范能让团队协作更顺畅
- 性能优化要有依据,不盲目优化
- 文档的价值不容忽视,好的文档能大大降低使用门槛
仓颉语言作为一门新兴的编程语言,具有现代化的语法和强大的标准库,非常适合开发基础库。希望本文能为仓颉三方库开发者提供一些参考和启发。
项目地址:https://gitcode.com/cj-awaresome/humanize
欢迎贡献:如果你有任何建议或想法,欢迎提交 Issue 或 Pull Request!
附录:完整示例代码
基本使用
import humanize.bytes.*
import humanize.comma.*
import humanize.ordinal.*
import humanize.time.*
main() {
// 字节格式化
println("文件大小: ${bytes(82854982u64)}") // 文件大小: 82.85 MB
// 千分位格式化
println("账户余额: $${comma(6582491i64)}") // 账户余额: $6,582,491
// 序数词
println("你是我的第 ${ordinal(193i64)} 个朋友") // 你是我的第 193rd 个朋友
// 时间格式化
println("最后更新: ${time(-3600i64)}") // 最后更新: 1 hour ago
}
批量处理
import humanize.bytes.*
main() {
let sizes = [1024u64, 1048576u64, 1073741824u64]
for (size in sizes) {
println("${size} bytes = ${bytes(size)}")
println("${size} bytes = ${iBytes(size)}")
}
}
日期:2025年
版本:v1.0.0
本文基于 humanize v1.0.0 版本编写,使用仓颉编译器 v1.0.3。
感谢大家的阅读,期待与大家一起共建仓颉生态!
参考资料
- 仓颉官网:https://cangjie-lang.cn
- 仓颉开源仓库:https://gitcode.com/cangjie
- 仓颉官方文档:https://cangjie-lang.cn/docs
- 仓颉开源三方库:https://gitcode.com/cangjie-tpc
- 仓颉编程语言白皮书:https://developer.huawei.com/consumer/cn/doc/cangjie-guides-V5/cj-wp-abstract-V5
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)