☰
Humanizer 时间单位体系深度解析:TimeUnit 枚举如何驱动日期与时长的人性化输出
2026/9/26 2:57:35 网站建设 项目流程
  • 开发工具

【免费下载链接】Humanizer

Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities

项目地址:https://gitcode.com/gh_mirrors/hu/Humanizer
点击查看免费下载

导读

Humanizer.Localisation.TimeUnit是 Humanizer 库中定义时间单位的最小公共语言:它将"毫秒、秒、分钟、小时、天、周、月、年"这 8 个时间粒度固化为一个枚举,并作为桥梁连接日期人性化(DateHumanize)、时间跨度人性化(TimeSpanHumanize)、单位符号转换(ToSymbol)与多语言本地化格式化器。本文以该枚举为骨架,结合 TimeUnit.cs 源码、TimeSpanHumanizeExtensions.cs 算法与 TimeUnitToSymbolExtensions.cs 扩展方法,完整讲解其定义、底层调用链、本地化机制与实战用法。读完本文,你将掌握如何用maxUnit/minUnit精确控制人性化输出的粒度,理解月/年近似计算的边界,并能把时间单位转成任意文化的符号形式。

一、TimeUnit 枚举定义与字段速查

1.1 官方 API 定义

关联文档给出的 API 签名与字段数值如下:

public enum TimeUnit
字段数值含义
Millisecond0一毫秒
Second1一秒
Minute2一分钟
Hour3一小时
Day4一天
Week5一周
Month6一月
Year7一年

1.2 源码中的完整实现

仓库中的实际定义位于 TimeUnit.cs,与文档完全一致,且每个成员都带有明确的 XML 文档注释。该枚举直接驻留在Humanizer.Localisation命名空间下,其设计意图从源码注释可见一斑:

Represents the time units supported by Humanizer's relative-time and duration formatters.

也就是说,TimeUnit是 Humanizer 相对时间(relative-time)与时长(duration)格式化两大功能线的公共枚举。它的取值顺序(从小到大)本身就构成了算法遍历的优先级基础——在 TimeSpanHumanizeExtensions.cs 中,库通过Enumerable.Reverse(Enum.GetValues<TimeUnit>())得到一个从大到小排列的单位数组,用于从最大单位开始逐级拆解时间跨度:

static readonly TimeUnit[] TimeUnits = [.. Enumerable.Reverse(Enum.GetValues<TimeUnit>())];

这一细节说明:枚举成员的声明顺序不是随意的,它直接参与算法的单位降序遍历逻辑。

二、TimeUnit 在人性化算法中的三大核心应用

TimeUnit本身不携带行为,它的价值体现在被各大扩展方法消费的调用链中。以下是它最核心的三个应用场景。

2.1 TimeSpan.Humanize:用 maxUnit / minUnit 控制输出粒度

TimeSpan.Humanize是时长人性化的主入口,其签名(见 TimeSpanHumanizeExtensions.cs)中两个直接以TimeUnit为类型的参数决定了输出的上下边界:

public static string Humanize( this TimeSpan timeSpan, int precision = 1, CultureInfo? culture = null, TimeUnit maxUnit = TimeUnit.Week, TimeUnit minUnit = TimeUnit.Millisecond, string? collectionSeparator = ", ", bool toWords = false)

参数语义(来自源码 XML 注释):

  • maxUnit(最大单位,默认TimeUnit.Week):输出中允许出现的最大时间单位。默认值为Week意味着TimeSpan.FromDays(400).Humanize()不会直接输出 "1 year",而是落到以周为最大刻度的表达。特别要注意:Month与Year一旦被选为maxUnit,它们对大跨度(超过 30 天)时间的计算是近似值——按一年 365.2425 天、一个月 30.4369 天折算(源码常量见 TimeSpanHumanizeExtensions.cs):
const double DaysInAYear = 365.2425; // 格里高利历 const double DaysInAMonth = DaysInAYear / 12;
  • minUnit(最小单位,默认TimeUnit.Millisecond):输出中允许出现的最小时间单位。例如要忽略毫秒级噪声,可设minUnit: TimeUnit.Second。

算法在GetTimeUnitPart(TimeSpanHumanizeExtensions.cs)中按timeUnitToGet <= maximumTimeUnit && timeUnitToGet >= minimumTimeUnit过滤单位,随后用switch表达式按单位类型拆解数值(毫秒取Timespan.Milliseconds、天/周/月/年走各自的"特殊大小写"分支,见GetTimeUnitNumericalValue,TimeSpanHumanizeExtensions.cs)。

实测示例(对应 TimeSpanHumanizeTests.cs 的TimeSpanWithMaxTimeUnit测试):

TimeSpan.FromMilliseconds(2_016_000_000).Humanize(maxUnit: TimeUnit.Year); // 例如输出 "3 weeks"(当 maxUnit 为 Week 时) // 或 "1 month" 级别的近似表达(当 maxUnit 为 Month/Year 时)

配合precision参数可输出多段组合,例如Humanize(precision: 4, culture: culture, maxUnit: TimeUnit.Year)(见 TimeSpanHumanizeTests.cs)。

2.2 日期人性化:DefaultHumanize 的分级阈值

在相对日期算法 DateTimeHumanizeAlgorithms.cs 中,TimeUnit是每个判断分支的返回值标签。DefaultHumanize(TimeSpan, ...)内部以一系列阈值阶梯决定输出哪个单位:

时间跨度条件输出单位
TotalMilliseconds < 500TimeUnit.Millisecond(0 毫秒,即 "now")
TotalSeconds < 60TimeUnit.Second
TotalSeconds < 120TimeUnit.Minute(1 分钟)
TotalMinutes < 60TimeUnit.Minute
TotalMinutes < 90TimeUnit.Hour(1 小时)
TotalHours < 24TimeUnit.Hour
TotalHours < 48TimeUnit.Day
TotalDays < 7TimeUnit.Day
TotalDays < 28TimeUnit.Week
TotalDays ∈ [28, 30)且同年同月TimeUnit.Month
TotalDays < 345TimeUnit.Month(按floor(days / 29.5)折算)
其余TimeUnit.Year(按floor(days / 365)折算)

每次判定后,算法调用formatter.DateHumanize(TimeUnit.X, tense, count)把枚举传给格式化器,由本地化层生成具体短语(如 "a minute ago" / "in 3 days")。Tense(过去/未来)与TimeUnit一起决定了短语的最终形态。该算法同样服务于DateTime、DateTimeOffset、DateOnly、TimeOnly四种类型的人性化入口(见 DateHumanizeExtensions.cs)。

2.3 单位符号:ToSymbol 扩展方法

TimeUnitToSymbolExtensions.cs 提供了把时间单位转成符号的单行入口:

public static string ToSymbol(this TimeUnit unit, CultureInfo? culture = null) => Configurator.GetFormatter(culture).TimeUnitHumanize(unit);

对应的 en-US 实测输出由 TimeUnitToSymbolTests.cs 固化:

TimeUnitToSymbol()(en-US)
TimeUnit.Millisecondms
TimeUnit.Seconds
TimeUnit.Minutemin
TimeUnit.Hourh
TimeUnit.Dayd
TimeUnit.Weekweek
TimeUnit.Monthmo
TimeUnit.Yeary

基于此,HumanizeToSymbols扩展(见 TimeSpanHumanizeExtensions.cs)可以把整个 TimeSpan 输出为紧凑符号串,例如"1h 30min",其内部正是对每个TimeUnit部件调用TimeUnitHumanize获取本地化符号后拼接(FormatTimePart中的string.Concat(amount, cultureFormatter.TimeUnitHumanize(timeUnit)),见 TimeSpanHumanizeExtensions.cs)。

三、本地化机制:TimeUnit 如何走向 100+ 语言

TimeUnit的另一个身份是本地化短语表的键。默认格式化器 DefaultFormatter.cs 通过LocalePhraseTable(由源码生成器从 YAML 生成的短语表)为每个文化解析短语,核心方法有三个:

  • DateHumanize(TimeUnit timeUnit, Tense timeUnitTense, int unit)——相对日期短语;
  • TimeSpanHumanize(TimeUnit timeUnit, int unit, bool toWords = false)——时长短语;
  • TimeUnitHumanize(TimeUnit timeUnit)——单位符号。

它们统一定义在 IFormatter.cs 接口中,任何自定义格式化器都必须实现。从TimeUnit到本地化文本的映射源头是各语言的 YAML 文件,例如 en.yml 中relativeDate段为每个past/future×TimeUnit组合配置了单数/复数短语:

past: minute: single: 'a minute ago' multiple: afterCount: 'ago' forms: singular: 'minute' default: 'minutes'

仓库Locales/目录下包含 100 余个 yml(如 zh-CN、ja、de、ru、ar 等),这些文件经Humanizer.SourceGenerators编译期生成LocalePhraseTableCatalog,从而让TimeUnit枚举值在不同的CultureInfo下产出完全不同的文本。这正是 Humanizer "meets all your .NET needs for … dates, times, timespans" 多语言能力的底层实现路径。

对于需要语法格(grammatical case)的语言(如匈牙利语、芬兰语、马拉雅拉姆语),TimeUnit还参与IGrammaticalCaseTimeSpanFormatter接口的格变体解析(DefaultFormatter.cs),可见该枚举贯穿了从最基础的符号输出到最复杂的形态学本地化。

四、进阶用法与注意事项

4.1 以 TimeUnit 为参数的完整调用组合

Humanize的完整形态(TimeSpanHumanizeExtensions.cs)允许同时控制精度、空单位计数与文化:

timeSpan.Humanize( precision: 2, countEmptyUnits: false, culture: new CultureInfo("zh-CN"), maxUnit: TimeUnit.Year, minUnit: TimeUnit.Second, collectionSeparator: " ", toWords: false);
  • precision:最多返回的单位数,默认 1(只返回最大单位);
  • countEmptyUnits:是否把数值为 0 的中间单位也计入precision(前导空单位永远不计);
  • collectionSeparator:多段输出的连接符,为 null 时使用文化默认的集合格式化器。

4.2 ToAge 与符号模式的单位边界

  • TimeSpan.ToAge(...)(TimeSpanHumanizeExtensions.cs)默认maxUnit = TimeUnit.Year,把时长转成 "40 years old" 式的年龄表达;
  • HumanizeToSymbols/HumanizeToSymbolsWithFractionalSeconds同样接受TimeUnit边界参数;其中小数秒模式要求maxUnit落在TimeUnit.Second与TimeUnit.Year之间,否则抛ArgumentOutOfRangeException(ValidateFractionalSecondArguments,TimeSpanHumanizeExtensions.cs)。

4.3 需要留意的三点边界事实

  1. 月与年的近似性:默认maxUnit = TimeUnit.Week时不会触发月/年近似;只有显式把maxUnit设为Month/Year,大跨度时间才会按 365.2425 天/年、30.4369 天/月的固定比率折算(源码注释明确说明这是 approximations)。
  2. 符号与词的差异:toWords为 true 时单位前的数字转为文字(如 "one day"),且部分语言为词模式提供独立的短语变体(SingleWordsVariant/MultipleWordsVariant,见 DefaultFormatter.cs)。
  3. 枚举顺序即算法顺序:TimeUnit的成员声明顺序被Enum.GetValues直接消费,新增单位时需谨慎调整拆解算法(GetTimeUnitNumericalValue的 switch)与其保持一致。

五、可验证的源码与测试指引

想要在本地深入验证上述行为,可直接阅读与运行以下文件:

  • 枚举定义:src/Humanizer/Localisation/TimeUnit.cs
  • 符号扩展与测试:src/Humanizer/TimeUnitToSymbolExtensions.cs、tests/Humanizer.Tests/TimeUnitToSymbolTests.cs
  • 时长算法:src/Humanizer/TimeSpanHumanizeExtensions.cs、tests/Humanizer.Tests/TimeSpanHumanizeTests.cs
  • 日期算法:src/Humanizer/DateTimeHumanizeStrategy/DateTimeHumanizeAlgorithms.cs、tests/Humanizer.Tests/DateHumanize.cs
  • 本地化短语源:src/Humanizer/Locales/en.yml(及同目录其他语言的 yml)

在测试项目中直接运行dotnet test tests/Humanizer.Tests/Humanizer.Tests.csproj --filter "FullyQualifiedName~TimeUnitToSymbolTests"即可复现文中符号映射表,--filter "FullyQualifiedName~TimeSpanWithMaxTimeUnit"可验证maxUnit边界行为。理解TimeUnit这一最小枚举,就等于拿到了解读 Humanizer 全部时间人性化能力的钥匙。

  • 开发工具

【免费下载链接】Humanizer

Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities

项目地址:https://gitcode.com/gh_mirrors/hu/Humanizer
点击查看免费下载

相关推荐

上一篇:openEuler内存与存储管理:GMEM内存池与HSAK高效存储解决方案的完整指南 🚀
下一篇:5个实用技巧:用Rprocps-ng提升你的Linux系统管理效率

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询