☰
Humanizer 的 DefaultTimeOnlyHumanizeStrategy 深度解析:TimeOnly 相对时间人性化转换原理与实战指南
2026/10/7 2:38:06 网站建设 项目流程
  • 开发工具

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

DefaultTimeOnlyHumanizeStrategy是 Humanizer 中把TimeOnly时刻之间的差值转成自然语言(如 "12 hours from now"、"4 hours ago")的默认策略类。本文以该 API 文档为主体,结合仓库中的源码与测试,完整讲解其方法签名、参数语义、底层分级换算算法、扩展方法调用链、配置替换方式与本地化输出机制,帮助你理解并驾驭 Humanizer 的TimeOnly.Humanize全流程。

类概览:默认的 "distance of time" 计算器

根据 API 文档,该类的定位是 "The default 'distance of time' -> words calculator",即把两个时刻之间的距离换算成词语的默认计算器。其完整声明如下:

public class DefaultTimeOnlyHumanizeStrategy : Humanizer.ITimeOnlyHumanizeStrategy

从继承与实现关系看:

  • 继承自System.Object;
  • 实现了 ITimeOnlyHumanizeStrategy 接口,接口文档的说明明确指出:实现该接口即可为TimeOnly.Humanize创建新策略,并可通过Configurator.TimeOnlyHumanizeStrategy接入配置。

需要特别注意的是,在源码实现中,整个类被#if NET6_0_OR_GREATER条件编译指令包裹。这是因为TimeOnly类型自 .NET 6 才引入,因此该类(以及整个ITimeOnlyHumanizeStrategy接口族)只在 .NET 6 及以上目标框架下可用,使用前请确认项目的目标框架。

该类没有自定义构造函数与状态,是一个极其轻量的策略实现,核心逻辑全部委托给算法层。

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

DefaultTimeOnlyHumanizeStrategy暴露的唯一方法是Humanize,API 文档给出的签名为:

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

各参数语义如下:

参数类型含义
inputTimeOnly要被人性化转换的时刻(目标时刻)
comparisonBaseTimeOnly比较基准时刻,用于计算与input的距离
cultureCultureInfo?输出使用的区域文化,null时使用当前线程的 CultureInfo

返回值是string,即两个时刻之间距离的本地化文字表述。

方法实现了ITimeOnlyHumanizeStrategy.Humanize(TimeOnly, TimeOnly, CultureInfo)接口约定。从源码可以看到,该方法的实现只有一行:

public string Humanize(TimeOnly input, TimeOnly comparisonBase, CultureInfo? culture) => DateTimeHumanizeAlgorithms.DefaultHumanize(input, comparisonBase, culture);

也就是说,DefaultTimeOnlyHumanizeStrategy本身是"薄壳",真正的算法在 DateTimeHumanizeAlgorithms 的DefaultHumanize(TimeOnly, TimeOnly, CultureInfo)方法中。

底层算法:DefaultHumanize 的分级换算原理

要真正理解这个策略,必须深入 DateTimeHumanizeAlgorithms.cs 中针对TimeOnly的重载:

public static string DefaultHumanize(TimeOnly input, TimeOnly comparisonBase, CultureInfo? culture) { var tense = input > comparisonBase ? Tense.Future : Tense.Past; var ts = new TimeSpan(Math.Abs(comparisonBase.Ticks - input.Ticks)); return DefaultHumanize(ts, true, 0, tense, culture); }

这里有两个关键步骤:

  1. 时态(Tense)判定:通过input > comparisonBase判断目标时刻是否晚于基准时刻。晚于则标记为Tense.Future(输出形如 "in 2 hours" 或英文的 "2 hours from now"),否则标记为Tense.Past(输出形如 "2 hours ago")。Tense枚举定义在 Tense.cs,包含Future与Past两个成员。

  2. 时间差计算:用Math.Abs(comparisonBase.Ticks - input.Ticks)取两个TimeOnly的 Ticks 差绝对值,构造一个不带日期部分的TimeSpan。注意这里对TimeOnly的差异只取绝对值,不跨日期累计(不会因为 "昨天 23:00 到今天 01:00" 就得到 2 天),并且sameMonth参数固定传true、days固定传0,这正是TimeOnly语义(纯时刻、无日期)在算法中的体现。

随后进入核心的 DefaultHumanize(TimeSpan, bool, int, Tense, CultureInfo) 私有重载,它像一个"阈值阶梯",从最小的单位开始逐级放大判断,直到落入第一个满足条件的区间。整个判断链如下:

判断条件(按顺序)触发单位输出示例(en-US,Past 时态)
TotalMilliseconds < 500Millisecond,数量 0"now"
TotalSeconds < 60Second,数量ts.Seconds"x seconds ago"
TotalSeconds < 120Minute,数量 1"a minute ago"
TotalMinutes < 60Minute,数量ts.Minutes"x minutes ago"
TotalMinutes < 90Hour,数量 1"an hour ago"
TotalHours < 24Hour,数量ts.Hours"x hours ago"
TotalHours < 48Day,数量days"yesterday" 一类表述
TotalDays < 7Day,数量ts.Days"x days ago"
TotalDays < 28Week,数量ts.Days / 7"x weeks ago"
TotalDays ∈ [28, 30)Month(同月)或 Day"a month ago" / "x days ago"
TotalDays < 345Month,数量Floor(TotalDays / 29.5)"x months ago"
其余情况Year,数量Floor(TotalDays / 365)(最小为 1)"x years ago"

几个值得注意的实现细节:

  • 最小输出为 "now":当差值小于 500 毫秒时,直接走TimeUnit.Millisecond且数量为 0 的分支,经格式化器输出 "now"(见 DefaultFormatter.cs 的DateHumanize_Now与DateHumanize_Today的短语表回退逻辑);
  • "约一"的模糊处理:TotalSeconds < 120输出 "1 minute"、TotalMinutes < 90输出 "1 hour",这是借鉴 Stack Overflow 经典相对时间算法的近似策略,源码注释中也保留了该出处链接;
  • 月份换算系数 29.5:TotalDays < 345时按月均 29.5 天取整折算月份,是纯时刻场景下对"月"的一种近似处理。

最终,算法调用Configurator.GetFormatter(culture)获取对应文化的格式化器,再按TimeUnit(枚举定义见 TimeUnit.cs,含 Millisecond 到 Year 共 8 个成员)、Tense与数量unit调用formatter.DateHumanize(...)完成短语渲染与本地化。

扩展方法入口:TimeOnly.Humanize 的完整调用链

DefaultTimeOnlyHumanizeStrategy平时并不会被直接调用,而是由扩展方法间接触发。在 DateHumanizeExtensions.cs 中定义了TimeOnly.Humanize入口:

public static string Humanize(this TimeOnly input, TimeOnly? timeToCompareAgainst = null, bool useUtc = true, CultureInfo? culture = null) { var comparisonBase = timeToCompareAgainst ?? TimeOnly.FromDateTime(useUtc ? DateTime.UtcNow : DateTime.Now); return Configurator.TimeOnlyHumanizeStrategy.Humanize(input, comparisonBase, culture); }

该扩展方法的关键点:

  • timeToCompareAgainst:比较基准,null时默认取当前时刻;
  • useUtc:当基准为null时,决定取 UTC 当前时刻(默认true)还是本地时刻(false),通过TimeOnly.FromDateTime从DateTime提取纯时刻部分;
  • culture:为null时使用当前线程文化;
  • 真正执行时读取的是Configurator.TimeOnlyHumanizeStrategy属性,而该属性默认值正是new DefaultTimeOnlyHumanizeStrategy()(见下文配置一节)。

此外,还有一个可空版本的重载 Humanize(this TimeOnly?):当TimeOnly?为null时不会抛异常,而是返回本地化的 "never"(通过formatter.DateHumanize_Never()实现)。

ITimeOnlyHumanizeStrategy接口本身定义在 ITimeOnlyHumanizeStrategy.cs,签名与文档一致,是实现自定义策略时必须遵循的契约。

策略可插拔:Configurator 与 Precision 策略对比

DefaultTimeOnlyHumanizeStrategy只是默认实现,Humanizer 通过 Configurator.cs 暴露了可替换的配置点:

public static ITimeOnlyHumanizeStrategy TimeOnlyHumanizeStrategy { get; set; } = new DefaultTimeOnlyHumanizeStrategy();

Configurator.TimeOnlyHumanizeStrategy属性默认被初始化为DefaultTimeOnlyHumanizeStrategy,你可以在应用启动阶段替换成任意自定义的ITimeOnlyHumanizeStrategy实现。Configurator的 XML 注释明确提示:该属性应在应用启动时只设置一次,多线程场景下访问需注意线程安全(使用 volatile 读取或合适的同步机制),生产环境不应在服务运行后再变更。

仓库内置的另一套策略是 PrecisionTimeOnlyHumanizeStrategy,它与默认策略的核心区别在于:

对比项DefaultTimeOnlyHumanizeStrategyPrecisionTimeOnlyHumanizeStrategy
算法入口DefaultHumanize(固定阈值阶梯)PrecisionHumanize(基于精度参数的近似换算)
构造参数无double precision = .75,默认精度 0.75
换算方式按毫秒/秒/分/时/日的硬编码阈值分级用precision参与各单位的进位判断(如seconds >= 59 * precision则分钟进位)
适用场景大多数场景的默认选择需要控制"近似程度"的精度敏感场景

精度策略的源码(PrecisionHumanize分支)位于 DateTimeHumanizeAlgorithms.cs,其先计算TimeSpan差值与时态,再进入带精度的单位换算逻辑。API 文档中PrecisionTimeOnlyHumanizeStrategy的说明同样可以在 Humanizer.PrecisionTimeOnlyHumanizeStrategy.md 查到。

测试验证:行为示例与边界保障

仓库测试文件 TimeOnlyHumanizeTests.cs(使用en-US文化)直接验证了默认策略的行为,可作为理解输出的权威参考:

// 同一时刻 -> "now" var inputTime = new TimeOnly(13, 07, 05); var baseTime = new TimeOnly(13, 07, 05); inputTime.Humanize(baseTime); // => "now" // 未来方向,12 小时差 -> "12 hours from now" new TimeOnly(13, 08, 05).Humanize(new TimeOnly(1, 08, 05)); // => "12 hours from now" // 过去方向,4 小时差 -> "4 hours ago" new TimeOnly(13, 07, 02).Humanize(new TimeOnly(17, 07, 05)); // => "4 hours ago"

测试覆盖的关键点还包括:

  • 文化敏感性:Humanize_UsesSpecifiedCulture理论测试遍历所有已发布 locale,验证culture参数生效——输出由Configurator.GetFormatter(culture).DateHumanize(TimeUnit.Minute, Tense.Future, 1)产生;
  • 可空语义:null的TimeOnly?调用Humanize()返回 "never",非空时与直接调用TimeOnly.Humanize()结果一致;
  • 多策略并行隔离:StrategiesAreIsolatedAcrossParallelCultures测试在同一时刻用不同线程、不同文化(en-US/fr、fr/is)并发执行默认策略与精度策略(PrecisionTimeOnlyHumanizeStrategy(0.5)),验证不同策略在不同文化下互不串扰(分别产出 "12 hours from now" 与 "demain")。

本地化输出:从数值到短语的最后一公里

DefaultTimeOnlyHumanizeStrategy计算出的只是"单位 + 数量 + 时态"这些中间结果,最终的人类可读短语由格式化器渲染。在 DefaultFormatter.cs 中:

public virtual string DateHumanize(TimeUnit timeUnit, Tense timeUnitTense, int unit) => TryFormatDateFromPhraseTable(timeUnit, timeUnitTense, unit, out var result) ? result : throw new InvalidOperationException(...);

DefaultFormatter通过LocalePhraseTable(由仓库中的 yml 语言资源与 SourceGenerator 生成的短语表驱动)查找对应文化、单位、时态的单数/复数形式,并用{count}、{prep}等占位符模板完成拼装(见 DefaultFormatter.cs 的TryFormatDateFromPhraseTable与RenderCountedPhrase)。这正是"12 hours from now" 在 en-US、而其他语言会输出各自语法结构的原因。

实战小结:何时使用、如何扩展

综合以上分析,使用DefaultTimeOnlyHumanizeStrategy的实战要点如下:

  1. 直接使用:大多数情况下无需显式引用该类,直接调用someTime.Humanize(comparisonBase, culture: ...)即可,内部默认走该策略;
  2. 显式调用:需要绕过扩展方法、直接获取策略时,可new DefaultTimeOnlyHumanizeStrategy().Humanize(input, comparisonBase, culture);
  3. 自定义策略:实现ITimeOnlyHumanizeStrategy并在应用启动时赋值给Configurator.TimeOnlyHumanizeStrategy,即可替换默认行为;需要近似精度控制时,可直接选用PrecisionTimeOnlyHumanizeStrategy(precision);
  4. 框架前提:由于依赖TimeOnly,全部相关类型仅在NET6_0_OR_GREATER下编译可用;
  5. 文化输出:始终记得culture参数为null时采用当前线程文化,跨区域部署时建议显式传入目标CultureInfo以保证输出一致。

通过本篇文章,你已完整掌握DefaultTimeOnlyHumanizeStrategy从接口契约、方法签名到阈值算法、调用链、配置替换与本地化渲染的整个闭环,可以直接在项目中使用或扩展 Humanizer 的TimeOnly.Humanize能力。

  • 开发工具

【免费下载链接】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
点击查看免费下载
上一篇:淘金币自动化脚本:10 分钟 5 步配好,每天省下 20 分钟
下一篇:一个 ini 文件调双风扇:TPFanCtrl2 完整上手指南

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

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

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

立即咨询