- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
导读
InDate.Four是 Humanizer FluentDate 流式日期 API 中的一个静态嵌套类,专门面向 .NET 6+ 引入的System.DateOnly类型,提供“从现在起 4 天/4 周/4 个月/4 年”以及“从任意给定日期起 4 天/4 周/4 个月/4 年”的计算能力。阅读本文后,你将掌握InDate.Four全部 4 个属性与 8 个方法重载的签名与语义、其背后由 T4 模板生成的源码实现、基于UtcNow与AddMonths/AddYears的日历运算原理,以及如何在可重复执行的测试与业务代码中正确使用这类流式日期成员。
InDate.Four 是什么:API 全景
在 Humanizer 3.0.10 的 API 文档中,InDate.Four被定义为public static class InDate.Four,继承自System.Object,是InDate这个静态 partial 类内部按数字分组的嵌套静态类之一。它的职责非常单一:把所有与“4”相关的相对日期计算集中在一个命名空间中,通过英文数字单词Four直接暴露给调用者。
InDate.Four由两类成员组成:
- 属性(4 个):
Days、Weeks、Months、Years,语义是“从当前时刻算起的相对日期”,均返回System.DateOnly; - 方法(8 个):
DaysFrom、WeeksFrom、MonthsFrom、YearsFrom,每个都提供DateOnly与DateTime两个输入重载,语义是“从指定日期算起的相对日期”,返回值统一为System.DateOnly。
完整成员签名如下(与 Humanizer.InDate.Four.md 文档一致):
| 成员 | 签名 | 语义 |
|---|---|---|
| 属性 | public static System.DateOnly Days { get; } | 从现在起 4 天 |
| 属性 | public static System.DateOnly Weeks { get; } | 从现在起 4 周 |
| 属性 | public static System.DateOnly Months { get; } | 从现在起 4 个月 |
| 属性 | public static System.DateOnly Years { get; } | 从现在起 4 年 |
| 方法 | public static System.DateOnly DaysFrom(System.DateOnly date) | 从给定日期起 4 天 |
| 方法 | public static System.DateOnly DaysFrom(System.DateTime date) | 从给定日期起 4 天 |
| 方法 | public static System.DateOnly WeeksFrom(System.DateOnly date) | 从给定日期起 4 周 |
| 方法 | public static System.DateOnly WeeksFrom(System.DateTime date) | 从给定日期起 4 周 |
| 方法 | public static System.DateOnly MonthsFrom(System.DateOnly date) | 从给定日期起 4 个月 |
| 方法 | public static System.DateOnly MonthsFrom(System.DateTime date) | 从给定日期起 4 个月 |
| 方法 | public static System.DateOnly YearsFrom(System.DateOnly date) | 从给定日期起 4 年 |
| 方法 | public static System.DateOnly YearsFrom(System.DateTime date) | 从给定日期起 4 年 |
属性成员:基于 UtcNow 的“从当下算起”
四个属性在 InDate.SomeTimeFrom.cs 中的Four类里均有直接对应的实现,全部以DateTime.UtcNow为时间基准:
public static class Four { public static DateOnly Days => DateOnly.FromDateTime(DateTime.UtcNow.AddDays(4)); public static DateOnly Weeks => DateOnly.FromDateTime(DateTime.UtcNow.AddDays(28)); public static DateOnly Months => DateOnly.FromDateTime(DateTime.UtcNow.AddMonths(4)); public static DateOnly Years => DateOnly.FromDateTime(DateTime.UtcNow.AddYears(4)); }几个值得注意的实现细节:
- 周按 7 天折算:
Weeks不是调用AddWeeks(DateOnly本身也不提供该方法),而是直接AddDays(28),即 4 × 7 天,这与人类直觉中的“一周七天”完全一致。 - UTC 时间基准:属性读取的是
DateTime.UtcNow而不是DateTime.Now,随后通过DateOnly.FromDateTime截断为纯日期(不含时间分量)。这意味着结果不依赖部署机器的本地时区,行为是可预测的。 - 动态求值:属性是表达式体(expression-bodied)的只读属性,每次访问都会重新取
UtcNow计算,而不是在类加载时缓存。因此同一次程序运行中先后两次读取InDate.Four.Days,在跨 UTC 午夜边界时可能得到不同的日期。
官方场景指南 fluent-dates-and-time-spans.mdx 也明确指出:不带From或Of(year)的属性会读取DateTime.Now或UtcNow,应避免在确定性代码(如测试断言)中直接使用——这正是下面要重点介绍*From方法的原因。
方法成员:从任意基准日期计算
四个*From方法提供双重重载,分别接收DateOnly与DateTime,并把任意输入统一归一化为DateOnly输出:
// DateOnly 输入:直接复用 AddDays/AddMonths/AddYears public static DateOnly DaysFrom(DateOnly date) => date.AddDays(4); public static DateOnly WeeksFrom(DateOnly date) => date.AddDays(28); public static DateOnly MonthsFrom(DateOnly date) => date.AddMonths(4); public static DateOnly YearsFrom(DateOnly date) => date.AddYears(4); // DateTime 输入:先做日历运算,再转 DateOnly public static DateOnly DaysFrom(DateTime date) => DateOnly.FromDateTime(date.AddDays(4)); public static DateOnly WeeksFrom(DateTime date) => DateOnly.FromDateTime(date.AddDays(28)); public static DateOnly MonthsFrom(DateTime date) => DateOnly.FromDateTime(date.AddMonths(4)); public static DateOnly YearsFrom(DateTime date) => DateOnly.FromDateTime(date.AddYears(4));两个重载的差异在于运算发生的层级:
- 传入
DateOnly时,直接在DateOnly上进行AddDays/AddMonths/AddYears,结果天然是DateOnly; - 传入
DateTime时,先调用DateTime的AddDays/AddMonths/AddYears(保留时间分量做进位处理),再通过DateOnly.FromDateTime丢弃时间、取出日期部分。
这种设计让调用方无论持有哪种日期类型,都能以一致的DateOnly结果收尾,非常适合需要“只关心日期、不关心时间”的业务场景,例如预约、排期、账单周期计算。
日历运算的归一化语义
MonthsFrom与YearsFrom直接委托给AddMonths/AddYears,因此继承 .NET 标准的月末归一化(normalization)规则:当月不存在同一天时,结果自动折叠到该月的最后一天。例如从 2 月 29 日(闰年)加上 4 个月,落在 6 月 29 日;而如果从某年 1 月 31 日加 1 个月,会得到 2 月 28 日(平年)而非抛异常。官方场景文档 fluent-dates-and-time-spans.mdx 对这一点有明确说明,并强调这类运算是日历粒度的操作,与把TimeSpan直接累加到日期上的“固定时长估算”有本质区别。
源码视角:partial class 与 T4 模板生成
InDate.Four只是整个 FluentDate 家族的一个切片。从源码结构看,InDate是一个跨越多个文件的public partial class:
- InDate.cs:提供
TheYear(int year),返回指定年份的 1 月 1 日; - InDate.Months.cs:提供
January、JanuaryOf(int year)等十二个月份成员,由 InDate.Months.tt 生成; - InDate.SomeTimeFrom.cs:包含
One到Ten十个嵌套静态类,Four是其中之一。
One~Ten并非手写,而是由 T4 文本模板 InDate.SomeTimeFrom.tt 循环生成的。模板核心逻辑如下(摘录):
<#for (var i = 1; i <= 10; i++){ var plural = i > 1 ? "s" : ""; var day = "Day" + plural; ... #> public static class <#= i.ToWords().Dehumanize() #> { public static DateOnly <#= day #> => DateOnly.FromDateTime(DateTime.UtcNow.AddDays(<#= i #>)); ...可以看到三个关键设计:
- 命名来自单词转换:类名用
i.ToWords().Dehumanize()生成——即先经 Humanizer 的数字转单词能力得到"four",再经 Dehumanize 规范化为 PascalCase 的Four。这正是One~Ten类名统一风格的来源,也展示了 Humanizer 自身的扩展方法在项目内部的复用。 - 成员命名遵循单复数:
i == 1时属性名是单数Day/Week/Month/Year,i > 1时带复数后缀Days/Weeks/Months/Years(见InDate.SomeTimeFrom.cs中One类与Two~Ten类的命名差异)。 - 生成的 .cs 与 .tt 一一对应:Humanizer.csproj 中配置了
TextTemplatingFileGenerator,将InDate.SomeTimeFrom.tt与生成的InDate.SomeTimeFrom.cs关联,确保仓库中提交的代码与模板保持同步。
版本与条件编译:为什么只有 .NET 6+ 可用
DateOnly是 .NET 6 新增的类型,因此 Humanizer 对整套InDate数字级成员都加了条件编译保护。在 InDate.SomeTimeFrom.cs 与 InDate.Months.cs 的顶部都可以看到:
#if NET6_0_OR_GREATER namespace Humanizer; ... #endif也就是说,只有目标框架满足NET6_0_OR_GREATER时(例如net8.0、net10.0、net11.0),InDate.Four才会被编译进程序集;而在net48、netstandard2.0等旧框架目标下,该成员不存在。这一点与 Humanizer.csproj 中声明的多目标框架(net11.0;net10.0;net8.0;net48;netstandard2.0)是一致的。如果你的项目仍基于旧框架,应改用面向DateTime的 In.SomeTimeFrom.cs(In.Four.Days、In.Four.MonthsFrom(...)等),其成员返回System.DateTime,且额外提供了Second、Minute、Hour等更细粒度的单位。
实战:在可重复测试与业务代码中使用
确定性优先:总是注入基准日期
由于不带From的属性依赖UtcNow,在单元测试中直接断言InDate.Four.Days会导致测试结果随时间漂移。正确的做法是用*From方法传入固定基准。仓库自带的测试 InDateTests.cs 展示了这一模式(针对Five,但Four同理):
[Fact] public void InFiveDays() { var baseDate = OnDate.January.The21st; var date = InDate.Five.DaysFrom(baseDate); Assert.Equal(baseDate.AddDays(5), date); }测试先构造固定的DateOnly基准,再用被测 API 计算结果并断言,保证结果与运行时刻无关。
与 In.Two.MonthsFrom 组合的完整示例
官方示例 scenarios-fluent-dates/Program.cs 演示了同族 API(In系列,返回DateTime)的用法:以固定起点2025-01-20 09:00调用In.Two.MonthsFrom(startingPoint),得到2025-03-20 09:00(输出见 expected-output.txt)。把In换成InDate,即可获得同样语义的DateOnly版本:
using Humanizer; // 业务场景:计算 4 个月后的日期(纯日期,无时间分量) var renewalDate = InDate.Four.MonthsFrom(DateOnly.FromDateTime(DateTime.Now)); // 混合输入:DateTime 输入也会被归一化为 DateOnly var dueDate = InDate.Four.WeeksFrom(new DateTime(2026, 9, 28, 9, 30, 0)); // dueDate 的类型是 DateOnly,值为 2026-10-26语义选择建议
- 需要固定时间长度(如“恰好 4 个 7 天”)时,
Weeks/Days是确定性的:AddDays(28)永远是 28 天; - 需要日历语义(如“下个月的同一天”)时,
Months/Years会按AddMonths/AddYears的月末归一化规则折叠,极端日期需注意结果可能不是“同一天”; - 需要确定可复现时,避免无参属性,优先
*From(date)注入基准;需要纯日期、无时区干扰时,优先DateOnly版本的InDate,而非DateTime版本的In。
延伸阅读
- InDate 类 API 参考:十二个月份、
TheYear与One~Ten全量成员 - In.Four 类 API 参考:面向
DateTime的同名数字级成员 - InDate 源码 与 T4 生成模板
- 流式日期与时间跨度场景指南 与 可运行示例
- InDate 单元测试:确定性基准的测试写法
- 开发工具
【免费下载链接】Humanizer
Humanizer meets all your .NET needs for manipulating and displaying strings, enums, dates, times, timespans, numbers and quantities
相关推荐
Humanizer InDate.Four 详解:用流式 API 计算 4 天、4 周、4 月、4 年后的 DateOnly 日期
Humanizer InDate.Four 详解:用流式 API 计算 4 天、4 周、4 月、4 年后的 DateOnly 日期 InDate.Four 是
开发工具Humanizer InDate.Four 详解:用流式 API 生成"4 天/周/月/年后"的 DateOnly 日期
Humanizer InDate.Four 详解:用流式 API 生成"4 天/周/月/年后"的 DateOnly 日期 Humanizer 的 FluentD
开发工具Humanizer InDate.Four 使用指南:用 DateOnly 快速计算 4 天、4 周、4 个月与 4 年后的日期
Humanizer InDate.Four 使用指南:用 DateOnly 快速计算 4 天、4 周、4 个月与 4 年后的日期 Humanizer 的 Flu
开发工具
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考