仓颉三方库开发实战:人性化格式转换库 humanize 的设计与实现

本文详细介绍了基于仓颉语言(Cangjie)开发的人性化格式转换库 humanize 的完整开发过程,从需求分析、架构设计、核心算法实现到工程实践,为仓颉三方库开发者提供一套可参考的实践方案。

目录


项目背景

需求来源

在实际开发中,将数据转换为人类可读的友好格式是一个常见的需求场景:

  • 文件大小显示:将字节数转换为 83 MB79 MiB,而不是 82854982 bytes
  • 时间显示:显示 1 hour ago 而不是 -3600 seconds
  • 数字格式化:显示 1,000,000 而不是 1000000
  • 统计信息:显示 193rd 而不是 193
  • 科学计算:将小数字格式化为 2.23 pF 而不是 2.2345e-12 F

虽然这些格式化功能看似简单,但每个功能都有其复杂性和细节需要处理。开发一个功能完整、易于使用的格式化库,对于提高开发效率和改善用户体验具有重要意义。

设计目标

基于实际需求,我们确定了以下设计目标:

  1. 易用性优先:提供简洁直观的 API 接口,一行代码完成格式化
  2. 功能完整:覆盖字节、时间、数字、序数词等常见格式化场景
  3. 标准兼容:遵循 SI 和 IEC 等国际标准
  4. 模块化设计:每个功能模块独立,可按需导入使用
  5. 性能优化:保证格式化操作的效率,适合高频调用场景

技术选型

为什么选择仓颉语言?

仓颉语言作为华为推出的全栈编程语言,具有以下特点:

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. 性能优化要有依据

经验:不要盲目优化,先测量再优化:

  1. 先实现功能:确保代码正确
  2. 测量性能:使用性能测试工具
  3. 找到瓶颈:分析哪里是性能热点
  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 人性化格式转换库,我深刻体会到:

  1. 模块化设计是项目成功的基石,合理的模块划分让项目易于维护和扩展
  2. 测试驱动开发保证了代码质量,让重构变得安全
  3. 代码规范的重要性不容忽视,规范能让团队协作更顺畅
  4. 性能优化要有依据,不盲目优化
  5. 文档的价值不容忽视,好的文档能大大降低使用门槛

仓颉语言作为一门新兴的编程语言,具有现代化的语法和强大的标准库,非常适合开发基础库。希望本文能为仓颉三方库开发者提供一些参考和启发。

项目地址: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
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐