-
Notifications
You must be signed in to change notification settings - Fork 248
New recurrence evaluator #901
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: version/6.0
Are you sure you want to change the base?
Conversation
|
Thanks for this PR, @maknapp! This looks like a very promising step towards version 6.0 in terms of the recurrence evaluation. Currently Variable Shadowing: I noticed several places where local variables or parameters hide class-level fields with the same name. This is error-prone. Can we rename the local vars or prefix class members with _ to ensure we don't accidentally use the wrong one (like
Redundant Logic: There are some if...else if blocks that seem to re-check conditions or could be simplified. flattening these with guard clauses or using switch expressions would make the evaluation logic much easier to follow. To make the review easier and ensure the long-term health of the repo, would you mind resolving the Blocker/High issues identified in the Sonar report? Regarding Coverage the 4.7% decrease in overall coverage may be caused by Thanks for the massive effort on the new evaluator. |
5c2e556 to
a9392b9
Compare
a9392b9 to
be27fd6
Compare
Replaced.
Done.
Yes :D
Redundant within the same method? I just split some of the more complex methods, which may have been the areas you were talking about.
I fixed some of them. The "break out of recursion" issues do not make sense - they generators intended to run forever. The remaining For the minor Linq issues, some of those cannot use Linq without affecting allocations. The simple ones can use Linq, but it is easier to debug without it. |
minichma
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wow, that's great work! Couldn't go through much of the code yet, so here are just some very first thoughts.
Some time ago I have also been working on some of the aspects you are addressing in this PR (e.g. caching). Unfortunately I didn't manage to complete it but just FYI, this is the branch, should you be interested. Just keep in mind that its work in progress:
https://github.com/minichma/ical.net/tree/work/minichma/feature/modernize_rrule2
Ical.Net/Evaluation/ByRuleValues.cs
Outdated
| private readonly int[] normalSeconds; | ||
|
|
||
| private readonly int[] byWeekNo; | ||
| private readonly NormalValues<int, int> weeks; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add comment: what is the cache key? In this case it is the number of weeks per year, right?
Ical.Net/Evaluation/ByRuleValues.cs
Outdated
| private readonly NormalValues<int, int> weeks; | ||
|
|
||
| private readonly int[] byYearDay; | ||
| private readonly NormalValues<int, int> yearDays; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this case the year is used as cache key. Followed the pattern of the weeks property, we could also use the number of days in the year as cache key. This way the number of variants would be limited (i.e. 365 or 366) and we could cache all of them. We could simply maintain something like Dictionary<int, NormalValues<int>> weeksByYearDays where the number of days per year would be the key. The NormalValues type itself wouldn't need to hold the cache key (which would be nice as caching is quite a different concern than holding the values). This way the number of cache misses could be improved even further. Same applies to other NormalValues properties.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I changed it to start caching all values with Dictionary. Benchmarks are generally faster with similar allocation. Is this okay to do with BYSETPOS though? How many different set sizes can a single rrule produce?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this okay to do with BYSETPOS though?
Good question. I guess it should be ok for BYSETPOS as well. The date part of an occurrence can have no more than 366 values per interval (days in a yearly recurrence), so 366 is a hard upper bound for the number of different set sizes (the time part doesn't affect this number, because it won't vary across intervals). The real max number of different set sizes will certainly be significantly lower, not sure what it is, but even 366 won't cause any issues. So I guess we're safe in this respect.
For the code logic this is the case. The analyzer just insists on every code path should |
Yes, absolutely, and therefore we should avoid Linq here for now. (Microsoft says this will be improved with net10.0.) |
Increase code coverage for
|
| // Helper that checks whether the given candidate matches any BYMONTHDAY entry | ||
| // taking negative values into account (relative to the month's length). | ||
| static bool MatchesAnyMonthDay(ZonedDateTime candidate, IEnumerable<int> monthDays) | ||
| if (weekDays.Length == 0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this ever be true at this stage? From my understanding the checks happen long before. So maybe remove it here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should never be true right now, but it is a helpful check. Future code changes can mistakenly make it reachable, and rest of the method does not support an empty array.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then System.Diagnostics.Debug.Assert(weekDays.Length != 0, "Weekdays must not be empty."); ?
| // optimize the start time for selecting candidates | ||
| // (only applicable where a COUNT is not specified) | ||
| if (pattern.Count is null && periodStartDt is not null) | ||
| throw new EvaluationException("Invalid frequency type"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add the invalid _frequency to the message?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are all of "not supported" -> "invalid" okay now?
| } | ||
| return StartByRules(); | ||
| } | ||
| else if (_rule.HasNegativeSetPos) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove (all) the redundant else then the if returns a value?
IMHO this would be easier to read.
| // Expand behavior | ||
| return dates.SelectMany(d => pattern.ByMonth | ||
| .Select(month => d.LocalDateTime.PlusMonths(month - d.Month).InZoneLeniently(d.Zone))); | ||
| throw new EvaluationException($"BYDAY offsets are not supported in {_frequency}"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refer to invalidity in RFC5545 instead of "support".
| } | ||
| else if (_frequency == FrequencyType.Weekly) | ||
| { | ||
| throw new EvaluationException("BYMONTHDAY is not supported in WEEKLY"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refer to invalidity in RFC5545 instead of "support"?
| } | ||
| else | ||
| { | ||
| throw new EvaluationException($"BYYEARDAY is not supported with {_frequency}"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refer to invalidity in RFC5545 instead of "support"?
| { | ||
| date = date.Next(targetDayOfWeek); | ||
| } | ||
| // Ignore values outside of the calendar year |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"of" is redundant?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was confused by this and had to look it up. This appears to be a US vs British English difference. :D
| } | ||
| else | ||
| { | ||
| throw new EvaluationException($"BYWEEKNO is not supported with {_frequency}"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Refer to invalidity in RFC5545 instead of "support"?
| .At(_seed.TimeOfDay) | ||
| .InZoneLeniently(_seed.Zone), | ||
|
|
||
| _ => throw new EvaluationException("Invalid frequency") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add the invalid _frequency value to the message?
| Assert.That(result, Is.EqualTo(expected)); | ||
| } | ||
|
|
||
| [Test] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Optional: Add the tests to RecurrenceTestCases.txt
(Avoids a lot of boilerplate code)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tests in RecurrenceTestCases.txt are all UTC events evaluated to "US-Eastern" time zone. These tests require the CalendarEvent to be in a specific time zone, so they cannot be added to the txt file.
axunonb
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is great work and excellent code, thanks a lot! Good to have you working on ical.net.
See comments in files and conversation.
There is no place to For the various |
A topic for another isssue/pr? |
Marked as accepted in SonarCloud |
| INSTANCES:20260601,20260908,20261208 | ||
|
|
||
| ############################## END ERRATA 1913 TESTS ############################## | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
👍
|
Now that |
d817ff7 to
743e7e9
Compare
|



This evaluator follows the same basic logic as the current one, but with a few key differences:
IsCandidateSetFullyExpanded.periodStartby skipping directly to it instead of looping. There is a new test in RecurrenceTestCases.txt commented out because it takes forever on the current evaluator.BugByWeekNoNotWorking()test was changed to YEARLY because the WEEKLY+BYWEEKNO is invalid.IncrementDate()does. This makes the seed value require minimal changes and no extra BY rules.ProcessRecurrencePattern()is NOT applied. No extra BY rules are added to evaluation.Before (be1d758):
After: