-
Notifications
You must be signed in to change notification settings - Fork 233
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
Refactor to use DateOnly and TimeOnly for date/time handling #658
Conversation
39c43e4
to
7a72ba1
Compare
Increased `CalDateTime` immutability CalDateTime: * `Date` is of type `DateOnly` (breaking vs. v4) * Make `UtzTzId` a public string * Remove `DateOnlyValue` * Remove `HasDate` (always true) (breaking vs. v4) * Remove overload CalDateTime(int year, int month, int day, string? tzId) (breaking vs. v4) * `HasTime` only has a getter (breaking vs. v4) * `Time` is a getter of type `TimeOnly`. Values are always rounded to the nearest second (breaking vs. v4) * Remove `TimeOnlyValue` * `Value` only has a getter (breaking vs. v4) * `TzId` only has a getter (breaking vs. v4) * Remove `AsSystemLocal` (breaking vs. v4) * Remove unused `TimeSpan? operator -(CalDateTime? left, IDateTime? right)` (breaking vs. v4) * Update Equality operators for comparing to floating date/time (breaking vs. v4) IDateTime (breaking vs. v4): * Remove `AsSystemLocal` * Add explicit bool IsFloating (implicit TzId == null) * `Date` is a getter of type `DateOnly` * `Time` is a getter of type `TimeOnly` * `HasTime` only has a getter * Remove `HasDate` * `Value` only has a getter * `TzId` only has a getter * `ToTimeZone(string?)` argument is a NRT Miscellaneous: * CTORs with `DateTime` arguments persist. Actually they are also comfortable, and now consistently processed * Keep `IDateTime` - maybe in another PR * Transitioned from DateTime to DateOnly and TimeOnly across the codebase for improved date and time handling. * Updated methods, properties, and tests to ensure compatibility with the new approach. * Updated xmldoc
7a72ba1
to
7463f67
Compare
ba7eb3e
to
67cda7c
Compare
383e552
to
dae01d7
Compare
…anged Remove fallback to system's local timezone for floating date/time
dae01d7
to
b8d5b8e
Compare
This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on this overview. Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out the documentation. |
@@ -988,6 +990,6 @@ private static IDateTime MatchTimeZone(IDateTime dt1, IDateTime dt2) | |||
|
|||
return dt1.IsUtc | |||
? new CalDateTime(copy.AsUtc) | |||
: new CalDateTime(copy.AsSystemLocal); |
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.
Isn't the code in MatchTimeZone(IDateTime dt1, IDateTime dt2)
completely useless?
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.
Its not the most elegant code, but it aims to adjust UNTIL, which usually is specified in UTC, to DTSTART's time zone, which usually isn't in UTC. This allows checking comparing the adjusted UNTIL with the generated recurrences without considering the time zone again. So the functionality is needed one way or the other.
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.
That's what I thought, but the way it is implemented looks like a noop. Then I removed the method invocation temporarily with no effect on unit tests. So this is just a hint re recurrences.
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 could easily be wrong, or relevant tests are missing. We'd need a test with UNTIL exactly at the last recurrence and then run the test with a TZ with positive offset and with one with a negative offset. In any case the last recurrence should be returned.
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 2 arguments to MatchTimeZone(IDateTime dt1, IDateTime dt2)
always have the same timezone, so there is nothing to adjust? [Edit] The method in effect is r.Until = r.Until
.
if (r.Until != DateTime.MinValue)
{
r.Until = MatchTimeZone(referenceDate, new CalDateTime(r.Until, referenceDate.TzId)).Value;
}
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.
You are fully right, its a noop. But I strongly believe that something's wrong there, because the conversion needs to be done somewhere. Should be investigated separately. Maybe the translation between UTC and tzid happened in the constructor of CalDateTime and was lost in one of the previous PRs?
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.
Created #663 to track
…t null Reasoning: DATE cannot have a timezone
Just noted, that I didn't submit my review comments from 2d ago, so please read my comments in the context of last Sat. |
Ical.Net/DataTypes/CalDateTime.cs
Outdated
return false; | ||
} | ||
|
||
if (left.IsFloating || right.IsFloating) |
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.
Suggest to add a check if IsFloating
equals. I.e.
if (left.IsFloating != right.IsFloating)
return false;
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.
On the first glance I thought the proposal worked, but it doesn't.
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.
If we don't add this check, we could consider instances as equal even if one is floating and the other has a TZ set, which seems wrong. What would be the reason for that behavior?
new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddSeconds(-1)), includeReferenceDateInResults); | ||
public static HashSet<Occurrence> GetOccurrences(IRecurrable recurrable, IDateTime dt, bool includeReferenceDateInResults) | ||
{ | ||
var endTimeOnly = dt.Time?.Add(TimeSpan.FromSeconds(-1)); |
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.
Be careful, Time could be 0:00 in which case we'd end up with a negative time.
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.
InvalidOperationException: see below
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.
Not sure, I understand. I think the problem still exists. If someone specifies a dt e.g. 20241203T000000, which is legit, then this code would lead to 20241203 T -000001.
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 think, we would introduce a new bug here and should consider fixing it right away.
@@ -988,6 +990,6 @@ private static IDateTime MatchTimeZone(IDateTime dt1, IDateTime dt2) | |||
|
|||
return dt1.IsUtc | |||
? new CalDateTime(copy.AsUtc) | |||
: new CalDateTime(copy.AsSystemLocal); |
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.
Its not the most elegant code, but it aims to adjust UNTIL, which usually is specified in UTC, to DTSTART's time zone, which usually isn't in UTC. This allows checking comparing the adjusted UNTIL with the generated recurrences without considering the time zone again. So the functionality is needed one way or the other.
This is a great improvement regarding readability and maintainability! |
* Updated CalDateTime to truncate time to seconds instead of rounding. * Removed AddMilliseconds and AddTicks methods and related test cases. * Fix: minus and plus operators for cases where time parts are equal * Adjusted EndOfDay method to use AddSeconds(-1) instead of AddTicks(-1) * Removed GetOccurrenceShouldExcludeDtEndZoned test.
Ical.Net/Calendar.cs
Outdated
@@ -205,11 +205,17 @@ public VTimeZone AddTimeZone(VTimeZone tz) | |||
/// <param name="dt">The date for which to return occurrences. Time is ignored on this parameter.</param> | |||
/// <returns>A list of occurrences that occur on the given date (<paramref name="dt"/>).</returns> | |||
public virtual HashSet<Occurrence> GetOccurrences(IDateTime dt) | |||
=> GetOccurrences<IRecurringComponent>(new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddSeconds(-1))); | |||
{ | |||
var endTimeOnly = dt.Time?.Add(TimeSpan.FromSeconds(-1)); |
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.
If Time is 0:00, you'll end up with a negative time.
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.
Track in #662
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 a new issue introduced in this PR, so shouldn't we fix it right away? The behavior is also somewhat different from the previous one. In the previous version we just truncated the time, in the new one we keep the time, if set, and observe the period until the same time next day. Is this intentional?
@@ -152,15 +152,19 @@ public virtual bool IsAllDay | |||
get => !Start.HasTime; | |||
set | |||
{ | |||
// Set whether or not the start date/time | |||
// Set whether the start date/time |
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 don't think this property should be editable. Whether the event is all-day should be specified via DTSTART.
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 just changed the wording of the comment :)
But yes, this one was easy. I removed the setter that was used only in 1 unit test. AllDay
can simply be set with a CalDateTime
without time.
Ical.Net/DataTypes/CalDateTime.cs
Outdated
_tzId = string.Equals(UtcTzId, tzId, StringComparison.OrdinalIgnoreCase) ? UtcTzId : tzId; | ||
_tzId = tzId switch | ||
{ | ||
_ when !timeOnly.HasValue && string.Equals(UtcTzId, tzId, StringComparison.OrdinalIgnoreCase) => UtcTzId, |
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.
Why does UTC receive special handling 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.
thanks, that's redundant
Ical.Net/DataTypes/CalDateTime.cs
Outdated
return false; | ||
} | ||
|
||
if (left.IsFloating || right.IsFloating) |
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.
If we don't add this check, we could consider instances as equal even if one is floating and the other has a TZ set, which seems wrong. What would be the reason for that behavior?
} | ||
else | ||
{ | ||
copy._dateOnly = DateOnly.FromDateTime(newValue); |
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 think we shouldn't silently convert a date-only value to date-time but rather throw an exception. This is tricky in multiple ways. In particular, if DST changes. Maybe related to #660. Do we need those operators at all? If yes, we might want to be more explicit, i.e. have a 'add/subtract nominal' vs 'add/subtract accurate' in correspondence with how RFC5545 distinguishes those cases.
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.
Created #661 for tracking a separate PR
Ical.Net/DataTypes/IDateTime.cs
Outdated
/// </summary> | ||
int Second { get; } | ||
|
||
/// <summary> | ||
/// Gets the millisecond for this date/time value. | ||
/// Gets the millisecond that applies to the <see cref="Value"/>. | ||
/// </summary> | ||
int Millisecond { get; } |
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?
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.
see below
Ical.Net/DataTypes/IDateTime.cs
Outdated
/// </summary> | ||
int Millisecond { get; } | ||
|
||
/// <summary> | ||
/// Gets the ticks for this date/time value. | ||
/// Gets the ticks that applies to the <see cref="Value"/>. | ||
/// </summary> | ||
long Ticks { get; } |
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?
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 question is applicable to other properties, too: Day, Month, Year...
All can be retrieved from the Value
property. We could clean-up, when this would bring any advantage. But not in this PR.
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.
Sure, my thought on Milliseconds and Ticks is just, that they are truncated starting with this PR, so providing getters for them could be confusing and considered inconsistent.
@@ -988,6 +990,6 @@ private static IDateTime MatchTimeZone(IDateTime dt1, IDateTime dt2) | |||
|
|||
return dt1.IsUtc | |||
? new CalDateTime(copy.AsUtc) | |||
: new CalDateTime(copy.AsSystemLocal); |
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.
You are fully right, its a noop. But I strongly believe that something's wrong there, because the conversion needs to be done somewhere. Should be investigated separately. Maybe the translation between UTC and tzid happened in the constructor of CalDateTime and was lost in one of the previous PRs?
new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddSeconds(-1)), includeReferenceDateInResults); | ||
public static HashSet<Occurrence> GetOccurrences(IRecurrable recurrable, IDateTime dt, bool includeReferenceDateInResults) | ||
{ | ||
var endTimeOnly = dt.Time?.Add(TimeSpan.FromSeconds(-1)); |
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.
Not sure, I understand. I think the problem still exists. If someone specifies a dt e.g. 20241203T000000, which is legit, then this code would lead to 20241203 T -000001.
@@ -19,7 +19,7 @@ public static IDateTime StartOfDay(IDateTime dt) | |||
=> dt.AddHours(-dt.Hour).AddMinutes(-dt.Minute).AddSeconds(-dt.Second); | |||
|
|||
public static IDateTime EndOfDay(IDateTime dt) |
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.
We should probably get rid of that. Calculations should be done precisely, adding -1 whatever shouldn't be necessary.
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.
Created issue #662 for a separate PR
Sorry for the many comments. You're working on the big ones here, so many things to consider. |
No need for excuses :) - comments are all very welcome. |
* Made `IsAllDay` in `CalendarEvent.cs` read-only, * Updated `Issue432_AllDay` unit test in `RecurrenceTests.cs` to remove time component from `Start` property and eliminate setting the `IsAllDay` property. (The only place where the setter was used.) * Simplified `CalDateTime.cs` initialization by removing condition related to `UtcTzId` when `timeOnly` is not provided.
3820342
to
5797236
Compare
* `operator ==`: if (left.IsFloating != right.IsFloating) return false; * Add unit test
@minichma PR comments should all all be resolved or addressed |
I'm on a business trip until Sat. Not sure, whether I'll find the time until then. |
Ical.Net/Calendar.cs
Outdated
@@ -205,11 +205,17 @@ public VTimeZone AddTimeZone(VTimeZone tz) | |||
/// <param name="dt">The date for which to return occurrences. Time is ignored on this parameter.</param> | |||
/// <returns>A list of occurrences that occur on the given date (<paramref name="dt"/>).</returns> | |||
public virtual HashSet<Occurrence> GetOccurrences(IDateTime dt) | |||
=> GetOccurrences<IRecurringComponent>(new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddSeconds(-1))); | |||
{ | |||
var endTimeOnly = dt.Time?.Add(TimeSpan.FromSeconds(-1)); |
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 a new issue introduced in this PR, so shouldn't we fix it right away? The behavior is also somewhat different from the previous one. In the previous version we just truncated the time, in the new one we keep the time, if set, and observe the period until the same time next day. Is this intentional?
if (!dt.HasTime && hours % 24 > 0) | ||
var copy = Copy<CalDateTime>(); | ||
var newValue = copy.Value.AddHours(hours); | ||
if (!copy.HasTime && (hours % 24 == 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.
We should consider (in a subsequent PR) to remove the special handling of multiples of 24. This is the kind of magic we had in v4 with midnight. A user wouldn't understand, why adding multiple of 24h behaves differently than adding other values. I suggest to either disallow adding hours to date-only instances altogether (preferred) , or, to always convert to date-time.
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.
new CalDateTime(dt.Date), new CalDateTime(dt.Date.AddDays(1).AddSeconds(-1)), includeReferenceDateInResults); | ||
public static HashSet<Occurrence> GetOccurrences(IRecurrable recurrable, IDateTime dt, bool includeReferenceDateInResults) | ||
{ | ||
var endTimeOnly = dt.Time?.Add(TimeSpan.FromSeconds(-1)); |
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 think, we would introduce a new bug here and should consider fixing it right away.
It's existing code logic though: ical.net/Ical.Net/Evaluation/RecurrenceUtil.cs Lines 16 to 17 in add58a9
[Edit] |
Removed the `endTimeOnly` variable and its usage in the `GetOccurrences` method. The code now directly creates the `CalDateTime` object for the end of the period without adjusting the time component. This change streamlines the code and avoids potential issues related to time adjustments.
Comments resolved or tracked by issues |
The `GetOccurrences` methods in `Calendar.cs` have been simplified by removing the code that adjusted the end time by subtracting one second or one tick. The methods now directly use the start date and the date one day after the start date without adjusting the time component. This change simplifies the logic and removes unnecessary time adjustments. The adjustments are redudant, because public static HashSet<Occurrence> RecurrenceUtil.GetOccurrences(IRecurrable recurrable, IDateTime periodStart, IDateTime periodEnd, bool includeReferenceDateInResults) uses precise "LessThan" for comparing end date/times Resolves ical-org#662
|
The remaining time adjustments were only found in class The adjustments are redundant, because calls finally go to |
Great, thank you! The difference to the previous code is, that the previous version calculated 23:59:59 the previous day, but the new code calculated -0:00:01 the same day. So now its certainly better. |
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.
Great improvement!
Increased
CalDateTime
immutabilityCalDateTime:
Date
is of typeDateOnly
(breaking vs. v4)UtzTzId
a public stringDateOnlyValue
HasDate
(always true) (breaking vs. v4)Millisecond
(breaking vs. v4)Ticks
(breaking vs. v4)HasTime
only has a getter (breaking vs. v4)Time
is a getter of typeTimeOnly
. Values are always rounded to the nearest second (breaking vs. v4)AddMilliseconds
takesdouble
as argument, likeDateTime.AddMilliseconds
(breaking vs. v4)TimeOnlyValue
Value
only has a getter (breaking vs. v4)TzId
only has a getter (breaking vs. v4)AsSystemLocal
(breaking vs. v4)AsDateTimeOffset
(breaking vs. v4)AddMilliseconds
(breaking vs. v4)AddTicks
(breaking vs. v4)TimeSpan? operator -(CalDateTime? left, IDateTime? right)
(breaking vs. v4)IDateTime (breaking vs. v4):
AsSystemLocal
(breaking vs. v4)`AsDateTimeOffset
(breaking vs. v4)`AddMilliseconds
(breaking vs. v4)AddTicks
(breaking vs. v4)Millisecond
(breaking vs. v4)Ticks
(breaking vs. v4)Date
is a getter of typeDateOnly
Time
is a getter of typeTimeOnly
HasTime
only has a getterHasDate
(always true) (breaking vs. v4)Value
only has a getterTzId
only has a getterToTimeZone(string?)
argument is a NRTMiscellaneous:
DateTime
arguments persist. Actually they are also comfortable, and now consistently processedIDateTime
- maybe in another PRResolves #656
Resolves #662