NCrontab icon indicating copy to clipboard operation
NCrontab copied to clipboard

Solution for new `GetPreviousOccurence`

Open DaveRMaltby opened this issue 2 years ago • 14 comments

…methods. https://github.com/atifaziz/NCrontab/issues/64 Moves from the start DateTime in a reverse fashion to avoid a need to move forward and guess how far in the past to begin the search from.

DaveRMaltby avatar Jan 29 '23 13:01 DaveRMaltby

Build appears to be failing due to an old .NET Standard library reference.

/home/appveyor/projects/ncrontab/NCrontab/NCrontab.csproj : error NU3037: Package 'NETStandard.Library 2.0.3' from source 'https://api.nuget.org/v3/index.json': The repository primary signature validity period has expired.

DaveRMaltby avatar Jan 30 '23 21:01 DaveRMaltby

I needed this so I could use CRON to define working windows with a limit of operations between each window I had to get when the CRON window started and when it ended to load the operations that were executed or scheduled in the window. By computing when was the last time the CRON was valid (when window started) without GetPrevOccurence, I had to loop back in time in a bizarre algorithm which had a huge CPU impact. By using this code the performance was similar to GetNextOccurence Thx @DaveRMaltby

SammyROCK avatar May 30 '23 13:05 SammyROCK

@atifaziz, I understand your position and made this contribution because I want my company (Servant Software LLC) to go beyond just mere consumption of these open source projects, but to be an active contributor. When contributing this PR, we had some competing priorities and so you're right, for this work to get merged into master it needs more efforts. I'll personally make sure that these are dealt with and provide you with updates.

Honestly, I thought that this repo had gone dormant and wanted to get this PR out there before forgetting to post it, so that others (like @SammyROCK) could at least get a fork of it, if they had such a specialized need, like myself. Very glad to see that you breathed new life into the repo today. Thank you! We plan to be here to help you as needed and totally understand if this feature doesn't make the cut.

DaveRMaltby avatar Jun 10 '23 13:06 DaveRMaltby

@atifaziz, I understand your position and made this contribution because I want my company (Servant Software LLC) to go beyond just mere consumption of these open source projects, but to be an active contributor.

@DaveRMaltby You can imagine this is like music to any maintainer's ears and I really commend your attitude and will.

When contributing this PR, we had some competing priorities and so you're right, for this work to get merged into master it needs more efforts.

Understandable.

Honestly, I thought that this repo had gone dormant

I time-share between many open source project (too many for my own good). There are periods when some get more love than others as I try to avoid moving in and out of different problem spaces so I can do each proper justice. On top of that, this particular library was always meant to do just one thing, so I consider it to be really feature complete (including stability and compatibility) and that too can lead to the impression of it being abandoned/dormant. The one (non-urgent) feature I've been meaning to add since a while is enabling unions of multiple schedules. This may be something to think about with respect to computing previous occurrences.

I'll personally make sure that these are dealt with and provide you with updates.

Looking forward.

atifaziz avatar Jun 10 '23 15:06 atifaziz

For reference, below is the difference between TryGetNextOccurrence and TryGetPrevOccurrence:

--- .\TryGetNextOccurrence.cs
+++ .\TryGetPreviousOccurrence.cs
@@ -1,4 +1,4 @@
-        DateTime? TryGetNextOccurrence(DateTime baseTime, DateTime endTime)
+        DateTime? TryGetPrevOccurrence(DateTime baseTime, DateTime startTime)
         {
             const int nil = -1;
 
@@ -9,109 +9,118 @@
             var baseMinute = baseTime.Minute;
             var baseSecond = baseTime.Second;
 
-            var endYear = endTime.Year;
-            var endMonth = endTime.Month;
-            var endDay = endTime.Day;
+            var startYear = startTime.Year;
+            var startMonth = startTime.Month;
+            var startDay = startTime.Day;
 
             var year = baseYear;
             var month = baseMonth;
             var day = baseDay;
             var hour = baseHour;
             var minute = baseMinute;
-            var second = baseSecond + 1;
+            var second = baseSecond - 1;
 
             //
             // Second
             //
 
             var seconds = _seconds ?? SecondZero;
-            second = seconds.Next(second);
+            second = seconds.Prev(second);
 
             if (second == nil)
             {
-                second = seconds.GetFirst();
-                minute++;
+                second = seconds.GetLast();
+                minute--;
             }
 
             //
             // Minute
             //
 
-            minute = _minutes.Next(minute);
+            minute = _minutes.Prev(minute);
 
             if (minute == nil)
             {
-                second = seconds.GetFirst();
-                minute = _minutes.GetFirst();
-                hour++;
+                second = seconds.GetLast();
+                minute = _minutes.GetLast();
+                hour--;
             }
-            else if (minute > baseMinute)
+            else if (minute < baseMinute)
             {
-                second = seconds.GetFirst();
+                second = seconds.GetLast();
             }
 
             //
             // Hour
             //
 
-            hour = _hours.Next(hour);
+            hour = _hours.Prev(hour);
 
+            // this variable for trick
+            // to keep day when it adjusted day 31 for
+            // "short" months
+            bool adjusting = false;
+
+            RetryDayMonth:
+
             if (hour == nil)
             {
-                minute = _minutes.GetFirst();
-                hour = _hours.GetFirst();
-                day++;
+                minute = _minutes.GetLast();
+                hour = _hours.GetLast();
+                day--;
             }
             else if (hour > baseHour)
             {
-                second = seconds.GetFirst();
-                minute = _minutes.GetFirst();
+                second = seconds.GetLast();
+                minute = _minutes.GetLast();
             }
 
             //
             // Day
             //
 
-            day = _days.Next(day);
+            
 
-            RetryDayMonth:
+            day = _days.Prev(day);
 
+            
             if (day == nil)
             {
-                second = seconds.GetFirst();
-                minute = _minutes.GetFirst();
-                hour = _hours.GetFirst();
-                day = _days.GetFirst();
-                month++;
+                second = seconds.GetLast();
+                minute = _minutes.GetLast();
+                hour = _hours.GetLast();
+                day = _days.GetLast();
+                month--;
             }
-            else if (day > baseDay)
+            else if (day < baseDay)
             {
-                second = seconds.GetFirst();
-                minute = _minutes.GetFirst();
-                hour = _hours.GetFirst();
+                second = seconds.GetLast();
+                minute = _minutes.GetLast();
+                hour = _hours.GetLast();
             }
 
             //
             // Month
             //
 
-            month = _months.Next(month);
+            month = _months.Prev(month);
 
             if (month == nil)
             {
-                second = seconds.GetFirst();
-                minute = _minutes.GetFirst();
-                hour = _hours.GetFirst();
-                day = _days.GetFirst();
-                month = _months.GetFirst();
-                year++;
+                second = seconds.GetLast();
+                minute = _minutes.GetLast();
+                hour = _hours.GetLast();
+                day = _days.GetLast();
+                month = _months.GetLast();
+                year--;
             }
-            else if (month > baseMonth)
+            else if (month < baseMonth)
             {
-                second = seconds.GetFirst();
-                minute = _minutes.GetFirst();
-                hour = _hours.GetFirst();
-                day = _days.GetFirst();
+                second = seconds.GetLast();
+                minute = _minutes.GetLast();
+                hour = _hours.GetLast();
+                if (!adjusting)
+                    day = _days.GetLast();
             }
 
             //
@@ -119,7 +128,7 @@
             // object. Otherwise we would get an exception.
             //
 
-            if (year > Calendar.MaxSupportedDateTime.Year)
+            if (year < Calendar.MinSupportedDateTime.Year)
                 return null;
 
             //
@@ -143,25 +152,26 @@
 
             if (day > 28 && dateChanged && day > Calendar.GetDaysInMonth(year, month))
             {
-                if (year >= endYear && month >= endMonth && day >= endDay)
-                    return endTime;
+                if (year <= startYear && month <= startMonth && day <= startDay)
+                    return startTime;
 
-                day = nil;
+                hour = nil;
+                adjusting = true;
                 goto RetryDayMonth;
             }
 
-            var nextTime = new DateTime(year, month, day, hour, minute, second, 0, baseTime.Kind);
+            var prevTime = new DateTime(year, month, day, hour, minute, second, 0, baseTime.Kind);
 
-            if (nextTime >= endTime)
-                return endTime;
+            if (prevTime <= startTime)
+                return startTime;
 
             //
             // Day of week
             //
 
-            if (_daysOfWeek.Contains((int)nextTime.DayOfWeek))
-                return nextTime;
+            if (_daysOfWeek.Contains((int)prevTime.DayOfWeek))
+                return prevTime;
 
-            return TryGetNextOccurrence(new DateTime(year, month, day, 23, 59, 59, 0, baseTime.Kind), endTime);
+            return TryGetPrevOccurrence(new DateTime(year, month, day, 0, 0, 0, 0, baseTime.Kind), startTime);
         }

Unfortunately, this at the line level, but actually the delta is even less when you look at the level of characters and then the duplication really stands out. If each method is extracted into a separate files, say named TryGetNextOccurrence.cs and TryGetPreviousOccurrence.cs, then you can visualise the character-level differences with (without the files needing to be part of the Git repo):

git diff --no-index --word-diff=color --word-diff-regex=. TryGetNextOccurrence.cs TryGetPreviousOccurrence.cs

atifaziz avatar Jun 10 '23 15:06 atifaziz

Thanks for this contribution, but I'm afraid it still needs a lot of work before it can be merged. It adds considerable new and fundamental logic that's duplicated between TryGetNextOccurrence and TryGetPrevOccurrence, but with very few tests ensuring full coverage (e.g. TryGetNextOccurrence has 0 uncovered blocks but TryGetPrevOccurrence has 11). Not only it adds to maintenance, I'm not confident that it's bug-free. Since you may not always be around to help maintain and fix any reported bugs, I want to avoid that burden falling entirely on me. If you have the will and motivation to address the following points then I'd be happy to work with you to get this into a mergeable state:

  1. Consolidating as much of the code duplication as possible.
  2. Ensuring full test coverage.
  3. Exercising any edge cases you can think of.

PS Also, please bear in mind that issue #64 was never committed as a feature so there is a slight chance that it may not still make the cut. That said, I am open to seeing if this can be done with some reasonable effort.

@atifaziz, are you using a particular tool to determine the uncovered blocks? I would like use the same tool locally to avoid guessing at when I've covered all blocks with unit tests. Thanks!

DaveRMaltby avatar Jun 12 '23 19:06 DaveRMaltby

I've completed #1, but haven't pushed it yet, as I'd like to complete #2 & #3 before presenting it here.

DaveRMaltby avatar Jun 13 '23 01:06 DaveRMaltby

are you using a particular tool to determine the uncovered blocks? I would like use the same tool locally to avoid guessing at when I've covered all blocks with unit tests.

I used the one built into Visual Studio for a very quick and cursory analysis, but unfortunately, the feature is only available in the Enterprise Edition. I'll refresh the coverage & reporting as part of the CI/CD for this repo so it's more accessible and drop a note here when it's available.

atifaziz avatar Jun 13 '23 15:06 atifaziz

I'll refresh the coverage & reporting as part of the CI/CD for this repo so it's more accessible and drop a note here when it's available.

This is now done with #115 and available in master with c6b44363058ddf6b40bc6d27101eb70698f6c93d. You may want to merge here. The coverage report can also be browsed online.

atifaziz avatar Jun 13 '23 16:06 atifaziz

Codecov Report

Patch coverage: 99.41% and project coverage change: +1.42 :tada:

Comparison is base (c6b4436) 88.37% compared to head (afbbec5) 89.80%.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #108      +/-   ##
==========================================
+ Coverage   88.37%   89.80%   +1.42%     
==========================================
  Files           5        8       +3     
  Lines         430      500      +70     
==========================================
+ Hits          380      449      +69     
- Misses         50       51       +1     
Impacted Files Coverage Δ
NCrontab/CrontabField.cs 66.00% <80.00%> (-3.73%) :arrow_down:
NCrontab/CrontabField.Iterator.cs 100.00% <100.00%> (ø)
NCrontab/CrontabFieldImpl.cs 89.72% <100.00%> (ø)
NCrontab/CrontabSchedule.cs 99.50% <100.00%> (+0.09%) :arrow_up:
NCrontab/Utils/DateTimeComponents.cs 100.00% <100.00%> (ø)
NCrontab/Utils/Incrementor.cs 100.00% <100.00%> (ø)

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Do you have feedback about the report comment? Let us know in this issue.

codecov[bot] avatar Jun 14 '23 18:06 codecov[bot]

@atifaziz, I believe all your requests have been completed. I have abstracted the logic such that there is now only one method used by Next and Prev for getting an occurrence called CrontabSchedule.TryGetOccurrence. Please let me know if there is something else needed here. Thanks for your consideration.

Note: @SammyROCK, there were some bugs that was in the version of my fork that you cloned. You may want to pull down the latest.

DaveRMaltby avatar Jun 14 '23 19:06 DaveRMaltby

Hi there! What is this "prev" feature's status? I also would need it, and I see that it is almost ready.

zorgoz avatar Jan 19 '24 11:01 zorgoz

Hi just coming in to chime this would be an awesome feature, I wrote a hacky solution to achieve the same thing, but would love a built in version!

ryanzwe avatar Mar 12 '24 02:03 ryanzwe