TimezoneHelper.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. using System;
  2. using System.Collections.Generic;
  3. using System.IO;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Xml;
  7. using Abp.Extensions;
  8. using Abp.IO.Extensions;
  9. using Abp.Logging;
  10. using Abp.Reflection.Extensions;
  11. using Abp.Xml.Extensions;
  12. using TimeZoneConverter;
  13. namespace Abp.Timing.Timezone
  14. {
  15. /// <summary>
  16. /// A helper class for timezone operations
  17. /// </summary>
  18. public static class TimezoneHelper
  19. {
  20. static readonly Dictionary<string, string> WindowsTimeZoneMappings = new Dictionary<string, string>();
  21. static readonly Dictionary<string, string> IanaTimeZoneMappings = new Dictionary<string, string>();
  22. static readonly object SyncObj = new object();
  23. /// <summary>
  24. /// Maps given windows timezone id to IANA timezone id
  25. /// </summary>
  26. /// <param name="windowsTimezoneId"></param>
  27. /// <returns></returns>
  28. /// <exception cref="Exception"></exception>
  29. public static string WindowsToIana(string windowsTimezoneId)
  30. {
  31. if (windowsTimezoneId.Equals("UTC", StringComparison.OrdinalIgnoreCase))
  32. {
  33. return "Etc/UTC";
  34. }
  35. GetTimezoneMappings();
  36. if (WindowsTimeZoneMappings.ContainsKey(windowsTimezoneId))
  37. {
  38. return WindowsTimeZoneMappings[windowsTimezoneId];
  39. }
  40. throw new Exception($"Unable to map {windowsTimezoneId} to iana timezone.");
  41. }
  42. /// <summary>
  43. /// Maps given IANA timezone id to windows timezone id
  44. /// </summary>
  45. /// <param name="ianaTimezoneId"></param>
  46. /// <returns></returns>
  47. /// <exception cref="Exception"></exception>
  48. public static string IanaToWindows(string ianaTimezoneId)
  49. {
  50. if (ianaTimezoneId.Equals("Etc/UTC", StringComparison.OrdinalIgnoreCase))
  51. {
  52. return "UTC";
  53. }
  54. GetTimezoneMappings();
  55. if (IanaTimeZoneMappings.ContainsKey(ianaTimezoneId))
  56. {
  57. return IanaTimeZoneMappings[ianaTimezoneId];
  58. }
  59. throw new Exception(string.Format("Unable to map {0} to windows timezone.", ianaTimezoneId));
  60. }
  61. /// <summary>
  62. /// Converts a date from one timezone to another
  63. /// </summary>
  64. /// <param name="date"></param>
  65. /// <param name="fromTimeZoneId"></param>
  66. /// <param name="toTimeZoneId"></param>
  67. /// <returns></returns>
  68. public static DateTime? Convert(DateTime? date, string fromTimeZoneId, string toTimeZoneId)
  69. {
  70. if (!date.HasValue)
  71. {
  72. return null;
  73. }
  74. var sourceTimeZone = FindTimeZoneInfo(fromTimeZoneId);
  75. var destinationTimeZone = FindTimeZoneInfo(toTimeZoneId);
  76. return TimeZoneInfo.ConvertTime(date.Value, sourceTimeZone, destinationTimeZone);
  77. }
  78. /// <summary>
  79. /// Converts a utc datetime to a local time based on a timezone
  80. /// </summary>
  81. /// <param name="date"></param>
  82. /// <param name="toTimeZoneId"></param>
  83. /// <returns></returns>
  84. public static DateTime? ConvertFromUtc(DateTime? date, string toTimeZoneId)
  85. {
  86. return Convert(date, "UTC", toTimeZoneId);
  87. }
  88. /// <summary>
  89. /// Converts a utc datetime in to a datetimeoffset
  90. /// </summary>
  91. /// <param name="date"></param>
  92. /// <param name="timeZoneId"></param>
  93. /// <returns></returns>
  94. public static DateTimeOffset? ConvertFromUtcToDateTimeOffset(DateTime? date, string timeZoneId)
  95. {
  96. var zonedDate = ConvertFromUtc(date, timeZoneId);
  97. return ConvertToDateTimeOffset(zonedDate, timeZoneId);
  98. }
  99. /// <summary>
  100. /// Converts a nullable date with a timezone to a nullable datetimeoffset
  101. /// </summary>
  102. /// <param name="date"></param>
  103. /// <param name="timeZoneId"></param>
  104. /// <returns></returns>
  105. public static DateTimeOffset? ConvertToDateTimeOffset(DateTime? date, string timeZoneId)
  106. {
  107. if (!date.HasValue)
  108. {
  109. return null;
  110. }
  111. return ConvertToDateTimeOffset(date.Value, timeZoneId);
  112. }
  113. /// <summary>
  114. /// Converts a date with a timezone to a datetimeoffset
  115. /// </summary>
  116. /// <param name="date"></param>
  117. /// <param name="timeZoneId"></param>
  118. /// <returns></returns>
  119. public static DateTimeOffset ConvertToDateTimeOffset(DateTime date, string timeZoneId)
  120. {
  121. var timeZone = FindTimeZoneInfo(timeZoneId);
  122. var offset = timeZone.BaseUtcOffset;
  123. var rule = timeZone.GetAdjustmentRules().FirstOrDefault(x => date >= x.DateStart && date <= x.DateEnd);
  124. if (!timeZone.SupportsDaylightSavingTime || rule == null)
  125. {
  126. return new DateTimeOffset(date, offset);
  127. }
  128. var daylightStart = GetDaylightTransition(date, rule.DaylightTransitionStart);
  129. var daylightEnd = GetDaylightTransition(date, rule.DaylightTransitionEnd);
  130. if (date >= daylightStart && date <= daylightEnd)
  131. {
  132. offset = offset.Add(rule.DaylightDelta);
  133. }
  134. return new DateTimeOffset(date, offset);
  135. }
  136. private static DateTime GetDaylightTransition(DateTime date, TimeZoneInfo.TransitionTime transitionTime)
  137. {
  138. var daylightTime = new DateTime(date.Year, transitionTime.Month, 1);
  139. if (transitionTime.IsFixedDateRule)
  140. {
  141. daylightTime = new DateTime(daylightTime.Year, daylightTime.Month, transitionTime.Day, transitionTime.TimeOfDay.Hour, transitionTime.TimeOfDay.Minute, transitionTime.TimeOfDay.Second);
  142. }
  143. else
  144. {
  145. daylightTime = daylightTime.NthOf(transitionTime.Week, transitionTime.DayOfWeek);
  146. }
  147. daylightTime = new DateTime(daylightTime.Year,
  148. daylightTime.Month,
  149. daylightTime.Day,
  150. transitionTime.TimeOfDay.Hour,
  151. transitionTime.TimeOfDay.Minute,
  152. transitionTime.TimeOfDay.Second);
  153. return daylightTime;
  154. }
  155. //from https://stackoverflow.com/questions/6140018/how-to-calculate-2nd-friday-of-month-in-c-sharp
  156. private static DateTime NthOf(this DateTime currentDate, int occurrence, DayOfWeek day)
  157. {
  158. var firstDay = new DateTime(currentDate.Year, currentDate.Month, 1);
  159. var firstOccurrence = firstDay.DayOfWeek == day ? firstDay : firstDay.AddDays(day - firstDay.DayOfWeek);
  160. if (firstOccurrence.Month < currentDate.Month) occurrence = occurrence + 1;
  161. return firstOccurrence.AddDays(7 * (occurrence - 1));
  162. }
  163. public static DateTime? ConvertTimeByIanaTimeZoneId(DateTime? date, string fromIanaTimeZoneId, string toIanaTimeZoneId)
  164. {
  165. if (!date.HasValue)
  166. {
  167. return null;
  168. }
  169. var sourceTimeZone = FindTimeZoneInfo(IanaToWindows(fromIanaTimeZoneId));
  170. var destinationTimeZone = FindTimeZoneInfo(IanaToWindows(toIanaTimeZoneId));
  171. return TimeZoneInfo.ConvertTime(date.Value, sourceTimeZone, destinationTimeZone);
  172. }
  173. public static DateTime? ConvertTimeFromUtcByIanaTimeZoneId(DateTime? date, string toIanaTimeZoneId)
  174. {
  175. return ConvertTimeByIanaTimeZoneId(date, "Etc/UTC", toIanaTimeZoneId);
  176. }
  177. public static DateTime? ConvertTimeToUtcByIanaTimeZoneId(DateTime? date, string fromIanaTimeZoneId)
  178. {
  179. return ConvertTimeByIanaTimeZoneId(date, fromIanaTimeZoneId, "Etc/UTC");
  180. }
  181. public static TimeZoneInfo FindTimeZoneInfo(string windowsOrIanaTimeZoneId)
  182. {
  183. return TZConvert.GetTimeZoneInfo(windowsOrIanaTimeZoneId);
  184. }
  185. public static List<string> GetWindowsTimeZoneIds(bool ignoreTimeZoneNotFoundException = true)
  186. {
  187. return TZConvert.KnownWindowsTimeZoneIds.ToList();
  188. }
  189. private static void GetTimezoneMappings()
  190. {
  191. if (WindowsTimeZoneMappings.Count > 0 && IanaTimeZoneMappings.Count > 0)
  192. {
  193. return;
  194. }
  195. lock (SyncObj)
  196. {
  197. if (WindowsTimeZoneMappings.Count > 0 && IanaTimeZoneMappings.Count > 0)
  198. {
  199. return;
  200. }
  201. var assembly = typeof(TimezoneHelper).GetAssembly();
  202. var resourceNames = assembly.GetManifestResourceNames();
  203. var resourceName = resourceNames.First(r => r.Contains("WindowsZones.xml"));
  204. using (Stream stream = assembly.GetManifestResourceStream(resourceName))
  205. {
  206. var bytes = stream.GetAllBytes();
  207. var xmlString = Encoding.UTF8.GetString(bytes, 3, bytes.Length - 3); //Skipping byte order mark
  208. var xmlDocument = new XmlDocument();
  209. xmlDocument.LoadXml(xmlString);
  210. var windowsMappingNodes = xmlDocument.SelectNodes("//supplementalData/windowsZones/mapTimezones/mapZone[@territory='001']");
  211. var ianaMappingNodes = xmlDocument.SelectNodes("//supplementalData/windowsZones/mapTimezones/mapZone");
  212. AddWindowsMappingsToDictionary(WindowsTimeZoneMappings, windowsMappingNodes);
  213. AddIanaMappingsToDictionary(IanaTimeZoneMappings, ianaMappingNodes);
  214. }
  215. }
  216. }
  217. private static void AddWindowsMappingsToDictionary(Dictionary<string, string> timeZoneMappings, XmlNodeList defaultMappingNodes)
  218. {
  219. foreach (XmlNode defaultMappingNode in defaultMappingNodes)
  220. {
  221. var windowsTimezoneId = defaultMappingNode.GetAttributeValueOrNull("other");
  222. var ianaTimezoneId = defaultMappingNode.GetAttributeValueOrNull("type");
  223. if (windowsTimezoneId.IsNullOrEmpty() || ianaTimezoneId.IsNullOrEmpty())
  224. {
  225. continue;
  226. }
  227. timeZoneMappings.Add(windowsTimezoneId, ianaTimezoneId);
  228. }
  229. }
  230. private static void AddIanaMappingsToDictionary(Dictionary<string, string> timeZoneMappings, XmlNodeList defaultMappingNodes)
  231. {
  232. foreach (XmlNode defaultMappingNode in defaultMappingNodes)
  233. {
  234. var ianaTimezoneId = defaultMappingNode.GetAttributeValueOrNull("type");
  235. var windowsTimezoneId = defaultMappingNode.GetAttributeValueOrNull("other");
  236. if (ianaTimezoneId.IsNullOrEmpty() || windowsTimezoneId.IsNullOrEmpty())
  237. {
  238. continue;
  239. }
  240. ianaTimezoneId
  241. .Split(" ", StringSplitOptions.RemoveEmptyEntries)
  242. .Where(id => !timeZoneMappings.ContainsKey(id))
  243. .ToList()
  244. .ForEach(ianaId => timeZoneMappings.Add(ianaId, windowsTimezoneId));
  245. }
  246. }
  247. }
  248. }