整理代码格式

This commit is contained in:
Falcon 2025-02-20 16:48:31 +08:00
parent 6092239b29
commit ace506c577
2 changed files with 226 additions and 225 deletions

View File

@ -1,4 +1,5 @@
using Microsoft.VisualStudio.TestTools.UnitTesting; using Falcon.SugarApi.TimedBackgroundTask;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System; using System;
namespace Falcon.SugarApi.Test 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 cron = new CronExpression("0 30 3 * * 1-5"); // 工作日凌晨3:30:00
var nextTime = cron.GetNextOccurrence(now20250213102512); var nextTime = cron.GetNextOccurrence(now20250213102512);
if(nextTime == null) {
Assert.Fail("nextTime is null!");
return;
}
Assert.IsTrue(DateToString(nextTime) == "20250214033000"); Assert.IsTrue(DateToString(nextTime) == "20250214033000");
cron = new CronExpression("");//下一秒触发 cron = new CronExpression("");//下一秒触发
nextTime = cron.GetNextOccurrence(now20250213102512); nextTime = cron.GetNextOccurrence(now20250213102512);

View File

@ -1,242 +1,245 @@
using Falcon.SugarApi; using System;
using Falcon.SugarApi.TimedBackgroundTask;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
/// <summary> namespace Falcon.SugarApi.TimedBackgroundTask
/// Cron表达式
/// </summary>
public class CronExpression
{ {
/// <summary>
/// 可选的秒范围枚举
/// </summary>
private List<int> Seconds { get; }
/// <summary>
/// 可选的分范围枚举
/// </summary>
private List<int> Minutes { get; }
/// <summary>
/// 可选的时范围枚举
/// </summary>
private List<int> Hours { get; }
/// <summary>
/// 可选的天范围枚举
/// </summary>
private List<int> DaysOfMonth { get; }
/// <summary>
/// 可选的月范围枚举
/// </summary>
private List<int> Months { get; }
/// <summary>
/// 可选的星期范围枚举
/// </summary>
private List<int> DaysOfWeek { get; }
/// <summary>
/// 可选的年范围枚举
/// </summary>
private List<int> Years { get; set; }
/// <summary> /// <summary>
/// 通过提供cron表达式构造对象 /// Cron表达式
/// <para>表达式以空格分隔,按顺序每一段分别为“秒,分,时,日,月,星期,年”</para>
/// <para>取值范围秒分时取值范围0-59日取值范围1-31月取值范围1-12星期取值范围0-6年取值范围两年内</para>
/// <para>取值*表示可以匹配任意值,比如*表示每一秒,* *表示所有分秒。</para>
/// <para>取值-表示取值范围比如1-5 3匹配3分1秒到3分5秒所有时间。</para>
/// <para>取值/表示周期取值比如0/10表示0秒开始每10秒匹配一次。</para>
/// <para>取值可以用分割表示取值枚举比如0 10,20,30 5 表示匹配每天早上5点10分、5点20分和5点30分。</para>
/// <para>表达式右侧的*可以省略。比如0 15 4表示每天上午4点15分和0 15 4 * * * *相同</para>
/// <para>星期取值范围0-60表示星期天1表示星期一以此类推。</para>
/// <para>日取值范围1-31不用考虑大月小月和二月的特殊情况方法会自动处理这些特殊日期</para>
/// <para>关于日期和星期匹配问题。一般的cron标准中日期和星期只要满足一个即可匹配但本方法不同我认为既然其他所有部分(比如分和秒)都是必须同时满足,那么日期和星期也是必须同时满足。</para>
/// <para>目前方法不支持*-/,分隔符的混合使用,等待后续改进</para>
/// </summary> /// </summary>
/// <param name="cronExpression">cron表达式</param> public class CronExpression
/// <exception cref="ArgumentException"></exception> {
public CronExpression(string cronExpression) { /// <summary>
var fields = cronExpression.Split(new[] { ' ' },StringSplitOptions.RemoveEmptyEntries); /// 可选的秒范围枚举
/// </summary>
private List<int> Seconds { get; }
/// <summary>
/// 可选的分范围枚举
/// </summary>
private List<int> Minutes { get; }
/// <summary>
/// 可选的时范围枚举
/// </summary>
private List<int> Hours { get; }
/// <summary>
/// 可选的天范围枚举
/// </summary>
private List<int> DaysOfMonth { get; }
/// <summary>
/// 可选的月范围枚举
/// </summary>
private List<int> Months { get; }
/// <summary>
/// 可选的星期范围枚举
/// </summary>
private List<int> DaysOfWeek { get; }
/// <summary>
/// 可选的年范围枚举
/// </summary>
private List<int> Years { get; set; }
Seconds = fields.Length > 0 ? GetRange(fields[0],0,59) : Enumerable.Range(0,60).ToList(); /// <summary>
Minutes = fields.Length > 1 ? GetRange(fields[1],0,59) : Enumerable.Range(0,60).ToList(); /// 通过提供cron表达式构造对象
Hours = fields.Length > 2 ? GetRange(fields[2],0,23) : Enumerable.Range(0,24).ToList(); /// <para>表达式以空格分隔,按顺序每一段分别为“秒,分,时,日,月,星期,年”</para>
DaysOfMonth = fields.Length > 3 ? GetRange(fields[3],1,31) : Enumerable.Range(1,31).ToList(); /// <para>取值范围秒分时取值范围0-59日取值范围1-31月取值范围1-12星期取值范围0-6年取值范围两年内</para>
Months = fields.Length > 4 ? GetRange(fields[4],1,12) : Enumerable.Range(1,12).ToList(); /// <para>取值*表示可以匹配任意值,比如*表示每一秒,* *表示所有分秒。</para>
DaysOfWeek = fields.Length > 5 ? GetRange(fields[5],0,6) : Enumerable.Range(0,7).ToList(); /// <para>取值-表示取值范围比如1-5 3匹配3分1秒到3分5秒所有时间。</para>
var nowYear = DateTime.Now.Year; /// <para>取值/表示周期取值比如0/10表示0秒开始每10秒匹配一次。</para>
Years = fields.Length > 6 ? GetRange(fields[6],nowYear,nowYear + 2) : Enumerable.Range(nowYear,2).ToList(); /// <para>取值可以用分割表示取值枚举比如0 10,20,30 5 表示匹配每天早上5点10分、5点20分和5点30分。</para>
} /// <para>表达式右侧的*可以省略。比如0 15 4表示每天上午4点15分和0 15 4 * * * *相同</para>
/// <para>星期取值范围0-60表示星期天1表示星期一以此类推。</para>
/// <para>日取值范围1-31不用考虑大月小月和二月的特殊情况方法会自动处理这些特殊日期</para>
/// <para>关于日期和星期匹配问题。一般的cron标准中日期和星期只要满足一个即可匹配但本方法不同我认为既然其他所有部分(比如分和秒)都是必须同时满足,那么日期和星期也是必须同时满足。</para>
/// <para>目前方法不支持*-/,分隔符的混合使用,等待后续改进</para>
/// </summary>
/// <param name="cronExpression">cron表达式</param>
/// <exception cref="ArgumentException"></exception>
public CronExpression(string cronExpression) {
var fields = cronExpression.Split(new[] { ' ' },StringSplitOptions.RemoveEmptyEntries);
/// <summary> 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();
/// </summary> Hours = fields.Length > 2 ? GetRange(fields[2],0,23) : Enumerable.Range(0,24).ToList();
/// <param name="afterTime">上一次的时间</param> DaysOfMonth = fields.Length > 3 ? GetRange(fields[3],1,31) : Enumerable.Range(1,31).ToList();
/// <returns>下一次到达时间</returns> Months = fields.Length > 4 ? GetRange(fields[4],1,12) : Enumerable.Range(1,12).ToList();
public DateTime GetNextOccurrence(DateTime afterTime) { DaysOfWeek = fields.Length > 5 ? GetRange(fields[5],0,6) : Enumerable.Range(0,7).ToList();
var ct = new CronResult(afterTime.AddSeconds(1)); var nowYear = DateTime.Now.Year;
while(!ct.IsAllAdjust) { Years = fields.Length > 6 ? GetRange(fields[6],nowYear,nowYear + 2) : Enumerable.Range(nowYear,2).ToList();
if(!ct.IsYearAdjust) { }
AdjustYear(ct);
} /// <summary>
if(!ct.IsMonthAdjust) { /// 通过提供的时间获取下一次时间
AdjustMonth(ct); /// </summary>
/// <param name="afterTime">上一次的时间</param>
/// <returns>下一次到达时间</returns>
public DateTime GetNextOccurrence(DateTime afterTime) {
var ct = new CronResult(afterTime.AddSeconds(1));
while(!ct.IsAllAdjust) {
if(!ct.IsYearAdjust) { 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) { return ct.AdjustTime;
AdjustDay(ct); }
if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust) {
continue; 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<int> 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<int>();
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) { if(exp.Contains('/')) {
AdjustHour(ct); var g = exp.Split('/',StringSplitOptions.RemoveEmptyEntries);
if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust) { var f = Math.Max(min,int.Parse(g[0]));
continue; var s = int.Parse(g[1]);
while(f <= max) {
list.Add(f);
f += s;
} }
return list;
} }
if(!ct.IsMinuteAdjust) { if(exp.Contains(',')) {
AdjustMinute(ct); foreach(var ie in exp.Split(',')) {
if(!ct.IsYearAdjust || !ct.IsMonthAdjust || !ct.IsDayAdjust || !ct.IsHourAdjust) { if(int.TryParse(ie,out int iei) && iei.Between(min,max)) {
continue; list.Add(iei);
}
else {
throw new ArgumentException("给定的exp表达式错误逗号分割的每一项都必须是数字",nameof(exp));
}
} }
return list;
} }
if(!ct.IsSecondAdjust) { throw new ArgumentException("提供的cron表达式错误",nameof(exp));
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<int> 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<int>();
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));
}
} }