- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
导读
本文围绕 Humanizer 中负责将DateTime转换为序数词日期文本的IDateToOrdinalWordConverter接口展开,系统讲解其两个Convert方法的重载语义、默认实现的本地化行为、基于 YAML 语言配置的注册机制,以及自定义转换器的注册方式。读完本文,你将掌握ToOrdinalWords扩展方法的底层调用链,理解英文与非英文文化下序数日期的不同渲染策略,并能根据业务需要接入自己的日期序数词转换实现。
接口定位:本地化ToOrdinalWords的转换契约
IDateToOrdinalWordConverter是 Humanizer 中“把日期转成序数词文本”这一能力的抽象契约,其 XML 文档注释明确指出:The interface used to localise the ToOrdinalWords method(用于本地化ToOrdinalWords方法的接口)。也就是说,所有文化(Culture)相关的序数日期文本生成逻辑,都收敛到这一个接口之下,由它统一承载。
接口定义位于 src/Humanizer/Localisation/DateToOrdinalWords/IDateToOrdinalWordConverter.cs,完整签名如下:
namespace Humanizer; /// <summary> /// Converts dates into the localized text used by <c>ToOrdinalWords</c>. /// </summary> public interface IDateToOrdinalWordConverter { /// <summary> /// Converts the given <paramref name="date"/> to ordinal words for the current culture. /// </summary> string Convert(DateTime date); /// <summary> /// Converts the given <paramref name="date"/> to ordinal words using the specified grammatical case. /// </summary> string Convert(DateTime date, GrammaticalCase grammaticalCase); }与DateOnly(.NET 6+)对应的IDateOnlyToOrdinalWordConverter接口同位于 src/Humanizer/Localisation/DateToOrdinalWords/IDateOnlyToOrdinalWordConverter.cs,二者结构对称,本文以IDateToOrdinalWordConverter为主线展开。
Convert(DateTime):按当前文化输出序数日期
string Convert(System.DateTime date);- 参数:
date,类型为System.DateTime,即待转换的日期值。 - 返回值:
System.String,即本地化的序数日期文本。
该方法不接收任何文化参数,实际文化由实现内部决定——默认实现读取的是CultureInfo.CurrentCulture(详见下文“默认实现”一节),因此输出的语言与格式随线程当前文化而变化。
Convert(DateTime, GrammaticalCase):按指定语法格输出序数日期
string Convert(System.DateTime date, Humanizer.GrammaticalCase grammaticalCase);- 参数:
date:System.DateTime,待转换日期;grammaticalCase:Humanizer.GrammaticalCase,希望应用于输出文本的语法格(如主格 Nominative、属格 Genitive 等)。
- 返回值:
System.String,本地化序数日期文本。
该重载主要为拥有格位系统的语言(如俄语、波兰语、乌克兰语等)服务:同一日期在不同语法格下需要不同的词形。GrammaticalCase枚举定义于 src/Humanizer/GrammaticalCase.cs,对于没有语法格概念的语言(如英语),此参数不会产生任何效果——默认实现会直接忽略它并委托给无参重载。
入口:ToOrdinalWords 扩展方法与配置中枢
IDateToOrdinalWordConverter并不直接暴露给调用方,日常使用是通过 src/Humanizer/DateToOrdinalWordsExtensions.cs 中的ToOrdinalWords扩展方法触发,例如:
new DateTime(2023, 1, 1).ToOrdinalWords() // en-US 下 => "1st of January, 2023" new DateTime(2020, 12, 22).ToOrdinalWords() // en-US 下 => "22nd of December, 2020"其实现非常简洁,直接委托给全局配置中枢:
public static string ToOrdinalWords(this DateTime input) => Configurator.DateToOrdinalWordsConverter.Convert(input); public static string ToOrdinalWords(this DateTime input, GrammaticalCase grammaticalCase) => Configurator.DateToOrdinalWordsConverter.Convert(input, grammaticalCase);这里的关键是Configurator。在 src/Humanizer/Configuration/Configurator.cs 中可以看到,日期序数词转换器与其他本地化组件一样,被组织在一个LocaliserRegistry注册表里:
public static LocaliserRegistry<IDateToOrdinalWordConverter> DateToOrdinalWordsConverters { get; } = new DateToOrdinalWordsConverterRegistry(); internal static IDateToOrdinalWordConverter DateToOrdinalWordsConverter => DateToOrdinalWordsConverters.ResolveForCulture(null);ResolveForCulture(null)表示按当前线程文化解析(null即CultureInfo.CurrentCulture),因此ToOrdinalWords的输出始终跟随运行线程的文化设置。整个调用链可概括为:
DateTime.ToOrdinalWords() -> Configurator.DateToOrdinalWordsConverter(按当前文化解析) -> DateToOrdinalWordsConverterRegistry(注册表) -> IDateToOrdinalWordConverter.Convert(date[, grammaticalCase])注册表与默认实现
注册表结构
src/Humanizer/Configuration/DateToOrdinalWordsConverterRegistry.cs 定义了该接口的注册表:
class DateToOrdinalWordsConverterRegistry : LocaliserRegistry<IDateToOrdinalWordConverter> { public DateToOrdinalWordsConverterRegistry() : base(_ => new DefaultDateToOrdinalWordConverter()) => DateToOrdinalWordsConverterRegistryRegistrations.Register(this); }它继承自泛型基类LocaliserRegistry<TLocaliser>(见 src/Humanizer/Configuration/LocaliserRegistry.cs),并以DefaultDateToOrdinalWordConverter作为兜底默认实现——任何未显式注册的文化都会回落到它。而DateToOrdinalWordsConverterRegistryRegistrations.Register是 Humanizer 源生成器(Source Generator)根据语言 YAML 配置自动生成的一部分,它把各文化的专用转换器按 locale 代码注册进注册表,实现了“配置驱动”的本地化扩展。
LocaliserRegistry<TLocaliser>提供了几个关键能力:
Register(string localeCode, TLocaliser localiser):注册指定 locale 的转换器实例;Register(string localeCode, Func<CultureInfo, TLocaliser> localiser):注册按文化工厂创建的转换器;ResolveForCulture(CultureInfo? culture):解析指定文化对应的转换器,null表示当前线程文化;- 解析时先按精确文化名匹配,再沿
culture.Parent链向上回退,最终落到默认实现。
默认实现:DefaultDateToOrdinalWordConverter
src/Humanizer/Localisation/DateToOrdinalWords/DefaultDateToOrdinalWordConverter.cs 是默认转换器,其行为可概括为两条规则:
class DefaultDateToOrdinalWordConverter : IDateToOrdinalWordConverter { const char LeftToRightMark = (char)0x200E; const char RightToLeftMark = (char)0x200F; const char ArabicLetterMark = (char)0x061C; public virtual string Convert(DateTime date) { var culture = CultureInfo.CurrentCulture; if (culture.TwoLetterISOLanguageName != "en") { // 非英语文化:直接采用该文化自身的短日期模式("d"), // 并剥离日历格式化时嵌入的方向性控制字符(LRM/RLM/ALM), // 使结果嵌入更大的序数短语时保持可读。 return SanitizeNonEnglishDate(date.ToString("d", culture)); } return date.Day.Ordinalize() + date.ToString(" MMMM yyyy"); } public virtual string Convert(DateTime date, GrammaticalCase grammaticalCase) => Convert(date); static string SanitizeNonEnglishDate(string value) => value.Replace(LeftToRightMark.ToString(), string.Empty) .Replace(RightToLeftMark.ToString(), string.Empty) .Replace(ArabicLetterMark.ToString(), string.Empty); }要点拆解:
- 英语文化(en):输出形式为「序数日 + 空格 + 月份全名 + 空格 + 年份」,例如
22nd December 2020(实际链接到ToOrdinalWords扩展方法后还带有 “of” 介词,见下文示例)。日部分由date.Day.Ordinalize()生成,即复用 Humanizer 的序数化能力。 - 非英语文化:不做“翻译式”处理,而是直接采用该文化自身的短日期格式字符串
"d"(例如ru-RU的dd.MM.yyyy),随后做一次“清洗”——移除格式化输出中可能嵌入的 Unicode 方向性控制字符:U+200E(LRM,左至右标记)、U+200F(RLM,右至左标记)与U+061C(ALM,阿拉伯字母标记)。这是因为部分日历(尤其阿拉伯、希伯来等 RTL 日历)在短日期输出中会内嵌这些控制符,剥离后文本在嵌入更大句子的场景下不会干扰阅读顺序。 - 语法格重载:默认实现中
Convert(date, grammaticalCase)直接忽略语法格参数并调用无参版本,注释明确说明这是因为回退转换器不随语法格变化词形——语法格支持由各语言的专用转换器提供。
模式化转换器:PatternDateToOrdinalWordsConverter
除了默认实现,还有基于预构建模式(pattern)的专用转换器 src/Humanizer/Localisation/DateToOrdinalWords/PatternDateToOrdinalWordsConverter.cs:
class PatternDateToOrdinalWordsConverter(OrdinalDatePattern pattern) : DefaultDateToOrdinalWordConverter { readonly OrdinalDatePattern pattern = pattern; public override string Convert(DateTime date) => pattern.Format(date); }它继承默认实现并重写Convert(DateTime),将格式化工作委托给 src/Humanizer/Localisation/DateToOrdinalWords/OrdinalDatePattern.cs 中定义的OrdinalDatePattern。这是源生成器按语言配置产出各文化转换器的核心载体,详见下一节。
语言配置驱动:YAML 中的 ordinal.date 与源码生成
Humanizer 的本地化数据以 YAML 形式存放于 src/Humanizer/Locales 目录(如en.yml、ru.yml、zh-CN.yml等),由Humanizer.SourceGenerators在编译期读取并生成注册代码(DateToOrdinalWordsConverterRegistryRegistrations即由此产出)。
以 src/Humanizer/Locales/en.yml 为例,英语的序数日期配置为:
ordinal: date: engine: 'pattern' pattern: '{day} MMMM yyyy' dayMode: 'Ordinal' dateOnly: engine: 'pattern' pattern: '{day} MMMM yyyy' dayMode: 'Ordinal'而以 src/Humanizer/Locales/ru.yml 为例,俄语的配置则把日期渲染为纯数字模式:
ordinal: date: engine: 'pattern' pattern: '{day} MMMM yyyy' dayMode: 'Numeric' dateOnly: engine: 'pattern' pattern: '{day} MMMM yyyy' dayMode: 'Numeric'可以看到配置中有三个关键字段:
| 字段 | 取值示例 | 含义 |
|---|---|---|
engine | pattern | 指定转换器的生成引擎类型;目前日期序数词走pattern模式,即生成PatternDateToOrdinalWordsConverter |
pattern | {day} MMMM yyyy | 日期格式模板,其中{day}是日的占位符,其余部分遵循 .NET 自定义日期格式说明符 |
dayMode | Ordinal/Numeric等 | 日部分的渲染模式 |
dayMode的完整取值定义在 src/Humanizer/Localisation/DateToOrdinalWords/OrdinalDatePattern.cs 的OrdinalDateDayMode枚举中:
enum OrdinalDateDayMode { /// <summary>Renders the day as a culture-aware numeric value.</summary> Numeric, /// <summary>Renders the day as an ordinal word.</summary> Ordinal, /// <summary>Renders the day as a numeric value except for the first day of the month.</summary> OrdinalWhenDayIsOne, /// <summary>Renders the first day of the month using the masculine ordinal form.</summary> MasculineOrdinalWhenDayIsOne, /// <summary>Renders the day as a numeric value followed by a dot suffix.</summary> DotSuffix }各模式的渲染逻辑对应OrdinalDatePattern.FormatDay(int day)的 switch 分支:Numeric输出本地化数字;Ordinal调用day.Ordinalize();OrdinalWhenDayIsOne仅在日为 1 时输出序数词;MasculineOrdinalWhenDayIsOne则在日为 1 时输出阳性序数形式(day.Ordinalize(GrammaticalGender.Masculine));DotSuffix输出数字加英文句点(如德语风格的1.)。
此外OrdinalDatePattern还承担了若干精细工作:
- 日历兼容:
OrdinalDateCalendarMode.Gregorian模式下会把文化日历强制切换为本地化公历(GregorianCalendar),保证年份输出一致;Native模式则保留文化原生日历(如泰历)。 - 属格月份:对斯拉夫语系等存在属格(genitive)月份词形的语言,当模式中“日与月相邻”(如
{day} MMMM)时,会替换为monthsGenitive数组中的属格月名(参见SubstituteMonth与IsDayAdjacentToMonth的实现)。 - Hijri 日历月份:支持
hijriMonths覆盖数组,用于HijriCalendar/UmAlQuraCalendar下的月名替换(ResolveMonthArray)。 - 方向性控制符剥离:与默认实现一样,最终输出会去除
\u200E、\u200F、\u061C等控制字符(StripDirectionalityControls)。
从源生成器一侧看,src/Humanizer.SourceGenerators/Generators/ProfileCatalogs/OrdinalDateProfileCatalogInput.cs 负责读取上述 YAML 配置并生成对应的注册表达式:engine为pattern时,产出new PatternDateToOrdinalWordsConverter(new OrdinalDatePattern(...))形式的注册代码(CreatePatternExpression),并把pattern、dayMode、calendarMode与月份数组等参数逐一映射进OrdinalDatePattern构造器。
自定义转换器:注册与替换
IDateToOrdinalWordConverter作为一个公开接口,允许使用者注入自己的实现。标准做法是通过Configurator.DateToOrdinalWordsConverters注册表注册:
using Humanizer; public sealed class MyDateToOrdinalWordConverter : IDateToOrdinalWordConverter { public string Convert(DateTime date) => $"Day {date.Day} of month {date.ToString("MMMM", System.Globalization.CultureInfo.CurrentCulture)} in {date.Year}"; public string Convert(DateTime date, GrammaticalCase grammaticalCase) => Convert(date); } // 应用启动阶段(如 Main 方法或 ModuleInitializer)注册: Configurator.DateToOrdinalWordsConverters.Register("en-US", new MyDateToOrdinalWordConverter()); // 或使用工厂形式: Configurator.DateToOrdinalWordsConverters.Register("fr-FR", _ => new MyDateToOrdinalWordConverter());注册后,在对应文化下调用new DateTime(2023, 5, 17).ToOrdinalWords()就会走自定义实现。需要注意两点:
- 注册时机:
LocaliserRegistry.Register在注册表首次被解析(即首次调用ToOrdinalWords)之后会抛InvalidOperationException(“Cannot register localisers after the registry has been used.”),因为注册表在首次使用时会将字典“冻结”为FrozenDictionary以提升读取性能。因此自定义注册必须放在应用启动阶段、任何ToOrdinalWords调用之前。 - 回退链:未注册的文化会沿
culture.Parent链逐级回退(如en-US未注册可回退到en),最终落到DefaultDateToOrdinalWordConverter。若需要全局替换所有文化的行为,可参考LocaliserRegistry的默认实现构造函数,把默认转换器替换为自定义类型。
行为验证与测试参考
仓库测试集Humanizer.Tests中与本主题相关的测试(如 tests/Humanizer.Tests/DateToOrdinalWordsExtensions.cs 及本地化目录下的日期序数测试)验证了以下行为:
- 英语文化下
DateTime输出包含序数词(st/nd/rd/th)的日期短语; - 非英语文化(如
ru、de、it等)输出遵循其文化短日期格式; GrammaticalCase重载在支持格位系统的语言上产生不同词形,在英语上无差异;- 剥离方向性控制字符后,RTL 日历输出不携带干扰阅读的嵌入标记。
如需扩充测试或本地化数据,可对照 src/Humanizer/Locales 下的 YAML 配置与 src/Humanizer.SourceGenerators 的生成逻辑;完整的 API 文档快照见 website/versioned_docs/version-3.0.1/api/Humanizer.IDateToOrdinalWordConverter.md。
小结
IDateToOrdinalWordConverter是 Humanizer 日期序数词本地化的核心抽象:Convert(DateTime)负责按当前文化生成序数日期文本,Convert(DateTime, GrammaticalCase)在支持格位系统的语言上提供语法格变体。其背后是由Configurator注册表、YAML 语言配置、源生成器与OrdinalDatePattern模式引擎构成的完整流水线——英语走“序数词 + 月份 + 年份”,其余文化走“文化短日期 + 方向性控制符清洗”。理解这一接口与它的默认实现、注册机制,既能帮助你正确使用ToOrdinalWords,也为定制多语言日期呈现提供了明确的扩展点。
- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
相关推荐
Humanizer 本地化序数日期转换接口 IDateToOrdinalWordConverter 深入解析
Humanizer 本地化序数日期转换接口 IDateToOrdinalWordConverter 深入解析 IDateToOrdinalWordConvert
开发工具Humanizer 日期转序数词本地化接口深度解析:IDateToOrdinalWordConverter 与 IDateOnlyToOrdinalWordConverter
Humanizer 日期转序数词本地化接口深度解析:IDateToOrdinalWordConverter 与 IDateOnlyToOrdinalWordCo
开发工具Humanizer 的 IDateToOrdinalWordConverter:日期转序数词文本的本地化转换接口解析
Humanizer 的 IDateToOrdinalWordConverter:日期转序数词文本的本地化转换接口解析 IDateToOrdinalWordCon
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考