Import des bestehenden Projektstands in Git. - .NET 10 WinForms Anwendung (Multi-Agent / Tool-System) - .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
120 lines
3.8 KiB
C#
120 lines
3.8 KiB
C#
namespace ClawdDotNet.Core.Scheduling;
|
|
|
|
/// <summary>
|
|
/// 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)
|
|
/// </summary>
|
|
public sealed class CronExpression
|
|
{
|
|
private readonly HashSet<int> _minutes;
|
|
private readonly HashSet<int> _hours;
|
|
private readonly HashSet<int> _daysOfMonth;
|
|
private readonly HashSet<int> _months;
|
|
private readonly HashSet<int> _daysOfWeek;
|
|
|
|
private CronExpression(
|
|
HashSet<int> minutes, HashSet<int> hours,
|
|
HashSet<int> daysOfMonth, HashSet<int> months,
|
|
HashSet<int> 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<int> ParseField(string field, int min, int max)
|
|
{
|
|
var result = new HashSet<int>();
|
|
|
|
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;
|
|
}
|
|
}
|