Skip to content

Commit 4fcfbfd

Browse files
committed
Replace ConcurrentDictionary with ConditionalWeakTable
This is the link for `CalDateTime` objects to is `ZonedDateTime` representation after the first call of `CalDateTimeExtensions.ToTimeZone`. All subsequent conversions use the cached `ZonedDateTime`. Costs for this link are very low.
1 parent 07cd4b8 commit 4fcfbfd

File tree

2 files changed

+21
-53
lines changed

2 files changed

+21
-53
lines changed

Ical.Net.Tests/RecurrenceTests.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4115,8 +4115,6 @@ public void Disallowed_Recurrence_RangeChecks_Should_Throw()
41154115
[Test]
41164116
public void AmbiguousLocalTime_WithShortDurationOfRecurrence()
41174117
{
4118-
CalDateTimeExtensions.CleanupCache();
4119-
41204118
// Short recurrence falls into an ambiguous local time
41214119
// for the end time of the second occurrence because
41224120
// of DST transition on 2025-10-25 03:00

Ical.Net/Evaluation/CalDateTimeExtensions.cs

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44
//
55

66
using System;
7-
using System.Collections.Concurrent;
87
using System.Globalization;
8+
using System.Runtime.CompilerServices;
99
using Ical.Net.DataTypes;
1010
using Ical.Net.Utility;
1111
using NodaTime;
@@ -18,62 +18,44 @@ namespace Ical.Net.Evaluation;
1818
/// </summary>
1919
public static class CalDateTimeExtensions
2020
{
21-
/// <summary>
22-
/// This struct is used as a key for the cache.
23-
/// It holds a weak reference to the <see cref="CalDateTime"/> object.
24-
/// </summary>
25-
private struct WeakCalDateTimeKey : IEquatable<WeakCalDateTimeKey>
21+
private class ZonedDateTimeBox(ZonedDateTime value)
2622
{
27-
private readonly WeakReference<CalDateTime> _weakRef;
28-
private readonly int _hashCode;
29-
30-
public WeakCalDateTimeKey(CalDateTime calDateTime)
31-
{
32-
_weakRef = new WeakReference<CalDateTime>(calDateTime);
33-
_hashCode = calDateTime.GetHashCode();
34-
}
35-
36-
public bool TryGetTarget(out CalDateTime? target) => _weakRef.TryGetTarget(out target);
37-
38-
public bool Equals(WeakCalDateTimeKey other)
39-
{
40-
if (!_weakRef.TryGetTarget(out var thisTarget) || !other._weakRef.TryGetTarget(out var otherTarget))
41-
return false;
42-
43-
return ReferenceEquals(thisTarget, otherTarget) || thisTarget.Equals(otherTarget);
44-
}
45-
46-
public override bool Equals(object? obj) => obj is WeakCalDateTimeKey other && Equals(other);
47-
48-
public override int GetHashCode() => _hashCode;
23+
public ZonedDateTime Value { get; } = value;
4924
}
5025

5126
/// <summary>
52-
/// The cache for storing resolved <see cref="ZonedDateTime"/> objects for <see cref="CalDateTime"/> objects.
27+
/// The cache for storing resolved <see cref="ZonedDateTime"/> objects for <see cref="CalDateTime"/> objects
5328
/// This is a thread-safe dictionary that uses weak references to avoid memory leaks.
29+
/// Dead <see cref="CalDateTime"/> keys are automatically removed from the cache.
30+
/// Key and Value must be reference types, so we use <see cref="ZonedDateTimeBox"/> to box the value.
31+
/// <para/>
32+
/// If a <see cref="CalDateTime"/> is instantiated from a lenient timezone conversion, this leniently
33+
/// determined <see cref="CalDateTime"/> must not be converted again.
34+
/// Lenient conversions resolve non-existing or ambiguous times caused by DST transitions.
35+
/// <para/>
36+
/// Therefore, when converting from one timezone to another, the conversion is done with the
37+
/// <see cref="ZonedDateTime"/> object created at the time of (first) conversion,
38+
/// which is stored in the cache.
5439
/// </summary>
55-
private static readonly ConcurrentDictionary<WeakCalDateTimeKey, ZonedDateTime> _cache = new();
40+
private static readonly ConditionalWeakTable<CalDateTime, ZonedDateTimeBox> _cache = new();
5641

5742
/// <summary>
5843
/// Stores a <see cref="ZonedDateTime"/> in the cache for the given <see cref="CalDateTime"/>.
5944
/// </summary>
6045
private static void AddToCache(CalDateTime calDateTime, ZonedDateTime zonedDateTime)
61-
{
62-
var key = new WeakCalDateTimeKey(calDateTime);
63-
_cache[key] = zonedDateTime;
64-
}
46+
=> _cache.Add(calDateTime, new ZonedDateTimeBox(zonedDateTime));
6547

6648
/// <summary>
6749
/// Resolves a <see cref="CalDateTime"/> to a <see cref="ZonedDateTime"/>,
6850
/// using the cache if possible.
6951
/// </summary>
7052
public static ZonedDateTime? ResolveZonedDateTime(CalDateTime calDateTime)
7153
{
72-
var key = new WeakCalDateTimeKey(calDateTime);
73-
7454
// Try to get from cache
75-
if (_cache.TryGetValue(key, out var zoned))
76-
return zoned;
55+
if (_cache.TryGetValue(calDateTime, out var zoned))
56+
{
57+
return zoned.Value;
58+
}
7759

7860
// Compute and cache
7961
ZonedDateTime? result = null;
@@ -88,23 +70,11 @@ private static void AddToCache(CalDateTime calDateTime, ZonedDateTime zonedDateT
8870
}
8971

9072
if (result.HasValue)
91-
_cache[key] = result.Value;
73+
AddToCache(calDateTime, result.Value);
9274

9375
return result;
9476
}
9577

96-
/// <summary>
97-
/// Removes dead entries from the cache.
98-
/// </summary>
99-
public static void CleanupCache()
100-
{
101-
foreach (var key in _cache.Keys)
102-
{
103-
if (!key.TryGetTarget(out _))
104-
_cache.TryRemove(key, out _);
105-
}
106-
}
107-
10878
/// <summary>
10979
/// Converts the date/time to UTC (Coordinated Universal Time)
11080
/// If <see cref="CalDateTime.IsFloating"/>==<see langword="true"/>

0 commit comments

Comments
 (0)