- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
INumberToWordsConverter 是 Humanizer 中负责把数字转换为本地化文字(基数词与序数词)的核心接口,所有ToWords()与ToOrdinalWords()扩展方法最终都会委托给它的实现。本文以 2.10.1 版 API 文档中该接口的完整定义为主体,结合当前仓库源码,逐一拆解接口成员的签名、参数语义与返回约定,并说明转换器如何通过注册表按区域性解析、内置实现如何组织,以及如何编写并注册自定义转换器,让读者既能看懂契约,也能落地实现。
INumberToWordsConverter:ToWords 与 ToOrdinalWords 背后的本地化扩展点
Humanizer 官方对该接口的定义非常直白:"An interface you should implement to localise ToWords and ToOrdinalWords methods"——即它是面向扩展开放的语言化组件,开发者通过实现它,把"数字 → 人类可读文字"的规则按语言/区域插入 Humanizer 体系。
接口的声明位于 INumberToWordsConverter.cs,命名空间为Humanizer,是公开接口:
public interface INumberToWordsConverter在 2.10.1 版 API 文档中,该接口只包含 5 个核心方法(3 个基数词转换 + 2 个序数词转换);而从当前仓库源码看,接口已在此基础上扩展出支持WordForm(词形)、ConvertToTuple(元组命名)的重载,当前版本共 11 个方法。无论版本如何演进,这 5 个核心方法始终是契约主干,也是所有实现必须回答的"最小问题集"。
完整调用链:从扩展方法到具体实现
INumberToWordsConverter并不直接面向使用者,它是通过 NumberToWordsExtension.cs 中的扩展方法间接驱动的。例如:
public static string ToWords(this long number, CultureInfo? culture = null, bool addAnd = true) => Configurator.GetNumberToWordsConverter(culture).Convert(number, addAnd);见 NumberToWordsExtension.cs。调用链可概括为:
- 用户调用
number.ToWords(...)/number.ToOrdinalWords(...)扩展方法; - 扩展方法经 Configurator.GetNumberToWordsConverter(culture) 解析出与当前区域性匹配的
INumberToWordsConverter; - 转换器执行
Convert/ConvertToOrdinal并返回本地化文字。
因此,INumberToWordsConverter的语义直接决定了ToWords()系列 API 的输出质量——它是整条数字本地化管线的"最后一公里"。
核心成员逐一解析(源自 2.10.1 API 文档)
以下 5 个方法完整继承自关联文档,签名、参数与返回值语义均以原文为准。
1. Convert(long number)
按区域设置的默认语法性别将数字转换为字符串:
string Convert(long number);| 项 | 说明 |
|---|---|
number | [System.Int64],待转换的数字 |
| 返回值 | [System.String],本地化后的数字文字 |
"默认语法性别"指语言本身约定的默认词形。对无性别区分的语言(如英语),结果与性别无关;对有性别区分的语言,则使用转换器内置的默认性别(详见下文"语法性别"小节)。
2. Convert(long number, bool addAnd)
按区域设置的默认语法性别转换数字,并可选择是否加入连词 "And":
string Convert(long number, bool addAnd);| 项 | 说明 |
|---|---|
number | [System.Int64],待转换的数字 |
addAnd | [System.Boolean],指定是否加入连词 "And" |
| 返回值 | [System.String],本地化后的数字文字 |
典型差异可用英语验证(见 NumberToWordsTests.cs):
3501L.ToWords(addAnd: true) // "three thousand five hundred and one" 3501L.ToWords(addAnd: false) // "three thousand five hundred one"注意:addAnd的语义是"语言相关连词的开关",并非所有语言都使用 "And" 这个词本身,具体连词由各区域性转换器内部决定。
3. Convert(long number, GrammaticalGender gender, bool addAnd = true)
按显式提供的语法性别转换数字:
string Convert(long number, Humanizer.GrammaticalGender gender, bool addAnd=true);| 项 | 说明 |
|---|---|
number | [System.Int64],待转换的数字 |
gender | GrammaticalGender,期望的语法性别 |
addAnd | [System.Boolean],是否加入连词,默认true |
| 返回值 | [System.String],本地化后的数字文字 |
这是对有性别的语言(俄语、希伯来语、西班牙语、意大利语等)最关键的入口。例如俄语中数字 1 的阳性与阴性词形不同,见 NumberToWordsExtension.cs 的文档注释:
1.ToWords(GrammaticalGender.Masculine) -> "один" 1.ToWords(GrammaticalGender.Feminine) -> "одна"4. ConvertToOrdinal(int number)
按区域设置的默认语法性别将数字转换为序数词字符串:
string ConvertToOrdinal(int number);| 项 | 说明 |
|---|---|
number | [System.Int32],待转换的数字 |
| 返回值 | [System.String],本地化后的序数词文字 |
注意序数词入口的数字类型是int(32 位),与基数词的long不同。英语示例(测试见 NumberToWordsTests.cs):
1.ToOrdinalWords() // "first" 21.ToOrdinalWords() // "twenty-first"5. ConvertToOrdinal(int number, GrammaticalGender gender)
按显式提供的语法性别将数字转换为序数词:
string ConvertToOrdinal(int number, Humanizer.GrammaticalGender gender);| 项 | 说明 |
|---|---|
number | [System.Int32],待转换的数字 |
gender | GrammaticalGender,期望的语法性别 |
| 返回值 | [System.String],本地化后的序数词文字 |
典型场景如巴西葡萄牙语:1.ToOrdinalWords(GrammaticalGender.Masculine)得到 "primeiro",而1.ToOrdinalWords(GrammaticalGender.Feminine)得到 "primeira"(见 NumberToWordsExtension.cs 的注释示例)。
理解 addAnd 与语法性别:两个关键参数的实现语义
语法性别枚举:GrammaticalGender
gender参数的类型是 GrammaticalGender.cs 定义的公开枚举,仅有三个取值:
public enum GrammaticalGender { Masculine, // 阳性 Feminine, // 阴性 Neuter // 中性 }它在接口中的作用是"输出语言要求":当目标语言区分词形时,转换器据此选择正确的词干与后缀;当语言不区分时(如英语、中文),该参数会被忽略。
两类基类:无性别与有性别的默认路由
从当前仓库源码结构看,INumberToWordsConverter的全部内置实现都通过两个内部抽象基类组织,它们承担了"重载转发"(overload fan-out)的职责:
- GenderlessNumberToWordsConverter(源码):面向不区分性别的语言。核心是
Convert(long)与ConvertToOrdinal(int)两个抽象方法;其余所有重载(含addAnd、gender、WordForm组合)默认都转发到核心方法。例如Convert(long, bool addAnd)在基类中的默认实现会忽略addAnd,因为"连词行为由具体转换器固化"(见 GenderlessNumberToWordsConverter.cs)。 - GenderedNumberToWordsConverter(源码):面向区分性别的语言,构造函数接收
defaultGender(默认GrammaticalGender.Masculine)。Convert(long)被实现为Convert(number, defaultGender),把无性别重载全部路由到Convert(long, GrammaticalGender, bool addAnd)与ConvertToOrdinal(int, GrammaticalGender)两个抽象方法(见 GenderedNumberToWordsConverter.cs)。
这意味着:对于使用者而言,不传gender与传gender的差异,本质上是"使用语言默认词形"与"强制指定词形"的差异;对于实现者而言,只需按语言特性选择继承恰当的基类,覆盖最核心的抽象方法即可,其余重载由基类自动补齐。
addAnd 的实际落地:以 TriadScale 转换器为例
英语等"三位一节"(triad)语言的内置转换器 TriadScaleNumberToWordsConverter.cs 展示了Convert的完整流程(见 TriadScaleNumberToWordsConverter.cs):
- 取绝对值;为 0 时直接返回区域的
ZeroWord; - 当指定阴性且数值恰为 1 时,返回
FeminineOneWord; - 依次按降序的
Scales(如千、百万、十亿)分解数值,对每个量级调用ConvertScalePart; - 剩余部分(百、十、个位)走
ConvertTriad; - 负数统一以
MinusWord + " " + 结果前缀。
连词(如英语的 "and")正是通过 profile 中的CountToScaleJoiner等字段嵌入各级拼接的,因此addAnd的开关最终体现为语言特定的连接词行为,而不是简单的字符串替换。
转换器如何被解析:注册表与区域性回退链
理解接口之后,下一个问题是:给定一个区域性,Humanizer 如何找到对应的INumberToWordsConverter?答案在配置层。
Configurator 与 NumberToWordsConverterRegistry
Configurator.cs 暴露了公开属性:
public static LocaliserRegistry<INumberToWordsConverter> NumberToWordsConverters { get; } = new NumberToWordsConverterRegistry();而 NumberToWordsConverterRegistry.cs 把默认(回退)转换器绑定为英语:
class NumberToWordsConverterRegistry : LocaliserRegistry<INumberToWordsConverter> { public NumberToWordsConverterRegistry() : base(_ => NumberToWordsProfileCatalog.Resolve("en", CultureInfo.InvariantCulture)) => NumberToWordsConverterRegistryRegistrations.Register(this); }即:当某个区域性没有专门的转换器时,回退到英语;同时注册表在构造时通过Register(this)批量注入各语言实现。
LocaliserRegistry 的解析策略
泛型注册表 LocaliserRegistry.cs 的解析逻辑(见 LocaliserRegistry.cs)值得关注:
- 精确匹配:优先用
culture.Name精确查找,例如pt-BR; - 父文化链回退:精确匹配失败后沿
culture.Parent逐级向上(pt-BR→pt→ 中性文化),直到找到注册项; - 最终回退:链上都没有时使用构造时传入的默认转换器(英语);
- 性能优化:注册表在首次使用后冻结字典(
ToFrozenDictionary),并对每个CultureInfo实例做缓存(ConditionalWeakTable),避免重复解析; - 注册时机约束:正因为"先冻结、后只读",Register 方法在冻结后再次调用会抛出
InvalidOperationException("Cannot register localisers after the registry has been used.")。自定义转换器必须在使用任何ToWords/ToOrdinalWords之前完成注册(如应用启动阶段)。
从接口到实现:Humanizer 内置转换器的三种形态
形态一:最小回退实现 DefaultNumberToWordsConverter
DefaultNumberToWordsConverter.cs 是最简实现:当某语言没有专门的字词渲染规则时,它直接委托 .NET 框架的区域性数字格式化器输出:
public override string Convert(long number) => number.ToString(culture); public override string ConvertToOrdinal(int number) => number.ToString(culture);见 DefaultNumberToWordsConverter.cs。它保证"任何区域性都能返回一个合理结果",是契约完整性的兜底。
形态二:族化组合器(以 TriadScale 为例)
在src/Humanizer/Localisation/NumberToWords/目录下共有 40 余个转换器实现(如 ConjunctionalScaleNumberToWordsConverter.cs、EastSlavicNumberToWordsConverter.cs、IndianGroupingNumberToWordsConverter.cs 等),按语言的数词组合结构划分为"三进制(triad)""连词量级""印度分组""东斯拉夫变格"等族。这些实现以不可变的Profile记录(词表 + 规则)驱动,例如TriadScaleNumberToWordsConverter的 Profile 包含零词、负号词、十的词干、序数后缀、百/十/个位映射与降序量级表(见 TriadScaleNumberToWordsConverter.cs)。
其序数转换逻辑(TriadScaleNumberToWordsConverter.cs)也很有代表性:个位数直接查OrdinalUnderTen表并附加性别后缀;末两位为 10 时把词尾替换为TenOrdinalStem;否则剥去最后一个字符、做词干元音恢复、应用精确量级序数变换,最后追加CommonOrdinalStem与性别后缀——可见"序数词"并非简单地在基数词后拼 "th",而是随语言形态变化的词干改写过程。
形态三:数据驱动生成
从仓库结构可以推断,各语言的词表与规则数据存放于 src/Humanizer/Locales/ 下的*.yml(如 en.yml、ru.yml),而 Humanizer.SourceGenerators 项目在编译期把这些数据生成注册代码(NumberToWordsProfileCatalog及...RegistryRegistrations)。这意味着多数语言无需手写转换器类,修改 yml 数据并经源生成器重新编译即可接入新语言;INumberToWordsConverter的族化组合器则负责把数据翻译为实际输出。
如何自定义 INumberToWordsConverter 并接入 Humanizer
若目标语言不在内置支持列表中,或需要覆盖某语言的特殊规则,可按以下步骤接入:
第一步:实现接口
注意两个内部基类(GenderlessNumberToWordsConverter、GenderedNumberToWordsConverter)均为internal,外部使用者不能直接继承,因此需要直接实现公开接口。以 2.10.1 的 5 方法契约为最小集,示例骨架如下:
public sealed class MyNumberToWordsConverter : INumberToWordsConverter { public string Convert(long number) => Convert(number, GrammaticalGender.Masculine); public string Convert(long number, bool addAnd) => Convert(number, GrammaticalGender.Masculine, addAnd); public string Convert(long number, GrammaticalGender gender, bool addAnd = true) { // 在这里实现目标语言的基数词规则: // 拆分量级 -> 查词表 -> 按 addAnd 决定是否插入连词 -> 处理负数前缀 return /* 目标语言文字 */; } public string ConvertToOrdinal(int number) => ConvertToOrdinal(number, GrammaticalGender.Masculine); public string ConvertToOrdinal(int number, GrammaticalGender gender) { // 实现序数词规则:词干改写 + 性别后缀 return /* 目标语言序数词文字 */; } }若针对当前版本源码,还需补齐WordForm相关重载与ConvertToTuple(无特殊需要时委托给核心方法即可,可参考 GenderlessNumberToWordsConverter.cs 的默认转发模式)。
第二步:注册到 Configurator
在应用启动阶段(任何ToWords调用之前)执行:
Configurator.NumberToWordsConverters.Register("my-LC", new MyNumberToWordsConverter());Register接受区域性代码字符串,见 LocaliserRegistry.cs。注册后,my-LC及其(未单独注册的)子文化都会通过父文化链命中该实现。
第三步:用测试验证行为
Humanizer 的测试体系(如 NumberToWordsTests.cs)以[Theory]/[InlineData]驱动,覆盖基数词、序数词、连词开关与多语言输出。自定义转换器同样建议用这类数据驱动测试锁定输出,例如参照 ToWordsLong 与 ToWordsWithoutAnd 的写法验证addAnd两种开关路径。
测试与验证:行为契约的可信依据
接口的语义最终由测试固化。在 NumberToWordsTests.cs 中可找到直接支撑上文各结论的用例:
- 英语序数词:
0→"zeroth"、21→"twenty-first"、112→"hundred and twelfth"、1000000→"millionth"(L4-L49); - 英语基数词:
-1→"minus one"、122→"one hundred and twenty-two"、1234567890→"one billion two hundred and thirty-four million five hundred and sixty-seven thousand eight hundred and ninety"(L188-L221); - long 大数:
1111111111111111111L→"one quintillion one hundred and eleven quadrillion …"(L223-L245); - 多语言显式区域性:阿拉伯语
22→"اثنان و عشرون"、俄语40→"сорок"、克罗地亚语1021→"tisuću dvadeset jedan"、泰米尔语555→"ஐந்நூற்று ஐம்பத்து ஐந்து"(L124-L133); - 序数词多语言:阿拉伯语
21→"الحادي و العشرون"、俄语1112→"одна тысяча сто двенадцатый"(L51-L56)。
这些用例直接验证了Convert与ConvertToOrdinal在真实语言上的输出,也是实现自定义转换器时最值得对照的行为基准。
小结
INumberToWordsConverter是 Humanizer 数字本地化能力的核心扩展点:它用 5 个核心方法(当前版本已扩展至 11 个)定义了"数字 → 本地化基数词/序数词"的完整契约;通过Configurator.NumberToWordsConverters注册表按区域性解析,并遵循"精确匹配 → 父文化回退 → 默认英语"的解析链;内置实现则以无性别/有性别两个内部基类组织重载转发,以数据驱动的族化组合器承载具体语言规则。理解这份契约,既有助于精准使用ToWords()/ToOrdinalWords()的重载参数(addAnd、GrammaticalGender),也为接入自定义语言提供了清晰的实现与注册路径。
- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
相关推荐
Humanizer IStringTransformer 接口全解析:字符串转换契约、内置实现与自定义管线
Humanizer IStringTransformer 接口全解析:字符串转换契约、内置实现与自定义管线 IStringTransformer 是 Human
开发工具Humanizer 的 INumberToWordsConverter:深入解析数字转单词的多语言转换接口
Humanizer 的 INumberToWordsConverter:深入解析数字转单词的多语言转换接口 导读 INumberToWordsConverter
开发工具memU bridging 任务跑过却没有产生记忆:如何按 prepare、jobs、commit 顺序检查
memU bridging 任务跑过却没有产生记忆:如何按 prepare、jobs、commit 顺序检查 memU 的 Codex 适配器通过一个计划任务(
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考