☰
Humanizer 的 IDateTimeHumanizeStrategy:深入解析 DateTime.Humanize 可插拔策略接口
2026/10/7 9:24:39 网站建设 项目流程
  • 开发工具

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

IDateTimeHumanizeStrategy是 Humanizer 中负责把两个DateTime之间的时间距离转化为自然语言句子的策略接口,所有DateTime.Humanize()调用最终都会委托给挂载在Configurator.DateTimeHumanizeStrategy上的某个实现。本文以该接口为核心,结合仓库源码与测试,说明接口契约、内置的默认与精度策略实现、底层算法、注册与切换方式,以及如何编写并挂载自定义策略,帮助你完全掌控 Humanizer 相对时间表达的生成逻辑。

接口定义与契约

IDateTimeHumanizeStrategy定义在 src/Humanizer/DateTimeHumanizeStrategy/IDateTimeHumanizeStrategy.cs,完整声明如下:

namespace Humanizer; /// <summary> /// Implement this interface to create a new strategy for DateTime.Humanize and hook it in the Configurator.DateTimeHumanizeStrategy /// </summary> public interface IDateTimeHumanizeStrategy { /// <summary> /// Calculates the distance of time in words between two provided dates used for DateTime.Humanize /// </summary> string Humanize(DateTime input, DateTime comparisonBase, CultureInfo? culture); }

接口只有一个方法Humanize,契约要点如下:

参数类型含义
inputSystem.DateTime要被人类化的目标日期
comparisonBaseSystem.DateTime比较基准日期(通常为"现在")
cultureSystem.Globalization.CultureInfo?本地化文化信息,传入null表示使用当前线程文化
返回值System.String两个日期之间时间距离的文字表述

从方法签名可以看出,Humanizer 把"相对时间表述"抽象成一次纯粹的"距离计算 + 本地化格式化":策略只负责根据两个时刻计算出一个语义化结果,具体是"现在""2 分钟前""下个月"还是其他说法,取决于策略算法与所选文化的格式化器。XML 文档注释同时明确了该接口的设计意图——实现它即可为DateTime.Humanize创建新策略,并挂载到Configurator.DateTimeHumanizeStrategy(详见下文"策略挂载点"一节)。

策略挂载点:Configurator.DateTimeHumanizeStrategy

接口注释中提到的挂载点位于 src/Humanizer/Configuration/Configurator.cs,其声明如下:

/// <summary> /// The strategy to be used for DateTime.Humanize /// </summary> public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; } = new DefaultDateTimeHumanizeStrategy();

值得注意的几点:

  • 默认实现:该属性默认初始化为new DefaultDateTimeHumanizeStrategy(),即你不做任何配置时,DateTime.Humanize就走默认策略。
  • 静态可变:DateTimeHumanizeStrategy是静态属性,可在应用启动阶段替换为任何自定义实现。
  • 线程安全要求:源码remarks明确要求"该属性应只在应用启动时、任何人类化操作发生之前设置一次",在多线程场景下访问需要使用 volatile 读取或同步机制;生产应用中应避免在服务开始处理请求后修改该值。测试代码 tests/Humanizer.Tests/DateHumanize.cs 也正是通过Configurator.DateTimeHumanizeStrategy = new PrecisionDateTimeHumanizeStrategy(precision.Value)/new DefaultDateTimeHumanizeStrategy()来切换两种策略的。

调用链:从 DateTime.Humanize 到策略

接口本身不直接暴露给用户,真正被调用的是DateHumanizeExtensions中的扩展方法。在 src/Humanizer/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); }

这条调用链揭示了几个重要事实:

  1. 当未显式传入dateToCompareAgainst时,默认以DateTime.UtcNow为比较基准;
  2. utcDate参数(null时取input.Kind != DateTimeKind.Local)决定基准时刻被转换为 UTC 还是本地时间,避免DateTimeKind差异导致的比较错误;
  3. 最终一行把三个参数(input、comparisonBase、culture)直接转交给Configurator.DateTimeHumanizeStrategy.Humanize——这正是接口契约的调用现场,也解释了为什么"实现该接口 + 替换该属性"即可完整接管相对时间输出;
  4. 可空版本DateTime?.Humanize()在值为null时不会走策略,而是直接返回文化相关的DateHumanize_Never()(通常为 "never"),见同一文件 src/Humanizer/DateHumanizeExtensions.cs。

此外,DateTimeOffset.Humanize走的是同族接口IDateTimeOffsetHumanizeStrategy,其默认实现 src/Humanizer/DateTimeHumanizeStrategy/DefaultDateTimeOffsetHumanizeStrategy.cs 内部会把两个DateTimeOffset统一转成UtcDateTime后复用同一套默认算法,可见策略族之间存在明显的代码复用设计。

内置实现之一:DefaultDateTimeHumanizeStrategy

默认策略类位于 src/Humanizer/DateTimeHumanizeStrategy/DefaultDateTimeHumanizeStrategy.cs,实现非常薄:

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

真正的算法在静态类DateTimeHumanizeAlgorithms(src/Humanizer/DateTimeHumanizeStrategy/DateTimeHumanizeAlgorithms.cs)中。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); return DefaultHumanize(ts, sameMonth, days, tense, culture);

随后按时间差落入的区间选择时间单位,核心分段规则(摘录自DefaultHumanize(TimeSpan, bool, int, Tense, CultureInfo)的if链)如下:

时间差条件输出单位
TotalMilliseconds < 500TimeUnit.Millisecond,量词 0("now")
TotalSeconds < 60TimeUnit.Second
TotalSeconds < 120TimeUnit.Minute,量词 1("a minute")
TotalMinutes < 60TimeUnit.Minute
TotalMinutes < 90TimeUnit.Hour,量词 1("an hour")
TotalHours < 24TimeUnit.Hour
TotalHours < 48TimeUnit.Day
TotalDays < 7TimeUnit.Day
TotalDays < 28TimeUnit.Week
TotalDays 在 [28, 30)若同月输出TimeUnit.Month量词 1,否则输出TimeUnit.Day
TotalDays < 345TimeUnit.Month,量词为Floor(TotalDays / 29.5)
其余TimeUnit.Year,量词为Floor(TotalDays / 365)(至少为 1)

最终统一通过Configurator.GetFormatter(culture).DateHumanize(timeUnit, tense, unit)生成本地化文案,即"把时间和量词交给文化对应的格式化器",因此默认策略天然支持全部已覆盖文化的相对时间表达。

内置实现之二:PrecisionDateTimeHumanizeStrategy

精度策略类位于 src/Humanizer/DateTimeHumanizeStrategy/PrecisionDateTimeHumanizeStrategy.cs,是默认策略的"近似精度版":

public class PrecisionDateTimeHumanizeStrategy(double precision = .75) : IDateTimeHumanizeStrategy { readonly double precision = precision; public string Humanize(DateTime input, DateTime comparisonBase, CultureInfo? culture) => DateTimeHumanizeAlgorithms.PrecisionHumanize(input, comparisonBase, precision, culture); }
  • 构造函数参数:precision为近似精度,默认0.75;源码remarks明确说明"若未提供,将使用 0.75 作为默认精度"。API 文档页 Humanizer.PrecisionDateTimeHumanizeStrategy.md 中记录的签名即为public PrecisionDateTimeHumanizeStrategy(double precision = 0.75)。
  • 算法差异:对应算法PrecisionHumanize在 src/Humanizer/DateTimeHumanizeStrategy/DateTimeHumanizeAlgorithms.cs 中,先按precision做"进位式"近似(例如ts.Milliseconds >= 999 * precision则秒数加 1、seconds >= 59 * precision则分钟加 1……),再做月、年推算,最后从大到小选择首个非零时间单位输出。这种设计让使用者可以通过调节precision控制"何时向上取整到下一个单位"。

精度策略的典型行为可以通过测试直观验证。tests/Humanizer.Tests/DateTimeHumanizePrecisionStrategyTests.cs 在UseCulture("en-US")与默认精度0.75下断言:

  • 749 毫秒 → "now",750 毫秒 → "one second ago"(临界点由999 * 0.75 ≈ 749.25决定);
  • 44 秒 → "44 seconds ago",45 秒 → "a minute ago"(59 * 0.75 = 44.25处进位);
  • 44 分钟 → "44 minutes ago",45 分钟 → "an hour ago";
  • 17 小时 → "17 hours ago",18 小时 → "yesterday",42 小时 → "2 days ago"。

对比默认策略 tests/Humanizer.Tests/DateHumanize.cs 与 tests/Humanizer.Tests/DateHumanizeDefaultStrategyTests.cs,可以看到两种策略在边界处的取舍不同:默认策略偏向"整单位直觉"(如 45 秒进分钟、90 分钟进小时、48 小时进天),精度策略则完全由precision参数驱动。

编写并挂载自定义策略

基于接口契约,自定义策略只需实现Humanize(DateTime input, DateTime comparisonBase, CultureInfo? culture)。一个完整可运行的示例:

using System.Globalization; using Humanizer; public sealed class CustomDateTimeHumanizeStrategy : IDateTimeHumanizeStrategy { public string Humanize(DateTime input, DateTime comparisonBase, CultureInfo? culture) { var delta = comparisonBase - input; return delta.TotalDays switch { < 1 => delta.Hours < 1 ? "just now" : $"{delta.Hours} hour(s) ago", < 7 => $"{delta.Days} day(s) ago", < 30 => $"{delta.Days / 7} week(s) ago", < 365 => $"{delta.Days / 30} month(s) ago", _ => $"{delta.Days / 365} year(s) ago" }; } }

在应用启动阶段(如Program.Main、Startup或ModuleInitializer)挂载:

Configurator.DateTimeHumanizeStrategy = new CustomDateTimeHumanizeStrategy();

之后所有DateTime.Humanize()调用都会自动走自定义逻辑。若只想微调近似程度而不替换整个策略,则使用内置的PrecisionDateTimeHumanizeStrategy即可:

Configurator.DateTimeHumanizeStrategy = new PrecisionDateTimeHumanizeStrategy(precision: 0.9);

需要强调的约束(来自 src/Humanizer/Configuration/Configurator.cs 的remarks):由于该属性是进程级静态状态且被扩展方法直接读取,务必在应用启动早期、任何Humanize调用发生之前完成设置,并在多线程场景下注意可见性与同步,生产环境不应在运行中反复变更。

策略族全景与延伸阅读

IDateTimeHumanizeStrategy并非孤立存在,Humanizer 为不同日期时间类型建立了平行的策略族,它们共享同一套DateTimeHumanizeAlgorithms核心算法:

  • DateTime→IDateTimeHumanizeStrategy(本文主题),默认DefaultDateTimeHumanizeStrategy,精度版PrecisionDateTimeHumanizeStrategy,均位于 src/Humanizer/DateTimeHumanizeStrategy/;
  • DateTimeOffset→IDateTimeOffsetHumanizeStrategy,默认实现复用DateTimeHumanizeAlgorithms.DefaultHumanize(input.UtcDateTime, comparisonBase.UtcDateTime, culture);
  • DateOnly、TimeOnly(.NET 6+ 条件编译NET6_0_OR_GREATER)→ 各自的IDateOnlyHumanizeStrategy、ITimeOnlyHumanizeStrategy,对应挂载点Configurator.DateOnlyHumanizeStrategy、Configurator.TimeOnlyHumanizeStrategy。

也就是说,掌握了IDateTimeHumanizeStrategy的"接口 → 挂载点 → 扩展方法 → 算法 → 文化格式化器"这条链路,就能举一反三理解整个 Humanizer 日期人类化体系的扩展方式。更多背景可参考 ARCHITECTURE.md 与 API 文档目录 website/versioned_docs/version-3.0.8/api/,其中包含 Humanizer.DefaultDateTimeHumanizeStrategy.md 与 Humanizer.PrecisionDateTimeHumanizeStrategy.md 的完整 API 记录。

  • 开发工具

【免费下载链接】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
点击查看免费下载
上一篇:HTTrack安全实践指南:7个关键步骤避免网站镜像风险与陷阱
下一篇:ta-lib-python性能优化案例:从2秒到200毫秒

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

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

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

立即咨询