Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified Ical.Net.Tests/Calendars/Recurrence/Bug2912657.ics
Binary file not shown.
10 changes: 5 additions & 5 deletions Ical.Net.Tests/Calendars/Recurrence/Bug2916581.ics
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ PRODID:-//Microsoft Corporation//Windows Calendar 1.0//EN
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VTIMEZONE
TZID:大阪、札幌、東京
TZID:Asia/Tokyo
BEGIN:STANDARD
DTSTART:20000101T000000
TZNAME:東京 (標準時)
Expand All @@ -14,16 +14,16 @@ END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTAMP:20091217T220735Z
DTSTART;TZID=大阪、札幌、東京:20091225T110000
DTEND;TZID=大阪、札幌、東京:20091225T113000
DTSTART;TZID=Asia/Tokyo:20091225T110000
DTEND;TZID=Asia/Tokyo:20091225T113000
RRULE:FREQ=WEEKLY
SUMMARY:Friday Job
UID:55BEC619-0C7B-48C0-8A17-2F358EA69DDD
END:VEVENT
BEGIN:VEVENT
DTSTAMP:20091217T220758Z
DTSTART;TZID=大阪、札幌、東京:20091226T110000
DTEND;TZID=大阪、札幌、東京:20091226T113000
DTSTART;TZID=Asia/Tokyo:20091226T110000
DTEND;TZID=Asia/Tokyo:20091226T113000
RRULE:FREQ=WEEKLY
SUMMARY:Saturday Job
UID:AD43DEDA-7DE4-4921-BCC2-A66DDFADA41D
Expand Down
6 changes: 3 additions & 3 deletions Ical.Net.Tests/Calendars/Recurrence/Bug2959692.ics
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ BEGIN:VCALENDAR
PRODID:-//Microsoft Corporation//Outlook 11.0 MIMEDIR//EN VERSION:2.0
METHOD:PUBLISH
BEGIN:VTIMEZONE
TZID:Sarajewo, Skopie, Warszawa, Zagrzeb
TZID:Europe/Warsaw
BEGIN:STANDARD
DTSTART:20071028T030000
RRULE:FREQ=YEARLY;INTERVAL=1;BYDAY=-1SU;BYMONTH=10
Expand All @@ -20,8 +20,8 @@ END:DAYLIGHT
END:VTIMEZONE
BEGIN:VEVENT
ORGANIZER:MAILTO:[email protected]
DTSTART;TZID="Sarajewo, Skopie, Warszawa, Zagrzeb":20080103T170000
DTEND;TZID="Sarajewo, Skopie, Warszawa, Zagrzeb":20080103T173000
DTSTART;TZID="Europe/Warsaw":20080103T170000
DTEND;TZID="Europe/Warsaw":20080103T173000
RRULE:FREQ=WEEKLY;INTERVAL=2;BYDAY=TH;WKST=MO
TRANSP:OPAQUE
SEQUENCE:0
Expand Down
6 changes: 3 additions & 3 deletions Ical.Net.Tests/Calendars/Recurrence/Bug2966236.ics
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ PRODID:-//Microsoft Corporation//Outlook 11.0 MIMEDIR//EN
VERSION:2.0
METHOD:PUBLISH
BEGIN:VTIMEZONE
TZID:Beijing
TZID:Asia/Shanghai
BEGIN:STANDARD
DTSTART:16010101T000000
TZOFFSETFROM:+0800
Expand All @@ -12,8 +12,8 @@ TZNAME:Standard Time
END:STANDARD
END:VTIMEZONE
BEGIN:VEVENT
DTSTART;TZID="Beijing":20100119T080000
DTEND;TZID="Beijing":20100119T083000
DTSTART;TZID="Asia/Shanghai":20100119T080000
DTEND;TZID="Asia/Shanghai":20100119T083000
RRULE:FREQ=DAILY;INTERVAL=7;WKST=SU
TRANSP:OPAQUE
SEQUENCE:0
Expand Down
4 changes: 2 additions & 2 deletions Ical.Net.Tests/Calendars/Serialization/Bug2938007.ics
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
BEGIN:VEVENT
DTSTAMP:20100123T152945Z
DTSTART;TZID=大阪、札幌、東京:20100117T000000
DTEND;TZID=大阪、札幌、東京:20100117T003000
DTSTART;TZID=Asia/Tokyo:20100117T000000
DTEND;TZID=Asia/Tokyo:20100117T003000
RRULE:FREQ=WEEKLY;COUNT=3
SUMMARY:WeeklyTest
UID:4F1AD425-E0AE-4480-8393-CA754C5870DE
Expand Down
2 changes: 1 addition & 1 deletion Ical.Net.Tests/RecurrenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2291,7 +2291,7 @@
public void Bug2912657()
{
var iCal = Calendar.Load(IcsFiles.Bug2912657);
var localTzid = iCal.TimeZones[0].TzId;
var localTzid = iCal.Events.First().Start.TzId;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


// Daily recurrence
EventOccurrenceTest(
Expand Down Expand Up @@ -3586,7 +3586,7 @@
[TestCase(null, false)]
[TestCase(CalDateTime.UtcTzId, false)]
[TestCase("America/New_York", true)]
public void DisallowedUntilShouldThrow(string? tzId, bool shouldThrow)

Check warning on line 3589 in Ical.Net.Tests/RecurrenceTests.cs

View workflow job for this annotation

GitHub Actions / coverage

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 3589 in Ical.Net.Tests/RecurrenceTests.cs

View workflow job for this annotation

GitHub Actions / coverage

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 3589 in Ical.Net.Tests/RecurrenceTests.cs

View workflow job for this annotation

GitHub Actions / coverage

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 3589 in Ical.Net.Tests/RecurrenceTests.cs

View workflow job for this annotation

GitHub Actions / tests

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 3589 in Ical.Net.Tests/RecurrenceTests.cs

View workflow job for this annotation

GitHub Actions / tests

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.

Check warning on line 3589 in Ical.Net.Tests/RecurrenceTests.cs

View workflow job for this annotation

GitHub Actions / tests

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
{
var dt = new CalDateTime(2025, 11, 08, 10, 30, 00, tzId);
var recPattern = new RecurrencePattern(FrequencyType.Daily, 1);
Expand Down
6 changes: 3 additions & 3 deletions Ical.Net/CalendarComponents/VTimeZone.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
public class VTimeZone : CalendarComponent
{
public static VTimeZone FromLocalTimeZone()
=> FromDateTimeZone(DateUtil.LocalDateTimeZone.Id);
=> FromDateTimeZone(DefaultTimeZoneResolver.LocalDateTimeZone.Id);

Check warning on line 23 in Ical.Net/CalendarComponents/VTimeZone.cs

View check run for this annotation

Codecov / codecov/patch

Ical.Net/CalendarComponents/VTimeZone.cs#L23

Added line #L23 was not covered by tests

public static VTimeZone FromLocalTimeZone(DateTime earliestDateTimeToSupport, bool includeHistoricalData)
=> FromDateTimeZone(DateUtil.LocalDateTimeZone.Id, earliestDateTimeToSupport, includeHistoricalData);
=> FromDateTimeZone(DefaultTimeZoneResolver.LocalDateTimeZone.Id, earliestDateTimeToSupport, includeHistoricalData);

Check warning on line 26 in Ical.Net/CalendarComponents/VTimeZone.cs

View check run for this annotation

Codecov / codecov/patch

Ical.Net/CalendarComponents/VTimeZone.cs#L26

Added line #L26 was not covered by tests

public static VTimeZone FromSystemTimeZone(TimeZoneInfo tzinfo)
=> FromSystemTimeZone(tzinfo, new DateTime(DateTime.Now.Year, 1, 1), false);
Expand Down Expand Up @@ -318,7 +318,7 @@
Properties.Remove("TZID");
}

_nodaZone = DateUtil.GetZone(value, useLocalIfNotFound: false);
_nodaZone = DateUtil.GetZone(value);
var id = _nodaZone.Id;
if (string.IsNullOrWhiteSpace(id))
{
Expand Down
96 changes: 96 additions & 0 deletions Ical.Net/DefaultTimeZoneResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//
// Copyright ical.net project maintainers and contributors.
// Licensed under the MIT license.
//

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NodaTime;
using NodaTime.TimeZones;

namespace Ical.Net;

public static class DefaultTimeZoneResolver
{
private static Dictionary<string, string> InitializeWindowsMappings()
=> TzdbDateTimeZoneSource.Default.WindowsMapping.PrimaryMapping
.ToDictionary(k => k.Key, v => v.Value, StringComparer.OrdinalIgnoreCase);

private static readonly Lazy<Dictionary<string, string>> _windowsMapping
= new Lazy<Dictionary<string, string>>(InitializeWindowsMappings, LazyThreadSafetyMode.PublicationOnly);

/// <summary>
/// Use this method to turn a raw string into a NodaTime DateTimeZone. It searches all time zone providers (IANA, BCL, serialization, etc) to see if
/// the string matches. If it doesn't, it walks each provider, and checks to see if the time zone the provider knows about is contained within the
/// target time zone string. Some older icalendar programs would generate nonstandard time zone strings, and this secondary check works around
/// that.
/// </summary>
/// <param name="tzId">A BCL, IANA, or serialization time zone identifier</param>
/// <exception cref="ArgumentException">Processing failed</exception>
/// <remarks>The DateTimeZone if found or null otherwise.</remarks>
public static DateTimeZone GetZone(string tzId)
{
var exMsg = $"Unrecognized time zone id {tzId}";

if (string.IsNullOrWhiteSpace(tzId))
{
return DateTimeZoneProviders.Tzdb.GetSystemDefault();
}

if (tzId.StartsWith("/", StringComparison.OrdinalIgnoreCase))
{
tzId = tzId.Substring(1, tzId.Length - 1);
}

var zone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(tzId);
if (zone != null)
{
return zone;
}

if (_windowsMapping.Value.TryGetValue(tzId, out var ianaZone))
{
return DateTimeZoneProviders.Tzdb.GetZoneOrNull(ianaZone) ?? throw new ArgumentException(exMsg);
}

zone = NodaTime.Xml.XmlSerializationSettings.DateTimeZoneProvider.GetZoneOrNull(tzId);
if (zone != null)
{
return zone;

Check warning on line 61 in Ical.Net/DefaultTimeZoneResolver.cs

View check run for this annotation

Codecov / codecov/patch

Ical.Net/DefaultTimeZoneResolver.cs#L61

Added line #L61 was not covered by tests
}

// US/Eastern is commonly represented as US-Eastern
var newTzId = tzId.Replace("-", "/");
zone = NodaTime.Xml.XmlSerializationSettings.DateTimeZoneProvider.GetZoneOrNull(newTzId);
if (zone != null)
{
return zone;
}

var providerId = DateTimeZoneProviders.Tzdb.Ids.FirstOrDefault(tzId.Contains);
if (providerId != null)
{
return DateTimeZoneProviders.Tzdb.GetZoneOrNull(providerId) ?? throw new ArgumentException(exMsg);
}

if (_windowsMapping.Value.Keys
.Where(tzId.Contains)
.Any(pId => _windowsMapping.Value.TryGetValue(pId, out ianaZone))
)
{
return DateTimeZoneProviders.Tzdb.GetZoneOrNull(ianaZone!) ?? throw new ArgumentException(exMsg);
}

providerId = NodaTime.Xml.XmlSerializationSettings.DateTimeZoneProvider.Ids.FirstOrDefault(tzId.Contains);
if (providerId != null)
{
return NodaTime.Xml.XmlSerializationSettings.DateTimeZoneProvider.GetZoneOrNull(providerId) ?? throw new ArgumentException(exMsg);
}

return null;
}

internal static readonly DateTimeZone LocalDateTimeZone = DateTimeZoneProviders.Tzdb.GetSystemDefault();
}
22 changes: 22 additions & 0 deletions Ical.Net/TimeZoneResolvers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
//
// Copyright ical.net project maintainers and contributors.
// Licensed under the MIT license.
//

using System;
using NodaTime;

namespace Ical.Net;

public static class TimeZoneResolvers
{
/// <summary>
/// The default time zone resolver.
/// </summary>
public static Func<string, DateTimeZone> Default => tzId => DefaultTimeZoneResolver.GetZone(tzId);

/// <summary>
/// Gets or sets a function that returns the NodaTime DateTimeZone for the given TZID.
/// </summary>
public static Func<string, DateTimeZone> TimeZoneResolver { get; set; } = Default;
}
94 changes: 6 additions & 88 deletions Ical.Net/Utility/DateUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@

#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Ical.Net.DataTypes;
using NodaTime;
using NodaTime.TimeZones;

namespace Ical.Net.Utility;

Expand Down Expand Up @@ -41,91 +37,15 @@ public static DateTime FirstDayOfWeek(DateTime dt, DayOfWeek firstDayOfWeek, out
return dt;
}

private static readonly Lazy<Dictionary<string, string>> _windowsMapping
= new Lazy<Dictionary<string, string>>(InitializeWindowsMappings, LazyThreadSafetyMode.PublicationOnly);

private static Dictionary<string, string> InitializeWindowsMappings()
=> TzdbDateTimeZoneSource.Default.WindowsMapping.PrimaryMapping
.ToDictionary(k => k.Key, v => v.Value, StringComparer.OrdinalIgnoreCase);

public static readonly DateTimeZone LocalDateTimeZone = DateTimeZoneProviders.Tzdb.GetSystemDefault();

/// <summary>
/// Use this method to turn a raw string into a NodaTime DateTimeZone. It searches all time zone providers (IANA, BCL, serialization, etc) to see if
/// the string matches. If it doesn't, it walks each provider, and checks to see if the time zone the provider knows about is contained within the
/// target time zone string. Some older icalendar programs would generate nonstandard time zone strings, and this secondary check works around
/// that.
/// Returns the NodaTime DateTimeZone for the given TZID according to the
/// current time zone resolver set in TimeZoneResolvers.TimeZoneResolver.
/// </summary>
/// <param name="tzId">A BCL, IANA, or serialization time zone identifier</param>
/// <param name="useLocalIfNotFound">If true, this method will return the system local time zone if tzId doesn't match a known time zone identifier.
/// Otherwise, it will throw an exception.</param>
/// <param name="tzId">A time zone identifier</param>
/// <exception cref="ArgumentException">Unrecognized time zone id</exception>
public static DateTimeZone GetZone(string tzId, bool useLocalIfNotFound = true)
{
var exMsg = $"Unrecognized time zone id {tzId}";

if (string.IsNullOrWhiteSpace(tzId))
{
return LocalDateTimeZone;
}

if (tzId.StartsWith("/", StringComparison.OrdinalIgnoreCase))
{
tzId = tzId.Substring(1, tzId.Length - 1);
}

var zone = DateTimeZoneProviders.Tzdb.GetZoneOrNull(tzId);
if (zone != null)
{
return zone;
}

if (_windowsMapping.Value.TryGetValue(tzId, out var ianaZone))
{
return DateTimeZoneProviders.Tzdb.GetZoneOrNull(ianaZone) ?? throw new ArgumentException(exMsg);
}

zone = NodaTime.Xml.XmlSerializationSettings.DateTimeZoneProvider.GetZoneOrNull(tzId);
if (zone != null)
{
return zone;
}

// US/Eastern is commonly represented as US-Eastern
var newTzId = tzId.Replace("-", "/");
zone = NodaTime.Xml.XmlSerializationSettings.DateTimeZoneProvider.GetZoneOrNull(newTzId);
if (zone != null)
{
return zone;
}

var providerId = DateTimeZoneProviders.Tzdb.Ids.FirstOrDefault(tzId.Contains);
if (providerId != null)
{
return DateTimeZoneProviders.Tzdb.GetZoneOrNull(providerId) ?? throw new ArgumentException(exMsg);
}

if (_windowsMapping.Value.Keys
.Where(tzId.Contains)
.Any(pId => _windowsMapping.Value.TryGetValue(pId, out ianaZone))
)
{
return DateTimeZoneProviders.Tzdb.GetZoneOrNull(ianaZone!) ?? throw new ArgumentException(exMsg);
}

providerId = NodaTime.Xml.XmlSerializationSettings.DateTimeZoneProvider.Ids.FirstOrDefault(tzId.Contains);
if (providerId != null)
{
return NodaTime.Xml.XmlSerializationSettings.DateTimeZoneProvider.GetZoneOrNull(providerId) ?? throw new ArgumentException(exMsg);
}

if (useLocalIfNotFound)
{
return LocalDateTimeZone;
}

throw new ArgumentException(exMsg);
}
public static DateTimeZone GetZone(string tzId)
=> (TimeZoneResolvers.TimeZoneResolver ?? throw new InvalidOperationException())(tzId)
?? throw new ArgumentException($"Unrecognized time zone id {tzId}");

public static ZonedDateTime AddYears(ZonedDateTime zonedDateTime, int years)
{
Expand Down Expand Up @@ -177,8 +97,6 @@ public static ZonedDateTime FromTimeZoneToTimeZone(DateTime dateTime, DateTimeZo
return newZone;
}

public static bool IsSerializationTimeZone(DateTimeZone zone) => NodaTime.Xml.XmlSerializationSettings.DateTimeZoneProvider.GetZoneOrNull(zone.Id) != null;

/// <summary>
/// Truncate to the specified TimeSpan's magnitude. For example, to truncate to the nearest second, use TimeSpan.FromSeconds(1)
/// </summary>
Expand Down
Loading