☰
Humanizer DefaultDateTimeHumanizeStrategy 深度解析:DateTime 相对时间转文字的默认计算策略
2026/10/7 21:08:04 网站建设 项目流程
  • 开发工具

【免费下载链接】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 中负责把两个DateTime之间的时间差"翻译"成人话的默认策略类,也是DateTime.Humanize()扩展方法在没有显式配置时实际执行的算法入口。本篇将结合仓库源码与测试用例,完整讲解该类的类结构、Humanize方法契约、完整调用链路、区间判定算法、时态与本地化机制,以及与PrecisionDateTimeHumanizeStrategy的差异和切换方式,帮助你彻底理解并驾驭 Humanizer 的相对时间人性化能力。

类概览:继承关系与核心职责

依据 API 文档 Humanizer.DefaultDateTimeHumanizeStrategy.md,该类的完整声明如下:

public class DefaultDateTimeHumanizeStrategy : Humanizer.IDateTimeHumanizeStrategy
  • 继承链:System.Object→DefaultDateTimeHumanizeStrategy
  • 实现接口:Humanizer.IDateTimeHumanizeStrategy
  • 职责描述:The default 'distance of time' -> words calculator,即"时间距离 → 词语"的默认计算器,把两个时间点之间的差值转换为类似"3 小时前""2 天后"这样的人类可读语句。

接口本身定义在 src/Humanizer/DateTimeHumanizeStrategy/IDateTimeHumanizeStrategy.cs,其 XML 注释明确说明了设计意图:

Implement this interface to create a new strategy for DateTime.Humanize and hook it in the Configurator.DateTimeHumanizeStrategy

也就是说,IDateTimeHumanizeStrategy是DateTime.Humanize的可插拔策略契约,任何自定义策略只要实现该接口并挂载到Configurator.DateTimeHumanizeStrategy,即可替换默认行为。DefaultDateTimeHumanizeStrategy正是该契约的标准开箱即用实现。

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,即两个日期之间距离的文字描述。

从实现上看(src/Humanizer/DateTimeHumanizeStrategy/DefaultDateTimeHumanizeStrategy.cs),这个类是一个非常轻薄的委托层:

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

真正的算法逻辑全部集中在静态类DateTimeHumanizeAlgorithms的DefaultHumanize方法中,下一节展开分析。

完整调用链路:从 Humanize() 到算法内核

理解DefaultDateTimeHumanizeStrategy不能只看类本身,还需要知道它是如何被触发的。整条调用链如下:

  1. 入口:开发者在业务代码中调用dateTime.Humanize()扩展方法,定义于 src/Humanizer/DateHumanizeExtensions.cs:
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参数(或根据input.Kind推断)统一 UTC/本地时间基准;最后把调用转发给Configurator.DateTimeHumanizeStrategy。

  1. 策略分发:Configurator.DateTimeHumanizeStrategy是一个公开可写的静态属性,默认值就是new DefaultDateTimeHumanizeStrategy()(见 src/Humanizer/Configuration/Configurator.cs):
public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; } = new DefaultDateTimeHumanizeStrategy();
  1. 算法执行:策略实例的Humanize方法再委托给DateTimeHumanizeAlgorithms.DefaultHumanize。

这条链路清晰体现了 Humanizer 的架构风格:扩展方法负责参数归一化,配置中心负责策略装配,策略类负责分发,算法静态类负责核心计算。

核心算法:区间阈值与单位换算

DefaultHumanize的完整实现位于 src/Humanizer/DateTimeHumanizeStrategy/DateTimeHumanizeAlgorithms.cs。入口先做两件事:

var tense = input > comparisonBase ? Tense.Future : Tense.Past; var ts = new TimeSpan(Math.Abs(comparisonBase.Ticks - input.Ticks));
  • 时态判定:input > comparisonBase说明目标日期在基准之后,属于未来(Tense.Future),否则属于过去(Tense.Past);
  • 时间差计算:用两个日期Ticks之差的绝对值构造TimeSpan,再对TimeSpan做阶梯式区间判定。

随后是几个前处理步骤:用comparisonBase.Date.AddMonths(...)判断是否处于"同一月份",并计算日历日差days。最后进入DefaultHumanize(ts, sameMonth, days, tense, culture)的逐级区间判定逻辑,阈值与输出单位对应关系如下表:

区间条件(TimeSpan 度量)输出单位数值取法
TotalMilliseconds < 500Millisecond固定 0(如 "now")
TotalSeconds < 60Secondts.Seconds
TotalSeconds < 120Minute固定 1
TotalMinutes < 60Minutets.Minutes
TotalMinutes < 90Hour固定 1
TotalHours < 24Hourts.Hours
TotalHours < 48Day日历日差days
TotalDays < 7Dayts.Days
TotalDays < 28Weekts.Days / 7
TotalDays在 28~30 之间Month / Day同一月则 1 个月,否则按天
TotalDays < 345MonthFloor(TotalDays / 29.5)
其余(≥ 345 天)YearFloor(TotalDays / 365),最小为 1

从代码结构可以推断几个设计要点:

  • 靠近阈值的"向上取整":59 秒以上按 1 分钟输出、89 分钟以上按 1 小时输出、47 小时以上按天数输出,这与人类习惯的"约 X 分钟/小时前"表述一致;
  • 28~30 天的月份边界处理:sameMonth标志决定是输出"1 个月"还是继续按天数输出,避免日历上未真正跨月却输出"1 个月前"的歧义;
  • 月与年的近似换算:月按 29.5 天平均折算、年按 365 天折算,说明该算法以近似值优先、不做精确日历运算,这也是"距离时间"场景的合理取舍;
  • 当年数折算为 0 时兜底为 1(if (years == 0) years = 1;),保证至少输出"1 年"。

本地化输出:Formatter 的角色

DefaultHumanize本身不拼写任何语言文本,而是把"单位 + 数量 + 时态"交给Configurator.GetFormatter(culture)得到的IFormatter,调用其DateHumanize(TimeUnit, Tense, quantity)方法完成最终文案(如英文的 "3 hours ago"、中文的"3 小时前"):

var formatter = Configurator.GetFormatter(culture); ... return formatter.DateHumanize(TimeUnit.Day, tense, days);

TimeUnit枚举定义于 src/Humanizer/Localisation/TimeUnit.cs,覆盖 Millisecond、Second、Minute、Hour、Day、Week、Month、Year 等完整单位集合;每个文化区域通过Locale下的 YAML 数据与源码生成器提供各自的复数规则与特殊表述(例如阿拉伯语的"أمس / منذ يومين"、俄语的"секунду назад / 2 секунды назад"等)。多语言验证数据可参见 tests/Humanizer.Tests/Localisation/LocaleDateHumanizeTheoryData.cs,其中覆盖了 ar、az、cs、he、hr、hy、is、ku、lb、mt、nl、pl、ru、sk、sl、uk 等大量区域的过去/未来时态断言。

实际使用示例

以下示例展示Humanize扩展方法在默认策略下的典型用法(需引用Humanizer命名空间):

using Humanizer; // 过去时态:假设当前时间为 2026-10-06 12:00 var threeHoursAgo = DateTime.UtcNow.AddHours(-3); Console.WriteLine(threeHoursAgo.Humanize()); // "3 hours ago" // 未来时态 var tomorrow = DateTime.UtcNow.AddDays(1); Console.WriteLine(tomorrow.Humanize()); // "1 day from now" // 指定比较基准:不依赖系统当前时间,便于测试与离线计算 var input = new DateTime(2026, 10, 1, 0, 0, 0, DateTimeKind.Utc); var baseDate = new DateTime(2026, 10, 6, 0, 0, 0, DateTimeKind.Utc); Console.WriteLine(input.Humanize(utcDate: true, dateToCompareAgainst: baseDate)); // "5 days ago" // 指定区域性:强制使用 en-US 输出 Console.WriteLine(threeHoursAgo.Humanize(culture: new CultureInfo("en-US")));

utcDate参数是可空的bool?:显式传入true/false强制按 UTC/本地处理比较基准;传null时按input.Kind推断。culture传null时使用当前线程区域性。该扩展方法的完整参数语义见 src/Humanizer/DateHumanizeExtensions.cs。

与其他策略的对比与切换

Humanizer 在DateTimeHumanizeStrategy目录下还提供了另一个开箱策略 PrecisionDateTimeHumanizeStrategy.cs:

public class PrecisionDateTimeHumanizeStrategy(double precision = .75) : IDateTimeHumanizeStrategy

两者实现同一接口,差异在于:

  • DefaultDateTimeHumanizeStrategy:使用上文的分级阈值,输出稳定的"整单位"近似值(如 59 秒 → "1 minute ago"),行为直观、跨语言一致;
  • PrecisionDateTimeHumanizeStrategy:引入可配置的precision参数(默认 0.75),在PrecisionHumanize算法中按比例决定是否进位到更大单位(如seconds >= 59 * precision才进位),适合需要微调近似程度的场景。

切换方式即在应用启动时重设Configurator.DateTimeHumanizeStrategy,例如:

Configurator.DateTimeHumanizeStrategy = new PrecisionDateTimeHumanizeStrategy(0.9);

从 Configurator.cs 的注释可以确认一个重要约束:该属性只应在应用启动阶段、任何人性化操作发生之前设置一次;多线程环境下建议使用volatile读取或适当的同步机制,生产环境中应避免在服务运行期间变更。

测试验证:如何证明策略行为

测试侧有两处直接证据:

  1. tests/Humanizer.Tests/DateHumanize.cs 的Verify辅助方法会在测试中显式安装策略:
if (precision.HasValue) { Configurator.DateTimeHumanizeStrategy = new PrecisionDateTimeHumanizeStrategy(precision.Value); } else { Configurator.DateTimeHumanizeStrategy = new DefaultDateTimeHumanizeStrategy(); }

随后按TimeUnit构造对应的TimeSpan偏移(月按 31 天、年按 366 天换算),并用注入的固定基准日期(new DateTime(2013, 6, 20, ...))分别验证 UTC 与本地路径,说明默认策略的判定是基于 TimeSpan 区间而非真实日历的近似算法——这也解释了为什么测试中"1 个月"用 31 天、"1 年"用 366 天来构造。

  1. 公开 API 契约文件(如 tests/Humanizer.Tests/ApiApprover/PublicApiApprovalTest.Approve_Public_Api.DotNet8_0.verified.txt)中锁定DefaultDateTimeHumanizeStrategy的公共签名(含隐式无参构造函数),确保类的公开 API 跨版本稳定。

小结

DefaultDateTimeHumanizeStrategy是 Humanizer 相对时间人性化的默认心脏:它通过IDateTimeHumanizeStrategy契约接入Configurator,将Humanize扩展方法归一化后的时间差委托给DateTimeHumanizeAlgorithms.DefaultHumanize,以一套从毫秒到年的分级阈值完成单位换算,再借由IFormatter与TimeUnit/Tense组合输出本地化文案。理解它的区间算法、时态判定和策略可替换机制,你就能在"开箱即用"与"精确可控"之间自如选择,也能基于同一接口编写完全自定义的日期人性化策略。

  • 开发工具

【免费下载链接】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
点击查看免费下载
上一篇:CFSSL 中的 sqlx/types 数据交换类型:GzippedText、JSONText 与 BitBool 的 Scanner/Valuer 实现指南
下一篇:照片EXIF如何还原一段关系的时间线?ex-skill photo_analyzer源码分析

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

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

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

立即咨询