- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
Humanizer 的DefaultDateTimeHumanizeStrategy是 .NET 平台下将两个DateTime之间的时间距离转换为自然语言(如 "2 hours ago"、"tomorrow"、"one month from now")的默认计算策略。本文以该策略的 API 参考文档为核心,结合 src/Humanizer/DateTimeHumanizeStrategy/DefaultDateTimeHumanizeStrategy.cs 及配套算法源码、配置入口和测试用例,完整讲解它的类结构、方法签名、底层分级算法、时态判断与本地化机制,并给出可直接运行的实战示例。读完本文,你将能掌握DateTime.Humanize()的默认输出规则、替换为自定义策略的方法,以及通过测试数据验证输出结果的具体方式。
类定位:Humanizer 日期人性化的策略模式入口
在 Humanizer 中,"人性化时间距离"(distance of time in words)并不是硬编码在扩展方法里的,而是通过策略模式实现,DefaultDateTimeHumanizeStrategy就是这一体系的默认实现。
public class DefaultDateTimeHumanizeStrategy : Humanizer.DateTimeHumanizeStrategy.IDateTimeHumanizeStrategy类声明要点:
- 命名空间:
Humanizer.DateTimeHumanizeStrategy; - 继承链:
System.Object→DefaultDateTimeHumanizeStrategy; - 实现接口:IDateTimeHumanizeStrategy,该接口只声明了一个方法
string Humanize(DateTime input, DateTime comparisonBase, CultureInfo? culture)。
接口的设计目标(见 IDateTimeHumanizeStrategy.cs)是:允许开发者实现自己的策略,并通过Configurator.DateTimeHumanizeStrategy把它挂接到DateTime.Humanize上。也就是说,DefaultDateTimeHumanizeStrategy只是 Humanizer 提供给开箱即用的"默认"答案,整个计算链路是开放的、可替换的。
策略被挂接的位置在 Configurator.cs,默认值正是本类:
public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; } = new DefaultDateTimeHumanizeStrategy();该属性属于Humanizer.Configuration.Configurator静态类,因此在实际应用中,可以通过一行代码全局替换人性化策略,例如:
Configurator.DateTimeHumanizeStrategy = new PrecisionDateTimeHumanizeStrategy(0.75);Humanize 方法:签名、参数与返回值
文档核心 API 是Humanize(DateTime, DateTime, CultureInfo)方法,官方定义为"计算两个给定日期之间的时间距离(用文字表达)"。
public string Humanize(System.DateTime input, System.DateTime comparisonBase, System.Globalization.CultureInfo culture);参数说明
| 参数 | 类型 | 含义 |
|---|---|---|
input | System.DateTime | 要被人性化描述的日期,即"目标时间点" |
comparisonBase | System.DateTime | 比较基准日期,即"当前时间点" |
culture | System.Globalization.CultureInfo | 用于本地化输出的区域性信息,例如en-US、zh-CN、ru-RU |
返回值
System.String:一段本地化的自然语言描述,例如"one year ago"、"in 2 weeks"。
实现:一行委托给核心算法
DefaultDateTimeHumanizeStrategy的源码极其简洁,它本身不包含任何计算逻辑,而是把工作委托给 DateTimeHumanizeAlgorithms.DefaultHumanize:
public string Humanize(DateTime input, DateTime comparisonBase, CultureInfo? culture) => DateTimeHumanizeAlgorithms.DefaultHumanize(input, comparisonBase, culture);真正的时间距离换算、时态判定、单位取舍,全部发生在DateTimeHumanizeAlgorithms这个静态算法类中。
完整调用链:从扩展方法到最终输出
在实际使用中,开发者通常不会直接调用策略的Humanize,而是通过扩展方法触发。整条调用链如下:
- 入口:DateHumanizeExtensions.cs 中的
DateTime.Humanize()扩展方法:public static string Humanize(this DateTime input, bool? utcDate = null, DateTime? dateToCompareAgainst = null, CultureInfo? culture = null)它负责确定
comparisonBase(默认取DateTime.UtcNow),并根据utcDate参数将基准统一为 UTC 或本地时间,然后调用Configurator.DateTimeHumanizeStrategy.Humanize(input, comparisonBase, culture); - 策略分发:
Configurator.DateTimeHumanizeStrategy当前指向DefaultDateTimeHumanizeStrategy; - 算法执行:
DefaultDateTimeHumanizeStrategy.Humanize委托给DateTimeHumanizeAlgorithms.DefaultHumanize; - 本地化输出:算法内部通过
Configurator.GetFormatter(culture)取得 IFormatter 实例,调用formatter.DateHumanize(TimeUnit, Tense, int)拼出最终文本。
值得注意的是扩展方法还提供了两个便捷重载(见 DateHumanizeExtensions.cs):
DateTime?可空重载:输入为null时返回本地化的 "never"(由formatter.DateHumanize_Never()提供);- 显式传入
dateToCompareAgainst和culture,可在不依赖系统时钟的情况下做确定性测试(测试代码正是这样做的)。
默认算法逐级拆解:阈值、时态与单位选择
DateTimeHumanizeAlgorithms.DefaultHumanize是整篇文章的核心。它的第一步是计算"时态"与"时间跨度":
var tense = input > comparisonBase ? Tense.Future : Tense.Past; var ts = new TimeSpan(Math.Abs(comparisonBase.Ticks - input.Ticks));- 时态:
input晚于comparisonBase判定为Tense.Future(未来),否则为Tense.Past(过去)。两个枚举值的语义见 Tense.cs:Future 输出类似 "in 2 days",Past 输出类似 "2 days ago"; - 时间差:通过
Ticks差取绝对值构造TimeSpan,因此算法只关心"距离"的大小,不关心正负方向; - 月份特殊判定:
sameMonth用于判断input与comparisonBase是否恰好相差一个月(考虑未来/过去方向),这影响 28~30 天区间内的输出归属。
随后进入由小到大、逐级匹配的阈值判断(见 DateTimeHumanizeAlgorithms.cs)。下表汇总了完整阈值与对应输出单位:
| 区间条件 | 输出单位与数量 | 典型输出(en-US,Past) |
|---|---|---|
ts.TotalMilliseconds < 500 | Millisecond,0 | "now" |
ts.TotalSeconds < 60 | Second,ts.Seconds | "10 seconds ago" |
ts.TotalSeconds < 120 | Minute,1 | "a minute ago" |
ts.TotalMinutes < 60 | Minute,ts.Minutes | "44 minutes ago" |
ts.TotalMinutes < 90 | Hour,1 | "an hour ago" |
ts.TotalHours < 24 | Hour,ts.Hours | "10 hours ago" |
ts.TotalHours < 48 | Day,days | "yesterday"(跨日)或 "2 days ago" |
ts.TotalDays < 7 | Day,ts.Days | "6 days ago" |
ts.TotalDays < 28 | Week,ts.Days / 7 | "one week ago"、"2 weeks ago" |
28 ≤ TotalDays < 30 | 若sameMonth为 Month,1,否则 Day | "one month ago" 或按天 |
TotalDays < 345 | Month,floor(TotalDays / 29.5) | "10 months ago" |
| 其余 | Year,floor(TotalDays / 365)(至少 1) | "one year ago" |
几个值得注意的算法细节:
- "一小时"边界:90 分钟以内一律输出 "an hour" 而非 "1 hour",这与英语本地化习惯一致;
- "昨天/明天"判定:
TotalHours < 48分支使用的是days = Math.Abs((input.Date - comparisonBase.Date).Days),即按日历日差值而非按小时数计算,所以跨天 1 天输出 "yesterday"/"tomorrow",而不是 "24 hours ago"; - 28~30 天的月份边界:
sameMonth成立时输出 "one month",否则输出 "N days"。测试 TwentyEightDaysUsesCalendarMonth 专门验证了这一点:从 2023-03-01 往前 28 天输出 "one month ago",从 2023-02-01 往后 28 天输出 "one month from now"; - 年度下限:
TotalDays / 365计算出的years为 0 时强制置 1,避免出现 "0 years"。
最终输出统一调用:
formatter.DateHumanize(TimeUnit.Year, tense, years);其中TimeUnit枚举(Millisecond / Second / Minute / Hour / Day / Week / Month / Year)见 TimeUnit.cs,数量词的复数形式("one second" vs "2 seconds")由各语言 formatter 内部处理。
本地化:culture 如何影响最终文案
culture参数在算法中并不参与数值计算,而是决定选用哪个语言的 formatter。算法内部通过Configurator.GetFormatter(culture)(见 Configurator.cs)从FormatterRegistry解析出对应文化的IFormatter,再由它把(TimeUnit, Tense, count)三元组渲染成本地语言的自然语句。
这一机制可以从测试得到直观印证。CanSpecifyCultureExplicitly 展示了同一组数值在不同文化下的输出:
[InlineData(1, TimeUnit.Year, Tense.Future, "en-US", "one year from now")] [InlineData(40, TimeUnit.Second, Tense.Past, "ru-RU", "40 секунд назад")] [InlineData(2, TimeUnit.Day, Tense.Past, "sv-SE", "för 2 dagar sedan")] [InlineData(2, TimeUnit.Week, Tense.Future, "de-DE", "in 2 Wochen")]如果调用时不传culture(传null),Humanizer 将使用当前线程的CurrentCulture。完整的本地化文案由各语言的.yml语言资源文件维护(见 src/Humanizer/Locales 下的en.yml、ru.yml、de.yml等),并由 SourceGenerator 在编译期生成对应的 Formatter 类型。
与 PrecisionDateTimeHumanizeStrategy 的对比
理解默认策略的最佳参照系是同接口的另一个实现 PrecisionDateTimeHumanizeStrategy:
public class PrecisionDateTimeHumanizeStrategy(double precision = .75) : IDateTimeHumanizeStrategy { public string Humanize(DateTime input, DateTime comparisonBase, CultureInfo? culture) => DateTimeHumanizeAlgorithms.PrecisionHumanize(input, comparisonBase, precision, culture); }两者差异集中在两点:
- 默认策略(本主题):用一系列固定阈值直接"截断"到最合适的单位,简单直观、输出稳定,是
Configurator的默认选择; - 精度策略:接受一个
precision(默认 0.75)参数,在单位进位时做近似舍入(如毫秒数>= 999 * precision才进位到秒),适合需要微调边界行为的场景。
测试辅助类 DateHumanize.cs 会按precision是否传入在这两个策略之间切换,正是两者在测试体系中可互换的证明。在.NET 6+目标框架下,算法类还为DateOnly、TimeOnly提供了对应重载(见 DateTimeHumanizeAlgorithms.cs),行为与DateTime版本保持一致。
实战:使用与预期输出速查
引入命名空间using Humanizer;后即可直接调用:
using Humanizer; var now = DateTime.UtcNow; now.AddSeconds(-10).Humanize(); // "10 seconds ago" now.AddMinutes(45).Humanize(); // "45 minutes from now" now.AddHours(-23).Humanize(); // "23 hours ago" now.AddDays(-1).Humanize(); // "yesterday" now.AddDays(1).Humanize(); // "tomorrow" now.AddDays(-13).Humanize(); // "one week ago" now.AddDays(-32).Humanize(); // "one month ago" now.AddDays(-400).Humanize(); // "one year ago" // 可空日期与显式文化 DateTime? never = null; never.Humanize(); // "never" now.AddMonths(-10).Humanize(culture: new System.Globalization.CultureInfo("ru-RU"));以上英文输出均可在 DateHumanizeDefaultStrategyTests.cs 的 Theory 数据中找到对应断言,例如:
SecondsAgo:60 秒 → "a minute ago"(L4-L10);HoursAgo:24 小时 → "yesterday"(L42-L48);DaysAgo:7/13 天 → "one week ago",32 天 → "one month ago"(L70-L79);MonthsAgo:12 个月 → "one year ago"(L102-L108);Now:0 差值 → "now"(L130-L132)。
总结
DefaultDateTimeHumanizeStrategy是 Humanizer 日期人性化的"标准答案":它实现IDateTimeHumanizeStrategy接口,把全部计算委托给DateTimeHumanizeAlgorithms.DefaultHumanize,通过一套由毫秒到年的分级阈值把TimeSpan距离映射为最合适的TimeUnit,再借助文化相关的IFormatter输出本地化文案。理解它等于理解了DateTime.Humanize()的全部默认行为——包括 "yesterday/tomorrow" 的日历日判定、28~30 天的月份边界特判、一周内的周单位折算,以及通过Configurator.DateTimeHumanizeStrategy替换为自定义或PrecisionDateTimeHumanizeStrategy的扩展路径。若需进一步研究接口契约,可参考 IDateTimeHumanizeStrategy 文档,或直接阅读 DateTimeHumanizeAlgorithms.cs 中的完整实现。
- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
相关推荐
Humanizer 中 DefaultDateTimeOffsetHumanizeStrategy 源码解析:DateTimeOffset 相对时间人性化默认策略
Humanizer 中 DefaultDateTimeOffsetHumanizeStrategy 源码解析:DateTimeOffset 相对时间人性化默认策
开发工具Humanizer 中 DefaultTimeOnlyHumanizeStrategy 源码解读:TimeOnly 相对时间"人性化"的默认策略实现
Humanizer 中 DefaultTimeOnlyHumanizeStrategy 源码解读:TimeOnly 相对时间"人性化"的默认策略实现 本文围绕
开发工具Humanizer 的 DefaultTimeOnlyHumanizeStrategy 详解:TimeOnly 相对时间人文化的默认策略与源码剖析
Humanizer 的 DefaultTimeOnlyHumanizeStrategy 详解:TimeOnly 相对时间人文化的默认策略与源码剖析 导读 本文围
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考