Skip to content

Commit bd0b2b0

Browse files
Adding Windows Display Names (#50)
1 parent 22b2a9b commit bd0b2b0

File tree

6 files changed

+223
-18
lines changed

6 files changed

+223
-18
lines changed

src/TimeZoneNames.DataBuilder/DataExtractor.cs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Xml.Linq;
88
using System.Xml.XPath;
99
using Newtonsoft.Json;
10+
using Newtonsoft.Json.Linq;
1011
using NodaTime;
1112
using NodaTime.TimeZones;
1213

@@ -16,13 +17,15 @@ public class DataExtractor
1617
{
1718
private readonly string _cldrPath;
1819
private readonly string _nzdPath;
20+
private readonly string _tzresPath;
1921

2022
private readonly TimeZoneData _data = new TimeZoneData();
2123

2224
private DataExtractor(string dataPath)
2325
{
2426
_cldrPath = Path.Combine(dataPath, "cldr") + "\\";
2527
_nzdPath = Path.Combine(dataPath, "nzd") + "\\";
28+
_tzresPath = Path.Combine(dataPath, "tzres") + "\\";
2629
}
2730

2831
public static DataExtractor Load(string dataPath, bool overwrite)
@@ -65,7 +68,8 @@ private void LoadData()
6568
{
6669
LoadZoneCountries,
6770
LoadZoneAliases,
68-
LoadLanguages
71+
LoadLanguages,
72+
LoadDisplayNames
6973
};
7074
Task.WhenAll(actions.Select(Task.Run)).Wait();
7175

@@ -78,7 +82,10 @@ private void LoadData()
7882

7983
private void DownloadData(bool overwrite)
8084
{
81-
Task.WaitAll(DownloadCldrAsync(overwrite), DownloadNzdAsync(overwrite));
85+
Task.WaitAll(
86+
DownloadCldrAsync(overwrite),
87+
DownloadNzdAsync(overwrite),
88+
DownloadTZResAsync(overwrite));
8289
}
8390

8491
private async Task DownloadCldrAsync(bool overwrite)
@@ -101,6 +108,16 @@ private async Task DownloadNzdAsync(bool overwrite)
101108
}
102109
}
103110

111+
private async Task DownloadTZResAsync(bool overwrite)
112+
{
113+
var exists = Directory.Exists(_tzresPath);
114+
if (overwrite || !exists)
115+
{
116+
if (exists) Directory.Delete(_tzresPath, true);
117+
await Downloader.DownloadTZResAsync(_tzresPath);
118+
}
119+
}
120+
104121
private void LoadZoneCountries()
105122
{
106123
foreach (var location in _tzdbSource.ZoneLocations.OrderBy(x => GetStandardOffset(x.ZoneId)).ThenBy(x => GetDaylightOffset(x.ZoneId)))
@@ -471,6 +488,21 @@ private static TimeZoneValues GetTimeZoneValues(XContainer element)
471488
return values;
472489
}
473490

491+
private void LoadDisplayNames()
492+
{
493+
using (var textReader = File.OpenText(_tzresPath + "tzinfo.json"))
494+
using (var jsonReader = new JsonTextReader(textReader))
495+
{
496+
var data = JObject.Load(jsonReader);
497+
foreach (var item in data["Languages"])
498+
{
499+
var locale = item.Value<string>("Locale").Replace("-", "_");
500+
var timeZones = item["TimeZones"].ToObject<Dictionary<string, string>>();
501+
_data.DisplayNames.Add(locale, timeZones);
502+
}
503+
}
504+
}
505+
474506
private void PatchData()
475507
{
476508
//// Fixup Abbreviations

src/TimeZoneNames.DataBuilder/Downloader.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ public static async Task DownloadNzdAsync(string dir)
3030
}
3131
}
3232

33+
public static async Task DownloadTZResAsync(string dir)
34+
{
35+
const string url = "https://raw.githubusercontent.com/tomkludy/TimeZoneWindowsResourceExtractor/master/TZResScraper/tzinfo.json";
36+
await DownloadAsync(url, dir);
37+
}
38+
3339
private static async Task DownloadAsync(string url, string dir)
3440
{
3541
if (!Directory.Exists(dir))

src/TimeZoneNames/TZNames.cs

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ public static IDictionary<string, string> GetTimeZonesForCountry(string countryC
9191
Id = x,
9292
Name = GetNames(x, langKey, false).Generic
9393
})
94-
.ToDictionary(x => x.Id, x => x.Name);
94+
.ToDictionary(x => x.Id, x => x.Name, StringComparer.OrdinalIgnoreCase);
9595

9696
// Append city names only when needed to differentiate zones with the same name
9797
foreach (var group in results.GroupBy(x => x.Value).Where(x => x.Count() > 1).ToArray())
@@ -161,7 +161,7 @@ private static IDictionary<string, string> GetFixedTimeZoneNames(string language
161161
Id = x,
162162
Name = GetNames(x, langKey, abbreviations).Generic
163163
})
164-
.ToDictionary(x => x.Id, x => x.Name);
164+
.ToDictionary(x => x.Id, x => x.Name, StringComparer.OrdinalIgnoreCase);
165165

166166
return results;
167167
}
@@ -229,9 +229,61 @@ public static IDictionary<string, string> GetCountryNames(string languageCode)
229229
}
230230

231231
return results.OrderBy(x => x.Value, comparer)
232-
.ToDictionary(x => x.Key, x => x.Value);
232+
.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase);
233233
}
234234

235+
/// <summary>
236+
/// Gets the localized names for a given IANA or Windows time zone identifier.
237+
/// </summary>
238+
/// <param name="timeZoneId">An IANA or Windows time zone identifier.</param>
239+
/// <param name="languageCode">The IETF language tag (culture code) to use when localizing the display names.</param>
240+
/// <returns>A display name associated with this time zone.</returns>
241+
public static string GetDisplayNameForTimeZone(string timeZoneId, string languageCode)
242+
{
243+
var langKey = GetLanguageKey(languageCode, true);
244+
if (langKey == null)
245+
throw new ArgumentException("Invalid Language Code", nameof(languageCode));
246+
247+
var displayNames = Data.DisplayNames[langKey];
248+
249+
if (displayNames.TryGetValue(timeZoneId, out var displayName))
250+
return displayName;
251+
252+
timeZoneId = TZConvert.IanaToWindows(timeZoneId);
253+
if (displayNames.TryGetValue(timeZoneId, out displayName))
254+
return displayName;
255+
256+
return null;
257+
}
258+
259+
/// <summary>
260+
/// Get display names suitable for use in a single drop-down list to select a time zone.
261+
/// </summary>
262+
/// <param name="languageCode">The IETF language tag (culture code) to use when localizing the display names.</param>
263+
/// <param name="useIanaZoneIds"><c>true</c> to use IANA time zone keys, otherwise uses Windows time zone keys.</param>
264+
/// <returns>A dictionary where the key is the time zone id, and the name is the localized display name.</returns>
265+
public static IDictionary<string, string> GetDisplayNames(string languageCode, bool useIanaZoneIds = false)
266+
{
267+
var langKey = GetLanguageKey(languageCode, true);
268+
if (langKey == null)
269+
throw new ArgumentException("Invalid Language Code", nameof(languageCode));
270+
271+
var displayNames = Data.DisplayNames[langKey];
272+
273+
if (!useIanaZoneIds)
274+
return displayNames;
275+
276+
// Remove obsolete zones before mapping
277+
displayNames.Remove("Mid-Atlantic Standard Time");
278+
displayNames.Remove("Kamchatka Standard Time");
279+
280+
var languageCodeParts = languageCode.Split('_', '-');
281+
var territoryCode = languageCodeParts.Length < 2 ? "001" : languageCodeParts[1];
282+
return displayNames.ToDictionary(x => TZConvert.WindowsToIana(x.Key, territoryCode), x => x.Value, StringComparer.OrdinalIgnoreCase);
283+
}
284+
285+
286+
235287
/// <summary>
236288
/// Gets a list of all language codes supported by this library.
237289
/// </summary>
@@ -277,15 +329,32 @@ private static IComparer<string> GetComparer(string langKey)
277329
#endif
278330
}
279331

280-
private static string GetLanguageKey(string languageCode)
332+
private static string GetLanguageKey(string languageCode, bool forDisplayNames = false)
281333
{
282334
var key = languageCode.ToLowerInvariant().Replace('-', '_');
283335
while (true)
284336
{
285-
if (Data.CldrLanguageData.ContainsKey(key))
286-
return key;
337+
if (forDisplayNames)
338+
{
339+
if (Data.DisplayNames.ContainsKey(key))
340+
return key;
341+
}
342+
else
343+
{
344+
if (Data.CldrLanguageData.ContainsKey(key))
345+
return key;
346+
}
287347

288348
key = GetLanguageSubkey(key);
349+
350+
if (key == null)
351+
{
352+
var keys = forDisplayNames ? (IEnumerable<string>)Data.DisplayNames.Keys : Data.CldrLanguageData.Keys;
353+
key = keys.FirstOrDefault(x => x.Split('_')[0].Equals(languageCode, StringComparison.OrdinalIgnoreCase));
354+
355+
if (key == null)
356+
throw new Exception("Could not find a language with code " + languageCode);
357+
}
289358
}
290359
}
291360

src/TimeZoneNames/TimeZoneData.cs

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,22 @@ namespace TimeZoneNames
1010
{
1111
internal class TimeZoneData
1212
{
13-
public Dictionary<string, string[]> TzdbZoneCountries { get; } = new Dictionary<string, string[]>();
13+
public Dictionary<string, string[]> TzdbZoneCountries { get; } = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase);
1414

15-
public Dictionary<string, string[]> CldrZoneCountries { get; } = new Dictionary<string, string[]>();
15+
public Dictionary<string, string[]> CldrZoneCountries { get; } = new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase);
1616

17-
public Dictionary<string, string> CldrMetazones { get; } = new Dictionary<string, string>();
17+
public Dictionary<string, string> CldrMetazones { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
1818

19-
public Dictionary<string, string> CldrPrimaryZones { get; } = new Dictionary<string, string>();
19+
public Dictionary<string, string> CldrPrimaryZones { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
2020

21-
public Dictionary<string, string> CldrAliases { get; } = new Dictionary<string, string>();
21+
public Dictionary<string, string> CldrAliases { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
2222

23-
public Dictionary<string, CldrLanguageData> CldrLanguageData { get; } = new Dictionary<string, CldrLanguageData>();
23+
public Dictionary<string, CldrLanguageData> CldrLanguageData { get; } = new Dictionary<string, CldrLanguageData>(StringComparer.OrdinalIgnoreCase);
2424

2525
public List<TimeZoneSelectionData> SelectionZones { get; } = new List<TimeZoneSelectionData>();
2626

27+
public Dictionary<string, Dictionary<string, string>> DisplayNames = new Dictionary<string, Dictionary<string, string>>(StringComparer.OrdinalIgnoreCase);
28+
2729
[SecuritySafeCritical]
2830
public static TimeZoneData Load()
2931
{
@@ -49,13 +51,13 @@ internal class CldrLanguageData
4951

5052
public string FallbackFormat { get; set; }
5153

52-
public Dictionary<string, TimeZoneValues> ShortNames { get; } = new Dictionary<string, TimeZoneValues>();
54+
public Dictionary<string, TimeZoneValues> ShortNames { get; } = new Dictionary<string, TimeZoneValues>(StringComparer.OrdinalIgnoreCase);
5355

54-
public Dictionary<string, TimeZoneValues> LongNames { get; } = new Dictionary<string, TimeZoneValues>();
56+
public Dictionary<string, TimeZoneValues> LongNames { get; } = new Dictionary<string, TimeZoneValues>(StringComparer.OrdinalIgnoreCase);
5557

56-
public Dictionary<string, string> CountryNames { get; } = new Dictionary<string, string>();
58+
public Dictionary<string, string> CountryNames { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
5759

58-
public Dictionary<string, string> CityNames { get; } = new Dictionary<string, string>();
60+
public Dictionary<string, string> CityNames { get; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
5961
}
6062

6163
internal class TimeZoneSelectionData

src/TimeZoneNames/data.json.gz

47.4 KB
Binary file not shown.
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System;
2+
using System.Globalization;
3+
using System.Linq;
4+
using TimeZoneConverter;
5+
using Xunit;
6+
using Xunit.Abstractions;
7+
8+
namespace TimeZoneNames.Tests
9+
{
10+
public class DisplayNamesTests
11+
{
12+
private readonly ITestOutputHelper _output;
13+
14+
public DisplayNamesTests(ITestOutputHelper output)
15+
{
16+
_output = output;
17+
}
18+
19+
[Fact]
20+
public void Can_Get_DisplayNames_For_English()
21+
{
22+
var displayNames = TZNames.GetDisplayNames("en");
23+
24+
foreach (var item in displayNames)
25+
{
26+
_output.WriteLine($"{item.Key} = {item.Value}");
27+
}
28+
29+
Assert.NotEmpty(displayNames);
30+
}
31+
32+
[Fact]
33+
public void Can_Get_DisplayNames_For_OS_Culture()
34+
{
35+
var languageCode = CultureInfo.InstalledUICulture.IetfLanguageTag;
36+
37+
var displayNames = TZNames.GetDisplayNames(languageCode);
38+
39+
var expected = TimeZoneInfo.GetSystemTimeZones().ToDictionary(x => x.Id, x => x.DisplayName);
40+
41+
Assert.Equal(expected, displayNames);
42+
}
43+
44+
[Fact]
45+
public void Can_Get_DisplayNames_For_French_Canada_With_Windows_Ids()
46+
{
47+
var displayNames = TZNames.GetDisplayNames("fr-CA");
48+
49+
var pacific = displayNames["Pacific Standard Time"];
50+
var mountain = displayNames["Mountain Standard Time"];
51+
var central = displayNames["Central Standard Time"];
52+
var eastern = displayNames["Eastern Standard Time"];
53+
54+
Assert.Equal("(UTC-08:00) Pacifique (É.-U. et Canada)", pacific);
55+
Assert.Equal("(UTC-07:00) Montagnes Rocheuses (É.-U. et Canada)", mountain);
56+
Assert.Equal("(UTC-06:00) Centre (É.-U. et Canada)", central);
57+
Assert.Equal("(UTC-05:00) Est (É.-U. et Canada)", eastern);
58+
}
59+
60+
[Fact]
61+
public void Can_Get_DisplayNames_For_French_Canada_With_IANA_Ids()
62+
{
63+
var displayNames = TZNames.GetDisplayNames("fr-CA", true);
64+
65+
var pacific = displayNames["America/Vancouver"];
66+
var mountain = displayNames["America/Edmonton"];
67+
var central = displayNames["America/Winnipeg"];
68+
var eastern = displayNames["America/Toronto"];
69+
70+
Assert.Equal("(UTC-08:00) Pacifique (É.-U. et Canada)", pacific);
71+
Assert.Equal("(UTC-07:00) Montagnes Rocheuses (É.-U. et Canada)", mountain);
72+
Assert.Equal("(UTC-06:00) Centre (É.-U. et Canada)", central);
73+
Assert.Equal("(UTC-05:00) Est (É.-U. et Canada)", eastern);
74+
}
75+
76+
[Fact]
77+
public void Can_Get_DisplayName_For_Every_Windows_Zone()
78+
{
79+
foreach (var id in TZConvert.KnownWindowsTimeZoneIds)
80+
{
81+
var displayName = TZNames.GetDisplayNameForTimeZone(id, "en");
82+
Assert.NotNull(displayName);
83+
}
84+
}
85+
86+
[Fact]
87+
public void Can_Get_DisplayName_For_Every_IANA_Zone()
88+
{
89+
foreach (var id in TZConvert.KnownIanaTimeZoneNames)
90+
{
91+
var displayName = TZNames.GetDisplayNameForTimeZone(id, "en");
92+
Assert.NotNull(displayName);
93+
}
94+
}
95+
}
96+
}

0 commit comments

Comments
 (0)