Humanizer 冠词处理演进:从 EnglishArticles 枚举到 EnglishArticle 前缀排序的完整解析
Humanizer 冠词处理演进:从 EnglishArticles 枚举到 EnglishArticle 前缀排序的完整解析
本文以 Humanizer 2.13.14 快照的 API 参考文档 Humanizer.EnglishArticles.md 为主体,完整梳理 EnglishArticles 枚举的定义与字段语义,并结合迁移文档与当前源码,讲清楚该 API 在 Humanizer 3 中被移除后、由 EnglishArticle 静态类接管的“忽略冠词排序”能力的实现原理、边界行为与迁移方式。读完后你可以理解:为什么一个只有三个字段(A / An / The)的枚举会出现在 API 参考里、它的实际用途场景,以及现代代码中如何用 AppendArticlePrefix / PrependArticleSuffix 完成“先去掉冠词再排序、排完再恢复”的完整链路。
一、原始文档:EnglishArticles 枚举的完整定义
2.13.14 版本的 API 参考文档对该类型的全部描述如下:
EnglishArticles Enum
Definite and Indefinite English Articles
public enum EnglishArticles
其三个字段及文档附带的说明:
| 字段 | 值 | 文档说明 |
|---|---|---|
A |
0 |
A |
An |
1 |
An |
The |
2 |
The |
三个取值恰好对应英语中的三个冠词:不定冠词 a / an(元音开头用 an)与定冠词 the。从枚举命名与字段取值看,它的用途是作为“冠词前缀/后缀”的显式标记供上层逻辑判断——例如判断某字符串是否以冠词开头、或在展示层统一冠词形式。
值得注意的是,同一仓库的 2.14.1 快照文档 Humanizer.EnglishArticles.md 仍收录了该枚举,而当前 main 源码中已不存在 EnglishArticles(全仓库检索 src 目录无此标识符)。结合迁移文档 version-3-migration.mdx 中“EnglishArticles → Replace the enum-dependent logic in the application(用应用内自有逻辑替换依赖该枚举的代码)”的迁移说明,可以确认:该枚举是 Humanizer 2.x 的公共 API,在 3.0 中被移除,官方未提供直接等价物。
二、功能接管者:EnglishArticle 静态类与“忽略冠词排序”
当前仓库中承接冠词处理职责的公共类型是 ArticlePrefixSort.cs 中的 EnglishArticle 静态类(XML 注释为 “Contains methods for removing, appending and prepending article prefixes for sorting strings ignoring the article”,即“提供移除、追加和前置冠词前缀的方法,用于忽略冠词排序字符串”)。它与 EnglishArticles 枚举共享同一核心主题——处理 A / An / The 三类冠词——但把“标记”换成了“操作”:
public static class EnglishArticle
{
// 移除冠词前缀并追加到同一字符串末尾,然后排序,返回排序结果
public static string[] AppendArticlePrefix(string[] items);
// 把上一步追加到末尾的冠词重新前置回去,得到最终结果
public static string[] PrependArticleSuffix(string[] appended);
}
典型用法是两个方法配对调用:先 AppendArticlePrefix 把 “The Theater” 变成 “Theater The” 并排序,再 PrependArticleSuffix 还原为 “The Theater”。测试用例 ArticlePrefixSortTests.cs 中的这一行完整演示了该模式:
// 输入 ["Ant", "The Theater", "The apple", "Fox", "Bear"]
// 排序(忽略冠词)后输出 ["Ant", "The apple", "Bear", "Fox", "The Theater"]
Assert.Equal(expectedOutput,
EnglishArticle.PrependArticleSuffix(EnglishArticle.AppendArticlePrefix(input)));
不处理冠词的话,“The Theater” 会因为首字母 T 排在 “The apple” 之后甚至挤入 T 段,破坏“按实体名排序”的直觉;这套 API 让 “The apple”“Theater” 按实体本身归位。
三、源码级实现剖析:前缀识别与 Unicode 边界
3.1 前缀识别:TryGetArticlePrefixLength
ArticlePrefixSort.cs 中 TryGetArticlePrefixLength 按 The / the / a / A / An / an 六种大小写组合依次匹配,命中后返回冠词长度(3 / 1 / 2)。真正的判定核心是 IsArticle:
static bool IsArticle(ReadOnlySpan<char> item, string article)
{
var articleLength = article.Length;
return item.Length > articleLength + 1 &&
item[..articleLength].SequenceEqual(article) &&
item[articleLength] == ' ' && // 冠词后必须紧跟空格
IsRegexWordCharacter(item[articleLength + 1]); // 空格后第一个字符须是“单词字符”
}
三条规则缺一不可:
- 字符串必须比冠词本身更长(
item.Length > articleLength + 1),避免把独立的 “A” / “The” 自身误判; - 冠词后必须紧跟一个空格(
"The\tTheater"因 tab 分隔不成立); - 空格后的字符必须属于
IsRegexWordCharacter认可的字词字符集。
AppendArticlePrefix 命中后执行 item<a href="https://link.gitcode.com/i/1fbbc088f1ff70e62db693a7aff0e916" target="_blank">articleLength..].TrimStart() 剥离冠词并 TrimStart 掉多余空白,再拼回 "{removed} {article}";未命中则原样 Trim 保留(见 [L20-L41)。
3.2 “单词字符”的 Unicode 定义
IsRegexWordCharacter 没有用正则,而是按 Unicode 类别枚举,等价于 \w 但额外放行了两个特殊字符:
static bool IsRegexWordCharacter(char c) =>
c is '\u200C' or '\u200D' || // 零宽不连字 / 零宽连字
CharUnicodeInfo.GetUnicodeCategory(c) is
UnicodeCategory.UppercaseLetter or UnicodeCategory.LowercaseLetter
or UnicodeCategory.TitlecaseLetter or UnicodeCategory.ModifierLetter
or UnicodeCategory.OtherLetter or UnicodeCategory.DecimalDigitNumber
or UnicodeCategory.ConnectorPunctuation or UnicodeCategory.NonSpacingMark;
测试 AppendArticlePrefixPreservesRegexEquivalentEdges 逐条验证了这些边界:
| 输入 | 输出 | 说明 |
|---|---|---|
"The Éclair" |
"Éclair The" |
大写带重音字母(OtherLetter)识别成功 |
"The _underscore" |
"_underscore The" |
下划线属 ConnectorPunctuation |
"The 7th Seal" |
"7th Seal The" |
数字开头识别成功 |
"The Ⅻ Monkeys" |
"The Ⅻ Monkeys" |
Unicode 罗马数字 Ⅻ 非 \w,不识别、原样保留 |
"The \u200Cjoiner" / "The \u200Djoiner" |
均识别 | 零宽连接符被显式放行 |
"A Theater" |
"A Theater" |
两个空格不满足“紧跟单个空格后是词字符”的严格形态,保持原样 |
"The\tTheater" |
"The\tTheater" |
tab 分隔不识别 |
"An!" |
"An!" |
长度不足 + 非词字符,原样保留 |
3.3 还原与零分配优化
PrependArticleSuffix(L112-L158)按后缀长度从长到短检查 “ the” / “ an” / “ a” 三种尾部形态并搬回句首。其中真正的重排由 RearrangeArticle 完成,并对不同运行时做了条件编译:
#if NET6_0_OR_GREATER
return string.Create(item.Length, (item, suffixLength, totalLength), (span, state) =>
{
// 直接在目标 span 上重排,避免中间字符串
});
#else
var source = item.AsSpan();
var suffix = source[^suffixLength..];
var prefix = source[..^totalLength];
return $"{suffix} {prefix}";
#endif
即 .NET 6+ 走 string.Create 的预分配路径,低版本回退到字符串插值。
3.4 异常约定
An_Empty_String_Array_Throws_ArgumentOutOfRangeException 固化了一个行为约定:AppendArticlePrefix 接收空数组时抛出 ArgumentOutOfRangeException(源码见 L15-L18),调用方在数据可能为空时需先判空。
四、从枚举到新 API 的迁移路径
迁移文档 version-3-migration.mdx 在 “Replace removed APIs” 一节明确列出:
| 2.x 用法 | Humanizer 3 处理建议 |
|---|---|
EnglishArticles |
Replace the enum-dependent logic in the application |
也就是说官方没有在 3.0 提供替代类型,而是要求把“依赖枚举值判断冠词”的业务逻辑下沉到应用自身。从仓库结构看,这一取舍与 Humanizer 3 的整体方向一致:冠词的“识别与重排”属于字符串工具范畴,由功能完备的 EnglishArticle 静态类承担;而“用枚举承载冠词常量”这种低信息量的类型,对下游价值有限,于是直接裁掉。
如果你的代码原本是这样的形态:
// Humanizer 2.x:依赖枚举做冠词判断(示意)
bool IsDefinite(string s) =>
s.StartsWith("The") && EnglishArticles.The.ToString() == "The";
迁移到 3.0 后的两种常见做法:
// 做法一:本地常量 + 字符串方法
private const string DefiniteArticle = "The";
// 做法二:需要排序能力时直接使用 EnglishArticle
var sorted = EnglishArticle.PrependArticleSuffix(
EnglishArticle.AppendArticlePrefix(titles));
同时注意 3.0 的包资产与命名空间整理:包资产为 netstandard2.0 / net48 / net8.0 / net10.0,原 Humanizer.Localisation 等子命名空间已并入 Humanizer,可用 HUMANIZER001 分析器批量修正 using(见迁移文档第 2 节)。
五、公共 API 签名的持续约束
EnglishArticle 的公开签名被 PublicApiApproval 测试锁定在四个目标框架的 verified 基线中(如 PublicApiApprovalTest.Approve_Public_Api.DotNet10_0.verified.txt 第 381 行的 public static class EnglishArticle),覆盖 net48、.NET 8 / 10 / 11 目标。这说明两个事实:一是该类型属于稳定公共 API,后续版本不会随意改动其方法签名;二是它在各框架下的可见性是一致的,不存在条件编译造成的 API 差异(string.Create 优化只影响内部实现,不影响签名)。
六、实践要点与适用限制小结
- 版本前提:
EnglishArticles枚举属于 2.x 时代 API,2.13.14 / 2.14.1 快照文档中可见;3.0 起被移除,当前main源码中不存在。3.x 用户应直接使用EnglishArticle。 - 能力边界:该实现只处理英语三类冠词(
a/an/the及其大写形式),且识别受“冠词 + 单空格 + Unicode 单词字符”三重条件约束;Ⅻ一类 Unicode 数字、tab 分隔、双空格前缀均不会被识别(测试用例即为契约)。 - 行为契约:空数组输入抛
ArgumentOutOfRangeException;配对调用AppendArticlePrefix→PrependArticleSuffix才能得到“忽略冠词的排序结果 + 冠词还原”;单独调用只会得到中间形态。 - 相关入口:实现见 src/Humanizer/ArticlePrefixSort.cs,行为测试见 tests/Humanizer.Tests/ArticlePrefixSortTests.cs,移除决策依据见 website/versioned_docs/version-2.13.14/upgrading/version-3-migration.mdx。