namespace ClawdDotNet.Core.Scheduling; /// /// Einfaches Cron-Parsing für 5-Felder-Ausdrücke: Minute Stunde Tag Monat Wochentag /// Unterstützt: Zahlen, Wildcards (*), Bereiche (1-5), Listen (1,3,5), Schritte (*/5) /// public sealed class CronExpression { private readonly HashSet _minutes; private readonly HashSet _hours; private readonly HashSet _daysOfMonth; private readonly HashSet _months; private readonly HashSet _daysOfWeek; private CronExpression( HashSet minutes, HashSet hours, HashSet daysOfMonth, HashSet months, HashSet daysOfWeek) { _minutes = minutes; _hours = hours; _daysOfMonth = daysOfMonth; _months = months; _daysOfWeek = daysOfWeek; } public static CronExpression Parse(string expression) { var parts = expression.Trim().Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 5) throw new FormatException($"Cron expression must have 5 fields, got {parts.Length}: '{expression}'"); return new CronExpression( ParseField(parts[0], 0, 59), ParseField(parts[1], 0, 23), ParseField(parts[2], 1, 31), ParseField(parts[3], 1, 12), ParseField(parts[4], 0, 6) ); } public bool Matches(DateTime dt) { return _minutes.Contains(dt.Minute) && _hours.Contains(dt.Hour) && _daysOfMonth.Contains(dt.Day) && _months.Contains(dt.Month) && _daysOfWeek.Contains((int)dt.DayOfWeek); } public DateTime? GetNextOccurrence(DateTime after) { var candidate = new DateTime(after.Year, after.Month, after.Day, after.Hour, after.Minute, 0) .AddMinutes(1); // Suche maximal 2 Jahre in die Zukunft var limit = after.AddYears(2); while (candidate < limit) { if (Matches(candidate)) return candidate; candidate = candidate.AddMinutes(1); // Optimierung: überspringe ungültige Stunden/Tage if (!_months.Contains(candidate.Month)) { candidate = new DateTime(candidate.Year, candidate.Month, 1).AddMonths(1); continue; } if (!_daysOfMonth.Contains(candidate.Day) || !_daysOfWeek.Contains((int)candidate.DayOfWeek)) { candidate = new DateTime(candidate.Year, candidate.Month, candidate.Day).AddDays(1); continue; } if (!_hours.Contains(candidate.Hour)) { candidate = new DateTime(candidate.Year, candidate.Month, candidate.Day, candidate.Hour, 0, 0) .AddHours(1); } } return null; } private static HashSet ParseField(string field, int min, int max) { var result = new HashSet(); foreach (var part in field.Split(',')) { if (part == "*") { for (var i = min; i <= max; i++) result.Add(i); } else if (part.Contains('/')) { var split = part.Split('/'); var start = split[0] == "*" ? min : int.Parse(split[0]); var step = int.Parse(split[1]); for (var i = start; i <= max; i += step) result.Add(i); } else if (part.Contains('-')) { var split = part.Split('-'); var from = int.Parse(split[0]); var to = int.Parse(split[1]); for (var i = from; i <= to; i++) result.Add(i); } else { result.Add(int.Parse(part)); } } return result; } }