- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
本篇文章以 Humanizer 开源库(.NET 字符串、日期、时间、数字人性化处理库)3.0.10 版本文档中的DefaultDateTimeOffsetHumanizeStrategy类为核心,系统讲解该类在“将两个时刻之间的距离转成自然语言”中的定位、调用链与算法原理。读完本文,你将掌握DateTimeOffset.Humanize()的完整工作流程、默认策略的分级阈值规则、UTC 时区偏移处理机制、文化本地化机制,以及如何通过Configurator切换到精度策略实现自定义相对时间表达。
一、类概述:默认的“时间距离 → 文字”计算器
DefaultDateTimeOffsetHumanizeStrategy是 Humanizer 为DateTimeOffset类型提供的默认距离时间(distance of time)转文字计算器。在 API 文档 中,其类定义如下:
public class DefaultDateTimeOffsetHumanizeStrategy : Humanizer.IDateTimeOffsetHumanizeStrategy从类声明可以看出:
- 继承关系:
System.Object→DefaultDateTimeOffsetHumanizeStrategy(没有基类,直接继承自Object); - 接口实现:实现了 IDateTimeOffsetHumanizeStrategy 接口,这意味着它可被替换、可被注入到配置系统。
对应源码位于 src/Humanizer/DateTimeHumanizeStrategy/DefaultDateTimeOffsetHumanizeStrategy.cs,全文非常精简,核心逻辑全部委托给内部静态算法类:
namespace Humanizer; public class DefaultDateTimeOffsetHumanizeStrategy : IDateTimeOffsetHumanizeStrategy { public string Humanize(DateTimeOffset input, DateTimeOffset comparisonBase, CultureInfo? culture) => DateTimeHumanizeAlgorithms.DefaultHumanize(input.UtcDateTime, comparisonBase.UtcDateTime, culture); }为什么 DateTimeOffset 需要独立策略?
DateTimeOffset与DateTime最大的区别在于它携带时区偏移(offset),同一时刻在不同时区下具有不同的本地钟面时间。Humanizer 为此设计了两个独立策略接口与两个默认实现:
| 输入类型 | 策略接口 | 默认实现 |
|---|---|---|
DateTime | IDateTimeHumanizeStrategy | DefaultDateTimeHumanizeStrategy |
DateTimeOffset | IDateTimeOffsetHumanizeStrategy | DefaultDateTimeOffsetHumanizeStrategy |
从 DateTimeHumanizeAlgorithms.cs 可以看出,DateTime版与DateTimeOffset版最终都汇聚到同一个DefaultHumanize算法,区别在于DateTimeOffset 版本会先把两个时刻统一转换为 UTC 再进行差值计算(调用input.UtcDateTime/comparisonBase.UtcDateTime),从而保证“不同时区偏移下的同一绝对时刻”能够被正确比较。这正是该策略存在的最重要理由。
二、Humanize 方法签名与参数语义
文档为DefaultDateTimeOffsetHumanizeStrategy定义了唯一的公开方法:
public string Humanize(System.DateTimeOffset input, System.DateTimeOffset comparisonBase, System.Globalization.CultureInfo? culture);参数说明
| 参数 | 类型 | 语义 |
|---|---|---|
input | DateTimeOffset | 要被“人性化”的目标时刻(待描述的那个时间点) |
comparisonBase | DateTimeOffset | 比较基准时刻(参照系,通常是“现在”) |
culture | CultureInfo? | 输出文案使用的文化;传null时使用当前线程的CultureInfo |
该方法实现 IDateTimeOffsetHumanizeStrategy.Humanize(DateTimeOffset, DateTimeOffset, CultureInfo),返回值类型为System.String,即形如"an hour from now"、"30 minutes ago"、"one week from now"这样的自然语言句子。
扩展方法入口:调用方视角
在实际业务代码中,开发者几乎不会直接调用策略类的Humanize,而是通过 DateHumanizeExtensions.cs 提供的扩展方法触发:
public static string Humanize(this DateTimeOffset input, DateTimeOffset? dateToCompareAgainst = null, CultureInfo? culture = null) { var comparisonBase = dateToCompareAgainst ?? DateTimeOffset.UtcNow; return Configurator.DateTimeOffsetHumanizeStrategy.Humanize(input, comparisonBase, culture); }关键行为:
- 不传
dateToCompareAgainst时,基准时刻默认为DateTimeOffset.UtcNow; - 最终调用
Configurator.DateTimeOffsetHumanizeStrategy完成人性化; - 同时提供
DateTimeOffset?可空重载(见 DateHumanizeExtensions.cs),当值为null时直接返回本地化文案"never"(由IFormatter.DateHumanize_Never()生成)。
三、默认策略的算法原理:从毫秒到年的分级阈值
DefaultDateTimeOffsetHumanizeStrategy的核心逻辑位于 DateTimeHumanizeAlgorithms.DefaultHumanize,它先计算出两个时刻之间的TimeSpan与“时态”(将来/过去),再交给内部的DefaultHumanize(TimeSpan ts, bool sameMonth, int days, Tense tense, CultureInfo? culture)进行逐级判断。
public static string DefaultHumanize(DateTime input, DateTime comparisonBase, CultureInfo? culture) { 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); return DefaultHumanize(ts, sameMonth, days, tense, culture); }时态由 Tense 枚举 表示:Future(将来,如 "in 2 days")与Past(过去,如 "2 days ago");时间单位由 TimeUnit 枚举 覆盖:Millisecond、Second、Minute、Hour、Day、Week、Month、Year。
分级判断完整阈值表
内部私有方法(DateTimeHumanizeAlgorithms.cs)按照从“小单位”到“大单位”的顺序逐级匹配:
| 条件(时间跨度 ts) | 输出单位 | 输出数值 |
|---|---|---|
TotalMilliseconds < 500 | Millisecond | 0(即 "now" 一类文案) |
TotalSeconds < 60 | Second | ts.Seconds |
TotalSeconds < 120 | Minute | 1 |
TotalMinutes < 60 | Minute | ts.Minutes |
TotalMinutes < 90 | Hour | 1 |
TotalHours < 24 | Hour | ts.Hours |
TotalHours < 48 | Day | days |
TotalDays < 7 | Day | ts.Days |
TotalDays < 28 | Week | ts.Days / 7 |
28 <= TotalDays < 30 | Month 或 Day | 同月则 Month=1,否则 Day=ts.Days |
TotalDays < 345 | Month | Floor(TotalDays / 29.5) |
| 其余 | Year | Floor(TotalDays / 365)(最小为 1) |
其中sameMonth判定用于处理 28~30 天边界:若两个日期刚好相差一个月历月(例如今天与上个月的今天),优先输出 “one month”,否则仍按天数输出。年份计算则保证不足一年的跨度(如 300 天)不会错误地归零,最小输出 1 年。
本地化文案的最终生成
算法本身只负责“算出单位 + 数值 + 时态”,具体文字由Configurator.GetFormatter(culture)解析出的IFormatter完成:
var formatter = Configurator.GetFormatter(culture); if (ts.TotalSeconds < 60) { return formatter.DateHumanize(TimeUnit.Second, tense, ts.Seconds); }IFormatter会根据culture从 FormatterRegistry 解析出对应语言的格式化器,从而输出不同语言的相对时间短语。这也是 Humanizer 支持 100+ 语言本地化的关键一环。
四、测试验证:不同时区偏移与边界行为
仓库在 tests/Humanizer.Tests/DateTimeOffsetHumanizeTests.cs 中为该策略提供了详尽的测试用例,可直接作为行为契约理解:
相同偏移下的时距计算
var inputTime = new DateTimeOffset(2015, 07, 05, 04, 0, 0, TimeSpan.Zero); var baseTime = new DateTimeOffset(2015, 07, 05, 03, 0, 0, TimeSpan.Zero); Assert.Equal("an hour from now", inputTime.Humanize(baseTime));不同偏移下的正确换算(UTC 归一化)
var inputTime = new DateTimeOffset(2015, 07, 05, 03, 0, 0, new(2, 0, 0)); // UTC+2 的 03:00 var baseTime = new DateTimeOffset(2015, 07, 05, 02, 30, 0, new(1, 0, 0)); // UTC+1 的 02:30 Assert.Equal("30 minutes ago", inputTime.Humanize(baseTime));两个时刻的本地钟面时间相差 30 分钟,但换算成 UTC 后分别对应 01:00 与 01:30,差值仍是 30 分钟,输出 "30 minutes ago"。这个用例直接证明了策略内部先做 UTC 归一化的必要性。
跨时区周数计算
var baseTime = new DateTimeOffset(2024, 01, 01, 10, 0, 0, TimeSpan.FromHours(2)); var inputTime = new DateTimeOffset(2024, 01, 08, 03, 0, 0, TimeSpan.FromHours(-5)); Assert.Equal("one week from now", inputTime.Humanize(baseTime));可空重载与“never”文案
DateTimeOffset? never = null; Assert.Equal("never", never.Humanize());本地化输出验证
测试还通过LocaleCoverageData数据驱动,验证了在不同CultureInfo下AddDays(-1).Humanize(baseTime, culture)会输出对应语言的 “yesterday” 类文案,证实culture参数贯穿整个调用链。
五、配置与替换:如何切换为精度策略
DefaultDateTimeOffsetHumanizeStrategy之所以被设计为实现接口的可替换类,是因为 Humanizer 允许通过 Configurator 全局替换策略:
public static IDateTimeOffsetHumanizeStrategy DateTimeOffsetHumanizeStrategy { get; set; } = new DefaultDateTimeOffsetHumanizeStrategy();- 默认值即
DefaultDateTimeOffsetHumanizeStrategy实例; - 该属性应在应用启动时设置一次。源码注释明确建议:在服务开始处理请求前完成配置,多线程场景下注意同步(volatile 读取或合适的加锁),生产应用中避免运行时热切换。
精度策略:PrecisionDateTimeOffsetHumanizeStrategy
仓库同时提供了另一种内置实现 PrecisionDateTimeOffsetHumanizeStrategy:
public class PrecisionDateTimeOffsetHumanizeStrategy(double precision = .75) : IDateTimeOffsetHumanizeStrategy { public string Humanize(DateTimeOffset input, DateTimeOffset comparisonBase, CultureInfo? culture) => DateTimeHumanizeAlgorithms.PrecisionHumanize(input.UtcDateTime, comparisonBase.UtcDateTime, precision, culture); }它通过构造函数传入“逼近精度”precision(默认0.75),在 PrecisionHumanize 中采用“进位式”近似:例如当秒数达到59 * precision时进位为 1 分钟,天数达到23 * precision时进位为 1 天,并利用30 * precision、365 * precision作为月/年边界。典型用法:
Configurator.DateTimeOffsetHumanizeStrategy = new PrecisionDateTimeOffsetHumanizeStrategy(0.75);对应测试(DateTimeOffsetHumanizeTests.cs)展示了精度策略在跨偏移场景下的输出:
var inputTime = new DateTimeOffset(2015, 07, 05, 03, 45, 0, new(2, 0, 0)); // UTC+2 var baseTime = new DateTimeOffset(2015, 07, 05, 02, 30, 0, new(-5, 0, 0)); // UTC-5 Assert.Equal("6 hours ago", inputTime.Humanize(baseTime));从源码结构看,两套策略共用DateTimeHumanizeAlgorithms的两种算法入口,接口设计让自定义策略的接入成本极低——只需实现IDateTimeOffsetHumanizeStrategy并赋值给Configurator.DateTimeOffsetHumanizeStrategy即可。
六、与其他 Humanize 策略的关系(快速定位)
为便于读者在仓库中快速定位相关类型,这里给出策略家族总览:
| 类型 | 源码位置 | 用途 |
|---|---|---|
IDateTimeOffsetHumanizeStrategy | IDateTimeOffsetHumanizeStrategy.cs | DateTimeOffset.Humanize的策略契约 |
DefaultDateTimeOffsetHumanizeStrategy | DefaultDateTimeOffsetHumanizeStrategy.cs | 默认实现,分级阈值算法 |
PrecisionDateTimeOffsetHumanizeStrategy | PrecisionDateTimeOffsetHumanizeStrategy.cs | 精度近似实现 |
DefaultDateTimeHumanizeStrategy | DefaultDateTimeHumanizeStrategy.cs | DateTime.Humanize的默认实现 |
DateTimeHumanizeAlgorithms | DateTimeHumanizeAlgorithms.cs | 两种算法的内部实现(默认/精度) |
DateHumanizeExtensions | DateHumanizeExtensions.cs | 对外暴露.Humanize()扩展方法 |
七、快速上手示例
将以下代码放入 .NET 项目中(引用 Humanizer 包),即可体验默认策略:
using Humanizer; // 1) 相对当前时刻(默认基准为 DateTimeOffset.UtcNow) var created = DateTimeOffset.UtcNow.AddHours(-3); Console.WriteLine(created.Humanize()); // 输出类似 "3 hours ago" // 2) 指定基准时刻与语言文化 var input = new DateTimeOffset(2015, 7, 5, 4, 0, 0, TimeSpan.Zero); var baseTime = new DateTimeOffset(2015, 7, 5, 3, 0, 0, TimeSpan.Zero); Console.WriteLine(input.Humanize(baseTime)); // "an hour from now" // 3) 可空重载:null 输出 "never" DateTimeOffset? maybe = null; Console.WriteLine(maybe.Humanize()); // "never" // 4) 切换到精度策略(在应用启动时配置一次) Configurator.DateTimeOffsetHumanizeStrategy = new PrecisionDateTimeOffsetHumanizeStrategy(0.75);结语
DefaultDateTimeOffsetHumanizeStrategy虽然源码体量极小,却是 Humanizer 处理DateTimeOffset相对时间表达的核心枢纽:它通过统一 UTC 归一化解决时区偏移比较问题,借助 DateTimeHumanizeAlgorithms 的分级阈值表将任意时间跨度映射到最自然的时间单位,再交由IFormatter输出本地化文案。理解这一默认策略,是深入掌握 Humanizer 日期人性化机制、乃至自定义相对时间策略的最佳切入点。
- 开发工具
【免费下载链接】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 DefaultDateTimeOffsetHumanizeStrategy 源码解析:DateTimeOffset 相对时间转自然语言的核心策略
Humanizer DefaultDateTimeOffsetHumanizeStrategy 源码解析:DateTimeOffset 相对时间转自然语言的核心
开发工具Humanizer 时间人性化策略详解:DefaultDateTimeOffsetHumanizeStrategy 如何把 DateTimeOffset 距离转成本地化文字
Humanizer 时间人性化策略详解:DefaultDateTimeOffsetHumanizeStrategy 如何把 DateTimeOffset 距离转
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考