Simplify and cleanup CalDateTime#934
Conversation
|
@axunonb version/6.0 is protected and never supposed to move. Why was version/6.0 rebased? Losing all commit dates... can we change it back? |
|
See #930 (comment) |
|
I remember reading that comment now and was confused then but skipped over it and forgot to go back. I do not like changing history of any shared/major branches. Looking back to #854 (comment), I was assuming this meant that version/6.0 was eventually going to be renamed to master or merged with a merge commit, not rebased. Rebasing a large branch like this repeatedly to keep up-to-date with master also makes links and history in github more confusing. I would strongly prefer to go back to the original version/6.0 branch and never rebase it. |
|
Sure, feel free to recreate branch version/6.0 from version/6.0-backkup the way you prefer. The latest 3 commit are missing in the backup. |
|
I cannot force push to version/6.0 branch because it is restricted. I pushed a v6 branch which is the original with 2 of the latest commits. The encoding commit 58494b4 is a noop for me and just produces an empty commit. |
The assumption was you can as a maintainer, sorry. You're now entitled to force push, while it is generally blocked. |
|
Still blocked with:
We could just switch to using "v6" (shorter name is better?) and mark it protected.
Sure, that would work too. |
|
@maknapp It would be helpful to rebase this branch before the review. |
|
See previous comment. I still cannot force push version/6.0 branch to revert it back to the previous base commit. This PR is based off of the original version/6.0 branch. |
|
Okay pushing worked now. I think everything is back to what it was except commit 58494b4 which was empty for me. Verify your files are still UTF-8 after switching to current version/6.0 branch. |
git ls-files | ForEach-Object { $b=[IO.File]::ReadAllBytes($_); $e=if($b.Length-ge 3-and $b[0] -eq 0xEF-and $b[1] -eq 0xBB-and $b[2] -eq 0xBF){"UTF-8-BOM"}elseif($b.Length-ge 2-and $b[0] -eq 0xFF-and $b[1] -eq 0xFE){"UTF-16LE"}elseif($b.Length-ge 2-and $b[0] -eq 0xFE-and $b[1] -eq 0xFF){"UTF-16BE"}else{try{[Text.UTF8Encoding]::new($false,$true).GetString($b)|Out-Null;"UTF-8"}catch{"OTHER"}}; if($e -ne "UTF-8"){[PSCustomObject]@{Encoding=$e;File=$_}} } | Format-Table -AutoSizereported no files that are not [Edit]
|
| /// <para/> | ||
| /// See RFC 5545, Section 3.3.4 and 3.3.5. | ||
| /// </summary> | ||
| public sealed class CalDateTime : IFormattable, IEquatable<CalDateTime> |
There was a problem hiding this comment.
The breaking public API changes with no deprecation step should be clearly called out with the break in the PR description. This will be helpful for any changelog/migration docs.
There was a problem hiding this comment.
The breaking changes I collected so far are:
ToTimeZone(string?)removedAsUtcproperty removedValueproperty removedCalDateTime(DateTime, bool hasTime, ...)constructor shape changedCalDateTime(ZonedDateTime)/CalDateTime(Instant)constructors replaced by factories
There was a problem hiding this comment.
Updated PR with breaking changes from main branch (NodaTime constructors are not on main).
| public static CalDateTime UtcNow => new CalDateTime(DateTime.UtcNow, UtcTzId, true); | ||
| /// <param name="value">The value to copy the local date from.</param> | ||
| /// <returns>A new <see cref="CalDateTime"/> with same date as the specified <see cref="DateTime"/>.</returns> | ||
| public static CalDateTime FromDate(DateTime value) => new(LocalDate.FromDateTime(value)); |
There was a problem hiding this comment.
If DATE creation is moved to a static factory like with this method, it should follow the From* / To* convention already in use. The PR description mentions this but doesn't yet clarify the chosen name(s).
There was a problem hiding this comment.
I have been thinking about different options for this. It definitely cannot be FromDateTime() because it does not say a DATE value is being created. Some options:
// Clearly states the type and value used; will saying date-time-date confuse people?
public static CalDateTime FromDateTimeDate(DateTime value);
// Shortest name; implies date only but someone could assume time is included
public static CalDateTime FromDate(DateTime value);
// Change DateOnly constructor to static too?; Might confuse people into thinking value is DateOnly
public static CalDateTime FromDateOnly(DateTime value);
public static CalDateTime FromDateOnly(DateOnly value);
// Longest name; explicitly says type and that it will be a DATE value
public static CalDateTime FromDateTimeDateOnly(DateTime value);Which do you like best, or do you have another name in mind?
There was a problem hiding this comment.
I'd say the cleanest approach is FromDate(...) with overloads:
The name describes the result (a DATE value), not the input type — which is the most important thing to communicate. It also mirrors the BCL precedent of DateOnly.FromDateTime(), where users already understand the pattern of "create this date-focused type from that input". The overload family removes any residual ambiguity:
// Ical.Net\DataTypes\CalDateTime.cs
public static CalDateTime FromDate(int year, int month, int day)
=> new CalDateTime(new LocalDate(year, month, day), null, null);
public static CalDateTime FromDate(DateTime value)
=> new CalDateTime(LocalDate.FromDateTime(value), null, null);
public static CalDateTime FromDate(LocalDate value)
=> new CalDateTime(value, null, null);
#if NET6_0_OR_GREATER
public static CalDateTime FromDate(DateOnly value)
=> new CalDateTime(value.ToLocalDate(), null, null);
#endifOnce a user sees any one overload, all others are predictable.
Another consideration:
If this factory lands, the DateTime DATE-TIME constructor could become a paired FromDateTime(...) static factory:
public static CalDateTime FromDateTime(DateTime value, string? tzId = null) { ... }That would give a symmetric, discoverable pair: FromDate for RFC 5545 DATE, FromDateTime for RFC 5545 DATE-TIME - directly matching the spec's own type names.
Would you follow this idea?
There was a problem hiding this comment.
FromDate goes against BCL precedent, not the same. All From* static constructors that I can think of describe the input type to convert from, never the result (a DATE value). I cannot think of any types with multiple FromX overloads like this.
I would like converting the DateTime constructor to CalDateTime.FromDateTime(). It is generally not good to have both.
If we wanted to match the BCL and NodaTime, it should be just FromDateTimeDate with no overloads. That would be most similar to things like DateTimeOffset.FromUnixTime* or LocalTime.From*.
public static CalDateTime FromDateTimeDate(DateTime value);
public static CalDateTime FromDateTime(DateTime value);
public static CalDateTime FromDateOnly(DateOnly value);
public static CalDateTime FromInstant(Instant value);There was a problem hiding this comment.
From* describes the input - my wrong.
So yes, then this makes sense.
| public CalDateTime ToTimeZone(string? otherTzId) | ||
| /// <param name="defaultZone">The time zone to use if this value has no time zone.</param> | ||
| /// <returns>A zoned date time representing this value in same time zone or the specified time zone.</returns> | ||
| public ZonedDateTime AsZonedOrDefault(DateTimeZone defaultZone) |
There was a problem hiding this comment.
AsZonedOrDefault(DateTimeZone) has an inconsistent name relative to ToZonedDateTime().
As we're tidying the API, this might be a good time to rename it to something like ToZonedDateTimeOrFloating?
There was a problem hiding this comment.
I named this completely different on purpose to prevent people from confusing the intention of the argument. The time zone argument for the ToZonedDateTime methods is for converting to that time zone, whereas this one only adds the specified time zone if there is none.
I am open to other names, but I do not like ToZonedDateTimeOrFloating because my first thought is the result can somehow be floating.
There was a problem hiding this comment.
Maybe ToZonedDateTimeWithDefault? Else leave as is.
There was a problem hiding this comment.
Another option is to remove all ToZonedDateTime() and just rename AsZonedOrDefault() to ToZonedDateTime(). This would remove all ways to directly convert to another time zone. Users can make their own extension methods if they convert time zones all the time.
Will people still get confused and think ToZonedDateTime(DateTimeZone defaultZone) will always output a value in that time zone? Would it still need to be named something like ToZonedDateTimeWithDefault()?
dt.ToZonedDateTime() == dt.AsZonedOrDefault(DateTimeZone.Utc);
dt.ToZonedDateTime(dateTimeZone) == dt.AsZonedOrDefault(dateTimeZone).WithZone(dateTimeZone);
// This one is only used in tests and mostly to prevent having to change tests too much
dt.ToZonedDateTime("America/New_York") == dt.AsZonedOrDefault(tzdb["America/New_York"]).WithZone(tzdb["America/New_York"]);There was a problem hiding this comment.
// Current ToZonedDateTime(DateTimeZone) — ALWAYS converts to the target zone
dt.ToZonedDateTime(americaNewYork); // result is always in America/New_York
// AsZonedOrDefault — only uses the zone if floating
dt.AsZonedOrDefault(americaNewYork); // result MAY NOT be in America/New_YorkUser may expect a conversion-to-zone, not a fallback-if-floating.
There was a problem hiding this comment.
Nice! See some mostly minor inline comments.
As for SonarCloud "Refactor 'GetHashCode' to not reference mutable fields." this is unfortunately the case for many other classes. Can be kept unchanged for now.
Maybe have a quick look at the Codecov Report, especially for CalDateTime decreasing coverage.
[Edit re CalDateTime coverage]:
public ZonedDateTime ToZonedDateTime(): Would be good to cover InvalidOperationException for _tzId == null?
public CalDateTime(LocalDate date) cover ArgumentOutOfRangeException?
The other uncovered methods are related to DateOnly / TimeOnly - not strictly necessary to cover
d1bb7d7 to
e98bc96
Compare
If I remember correctly, this what I mentioned some time ago, and the reply was |
e98bc96 to
c54e562
Compare
|
This latest commit d887305 removes the constructors with separate date and time in favor of just |
Few things come to my mind:
|
Time zone can be ignored for DATE values since they cannot have a time zone. This also verifies that end is not before start for DATE values.
This was only being used in tests.
Explicit names for ToDateTimeUtc and ToDateTimeUnspecified help clarify intention when using DateTime.
That being said, I'm on the fence about being split like this - should probably be all or nothing. All types beyond
Yes.
Yes. |
d887305 to
a66cef3
Compare
a66cef3 to
607a959
Compare
|
I tried to remove all
|
Yes, the sound like a good way to go. |
| /// with the current local date/time and no time zone. | ||
| /// </summary> | ||
| public static CalDateTime Now => new CalDateTime(DateTime.Now, null, true); | ||
| public static CalDateTime Now => new(DateTime.Now); |
There was a problem hiding this comment.
This produces a floating CalDateTime, although it looks like current moment.
Should we remove CalDateTime.Now altogether?
There was a problem hiding this comment.
Since users have the option of using NodaTime entirely, I think it is fine leaving this as-is for those who want to keep using DateTime.
| /// </summary> | ||
| /// <param name="value">The value to copy the local date and time from.</param> | ||
| /// <param name="tzId">The time zone ID.</param> | ||
| public CalDateTime(DateTime value, string? tzId = null) : this( |
There was a problem hiding this comment.
The previous version threw on DateTimeKind.Local. Now we silently get a floating CalDateTime.
If we go this way, XML doc should at minimum call this out explicitly, even if throwing is considered too strict.
There was a problem hiding this comment.
I changed the constructor to FromDateTime and changed the summary to say "Converts a DateTime of any kind to..." (similar to NodaTime.FromDateTime summary). Is that good?
DateTime.Today is calculated from DateTime.Now but NodaTime also does the same calculation, so just use DateTime.Now to get the same result.
|
|
I think this is ready to merge now. We can see how these changes feel and remove/change more later. I removed the time properties to force users to check for null themselves. While adding tests I noticed that |




The intent here is to remove more from CalDateTime while keeping it simple to use. See individual commits for changes. I can change to separate PRs if that is preferred.
Breaking Changes:
ToTimeZone(string?)removedAsUtcproperty removedValueproperty removedCalDateTime(DateTime, string? tzId, bool? hasTime = null)removedCalDateTime(DateTime, bool? hasTime = null)removedCalDateTime(DateOnly date)->CalDateTime FromDateOnly(DateOnly date)(and only .NET 6+)CalDateTime(DateOnly date, TimeOnly? time, string? tzId = null)removedHour,Minute,Secondproperties removed