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