Skip to content

Commit 2dfa32a

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

File tree

14 files changed

+1099
-19
lines changed

14 files changed

+1099
-19
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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.TimeZones, 2030);
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 firstTimeZone = calendar.TimeZones.First(tz => tz.TzId == "Custom/Timezone");
74+
var timeZone = firstTimeZone.ToDateTimeZone(2000);
75+
76+
var intervals = timeZone?
77+
.GetZoneIntervals(Instant.FromUtc(1970, 1, 1, 0, 0, 0),
78+
Instant.FromUtc(1970, 3, 8, 8, 0, 0))
79+
.ToList();
80+
81+
var standardInterval = intervals?.FirstOrDefault(i => i.Name == "CST");
82+
var daylightInterval = intervals?.FirstOrDefault(i => i.Name == "CDT");
83+
84+
Assert.Multiple(() =>
85+
{
86+
Assert.That(timeZone, Is.Not.Null);
87+
Assert.That(timeZone?.Id, Is.EqualTo("Custom/Timezone"));
88+
89+
Assert.That(intervals, Is.Not.Null);
90+
Assert.That(intervals, Has.Count.EqualTo(2));
91+
92+
Assert.That(standardInterval, Is.Not.Null);
93+
Assert.That(standardInterval?.WallOffset, Is.EqualTo(Offset.FromHours(-5)));
94+
95+
Assert.That(daylightInterval, Is.Not.Null);
96+
Assert.That(daylightInterval?.WallOffset, Is.EqualTo(Offset.FromHours(-4)));
97+
});
98+
}
99+
100+
[Test, Category(nameof(CalTimeZoneProvider))]
101+
[TestCase("UTC", 0)]
102+
[TestCase("UTC+01:00", 1)]
103+
[TestCase("UTC-05:00", -5)]
104+
public void VDateTimeZone_ShouldReturnOffsetZone_UtcAndUtcOffset(string tzId, int expectedOffsetHours)
105+
{
106+
var ics = IcsFiles.EventWithCustomTz;
107+
var calendar = Calendar.Load(ics);
108+
109+
var provider = new CalTimeZoneProvider();
110+
provider.AddRangeFrom(calendar.TimeZones, 2030);
111+
112+
var result = provider.GetZoneOrNull(tzId);
113+
114+
Assert.Multiple(() =>
115+
{
116+
Assert.That(result, Is.Not.Null);
117+
Assert.That(result!.MaxOffset, Is.EqualTo(Offset.FromHours(expectedOffsetHours)));
118+
Assert.That(result.Id, Is.EqualTo(provider[tzId].Id));
119+
});
120+
}
121+
122+
[Test, Category(nameof(CalTimeZoneProvider))]
123+
[TestCase("UTC", 0)]
124+
[TestCase("UTC+01:00", 1)]
125+
[TestCase("UTC-02:00", -2)]
126+
public void Provider_ShouldHandle_UtcAndUtcOffset(string utcTzId, int offset)
127+
{
128+
var toConvert = new CalDateTime(2023, 6, 1, 14, 0, 0, "UTC");
129+
130+
// Act
131+
// The custom provider can delegate to
132+
// DateTimeZone.Utc and DateTimeZone.Offset
133+
TimeZoneResolvers.TimeZoneResolver = _provider.GetZoneOrNull;
134+
135+
var result = toConvert.ToTimeZone(utcTzId);
136+
137+
Assert.Multiple(() =>
138+
{
139+
Assert.That(result.TzId, Is.EqualTo(utcTzId));
140+
Assert.That(result.Value, Is.EqualTo(toConvert.Value.AddHours(offset)));
141+
});
142+
}
143+
144+
[Test, Category(nameof(CalTimeZoneProvider))]
145+
public void Occurrences_WithCustomTimeZone()
146+
{
147+
var ics = IcsFiles.EventWithCustomTz;
148+
var calendar = Calendar.Load(ics);
149+
var calTimeZoneProvider = new CalTimeZoneProvider();
150+
calTimeZoneProvider.AddRangeFrom(calendar.TimeZones, 2030);
151+
152+
TimeZoneResolvers.TimeZoneResolver = tzId =>
153+
TimeZoneResolvers.Default(tzId)
154+
// use custom timezones from the calendar if not found in the default resolver
155+
?? calTimeZoneProvider.GetZoneOrNull(tzId);
156+
157+
var occ = calendar.GetOccurrences().ToList();
158+
159+
Assert.Multiple(() =>
160+
{
161+
Assert.That(occ, Has.Count.EqualTo(1));
162+
Assert.That(occ[0].Period.StartTime, Is.EqualTo(new CalDateTime(2025, 11, 1, 9, 0, 0, "Custom/Timezone")));
163+
// Offset is -4 hours for daylight saving time from 2025-03-08
164+
Assert.That(occ[0].Period.StartTime.AsUtc, Is.EqualTo(new DateTime(2025, 11, 1, 13, 0, 0, DateTimeKind.Utc)));
165+
});
166+
}
167+
}
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)