- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
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);各参数语义如下:
| 参数 | 类型 | 含义 |
|---|---|---|
input | TimeOnly | 要被人性化转换的时刻(目标时刻) |
comparisonBase | TimeOnly | 比较基准时刻,用于计算与input的距离 |
culture | CultureInfo? | 输出使用的区域文化,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); }这里有两个关键步骤:
时态(Tense)判定:通过
input > comparisonBase判断目标时刻是否晚于基准时刻。晚于则标记为Tense.Future(输出形如 "in 2 hours" 或英文的 "2 hours from now"),否则标记为Tense.Past(输出形如 "2 hours ago")。Tense枚举定义在 Tense.cs,包含Future与Past两个成员。时间差计算:用
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 < 500 | Millisecond,数量 0 | "now" |
TotalSeconds < 60 | Second,数量ts.Seconds | "x seconds ago" |
TotalSeconds < 120 | Minute,数量 1 | "a minute ago" |
TotalMinutes < 60 | Minute,数量ts.Minutes | "x minutes ago" |
TotalMinutes < 90 | Hour,数量 1 | "an hour ago" |
TotalHours < 24 | Hour,数量ts.Hours | "x hours ago" |
TotalHours < 48 | Day,数量days | "yesterday" 一类表述 |
TotalDays < 7 | Day,数量ts.Days | "x days ago" |
TotalDays < 28 | Week,数量ts.Days / 7 | "x weeks ago" |
TotalDays ∈ [28, 30) | Month(同月)或 Day | "a month ago" / "x days ago" |
TotalDays < 345 | Month,数量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,它与默认策略的核心区别在于:
| 对比项 | DefaultTimeOnlyHumanizeStrategy | PrecisionTimeOnlyHumanizeStrategy |
|---|---|---|
| 算法入口 | 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的实战要点如下:
- 直接使用:大多数情况下无需显式引用该类,直接调用
someTime.Humanize(comparisonBase, culture: ...)即可,内部默认走该策略; - 显式调用:需要绕过扩展方法、直接获取策略时,可
new DefaultTimeOnlyHumanizeStrategy().Humanize(input, comparisonBase, culture); - 自定义策略:实现
ITimeOnlyHumanizeStrategy并在应用启动时赋值给Configurator.TimeOnlyHumanizeStrategy,即可替换默认行为;需要近似精度控制时,可直接选用PrecisionTimeOnlyHumanizeStrategy(precision); - 框架前提:由于依赖
TimeOnly,全部相关类型仅在NET6_0_OR_GREATER下编译可用; - 文化输出:始终记得
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
相关推荐
Humanizer 中 DefaultTimeOnlyHumanizeStrategy 源码解读:TimeOnly 相对时间"人性化"的默认策略实现
Humanizer 中 DefaultTimeOnlyHumanizeStrategy 源码解读:TimeOnly 相对时间"人性化"的默认策略实现 本文围绕
开发工具Humanizer 的 DefaultTimeOnlyHumanizeStrategy 详解:TimeOnly 相对时间人文化的默认策略与源码剖析
Humanizer 的 DefaultTimeOnlyHumanizeStrategy 详解:TimeOnly 相对时间人文化的默认策略与源码剖析 导读 本文围
开发工具Humanizer 中的 DefaultTimeOnlyHumanizeStrategy 详解:TimeOnly 时间差人性化措辞的默认计算策略
Humanizer 中的 DefaultTimeOnlyHumanizeStrategy 详解:TimeOnly 时间差人性化措辞的默认计算策略 Default
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考