Sparkdo.Crontab
3.0.0-preview.1
dotnet add package Sparkdo.Crontab --version 3.0.0-preview.1
NuGet\Install-Package Sparkdo.Crontab -Version 3.0.0-preview.1
<PackageReference Include="Sparkdo.Crontab" Version="3.0.0-preview.1" />
<PackageVersion Include="Sparkdo.Crontab" Version="3.0.0-preview.1" />
<PackageReference Include="Sparkdo.Crontab" />
paket add Sparkdo.Crontab --version 3.0.0-preview.1
#r "nuget: Sparkdo.Crontab, 3.0.0-preview.1"
#:package Sparkdo.Crontab@3.0.0-preview.1
#addin nuget:?package=Sparkdo.Crontab&version=3.0.0-preview.1&prerelease
#tool nuget:?package=Sparkdo.Crontab&version=3.0.0-preview.1&prerelease
Sparkdo.Crontab
.NET 10/11 的纯算法 Cron 库:把表达式解析成不可变的 Cron 对象,回答"下一次、上一次、这段时间内的每一次是什么时候"。不托管后台任务、不保存状态、不执行作业——调度语义属于 Sparkdo Scheduler。零外部依赖,AOT / trimming / single-file 友好,Next 主路径 0 B 分配。
dotnet add package Sparkdo.Crontab
using Sparkdo.Crontab;
var cron = Cron.Parse("*/15 * * * *");
var start = new DateTimeOffset(2026, 1, 1, 0, 7, 0, TimeSpan.Zero);
cron.Next(start); // 2026-01-01 00:15:00 +00:00(严格晚于)
cron.Previous(start); // 2026-01-01 00:00:00 +00:00(严格早于)
cron.Between(start, start.AddHours(1)); // 00:15、00:30、00:45(升序,默认不含两端)
cron.Matches(new DateTimeOffset(2026, 1, 1, 0, 45, 0, TimeSpan.Zero)); // true
表达式格式
默认 Standard = 5 段"分 时 日 月 周"。六段必须显式声明是秒还是年:
CronFormat |
段数 | 字段顺序 | 示例 |
|---|---|---|---|
Standard |
5 | 分 时 日 月 周 | 0 9 * * 1-5 |
Seconds |
6 | 秒 分 时 日 月 周 | 0 */5 * * * * |
Years |
6 | 分 时 日 月 周 年 | 0 0 1 1 * 2030 |
SecondsAndYears |
7 | 秒 分 时 日 月 周 年 | 0 0 0 1 1 * 2030 |
Cron.Parse("0 */5 * * * *", CronFormat.Seconds); // 每 5 分钟:从 00:07 → 00:10
Cron.Parse("0 0 1 1 * 2030", CronFormat.Years); // 2030 年元旦 00:00
Cron.Parse("0 */5 * * * *"); // CronParseException:字段数量应为 5,实际为 6。
取值范围:秒/分 0-59、时 0-23、日 1-31、月 1-12、周 0-7(0 与 7 都是周日)、年 1-9999。
字段语法速查
| 写法 | 含义 | 实测例(Standard,起点为固定 offset UTC) |
|---|---|---|
* / ? |
任意值(? 只允许写在日、周字段,语义等同 *) |
0 9 ? * MON-FRI |
a,b,c / a-b / a-b/n |
列表 / 闭区间 / 区间内步长 | 1-10/3 * * * * 匹配 01、04、07、10 |
*/n / s/n |
从字段最小值 / 从 s 起步长 n |
5/15 * * * * 从 00:06 → 00:20 |
JAN–DEC / SUN–SAT |
三字母名称(大小写无关,全拼报错) | 0 0 1 JAN-DEC/6 * → 1 月与 7 月 1 日 |
L / LW / nW(日字段) |
月末日 / 最后一个工作日 / 最接近 n 日的工作日(不跨月) | 0 0 15W * * 2026-08 → 08-14 |
nL / n#k(周字段) |
月内最后一个星期 n / 第 k 个星期 n(k ∈ 1–5) | 0 0 * * MON#3 2026-01 → 01-19 |
R(秒/分/时字段) |
解析期随机取一个合法值,同一实例恒定;不可作缓存键或落库标识(文本相同即等值,但各实例定值彼此独立) | R * * * * 用于打散整点热点 |
@daily 等宏 |
见下表,大小写无关 | Cron.Parse("@daily").ToString() → 0 0 * * * |
日字段与周字段是 AND(与 Quartz 的 OR 习惯不同):0 9 15 * MON 只匹配"15 日且是周一",从 2026-01-01 → 2026-06-15 09:00。没有限制的一侧写 * 或 ?。
星期端点会做 7 归一化:5-7 = 五六日、7-0 = 仅周日、7/2 = 日二四六。倒置范围(8-5)在解析期拒绝。
宏与工厂
| 预设 / 宏 | 展开 | 工厂 | 生成表达式 |
|---|---|---|---|
Cron.Secondly / @secondly |
* * * * * * |
HourlyAt(minute) |
{minute} * * * * |
Cron.Minutely / @minutely |
* * * * * |
DailyAt(hour, minute = 0) |
{minute} {hour} * * * |
Cron.Hourly / @hourly |
0 * * * * |
WeeklyAt(day, hour = 0, minute = 0) |
{minute} {hour} * * {0..6} |
Cron.Daily / @daily |
0 0 * * * |
MonthlyAt(day, hour = 0, minute = 0) |
{minute} {hour} {day} * * |
Cron.Weekly / @weekly |
0 0 * * 0 |
YearlyAt(month, day, hour = 0, minute = 0) |
{minute} {hour} {day} {month} * |
Cron.Monthly / @monthly |
0 0 1 * * |
EverySecond() / EveryMinute() |
返回共享预设实例 |
Cron.Yearly / @yearly |
0 0 1 1 * |
||
Cron.Workday / @workday |
0 0 * * 1-5 |
工厂参数越界(DailyAt(24)、HourlyAt(60))或组合出永不触发的规则(YearlyAt(2, 31))都在调用时抛 CronParseException。
校验外部输入
try
{
var cron = Cron.Parse(input);
}
catch (CronParseException ex)
{
// ex.Expression 整串原文 / ex.Field 出错的逗号分段("0,15,bad" 只给 "bad")或整字段原文 / ex.Position 该原文内 0-based 下标 / ex.Reason 中文诊断
}
// 只要布尔结论时用 TryParse / IsValid(TryParse 只吞 CronParseException,其它异常照常传播)
if (Cron.TryParse(input, out var cron) && cron.Next(DateTimeOffset.UtcNow) <= deadline) { }
解析期即拒绝:空/null 表达式、字段数不符、值越界、零步长、倒置范围、空段、未知宏、特殊语法用错字段、月内序号越界、全拼名称,以及"日字段在月字段可选月份内永无命中"的表达式(0 0 31 2 *)与闰日配非闰年(0 0 29 2 * 2100)。
时间语义
不给 TimeZone 时只按 baseTime.Offset 做墙钟算术,不读机器时区、不感知 DST,结果的 offset 恒等于输入的 offset:
Cron.Parse("0 9 * * *").Next(new DateTimeOffset(2026, 1, 1, 8, 0, 0, TimeSpan.FromHours(8)));
// → 2026-01-01 09:00:00 +08:00
需要本地时间与 DST 规则时显式传 TimeZoneInfo(时区 ID 在 Windows 与 Linux 不同,请按部署环境验证):
var zone = TimeZoneInfo.FindSystemTimeZoneById("Asia/Shanghai");
var options = CronOptions.InTimeZone(zone);
Cron.Parse("0 9 * * *").Next(DateTimeOffset.UtcNow, options); // → 该时区 09:00,offset 随 DST 变化
CronOptions(readonly record struct,可用 with 派生)四个旋钮:
| 属性 | 默认 | 作用 |
|---|---|---|
TimeZone |
null |
null = 固定 offset;非 null = 按该时区墙钟解释规则。 |
Boundary |
Exclusive |
IncludeStart 让 Next/Between 含起点,IncludeEnd 让 Previous/Between 含终点,IncludeBoth 两端都含。 |
InvalidTimePolicy |
Skip |
春季拨快造成的不存在时刻:Skip 让落在 gap 内的触发点作废,ShiftForward 前移到 gap 后第一个有效分钟。例:30 2 8 3 * 在 America/New_York 的 2026-03-08 无 02:30,该规则当天只有这一个点,默认直接给 2027-03-08(30 2,3 8 3 * 仍会给当天的 03:30)。 |
AmbiguousTimePolicy |
EarlierOffset |
秋季回拨出现两遍的本地时刻:取较早/较晚 offset;Both 让 Between 返回两条并按真实 instant 升序排列。 |
三条容易踩的规则:
- 起点先按最小单位截断。
Standard格式下* * * * *对00:07:30的Next是00:08:00;即使给了IncludeStart也不会返回00:07:30。要按秒推进请用Seconds格式。 Matches按 instant 自身 offset 的墙钟判定,不做时区转换、不读CronOptions;需要按某时区判断请用Matches(instant, timeZone)重载。规则没有秒字段时,秒非 0 一律false。- 窗口内找不到就抛。无年字段约束的规则在查询方向 5 年内搜索,越界抛
InvalidOperationException(年字段有界时上限自动扩到该年份);Between惰性求值,窗口耗尽的那一步同样抛。
不可变与可共享
Cron 无状态、线程安全,可长期持有并跨线程共享;相等性按表达式文本 Ordinal 比较(ToString() 原样返回输入文本,因此空白变体不等值——落库或做缓存键前先规范化)。例外:含 R 的表达式文本相同即等值,但各实例的随机定值彼此独立、触发时刻可以不同,不可作缓存键或落库标识。解析有约 1 KB 冷路径成本,热查询 80–300 ns / 0 B,所以高频路径请缓存 Cron 实例而不是反复 Parse。
边界
- 不提供 DI、Hosting、后台 worker、任务队列、持久化、分布式锁、misfire / catch-up 与执行语义。
- 不做 Cron → 人类可读文本格式化,不做表达式改写或等价归一化。
- 不读机器本地时区;DST 数据来自 BCL
TimeZoneInfo。 - 不暴露已解析的字段结构(
Cron是不透明规则句柄)。
完整语法语义、DST 策略矩阵、异常一览、基准数据与用法配方见源码仓库 src/crontab/README.md。
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. net11.0 is compatible. |
-
net10.0
- No dependencies.
-
net11.0
- No dependencies.
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Sparkdo.Crontab:
| Package | Downloads |
|---|---|
|
Sparkdo.Scheduler
Sparkdo Scheduler 运行时与默认 in-memory provider,内嵌源生成器分析器资产。 |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0-preview.1 | 79 | 10/30/2026 |
| 2.0.0-preview.2 | 115 | 7/20/2026 |
| 2.0.0-preview.1 | 91 | 7/18/2026 |