-
Notifications
You must be signed in to change notification settings - Fork 231
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
Daylight saving transition bugs #623
Comments
Here are two more tests that fail, but should pass, using all-day events on the days when the clock goes an hour forward or back. [Fact]
public void ClockGoingForwardAllDayTest()
{
// Arrange
string timeZoneId = "Europe/London";
CalendarEvent myEvent = new CalendarEvent();
myEvent.Start = new CalDateTime(2025, 3, 30, timeZoneId);
myEvent.Start.HasTime = false;
myEvent.End = new CalDateTime(2025, 3, 31, timeZoneId);
myEvent.End.HasTime = false;
Debug.Assert(myEvent.IsAllDay);
Calendar calendar = new Calendar();
calendar.Events.Add(myEvent);
// Act
IDateTime start = new CalDateTime(2025, 3, 30, 0, 0, 0, timeZoneId);
IDateTime end = new CalDateTime(2025, 3, 31, 0, 0, 0, timeZoneId);
ICollection<Occurrence> occurrences = calendar.GetOccurrences<CalendarEvent>(start, end);
// Assert
Assert.Equal(23, myEvent.Duration.TotalHours);
Assert.Single(occurrences);
Occurrence occurrence = occurrences.Single();
Assert.Same(myEvent, occurrence.Source);
Assert.Equal(myEvent.Duration, occurrence.Period.Duration);
Assert.False(occurrence.Period.StartTime.HasTime);
Assert.Equal(myEvent.Start, occurrence.Period.StartTime);
Assert.False(occurrence.Period.EndTime.HasTime);
Assert.Equal(myEvent.End, occurrence.Period.EndTime);
} and [Fact]
public void ClockGoingBackAllDayTest()
{
// Arrange
string timeZoneId = "GMT Standard Time";
CalendarEvent myEvent = new CalendarEvent();
myEvent.Start = new CalDateTime(2024, 10, 27, timeZoneId);
myEvent.Start.HasTime = false;
myEvent.End = new CalDateTime(2024, 10, 28, timeZoneId);
myEvent.End.HasTime = false;
Debug.Assert(myEvent.IsAllDay);
Calendar calendar = new Calendar();
calendar.Events.Add(myEvent);
// Act
IDateTime start = new CalDateTime(2024, 10, 27, 0, 0, 0, timeZoneId);
IDateTime end = new CalDateTime(2024, 10, 28, 0, 0, 0, timeZoneId);
ICollection<Occurrence> occurrences = calendar.GetOccurrences<CalendarEvent>(start, end);
// Assert
Assert.Equal(25, myEvent.Duration.TotalHours);
Assert.Single(occurrences);
Occurrence occurrence = occurrences.Single();
Assert.Same(myEvent, occurrence.Source);
Assert.Equal(myEvent.Duration, occurrence.Period.Duration);
Assert.False(occurrence.Period.StartTime.HasTime);
Assert.Equal(myEvent.Start, occurrence.Period.StartTime);
Assert.False(occurrence.Period.EndTime.HasTime);
Assert.Equal(myEvent.End, occurrence.Period.EndTime);
} UPDATE 1 However, [Fact]
public void ClockGoingForwardAllDayNonLocalTest()
{
// Arrange
string timeZoneId = "Pacific Standard Time";
CalendarEvent myEvent = new CalendarEvent();
myEvent.Start = new CalDateTime(2025, 3, 9, timeZoneId);
myEvent.Start.HasTime = false;
myEvent.End = new CalDateTime(2025, 3, 10, timeZoneId);
myEvent.End.HasTime = false;
Debug.Assert(myEvent.IsAllDay);
Calendar calendar = new Calendar();
calendar.Events.Add(myEvent);
// Act
IDateTime start = new CalDateTime(2025, 3, 9, 0, 0, 0, timeZoneId);
IDateTime end = new CalDateTime(2025, 3, 10, 0, 0, 0, timeZoneId);
ICollection<Occurrence> occurrences = calendar.GetOccurrences<CalendarEvent>(start, end);
// Assert
Assert.Equal(23, myEvent.Duration.TotalHours);
Assert.Single(occurrences);
Occurrence occurrence = occurrences.Single();
Assert.Same(myEvent, occurrence.Source);
Assert.Equal(myEvent.Duration, occurrence.Period.Duration);
Assert.False(occurrence.Period.StartTime.HasTime);
Assert.Equal(myEvent.Start, occurrence.Period.StartTime);
Assert.False(occurrence.Period.EndTime.HasTime);
Assert.Equal(myEvent.End, occurrence.Period.EndTime);
} and [Fact]
public void ClockGoingBackAllDayNonLocalTest()
{
// Arrange
string timeZoneId = "Pacific Standard Time";
CalendarEvent myEvent = new CalendarEvent();
myEvent.Start = new CalDateTime(2024, 11, 3, timeZoneId);
myEvent.Start.HasTime = false;
myEvent.End = new CalDateTime(2024, 11, 4, timeZoneId);
myEvent.End.HasTime = false;
Debug.Assert(myEvent.IsAllDay);
Calendar calendar = new Calendar();
calendar.Events.Add(myEvent);
// Act
IDateTime start = new CalDateTime(2024, 11, 3, 0, 0, 0, timeZoneId);
IDateTime end = new CalDateTime(2024, 11, 4, 0, 0, 0, timeZoneId);
ICollection<Occurrence> occurrences = calendar.GetOccurrences<CalendarEvent>(start, end);
// Assert
Assert.Equal(25, myEvent.Duration.TotalHours);
Assert.Single(occurrences);
Occurrence occurrence = occurrences.Single();
Assert.Same(myEvent, occurrence.Source);
Assert.Equal(myEvent.Duration, occurrence.Period.Duration);
Assert.False(occurrence.Period.StartTime.HasTime);
Assert.Equal(myEvent.Start, occurrence.Period.StartTime);
Assert.False(occurrence.Period.EndTime.HasTime);
Assert.Equal(myEvent.End, occurrence.Period.EndTime);
} I tried a few more tests where I now schedule an all day event in [Fact]
public void ClockGoingForwardAllDayNonLocalTest()
{
// Arrange
string timeZoneId = "Pacific Standard Time";
CalendarEvent myEvent = new CalendarEvent();
myEvent.Start = new CalDateTime(2025, 3, 30, timeZoneId);
myEvent.Start.HasTime = false;
myEvent.End = new CalDateTime(2025, 3, 31, timeZoneId);
myEvent.End.HasTime = false;
Debug.Assert(myEvent.IsAllDay);
Calendar calendar = new Calendar();
calendar.Events.Add(myEvent);
// Act
IDateTime start = new CalDateTime(2025, 3, 30, 0, 0, 0, timeZoneId);
IDateTime end = new CalDateTime(2025, 3, 31, 0, 0, 0, timeZoneId);
ICollection<Occurrence> occurrences = calendar.GetOccurrences<CalendarEvent>(start, end);
// Assert
Assert.Equal(24, myEvent.Duration.TotalHours);
Assert.Single(occurrences);
Occurrence occurrence = occurrences.Single();
Assert.Same(myEvent, occurrence.Source);
Assert.Equal(myEvent.Duration, occurrence.Period.Duration);
Assert.False(occurrence.Period.StartTime.HasTime);
Assert.Equal(myEvent.Start, occurrence.Period.StartTime);
Assert.False(occurrence.Period.EndTime.HasTime);
Assert.Equal(myEvent.End, occurrence.Period.EndTime);
} and [Fact]
public void ClockGoingBackAllDayNonLocalTest()
{
// Arrange
string timeZoneId = "Pacific Standard Time";
CalendarEvent myEvent = new CalendarEvent();
myEvent.Start = new CalDateTime(2024, 10, 27, timeZoneId);
myEvent.Start.HasTime = false;
myEvent.End = new CalDateTime(2024, 10, 28, timeZoneId);
myEvent.End.HasTime = false;
Debug.Assert(myEvent.IsAllDay);
Calendar calendar = new Calendar();
calendar.Events.Add(myEvent);
// Act
IDateTime start = new CalDateTime(2024, 10, 27, 0, 0, 0, timeZoneId);
IDateTime end = new CalDateTime(2024, 10, 28, 0, 0, 0, timeZoneId);
ICollection<Occurrence> occurrences = calendar.GetOccurrences<CalendarEvent>(start, end);
// Assert
Assert.Equal(24, myEvent.Duration.TotalHours);
Assert.Single(occurrences);
Occurrence occurrence = occurrences.Single();
Assert.Same(myEvent, occurrence.Source);
Assert.Equal(myEvent.Duration, occurrence.Period.Duration);
Assert.False(occurrence.Period.StartTime.HasTime);
Assert.Equal(myEvent.Start, occurrence.Period.StartTime);
Assert.False(occurrence.Period.EndTime.HasTime);
Assert.Equal(myEvent.End, occurrence.Period.EndTime);
} But the other tests are supposed to pass as well. UPDATE 2 So it appears I cannot always rely on the duration or end time of an occurrence. If I specify an event as an all-day event, an occurrence of that event should be all-day. Since ical.net uses NodaTime, it should use a one-day Period, not a Duration. A duration can be 23, 24 or 25 hours but a one-day Period is always one day. Similarly if I specify an event from 01:00 to 02:00 on Sunday 27 October 2024 in GMT that should again be a 1 hour Period, not a 1 hour Duration. Perhaps CalendarEvent should get constructor overloads that allows you to specify if you define an event using a Period or Duration. |
@RemcoBlok Thanks a lot. Acknowledge the issues. |
@axunonb could you send me a direct message please? |
@RemcoBlok ✔️ Done via LinkedIn |
Excited to see lots of activity in this repo. If I am not mistaken the iCalendar standard allows for including an offset as well as a time zone. However CalDateTime only takes a DateTime and tzId string. Could we add a constructor overload to CalDateTime to accept a DateTimeOffset and tzId? That way CalDateTime allows for unambiguous times when the clocks go back one hour. Thanks |
Thanks! So this is what you have in mind for CTOR overload? public CalDateTime(DateTimeOffset value, string? tzId)
{
var dateTime = value.DateTime;
// Adjust the DateTime by the offset
var adjustedDateTime = dateTime + value.Offset;
Initialize(new DateOnly(adjustedDateTime.Year, adjustedDateTime.Month, adjustedDateTime.Day),
new TimeOnly(adjustedDateTime.Hour, adjustedDateTime.Minute, adjustedDateTime.Second),
adjustedDateTime.Kind,
tzId,
null);
} |
Hm, that was not what I had in mind. I was thinking of the serialized Date-Time having both the offset and tzid. But I was mistaken thinking that the iCalendar standard would support that. I now found clearly in https://icalendar.org/iCalendar-RFC-5545/3-3-5-date-time.html that storing an offset is not permitted. That means certain times will be ambiguous, in which case Form # 3 says it must be interpreted as the first occurrence, which is what ical.net is currently doing. But that leaves no way to save a Date-Time that represents the second occurrence, unless against the standard we do store an offset. I think changing the offset in your code will not work either as how would deserialization of such a Date-Time be correctly interpreted? Or the evaluation of occurrences. Apologies for my misinformed suggestion on offsets. Of course the above issues with daylight saving transitions are still issues. |
In Ical.Net 4.3.1 I see bugs when getting occurrences for an event around the daylight saving transition when the clock
goes an hour forward or back.
For instance the following test currently fails when the clock goes an hour forward so that between 00:00 and 02:00 a single hour elapsed.
The test fails when asserting
with
Similarly, the following test currently fails when the clock goes back an hour so that between 01:00 and 02:00 two hours elapsed. Note that 01:00 is an ambiguous time, but iCal.Net should use the first occurrence of 01:00 (as NodaTime does by default), therefore two hours elapse between 01:00 and 02:00.
The test fails when asserting
with
The tests should pass, but currently fail.
Since occurrence.Period.Duration is correct, as a workround, I should not use occurrence.Period.EndTime, but calculate the end time of the occurrence period from occurrence.Period.StartTime and occurrence.Period.Duration.
The text was updated successfully, but these errors were encountered: