Files
KinginfoGateway/src/Admin/ThingsGateway.Furion/TimeCrontab/Crontab.Internal.cs
2025-05-14 18:52:19 +08:00

898 lines
38 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ------------------------------------------------------------------------
// 版权信息
// 版权归百小僧及百签科技(广东)有限公司所有。
// 所有权利保留。
// 官方网站https://baiqian.com
//
// 许可证信息
// 项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。
// 许可证的完整文本可以在源代码树根目录中的 LICENSE-APACHE 和 LICENSE-MIT 文件中找到。
// ------------------------------------------------------------------------
namespace ThingsGateway.TimeCrontab;
/// <summary>
/// Cron 表达式抽象类
/// </summary>
/// <remarks>主要将 Cron 表达式转换成 OOP 类进行操作</remarks>
public partial class Crontab
{
/// <summary>
/// 解析 Cron 表达式字段并存储其 所有发生值 字符解析器
/// </summary>
/// <param name="expression">Cron 表达式</param>
/// <param name="format">Cron 表达式格式化类型</param>
/// <returns><see cref="Dictionary{TKey, TValue}"/></returns>
/// <exception cref="TimeCrontabException"></exception>
private static Dictionary<CrontabFieldKind, List<ICronParser>> ParseToDictionary(string expression, CronStringFormat format)
{
// Cron 表达式空检查
if (string.IsNullOrWhiteSpace(expression))
{
throw new TimeCrontabException("The provided cron string is null, empty or contains only whitespace.");
}
// 通过空白符切割 Cron 表达式每个字段域
var instructions = expression.Split(separator, StringSplitOptions.RemoveEmptyEntries);
// 验证当前 Cron 格式化类型字段数量和表达式字段数量是否一致
var expectedCount = Constants.ExpectedFieldCounts[format];
if (instructions.Length > expectedCount)
{
throw new TimeCrontabException(string.Format("The provided cron string <{0}> has too many parameters.", expression));
}
if (instructions.Length < expectedCount)
{
throw new TimeCrontabException(string.Format("The provided cron string <{0}> has too few parameters.", expression));
}
// 初始化字段偏移量和字段字符解析器
var defaultFieldOffset = 0;
var fieldParsers = new Dictionary<CrontabFieldKind, List<ICronParser>>();
// 判断当前 Cron 格式化类型是否包含秒字段域,如果包含则优先解析秒字段域字符解析器
if (format == CronStringFormat.WithSeconds || format == CronStringFormat.WithSecondsAndYears)
{
fieldParsers.Add(CrontabFieldKind.Second, ParseField(instructions[0], CrontabFieldKind.Second));
defaultFieldOffset = 1;
}
// Cron 常规字段域
fieldParsers.Add(CrontabFieldKind.Minute, ParseField(instructions[defaultFieldOffset + 0], CrontabFieldKind.Minute)); // 偏移量 1
fieldParsers.Add(CrontabFieldKind.Hour, ParseField(instructions[defaultFieldOffset + 1], CrontabFieldKind.Hour)); // 偏移量 2
fieldParsers.Add(CrontabFieldKind.Day, ParseField(instructions[defaultFieldOffset + 2], CrontabFieldKind.Day)); // 偏移量 3
fieldParsers.Add(CrontabFieldKind.Month, ParseField(instructions[defaultFieldOffset + 3], CrontabFieldKind.Month)); // 偏移量 4
fieldParsers.Add(CrontabFieldKind.DayOfWeek, ParseField(instructions[defaultFieldOffset + 4], CrontabFieldKind.DayOfWeek)); // 偏移量 5
// 判断当前 Cron 格式化类型是否包含年字段域,如果包含则解析年字段域字符解析器
if (format == CronStringFormat.WithYears || format == CronStringFormat.WithSecondsAndYears)
{
fieldParsers.Add(CrontabFieldKind.Year, ParseField(instructions[defaultFieldOffset + 5], CrontabFieldKind.Year)); // 偏移量 6
}
// 检查非法字符解析器,如 2 月没有 30 和 31 号
CheckForIllegalParsers(fieldParsers);
return fieldParsers;
}
private static readonly char[] separator = new[] { ' ' };
/// <summary>
/// 解析 Cron 单个字段域所有发生值 字符解析器
/// </summary>
/// <param name="field">字段值</param>
/// <param name="kind">Cron 表达式格式化类型</param>
/// <returns><see cref="List{T}"/></returns>
/// <exception cref="TimeCrontabException"></exception>
private static List<ICronParser> ParseField(string field, CrontabFieldKind kind)
{
/*
* 在 Cron 表达式中,单个字段域值也支持定义多个值(我们称为值中值),如 1,2,3 或 SUN,FRI,SAT
* 所以,这里需要将字段域值通过 , 进行切割后独立处理
*/
try
{
return field.Split(',').Select(parser => ParseParser(parser, kind)).ToList();
}
catch (Exception ex)
{
throw new TimeCrontabException(
string.Format("There was an error parsing '{0}' for the {1} field.", field, Enum.GetName(typeof(CrontabFieldKind), kind))
, ex);
}
}
/// <summary>
/// 解析 Cron 字段域值中值
/// </summary>
/// <param name="parser">字段值中值</param>
/// <param name="kind">Cron 表达式格式化类型</param>
/// <returns><see cref="ICronParser"/></returns>
/// <exception cref="TimeCrontabException"></exception>
private static ICronParser ParseParser(string parser, CrontabFieldKind kind)
{
// Cron 字段中所有字母均采用大写方式,所以需要转换所有为大写再操作
var newParser = parser.ToUpperInvariant();
try
{
// 判断值是否以 * 字符开头
if (newParser.StartsWith('*'))
{
// 继续往后解析
newParser = newParser[1..];
// 判断是否以 / 字符开头,如果是,则该值为带步长的 Cron 值
if (newParser.StartsWith('/'))
{
// 继续往后解析
newParser = newParser[1..];
// 解析 Cron 值步长并创建 StepParser 解析器
var steps = GetValue(ref newParser, kind);
return new StepParser(0, steps, kind);
}
// 处理 * 携带意外值
if (!string.IsNullOrEmpty(newParser))
{
throw new TimeCrontabException(string.Format("Invalid parser '{0}'.", parser));
}
// 否则,创建 AnyParser 解析器
return new AnyParser(kind);
}
// 判断值是否以 L 字符开头
if (newParser.StartsWith('L') && kind == CrontabFieldKind.Day)
{
// 继续往后解析
newParser = newParser[1..];
// 是否是 LW 字符,如果是,创建 LastWeekdayOfMonthParser 解析器
if (newParser == "W")
{
return new LastWeekdayOfMonthParser(kind);
}
// 否则创建 LastDayOfMonthParser 解析器
else
{
return new LastDayOfMonthParser(kind);
}
}
// 判断值是否等于 R
if (newParser == "R")
{
return new RandomParser(kind);
}
// 判断值是否等于 ?
if (newParser == "?")
{
// 创建 BlankDayOfMonthOrWeekParser 解析器
return new BlankDayOfMonthOrWeekParser(kind);
}
/*
* 如果上面均不匹配那么该值类似取值有21/21-101-10/2SUNSUNDAYSUNLJAN3W3L2#5 等
*/
// 继续推进解析
var firstValue = GetValue(ref newParser, kind);
// 如果没有返回新的待解析字符,则认为这是一个具体值
if (string.IsNullOrEmpty(newParser))
{
// 对年份进行特别处理
if (kind == CrontabFieldKind.Year)
{
return new SpecificYearParser(firstValue, kind);
}
else
{
// 创建 SpecificParser 解析器
return new SpecificParser(firstValue, kind);
}
}
// 如果存在待解析字符,如 - / # L W 值,则进一步解析
switch (newParser[0])
{
// 判断值是否以 / 字符开头
case '/':
{
// 继续往后解析
newParser = newParser[1..];
// 解析 Cron 值步长并创建 StepParser 解析器
var steps = GetValue(ref newParser, kind);
return new StepParser(firstValue, steps, kind);
}
// 判断值是否以 - 字符开头
case '-':
{
// 继续往后解析
newParser = newParser[1..];
// 获取范围结束值
var endValue = GetValue(ref newParser, kind);
int? steps = null;
// 继续推进解析,判断是否以 / 开头,如果是,则获取步长
if (newParser.StartsWith('/'))
{
newParser = newParser[1..];
steps = GetValue(ref newParser, kind);
}
// 创建 RangeParser 解析器
return new RangeParser(firstValue, endValue, steps, kind);
}
// 判断值是否以 # 字符开头
case '#':
{
// 继续往后解析
newParser = newParser[1..];
// 获取第几个
var weekNumber = GetValue(ref newParser, kind);
// 继续推进解析,如果存在其他字符,则抛异常
if (!string.IsNullOrEmpty(newParser))
{
throw new TimeCrontabException(string.Format("Invalid parser '{0}.'", parser));
}
// 创建 SpecificDayOfWeekInMonthParser 解析器
return new SpecificDayOfWeekInMonthParser(firstValue, weekNumber, kind);
}
// 判断解析值是否等于 L 或 W
default:
// 创建 LastDayOfWeekInMonthParser 解析器
if (newParser == "L" && kind == CrontabFieldKind.DayOfWeek)
{
return new LastDayOfWeekInMonthParser(firstValue, kind);
}
// 创建 NearestWeekdayParser 解析器
else if (newParser == "W" && kind == CrontabFieldKind.Day)
{
return new NearestWeekdayParser(firstValue, kind);
}
break;
}
throw new TimeCrontabException(string.Format("Invalid parser '{0}'.", parser));
}
catch (Exception ex)
{
throw new TimeCrontabException(string.Format("Invalid parser '{0}'. See inner exception for details.", parser), ex);
}
}
/// <summary>
/// 将 Cron 字段值中值进一步解析
/// </summary>
/// <param name="parser">当前解析值</param>
/// <param name="kind">Cron 表达式格式化类型</param>
/// <returns><see cref="int"/></returns>
/// <exception cref="TimeCrontabException"></exception>
private static int GetValue(ref string parser, CrontabFieldKind kind)
{
// 值空检查
if (string.IsNullOrEmpty(parser))
{
throw new TimeCrontabException("Expected number, but parser was empty.");
}
// 字符偏移量
int offset;
// 判断首个字符是数字还是字符串
var isDigit = char.IsDigit(parser[0]);
var isLetter = char.IsLetter(parser[0]);
// 推进式遍历值并检查每一个字符,一旦出现类型不连贯则停止检查
for (offset = 0; offset < parser.Length; offset++)
{
// 如果存在不连贯数字或字母则跳出循环
if ((isDigit && !char.IsDigit(parser[offset])) || (isLetter && !char.IsLetter(parser[offset])))
{
break;
}
}
var maximum = Constants.MaximumDateTimeValues[kind];
// 前面连贯类型的值
var valueToParse = parser[..offset];
// 处理数字开头的连贯类型值
if (int.TryParse(valueToParse, out var value))
{
// 导出下一轮待解析的值(依旧采用推进式)
parser = parser[offset..];
var returnValue = value;
// 验证值范围
if (returnValue > maximum)
{
throw new TimeCrontabException(string.Format("Value for {0} parser exceeded maximum value of {1}.", Enum.GetName(typeof(CrontabFieldKind), kind), maximum));
}
return returnValue;
}
// 处理字母开头的连贯类型值通常认为这是一个单词如SUNJAN
else
{
List<KeyValuePair<string, int>> replaceVal = null;
// 判断当前 Cron 字段类型是否是星期,如果是,则查找该单词是否在 Constants.Days 定义之中
if (kind == CrontabFieldKind.DayOfWeek)
{
replaceVal = Constants.Days.Where(x => valueToParse.StartsWith(x.Key)).ToList();
}
// 判断当前 Cron 字段类型是否是月份,如果是,则查找该单词是否在 Constants.Months 定义之中
else if (kind == CrontabFieldKind.Month)
{
replaceVal = Constants.Months.Where(x => valueToParse.StartsWith(x.Key)).ToList();
}
// 如果存在且唯一,则进入下一轮判断
// 接下来的判断是处理 SUN + L 的情况,如 SUNL == 0L == SUNDAY它们都是合法的 Cron 值
if (replaceVal != null && replaceVal.Count == 1)
{
var missingParser = "";
// 处理带 L 和不带 L 的单词问题
if (parser.Length == offset
&& parser.EndsWith('L')
&& kind == CrontabFieldKind.DayOfWeek)
{
missingParser = "L";
}
parser = parser[offset..] + missingParser;
// 转换成 int 值返回SUNJAN.....
var returnValue = replaceVal.First().Value;
// 验证值范围
if (returnValue > maximum)
{
throw new TimeCrontabException(string.Format("Value for {0} parser exceeded maximum value of {1}.", Enum.GetName(typeof(CrontabFieldKind), kind), maximum));
}
return returnValue;
}
}
throw new TimeCrontabException("Parser does not contain expected number.");
}
/// <summary>
/// 检查非法字符解析器,如 2 月没有 30 和 31 号
/// </summary>
/// <remarks>检查 2 月份是否存在 30 和 31 天的非法数值解析器</remarks>
/// <param name="parsers">Cron 字段解析器字典集合</param>
/// <exception cref="TimeCrontabException"></exception>
private static void CheckForIllegalParsers(Dictionary<CrontabFieldKind, List<ICronParser>> parsers)
{
// 获取当前 Cron 表达式月字段和天字段所有数值
var monthSingle = GetSpecificParsers(parsers, CrontabFieldKind.Month);
var daySingle = GetSpecificParsers(parsers, CrontabFieldKind.Day);
// 如果月份为 2 月单天数出现 30 和 31 天,则是无效数值
if (monthSingle.Count != 0 && monthSingle.All(x => x.SpecificValue == 2))
{
if (daySingle.Count != 0 && daySingle.All(x => (x.SpecificValue == 30) || (x.SpecificValue == 31)))
{
throw new TimeCrontabException("The February 30 and 31 don't exist.");
}
}
}
/// <summary>
/// 查找 Cron 字段类型所有具体值解析器
/// </summary>
/// <param name="parsers">Cron 字段解析器字典集合</param>
/// <param name="kind">Cron 字段种类</param>
/// <returns><see cref="List{T}"/></returns>
private static List<SpecificParser> GetSpecificParsers(Dictionary<CrontabFieldKind, List<ICronParser>> parsers, CrontabFieldKind kind)
{
var kindParsers = parsers[kind];
// 查找 Cron 字段类型所有具体值解析器
return kindParsers.Where(x => x.GetType() == typeof(SpecificParser)).Cast<SpecificParser>()
.Union(
kindParsers.Where(x => x.GetType() == typeof(RangeParser)).SelectMany(x => ((RangeParser)x).SpecificParsers)
).Union(
kindParsers.Where(x => x.GetType() == typeof(StepParser)).SelectMany(x => ((StepParser)x).SpecificParsers)
).ToList();
}
/// <summary>
/// 获取特定时间范围下一个发生时间
/// </summary>
/// <param name="baseTime">起始时间</param>
/// <param name="endTime">结束时间</param>
/// <returns><see cref="DateTime"/></returns>
private DateTime InternalGetNextOccurence(DateTime baseTime, DateTime endTime)
{
// 判断当前 Cron 格式化类型是否支持秒
var isSecondFormat = Format == CronStringFormat.WithSeconds || Format == CronStringFormat.WithSecondsAndYears;
// 由于 Cron 格式化类型不包含毫秒,则裁剪掉毫秒部分
var newValue = baseTime;
newValue = newValue.AddMilliseconds(-newValue.Millisecond);
// 如果当前 Cron 格式化类型不支持秒,则裁剪掉秒部分
if (!isSecondFormat)
{
newValue = newValue.AddSeconds(-newValue.Second);
}
// 初始化是否存在随机 R 标识符
var randomSecond = false;
var randomMinute = false;
var randomHour = false;
// 获取分钟、小时所有字符解析器
var minuteParsers = Parsers[CrontabFieldKind.Minute].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
randomMinute = minuteParsers.OfType<RandomParser>().Any();
var hourParsers = Parsers[CrontabFieldKind.Hour].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
randomHour = hourParsers.OfType<RandomParser>().Any();
// 获取秒、分钟、小时解析器中最小起始值
// 该值主要用来获取下一个发生值的输入参数
var firstSecondValue = newValue.Second;
var firstMinuteValue = minuteParsers.Select(x => x.First()).Min();
var firstHourValue = hourParsers.Select(x => x.First()).Min();
// 定义一个标识,标识当前时间下一个发生时间值是否进入新一轮循环
// 如:如果当前时间的秒数为 59那么下一个秒数应该为 00那么当前时间分钟就应该 +1
// 以此类推,如果 +1 后分钟数为 59那么下一个分钟数也应该为 00那么当前时间小时数就应该 +1
// ....
var overflow = true;
// 处理 Cron 格式化类型包含秒的情况 =================================================================
var newSeconds = newValue.Second;
if (isSecondFormat)
{
// 获取秒所有字符解析器
var secondParsers = Parsers[CrontabFieldKind.Second].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
randomSecond = secondParsers.OfType<RandomParser>().Any();
// 获取秒解析器最小起始值
firstSecondValue = secondParsers.Select(x => x.First()).Min();
// 获取秒下一个发生值
newSeconds = Increment(secondParsers, newValue.Second, firstSecondValue, out overflow);
// 设置起始时间为下一个秒时间
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newValue.Hour, newValue.Minute, newSeconds);
// 如果当前秒并没有进入下一轮循环但存在不匹配的字符过滤器
if (!overflow && !IsMatch(newValue))
{
// 重置秒为起始值并标记 overflow 为 true 进入新一轮循环
newSeconds = firstSecondValue;
// 此时计算时间秒部分应该为起始值
// 如 22:10:59 -> 22:10:00
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newValue.Hour, newValue.Minute, newSeconds);
// 标记进入下一轮循环
overflow = true;
}
// 如果程序到达这里,说明并没有进入上面分支,则直接返回下一秒时间
if (!overflow)
{
return MinDate(newValue, endTime);
}
}
// 程序到达这里,说明秒部分已经标识进入新一轮循环,那么分支就应该获取下一个分钟发生值 =================================================================
var newMinutes = Increment(minuteParsers, newValue.Minute + (overflow ? 0 : -1), firstMinuteValue, out overflow);
// 设置起始时间为下一个分钟时间
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newValue.Hour, newMinutes, overflow ? firstSecondValue : newSeconds);
// 如果当前分钟并没有进入下一轮循环但存在不匹配的字符过滤器
if (!overflow && !IsMatch(newValue))
{
// 重置秒,分钟为起始值并标记 overflow 为 true 进入新一轮循环
newSeconds = firstSecondValue;
newMinutes = firstMinuteValue;
// 此时计算时间秒和分钟部分应该为起始值
// 如 22:59:59 -> 22:00:00
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newValue.Hour, newMinutes, firstSecondValue);
// 标记进入下一轮循环
overflow = true;
}
// 如果程序到达这里,说明并没有进入上面分支,则直接返回下一分钟时间
if (!overflow)
{
return MinDate(newValue, endTime);
}
// 程序到达这里,说明分钟部分已经标识进入新一轮循环,那么分支就应该获取下一个小时发生值 =================================================================
var newHours = Increment(hourParsers, newValue.Hour + (overflow ? 0 : -1), firstHourValue, out overflow);
// 设置起始时间为下一个小时时间
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newHours,
overflow && !randomMinute ? firstMinuteValue : newMinutes,
overflow && !randomSecond ? firstSecondValue : newSeconds);
// 如果当前小时并没有进入下一轮循环但存在不匹配的字符过滤器
if (!overflow && !IsMatch(newValue))
{
// 此时计算时间秒,分钟和小时部分应该为起始值
// 如 23:59:59 -> 23:00:00
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, firstHourValue, firstMinuteValue, firstSecondValue);
// 标记进入下一轮循环
overflow = true;
}
// 如果程序到达这里,说明并没有进入上面分支,则直接返回下一小时时间
if (!randomHour && !overflow)
{
return MinDate(newValue, endTime);
}
// 如果程序达到这里,说明天数变了(一旦天数变了,那么月份可能也变了,星期可能也变了,年份也可能变了)
// 所以这里的计算最为复杂
List<ITimeParser> yearParsers = null;
// 首先先判断当前 Cron 格式化类型是否支持年份
var isYearFormat = Format == CronStringFormat.WithYears || Format == CronStringFormat.WithSecondsAndYears;
// 如果支持,读取年份字符过滤器
if (isYearFormat)
{
yearParsers = Parsers[CrontabFieldKind.Year].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
}
// 程序能够执行到这里,那么说明时间已经是 23:59:59所以起始时间追加 1 天
// 这里的代码看起来很奇怪,但是是为了处理终止时间为 12/31/9999 23:59:59.999 的情况,也就是世界末日了~~~
try
{
newValue = newValue.AddDays(1);
}
catch
{
return endTime;
}
// 在有效的年份时间内死循环至天、周、月、年全部匹配才终止循环
while (!(IsMatch(newValue, CrontabFieldKind.Day)
&& IsMatch(newValue, CrontabFieldKind.DayOfWeek)
&& IsMatch(newValue, CrontabFieldKind.Month)
&& (!isYearFormat || IsMatch(newValue, CrontabFieldKind.Year))))
{
// 如果当前匹配到的时间已经大于或等于终止时间,则直接返回
if (newValue >= endTime)
{
return MinDate(newValue, endTime);
}
// 如果 Cron 年份字段域获取下一个发生值为 null那么直接返回 终止时间
// 也就是已经没有匹配项了
if (isYearFormat && yearParsers!.Select(x => x.Next(newValue.Year - 1)).All(x => x == null))
{
return endTime;
}
// 同样防止终止时间为 12/31/9999 23:59:59.999 的情况
try
{
// 不断增加 1 天直至匹配成功
newValue = newValue.AddDays(1);
}
catch
{
return endTime;
}
}
return MinDate(newValue, endTime);
}
/// <summary>
/// 获取特定时间范围上一个发生时间
/// </summary>
/// <param name="baseTime">起始时间</param>
/// <param name="endTime">结束时间</param>
/// <returns><see cref="DateTime"/></returns>
private DateTime InternalGetPreviousOccurence(DateTime baseTime, DateTime endTime)
{
// 判断当前 Cron 格式化类型是否支持秒
var isSecondFormat = Format == CronStringFormat.WithSeconds || Format == CronStringFormat.WithSecondsAndYears;
// 由于 Cron 格式化类型不包含毫秒,则裁剪掉毫秒部分
var newValue = baseTime;
newValue = newValue.AddMilliseconds(-newValue.Millisecond);
// 如果当前 Cron 格式化类型不支持秒,则裁剪掉秒部分
if (!isSecondFormat)
{
newValue = newValue.AddSeconds(-newValue.Second);
}
// 获取分钟、小时所有字符解析器
var minuteParsers = Parsers[CrontabFieldKind.Minute].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
var hourParsers = Parsers[CrontabFieldKind.Hour].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
// 获取秒、分钟、小时解析器中最小起始值
// 该值主要用来获取上一个发生值的输入参数
var lastSecondValue = newValue.Second;
var lastMinuteValue = minuteParsers.Select(x => x.Last()).Max();
var lastHourValue = hourParsers.Select(x => x.Last()).Max();
// 定义一个标识,标识当前时间山歌一个发生时间值是否进入新一轮循环
// 如:如果当前时间的秒数为 00那么上一个秒数应该为 59那么当前时间分钟就应该 -1
// 以此类推,如果 -1 后分钟数为 00那么上一个分钟数也应该为 59那么当前时间小时数就应该 -1
// ....
var overflow = true;
// 处理 Cron 格式化类型包含秒的情况 =================================================================
var newSeconds = newValue.Second;
if (isSecondFormat)
{
// 获取秒所有字符解析器
var secondParsers = Parsers[CrontabFieldKind.Second].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
// 获取秒解析器最大末尾值
lastSecondValue = secondParsers.Select(x => x.Last()).Max();
// 获取秒上一个发生值
newSeconds = Decrement(secondParsers, newValue.Second, lastSecondValue, out overflow);
// 设置起始时间为上一个秒时间
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newValue.Hour, newValue.Minute, newSeconds);
// 如果当前秒并没有进入下一轮循环但存在不匹配的字符过滤器
if (!overflow && !IsMatch(newValue))
{
// 重置秒为起始值并标记 overflow 为 true 进入新一轮循环
newSeconds = lastSecondValue;
// 此时计算时间秒部分应该为末尾值
// 如 22:10:00 -> 22:09:59
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newValue.Hour, newValue.Minute, newSeconds);
// 标记进入下一轮循环
overflow = true;
}
// 如果程序到达这里,说明并没有进入上面分支,则直接返回上一秒时间
if (!overflow)
{
return MaxDate(newValue, endTime);
}
}
// 程序到达这里,说明秒部分已经标识进入新一轮循环,那么分支就应该获取上一个分钟发生值 =================================================================
var newMinutes = Decrement(minuteParsers, newValue.Minute + (overflow ? 0 : 1), lastMinuteValue, out overflow);
// 设置起始时间为上一个分钟时间
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newValue.Hour, newMinutes, overflow ? lastSecondValue : newSeconds);
// 如果当前分钟并没有进入下一轮循环但存在不匹配的字符过滤器
if (!overflow && !IsMatch(newValue))
{
// 重置秒,分钟为起始值并标记 overflow 为 true 进入新一轮循环
newSeconds = lastSecondValue;
newMinutes = lastMinuteValue;
// 此时计算时间秒和分钟部分应该为起始值
// 如 22:00:00 -> 21:59:59
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newValue.Hour, newMinutes, lastSecondValue);
// 标记进入下一轮循环
overflow = true;
}
// 如果程序到达这里,说明并没有进入上面分支,则直接返回上一分钟时间
if (!overflow)
{
return MaxDate(newValue, endTime);
}
// 程序到达这里,说明分钟部分已经标识进入新一轮循环,那么分支就应该获取下一个小时发生值 =================================================================
var newHours = Decrement(hourParsers, newValue.Hour + (overflow ? 0 : 1), lastHourValue, out overflow);
// 设置起始时间为上一个小时时间
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, newHours,
overflow ? lastMinuteValue : newMinutes,
overflow ? lastSecondValue : newSeconds);
// 如果当前小时并没有进入下一轮循环但存在不匹配的字符过滤器
if (!overflow && !IsMatch(newValue))
{
// 此时计算时间秒,分钟和小时部分应该为末尾值
// 如 24:00:00 -> 23:59:59
newValue = new DateTime(newValue.Year, newValue.Month, newValue.Day, lastHourValue, lastMinuteValue, lastSecondValue);
// 标记进入下一轮循环
overflow = true;
}
// 如果程序到达这里,说明并没有进入上面分支,则直接返回下一小时时间
if (!overflow)
{
return MaxDate(newValue, endTime);
}
// 如果程序达到这里,说明天数变了(一旦天数变了,那么月份可能也变了,星期可能也变了,年份也可能变了)
// 所以这里的计算最为复杂
List<ITimeParser> yearParsers = null;
// 首先先判断当前 Cron 格式化类型是否支持年份
var isYearFormat = Format == CronStringFormat.WithYears || Format == CronStringFormat.WithSecondsAndYears;
// 如果支持,读取年份字符过滤器
if (isYearFormat)
{
yearParsers = Parsers[CrontabFieldKind.Year].Where(x => x is ITimeParser).Cast<ITimeParser>().ToList();
}
// 程序能够执行到这里,那么说明时间已经是 24:00:00所以起始时间减 1 天
// 这里的代码看起来很奇怪,但是是为了处理终止时间为 12/31/9999 23:59:59.999 的情况,也就是世界末日了~~~
try
{
newValue = newValue.AddDays(-1);
}
catch
{
return endTime;
}
// 在有效的年份时间内死循环至天、周、月、年全部匹配才终止循环
while (!(IsMatch(newValue, CrontabFieldKind.Day)
&& IsMatch(newValue, CrontabFieldKind.DayOfWeek)
&& IsMatch(newValue, CrontabFieldKind.Month)
&& (!isYearFormat || IsMatch(newValue, CrontabFieldKind.Year))))
{
// 如果当前匹配到的时间已经大于或等于终止时间,则直接返回
if (newValue <= endTime)
{
return MaxDate(newValue, endTime);
}
// 如果 Cron 年份字段域获取下一个发生值为 null那么直接返回 终止时间
// 也就是已经没有匹配项了
if (isYearFormat && yearParsers.Select(x => x.Previous(newValue.Year + 1)).All(x => x == null))
{
return endTime;
}
// 同样防止终止时间为 12/31/9999 23:59:59.999 的情况
try
{
newValue = newValue.AddDays(-1);
}
catch
{
return endTime;
}
}
return MaxDate(newValue, endTime);
}
/// <summary>
/// 获取当前时间解析器下一个发生值
/// </summary>
/// <param name="parsers">解析器</param>
/// <param name="value">当前值</param>
/// <param name="defaultValue">默认值</param>
/// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param>
/// <returns><see cref="int"/></returns>
private static int Increment(List<ITimeParser> parsers, int value, int defaultValue, out bool overflow)
{
// 检查是否是随机 R 字符解析器
if (parsers.Count == 1 && parsers.First() is RandomParser randomParser)
{
overflow = true;
return randomParser.Next(value).Value;
}
var nextValue = parsers.Select(x => x.Next(value))
.Where(x => x > value)
.Min()
?? defaultValue;
// 如果此时秒或分钟或23到达最大值则应该返回起始值
overflow = nextValue <= value;
return nextValue;
}
/// <summary>
/// 获取当前时间解析器上一个发生值
/// </summary>
/// <param name="parsers">解析器</param>
/// <param name="value">当前值</param>
/// <param name="defaultValue">默认值</param>
/// <param name="overflow">控制秒、分钟、小时到达59秒/分和23小时开关</param>
/// <returns><see cref="int"/></returns>
private static int Decrement(List<ITimeParser> parsers, int value, int defaultValue, out bool overflow)
{
var previousValue = parsers.Select(x => x.Previous(value))
.Where(x => x < value)
.Max()
?? defaultValue;
// 如果此时秒或分钟或00到达最小值则应该返回末尾值
overflow = previousValue >= value;
return previousValue;
}
/// <summary>
/// 处理下一个发生时间边界值
/// </summary>
/// <remarks>如果发生时间大于终止时间,则返回终止时间,否则返回发生时间</remarks>
/// <param name="newTime">下一个发生时间</param>
/// <param name="endTime">终止时间</param>
/// <returns><see cref="DateTime"/></returns>
private static DateTime MinDate(DateTime newTime, DateTime endTime)
{
return newTime >= endTime ? endTime : newTime;
}
/// <summary>
/// 处理上一个发生时间边界值
/// </summary>
/// <remarks>如果发生时间小于终止时间,则返回终止时间,否则返回发生时间</remarks>
/// <param name="newTime">下一个发生时间</param>
/// <param name="endTime">终止时间</param>
/// <returns><see cref="DateTime"/></returns>
private static DateTime MaxDate(DateTime newTime, DateTime endTime)
{
return newTime <= endTime ? endTime : newTime;
}
/// <summary>
/// 判断 Cron 所有字段字符解析器是否都能匹配当前时间各个部分
/// </summary>
/// <param name="datetime">当前时间</param>
/// <returns><see cref="bool"/></returns>
private bool IsMatch(DateTime datetime)
{
return Parsers.All(fieldKind =>
fieldKind.Value.Any(parser => parser.IsMatch(datetime))
);
}
/// <summary>
/// 判断当前 Cron 字段类型字符解析器和当前时间至少存在一种匹配
/// </summary>
/// <param name="datetime">当前时间</param>
/// <param name="kind">Cron 字段种类</param>
/// <returns></returns>
private bool IsMatch(DateTime datetime, CrontabFieldKind kind)
{
return Parsers.Where(x => x.Key == kind)
.SelectMany(x => x.Value)
.Any(parser => parser.IsMatch(datetime));
}
/// <summary>
/// 将 Cron 字段解析器转换成字符串
/// </summary>
/// <param name="paramList">Cron 字段字符串集合</param>
/// <param name="kind">Cron 字段种类</param>
private void JoinParsers(List<string> paramList, CrontabFieldKind kind)
{
paramList.Add(
string.Join(",", Parsers
.Where(x => x.Key == kind)
.SelectMany(x => x.Value.Select(y => y.ToString())).ToArray()
)
);
}
}