feat(worker): add NextDueCalculator with workday + catch-up logic
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
66
src/ClaudeDo.Worker/Prime/NextDueCalculator.cs
Normal file
66
src/ClaudeDo.Worker/Prime/NextDueCalculator.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
namespace ClaudeDo.Worker.Prime;
|
||||
|
||||
public sealed record NextDue(PrimeScheduleDto Schedule, DateTimeOffset At, bool FireImmediately);
|
||||
|
||||
public static class NextDueCalculator
|
||||
{
|
||||
public static NextDue? Compute(
|
||||
IEnumerable<PrimeScheduleDto> schedules,
|
||||
DateTimeOffset now,
|
||||
TimeSpan catchUp)
|
||||
{
|
||||
NextDue? best = null;
|
||||
foreach (var s in schedules)
|
||||
{
|
||||
if (!s.Enabled) continue;
|
||||
var due = ComputeFor(s, now, catchUp);
|
||||
if (due is null) continue;
|
||||
if (best is null || due.At < best.At) best = due;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
private static NextDue? ComputeFor(PrimeScheduleDto s, DateTimeOffset now, TimeSpan catchUp)
|
||||
{
|
||||
if (s.EndDate < DateOnly.FromDateTime(now.LocalDateTime)) return null;
|
||||
|
||||
var todayLocal = DateOnly.FromDateTime(now.LocalDateTime);
|
||||
var alreadyFiredToday = s.LastRunAt is { } last &&
|
||||
DateOnly.FromDateTime(last.LocalDateTime) == todayLocal;
|
||||
|
||||
if (!alreadyFiredToday)
|
||||
{
|
||||
var startOrToday = s.StartDate > todayLocal ? s.StartDate : todayLocal;
|
||||
if (startOrToday == todayLocal && IsEligibleDay(s, todayLocal))
|
||||
{
|
||||
var todayTarget = ToOffset(todayLocal, s.TimeOfDay, now.Offset);
|
||||
if (todayTarget >= now)
|
||||
return new NextDue(s, todayTarget, false);
|
||||
if (now <= todayTarget + catchUp)
|
||||
return new NextDue(s, now, true);
|
||||
}
|
||||
}
|
||||
|
||||
var d = todayLocal.AddDays(1);
|
||||
if (s.StartDate > d) d = s.StartDate;
|
||||
for (int i = 0; i < 8; i++)
|
||||
{
|
||||
if (d > s.EndDate) return null;
|
||||
if (IsEligibleDay(s, d))
|
||||
return new NextDue(s, ToOffset(d, s.TimeOfDay, now.Offset), false);
|
||||
d = d.AddDays(1);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsEligibleDay(PrimeScheduleDto s, DateOnly d)
|
||||
{
|
||||
if (d < s.StartDate || d > s.EndDate) return false;
|
||||
if (!s.WorkdaysOnly) return true;
|
||||
var dow = d.ToDateTime(TimeOnly.MinValue).DayOfWeek;
|
||||
return dow != DayOfWeek.Saturday && dow != DayOfWeek.Sunday;
|
||||
}
|
||||
|
||||
private static DateTimeOffset ToOffset(DateOnly day, TimeSpan time, TimeSpan offset) =>
|
||||
new(day.Year, day.Month, day.Day, time.Hours, time.Minutes, time.Seconds, offset);
|
||||
}
|
||||
Reference in New Issue
Block a user