-
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
VEVENT
duration not handled correctly in case of a DST change between DTSTART
and DTEND
#660
Comments
No, it's not about local time, but floating time:. var occurrences = calendar.GetOccurrences(new CalDateTime("20241128")).ToList(); A floating time can be converted to a zoned time, public IDateTime ToTimeZone(string otherTzId)
{
// That makes the difference:
if (IsFloating) return new CalDateTime(_dateOnly, _timeOnly, otherTzId);
var zonedOriginal = DateUtil.ToZonedDateTimeLeniently(Value, TzId);
var converted = zonedOriginal.WithZone(DateUtil.GetZone(otherTzId));
return converted.Zone == DateTimeZone.Utc
? new CalDateTime(converted.ToDateTimeUtc(), UtcTzId)
: new CalDateTime(converted.ToDateTimeUnspecified(), otherTzId);
} Does this make sense? EDIT: Okay for here the value is date-only and remains floating. To convert, it would need a time. |
I apologize for causing confusion, by updating the test case in the description multiple times. My assumption regarding #656 was, that it would be fixed with #658, but that it would be likely to persist, if timezones would be specified explicitly. So I intended to create a test, with no floating time involved, so we can analyze this independently. However, in the first version of the description the test case did contain floating time. Given the updated test case without floating time involved, will it still be fixed by #658? |
The test method is now updated as follows, and it fails with one event on The case is explicitly mentioned here: Looks like something's wrong the endTime in the where section. Couldn't find out why yet. [Edit] [Test]
public void GetOccurrenceShouldExcludeDtEndZoned()
{
var ical = """
BEGIN:VCALENDAR
VERSION:2.0
PRODID:Video Wall
BEGIN:VEVENT
UID:VW6
DTSTAMP:20240630T000000Z
DTSTART;TZID=Europe/London;VALUE=DATETIME:20241001T000000
DTEND;TZID=Europe/London;VALUE=DATETIME:20241128T000000
SUMMARY:New home speech.mp4
COMMENT:New location announcement; may need update before Thanksgiving
END:VEVENT
END:VCALENDAR
""";
var calendar = Calendar.Load(ical);
var occurrences = calendar.GetOccurrences(new CalDateTime("20241128T000000", "Europe/London"), new CalDateTime("20250101T000000", "Europe/London")).ToList();
Assert.That(occurrences, Is.Empty); // fails
} |
I assume it's not the 'where', it's that dtend is considered to be at 20241128T010000, which is caused by faulty calculations. |
Omg, just noted, that the test was still wrong, due to the wrong value type. Fixed again, but probably doesn't make any difference. |
Omg, I should have noticed this. But yes, it still fails because the faulty dtend (not the whole where clause) |
This comment was marked as outdated.
This comment was marked as outdated.
Why do we get 1 occurrence instead of none: ical.net/Ical.Net/Evaluation/EventEvaluator.cs Lines 41 to 65 in add58a9
|
@minichma Looks like the cause is identified - not sure how a fix could look like. |
@minichma If we subtract the difference in zoned time (aka public virtual TimeSpan GetFirstDuration()
{
if (Properties.ContainsKey("DURATION"))
return Duration;
if (DtStart is not null)
{
if (DtEnd is not null)
{
// before (subtracts AsUtc): return DtEnd.Subtract(DtStart);
return DtEnd.Value - DtStart.Value; // new
}
else if (!DtStart.HasTime)
{
// For cases where a "VEVENT" calendar component
// specifies a "DTSTART" property with a DATE value type but no
// "DTEND" nor "DURATION" property, the event's duration is taken to
// be one day.
return TimeSpan.FromDays(1);
}
else
{
// For cases where a "VEVENT" calendar component
// specifies a "DTSTART" property with a DATE-TIME value type but no
// "DTEND" property, the event ends on the same calendar date and
// time of day specified by the "DTSTART" property.
return TimeSpan.Zero;
}
}
// This is an illegal state. We return zero for compatibility reasons.
return TimeSpan.Zero;
} |
Proposal for public virtual TimeSpan GetFirstDuration()
{
if (Properties.ContainsKey("DURATION"))
return Duration;
if (DtStart is { HasTime: false } && DtEnd is null)
{
// For cases where a "VEVENT" calendar component
// specifies a "DTSTART" property with a DATE value type but no
// "DTEND" nor "DURATION" property, the event's duration is taken to
// be one day.
return TimeSpan.FromDays(1);
}
// we can't calculate a duration
if (DtStart == null || DtEnd == null) return default;
// For NodaTime, we need a time zone to calculate the duration
if (DtStart.IsFloating || DtEnd.IsFloating)
{
return DtEnd.Value - DtStart.Value;
}
// Handle time zones and DST transitions.
var startZoned = DateUtil.ToZonedDateTimeLeniently(DtStart.Value, DtStart.TzId);
var endZoned = DateUtil.ToZonedDateTimeLeniently(DtEnd.Value, DtEnd.TzId);
// Nominal Duration:
// This is the duration calculated based on the local date and time values, ignoring time
// zones and DST transitions. It represents the difference in calendar time.
var duration = NodaTime.Period.Between(startZoned.LocalDateTime, endZoned.LocalDateTime, PeriodUnits.Seconds).ToDuration();
// Actual Duration:
// This is the duration calculated based on the Instant values,
// which represent a specific point in time in UTC. It takes into account the time
// zones and DST transitions, providing the actual elapsed time between the two points
// in time in UTC.
// It takes into account the time zones and DST transitions.
// var duration = endZoned.ToInstant() - startZoned.ToInstant();
return duration.ToTimeSpan();
} |
Yeah, that's a horrible topic. And its why I will try to comment in more detail the next days. |
I think there are multiple issues involved here. The most basic one is that the current implementation of public static IEnumerable<TestCaseData> TestSomethingTestCases { get; } = [
new TestCaseData(new CalDateTime(2024, 10, 27, 0, 0, 0, tzId: null), TimeSpan.FromHours(4)),
new TestCaseData(new CalDateTime(2024, 10, 27, 0, 0, 0, tzId: CalDateTime.UtcTzId), TimeSpan.FromHours(4)),
new TestCaseData(new CalDateTime(2024, 10, 27, 0, 0, 0, tzId: "Europe/Paris"), TimeSpan.FromHours(4)),
];
[Test, TestCaseSource(nameof(TestSomethingTestCases))]
public void TestSomething(CalDateTime t, TimeSpan d)
{
Assert.Multiple(() =>
{
Assert.That(t.Add(d).Subtract(d), Is.EqualTo(t));
Assert.That(t.Add(d).Subtract(t), Is.EqualTo(d));
});
} My first thought would be that we should probably remove all the arithmetic operator overloads, because they are ambiguous, and replace the
where the 'Nominal' variants can only be used if both values have the same tzId set. The `GetFirstDuration´ should probably be replaced by something like public struct EventDuration(TimeSpan nominalPart, TimeSpan exactPart)
{
public TimeSpan NominalPart => nominalPart;
public TimeSpan ExactPart => exactPart;
}
public EventDuration GetEffectiveDuration() {
...
} The |
Great finding
Agree |
Let's track arithmetic operators with #670 |
Isn't this
|
Yes, this is valid, but to my understanding no, this is not supposed to use nominal time, but exact time according to the RFC
This is somewhat counterintuitive if the TZ is the same for DTSTART and DTEND, but if the TZ is different like in your example, then there is no meaningful way of calculating a nominal duration, so this probably is the reason for exact duration being specified in this case. |
This was what I not assumed, before looking around what others are doing. PHP/* Run online with https://onlinephp.io/ */
<?php
$start = new DateTime('2024-10-01 00:00:00', new DateTimeZone('America/New_York'));
$occurrences = [];
$interval = new DateInterval('P1D'); // Assuming daily occurrences
for ($i = 0; $i < 60; $i++) { // Check for 60 days to cover the DST change
$occurrence_start = clone $start;
$occurrence_start->add(new DateInterval('P' . $i . 'D'));
$occurrence_end = clone $occurrence_start;
$occurrence_end->setTimezone(new DateTimeZone('Europe/London'));
$occurrences[] = [
'start' => $occurrence_start->format('Y-m-d H:i:s T'),
'end' => $occurrence_end->format('Y-m-d H:i:s T')
];
}
echo "Occurrences around DST change:\n";
foreach ($occurrences as $occurrence) {
echo "Start: " . $occurrence['start'] . " - End: " . $occurrence['end'] . "\n";
}
?> Output:
Python:from datetime import datetime, timedelta
import pytz
# Define start time with time zone
start = datetime(2024, 10, 1, 0, 0, 0, tzinfo=pytz.timezone('America/New_York'))
occurrences = []
interval = timedelta(days=1) # Assuming daily occurrences
for i in range(60): # Check for 60 days to cover the DST change
occurrence_start = start + i * interval
occurrence_end = occurrence_start.astimezone(pytz.timezone('Europe/London'))
occurrences.append({
'start': occurrence_start.astimezone(pytz.timezone('America/New_York')).strftime('%Y-%m-%d %H:%M:%S %Z%z'),
'end': occurrence_end.strftime('%Y-%m-%d %H:%M:%S %Z%z')
})
print("Occurrences around DST change:")
for occurrence in occurrences:
print(f"Start: {occurrence['start']} - End: {occurrence['end']}") Output:
|
@minichma Forgive me for dealing with “nominal” and “exact” once again, and being verbose. Nominal vs accurate in terms of durationRFC 5545 3.3.6 says: "The format can represent nominal durations (weeks and days) and accurate durations (hours, minutes, and seconds)." So it is just description of the duration, that is accurate or nominal. So when we have
although the duration is expressed nominal (in RFC terms), it still ends up in accurate occurrence durations that consider the actual start and end times, including any DST changes:
The method Currently, the problem comes from
that calculates the real-world, accurate duration, while is should just use the pure date/time difference. That's the cause of wrong occurrences. This being said, my conclusion also was, that It could well be that we have talked past each other so far because of the word "duration" having different meanings in section 3.6.1 and |
Yes absolutely. Probably I was somewhat unclear and we are also mixing two related but different topics.
|
Updated `CalendarEvent` and `EventEvaluator` to enhance event handling, particularly around time zones and daylight saving time transitions. Key changes include: - Enabled nullable reference types in `CalendarEvent` and `EventEvaluator.` - Updated `CalendarEvent` to throw `InvalidOperationException` when trying to set both `DtEnd` and `Duration`. - Event end time and duration calculations in `EventEvaluator` reflect any DST transitions. - Added new test class `RecurrenceTests_From_Issues` to validate changes with various edge cases and scenarios. Resolves ical-org#660 Resolves ical-org#623 Resolves ical-org#671 Resolves ical-org#567 Resolves ical-org#342 Resolves ical-org#298
Updated `CalendarEvent` and `EventEvaluator` to enhance event handling, particularly around time zones and daylight saving time transitions. Key changes include: - Enabled nullable reference types in `CalendarEvent` and `EventEvaluator.` - Updated `CalendarEvent` to throw `InvalidOperationException` when trying to set both `DtEnd` and `Duration`. - Event end time and duration calculations in `EventEvaluator` reflect any DST transitions. - Added new test class `RecurrenceTests_From_Issues` to validate changes with various edge cases and scenarios. Resolves ical-org#660 Resolves ical-org#623 Resolves ical-org#671 Resolves ical-org#567 Resolves ical-org#342 Resolves ical-org#298
The following test fails, because there is a DST change between DTSTART and DTEND, which is not handled correctly:
Related issues may appear in cases with recurring events where some of the occurrences do have DST changes between DTSTART and DTEND and others don't.
The following specification from RFC 5545 3.8.5.3 is also related to the topic, because it makes a distinction between the duration being specified via DTEND or via DURATION. Also related to #598
This is a follow-up of #656. While #656 discusses the case that DTSTART/DTEND are specified in floating time, this issue handles the slightly different case that DTSTART/DTEND specify a time zone.
The text was updated successfully, but these errors were encountered: