namespace CompatBot.Utils; public static class TimeParser { public static readonly Dictionary TimeZoneMap; public static readonly Dictionary TimeZoneAcronyms = new() { ["PT"] = ["Pacific Standard Time", "America/Los_Angeles"], ["PST"] = ["Pacific Standard Time", "America/Los_Angeles"], ["PDT"] = ["Pacific Standard Time", "Pacific Daylight Time", "America/Los_Angeles"], ["EST"] = ["Eastern Standard Time", "America/New_York"], ["EDT"] = ["Eastern Standard Time", "Eastern Daylight Time", "America/New_York"], ["CEST"] = ["Central European Standard Time", "Europe/Berlin"], ["BST"] = ["British Summer Time", "GMT Standard Time", "Europe/London"], ["JST"] = ["Japan Standard Time", "Tokyo Standard Time", "Asia/Tokyo"], }; static TimeParser() { var uniqueNames = new HashSet( from tznl in TimeZoneAcronyms.Values from tzn in tznl select tzn, StringComparer.InvariantCultureIgnoreCase ); Config.Log.Trace("[TZI] Unique TZA names: " + uniqueNames.Count); var tzList = TimeZoneInfo.GetSystemTimeZones(); Config.Log.Trace("[TZI] System TZI count: " + tzList.Count); var result = new Dictionary(); var standardNames = new Dictionary(); var daylightNames = new Dictionary(); foreach (var tzi in tzList) { Config.Log.Trace($"[TZI] Checking {tzi.Id} ({tzi.DisplayName} / {tzi.StandardName} / {tzi.DaylightName})"); if (uniqueNames.Contains(tzi.StandardName) || uniqueNames.Contains(tzi.DaylightName) || uniqueNames.Contains(tzi.Id)) { Config.Log.Trace("[TZI] Looks like it's a match!"); var acronyms = ( from tza in TimeZoneAcronyms where tza.Value.Contains(tzi.StandardName) || tza.Value.Contains(tzi.DaylightName) || tza.Value.Contains(tzi.Id) select tza.Key ).ToList(); Config.Log.Trace("[TZI] Matched acronyms: " + string.Join(", ", acronyms)); foreach (var tza in acronyms) result[tza] = tzi; } else { var a = tzi.StandardName.GetAcronym(includeAllCaps: true, includeAllDigits: true); if (TimeZoneAcronyms.ContainsKey(a)) continue; standardNames.TryAdd(a, tzi); a = tzi.DaylightName.GetAcronym(includeAllCaps: true, includeAllDigits: true); if (TimeZoneAcronyms.ContainsKey(a) || standardNames.ContainsKey(a)) continue; daylightNames.TryAdd(a, tzi); } } Config.Log.Trace("[TZI] Total matched acronyms: " + result.Count); TimeZoneMap = result.Concat(standardNames).Concat(daylightNames) .DistinctBy(kvp => kvp.Key) .ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } public static bool TryParse(string dateTime, out DateTime result) { result = default; if (string.IsNullOrEmpty(dateTime)) return false; dateTime = dateTime.ToUpperInvariant(); if (char.IsDigit(dateTime[^1])) return DateTime.TryParse(dateTime, out result); var cutIdx = dateTime.LastIndexOf(' '); if (cutIdx < 0) return false; var tza = dateTime[(cutIdx+1)..]; dateTime = dateTime[..cutIdx]; if (TimeZoneMap.TryGetValue(tza, out var tzi)) { if (!DateTime.TryParse(dateTime, out result)) return false; result = TimeZoneInfo.ConvertTimeToUtc(result, tzi); return true; } return false; } public static DateTime Normalize(this DateTime date) => date.Kind switch { DateTimeKind.Utc => date, DateTimeKind.Local => date.ToUniversalTime(), _ => date.AsUtc(), }; public static List GetSupportedTimeZoneAbbreviations() => TimeZoneMap.Keys.ToList(); }