Skip to content

Commit 1be4a17

Browse files
committed
Refactor Period to be usable with CalDateTimeZoned internally
Update `EventEvaluator` to use `CalDateTimeZoned` with `Period` No change zu the public API Add unit test AmbiguousLocalTime_WithShortDurationOfRecurrence which resolves #737 Make CalDateTimeEvaluator.ToString use existing ZoneDateTime
1 parent cb1745b commit 1be4a17

File tree

6 files changed

+253
-59
lines changed

6 files changed

+253
-59
lines changed

Ical.Net.Tests/PeriodTests.cs

Lines changed: 131 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#nullable enable
77
using System;
88
using Ical.Net.DataTypes;
9+
using Ical.Net.Evaluation;
910
using NUnit.Framework;
1011

1112
namespace Ical.Net.Tests;
@@ -17,21 +18,29 @@ public class PeriodTests
1718
public void CreatePeriodWithArguments()
1819
{
1920
var period = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"));
20-
var periodWithEndTime = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"), new CalDateTime(2025, 1, 1, 1, 0, 0, "America/New_York"));
21-
var periodWithDuration = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"), Duration.FromHours(1));
21+
var periodWithEndTime = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"),
22+
new CalDateTime(2025, 1, 1, 1, 0, 0, "America/New_York"));
23+
var periodWithDuration =
24+
new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"), Duration.FromHours(1));
2225

2326
Assert.Multiple(() =>
2427
{
2528
Assert.That(period.StartTime, Is.EqualTo(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York")));
2629
Assert.That(period.EndTime, Is.Null);
2730
Assert.That(period.Duration, Is.Null);
28-
29-
Assert.That(periodWithEndTime.StartTime, Is.EqualTo(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York")));
30-
Assert.That(periodWithEndTime.EffectiveEndTime, Is.EqualTo(new CalDateTime(2025, 1, 1, 1, 0, 0, "America/New_York")));
31+
Assert.That(period.EffectiveEndTime, Is.Null);
32+
Assert.That(period.EffectiveDuration, Is.Null);
33+
34+
Assert.That(periodWithEndTime.StartTime,
35+
Is.EqualTo(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York")));
36+
Assert.That(periodWithEndTime.EffectiveEndTime,
37+
Is.EqualTo(new CalDateTime(2025, 1, 1, 1, 0, 0, "America/New_York")));
3138
Assert.That(periodWithEndTime.EffectiveDuration, Is.EqualTo(Duration.FromHours(1)));
3239

33-
Assert.That(periodWithDuration.StartTime, Is.EqualTo(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York")));
34-
Assert.That(periodWithDuration.EffectiveEndTime, Is.EqualTo(new CalDateTime(2025, 1, 1, 1, 0, 0, "America/New_York")));
40+
Assert.That(periodWithDuration.StartTime,
41+
Is.EqualTo(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York")));
42+
Assert.That(periodWithDuration.EffectiveEndTime,
43+
Is.EqualTo(new CalDateTime(2025, 1, 1, 1, 0, 0, "America/New_York")));
3544
Assert.That(periodWithDuration.EffectiveDuration, Is.EqualTo(Duration.FromHours(1)));
3645

3746
Assert.That(Period.Create(period.StartTime).Duration, Is.Null);
@@ -42,6 +51,7 @@ public void CreatePeriodWithArguments()
4251
[Test]
4352
public void CreatePeriodWithInvalidArgumentsShouldThrow()
4453
{
54+
// Test with CalDateTime
4555
Assert.Multiple(() =>
4656
{
4757
// EndTime is before StartTime
@@ -62,6 +72,28 @@ public void CreatePeriodWithInvalidArgumentsShouldThrow()
6272
Assert.Throws<ArgumentException>(() => _ = new Period(new CalDateTime(2025, 1, 2, 0, 0, 0),
6373
new CalDateTime(2025, 1, 1)));
6474
});
75+
76+
// Test with CalDateTimeZoned
77+
Assert.Multiple(() =>
78+
{
79+
// EndTime is before StartTime
80+
Assert.Throws<ArgumentException>(() => _ = new Period(
81+
new CalDateTime(2025, 1, 2, 0, 0, 0, "America/New_York").AsZoned(),
82+
new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York").AsZoned()));
83+
84+
// Duration is negative
85+
Assert.Throws<ArgumentException>(() =>
86+
_ = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York").AsZoned(), Duration.FromHours(-1)));
87+
88+
// Timezones are different
89+
Assert.Throws<ArgumentException>(() => _ = new Period(
90+
new CalDateTime(2025, 1, 2, 0, 0, 0, "America/New_York").AsZoned(),
91+
new CalDateTime(2025, 1, 1, 0, 0, 0, "Europe/Vienna").AsZoned()));
92+
93+
// StartTime is date-only while EndTime has time
94+
Assert.Throws<ArgumentException>(() => _ = new Period(new CalDateTime(2025, 1, 2, 0, 0, 0).AsZoned(),
95+
new CalDateTime(2025, 1, 1).AsZoned()));
96+
});
6597
}
6698

6799
[Test]
@@ -82,6 +114,7 @@ public void Timezones_StartTime_EndTime_MustBeEqual()
82114
foreach (var p in periods)
83115
{
84116
Assert.Throws<ArgumentException>(() => _ = new Period(p.Item1, p.Item2));
117+
Assert.Throws<ArgumentException>(() => _ = new Period(p.Item1.AsZoned(), p.Item2.AsZoned()));
85118
}
86119
});
87120
}
@@ -100,4 +133,95 @@ public void CollidesWithPeriod()
100133
Assert.That(period2.CollidesWith(period3), Is.True);
101134
});
102135
}
136+
137+
[Test]
138+
public void CreatePeriodWith_StartTime_and_Duration()
139+
{
140+
var dt = new CalDateTime(2025, 7, 1, 10, 0, 0, "Europe/London");
141+
var periodCal = new Period(dt, Duration.FromHours(1));
142+
var periodZoned = new Period(dt.AsZoned(), Duration.FromHours(1));
143+
144+
Assert.Multiple(() =>
145+
{
146+
Assert.That(periodCal.StartTime, Is.EqualTo(dt));
147+
Assert.That(periodCal.EffectiveEndTime, Is.EqualTo(new CalDateTime(2025, 7, 1, 11, 0, 0, "Europe/London")));
148+
Assert.That(periodZoned.StartTime, Is.EqualTo(dt));
149+
Assert.That(periodZoned.EffectiveEndTime,
150+
Is.EqualTo(new CalDateTime(2025, 7, 1, 11, 0, 0, "Europe/London")));
151+
});
152+
}
153+
154+
[Test]
155+
public void CreatePeriodWith_StartTime_and_EndTime()
156+
{
157+
var dt1 = new CalDateTime(2025, 7, 1, 10, 0, 0, "Europe/London");
158+
var dt2 = new CalDateTime(2025, 7, 1, 11, 0, 0, "Europe/London");
159+
var periodCal = new Period(dt1, dt2);
160+
var periodZoned = new Period(dt1.AsZoned(), dt2.AsZoned());
161+
162+
Assert.Multiple(() =>
163+
{
164+
Assert.That(periodCal.StartTime, Is.EqualTo(dt1));
165+
Assert.That(periodCal.EndTime, Is.EqualTo(dt2));
166+
Assert.That(periodCal.EffectiveEndTime,
167+
Is.EqualTo(new CalDateTime(2025, 7, 1, 11, 0, 0, "Europe/London")));
168+
169+
Assert.That(periodZoned.StartTime, Is.EqualTo(dt1));
170+
Assert.That(periodZoned.EndTime, Is.EqualTo(dt2));
171+
Assert.That(periodZoned.EffectiveEndTime,
172+
Is.EqualTo(new CalDateTime(2025, 7, 1, 11, 0, 0, "Europe/London")));
173+
});
174+
}
175+
176+
[Test]
177+
public void CreatePeriodWith_StartTime_Only()
178+
{
179+
var dt = new CalDateTime(2025, 7, 1, 10, 0, 0, "Europe/London");
180+
var periodCal = new Period(dt);
181+
var periodZoned = new Period(dt.AsZoned());
182+
183+
Assert.Multiple(() =>
184+
{
185+
Assert.That(periodCal.StartTime, Is.EqualTo(dt));
186+
Assert.That(periodCal.EndTime, Is.Null);
187+
Assert.That(periodCal.Duration, Is.Null);
188+
Assert.That(periodCal.EffectiveEndTime, Is.Null);
189+
190+
Assert.That(periodZoned.StartTime, Is.EqualTo(dt));
191+
Assert.That(periodZoned.EndTime, Is.Null);
192+
Assert.That(periodZoned.Duration, Is.Null);
193+
Assert.That(periodZoned.EffectiveEndTime, Is.Null);
194+
});
195+
}
196+
197+
[Test]
198+
public void PeriodCompareToNull_ShouldReturnOne()
199+
{
200+
var dt = new CalDateTime(2025, 7, 1, 10, 0, 0, "Europe/London");
201+
var period = new Period(dt, Duration.FromHours(1));
202+
203+
Assert.That(period.CompareTo(null), Is.EqualTo(1));
204+
}
205+
206+
[Test]
207+
public void PeriodCopyFrom_ShouldBeEquivalentToOriginal()
208+
{
209+
var original = new Period(new CalDateTime(2025, 1, 1, 0, 0, 0, "America/New_York"), Duration.FromHours(10));
210+
var copy = new Period();
211+
copy.CopyFrom(original);
212+
213+
var copyEmpty = new Period(); // internal CTOR
214+
// another ICopyable instance which is not a Period
215+
copyEmpty.CopyFrom(new PeriodList());
216+
217+
Assert.Multiple(() =>
218+
{
219+
Assert.That(copy.StartTime, Is.EqualTo(original.StartTime));
220+
Assert.That(copy.Duration, Is.EqualTo(original.Duration));
221+
222+
Assert.That(copyEmpty.StartTime, Is.Null);
223+
Assert.That(copyEmpty.EndTime, Is.Null);
224+
Assert.That(copyEmpty.Duration, Is.Null);
225+
});
226+
}
103227
}

Ical.Net.Tests/RecurrenceTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4031,4 +4031,37 @@ public void Disallowed_Recurrence_RangeChecks_Should_Throw()
40314031
Assert.That(() => serializer.CheckRange("a", (int?) 0, 1, 2, false), Throws.TypeOf<ArgumentOutOfRangeException>());
40324032
});
40334033
}
4034+
4035+
[Test]
4036+
public void AmbiguousLocalTime_WithShortDurationOfRecurrence()
4037+
{
4038+
// Short recurrence falls into an ambiguous local time
4039+
// for the end time of the second occurrence because
4040+
// of DST transition on 2025-10-25 03:00
4041+
// See also: https://github.com/ical-org/ical.net/issues/737
4042+
var ics = """
4043+
BEGIN:VCALENDAR
4044+
BEGIN:VEVENT
4045+
DTSTART;TZID=Europe/Vienna:20201024T023000
4046+
DURATION:PT45M
4047+
RRULE:FREQ=DAILY;UNTIL=20201025T013000Z
4048+
END:VEVENT
4049+
END:VCALENDAR
4050+
""";
4051+
var cal = Calendar.Load(ics)!;
4052+
var occ = cal.GetOccurrences().ToList();
4053+
4054+
Assert.Multiple(() =>
4055+
{
4056+
Assert.That(occ.Count, Is.EqualTo(2));
4057+
4058+
Assert.That(occ[0].Period.StartTime, Is.EqualTo(new CalDateTime(2020, 10, 24, 2, 30, 0, "Europe/Vienna")));
4059+
Assert.That(occ[0].Period.EndTime, Is.EqualTo(new CalDateTime(2020, 10, 24, 3, 15, 0, "Europe/Vienna")));
4060+
Assert.That(occ[0].Period.EffectiveDuration, Is.EqualTo(new Duration(0, 0, 0, 45, 0)));
4061+
4062+
Assert.That(occ[1].Period.StartTime, Is.EqualTo(new CalDateTime(2020, 10, 25, 2, 30, 0, "Europe/Vienna")));
4063+
Assert.That(occ[1].Period.EndTime, Is.EqualTo(new CalDateTime(2020, 10, 25, 2, 15, 0, "Europe/Vienna")));
4064+
Assert.That(occ[1].Period.EffectiveDuration, Is.EqualTo(new Duration(0, 0, 0, 45, 0)));
4065+
});
4066+
}
40344067
}

Ical.Net/CalendarComponents/FreeBusy.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public class FreeBusy : UniqueComponent, IMergeable
2222
}
2323

2424
var occurrences = occ.GetOccurrences<CalendarEvent>(freeBusyRequest.Start, options)
25-
.TakeWhile(p => (freeBusyRequest.End == null) || (p.Period.StartTime.AsZoned().LessThan(freeBusyRequest.End.AsZoned())));
25+
.TakeWhile(p => (freeBusyRequest.End == null) || (p.Period.Start.LessThan(freeBusyRequest.End.AsZoned())));
2626

2727
var contacts = new List<string>();
2828
var isFilteredByAttendees = false;

0 commit comments

Comments
 (0)