☰
Humanizer 的 DefaultDateTimeHumanizeStrategy 深入解析:DateTime 相对时间文案的默认策略与算法原理
2026/9/26 7:17:19 网站建设 项目流程
  • 开发工具

【免费下载链接】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
点击查看免费下载

DefaultDateTimeHumanizeStrategy是 Humanizer 库中把“两个时刻之间的距离”翻译成人话(如yesterday、3 hours ago、a month from now)的默认计算器。本文围绕它在 Humanizer.DateTimeHumanizeStrategy.IDateTimeHumanizeStrategy 中的角色,逐步拆解其类定义、Humanize方法签名、底层分级判定算法、文化本地化机制、配置与替换方式,并结合测试代码与官方场景示例给出可直接运行的代码,帮助你彻底掌握DateTime.Humanize()的默认行为及其边界。

类定位:默认的“时间距离 → 文字”计算器

DefaultDateTimeHumanizeStrategy是DateTime.Humanize()扩展方法在默认配置下使用的策略实现。其完整类声明为:

public class DefaultDateTimeHumanizeStrategy : Humanizer.DateTimeHumanizeStrategy.IDateTimeHumanizeStrategy
  • 继承链:System.Object→DefaultDateTimeHumanizeStrategy;
  • 实现的接口:IDateTimeHumanizeStrategy,该接口定义了唯一的契约方法string Humanize(DateTime input, DateTime comparisonBase, CultureInfo? culture),用于“计算两个日期之间的时间距离并转化为文字”。

从实现上看,这个类本身非常薄,是典型的“策略封装 + 算法委托”结构。DefaultDateTimeHumanizeStrategy.cs 的完整源码仅有一个方法体,将全部计算委托给内部的算法类:

public class DefaultDateTimeHumanizeStrategy : IDateTimeHumanizeStrategy { public string Humanize(DateTime input, DateTime comparisonBase, CultureInfo? culture) => DateTimeHumanizeAlgorithms.DefaultHumanize(input, comparisonBase, culture); }

其中DateTimeHumanizeAlgorithms定义在 DateTimeHumanizeAlgorithms.cs,是Default、Precision两套策略共享的算法引擎(DefaultDateTimeHumanizeStrategy走DefaultHumanize,PrecisionDateTimeHumanizeStrategy走PrecisionHumanize)。

Humanize 方法:签名、参数与返回语义

原 API 文档给出的方法签名如下:

public string Humanize(System.DateTime input, System.DateTime comparisonBase, System.Globalization.CultureInfo culture);

三个参数的语义分别是:

参数类型含义
inputSystem.DateTime要被“人性化”的目标时刻,即希望被描述为相对时间的那一侧
comparisonBaseSystem.DateTime参照基准时刻,input相对它计算时间距离(过去 / 未来)
cultureSystem.Globalization.CultureInfo用于输出本地化文案的文化对象;传入null时使用当前线程文化

返回值System.String为本地化的相对时间文案,例如yesterday、3 hours ago、a month from now等。

该方法直接实现了IDateTimeHumanizeStrategy.Humanize(DateTime, DateTime, CultureInfo),因此可以被Configurator.DateTimeHumanizeStrategy以多态方式调用(详见下文“配置与替换”一节)。

扩展方法层:谁在调用它

DefaultDateTimeHumanizeStrategy并不是直接暴露给业务代码的,真正入口是 DateHumanizeExtensions.cs 中的DateTime.Humanize()扩展方法:

public static string Humanize(this DateTime input, bool? utcDate = null, DateTime? dateToCompareAgainst = null, CultureInfo? culture = null) { var comparisonBase = dateToCompareAgainst ?? DateTime.UtcNow; utcDate ??= input.Kind != DateTimeKind.Local; comparisonBase = utcDate.Value ? comparisonBase.ToUniversalTime() : comparisonBase.ToLocalTime(); return Configurator.DateTimeHumanizeStrategy.Humanize(input, comparisonBase, culture); }

几个值得注意的默认行为:

  • 基准时刻:不传dateToCompareAgainst时,默认取DateTime.UtcNow;
  • 时区语义:utcDate为null时,依据input.Kind推断(非 Local 即按 UTC 处理),随后把基准时刻统一换算到同一时区,避免“本地时间与 UTC 时间混比”造成的边界漂移;
  • 可空重载:DateTime?的重载在值为null时返回该文化的never短语(由Formatter.DateHumanize_Never()提供,见 DefaultFormatter.cs);
  • 该方法返回后,文案中出现的数字由DefaultFormatter通过当前文化的数字系统呈现(如 DefaultFormatter.cs 所示,FormatCountValue使用CultureInfo.CurrentCulture格式化计数值)。

核心算法:DefaultHumanize 的分级判定

整个策略的灵魂在DateTimeHumanizeAlgorithms.DefaultHumanize。它先计算两个基础量,再做分层阈值判断:

var tense = input > comparisonBase ? Tense.Future : Tense.Past; var ts = new TimeSpan(Math.Abs(comparisonBase.Ticks - input.Ticks)); var sameMonth = comparisonBase.Date.AddMonths(tense == Tense.Future ? 1 : -1) == input.Date; var days = Math.Abs((input.Date - comparisonBase.Date).Days);
  • tense:input晚于基准为Future,否则为Past,决定输出… from now还是… ago;
  • ts:两时刻Ticks差的绝对值,作为细分判断的依据;
  • sameMonth:判断“相差整一个月”的情况,用于 28~30 天窗口内区分“1 个月”与“N 天”;
  • days:按日历日计算的整日差。

随后按时间跨度从小到大依次命中唯一分支(源码位于 DateTimeHumanizeAlgorithms.cs):

判定条件输出单位计数值英文示例(过去式)
TotalMilliseconds < 500Millisecond0now
TotalSeconds < 60Secondts.Seconds5 seconds ago
TotalSeconds < 120Minute1a minute ago
TotalMinutes < 60Minutets.Minutes30 minutes ago
TotalMinutes < 90Hour1an hour ago
TotalHours < 24Hourts.Hours5 hours ago
TotalHours < 48Daydays(日历日差)yesterday(1 天)
TotalDays < 7Dayts.Days3 days ago
TotalDays < 28Weekts.Days / 72 weeks ago
TotalDays ∈ [28, 30)Month(sameMonth为真)或 Day1 或ts.Daysa month ago/28 days ago
TotalDays < 345Monthfloor(TotalDays / 29.5)3 months ago
其余(≥ 345 天)Yearfloor(TotalDays / 365),至少 12 years ago

几个容易被忽略的细节:

  • 毫秒级即“现在”:只要跨度小于 500ms,直接返回now,而DateOnly特例还会进一步映射为today(见 DateTimeHumanizeAlgorithms.cs,DateOnlyHumanizeToday在DefaultFormatter下把now改写为today);
  • "四舍五入式"的取整惯例:120 秒归为 “1 分钟”、90 分钟归为 “1 小时”、48 小时归为 “1 天”,这是一种向下取整的近似策略,与PrecisionDateTimeHumanizeStrategy的“按精度渐进进位”形成对比;
  • 年与月的估算:月份按29.5天、年份按365天估算,years计算结果为 0 时强制置 1,因此任何超过 345 天的跨度至少会输出a year …。

与 Precision 策略的分工

同一算法文件中还提供了PrecisionHumanize(DateTimeHumanizeAlgorithms.cs),由 PrecisionDateTimeHumanizeStrategy.cs 使用,其构造函数接受一个precision参数(默认0.75)。区别在于:

  • 默认策略:以“自然语言习惯”为优先,例如 48 小时直接说 “2 days ago” 或 “yesterday”;
  • 精度策略:以“数值近似精度”为优先,当ts.Seconds >= 59 * precision时才进位到分钟,适合需要更渐进、更精确的阈值场景。

两者共用同一套Formatter.DateHumanize输出管线,只是“单位与计数”的判定算法不同。

本地化:文案从哪来

DefaultHumanize判定出(TimeUnit, tense, count)后,统一交给Configurator.GetFormatter(culture)解析出的IFormatter渲染(DateTimeHumanizeAlgorithms.cs),最终由 DefaultFormatter.cs 的DateHumanize(TimeUnit, Tense, int)完成“单位 + 时态 + 数量 → 短语”的映射,数据来自生成的LocalePhraseTable。

英语短语数据位于 Locales/en.yml,结构为relativeDate下的now / today / never以及past / future两组:

phrases: relativeDate: now: 'now' today: 'today' never: 'never' past: day: single: 'yesterday' multiple: afterCount: 'ago' forms: singular: 'day' default: 'days' future: day: single: 'tomorrow' multiple: afterCount: 'from now' forms: singular: 'day' default: 'days'

要点:

  • 单复数与特例:count == 1时优先命中single短语(如yesterday、a minute ago),否则使用multiple下的单复数forms并拼接afterCount(ago/from now);
  • 文化覆盖:Humanizer 在 Locales 目录为 90+ 种语言提供同类短语表,Configurator.GetFormatter(culture)按文化解析;culture为null时回退到当前线程文化,这与DateTime.Humanize()的culture参数语义一致;
  • 不匹配即抛错:若某文化缺少所需短语,DateHumanize会抛出InvalidOperationException(DefaultFormatter.cs),提示缺失的 culture 与时间单位。

配置与替换:如何换掉默认策略

DefaultDateTimeHumanizeStrategy之所以是“默认”,是因为它在 Configurator.cs 中被设为DateTimeHumanizeStrategy属性的初始值:

public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; } = new DefaultDateTimeHumanizeStrategy();

因此有两种使用方式:

1. 不配置,直接用默认行为

using Humanizer; var comparison = new DateTime(2025, 1, 20, 12, 0, 0, DateTimeKind.Utc); Console.WriteLine(comparison.AddDays(-1).Humanize(utcDate: true, dateToCompareAgainst: comparison)); // yesterday Console.WriteLine(comparison.AddMinutes(-90).Humanize(utcDate: true, dateToCompareAgainst: comparison)); // an hour ago

2. 启动期替换策略

当应用需要更精细的阈值(如“90 分钟才算 1 小时”)时,可在启动阶段替换:

Configurator.DateTimeHumanizeStrategy = new PrecisionDateTimeHumanizeStrategy(0.75);

官方文档明确提醒(Configurator.cs 的 remarks):

  • 该属性只应在应用启动期设置一次,任何Humanize操作发生之后再修改,可能造成多线程下的不一致;
  • 生产应用中避免在开始服务请求后动态变更;
  • 多线程场景下应使用volatile读取或合适的同步机制。

可空值与 DateOnly/TimeOnly

DateTime?的Humanize重载在值为null时直接返回文化的never短语,不会进入DefaultDateTimeHumanizeStrategy。对于 .NET 6+ 的DateOnly、TimeOnly,Humanizer 分别通过DateOnlyHumanizeStrategy、TimeOnlyHumanizeStrategy属性提供独立策略(默认同为各自“默认策略”类),其中DateOnly的相等特例会被渲染为today而非now(DateTimeHumanizeAlgorithms.cs)。

测试验证与官方示例

测试侧,tests/Humanizer.Tests/DateHumanize.cs 的Verify辅助方法展示了策略如何参与断言:它按timeUnit把单位映射为TimeSpan(其中Month = 31 天、Year = 366 天),然后注入固定的基准时刻(UTC2013-06-20 09:58:22与本地2013-06-20 11:58:22)来消除“CPU 滴答导致测试不稳定”的问题,并通过Configurator.DateTimeHumanizeStrategy = new DefaultDateTimeHumanizeStrategy()显式切换策略后再断言输出文案。同时,ApiApprover 的快照文件确认了该类型对外公开的 API 形态。

官方场景示例 website/docs/scenarios/relative-dates-and-times.mdx 提供了覆盖DateTime/DateTimeOffset/DateOnly/TimeOnly的完整可运行样例(Program.cs),其输出为:

DateTime: yesterday DateTimeOffset: yesterday DateOnly: yesterday TimeOnly: a minute from now Duration: 2 hours, 5 minutes

示例中反复强调一个实践要点:当输出必须是确定性的时候,显式传入dateToCompareAgainst与culture,避免隐式读取机器时钟造成的结果漂移。

使用建议与边界提醒

  • 显式注入基准:日志、审计等场景应传dateToCompareAgainst,让输出与数据产生时刻一致,而不是与“读取日志那一刻”绑定;
  • UTC/本地分离:不要无意中把本地DateTime与 UTC 基准混比;utcDate参数负责统一换算,混合Kind会移动 48 小时、28 天等边界;
  • 选择合适的类型:TimeOnly没有日期上下文,跨午夜比较时无法区分“昨天”还是“明天”,应避免用TimeOnly表达跨日相对时间;
  • 需要更精确的阈值时优先考虑PrecisionDateTimeHumanizeStrategy,但要注意精度参数影响的是“进位时机”而非文案本身的措辞;
  • 空值语义:可空重载返回never适合可选活动时间戳,但领域逻辑中仍应显式处理缺失值。

小结

DefaultDateTimeHumanizeStrategy以极小的类体封装了完整的相对时间判定管线:DateTime.Humanize()扩展方法负责基准与时区归一化,DateTimeHumanizeAlgorithms.DefaultHumanize负责“单位 + 数量”的阈值判定,DefaultFormatter与LocalePhraseTable负责把结果渲染成目标文化的自然语言。理解这三层分工,你就既能预测默认策略在任何跨度下的输出,也能在需要时通过Configurator.DateTimeHumanizeStrategy无缝切换到自定义或精度策略。

  • 开发工具

【免费下载链接】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
点击查看免费下载
上一篇:为什么你的PHP测试这么慢?phpunit-speedtrap揭示真相
下一篇:202309051233 异步编程模式

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

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

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

立即咨询