using System; using System.Collections.Generic; using System.Globalization; namespace FileInterface.Scheduler { /// /// 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. /// public sealed class CronExpression { private readonly HashSet _minutes; private readonly HashSet _hours; private readonly HashSet _days; private readonly HashSet _months; private readonly HashSet _daysOfWeek; public string Expression { get; private set; } private CronExpression(string expression, HashSet minutes, HashSet hours, HashSet days, HashSet months, HashSet 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 ParseField(string text, int min, int max, bool normalizeSunday) { var result = new HashSet(); 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; } } }