Skip to content

Commit

Permalink
Simplify mapping between Windows and TZDB time zone IDs
Browse files Browse the repository at this point in the history
Fixes nodatime#274.

I'd be open to the idea of adding convenience methods to TzdbDateTimeZoneSource as well, that just pass through to WindowsZones, if that were considered useful.
  • Loading branch information
jskeet committed Apr 19, 2019
1 parent 94f8515 commit 7c6634c
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 16 deletions.
74 changes: 62 additions & 12 deletions src/NodaTime.Test/TimeZones/Cldr/WindowsZonesTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,38 @@
using NodaTime.TimeZones.Cldr;
using NodaTime.TimeZones.IO;
using NUnit.Framework;
using System;
using System.Collections.Generic;
using System.IO;

namespace NodaTime.Test.TimeZones.Cldr
{
public class WindowsZonesTest
{
private static readonly MapZone MapZone1 = new MapZone("windowsId1", "territory1", new[] { "id1.1", "id1.2", "id1.3" });
private static readonly MapZone MapZone2 = new MapZone("windowsId2", MapZone.PrimaryTerritory, new[] { "primaryId2" });
private static readonly MapZone MapZone2a = new MapZone("windowsId2", MapZone.PrimaryTerritory, new[] { "primaryId2" });
private static readonly MapZone MapZone2b = new MapZone("windowsId2", "territory2", new[] { "primaryId2", "id2.1", "id2.2" });
private static readonly MapZone MapZone3 = new MapZone("windowsId3", MapZone.PrimaryTerritory, new[] { "primaryId3" });

private static readonly WindowsZones SampleZones = new WindowsZones("version", "tzdbVersion", "windowsVersion", new[] { MapZone1, MapZone2a, MapZone2b, MapZone3 });

[Test]
public void Properties()
{
var zones = new WindowsZones("version", "tzdbVersion", "windowsVersion", new[] { MapZone1, MapZone2, MapZone3 });
Assert.AreEqual("version", zones.Version);
Assert.AreEqual("tzdbVersion", zones.TzdbVersion);
Assert.AreEqual("windowsVersion", zones.WindowsVersion);
Assert.AreEqual("primaryId2", zones.PrimaryMapping["windowsId2"]);
Assert.AreEqual("primaryId3", zones.PrimaryMapping["windowsId3"]);
Assert.AreEqual(new[] { MapZone1, MapZone2, MapZone3 }, zones.MapZones);
Assert.AreEqual("version", SampleZones.Version);
Assert.AreEqual("tzdbVersion", SampleZones.TzdbVersion);
Assert.AreEqual("windowsVersion", SampleZones.WindowsVersion);
Assert.AreEqual("primaryId2", SampleZones.PrimaryMapping["windowsId2"]);
Assert.AreEqual("primaryId3", SampleZones.PrimaryMapping["windowsId3"]);
Assert.AreEqual(new[] { MapZone1, MapZone2a, MapZone2b, MapZone3 }, SampleZones.MapZones);
}

[Test]
public void ReadWrite()
{
var zones = new WindowsZones("version", "tzdbVersion", "windowsVersion", new[] { MapZone1, MapZone2, MapZone3 });

var stream = new MemoryStream();
var writer = new DateTimeZoneWriter(stream, null);
zones.Write(writer);
SampleZones.Write(writer);
stream.Position = 0;

var reader = new DateTimeZoneReader(stream, null);
Expand All @@ -45,7 +47,55 @@ public void ReadWrite()
Assert.AreEqual("windowsVersion", zones2.WindowsVersion);
Assert.AreEqual("primaryId2", zones2.PrimaryMapping["windowsId2"]);
Assert.AreEqual("primaryId3", zones2.PrimaryMapping["windowsId3"]);
Assert.AreEqual(new[] { MapZone1, MapZone2, MapZone3 }, zones2.MapZones);
Assert.AreEqual(new[] { MapZone1, MapZone2a, MapZone2b, MapZone3 }, zones2.MapZones);
}

[Test]
[TestCase("windowsId2", "primaryId2")]
[TestCase("windowsId3", "primaryId3")]
public void MapWindowsToTzdb_Valid(string windowsId, string expectedTzdbId)
{
Assert.AreEqual(expectedTzdbId, SampleZones.MapWindowsToTzdb(windowsId));
Assert.AreEqual(expectedTzdbId, SampleZones.MapWindowsToTzdbOrNull(windowsId));
}

[Test]
[TestCase("windowsId1")]
[TestCase("windowsIdUnknown")]
public void MapWindowsToTzdb_Invalid(string windowsId)
{
Assert.Throws<KeyNotFoundException>(() => SampleZones.MapWindowsToTzdb(windowsId));
Assert.Null(SampleZones.MapWindowsToTzdbOrNull(windowsId)); ;
}

[Test]
[TestCase("id1.1", "windowsId1")]
[TestCase("id1.2", "windowsId1")]
[TestCase("id1.3", "windowsId1")]
[TestCase("primaryId2", "windowsId2")]
[TestCase("id2.1", "windowsId2")]
[TestCase("id2.2", "windowsId2")]
[TestCase("primaryId3", "windowsId3")]
public void MapTzdbToWindows_Valid(string tzdbId, string expectedWindowsId)
{
Assert.AreEqual(expectedWindowsId, SampleZones.MapTzdbToWindows(tzdbId));
Assert.AreEqual(expectedWindowsId, SampleZones.MapTzdbToWindowsOrNull(tzdbId));
}

[Test]
public void MapTzdbToWindows_Invalid()
{
Assert.Throws<KeyNotFoundException>(() => SampleZones.MapTzdbToWindows("unknown"));
Assert.Null(SampleZones.MapTzdbToWindowsOrNull("unknown"));
}

[Test]
public void MapMethods_NullInput()
{
Assert.Throws<ArgumentNullException>(() => SampleZones.MapWindowsToTzdb(null!));
Assert.Throws<ArgumentNullException>(() => SampleZones.MapWindowsToTzdbOrNull(null!));
Assert.Throws<ArgumentNullException>(() => SampleZones.MapTzdbToWindows(null!));
Assert.Throws<ArgumentNullException>(() => SampleZones.MapTzdbToWindowsOrNull(null!));
}
}
}
83 changes: 79 additions & 4 deletions src/NodaTime/TimeZones/Cldr/WindowsZones.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
// Use of this source code is governed by the Apache License 2.0,
// as found in the LICENSE.txt file.

using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using JetBrains.Annotations;
using NodaTime.Annotations;
using NodaTime.TimeZones.IO;
using NodaTime.Utility;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading;

namespace NodaTime.TimeZones.Cldr
{
Expand Down Expand Up @@ -103,6 +104,8 @@ public sealed class WindowsZones
/// to TZDB zone ID.</value>
public IDictionary<string, string> PrimaryMapping { get; }

private readonly Lazy<IReadOnlyDictionary<string, string>> tzdbToWindowsMapping;

internal WindowsZones(string version, string tzdbVersion,
string windowsVersion, IList<MapZone> mapZones)
: this(Preconditions.CheckNotNull(version, nameof(version)),
Expand All @@ -121,6 +124,78 @@ private WindowsZones(string version, string tzdbVersion, string windowsVersion,
this.PrimaryMapping = new NodaReadOnlyDictionary<string, string>(
mapZones.Where(z => z.Territory == MapZone.PrimaryTerritory)
.ToDictionary(z => z.WindowsId, z => z.TzdbIds.Single()));
tzdbToWindowsMapping = new Lazy<IReadOnlyDictionary<string, string>>(BuildTzdbToWindowsMapping, LazyThreadSafetyMode.PublicationOnly);
}

private IReadOnlyDictionary<string, string> BuildTzdbToWindowsMapping()
{

var ret = new Dictionary<string, string>();
foreach (var mapZone in MapZones)
{
foreach (var tzdbId in mapZone.TzdbIds)
{
// This is forgiving of duplicate TZDB IDs. That's likely to be more useful than throwing an exception.
ret[tzdbId] = mapZone.WindowsId;
}
}
return ret;
}

/// <summary>
/// Maps the given Windows time zone ID to the corresponding CLDR-canonical TZDB ID for the primary territory,
/// throwing <see cref="KeyNotFoundException"/> on failure.
/// </summary>
/// <remarks>
/// This method is equivalent to looking up the ID in <see cref="PrimaryMapping"/>, but provides symmetry
/// with <see cref="MapTzdbToWindows(string)"/>.
/// </remarks>
/// <param name="windowsZoneId">The Windows time zone ID to map.</param>
/// <exception cref="KeyNotFoundException">Either <paramref name="windowsZoneId"/> was not known to this mapping object,
/// or it had no primary territory.</exception>
/// <returns>The corresponding CLDR-canonical time zone ID.</returns>
public string MapWindowsToTzdb(string windowsZoneId) =>
MapWindowsToTzdbOrNull(windowsZoneId) ?? throw new KeyNotFoundException("Unknown zone ID, or no primary mapping");

/// <summary>
/// Maps the given Windows time zone ID to the corresponding CLDR-canonical TZDB ID for the primary territory,
/// returning a null reference if no mapping is available.
/// </summary>
/// <remarks>
/// This method is equivalent to looking up the ID in <see cref="PrimaryMapping"/>, but provides symmetry
/// with <see cref="MapTzdbToWindowsOrNull(string)"/>.
/// </remarks>
/// <param name="windowsZoneId">The Windows time zone ID to map.</param>
/// <returns>The corresponding CLDR-canonical time zone ID, or <c>null</c> if there is no mapping available.</returns>
public string? MapWindowsToTzdbOrNull(string windowsZoneId)
{
Preconditions.CheckNotNull(windowsZoneId, nameof(windowsZoneId));
PrimaryMapping.TryGetValue(windowsZoneId, out var result);
return result;
}

/// <summary>
/// Maps the given TZDB time zone ID to the corresponding Windows zone ID,
/// throwing <see cref="KeyNotFoundException"/> on failure.
/// </summary>
/// <param name="tzdbId">The Windows time zone ID to map.</param>
/// <exception cref="KeyNotFoundException"><paramref name="tzdbId"/> is not known to this mapping object.</exception>
/// <returns>The corresponding CLDR-canonical time zone ID.</returns>
public string MapTzdbToWindows(string tzdbId) =>
MapTzdbToWindowsOrNull(tzdbId) ?? throw new KeyNotFoundException("Unknown zone ID, or no Windows mapping");

/// <summary>
/// Maps the given TZDB time zone ID to the corresponding Windows zone ID,
/// returning a null reference if no mapping is available.
/// </summary>
/// <param name="tzdbId">The Windows time zone ID to map.</param>
/// <exception cref="KeyNotFoundException"><paramref name="tzdbId"/> is not known to this mapping object.</exception>
/// <returns>The corresponding CLDR-canonical time zone ID.</returns>
public string? MapTzdbToWindowsOrNull(string tzdbId)
{
Preconditions.CheckNotNull(tzdbId, nameof(tzdbId));
tzdbToWindowsMapping.Value.TryGetValue(tzdbId, out var result);
return result;
}

internal static WindowsZones Read(IDateTimeZoneReader reader)
Expand Down

0 comments on commit 7c6634c

Please sign in to comment.