132 lines
4.9 KiB
C#
132 lines
4.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
|
|
namespace FileInterface.Scheduler
|
|
{
|
|
/// <summary>
|
|
/// Standard 5-field cron expression: minute hour day-of-month month day-of-week.
|
|
/// Supported: *, */n, a,b,c, a-b, a-b/n. Day-of-week: 0 or 7 = Sunday.
|
|
/// </summary>
|
|
public sealed class CronExpression
|
|
{
|
|
private readonly HashSet<int> _minutes;
|
|
private readonly HashSet<int> _hours;
|
|
private readonly HashSet<int> _days;
|
|
private readonly HashSet<int> _months;
|
|
private readonly HashSet<int> _daysOfWeek;
|
|
|
|
public string Expression { get; private set; }
|
|
|
|
private CronExpression(string expression,
|
|
HashSet<int> minutes,
|
|
HashSet<int> hours,
|
|
HashSet<int> days,
|
|
HashSet<int> months,
|
|
HashSet<int> daysOfWeek)
|
|
{
|
|
Expression = expression;
|
|
_minutes = minutes;
|
|
_hours = hours;
|
|
_days = days;
|
|
_months = months;
|
|
_daysOfWeek = daysOfWeek;
|
|
}
|
|
|
|
public static CronExpression Parse(string expression)
|
|
{
|
|
if (String.IsNullOrWhiteSpace(expression))
|
|
throw new FormatException("Cron expression is empty.");
|
|
|
|
string[] parts = expression.Trim().Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
if (parts.Length != 5)
|
|
throw new FormatException("Cron must contain exactly 5 fields: minute hour day month day-of-week.");
|
|
|
|
return new CronExpression(
|
|
expression.Trim(),
|
|
ParseField(parts[0], 0, 59, false),
|
|
ParseField(parts[1], 0, 23, false),
|
|
ParseField(parts[2], 1, 31, false),
|
|
ParseField(parts[3], 1, 12, false),
|
|
ParseField(parts[4], 0, 7, true));
|
|
}
|
|
|
|
public bool IsMatch(DateTime localTime)
|
|
{
|
|
int dow = (int)localTime.DayOfWeek;
|
|
return _minutes.Contains(localTime.Minute)
|
|
&& _hours.Contains(localTime.Hour)
|
|
&& _days.Contains(localTime.Day)
|
|
&& _months.Contains(localTime.Month)
|
|
&& _daysOfWeek.Contains(dow);
|
|
}
|
|
|
|
private static HashSet<int> ParseField(string text, int min, int max, bool normalizeSunday)
|
|
{
|
|
var result = new HashSet<int>();
|
|
string[] segments = text.Split(',');
|
|
foreach (string rawSegment in segments)
|
|
{
|
|
string segment = rawSegment.Trim();
|
|
if (segment.Length == 0)
|
|
throw new FormatException("Empty cron field segment.");
|
|
|
|
int step = 1;
|
|
string rangePart = segment;
|
|
int slash = segment.IndexOf('/');
|
|
if (slash >= 0)
|
|
{
|
|
rangePart = segment.Substring(0, slash);
|
|
string stepText = segment.Substring(slash + 1);
|
|
if (!Int32.TryParse(stepText, NumberStyles.Integer, CultureInfo.InvariantCulture, out step) || step <= 0)
|
|
throw new FormatException("Invalid cron step: " + segment);
|
|
}
|
|
|
|
int start;
|
|
int end;
|
|
if (rangePart == "*")
|
|
{
|
|
start = min;
|
|
end = max;
|
|
}
|
|
else
|
|
{
|
|
int dash = rangePart.IndexOf('-');
|
|
if (dash >= 0)
|
|
{
|
|
start = ParseNumber(rangePart.Substring(0, dash), min, max);
|
|
end = ParseNumber(rangePart.Substring(dash + 1), min, max);
|
|
if (start > end)
|
|
throw new FormatException("Cron range start is greater than end: " + segment);
|
|
}
|
|
else
|
|
{
|
|
start = ParseNumber(rangePart, min, max);
|
|
end = start;
|
|
}
|
|
}
|
|
|
|
for (int value = start; value <= end; value += step)
|
|
{
|
|
int normalized = normalizeSunday && value == 7 ? 0 : value;
|
|
result.Add(normalized);
|
|
}
|
|
}
|
|
|
|
if (result.Count == 0)
|
|
throw new FormatException("Cron field produced no values: " + text);
|
|
return result;
|
|
}
|
|
|
|
private static int ParseNumber(string text, int min, int max)
|
|
{
|
|
int value;
|
|
if (!Int32.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
|
|
throw new FormatException("Invalid cron number: " + text);
|
|
if (value < min || value > max)
|
|
throw new FormatException("Cron value out of range: " + value + " (allowed " + min + "-" + max + ")");
|
|
return value;
|
|
}
|
|
}
|
|
}
|