- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
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);三个参数的语义分别是:
| 参数 | 类型 | 含义 |
|---|---|---|
input | System.DateTime | 要被“人性化”的目标时刻,即希望被描述为相对时间的那一侧 |
comparisonBase | System.DateTime | 参照基准时刻,input相对它计算时间距离(过去 / 未来) |
culture | System.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 < 500 | Millisecond | 0 | now |
TotalSeconds < 60 | Second | ts.Seconds | 5 seconds ago |
TotalSeconds < 120 | Minute | 1 | a minute ago |
TotalMinutes < 60 | Minute | ts.Minutes | 30 minutes ago |
TotalMinutes < 90 | Hour | 1 | an hour ago |
TotalHours < 24 | Hour | ts.Hours | 5 hours ago |
TotalHours < 48 | Day | days(日历日差) | yesterday(1 天) |
TotalDays < 7 | Day | ts.Days | 3 days ago |
TotalDays < 28 | Week | ts.Days / 7 | 2 weeks ago |
TotalDays ∈ [28, 30) | Month(sameMonth为真)或 Day | 1 或ts.Days | a month ago/28 days ago |
TotalDays < 345 | Month | floor(TotalDays / 29.5) | 3 months ago |
| 其余(≥ 345 天) | Year | floor(TotalDays / 365),至少 1 | 2 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 ago2. 启动期替换策略
当应用需要更精细的阈值(如“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
相关推荐
Humanizer 相对时间人性化:DefaultDateTimeHumanizeStrategy 默认策略源码级解析
Humanizer 相对时间人性化:DefaultDateTimeHumanizeStrategy 默认策略源码级解析 Humanizer 的 DefaultD
开发工具Humanizer DefaultDateTimeHumanizeStrategy 详解:默认相对时间短语化算法与本地化实现
Humanizer DefaultDateTimeHumanizeStrategy 详解:默认相对时间短语化算法与本地化实现 导读 本文围绕 Humanizer
开发工具Humanizer 默认日期人性化策略解析:DefaultDateTimeHumanizeStrategy 的算法、调用链与本地化原理
Humanizer 默认日期人性化策略解析:DefaultDateTimeHumanizeStrategy 的算法、调用链与本地化原理 导读 DefaultDa
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考