diff --git a/Falcon.SugarApi.Test/CronExpressionTests.cs b/Falcon.SugarApi.Test/CronExpressionTests.cs
index 068ff5f..ddcdac2 100644
--- a/Falcon.SugarApi.Test/CronExpressionTests.cs
+++ b/Falcon.SugarApi.Test/CronExpressionTests.cs
@@ -1,4 +1,5 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Falcon.SugarApi.TimedBackgroundTask;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
namespace Falcon.SugarApi.Test
@@ -12,10 +13,7 @@ namespace Falcon.SugarApi.Test
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);
diff --git a/Falcon.SugarApi/TimedBackgroundTask/CronExpression.cs b/Falcon.SugarApi/TimedBackgroundTask/CronExpression.cs
index 85033a9..4fb90ff 100644
--- a/Falcon.SugarApi/TimedBackgroundTask/CronExpression.cs
+++ b/Falcon.SugarApi/TimedBackgroundTask/CronExpression.cs
@@ -1,242 +1,245 @@
-using Falcon.SugarApi;
-using Falcon.SugarApi.TimedBackgroundTask;
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
-///
-/// Cron表达式
-///
-public class CronExpression
+namespace Falcon.SugarApi.TimedBackgroundTask
{
- ///
- /// 可选的秒范围枚举
- ///
- private List Seconds { get; }
- ///
- /// 可选的分范围枚举
- ///
- private List Minutes { get; }
- ///
- /// 可选的时范围枚举
- ///
- private List Hours { get; }
- ///
- /// 可选的天范围枚举
- ///
- private List DaysOfMonth { get; }
- ///
- /// 可选的月范围枚举
- ///
- private List Months { get; }
- ///
- /// 可选的星期范围枚举
- ///
- private List DaysOfWeek { get; }
- ///
- /// 可选的年范围枚举
- ///
- private 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标准中日期和星期只要满足一个即可匹配,但本方法不同,我认为既然其他所有部分(比如分和秒)都是必须同时满足,那么日期和星期也是必须同时满足。
- /// 目前方法不支持*-/,分隔符的混合使用,等待后续改进
+ /// Cron表达式
///
- /// cron表达式
- ///
- public CronExpression(string cronExpression) {
- var fields = cronExpression.Split(new[] { ' ' },StringSplitOptions.RemoveEmptyEntries);
+ public class CronExpression
+ {
+ ///
+ /// 可选的秒范围枚举
+ ///
+ private List Seconds { get; }
+ ///
+ /// 可选的分范围枚举
+ ///
+ private List Minutes { get; }
+ ///
+ /// 可选的时范围枚举
+ ///
+ private List Hours { get; }
+ ///
+ /// 可选的天范围枚举
+ ///
+ private List DaysOfMonth { get; }
+ ///
+ /// 可选的月范围枚举
+ ///
+ private List Months { get; }
+ ///
+ /// 可选的星期范围枚举
+ ///
+ private List DaysOfWeek { get; }
+ ///
+ /// 可选的年范围枚举
+ ///
+ private List Years { get; set; }
- 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,24).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();
- }
+ ///
+ /// 通过提供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标准中日期和星期只要满足一个即可匹配,但本方法不同,我认为既然其他所有部分(比如分和秒)都是必须同时满足,那么日期和星期也是必须同时满足。
+ /// 目前方法不支持*-/,分隔符的混合使用,等待后续改进
+ ///
+ /// cron表达式
+ ///
+ public CronExpression(string cronExpression) {
+ var fields = cronExpression.Split(new[] { ' ' },StringSplitOptions.RemoveEmptyEntries);
- ///
- /// 通过提供的时间获取下一次时间
- ///
- /// 上一次的时间
- /// 下一次到达时间
- 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);
+ 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,24).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) {
- continue;
+ 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);
}
}
- if(!ct.IsDayAdjust) {
- AdjustDay(ct);
- if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust) {
- continue;
+ 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();
+ var expChars = exp.ToCharArray().Distinct();
+ var outRangeChars = expChars.Where(a => a.NotIn(charList)).ToArray();
+ if(outRangeChars.Any()) {
+ throw new ArgumentOutOfRangeException(nameof(exp),$"非法字符{new string(outRangeChars)} 在表达式 {exp} 中");
+ }
+ //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(!ct.IsHourAdjust) {
- AdjustHour(ct);
- if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust) {
- continue;
+ 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(!ct.IsMinuteAdjust) {
- AdjustMinute(ct);
- if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust || !ct.IsHourAdjust) {
- continue;
+ 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;
}
- 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;
+ throw new ArgumentException("提供的cron表达式错误",nameof(exp));
}
}
- 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();
- var expChars = exp.ToCharArray().Distinct();
- var outRangeChars = expChars.Where(a => a.NotIn(charList)).ToArray();
- if(outRangeChars.Any()) {
- throw new ArgumentOutOfRangeException(nameof(exp),$"非法字符{new string(outRangeChars)} 在表达式 {exp} 中");
- }
- //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