From 070cf2bfb81fc268ec8e0a3eb651106c5b99171b Mon Sep 17 00:00:00 2001 From: "D. Moonfire" Date: Wed, 18 Jan 2023 23:51:37 -0600 Subject: [PATCH] fix(scheduler): added better error messages and regular expression for numerical paths --- .../NumericalPathSchedule.cs | 33 ++++++++--- .../README.md | 2 +- .../NumericalPathScheduleTests.cs | 57 +++++++++++++++++++ 3 files changed, 84 insertions(+), 8 deletions(-) diff --git a/src/MfGames.Nitride.Temporal.Schedules/NumericalPathSchedule.cs b/src/MfGames.Nitride.Temporal.Schedules/NumericalPathSchedule.cs index 602a5be..961608a 100644 --- a/src/MfGames.Nitride.Temporal.Schedules/NumericalPathSchedule.cs +++ b/src/MfGames.Nitride.Temporal.Schedules/NumericalPathSchedule.cs @@ -21,7 +21,7 @@ public partial class NumericalPathSchedule : ISchedule { public NumericalPathSchedule() { - this.PathRegex = @"^.*(\d+)"; + this.PathRegex = @"^.*?(\d+)[^\d]*$"; this.GetPath = entity => entity.Get().ToString(); this.CaptureGroup = 1; this.CaptureOffset = -1; @@ -61,13 +61,17 @@ public partial class NumericalPathSchedule : ISchedule /// /// Gets or sets the schedule period as a TimeSpan object. This is converted /// from SchedulePeriod using https://github.com/pengowray/TimeSpanParser. - /// If the value is null, then this will be instant (TimeSpan.Zero). + /// If the value is null or blank or "immediate", then this will be instant + /// (TimeSpan.Zero). /// public virtual TimeSpan SchedulePeriodTimeSpan { - get => this.SchedulePeriod == null - ? TimeSpan.Zero - : TimeSpanParser.Parse(this.SchedulePeriod); + get => string.IsNullOrWhiteSpace(this.SchedulePeriod) + || this.SchedulePeriod.Equals( + "immediate", + StringComparison.InvariantCultureIgnoreCase) + ? TimeSpan.Zero + : TimeSpanParser.Parse(this.SchedulePeriod); set => this.SchedulePeriod = value.ToString(); } @@ -95,9 +99,24 @@ public partial class NumericalPathSchedule : ISchedule return entity; } + if (match.Groups.Count < 2) + { + throw new InvalidOperationException( + "There must be at least one capture group in '" + + this.PathRegex + + "'."); + } + // Figure out the index/number of this entry. - int number = int.Parse(match.Groups[this.CaptureGroup].Value) - + this.CaptureOffset; + string numberValue = match.Groups[this.CaptureGroup].Value; + + if (!int.TryParse(numberValue, out int number)) + { + throw new FormatException( + path + ": Cannot parse '" + numberValue + "' as integer."); + } + + number += this.CaptureOffset; // Figure out the time from the start. DateTime start = this.ScheduleStart diff --git a/src/MfGames.Nitride.Temporal.Schedules/README.md b/src/MfGames.Nitride.Temporal.Schedules/README.md index 5f7338f..2b4764c 100644 --- a/src/MfGames.Nitride.Temporal.Schedules/README.md +++ b/src/MfGames.Nitride.Temporal.Schedules/README.md @@ -103,7 +103,7 @@ chapter will be set to 2023-01-08, and the third at 2023-01-15. - Defaults to `entity.Get().ToString()` - `Regex PathRegex` - The regular expression that retrieves the number. - - Defaults to `^.*(\d+)` which grabs the last number found. + - Defaults to `^.*?(\d+)[^\d]*$` which grabs the last number found. - `DateTime ScheduleStart` - The date that the schedule starts. - No default. diff --git a/tests/MfGames.Nitride.Temporal.Schedules.Tests/NumericalPathScheduleTests.cs b/tests/MfGames.Nitride.Temporal.Schedules.Tests/NumericalPathScheduleTests.cs index d47ed1d..de5049e 100644 --- a/tests/MfGames.Nitride.Temporal.Schedules.Tests/NumericalPathScheduleTests.cs +++ b/tests/MfGames.Nitride.Temporal.Schedules.Tests/NumericalPathScheduleTests.cs @@ -49,6 +49,7 @@ public class NumericalPathScheduleTests : TemporalSchedulesTestBase "schedules:", " - pathRegex: chapter-(\\d+)", " scheduleStart: 2023-01-01", + " schedulePeriod: 1 week", " access: public", "")); List? schedules = model.Schedules!; @@ -141,6 +142,62 @@ public class NumericalPathScheduleTests : TemporalSchedulesTestBase TestHelper.CompareObjects(expected, actual); } + [Fact] + public void ScheduleOffsetWorks() + { + using TemporalSchedulesTestContext context = this.CreateContext(); + + // Create a numerical series of entities. + var input = new List + { + new Entity().SetAll((UPath)"/chapter-11.md", new TestModel()), + new Entity().SetAll((UPath)"/chapter-12.md", new TestModel()), + new Entity().SetAll((UPath)"/chapter-13.md", new TestModel()), + }; + + var schedules = new List + { + new() + { + ScheduleStart = DateTime.Parse("2023-01-01"), + CaptureOffset = -11, + Access = "public", + }, + }; + + // Create the operation and run it, but treat it as being set after the + // second but before the third item. + Timekeeper time = context.Resolve(); + ApplySchedules op = context.Resolve() + .WithGetSchedules(_ => schedules); + var now = Instant.FromUtc(2023, 1, 9, 0, 0); + + time.Clock = new FakeClock(now); + + var actual = op + .Run(input) + .Select( + a => string.Format( + "{0} -- {1} -- {2}", + a.Get().ToString(), + a.Has() + ? time + .ToDateTime(a.Get()) + .ToString("yyyy-MM-dd") + : "none", + a.Get().Access)) + .ToList(); + + var expected = new List + { + "/chapter-11.md -- 2023-01-01 -- public", + "/chapter-12.md -- 2023-01-08 -- public", + "/chapter-13.md -- none -- private", + }; + + TestHelper.CompareObjects(expected, actual); + } + [Fact] public void SequencedScheduleWorks() {