mattjohnsonpint / TimeZoneConverter

Lightweight libraries to convert between IANA, Windows, Rails, and POSIX time zones.
Other
844 stars 81 forks source link

Supposedly identical K8S pods are returning different values #70

Closed jslaybaugh closed 4 years ago

jslaybaugh commented 4 years ago

Using:

Problem

Our application is running on linux containers in an Azure Kubernetes cluster. I have 5 pods in a single deployment in the kubernetes cluster and different pods are returning different values for TimeZoneInfo.StandardName and TimeZoneInfo.DaylightName on some pods from other pods in spite of the pods being identical images running identical code on identical configs. When I bash into the pods and run apt list they both show to have tzdata installed and show the same information for each:

$ apt list
...
tzdata/oldstable,oldstable-updates,now 2019c-0+deb9u1 all [installed,automatic]
...

I created a page to reproduce this that you can hit at https://brushfire.com/test/timezones. The first several lines are the machine name and the OS version and culture. Then it lists all the timezones in the following format:

{DisplayName}, {StandardName}, {DaylightName}, {Abbreviation}

As you can see from the following screenshots, on one pod I'm getting different values from another pod. All my code for returning this list and for my creation of the abbreviation is included below, but fundamentally the issue stems from TimeZoneInfo.StandardName and TimeZoneInfo.DaylightName returning different values on different pods. Any idea why this may be happening?

Screen Shot 2020-05-27 at 11 33 20 AM Screen Shot 2020-05-27 at 11 33 39 AM

TestController.cs

public ActionResult TimeZones()
{
    var values = LocalizationUtility.GetWindowsTimeZones().Select(x=> $"{x.DisplayName}, {TZConvert.GetTimeZoneInfo(x.Id).StandardName}, {TZConvert.GetTimeZoneInfo(x.Id).DaylightName}, {TZConvert.GetTimeZoneInfo(x.Id).Abbreviation(new DateTime(2020,12,1,13,15,16))}").ToList();
    values.Insert(0, $"Current Culture: {System.Threading.Thread.CurrentThread.CurrentCulture}");
    values.Insert(0, $"Current UI Culture: {System.Threading.Thread.CurrentThread.CurrentUICulture}");
    values.Insert(0, $"Version: {Environment.Version}");
    values.Insert(0, $"OSVersion: {Environment.OSVersion}");
    values.Insert(0, $"Machine: {Environment.MachineName}");
    return Content(string.Join(Environment.NewLine, values),"text/plain", Encoding.UTF8);
}

LocalizationUtility.cs

public static List<FormattedTimeZone> GetWindowsTimeZones()
{
    var ids = TZConvert.KnownWindowsTimeZoneIds
        .ToList();

    var list = new List<FormattedTimeZone>();

    ids.ForEach(x =>
    {
        TimeZoneInfo tz;
        var res = TZConvert.TryGetTimeZoneInfo(x, out tz);
        if (res)
        {
            // get the more helpful name from our custom lookup
            if (TIMEZONE_WINDOWS_LOOKUP.ContainsKey(x))
            {
                list.Add(new FormattedTimeZone { Id = x, Name = TIMEZONE_WINDOWS_LOOKUP[x], Offset = tz.BaseUtcOffset });
            }
        }
    });

    return list
        .OrderBy(x=>x.Offset)
        .ThenBy(x=>x.Name)
        .ToList();
}

private static readonly Dictionary<string, string> TIMEZONE_WINDOWS_LOOKUP = new Dictionary<string, string>
{
    {"Dateline Standard Time", "International Date Line West"},
    {"UTC-11", "Coordinated Universal Time-11"},
    {"Aleutian Standard Time", "Aleutian Islands"},
    {"Hawaiian Standard Time", "Hawaii"},
    {"Marquesas Standard Time", "Marquesas Islands"},
    {"Alaskan Standard Time", "Alaska"},
    {"UTC-09", "Coordinated Universal Time-09"},
    {"Pacific Standard Time (Mexico)", "Baja California"},
    {"UTC-08", "Coordinated Universal Time-08"},
    {"Pacific Standard Time", "Pacific Time (US & Canada)"},
    {"US Mountain Standard Time", "Arizona"},
    {"Mountain Standard Time (Mexico)", "Chihuahua, La Paz, Mazatlan"},
    {"Mountain Standard Time", "Mountain Time (US & Canada)"},
    {"Central America Standard Time", "Central America"},
    {"Central Standard Time", "Central Time (US & Canada)"},
    {"Easter Island Standard Time", "Easter Island"},
    {"Central Standard Time (Mexico)", "Guadalajara, Mexico City, Monterrey"},
    {"Canada Central Standard Time", "Saskatchewan"},
    {"SA Pacific Standard Time", "Bogota, Lima, Quito, Rio Branco"},
    {"Eastern Standard Time (Mexico)", "Chetumal"},
    {"Eastern Standard Time", "Eastern Time (US & Canada)"},
    {"Haiti Standard Time", "Haiti"},
    {"Cuba Standard Time", "Havana"},
    {"US Eastern Standard Time", "Indiana (East)"},
    {"Turks And Caicos Standard Time", "Turks and Caicos"},
    {"Paraguay Standard Time", "Asuncion"},
    {"Atlantic Standard Time", "Atlantic Time (Canada)"},
    {"Venezuela Standard Time", "Caracas"},
    {"Central Brazilian Standard Time", "Cuiaba"},
    {"SA Western Standard Time", "Georgetown, La Paz, Manaus, San Juan"},
    {"Pacific SA Standard Time", "Santiago"},
    {"Newfoundland Standard Time", "Newfoundland"},
    {"Tocantins Standard Time", "Araguaina"},
    {"E. South America Standard Time", "Brasilia"},
    {"SA Eastern Standard Time", "Cayenne, Fortaleza"},
    {"Argentina Standard Time", "City of Buenos Aires"},
    {"Greenland Standard Time", "Greenland"},
    {"Montevideo Standard Time", "Montevideo"},
    {"Magallanes Standard Time", "Punta Arenas"},
    {"Saint Pierre Standard Time", "Saint Pierre and Miquelon"},
    {"Bahia Standard Time", "Salvador"},
    {"UTC-02", "Coordinated Universal Time-02"},
    {"Mid-Atlantic Standard Time", "Mid-Atlantic - Old"},
    {"Azores Standard Time", "Azores"},
    {"Cape Verde Standard Time", "Cabo Verde Is."},
    {"UTC", "(UTC) Coordinated Universal Time"},
    {"GMT Standard Time", "Dublin, Edinburgh, Lisbon, London"},
    {"Greenwich Standard Time", "Monrovia, Reykjavik"},
    {"Sao Tome Standard Time", "Sao Tome"},
    {"Morocco Standard Time", "Casablanca"},
    {"W. Europe Standard Time", "Amsterdam, Berlin, Bern, Rome, Stockholm, Vienna"},
    {"Central Europe Standard Time", "Belgrade, Bratislava, Budapest, Ljubljana, Prague"},
    {"Romance Standard Time", "Brussels, Copenhagen, Madrid, Paris"},
    {"Central European Standard Time", "Sarajevo, Skopje, Warsaw, Zagreb"},
    {"W. Central Africa Standard Time", "West Central Africa"},
    {"Jordan Standard Time", "Amman"},
    {"GTB Standard Time", "Athens, Bucharest"},
    {"Middle East Standard Time", "Beirut"},
    {"Egypt Standard Time", "Cairo"},
    {"E. Europe Standard Time", "Chisinau"},
    {"Syria Standard Time", "Damascus"},
    {"West Bank Standard Time", "Gaza, Hebron"},
    {"South Africa Standard Time", "Harare, Pretoria"},
    {"FLE Standard Time", "Helsinki, Kyiv, Riga, Sofia, Tallinn, Vilnius"},
    {"Israel Standard Time", "Jerusalem"},
    {"Kaliningrad Standard Time", "Kaliningrad"},
    {"Sudan Standard Time", "Khartoum"},
    {"Libya Standard Time", "Tripoli"},
    {"Namibia Standard Time", "Windhoek"},
    {"Arabic Standard Time", "Baghdad"},
    {"Turkey Standard Time", "Istanbul"},
    {"Arab Standard Time", "Kuwait, Riyadh"},
    {"Belarus Standard Time", "Minsk"},
    {"Russian Standard Time", "Moscow, St. Petersburg"},
    {"E. Africa Standard Time", "Nairobi"},
    {"Iran Standard Time", "Tehran"},
    {"Arabian Standard Time", "Abu Dhabi, Muscat"},
    {"Astrakhan Standard Time", "Astrakhan, Ulyanovsk"},
    {"Azerbaijan Standard Time", "Baku"},
    {"Russia Time Zone 3", "Izhevsk, Samara"},
    {"Mauritius Standard Time", "Port Louis"},
    {"Saratov Standard Time", "Saratov"},
    {"Georgian Standard Time", "Tbilisi"},
    {"Volgograd Standard Time", "Volgograd"},
    {"Caucasus Standard Time", "Yerevan"},
    {"Afghanistan Standard Time", "Kabul"},
    {"West Asia Standard Time", "Ashgabat, Tashkent"},
    {"Ekaterinburg Standard Time", "Ekaterinburg"},
    {"Pakistan Standard Time", "Islamabad, Karachi"},
    {"Qyzylorda Standard Time", "Qyzylorda"},
    {"India Standard Time", "Chennai, Kolkata, Mumbai, New Delhi"},
    {"Sri Lanka Standard Time", "Sri Jayawardenepura"},
    {"Nepal Standard Time", "Kathmandu"},
    {"Central Asia Standard Time", "Astana"},
    {"Bangladesh Standard Time", "Dhaka"},
    {"Omsk Standard Time", "Omsk"},
    {"Myanmar Standard Time", "Yangon (Rangoon)"},
    {"SE Asia Standard Time", "Bangkok, Hanoi, Jakarta"},
    {"Altai Standard Time", "Barnaul, Gorno-Altaysk"},
    {"W. Mongolia Standard Time", "Hovd"},
    {"North Asia Standard Time", "Krasnoyarsk"},
    {"N. Central Asia Standard Time", "Novosibirsk"},
    {"Tomsk Standard Time", "Tomsk"},
    {"China Standard Time", "Beijing, Chongqing, Hong Kong, Urumqi"},
    {"North Asia East Standard Time", "Irkutsk"},
    {"Singapore Standard Time", "Kuala Lumpur, Singapore"},
    {"W. Australia Standard Time", "Perth"},
    {"Taipei Standard Time", "Taipei"},
    {"Ulaanbaatar Standard Time", "Ulaanbaatar"},
    {"Aus Central W. Standard Time", "Eucla"},
    {"Transbaikal Standard Time", "Chita"},
    {"Tokyo Standard Time", "Osaka, Sapporo, Tokyo"},
    {"North Korea Standard Time", "Pyongyang"},
    {"Korea Standard Time", "Seoul"},
    {"Yakutsk Standard Time", "Yakutsk"},
    {"Cen. Australia Standard Time", "Adelaide"},
    {"AUS Central Standard Time", "Darwin"},
    {"E. Australia Standard Time", "Brisbane"},
    {"AUS Eastern Standard Time", "Canberra, Melbourne, Sydney"},
    {"West Pacific Standard Time", "Guam, Port Moresby"},
    {"Tasmania Standard Time", "Hobart"},
    {"Vladivostok Standard Time", "Vladivostok"},
    {"Lord Howe Standard Time", "Lord Howe Island"},
    {"Bougainville Standard Time", "Bougainville Island"},
    {"Russia Time Zone 10", "Chokurdakh"},
    {"Magadan Standard Time", "Magadan"},
    {"Norfolk Standard Time", "Norfolk Island"},
    {"Sakhalin Standard Time", "Sakhalin"},
    {"Central Pacific Standard Time", "Solomon Is., New Caledonia"},
    {"Russia Time Zone 11", "Anadyr, Petropavlovsk-Kamchatsky"},
    {"New Zealand Standard Time", "Auckland, Wellington"},
    {"UTC+12", "Coordinated Universal Time+12"},
    {"Fiji Standard Time", "Fiji"},
    {"Kamchatka Standard Time", "Petropavlovsk-Kamchatsky - Old"},
    {"Chatham Islands Standard Time", "Chatham Islands"},
    {"UTC+13", "Coordinated Universal Time+13"},
    {"Tonga Standard Time", "Nuku'alofa"},
    {"Samoa Standard Time", "Samoa"},
    {"Line Islands Standard Time", "Kiritimati Island"},
};

Extensions.cs

public static string Abbreviation(this TimeZoneInfo tz, DateTime? dt)
{
    // we use standard stuff but in the event that culturally they use something
    // different, handle the overrides here
    if (tz.StandardName.MatchesTrimmed("GMT Standard Time"))
    {
        if (dt.HasValue && tz.IsDaylightSavingTime(dt.Value)) return "BST"; //summer is known as British Summer Time more than GMT Daylight time
        return "GMT"; //apparently the other times are known as GMT instead of GST
    }
    else if (tz.StandardName.MatchesTrimmed("E. Australia Standard Time"))
    {
        if (dt.HasValue && tz.IsDaylightSavingTime(dt.Value)) return "AEDT";
        return "AEST";
    }
    else if (tz.StandardName.MatchesTrimmed("W. Australia Standard Time"))
    {
        if (dt.HasValue && tz.IsDaylightSavingTime(dt.Value)) return "AWDT";
        return "AWST";
    }
    else if (tz.StandardName.MatchesTrimmed("Malay Peninsula Standard Time"))
    {
        if (dt.HasValue && tz.IsDaylightSavingTime(dt.Value)) return "SGT";
        return "SGT";
    }

    // ok now the normal stuff
    var name = tz.StandardName;
    if (dt.HasValue && tz.IsDaylightSavingTime(dt.Value)) name = tz.DaylightName;

    var words = name.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

    if (words.Length > 1)
        return string.Join("", words.Select(x => x.Substring(0, 1).ToUpper()).Where(x => !x.MatchesRegex(@"\(|\)")).ToArray());
    else
        return name;
}
mattjohnsonpint commented 4 years ago

The values returned from properties of a TimeZoneInfo object are coming from .NET itself, not from TimeZoneConverter.

On non-Windows systems, those values come from ICU when available, and fall back to offsets when not. You can parse through the source code here to understand the details.

On Windows, those value come from NLS data in the registry - up until .NET 5 which is switching to ICU across the board.

mattjohnsonpint commented 4 years ago

You might also be interested in my companion library - TimeZoneNames

jslaybaugh commented 4 years ago

Thanks, I'll definitely check out that library for the names -- it'll save me a bunch of lines of code in doing that myself.

But I'm still unclear on how two (seemingly) identical non-windows systems would return different values?

mattjohnsonpint commented 4 years ago

Sorry for the delayed response. Is it possible this is the solution: https://github.com/dotnet/core/issues/2186#issuecomment-472559583 ?