- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
本篇技术指南围绕 Humanizer 2.14.1 文档中的InDate.Six类展开,讲解如何用一行静态属性或方法,直接拿到“6 天后”“6 周后”“6 个月后”“6 年后”的DateOnly日期。读完本文,你将掌握 Humanizer FluentDate 相对日期 API 的属性/方法语义、DateOnly与DateTime两种重载的取舍、其 T4 模板生成机制与测试验证方式,并能在 .NET 6+ 项目中直接落地使用。
InDate.Six 是什么:FluentDate 相对日期体系中的一员
Humanizer 的 FluentDate 模块把"未来某个日期"的表述压缩成了类名与成员名本身。它围绕四个根类型展开:
In:返回DateTime的“未来相对时间/月份”API;On:基于DateTime的“某月某日”API;InDate:返回DateOnly的“未来相对日期/月份”API(即本文主角);OnDate:基于DateOnly的“某月某日”API。
InDate是一个用partial拆分的类(src/Humanizer/FluentDate/InDate.cs),内部嵌入了One到Ten十个静态嵌套类,分别对应数量 1 到 10。InDate.Six就是其中数量为 6 的那一个:文档 website/versioned_docs/version-2.14.1/api/Humanizer.InDate.Six.md 显示它继承自System.Object,是一个public static class,全部成员均为public static,无需实例化即可调用。
注意:整个
InDate系列(包括InDate.Six)在源码中被#if NET6_0_OR_GREATER条件编译保护(见 src/Humanizer/FluentDate/InDate.SomeTimeFrom.cs),因为DateOnly是 .NET 6 引入的类型。低于 .NET 6 的目标框架中不存在这套 API。
四个核心属性:从“现在”起算的相对日期
InDate.Six提供了 4 个静态属性,语义都是“从现在起 N 个单位后的日期”,返回值统一为System.DateOnly:
| 属性 | 说明 | 签名 |
|---|---|---|
Days | 6 天后的日期 | public static System.DateOnly Days { get; } |
Weeks | 6 周后的日期 | public static System.DateOnly Weeks { get; } |
Months | 6 个月后的日期 | public static System.DateOnly Months { get; } |
Years | 6 年后的日期 | public static System.DateOnly Years { get; } |
对应源码位于 src/Humanizer/FluentDate/InDate.SomeTimeFrom.cs 的Six类中,实现方式高度一致,例如:
public static DateOnly Days => DateOnly.FromDateTime(DateTime.UtcNow.AddDays(6)); public static DateOnly Weeks => DateOnly.FromDateTime(DateTime.UtcNow.AddDays(42)); // 6 * 7 public static DateOnly Months => DateOnly.FromDateTime(DateTime.UtcNow.AddMonths(6)); public static DateOnly Years => DateOnly.FromDateTime(DateTime.UtcNow.AddYears(6));有三个容易被忽略的实现细节值得注意:
- 基准时间永远是 UTC:属性基于
DateTime.UtcNow而非DateTime.Now计算,然后通过DateOnly.FromDateTime截掉时间部分,只保留年月日。因此“6 天后”不会受本地时区设置影响,测试可复现性更好。 - 周是天的整数倍:
Weeks没有独立的周类型概念,直接等价于AddDays(42)(6 周 × 7 天)。 - 月/年遵循 .NET 日历算法:
AddMonths(6)、AddYears(6)由DateOnly自身处理大小月与闰年,例如 8 月 31 日加 6 个月会得到次年 2 月的月末(28/29 日),而不是抛异常。
八个 From 方法:从指定日期起算
如果不想基于“现在”,而是基于某个确定的基准日期,InDate.Six还提供了 4 组共 8 个静态方法,每个单位都有DateOnly与DateTime两种参数重载:
| 方法 | 参数类型 | 返回类型 |
|---|---|---|
DaysFrom(DateOnly date) | DateOnly | DateOnly |
DaysFrom(DateTime date) | DateTime | DateOnly |
WeeksFrom(DateOnly date) | DateOnly | DateOnly |
WeeksFrom(DateTime date) | DateTime | DateOnly |
MonthsFrom(DateOnly date) | DateOnly | DateOnly |
MonthsFrom(DateTime date) | DateTime | DateOnly |
YearsFrom(DateOnly date) | DateOnly | DateOnly |
YearsFrom(DateTime date) | DateTime | DateOnly |
源码实现同样直观(src/Humanizer/FluentDate/InDate.SomeTimeFrom.cs):
public static DateOnly DaysFrom(DateOnly date) => date.AddDays(6); public static DateOnly DaysFrom(DateTime date) => DateOnly.FromDateTime(date.AddDays(6)); public static DateOnly MonthsFrom(DateOnly date) => date.AddMonths(6); public static DateOnly MonthsFrom(DateTime date) => DateOnly.FromDateTime(date.AddMonths(6));两组重载的差别在于:DateOnly版本直接在纯日期上做加法,天然无时间分量;DateTime版本则保留输入的时间部分参与运算(例如从某天 14:30 起算 6 天),最终仍通过DateOnly.FromDateTime统一收敛为纯日期返回。无论入参类型如何,所有 From 方法的返回值都固定是DateOnly。
源码视角:T4 模板生成十个兄弟类
InDate.Six不是手写代码,而是由 T4 文本模板批量生成的。模板位于 src/Humanizer/FluentDate/InDate.SomeTimeFrom.tt,核心逻辑是一个循环:
<#for (var i = 1; i <= 10; i++){ var plural = i > 1 ? "s" : ""; // 单复数处理 var day = "Day" + plural; // Day / Days var week = "Week" + plural; // Week / Weeks var month = "Month" + plural; // Month / Months var year = "Year" + plural; // Year / Years #> public static class <#= i.ToWords().Dehumanize() #>从中可以确认三个设计约定:
- 类名由
i.ToWords().Dehumanize()生成:数字先转英文单词(6→six),再反人类化处理(six→Six),因此得到One、Two……Ten; - 属性名按数量自动单复数:数量 1 时是单数
Day/Week/Month/Year(见InDate.One),数量 2~10 时是复数Days/Weeks/Months/Years,与文档中InDate.Six.Days等成员一一对应; - 生成器把
#if NET6_0_OR_GREATER条件编译直接写进模板输出,因此全部生成代码只对 .NET 6+ 生效。
这也解释了为什么文档目录下会同时存在Humanizer.InDate.One.md到Humanizer.InDate.Ten.md十个几乎同构的 API 页面:它们描述的是同一模板产出的十组平行类,本文讨论的InDate.Six只是数量为 6 的那一组。
测试如何保证这些 API 的正确性
仓库对 FluentDate 生成 API 的验证方式是反射遍历:测试不针对单个成员写死断言,而是枚举所有嵌套类与成员,再按类名反推数量、按成员名反推单位,统一断言。相关测试在 tests/Humanizer.Tests/FluentDate/GeneratedFluentDateTests.cs:
InDateRelativeDatePropertiesReturnExpectedUtcOffsets:对InDate所有相对日期属性(含Six.Days/Weeks/Months/Years)取值,断言结果落在DateTime.UtcNow加对应数量之后的时间区间内(测试第 L34-L35、L99-L115);InDateRelativeDateOnlyMethodsReturnExpectedOffsetsFromProvidedDate:用固定的基准日期2024-02-29(闰日,专门检验月/年加法边界)调用所有DateOnly参数的重载,逐项断言Assert.Equal(测试第 L38-L39、L117-L138);InDateRelativeDateTimeMethodsReturnExpectedOffsetsFromProvidedDateTime:同样的逻辑覆盖所有DateTime参数重载。
除此之外,tests/Humanizer.Tests/FluentDate/InDateTests.cs 给出了一个非常贴近实际用法的示例:用OnDate.January.The21st构造基准日,再交给InDate.Five.DaysFrom(baseDate),断言结果等于baseDate.AddDays(5)。把Five换成Six即可得到“6 天后”的等效验证,二者行为完全对称。
在项目中的完整使用示例
引入 Humanizer 命名空间后,InDate.Six的用法非常直观:
using Humanizer; // 属性:从"现在"起算 DateOnly inSixDays = InDate.Six.Days; // 今天 + 6 天 DateOnly inSixWeeks = InDate.Six.Weeks; // 今天 + 42 天 DateOnly inSixMonths = InDate.Six.Months; // 今天 + 6 个月 DateOnly inSixYears = InDate.Six.Years; // 今天 + 6 年 // 方法:从指定基准日起算(DateOnly 重载) DateOnly baseDate = new(2026, 9, 26); DateOnly dueDate = InDate.Six.MonthsFrom(baseDate); // 2027-03-26 // 方法:从 DateTime 起算,结果仍是 DateOnly DateOnly auditDate = InDate.Six.YearsFrom(DateTime.UtcNow);典型应用场景包括:订阅/会员“6 个月后续期提醒日”计算、合同“6 年后到期日”排期、报表“6 周滚动窗口”起点计算等。由于所有成员都是静态且无副作用,可以直接嵌入业务表达式而不需要任何额外配置。
与周边 API 的组合使用
InDate.Six属于相对日期(相对“现在”或相对给定基准日),与InDate家族的绝对日期 API 互补:
InDate的月份属性/方法返回“当前年第 X 月 1 日”或“指定年份的第 X 月 1 日”,例如InDate.January、InDate.JuneOf(2030),详见 website/versioned_docs/version-2.14.1/api/Humanizer.InDate.md;InDate.TheYear(int year)返回指定年份的 1 月 1 日,实现于 src/Humanizer/FluentDate/InDate.cs;- 兄弟类
InDate.One到InDate.Ten提供数量 1~10 的同一套相对日期 API,其中InDate.One使用单数成员名(Day、Week、Month、Year); - 需要
DateTime形态时,可改用In家族的相对时间 API;需要“某月某日”时使用OnDate(如测试中出现的OnDate.January.The21st)。
使用注意事项小结
- 目标框架:
InDate.Six全部成员仅存在于 .NET 6 及更高版本(NET6_0_OR_GREATER条件编译),旧框架下不可用; - 属性基于 UTC:
Days/Weeks/Months/Years以DateTime.UtcNow为基准,跨时区部署时结果一致;若业务要求本地日界线语义,请自行基于本地时间计算; - 返回值恒为 DateOnly:即使传入
DateTime重载,结果也会截断时间分量; - 周的天数换算:
Weeks固定按 7 天/周换算(6 周 = 42 天),不涉及“周起始日”概念; - 月/年边界:加月/加年的月末归整由 .NET
DateOnly实现保证,闰年场景(如 2024-02-29 加 6 年)行为与AddYears一致,可直接依赖测试 tests/Humanizer.Tests/FluentDate/GeneratedFluentDateTests.cs 作为行为基准。
如果只需要“6 天/周/月/年后”这类常见期限,InDate.Six是比手写DateTime.UtcNow.AddX()更富表达力的选择:成员名即文档,返回类型即约束,无需注释即可自解释。
- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
相关推荐
Humanizer FluentDate 指南:使用 InDate.Six 编写 6 天/周/月/年后日期计算
Humanizer FluentDate 指南:使用 InDate.Six 编写 6 天/周/月/年后日期计算 本篇技术指南围绕 Humanizer 的 InD
开发工具Humanizer InDate.Ten 详解:用 DateOnly 流式 API 计算 10 天/周/月/年后的日期
Humanizer InDate.Ten 详解:用 DateOnly 流式 API 计算 10 天/周/月/年后的日期 InDate.Ten 是 Humaniz
开发工具Humanizer InDate.Nine 详解:用流式日期 API 计算 9 天/周/月/年后的 DateOnly
Humanizer InDate.Nine 详解:用流式日期 API 计算 9 天/周/月/年后的 DateOnly Humanizer 的 FluentDat
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考