Skip to content

Commit 7abddfb

Browse files
committed
Create Ical.Net.TimeZone namespace and related classes
1 parent 095e392 commit 7abddfb

File tree

14 files changed

+1092
-4
lines changed

14 files changed

+1092
-4
lines changed
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
//
2+
// Copyright ical.net project maintainers and contributors.
3+
// Licensed under the MIT license.
4+
//
5+
6+
#nullable enable
7+
using System;
8+
using System.Linq;
9+
using Ical.Net.DataTypes;
10+
using Ical.Net.TimeZone;
11+
using NodaTime;
12+
using NUnit.Framework;
13+
14+
namespace Ical.Net.Tests;
15+
16+
[TestFixture]
17+
public class CalTimeZoneProviderTests
18+
{
19+
private Func<string, DateTimeZone> _tzResolverBackup = TimeZoneResolvers.TimeZoneResolver;
20+
private CalTimeZoneProvider _provider;
21+
22+
[OneTimeSetUp]
23+
public void Setup()
24+
{
25+
_tzResolverBackup = TimeZoneResolvers.TimeZoneResolver;
26+
27+
// Just needed to create a custom time zone provider
28+
const string dummyTimeZoneIcs =
29+
"""
30+
BEGIN:VCALENDAR
31+
PRODID:-//Dummy//NONSGML ICS//EN
32+
VERSION:2.0
33+
BEGIN:VTIMEZONE
34+
TZID:Custom/TimeZone
35+
BEGIN:STANDARD
36+
TZNAME:STD
37+
DTSTART:20221106T020000
38+
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
39+
TZOFFSETFROM:-0400
40+
TZOFFSETTO:-0500
41+
END:STANDARD
42+
BEGIN:DAYLIGHT
43+
TZNAME:DST
44+
DTSTART:20230312T020000
45+
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
46+
TZOFFSETFROM:-0500
47+
TZOFFSETTO:-0400
48+
END:DAYLIGHT
49+
END:VTIMEZONE
50+
END:VCALENDAR
51+
""";
52+
var c = Calendar.Load(dummyTimeZoneIcs);
53+
_provider = new CalTimeZoneProvider();
54+
_provider.AddRangeFrom(c);
55+
56+
TimeZoneResolvers.TimeZoneResolver = tzId =>
57+
TimeZoneResolvers.Default(tzId)
58+
// use custom timezones from the calendar if not found in the default resolver
59+
?? _provider.GetZoneOrNull(tzId);
60+
}
61+
62+
[OneTimeTearDown]
63+
public void TearDown()
64+
{
65+
TimeZoneResolvers.TimeZoneResolver = _tzResolverBackup;
66+
}
67+
68+
[Test, Category(nameof(CalTimeZoneProvider))]
69+
public void VTimeZoneProvider_ShouldResolve_CustomVTimeZone()
70+
{
71+
var ics = IcsFiles.EventWithCustomTz;
72+
var calendar = Calendar.Load(ics);
73+
var timeZone = calendar.TimeZones.First(tz => tz.TzId == "Custom/Timezone").ToDateTimeZone();
74+
75+
var intervals = timeZone?
76+
.GetZoneIntervals(Instant.FromUtc(1970, 1, 1, 0, 0, 0),
77+
Instant.FromUtc(1970, 3, 8, 8, 0, 0))
78+
.ToList();
79+
80+
var standardInterval = intervals?.FirstOrDefault(i => i.Name == "CST");
81+
var daylightInterval = intervals?.FirstOrDefault(i => i.Name == "CDT");
82+
83+
Assert.Multiple(() =>
84+
{
85+
Assert.That(timeZone, Is.Not.Null);
86+
Assert.That(timeZone?.Id, Is.EqualTo("Custom/Timezone"));
87+
88+
Assert.That(intervals, Is.Not.Null);
89+
Assert.That(intervals, Has.Count.EqualTo(3));
90+
91+
Assert.That(standardInterval, Is.Not.Null);
92+
Assert.That(standardInterval?.WallOffset, Is.EqualTo(Offset.FromHours(-5)));
93+
94+
Assert.That(daylightInterval, Is.Not.Null);
95+
Assert.That(daylightInterval?.WallOffset, Is.EqualTo(Offset.FromHours(-4)));
96+
});
97+
}
98+
99+
[Test, Category(nameof(CalTimeZoneProvider))]
100+
[TestCase("UTC", 0)]
101+
[TestCase("UTC+01:00", 1)]
102+
[TestCase("UTC-05:00", -5)]
103+
public void VDateTimeZone_ShouldReturnOffsetZone_UtcAndUtcOffset(string tzId, int expectedOffsetHours)
104+
{
105+
var ics = IcsFiles.EventWithCustomTz;
106+
var calendar = Calendar.Load(ics);
107+
108+
var provider = new CalTimeZoneProvider();
109+
provider.AddRangeFrom(calendar);
110+
111+
var result = provider.GetZoneOrNull(tzId);
112+
113+
Assert.Multiple(() =>
114+
{
115+
Assert.That(result, Is.Not.Null);
116+
Assert.That(result!.MaxOffset, Is.EqualTo(Offset.FromHours(expectedOffsetHours)));
117+
Assert.That(result.Id, Is.EqualTo(provider[tzId].Id));
118+
});
119+
}
120+
121+
[Test, Category(nameof(CalTimeZoneProvider))]
122+
[TestCase("UTC", 0)]
123+
[TestCase("UTC+01:00", 1)]
124+
[TestCase("UTC-02:00", -2)]
125+
public void Provider_ShouldHandle_UtcAndUtcOffset(string utcTzId, int offset)
126+
{
127+
var toConvert = new CalDateTime(2023, 6, 1, 14, 0, 0, "UTC");
128+
129+
// Act
130+
// The custom provider can delegate to
131+
// DateTimeZone.Utc and DateTimeZone.Offset
132+
TimeZoneResolvers.TimeZoneResolver = _provider.GetZoneOrNull;
133+
134+
var result = toConvert.ToTimeZone(utcTzId);
135+
136+
Assert.Multiple(() =>
137+
{
138+
Assert.That(result.TzId, Is.EqualTo(utcTzId));
139+
Assert.That(result.Value, Is.EqualTo(toConvert.Value.AddHours(offset)));
140+
});
141+
}
142+
143+
[Test, Category(nameof(CalTimeZoneProvider))]
144+
public void Occurrences_WithCustomTimeZone()
145+
{
146+
var ics = IcsFiles.EventWithCustomTz;
147+
var calendar = Calendar.Load(ics);
148+
var calTimeZoneProvider = new CalTimeZoneProvider();
149+
calTimeZoneProvider.AddRangeFrom(calendar);
150+
151+
TimeZoneResolvers.TimeZoneResolver = tzId =>
152+
TimeZoneResolvers.Default(tzId)
153+
// use custom timezones from the calendar if not found in the default resolver
154+
?? calTimeZoneProvider.GetZoneOrNull(tzId);
155+
156+
var occ = calendar.GetOccurrences().ToList();
157+
158+
Assert.Multiple(() =>
159+
{
160+
Assert.That(occ, Has.Count.EqualTo(1));
161+
Assert.That(occ[0].Period.StartTime, Is.EqualTo(new CalDateTime(2025, 11, 1, 9, 0, 0, "Custom/Timezone")));
162+
// Offset is -4 hours for daylight saving time from 2025-03-08
163+
Assert.That(occ[0].Period.StartTime.AsUtc, Is.EqualTo(new DateTime(2025, 11, 1, 13, 0, 0, DateTimeKind.Utc)));
164+
});
165+
}
166+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
BEGIN:VCALENDAR
2+
VERSION:2.0
3+
PRODID:-//Sample Corp//NONSGML Event//EN
4+
BEGIN:VTIMEZONE
5+
TZID:Custom/Timezone
6+
X-LIC-LOCATION:Custom/Timezone
7+
BEGIN:STANDARD
8+
DTSTART:19700101T000000
9+
TZOFFSETFROM:-0400
10+
TZOFFSETTO:-0500
11+
TZNAME:CST
12+
END:STANDARD
13+
BEGIN:DAYLIGHT
14+
DTSTART:19700308T020000
15+
TZOFFSETFROM:-0500
16+
TZOFFSETTO:-0400
17+
TZNAME:CDT
18+
END:DAYLIGHT
19+
END:VTIMEZONE
20+
BEGIN:VEVENT
21+
DTSTART;TZID=Custom/Timezone:20251101T090000
22+
DTEND;TZID=Custom/Timezone:20251101T100000
23+
SUMMARY:Sample Event
24+
END:VEVENT
25+
END:VCALENDAR
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
BEGIN:VCALENDAR
2+
VERSION:2.0
3+
PRODID:-//ical.net//iCal.NET 5.0//EN
4+
BEGIN:VTIMEZONE
5+
TZID:Special Timezone
6+
BEGIN:STANDARD
7+
DTSTART:20231026T020000
8+
TZOFFSETFROM:-0400
9+
TZOFFSETTO:-0500
10+
TZNAME:CST
11+
END:STANDARD
12+
BEGIN:DAYLIGHT
13+
DTSTART:20240310T020000
14+
TZOFFSETFROM:-0500
15+
TZOFFSETTO:-0400
16+
TZNAME:CDT
17+
END:DAYLIGHT
18+
BEGIN:STANDARD
19+
DTSTART:20241026T020000
20+
TZOFFSETFROM:-0400
21+
TZOFFSETTO:-0500
22+
TZNAME:CST
23+
END:STANDARD
24+
BEGIN:DAYLIGHT
25+
DTSTART:20250309T020000
26+
TZOFFSETFROM:-0500
27+
TZOFFSETTO:-0400
28+
TZNAME:CDT
29+
END:DAYLIGHT
30+
BEGIN:STANDARD
31+
DTSTART:20251026T020000
32+
TZOFFSETFROM:-0400
33+
TZOFFSETTO:-0500
34+
TZNAME:CST
35+
END:STANDARD
36+
END:VTIMEZONE
37+
BEGIN:VEVENT
38+
UID:recurring-event
39+
DTSTART;TZID=Special Timezone:20240101T090000
40+
DTEND;TZID=Special Timezone:20240101T100000
41+
RRULE:FREQ=MONTHLY;UNTIL=20251231T235959Z
42+
SUMMARY:Monthly Meeting
43+
END:VEVENT
44+
END:VCALENDAR

Ical.Net.Tests/Ical.Net.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
</ItemGroup>
2727
<ItemGroup>
2828
<EmbeddedResource Include="contrib\libical\icalrecur_test.out" />
29+
<EmbeddedResource Include="contrib\libical\Timezone\**\*" />
2930
</ItemGroup>
3031
<ItemGroup>
3132
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />

Ical.Net.Tests/IcsFiles.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ internal static string ReadStream(string manifestResource)
135135
internal static string Transparency2 => ReadStream("Ical.Net.Tests.Calendars.Serialization.Transparency2.ics");
136136
internal static string Trigger1 => ReadStream("Ical.Net.Tests.Calendars.Serialization.Trigger1.ics");
137137
internal static string UsHolidays => ReadStream("Ical.Net.Tests.Calendars.Serialization.USHolidays.ics");
138+
internal static string EventWithCustomTz => ReadStream("Ical.Net.Tests.Calendars.Timezone.EventWithCustomTz.ics");
139+
internal static string RecEventWithCustomTz => ReadStream("Ical.Net.Tests.Calendars.Timezone.RecEventWithCustomTz.ics");
140+
internal static string AmericaNewYork => ReadStream("Ical.Net.Tests.contrib.libical.Timezone.America_New_York.ics");
141+
internal static string AsiaJerusalem => ReadStream("Ical.Net.Tests.contrib.libical.Timezone.Asia_Jerusalem.ics");
138142
internal static string WeeklyCount1 => ReadStream("Ical.Net.Tests.Calendars.Recurrence.WeeklyCount1.ics");
139143
internal static string WeeklyCountWkst1 => ReadStream("Ical.Net.Tests.Calendars.Recurrence.WeeklyCountWkst1.ics");
140144
internal static string WeeklyCountWkst2 => ReadStream("Ical.Net.Tests.Calendars.Recurrence.WeeklyCountWkst2.ics");
@@ -168,5 +172,4 @@ internal static string ReadStream(string manifestResource)
168172
internal static string RecurrrenceTestCases => ReadStream("Ical.Net.Tests.Calendars.Recurrence.RecurrenceTestCases.txt");
169173

170174
internal static string LibicalIcalrecurTest => ReadStream("Ical.Net.Tests.contrib.libical.icalrecur_test.out");
171-
172175
}

0 commit comments

Comments
 (0)