fix: Streamlines event scheduler API. Adds months to weekly recurrence (#2178)

This commit is contained in:
Kamron Batman 2025-05-07 22:42:44 -07:00 committed by GitHub
parent 990e86b983
commit f95b3a8b3b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 347 additions and 137 deletions

View file

@ -41,8 +41,8 @@ public class EventSchedulerTests
public int CallCount { get; private set; }
public Action Callback { get; }
public TestScheduledEvent(DateTime startAfter, Action callback, IRecurrencePattern recurrence = null)
: base(startAfter, TimeOnly.FromDateTime(startAfter), recurrence, TimeZoneInfo.Utc)
public TestScheduledEvent(TimeOnly time, Action callback, IRecurrencePattern recurrence = null)
: base(time, recurrence)
{
CallCount = 0;
Callback = callback;
@ -105,25 +105,28 @@ public class EventSchedulerTests
var executionOrder = new List<int>();
// Create events with staggered occurrences
var evt3 = new TestScheduledEvent(
Core._now.AddSeconds(30),
() => executionOrder.Add(3)
);
var evt1Start = Core._now.AddSeconds(10);
var evt1 = new TestScheduledEvent(
Core._now.AddSeconds(10),
TimeOnly.FromDateTime(Core._now.AddSeconds(10)),
() => executionOrder.Add(1)
);
var evt2Start = Core._now.AddSeconds(20);
var evt2 = new TestScheduledEvent(
Core._now.AddSeconds(20),
TimeOnly.FromDateTime(evt2Start),
() => executionOrder.Add(2)
);
var evt3Start = Core._now.AddSeconds(30);
var evt3 = new TestScheduledEvent(
TimeOnly.FromDateTime(evt3Start),
() => executionOrder.Add(3)
);
// Add out of order
EventScheduler.Shared.ScheduleEvent(evt3);
EventScheduler.Shared.ScheduleEvent(evt1);
EventScheduler.Shared.ScheduleEvent(evt2);
evt3.Schedule(evt3Start);
evt1.Schedule(evt1Start);
evt2.Schedule(evt2Start);
// Advance time to after all events
Core._now = Core._now.AddSeconds(40);
@ -157,12 +160,12 @@ public class EventSchedulerTests
var recurrence = new TestRecurrencePattern(TimeSpan.FromSeconds(10), 3);
var evt = new TestScheduledEvent(
Core._now,
TimeOnly.FromDateTime(Core._now),
() => callCount++,
recurrence
);
EventScheduler.Shared.ScheduleEvent(evt);
evt.Schedule(Core._now);
// Advance time to after all occurrences should have happened
Core._now = Core._now.AddSeconds(50);
@ -271,14 +274,13 @@ public class EventSchedulerTests
var endTime = Core._now.AddSeconds(36);
var customEvent = new CustomEvent(
startTime,
endTime,
TimeOnly.FromDateTime(startTime),
endTime,
recurrence,
() => callCount++
);
EventScheduler.Shared.ScheduleEvent(customEvent);
customEvent.Schedule(startTime);
Core._now = Core._now.AddSeconds(50);
Timer.Slice(8);
@ -297,12 +299,11 @@ public class EventSchedulerTests
private readonly Action _callback;
public CustomEvent(
DateTime startAfter,
DateTime endOn,
TimeOnly time,
DateTime endOn,
IRecurrencePattern recurrence,
Action callback
) : base(startAfter, endOn, time, recurrence, TimeZoneInfo.Utc) => _callback = callback;
) : base(time, endOn, recurrence) => _callback = callback;
public override void OnEvent()
{

View file

@ -51,6 +51,152 @@ public class RecurrencePatternTests
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 5, 15, 12, 30, 1, AllowedDays.All, 2024, 5, 16, 12, 30)] // Next day (Thursday)
[InlineData(2024, 5, 15, 12, 30, 1, AllowedDays.Monday, 2024, 5, 20, 12, 30)] // Next Monday
[InlineData(2024, 5, 15, 12, 30, 2, AllowedDays.Wednesday, 2024, 5, 29, 12, 30)] // Biweekly Wednesday
public void WeeklyRecurrencePattern_BasicPatterns_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalWeeks, AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 12, 30, 12, 30, 1, AllowedDays.Monday, 2025, 1, 6, 12, 30)] // Across year boundary
[InlineData(2024, 5, 31, 12, 30, 1, AllowedDays.Saturday, 2024, 6, 1, 12, 30)] // Across month boundary
public void WeeklyRecurrencePattern_CrossBoundaries_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalWeeks, AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 5, 15, 12, 30, AllowedMonths.January | AllowedMonths.February, AllowedDays.Wednesday, 2025, 1, 1, 12, 30)] // Skip to January
[InlineData(2024, 12, 15, 12, 30, AllowedMonths.January | AllowedMonths.December, AllowedDays.Wednesday, 2024, 12, 18, 12, 30)] // Same month
[InlineData(2024, 1, 31, 12, 30, AllowedMonths.February | AllowedMonths.April, AllowedDays.Saturday, 2024, 2, 3, 12, 30)] // Next month allowed
public void WeeklyRecurrencePattern_MonthFiltering_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
AllowedMonths allowedMonths, AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(1, allowedMonths, allowedDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData("America/New_York", 2024, 3, 9, 12, 0, 1, AllowedDays.Sunday, 2024, 3, 10, 12, 0)] // Before spring forward
[InlineData("America/New_York", 2024, 3, 10, 2, 30, 1, AllowedDays.Sunday, 2024, 3, 17, 2, 30)] // During spring forward (invalid time)
[InlineData("America/New_York", 2024, 11, 2, 12, 0, 1, AllowedDays.Sunday, 2024, 11, 3, 12, 0)] // Before fall back
[InlineData("America/New_York", 2024, 11, 3, 1, 30, 1, AllowedDays.Sunday, 2024, 11, 10, 1, 30)] // During fall back (ambiguous time)
public void WeeklyRecurrencePattern_DSTTransitions_HandlesCorrectly(
string tzId, int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalWeeks, AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.FindSystemTimeZoneById(tzId);
var pattern = new WeeklyRecurrencePattern(intervalWeeks, AllowedMonths.All, allowedDays);
var startLocal = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0);
var afterUtc = startLocal.LocalToUtc(tz);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var resultLocal = TimeZoneInfo.ConvertTimeFromUtc(result, tz);
Assert.Equal(new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0), resultLocal);
}
[Theory]
[InlineData(2024, 5, 29, 12, 0, AllowedDays.None, 2024, 5, 29, 12, 30)] // Default to current day
[InlineData(2024, 5, 29, 12, 0, AllowedDays.Wednesday | AllowedDays.Friday, 2024, 5, 29, 12, 30)] // Current day is Wednesday
[InlineData(2024, 5, 29, 12, 0, AllowedDays.Monday | AllowedDays.Friday, 2024, 5, 31, 12, 30)] // Next allowed day (Friday)
public void WeeklyRecurrencePattern_DaysOfWeekHandling_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
AllowedDays allowedDays,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(1, AllowedMonths.All, allowedDays);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
}
[Theory]
[InlineData(2024, 1, 29, 12, 0, 1, 2024, 1, 29, 12, 30, AllowedMonths.January | AllowedMonths.March, 2024, 1, 30, 12, 30)] // Current month allowed
[InlineData(2024, 1, 31, 12, 0, 1, 2024, 1, 31, 12, 30, AllowedMonths.January | AllowedMonths.March, 2024, 3, 1, 12, 30)] // Skip February
public void WeeklyRecurrencePattern_WeekSpanningMonths_ReturnsCorrectDay(
int startYear, int startMonth, int startDay, int startHour, int startMinute,
int intervalWeeks,
int expectedYear, int expectedMonth, int expectedDay, int expectedHour, int expectedMinute,
AllowedMonths allowedMonths,
int nextExpectedYear, int nextExpectedMonth, int nextExpectedDay, int nextExpectedHour, int nextExpectedMinute)
{
// Arrange
var tz = TimeZoneInfo.Utc;
var pattern = new WeeklyRecurrencePattern(intervalWeeks, allowedMonths);
var afterUtc = new DateTime(startYear, startMonth, startDay, startHour, startMinute, 0, DateTimeKind.Utc);
var time = new TimeOnly(expectedHour, expectedMinute);
// Act - first call should hit expectedDate
var result = pattern.GetNextOccurrence(afterUtc, time, tz);
// Assert
var expected = new DateTime(expectedYear, expectedMonth, expectedDay, expectedHour, expectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(expected, result);
// Call again - should move to next available day in allowed months
var nextResult = pattern.GetNextOccurrence(result, time, tz);
var nextExpected = new DateTime(nextExpectedYear, nextExpectedMonth, nextExpectedDay, nextExpectedHour, nextExpectedMinute, 0, DateTimeKind.Utc);
Assert.Equal(nextExpected, nextResult);
}
[Theory]
[InlineData(2024, 11, 3, 1, 30, DayOfWeek.Sunday, OrdinalDayOccurrence.First, "America/New_York", 2024, 11, 3)]
[InlineData(2024, 3, 31, 0, 0, DayOfWeek.Sunday, OrdinalDayOccurrence.Last, "America/New_York", 2024, 3, 31)]
@ -145,7 +291,6 @@ public class RecurrencePatternTests
Assert.Equal(new DateTime(2024, 3, 31, 12, 0, 0), result);
}
// Additional utility method tests
[Theory]
[InlineData("America/New_York", 2024, 11, 3, 1, 30)] // Fall back - ambiguous time
public void LocalToUtc_AmbiguousTime_HandlesConsistently(

View file

@ -0,0 +1,22 @@
using System;
namespace Server.Engines.Events;
[Flags]
public enum AllowedDays : byte
{
None = 0,
Sunday = 1 << 0,
Monday = 1 << 1,
Tuesday = 1 << 2,
Wednesday = 1 << 3,
Thursday = 1 << 4,
Friday = 1 << 5,
Saturday = 1 << 6,
All = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday
}
public static class DaysOfWeekExtension
{
public static AllowedDays ToDaysOfWeek(this DayOfWeek dayOfWeek) => (AllowedDays)(1 << (int)dayOfWeek);
}

View file

@ -0,0 +1,23 @@
using System;
namespace Server.Engines.Events;
[Flags]
public enum AllowedMonths
{
None = 0,
January = 1 << 0,
February = 1 << 1,
March = 1 << 2,
April = 1 << 3,
May = 1 << 4,
June = 1 << 5,
July = 1 << 6,
August = 1 << 7,
September = 1 << 8,
October = 1 << 9,
November = 1 << 10,
December = 1 << 11,
All = January | February | March | April | May | June | July | August | September | October | November | December
}

View file

@ -0,0 +1,67 @@
using System;
using System.Runtime.CompilerServices;
using Server.Logging;
namespace Server.Engines.Events;
public abstract class BaseScheduledEvent
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseScheduledEvent));
public TimeZoneInfo TimeZone { get; private set; } = TimeZoneInfo.Utc;
public DateTime NextOccurrence { get; private set; } = DateTime.MaxValue;
public bool Cancelled { get; private set; }
public EventScheduler Scheduler { get; private set; }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Schedule(DateTime startAfter, TimeZoneInfo timeZone = null) =>
Schedule(EventScheduler.Shared, startAfter, timeZone);
public void Schedule(EventScheduler scheduler, DateTime startAfter, TimeZoneInfo timeZone = null)
{
Cancel();
Scheduler = scheduler;
TimeZone = timeZone ?? TimeZoneInfo.Utc;
Schedule(startAfter, timeZone, true);
}
private void Schedule(DateTime startAfter, TimeZoneInfo timeZone, bool isFirst)
{
Cancelled = false;
var afterUtc = startAfter.Kind == DateTimeKind.Utc ? startAfter : startAfter.LocalToUtc(timeZone);
var next = GetNextOccurrence(afterUtc);
// For the first occurrence, we should set it to the startAfter date if we have no recurrence.
NextOccurrence = next == DateTime.MaxValue && isFirst ? afterUtc : next;
if (NextOccurrence != DateTime.MaxValue)
{
Scheduler.ScheduleEvent(this);
}
}
protected abstract DateTime GetNextOccurrence(DateTime after);
public void Cancel()
{
Cancelled = true;
Scheduler?.UnscheduleEvent(this);
}
public virtual void Advance()
{
try
{
OnEvent();
}
catch (Exception ex)
{
logger.Error(ex, "OnEvent failed to execute.");
}
Schedule(NextOccurrence, TimeZone, false);
}
public abstract void OnEvent();
}

View file

@ -7,23 +7,17 @@ public sealed class CallbackScheduledEvent : ScheduledEvent
private readonly Action _callback;
public CallbackScheduledEvent(
DateTime after,
TimeOnly time,
Action callback,
IRecurrencePattern recurrencePattern = null,
TimeZoneInfo timeZone = null
) : base(after, time, recurrencePattern, timeZone) =>
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
IRecurrencePattern recurrencePattern = null
) : base(time, recurrencePattern) => _callback = callback ?? throw new ArgumentNullException(nameof(callback));
public CallbackScheduledEvent(
DateTime after,
DateTime endOn,
TimeOnly time,
DateTime endOn,
Action callback,
IRecurrencePattern recurrencePattern = null,
TimeZoneInfo timeZone = null
) : base(after, endOn, time, recurrencePattern, timeZone) =>
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
IRecurrencePattern recurrencePattern = null
) : base(time, endOn, recurrencePattern) => _callback = callback ?? throw new ArgumentNullException(nameof(callback));
public override void OnEvent() => _callback();
}

View file

@ -35,32 +35,49 @@ public class DailyRecurrencePattern : IRecurrencePattern
public class WeeklyRecurrencePattern : IRecurrencePattern
{
public int IntervalWeeks { get; }
public DaysOfWeek DaysOfWeek { get; }
public AllowedDays AllowedDays { get; }
public AllowedMonths AllowedMonths { get; }
public WeeklyRecurrencePattern(int intervalWeeks = 1, DaysOfWeek daysOfWeek = DaysOfWeek.None)
public WeeklyRecurrencePattern(int intervalWeeks = 1, AllowedMonths allowedMonths = AllowedMonths.All, AllowedDays allowedDays = AllowedDays.None)
{
IntervalWeeks = Math.Max(1, intervalWeeks);
DaysOfWeek = daysOfWeek;
AllowedDays = allowedDays == AllowedDays.None ? AllowedDays.All : allowedDays;
AllowedMonths = allowedMonths == AllowedMonths.None ? AllowedMonths.All : allowedMonths;
}
public DateTime GetNextOccurrence(DateTime afterUtc, TimeOnly time, TimeZoneInfo timeZone)
{
var local = TimeZoneInfo.ConvertTimeFromUtc(afterUtc, timeZone);
var daysOfWeek = DaysOfWeek == DaysOfWeek.None
? (DaysOfWeek)(1 << (int)local.DayOfWeek)
: DaysOfWeek;
var daysOfWeek = AllowedDays == AllowedDays.None ? local.DayOfWeek.ToDaysOfWeek() : AllowedDays;
var weekStart = local.Date.AddDays(-(int)local.DayOfWeek);
// No more days in this week, jump IntervalWeeks ahead
for (int week = 0; week <= 100; week++)
for (var week = 0; week <= 52; week++)
{
DateTime nextWeekStart = weekStart.AddDays(7 * IntervalWeeks * week);
var nextWeekStart = weekStart.AddDays(7 * IntervalWeeks * week);
var weekEnd = nextWeekStart.AddDays(6);
for (int i = 0; i < 7; i++)
var startMonth = (AllowedMonths)(1 << (nextWeekStart.Month - 1));
var endMonth = (AllowedMonths)(1 << (weekEnd.Month - 1));
// Skip the entire week if the start and end months are not in the allowed months
if ((AllowedMonths & (startMonth | endMonth)) == 0)
{
continue;
}
for (var i = 0; i < 7; i++)
{
var day = nextWeekStart.AddDays(i);
var dayOfWeekFlag = (DaysOfWeek)(1 << (int)day.DayOfWeek);
var currentMonth = (AllowedMonths)(1 << (day.Month - 1));
if ((AllowedMonths & currentMonth) == 0)
{
continue;
}
var dayOfWeekFlag = (AllowedDays)(1 << (int)day.DayOfWeek);
if ((daysOfWeek & dayOfWeekFlag) != 0)
{
var candidate = new DateTime(day.Year, day.Month, day.Day, time.Hour, time.Minute, 0);
@ -94,7 +111,7 @@ public class MonthlyRecurrencePattern : IRecurrencePattern
var month = local.Month;
var day = DayOfMonth == -1 ? local.Day : DayOfMonth;
for (int i = 0; i < 100; i++)
for (var i = 0; i < 100; i++)
{
var nextMonth = month + IntervalMonths * i;
var candidate = new DateTime(year, 1, 1)
@ -132,7 +149,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern
var year = local.Year;
var month = local.Month;
for (int i = 0; i < 100; i++)
for (var i = 0; i < 100; i++)
{
var nextMonth = month + IntervalMonths * i;
var candidateYearOffset = Math.DivRem(nextMonth - 1, 12, out var candidateMonthOffset);
@ -147,7 +164,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern
.Add(time.ToTimeSpan());
// Find the first occurrence of the desired day
int daysOffset = ((int)DayOfWeek - (int)firstOfMonth.DayOfWeek + 7) % 7;
var daysOffset = ((int)DayOfWeek - (int)firstOfMonth.DayOfWeek + 7) % 7;
if (daysOffset > 7)
{
daysOffset -= 7;
@ -168,7 +185,7 @@ public class MonthlyOrdinalRecurrencePattern : IRecurrencePattern
.Add(time.ToTimeSpan());
// Find the last occurrence of the desired day
int daysOffset = (int)lastOfMonth.DayOfWeek - (int)DayOfWeek + 7;
var daysOffset = (int)lastOfMonth.DayOfWeek - (int)DayOfWeek + 7;
if (daysOffset >= 7)
{
daysOffset -= 7;

View file

@ -2,7 +2,6 @@ using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Server.Logging;
namespace Server.Engines.Events;
@ -17,25 +16,9 @@ public interface IRecurrencePattern
public enum OrdinalDayOccurrence { Last = -1, First, Second, Third, Fourth, Fifth }
[Flags]
public enum DaysOfWeek : byte
{
None = 0,
Sunday = 1,
Monday = 2,
Tuesday = 4,
Wednesday = 8,
Thursday = 16,
Friday = 32,
Saturday = 64,
EveryDay = Sunday | Monday | Tuesday | Wednesday | Thursday | Friday | Saturday
}
public class EventScheduler : Timer
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(EventScheduler));
private readonly PriorityQueue<ScheduledEvent, DateTime> _schedule = new();
private readonly PriorityQueue<BaseScheduledEvent, DateTime> _schedule = new();
public static EventScheduler Shared { get; private set; }
@ -116,16 +99,24 @@ public class EventScheduler : Timer
TimeZoneInfo timeZone = null
)
{
var scheduledEvent = new CallbackScheduledEvent(after, time, callback, recurrencePattern, timeZone);
ScheduleEvent(scheduledEvent);
var scheduledEvent = new CallbackScheduledEvent(time, callback, recurrencePattern);
scheduledEvent.Schedule(this, after, timeZone);
return scheduledEvent;
}
public void ScheduleEvent(ScheduledEvent entry)
public void ScheduleEvent(BaseScheduledEvent entry)
{
if (entry != null && entry.NextOccurrence < DateTime.MaxValue)
{
_schedule.Enqueue(entry, entry.NextOccurrence);
}
}
public void UnscheduleEvent(BaseScheduledEvent entry)
{
if (entry != null)
{
_schedule.Enqueue(entry, entry.NextOccurrence);
_schedule.Remove(entry, out _, out _);
}
}
@ -149,21 +140,7 @@ public class EventScheduler : Timer
continue;
}
DateTime nextOccurrence;
try
{
nextOccurrence = entry.Advance();
}
catch (Exception e)
{
logger.Error(e, "Error while executing scheduled event.");
nextOccurrence = DateTime.MaxValue;
}
if (nextOccurrence < DateTime.MaxValue)
{
_schedule.Enqueue(entry, nextOccurrence);
}
entry.Advance(); // Advances the event and self queues if necessary
}
}
}

View file

@ -2,59 +2,31 @@ using System;
namespace Server.Engines.Events;
public abstract class ScheduledEvent
public abstract class ScheduledEvent : BaseScheduledEvent
{
public IRecurrencePattern Recurrence { get; }
public TimeZoneInfo TimeZone { get; }
public TimeOnly Time { get; }
public DateTime EndDate { get; }
public DateTime NextOccurrence { get; protected set; }
public bool Cancelled { get; private set; }
public ScheduledEvent(DateTime startOn, TimeZoneInfo timeZone = null)
: this(startOn, startOn, TimeOnly.FromDateTime(startOn), null, timeZone)
{
}
public ScheduledEvent(DateTime startAfter, TimeOnly time, IRecurrencePattern recurrence, TimeZoneInfo timeZone = null)
: this(startAfter, DateTime.MaxValue, time, recurrence, timeZone)
public ScheduledEvent(TimeOnly time, IRecurrencePattern recurrence = null)
: this(time, DateTime.MaxValue, recurrence)
{
}
public ScheduledEvent(
DateTime startAfter,
DateTime endOn,
TimeOnly time,
IRecurrencePattern recurrence,
TimeZoneInfo timeZone = null
DateTime endOn,
IRecurrencePattern recurrence = null
)
{
Time = time;
Recurrence = recurrence;
TimeZone = timeZone ?? TimeZoneInfo.Utc;
EndDate = endOn == DateTime.MaxValue || endOn.Kind == DateTimeKind.Utc ? endOn : endOn.LocalToUtc(TimeZone);
var afterUtc = startAfter.Kind == DateTimeKind.Utc ? startAfter : startAfter.LocalToUtc(TimeZone);
var next = GetOccurrence(afterUtc);
// For the first occurrence, we should set it to the startAfter date if we have no recurrence.
NextOccurrence = next == DateTime.MaxValue ? afterUtc : next;
}
protected virtual DateTime GetOccurrence(DateTime after)
protected override DateTime GetNextOccurrence(DateTime after)
{
var next = Recurrence?.GetNextOccurrence(after, Time, TimeZone) ?? DateTime.MaxValue;
return next >= EndDate ? DateTime.MaxValue : next;
}
public void Cancel() => Cancelled = true;
public virtual DateTime Advance()
{
OnEvent();
return NextOccurrence = GetOccurrence(NextOccurrence);
}
public abstract void OnEvent();
}

View file

@ -7,27 +7,23 @@ public class YearlyCallbackScheduledEvent : YearlyScheduledEvent
private readonly Action _callback;
protected YearlyCallbackScheduledEvent(
DateTime startAfter,
TimeOnly time,
MonthDay yearlyStart,
MonthDay yearlyEnd,
Action callback,
IRecurrencePattern recurrence,
TimeZoneInfo timeZone = null
) : this(startAfter, DateTime.MaxValue, time, yearlyStart, yearlyEnd, callback, recurrence, timeZone)
IRecurrencePattern recurrence
) : this(time, yearlyStart, yearlyEnd, DateTime.MaxValue, callback, recurrence)
{
}
protected YearlyCallbackScheduledEvent(
DateTime startAfter,
DateTime endOn,
TimeOnly time,
MonthDay yearlyStart,
MonthDay yearlyEnd,
DateTime endOn,
Action callback,
IRecurrencePattern recurrence,
TimeZoneInfo timeZone = null
) : base(startAfter, endOn, time, yearlyStart, yearlyEnd, recurrence, timeZone) => _callback = callback;
IRecurrencePattern recurrence
) : base(time, yearlyStart, yearlyEnd, endOn, recurrence) => _callback = callback;
public override void OnEvent() => _callback();
}

View file

@ -9,33 +9,29 @@ public abstract class YearlyScheduledEvent : ScheduledEvent
public MonthDay YearlyEnd { get; }
protected YearlyScheduledEvent(
DateTime startAfter,
TimeOnly time,
MonthDay yearlyStart,
MonthDay yearlyEnd,
IRecurrencePattern recurrence,
TimeZoneInfo timeZone = null
) : this(startAfter, DateTime.MaxValue, time, yearlyStart, yearlyEnd, recurrence, timeZone)
IRecurrencePattern recurrence
) : this( time, yearlyStart, yearlyEnd, DateTime.MaxValue, recurrence)
{
}
protected YearlyScheduledEvent(
DateTime startAfter,
DateTime endOn,
TimeOnly time,
MonthDay yearlyStart,
MonthDay yearlyEnd,
IRecurrencePattern recurrence,
TimeZoneInfo timeZone = null
) : base(startAfter, endOn, time, recurrence, timeZone)
DateTime endOn,
IRecurrencePattern recurrence
) : base(time, endOn, recurrence)
{
YearlyStart = yearlyStart;
YearlyEnd = yearlyEnd;
}
protected override DateTime GetOccurrence(DateTime after)
protected override DateTime GetNextOccurrence(DateTime after)
{
var next = base.GetOccurrence(after);
var next = base.GetNextOccurrence(after);
if (next == DateTime.MaxValue)
{