- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
导读
PluralizationForms是 Humanizer 中用于承载"一个名词的作者化单复数形态集合"的公开类型:你为名词显式提供单数形态与 CLDR(Unicode Common Locale Data Repository)各基数复数类别对应的形态,Humanizer 依据所选文化的基数复数规则自动挑选正确形态。本文完整讲解该类的构造函数、7 个属性与 3 个方法,并深入源码剖析其背后的 CLDR 48.2 规则引擎、decimal小数位(visible-fraction operands)语义与 NFC 规范化匹配机制,帮助你在多语言场景下准确、可控地处理数量词复数。
一、为什么需要 PluralizationForms
英语的复数规则很简单:1 item/2 items。但阿拉伯语有zero、one、two、few、many、other六种类别;俄语、波兰语等斯拉夫语言对few和many有严格的分段规则;法语连0都算复数。通用复数引擎可以覆盖常见语言,但总有些名词需要"作者化"(authored)的精确控制——比如品牌名、外来词、固定短语。
PluralizationForms就是为这种需求设计的纯数据容器:它不自己计算复数,只保存你显式写下的各形态;真正决定"当前数量该用哪个形态"的是文化对应的 CLDR 基数复数规则。正如其类注释所强调的两条契约(见 src/Humanizer/PluralizationForms.cs):
- Humanizer 应用受支持文化的基数复数规则来挑选已声明的形态;
- 缺失的形态不会被推断(Missing selected forms are not inferred)。
二、类型定义与整体设计
public sealed class PluralizationForms类声明在命名空间Humanizer中,继承自System.Object,且是sealed的——它只负责保存形态数据,不开放继承扩展。完整实现见 src/Humanizer/PluralizationForms.cs。
类的核心成员一览:
- 构造函数:
PluralizationForms(string singular, string other, string? zero, string? one, string? two, string? few, string? many) - 属性:
Singular、Other、Zero、One、Two、Few、Many - 方法:
Invariant(string)(静态工厂)、TryPluralize(decimal, CultureInfo, out string?)、TrySingularize(string, out string?)
其中 6 个属性对应 CLDR 的 6 种基数复数类别,这些类别由内部枚举CardinalPluralCategory定义(见 src/Humanizer/CardinalPluralCategory.cs)。需要注意:这些类别名是语法类别而非数值区间——one并不总是等于"数量 1",具体含义由所选文化的 CLDR 规则决定。例如葡萄牙语(pt)与欧洲葡萄牙语(pt-PT)的规则就不同,测试用例 tests/Humanizer.Tests/LocalizedInflectionTests.cs 验证了这一点。
三、构造函数:为名词声明完整形态集合
public PluralizationForms( string singular, string other, string? zero = null, string? one = null, string? two = null, string? few = null, string? many = null);3.1 参数详解
| 参数 | 类型 | 必填 | 含义 |
|---|---|---|---|
singular | string | 是 | 名词的单数形态 |
other | string | 是 | CLDRother类别对应的形态(所有语言都必须有的兜底类别) |
zero | string? | 否 | CLDRzero类别形态,不可用时传null |
one | string? | 否 | CLDRone类别形态,不可用时传null |
two | string? | 否 | CLDRtwo类别形态,不可用时传null |
few | string? | 否 | CLDRfew类别形态,不可用时传null |
many | string? | 否 | CLDRmany类别形态,不可用时传null |
只有singular和other是必填的;其余 5 个都是可选参数,支持命名参数调用。这在英语场景下非常简洁——英语的 CLDR 规则只有one和other两类,其余类别永远不会被选中,所以只需:
var forms = new PluralizationForms("item", "items", one: "item");3.2 验证规则与异常
从源码实现(src/Humanizer/PluralizationForms.cs)可以看到,构造函数内部调用RequireForm和ValidateOptionalForm完成校验:
singular或other为null→ 抛出System.ArgumentNullException;singular或other为空字符串或纯空白 → 抛出System.ArgumentException;- 任意可选形态(
zero/one/two/few/many)非null但为空或纯空白 → 同样抛出System.ArgumentException。
也就是说,可选参数要么不传(保持null),要么就必须传一个非空有效值。对应的测试见 tests/Humanizer.Tests/LocalizedInflectionTests.cs:
Assert.Throws<ArgumentNullException>(() => new PluralizationForms(null!, "items")); Assert.Throws<ArgumentException>(() => new PluralizationForms("item", " ")); Assert.Throws<ArgumentException>(() => new PluralizationForms("item", "items", one: ""));3.3 实战示例:阿拉伯语名词
阿拉伯语拥有全部 6 个非 singular 类别,适合展示完整构造。下面参考测试用例 tests/Humanizer.Tests/LocalizedInflectionTests.cs 的写法(形态取自仓库中 src/Humanizer/Locales/ar.yml 所体现的复数类别体系):
var forms = new PluralizationForms( "ملي ثانية", // singular "ملي ثانية", // other zero: "ملي ثانية", one: "ملي ثانية", two: "ملي ثانية", few: "ملي ثانية", many: "ملي ثانية");如果一个名词在所有类别下形态完全相同,可以直接使用下面的静态工厂方法。
四、属性:读取已声明的形态
7 个属性均为只读({ get; }),在构造时被赋值后不可修改:
| 属性 | 类型 | 说明 |
|---|---|---|
Singular | string | 名词的单数形态(必填) |
Other | string | CLDRother类别形态(必填) |
Zero | string? | CLDRzero类别形态,未声明时为null |
One | string? | CLDRone类别形态,未声明时为null |
Two | string? | CLDRtwo类别形态,未声明时为null |
Few | string? | CLDRfew类别形态,未声明时为null |
Many | string? | CLDRmany类别形态,未声明时为null |
可空属性是否返回null直接决定了TryPluralize在选中该类别时能否成功——这正是"缺失形态不被推断"的具体体现。
五、静态工厂:Invariant(string)
对于任何类别下形态都不变的名词(如品牌名token、缩写、专有名词),Invariant工厂方法把同一个词填充到所有 7 个形态位:
public static PluralizationForms Invariant(string word);源码实现非常直白(src/Humanizer/PluralizationForms.cs):
public static PluralizationForms Invariant(string word) => new(word, word, word, word, word, word, word);参数与异常:
word为null→System.ArgumentNullException;word为空或纯空白 →System.ArgumentException。
该工厂的典型用途是测试与占位:测试代码 tests/Humanizer.Tests/LocalizedInflectionTests.cs 用它构造"token",对每一种已发布 locale执行TryPluralize(1.0m, ...),验证任何受支持文化下都能成功返回"token"。
六、TryPluralize:按数量与文化挑选形态
这是类的核心方法,签名如下:
public bool TryPluralize( decimal quantity, System.Globalization.CultureInfo culture, [NotNullWhen(true)] out string? result);6.1 行为契约
quantity:数量值。其编码的十进制小数位(decimal scale)提供 CLDR 的 visible-fraction operands(见下文原理分析);culture:应用其基数复数规则的受支持文化;为null时抛出System.ArgumentNullException;result:被选中的作者化形态;当选中类别未声明对应形态时为null;- 返回值:选中形态可用返回
true,否则返回false。
6.2 实现原理与调用链
源码实现(src/Humanizer/PluralizationForms.cs)分两步:
- 调用
LocalizedInflectionCatalog.TrySelectCategory(culture, quantity, out var category)算出数量在该文化下属于哪个 CLDR 类别; - 调用内部方法
TryGetForm(category, out result)在已声明的形态中查找——找不到(对应属性为null)就返回false。
TryGetForm本质是一个switch表达式(src/Humanizer/PluralizationForms.cs):将CardinalPluralCategory枚举映射到对应属性;遇到未知枚举值抛出System.ArgumentOutOfRangeException(测试见 tests/Humanizer.Tests/LocalizedInflectionTests.cs)。
TrySelectCategory位于 src/Humanizer/Inflections/LocalizedInflectionCatalog.cs,其逻辑是:先解析该文化命中的 CLDR 规则集(CardinalPluralRuleKind,支持按 locale 归并到 LocaleProfileOwner),再交给CardinalPluralRules.Select(rule, quantity)计算类别;解析不到规则时返回false。
6.3 CLDR 48.2 规则与小数位(visible fraction)语义
规则集与选择器定义在 src/Humanizer/Localisation/GrammaticalNumber/CardinalPluralRules.cs:共实现了AmharicLike、EnglishLike、Arabic、Slovenian、CzechSlovak、Polish、RussianUkrainian、French、Portuguese等约 30 套 CLDR 48.2 基数规则。
CLDR 规则的一个关键点是:小数是否"可见"会影响类别判定。例如英语规则中,整数1属于one,而带可见小数位的1.0属于other。测试 tests/Humanizer.Tests/LocalizedInflectionTests.cs 精确验证了这一语义:
var forms = new PluralizationForms("item", "other", one: "one"); var culture = new CultureInfo("en"); Assert.True(forms.TryPluralize(1m, culture, out var integer)); Assert.True(forms.TryPluralize(1.0m, culture, out var visibleFraction)); Assert.Equal("one", integer); // 1 → one Assert.Equal("other", visibleFraction); // 1.0 → other其中quantity的十进制小数位由 src/Humanizer/Localisation/GrammaticalNumber/CardinalPluralOperands.cs 精确提取:通过decimal.GetBits读取编码的 96 位整数与 scale(V),再计算整数部分、小数部分、去尾零小数部分(W)等 CLDR 操作数;对double则采用 Ryu 最短往返算法还原十进制表示(该文件声明改编自 Ulf Adams 的 Ryu,版权与许可见 THIRD-PARTY-NOTICES.txt)。测试 tests/Humanizer.Tests/LocalizedInflectionTests.cs 展示了操作数提取的精确性,例如1.230m得到小数位数V=3、去尾零后W=2。
6.4 缺失形态返回 false
因为"缺失形态不被推断",所以当规则选中few而你未声明few形态时,即使other已声明,也不会被拿来兜底。测试用例 tests/Humanizer.Tests/LocalizedInflectionTests.cs 用new PluralizationForms("item", "items")(未声明zero/one等)在en与ar上验证:en下1、ar下0、2、3、11均返回false且result为null。
6.5 不受支持的文化不降级
两个值得注意的行为边界(均有测试佐证,见 tests/Humanizer.Tests/LocalizedInflectionTests.cs):
- 传入不受支持的文化(如世界语
eo)时,TryPluralize返回false,不会静默降级为英语规则; CultureInfo.InvariantCulture同样不受支持,返回false。
七、TrySingularize:从形态反查单数
public bool TrySingularize(string form, out string? result);form:一个精确的作者化形态(null时抛出System.ArgumentNullException);result:匹配成功时返回该形态集合的单数形态Singular,否则为null;- 返回值:
form与任一已声明形态匹配返回true,否则false。
7.1 匹配规则:NFC 规范化 + 序号比较 + 区分大小写
源码实现(src/Humanizer/PluralizationForms.cs)先将输入与每个候选形态做Normalize(若未处于 NFC 规范化形式则调用value.Normalize(NormalizationForm.FormC)),再通过string.Equals(normalized, candidate, StringComparison.Ordinal)做序号(ordinal)且区分大小写的比较。
这意味着:
"café"的 NFD 写法"cafe\u0301s"也能匹配上"cafés"(合成字符被 NFC 归一);- 但
"CAFÉS"(大小写不同)与"coffee"(不属于任何已声明形态)都匹配失败。
对应测试 tests/Humanizer.Tests/LocalizedInflectionTests.cs:
var forms = new PluralizationForms("café", "cafés"); Assert.True(forms.TrySingularize("cafe\u0301s", out var singular)); Assert.Equal("café", singular); Assert.False(forms.TrySingularize("CAFÉS", out singular)); Assert.Null(singular);7.2 多类别形态集合的反查示例
测试 tests/Humanizer.Tests/LocalizedInflectionTests.cs 展示了完整 7 形态名词的反查:无论传入"child"、"children"、"zero children"、"one child"、"two children"、"few children"还是"many children",只要它精确命中某个已声明形态,TrySingularize都返回统一的单数"child"。
八、完整实战示例
把前面所有知识点串起来,一个覆盖多语言场景的完整示例:
using System.Globalization; using Humanizer; // 1. 英语:只需 one + other var english = new PluralizationForms("item", "items", one: "item"); english.TryPluralize(1m, CultureInfo.GetCultureInfo("en"), out var enOne); // "item" english.TryPluralize(2m, CultureInfo.GetCultureInfo("en"), out var enTwo); // "items" english.TryPluralize(1.0m, CultureInfo.GetCultureInfo("en"), out var enFrac); // "items"(可见小数位 → other) // 2. 阿拉伯语:六类别齐全才完整 var arabic = new PluralizationForms( "عنصر", // singular "عناصر", // other zero: "عنصر", one: "عنصر", two: "عنصرين", few: "عناصر", many: "عنصر"); arabic.TryPluralize(0m, CultureInfo.GetCultureInfo("ar"), out var arZero); // zero → "عنصر" arabic.TryPluralize(2m, CultureInfo.GetCultureInfo("ar"), out var arTwo); // two → "عنصرين" arabic.TryPluralize(11m, CultureInfo.GetCultureInfo("ar"), out var arMany); // many → "عنصر" // 3. 不变名词:Invariant 工厂 var brand = PluralizationForms.Invariant("token"); brand.TryPluralize(1m, CultureInfo.GetCultureInfo("en"), out var brandForm); // "token" brand.TrySingularize("token", out var singular); // "token" // 4. 缺失形态不推断:只声明 two 别的类别,命中 two 之外就失败 var partial = new PluralizationForms("item", "items", two: "一对"); partial.TryPluralize(2m, CultureInfo.GetCultureInfo("ar"), out var pair); // true → "一对" partial.TryPluralize(3m, CultureInfo.GetCultureInfo("ar"), out var missing); // false, missing == null九、使用建议与边界总结
- 先查规则再声明形态:
TryPluralize的成功与否取决于"该文化的规则是否选中了已声明的类别"。英语只需one+other;阿拉伯语、俄语、波兰语等需要按需补齐zero/two/few/many。仓库中各语言的 src/Humanizer/Locales YAML 数据展示了真实语言中各类别形态的形态差异,可作为参考。 - 不要依赖兜底:
other只是规则层面最普遍的类别,规则选中few而few未声明时不会回退到other,务必用返回值判断是否成功。 - 注意可见小数位:
1与1.0在不同文化下可能属于不同类别,这是 CLDR 规则的预期行为,并非缺陷。 - 不受支持的文化:返回
false且不降级到英语,调用方需要自行处理降级策略。 - 反查是精确匹配:
TrySingularize只接受"恰好等于某个已声明形态"的输入,且比较区分大小写、做 NFC 归一化;它不做词法分析,无法推断未声明的变体。 - 配合既有复数 API:
PluralizationForms属于 Humanizer 本地化词形变化(Inflection)体系的显式作者化路径,与此前基于规则的Pluralize()/Singularize()/ToQuantity()等扩展方法(如 InflectorExtensions.cs)并存互补——前者精确可控,后者通用便捷,测试 tests/Humanizer.Tests/LocalizedInflectionTests.cs 也验证了旧契约在新引擎下保持行为不变。
- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
相关推荐
PHPStan 错误标识符 `parameter.notOptional` 完全解析:子类重写把可选参数变成必选参数
PHPStan 错误标识符 parameter.notOptional 完全解析:子类重写把可选参数变成必选参数 parameter.notOptional 是
开发工具代码质量静态分析深入解析 go-playground/locales:Go 语言基于 Unicode CLDR 的本地化与复数规则引擎
深入解析 go playground/locales:Go 语言基于 Unicode CLDR 的本地化与复数规则引擎 导读 go playground/loc
网络安全Cloud-Probe常见问题解答:新手必知的10个关键知识点
Cloud Probe常见问题解答:新手必知的10个关键知识点 Cloud Probe是一款专为云环境和虚拟化场景设计的网络数据包捕获与转发工具,能够解决在缺乏
网络可观测性
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考