Files
2026-08-19 19:27:41 +02:00

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;
}
}
}