From 3805d2b6451c91a70ed54031c969a069b815b32d Mon Sep 17 00:00:00 2001
From: Falcon <12919280+falconfly@user.noreply.gitee.com>
Date: Wed, 19 Feb 2025 15:57:11 +0800
Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90CronExpression=E5=B9=B6?=
=?UTF-8?q?=E6=B5=8B=E8=AF=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Falcon.SugarApi.Test/CronExpressionTests.cs | 90 +++++++
.../TimedBackgroundTask/CronExpression.cs | 235 ++++++++++++++++++
.../TimedBackgroundTask/CronResult.cs | 111 +++++++++
.../TimedBackgroundTask/TimePartEnum.cs | 56 +++++
4 files changed, 492 insertions(+)
create mode 100644 Falcon.SugarApi.Test/CronExpressionTests.cs
create mode 100644 Falcon.SugarApi/TimedBackgroundTask/CronExpression.cs
create mode 100644 Falcon.SugarApi/TimedBackgroundTask/CronResult.cs
create mode 100644 Falcon.SugarApi/TimedBackgroundTask/TimePartEnum.cs
diff --git a/Falcon.SugarApi.Test/CronExpressionTests.cs b/Falcon.SugarApi.Test/CronExpressionTests.cs
new file mode 100644
index 0000000..068ff5f
--- /dev/null
+++ b/Falcon.SugarApi.Test/CronExpressionTests.cs
@@ -0,0 +1,90 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+
+namespace Falcon.SugarApi.Test
+{
+ [TestClass()]
+ public class CronExpressionTests
+ {
+ [TestMethod]
+ public void CronExpressionTest() {
+ var now20250213102512 = new DateTime(2025,2,13,10,25,12);
+
+ var cron = new CronExpression("0 30 3 * * 1-5"); // 工作日凌晨3:30:00
+ var nextTime = cron.GetNextOccurrence(now20250213102512);
+ if(nextTime == null) {
+ Assert.Fail("nextTime is null!");
+ return;
+ }
+ Assert.IsTrue(DateToString(nextTime) == "20250214033000");
+ cron = new CronExpression("");//下一秒触发
+ nextTime = cron.GetNextOccurrence(now20250213102512);
+ Assert.IsTrue(DateToString(nextTime) == "20250213102513");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250213102514",nextTime.ToString());
+
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250213102515",nextTime.ToString());
+
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250213102516",nextTime.ToString());
+
+ cron = new CronExpression("10");//每分钟10秒时触发
+ nextTime = cron.GetNextOccurrence(now20250213102512);
+ Assert.IsTrue(DateToString(nextTime) == "20250213102610");
+
+ cron = new CronExpression("10 25");//每个小时的25分10秒触发
+ nextTime = cron.GetNextOccurrence(now20250213102512);
+ Assert.IsTrue(DateToString(nextTime) == "20250213112510");
+
+ cron = new CronExpression("10 25");//每个小时的25分10秒触发
+ nextTime = cron.GetNextOccurrence(now20250213102512);
+ Assert.IsTrue(DateToString(nextTime) == "20250213112510");
+
+ cron = new CronExpression("10/10 25");//每个小时的25分10秒触发,只有每10秒触发一次
+ nextTime = cron.GetNextOccurrence(now20250213102512);
+ Assert.IsTrue(DateToString(nextTime) == "20250213102520");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250213102530");
+
+ cron = new CronExpression("0 0 3-5");//每天凌晨3点到5点每小时触发一次
+ nextTime = cron.GetNextOccurrence(now20250213102512);
+ Assert.IsTrue(DateToString(nextTime) == "20250214030000");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250214040000");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250214050000");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250215030000");
+
+ cron = new CronExpression("0 0 3-5 * * 1-5");//每个工作日凌晨3点到5点每小时触发一次
+ nextTime = cron.GetNextOccurrence(now20250213102512);
+ Assert.IsTrue(DateToString(nextTime) == "20250214030000");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250214040000");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250214050000");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250217030000",DateToString(nextTime));
+
+ cron = new CronExpression("0 0/30 3-5 * * 1-5");//每个工作日凌晨3点到5点每小时触发两次,平均每半小时一次
+ nextTime = cron.GetNextOccurrence(now20250213102512);
+ Assert.IsTrue(DateToString(nextTime) == "20250214030000",nextTime.ToString());
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250214033000");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250214040000");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250214043000");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250214050000");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250214053000");
+ nextTime = cron.GetNextOccurrence(nextTime);
+ Assert.IsTrue(DateToString(nextTime) == "20250217030000");
+
+ }
+
+ private string? DateToString(DateTime? date) => date?.ToString("yyyyMMddHHmmss");
+ }
+}
\ No newline at end of file
diff --git a/Falcon.SugarApi/TimedBackgroundTask/CronExpression.cs b/Falcon.SugarApi/TimedBackgroundTask/CronExpression.cs
new file mode 100644
index 0000000..aa0f2e1
--- /dev/null
+++ b/Falcon.SugarApi/TimedBackgroundTask/CronExpression.cs
@@ -0,0 +1,235 @@
+using Falcon.SugarApi;
+using Falcon.SugarApi.TimedBackgroundTask;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+///
+/// Cron表达式
+///
+public class CronExpression
+{
+ ///
+ /// 可选的秒范围枚举
+ ///
+ public List Seconds { get; }
+ ///
+ /// 可选的分范围枚举
+ ///
+ public List Minutes { get; }
+ ///
+ /// 可选的时范围枚举
+ ///
+ public List Hours { get; }
+ ///
+ /// 可选的天范围枚举
+ ///
+ public List DaysOfMonth { get; }
+ ///
+ /// 可选的月范围枚举
+ ///
+ public List Months { get; }
+ ///
+ /// 可选的星期范围枚举
+ ///
+ public List DaysOfWeek { get; }
+ ///
+ /// 可选的年范围枚举
+ ///
+ public List Years { get; set; }
+
+ ///
+ /// 通过提供cron表达式构造对象
+ /// 表达式以空格分隔,按顺序每一段分别为“秒,分,时,日,月,星期,年”
+ /// 取值范围:秒分时取值范围0-59,日取值范围1-31,月取值范围1-12,星期取值范围0-6,年取值范围两年内
+ /// 取值*表示可以匹配任意值,比如*表示每一秒,* *表示所有分秒。
+ /// 取值-表示取值范围,比如1-5 3匹配3分1秒到3分5秒所有时间。
+ /// 取值/表示周期取值,比如0/10表示0秒开始每10秒匹配一次。
+ /// 取值可以用,分割,表示取值枚举,比如0 10,20,30 5 表示匹配每天早上5点10分、5点20分和5点30分。
+ /// 表达式右侧的*可以省略。比如0 15 4表示每天上午4点15分,和0 15 4 * * * *相同
+ /// 星期取值范围0-6,0表示星期天,1表示星期一,以此类推。
+ /// 日取值范围1-31,不用考虑大月小月和二月的特殊情况,方法会自动过滤这些特殊日期
+ ///
+ /// cron表达式
+ ///
+ public CronExpression(string cronExpression) {
+ var fields = cronExpression.Split(new[] { ' ' },StringSplitOptions.RemoveEmptyEntries);
+
+ Seconds = fields.Length > 0 ? GetRange(fields[0],0,59) : Enumerable.Range(0,60).ToList();
+ Minutes = fields.Length > 1 ? GetRange(fields[1],0,59) : Enumerable.Range(0,60).ToList();
+ Hours = fields.Length > 2 ? GetRange(fields[2],0,23) : Enumerable.Range(0,23).ToList();
+ DaysOfMonth = fields.Length > 3 ? GetRange(fields[3],1,31) : Enumerable.Range(1,31).ToList();
+ Months = fields.Length > 4 ? GetRange(fields[4],1,12) : Enumerable.Range(1,12).ToList();
+ DaysOfWeek = fields.Length > 5 ? GetRange(fields[5],0,6) : Enumerable.Range(0,7).ToList();
+ var nowYear = DateTime.Now.Year;
+ Years = fields.Length > 6 ? GetRange(fields[6],nowYear,nowYear + 2) : Enumerable.Range(nowYear,2).ToList();
+ }
+
+ ///
+ /// 通过提供的时间获取下一次时间
+ ///
+ /// 上一次的时间
+ /// 下一次到达时间
+ public DateTime GetNextOccurrence(DateTime afterTime) {
+ var ct = new CronResult(afterTime.AddSeconds(1));
+ while(!ct.IsAllAdjust) {
+ if(!ct.IsYearAdjust) {
+ AdjustYear(ct);
+ }
+ if(!ct.IsMonthAdjust) {
+ AdjustMonth(ct);
+ if(!ct.IsYearAdjust) {
+ continue;
+ }
+ }
+ if(!ct.IsDayAdjust) {
+ AdjustDay(ct);
+ if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust) {
+ continue;
+ }
+ }
+ if(!ct.IsHourAdjust) {
+ AdjustHour(ct);
+ if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust) {
+ continue;
+ }
+ }
+ if(!ct.IsMinuteAdjust) {
+ AdjustMinute(ct);
+ if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust || !ct.IsHourAdjust) {
+ continue;
+ }
+ }
+ if(!ct.IsSecondAdjust) {
+ AdjustSecond(ct);
+ }
+ }
+ return ct.AdjustTime;
+ }
+
+ private void AdjustYear(CronResult date) {
+ var next = Years.Where(a => a >= date.AdjustTime.Year);
+ if(next.Any()) {
+ date.SetAdjustTime(date.AdjustTime.AddYears(next.First() - date.AdjustTime.Year),TimePartEnum.Year);
+ date.IsYearAdjust = true;
+ }
+ else {
+ date.IsNullVal = true;
+ date.IsYearAdjust = true;
+ date.IsMonthAdjust = true;
+ date.IsDayAdjust = true;
+ date.IsHourAdjust = true;
+ date.IsMinuteAdjust = true;
+ date.IsSecondAdjust = true;
+ date.AdjustTime = DateTime.MaxValue;
+ }
+ }
+
+ private void AdjustMonth(CronResult date) {
+ var next = Months.Where(m => m >= date.AdjustTime.Month);
+ if(next.Any()) {
+ date.SetAdjustTime(date.AdjustTime.AddMonths(next.First() - date.AdjustTime.Month),TimePartEnum.YearMonth);
+ date.IsMonthAdjust = true;
+ }
+ else {
+ date.SetAdjustTime(date.AdjustTime.AddYears(1),TimePartEnum.YearMonth);
+ }
+ }
+
+ private void AdjustDay(CronResult date) {
+ var dt = date.AdjustTime;
+ int year = dt.Year;
+ int month = dt.Month;
+ int day = dt.Day;
+ int maxDayInMonth = DateTime.DaysInMonth(year,month);
+ var next = DaysOfMonth.Where(a => a >= day && a <= maxDayInMonth && DaysOfWeek.Contains((int)dt.DayOfWeek));
+ if(next.Any()) {
+ date.SetAdjustTime(dt.AddDays(next.First() - day),TimePartEnum.YearDay);
+ date.IsDayAdjust = true;
+ return;
+ }
+ if(!DaysOfWeek.Contains((int)dt.DayOfWeek)) {
+ date.SetAdjustTime(dt.AddDays(1),TimePartEnum.YearDay);
+ return;
+ }
+ date.SetAdjustTime(dt.AddMonths(1),TimePartEnum.YearMonth);
+ }
+
+ private void AdjustHour(CronResult date) {
+ var next = Hours.Where(m => m >= date.AdjustTime.Hour);
+ if(next.Any()) {
+ date.SetAdjustTime(date.AdjustTime.AddHours(next.First() - date.AdjustTime.Hour),TimePartEnum.YearHour);
+ date.IsHourAdjust = true;
+ return;
+ }
+ date.SetAdjustTime(date.AdjustTime.AddDays(1),TimePartEnum.YearHour);
+ }
+
+ private void AdjustMinute(CronResult date) {
+ var next = Minutes.Where(m => m >= date.AdjustTime.Minute);
+ if(next.Any()) {
+ date.SetAdjustTime(date.AdjustTime.AddMinutes(next.First() - date.AdjustTime.Minute),TimePartEnum.YearMinute);
+ date.IsMinuteAdjust = true;
+ return;
+ }
+ date.SetAdjustTime(date.AdjustTime.AddHours(1),TimePartEnum.YearMinute);
+ }
+
+ private void AdjustSecond(CronResult date) {
+ var next = Seconds.Where(m => m >= date.AdjustTime.Second);
+ if(next.Any()) {
+ date.SetAdjustTime(date.AdjustTime.AddSeconds(next.First() - date.AdjustTime.Second),TimePartEnum.YearSecond);
+ date.IsSecondAdjust = true;
+ return;
+ }
+ date.SetAdjustTime(date.AdjustTime.AddMinutes(1),TimePartEnum.YearSecond);
+ }
+
+ private static List GetRange(string exp,int min,int max) {
+ var charList = "0123456789*-/,".ToArray();
+ if(exp.ToCharArray().Any(a => a.NotIn(charList))) {
+ throw new ArgumentOutOfRangeException(nameof(exp));
+ }
+ var list = new List();
+ if(exp == "*") {
+ return Enumerable.Range(min,max - min + 1).ToList();
+ }
+ if(int.TryParse(exp,out int iexp) && iexp.Between(min,max)) {
+ list.Add(iexp);
+ return list;
+ }
+ if(exp.Contains('-')) {
+ var g = exp.Split('-',StringSplitOptions.RemoveEmptyEntries);
+ var i = int.Parse(g[0]);
+ var x = int.Parse(g[1]);
+ i = Math.Max(i,min);
+ x = Math.Min(x,max);
+ while(i <= x) {
+ list.Add(i++);
+ }
+ return list;
+ }
+ if(exp.Contains('/')) {
+ var g = exp.Split('/',StringSplitOptions.RemoveEmptyEntries);
+ var f = Math.Max(min,int.Parse(g[0]));
+ var s = int.Parse(g[1]);
+ while(f < max) {
+ list.Add(f);
+ f += s;
+ }
+ return list;
+ }
+ if(exp.Contains(',')) {
+ foreach(var ie in exp.Split(',')) {
+ if(int.TryParse(ie,out int iei) && iei.Between(min,max)) {
+ list.Add(iei);
+ }
+ else {
+ throw new ArgumentException("给定的exp表达式错误,逗号分割的每一项都必须是数字",nameof(exp));
+ }
+ }
+ return list;
+ }
+ throw new ArgumentException("提供的cron表达式错误",nameof(exp));
+ }
+}
\ No newline at end of file
diff --git a/Falcon.SugarApi/TimedBackgroundTask/CronResult.cs b/Falcon.SugarApi/TimedBackgroundTask/CronResult.cs
new file mode 100644
index 0000000..ff7e4fb
--- /dev/null
+++ b/Falcon.SugarApi/TimedBackgroundTask/CronResult.cs
@@ -0,0 +1,111 @@
+using System;
+
+namespace Falcon.SugarApi.TimedBackgroundTask
+{
+ public class CronResult
+ {
+ public DateTime AdjustTime { get; set; }
+ public bool IsYearAdjust { get; set; } = false;
+ public bool IsMonthAdjust { get; set; } = false;
+ public bool IsDayAdjust { get; set; } = false;
+ public bool IsHourAdjust { get; set; } = false;
+ public bool IsMinuteAdjust { get; set; } = false;
+ public bool IsSecondAdjust { get; set; } = false;
+
+ public bool IsAllAdjust
+ => (IsYearAdjust && IsMonthAdjust && IsYearAdjust
+ && IsDayAdjust && IsHourAdjust && IsMinuteAdjust && IsSecondAdjust) || IsNullVal;
+
+ ///
+ /// 没有匹配到值
+ ///
+ public bool IsNullVal { get; set; } = false;
+
+ public CronResult(DateTime last) => this.AdjustTime = last;
+
+ public void SetAdjustTime(DateTime dt,TimePartEnum part) {
+ if(!part.HasFlag(TimePartEnum.Year)) {
+ return;
+ }
+ if(dt.Year != this.AdjustTime.Year) {
+ this.AdjustTime = new DateTime(dt.Year,1,1,0,0,0);
+ IsYearAdjust = false;
+ IsMonthAdjust = false;
+ IsDayAdjust = false;
+ IsHourAdjust = false;
+ IsMinuteAdjust = false;
+ IsSecondAdjust = false;
+ return;
+ }
+ //else {
+ // IsYearAdjust = true;
+ //}
+ if(!part.HasFlag(TimePartEnum.Month)) {
+ return;
+ }
+ if(dt.Month != this.AdjustTime.Month) {
+ this.AdjustTime = new DateTime(dt.Year,dt.Month,1,0,0,0);
+ IsMonthAdjust = false;
+ IsDayAdjust = false;
+ IsHourAdjust = false;
+ IsMinuteAdjust = false;
+ IsSecondAdjust = false;
+ return;
+ }
+ //else {
+ // IsMonthAdjust = true;
+ //}
+ if(!part.HasFlag(TimePartEnum.Day)) {
+ return;
+ }
+ if(dt.Day != this.AdjustTime.Day) {
+ this.AdjustTime = new DateTime(dt.Year,dt.Month,dt.Day,0,0,0);
+ IsDayAdjust = false;
+ IsHourAdjust = false;
+ IsMinuteAdjust = false;
+ IsSecondAdjust = false;
+ return;
+ }
+ //else {
+ // IsDayAdjust = true;
+ //}
+ if(!part.HasFlag(TimePartEnum.Hour)) {
+ return;
+ }
+ if(dt.Hour != this.AdjustTime.Hour) {
+ this.AdjustTime = new DateTime(dt.Year,dt.Month,dt.Day,dt.Hour,0,0);
+ IsHourAdjust = false;
+ IsMinuteAdjust = false;
+ IsSecondAdjust = false;
+ return;
+ }
+ //else {
+ // IsHourAdjust = true;
+ //}
+ if(!part.HasFlag(TimePartEnum.Minute)) {
+ return;
+ }
+ if(dt.Minute != this.AdjustTime.Minute) {
+ this.AdjustTime = new DateTime(dt.Year,dt.Month,dt.Day,dt.Hour,dt.Minute,0);
+ IsMinuteAdjust = false;
+ IsSecondAdjust = false;
+ return;
+ }
+ //else {
+ // IsMinuteAdjust = true;
+ //}
+ if(!part.HasFlag(TimePartEnum.Second)) {
+ return;
+ }
+ if(dt.Second != this.AdjustTime.Second) {
+ this.AdjustTime = dt;
+ IsSecondAdjust = false;
+ return;
+ }
+ //else {
+ // IsSecondAdjust = true;
+ //}
+ }
+
+ }
+}
diff --git a/Falcon.SugarApi/TimedBackgroundTask/TimePartEnum.cs b/Falcon.SugarApi/TimedBackgroundTask/TimePartEnum.cs
new file mode 100644
index 0000000..0f2602d
--- /dev/null
+++ b/Falcon.SugarApi/TimedBackgroundTask/TimePartEnum.cs
@@ -0,0 +1,56 @@
+using System;
+
+namespace Falcon.SugarApi.TimedBackgroundTask
+{
+ ///
+ /// 时间结构枚举
+ ///
+ [Flags]
+ public enum TimePartEnum
+ {
+ ///
+ /// 年
+ ///
+ Year = 1,
+ ///
+ /// 月
+ ///
+ Month = 2,
+ ///
+ /// 日
+ ///
+ Day = 4,
+ ///
+ /// 小时
+ ///
+ Hour = 8,
+ ///
+ /// 分
+ ///
+ Minute = 16,
+ ///
+ /// 秒
+ ///
+ Second = 32,
+ ///
+ /// 年和月
+ ///
+ YearMonth = Year + Month,
+ ///
+ /// 年月日
+ ///
+ YearDay = YearMonth + Day,
+ ///
+ /// 年月日时
+ ///
+ YearHour = YearDay + Hour,
+ ///
+ /// 年月日时分
+ ///
+ YearMinute = YearHour + Minute,
+ ///
+ /// 年月入时分秒
+ ///
+ YearSecond = YearMinute + Second,
+ }
+}