{"id":33207,"date":"2021-06-08T07:20:57","date_gmt":"2021-06-08T14:20:57","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=33207"},"modified":"2021-06-08T07:20:57","modified_gmt":"2021-06-08T14:20:57","slug":"date-time-and-time-zone-enhancements-in-net-6","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/date-time-and-time-zone-enhancements-in-net-6\/","title":{"rendered":"Date, Time, and Time Zone Enhancements in .NET 6"},"content":{"rendered":"<p>I&#8217;m excited to share with you some of the improvements that have been made to .NET that are coming in .NET 6 in the area of dates, times, and time zones. You can try out all of the following, starting with <strong><a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/announcing-net-6-preview-4\/\">.NET 6 Preview 4<\/a>.<\/strong><\/p>\n<p>In this blog post, I&#8217;m going to cover the following topics:<\/p>\n<ul>\n<li><a href=\"#introducing-the-dateonly-and-timeonly-types\">The new DateOnly and TimeOnly types<\/a><\/li>\n<li><a href=\"#time-zone-conversion-apis\">Time Zone Conversion APIs<\/a><\/li>\n<li><a href=\"#time-zone-display-names-on-linux-and-macos\">Time Zone Display Names on Linux and macOS<\/a><\/li>\n<li><a href=\"#timezoneinfo-adjustmentrule-improvements\">TimeZoneInfo.AdjustmentRule Improvements<\/a><\/li>\n<\/ul>\n<p>For even more details, you can also refer to <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/45318\">dotnet\/runtime#45318<\/a> on GitHub.<\/p>\n<h2>Introducing the DateOnly and TimeOnly Types<\/h2>\n<p>If you&#8217;ve worked with dates and times in .NET, you&#8217;ve probably used <code>DateTime<\/code>, <code>DateTimeOffset<\/code>, <code>TimeSpan<\/code> and <code>TimeZoneInfo<\/code>. With this release, we introduce two additional types: <code>DateOnly<\/code> and <code>TimeOnly<\/code>. Both are in the <code>System<\/code> namespace and are built-in to .NET, just like the other date and time types.<\/p>\n<h3>The DateOnly Type<\/h3>\n<p>The <code>DateOnly<\/code> type is a structure that is intended to represent <em>only<\/em> a date. In other words, just a year, month, and day. Here&#8217;s a brief example:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Construction and properties\r\nDateOnly d1 = new DateOnly(2021, 5, 31);\r\nConsole.WriteLine(d1.Year);      \/\/ 2021\r\nConsole.WriteLine(d1.Month);     \/\/ 5\r\nConsole.WriteLine(d1.Day);       \/\/ 31\r\nConsole.WriteLine(d1.DayOfWeek); \/\/ Monday\r\n\r\n\/\/ Manipulation\r\nDateOnly d2 = d1.AddMonths(1);  \/\/ You can add days, months, or years. Use negative values to subtract.\r\nConsole.WriteLine(d2);     \/\/ \"6\/30\/2021\"  notice no time\r\n\r\n\/\/ You can use the DayNumber property to find out how many days are between two dates\r\nint days = d2.DayNumber - d1.DayNumber;\r\nConsole.WriteLine($\"There are {days} days between {d1} and {d2}\");\r\n\r\n\/\/ The usual parsing and string formatting tokens all work as expected\r\nDateOnly d3 = DateOnly.ParseExact(\"31 Dec 1980\", \"dd MMM yyyy\", CultureInfo.InvariantCulture);  \/\/ Custom format\r\nConsole.WriteLine(d3.ToString(\"o\", CultureInfo.InvariantCulture));   \/\/ \"1980-12-31\"  (ISO 8601 format)\r\n\r\n\/\/ You can combine with a TimeOnly to get a DateTime\r\nDateTime dt = d3.ToDateTime(new TimeOnly(0, 0));\r\nConsole.WriteLine(dt);       \/\/ \"12\/31\/1980 12:00:00 AM\"\r\n\r\n\/\/ If you want the current date (in the local time zone)\r\nDateOnly today = DateOnly.FromDateTime(DateTime.Today);<\/code><\/pre>\n<p>A <code>DateOnly<\/code> is ideal for scenarios such as birth dates, anniversary dates, hire dates, and other business dates that are not typically associated with any particular time. Another way to think about it is that a <code>DateOnly<\/code> represents the <em>entire<\/em> date (from the start of the day through the end of the day) such as would be visualized by a given square of a printed wall calendar. Until now, you may have used <code>DateTime<\/code> for this purpose, likely with the time set to midnight (<code>00:00:00.0000000<\/code>). While that still works, there are several advantages to using a <code>DateOnly<\/code> instead. These include:<\/p>\n<ul>\n<li>A <code>DateOnly<\/code> provides better type safety than a <code>DateTime<\/code> that is intended to represent just a date. This matters when using APIs, as not every action that makes sense for a date and time also makes sense for a whole date. For example, the <code>TimeZoneInfo.ConvertTime<\/code> method can be used to convert a <code>DateTime<\/code> from one time zone to another. Passing it a whole date makes no sense, as only a single point in time on that date could possibly be converted. With <code>DateTime<\/code>, these nonsensical operations can happen, and are partially to blame for bugs that might shift someone&#8217;s birthday a day late or a day early. Since no such time zone conversion API would work with a <code>DateOnly<\/code>, accidental misuse is prevented.<\/li>\n<li>A <code>DateTime<\/code> also contains a <code>Kind<\/code> property of type <code>DateTimeKind<\/code>, which can be either <code>Local<\/code>, <code>Utc<\/code> or <code>Unspecified<\/code>. The kind affects behavior of conversion APIs as well as formatting and parsing of strings. A <code>DateOnly<\/code> has no such kind &#8211; it is effectively <code>Unspecified<\/code>, always.<\/li>\n<li>When serializing a <code>DateOnly<\/code>, you only need to include the year, month, and day. This makes your data clearer by preventing a bunch of zeros from being tacked on to the end. It also makes it clear to any consumer of your API that the value represents a whole date, not the time at midnight on that date.<\/li>\n<li>When interacting with a database (such as SQL Server and others), whole dates are almost always stored in a <code>date<\/code> data type. Until now, the APIs for storing and retrieving such data have been strongly tied to the <code>DateTime<\/code> type. On storage, the time would be truncated, potentially leading to data loss. On retrieval, the time would be set to zeros and would be indistinguishable from a date at midnight. Having a <code>DateOnly<\/code> type allows a more exact matching type to a database&#8217;s <code>date<\/code> type. Note, there is <a href=\"https:\/\/github.com\/dotnet\/runtime\/issues\/49036#issuecomment-806444260\">still work to do<\/a> for the various data providers support this new type, but at least it is now possible.<\/li>\n<\/ul>\n<p>A <code>DateOnly<\/code> has a range from <code>0001-01-01<\/code> through <code>9999-12-31<\/code>, just like <code>DateTime<\/code>. We&#8217;ve also included a constructor that will accept any of the calendars supported by .NET. However just like <code>DateTime<\/code>, a <code>DateOnly<\/code> object is <em>always<\/em> representing values of the <a href=\"https:\/\/en.wikipedia.org\/wiki\/Proleptic_Gregorian_calendar\">Proleptic Gregorian calendar<\/a>, regardless of which calendar was used to construct it. If you do pass a calendar to the constructor, it will only be used to <em>interpret<\/em> the year, month, and day values passed into the same constructor. For example:<\/p>\n<pre><code class=\"language-csharp\">Calendar hebrewCalendar = new HebrewCalendar();\r\nDateOnly d4 = new DateOnly(5781, 9, 16, hebrewCalendar);                   \/\/ 16 Sivan 5781\r\nConsole.WriteLine(d4.ToString(\"d MMMM yyyy\", CultureInfo.InvariantCulture)); \/\/ 27 May 2021<\/code><\/pre>\n<p>For more on this, see <a href=\"https:\/\/docs.microsoft.com\/dotnet\/standard\/datetime\/working-with-calendars\"><em>Working with calendars<\/em><\/a>.<\/p>\n<h3>The TimeOnly Type<\/h3>\n<p>We also get a new <code>TimeOnly<\/code> type, which is a structure that is intended to represent <em>only<\/em> a time of day. If <code>DateOnly<\/code> is one half of a <code>DateTime<\/code>, then <code>TimeOnly<\/code> is the other half. Here&#8217;s a brief example:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Construction and properties\r\nTimeOnly t1 = new TimeOnly(16, 30);\r\nConsole.WriteLine(t1.Hour);      \/\/ 16\r\nConsole.WriteLine(t1.Minute);    \/\/ 30\r\nConsole.WriteLine(t1.Second);    \/\/ 0\r\n\r\n\/\/ You can add hours, minutes, or a TimeSpan (using negative values to subtract).\r\nTimeOnly t2 = t1.AddHours(10);\r\nConsole.WriteLine(t2);     \/\/ \"2:30 AM\"  notice no date, and we crossed midnight\r\n\r\n\/\/ If desired, we can tell how many days were \"wrapped\" as the clock passed over midnight.\r\nTimeOnly t3 = t2.AddMinutes(5000, out int wrappedDays);\r\nConsole.WriteLine($\"{t3}, {wrappedDays} days later\");  \/\/ \"1:50 PM, 3 days later\"\r\n\r\n\/\/ You can subtract to find out how much time has elapsed between two times.\r\n\/\/ Use \"end time - start time\".  The order matters, as this is a circular clock.  For example:\r\nTimeOnly t4 = new TimeOnly(2, 0);  \/\/  2:00  (2:00 AM)\r\nTimeOnly t5 = new TimeOnly(21, 0); \/\/ 21:00  (9:00 PM)\r\nTimeSpan x = t5 - t4;\r\nTimeSpan y = t4 - t5;\r\nConsole.WriteLine($\"There are {x.TotalHours} hours between {t4} and {t5}\"); \/\/ 19 hours\r\nConsole.WriteLine($\"There are {y.TotalHours} hours between {t5} and {t4}\"); \/\/  5 hours\r\n\r\n\/\/ The usual parsing and string formatting tokens all work as expected\r\nTimeOnly t6 = TimeOnly.ParseExact(\"5:00 pm\", \"h:mm tt\", CultureInfo.InvariantCulture);  \/\/ Custom format\r\nConsole.WriteLine(t6.ToString(\"T\", CultureInfo.InvariantCulture));   \/\/ \"17:00:00\"  (long time format)\r\n\r\n\/\/ You can get an equivalent TimeSpan for use with previous APIs\r\nTimeSpan ts = t6.ToTimeSpan();\r\nConsole.WriteLine(ts);      \/\/ \"17:00:00\"\r\n\r\n\/\/ Or, you can combine with a DateOnly to get a DateTime\r\nDateTime dt = new DateOnly(1970, 1, 1).ToDateTime(t6);\r\nConsole.WriteLine(dt);       \/\/ \"1\/1\/1970 5:00:00 PM\"\r\n\r\n\/\/ If you want the current time (in the local time zone)\r\nTimeOnly now = TimeOnly.FromDateTime(DateTime.Now);\r\n\r\n\/\/ You can easily tell if a time is between two other times\r\nif (now.IsBetween(t1, t2))\r\n    Console.WriteLine($\"{now} is between {t1} and {t2}.\");\r\nelse\r\n    Console.WriteLine($\"{now} is NOT between {t1} and {t2}.\");<\/code><\/pre>\n<p>A <code>TimeOnly<\/code> is ideal for scenarios such as recurring meeting times, daily alarm clock times, or the times that a business opens and closes each day of the week. Because a <code>TimeOnly<\/code> isn&#8217;t associated with any particular date, it is best visualized as a circular analog clock that might hang on your wall (albeit a 24-hour clock, not a 12-hour clock). Until now, there have been two common ways that such values were represented, either using a <code>TimeSpan<\/code> type or a <code>DateTime<\/code> type. While those approaches still work, there are several advantages to using a <code>TimeOnly<\/code> instead, including:<\/p>\n<ul>\n<li>A <code>TimeSpan<\/code> is primarily intended for <em>elapsed<\/em> time, such as you would measure with a stopwatch. Its upper range is more than 29,000 <em>years<\/em>, and its values can also be <em>negative<\/em> to indicate moving backward in time. Conversely, a <code>TimeOnly<\/code> is intended for a time-of-day value, so its range is from <code>00:00:00.0000000<\/code> to <code>23:59:59.9999999<\/code>, and is always positive. When a <code>TimeSpan<\/code> is used as a time of day, there is a risk that it could be manipulated such that it is out of an acceptable range. There is no such risk with a <code>TimeOnly<\/code>.<\/li>\n<li>Using a <code>DateTime<\/code> for a time-of-day value requires assigning some arbitrary date. A common date picked is <code>DateTime.MinValue<\/code> (<code>0001-01-01<\/code>), but that sometimes leads to out of range exceptions during manipulation, if time is subtracted. Picking some other arbitrary date still requires remembering to later disregard it &#8211; which can be a problem during serialization.<\/li>\n<li><code>TimeOnly<\/code> is a true time-of-day type, and so it offers superior type safety for such values vs <code>DateTime<\/code> or <code>TimeSpan<\/code>, in the same way that using a <code>DateOnly<\/code> offers better type safety for date values than a <code>DateTime<\/code>.<\/li>\n<li>A common operation with time-of-day values is to add or subtract some period of elapsed time. Unlike <code>TimeSpan<\/code>, a <code>TimeOnly<\/code> value will correctly handle such operations when crossing midnight. For example, an employee&#8217;s shift might start at <code>18:00<\/code> and last for 8 hours, ending at <code>02:00<\/code>. <code>TimeOnly<\/code> will take care of that during the addition operation, and it also has an <code>InBetween<\/code> method that can easily be used to tell if any given time is within the worker&#8217;s shift.<\/li>\n<\/ul>\n<h3>Why are they named &#8220;Only&#8221;?<\/h3>\n<p>Naming things is always difficult and this was no different. Several different names were considered and debated at length, but ultimately we decided on <code>DateOnly<\/code> and <code>TimeOnly<\/code> because they met several different constraints:<\/p>\n<ul>\n<li>They did not use any .NET language&#8217;s reserved keywords. <code>Date<\/code> would have been an ideal name, but it is a VB.NET language keyword and data type, which is an alias for <code>System.DateTime<\/code>, and thus could not be chosen.<\/li>\n<li>They would be easily discoverable in documentation and IntelliSense, by starting with &#8220;Date&#8221; or &#8220;Time&#8221;. We felt this was important, as many .NET developers are accustomed to using <code>DateTime<\/code> and <code>TimeSpan<\/code> types. Other platforms use prefixed names for the same functionality, such as Java&#8217;s <code>LocalDate<\/code> and <code>LocalTime<\/code> classes in the <a href=\"https:\/\/docs.oracle.com\/javase\/8\/docs\/api\/java\/time\/package-summary.html\">java.time<\/a> package, or the <code>PlainDate<\/code> and <code>PlainTime<\/code> types in the upcoming <a href=\"https:\/\/tc39.es\/proposal-temporal\/docs\/\">Temporal<\/a> proposal for JavaScript. However, both of those counterexamples have all date and time types grouped in a specific namespace, where .NET has its date and time types in the much larger <code>System<\/code> namespace.<\/li>\n<li>They would avoid confusion with existing APIs as much as possible. In particular, both the <code>DateTime<\/code> and <code>DateTimeOffset<\/code> types have properties that are named <code>Date<\/code> (which returns a <code>DateTime<\/code>) and <code>TimeOfDay<\/code> (which returns a <code>TimeSpan<\/code>). We felt that it would be extremely confusing if we used the name <code>TimeOfDay<\/code> instead of <code>TimeOnly<\/code>, but the <code>DateTime.TimeOfDay<\/code> property returned a <code>TimeSpan<\/code> type instead of a <code>TimeOfDay<\/code> type. If we could go back and do it all over from scratch then we would pick these as both the names of the properties <em>and<\/em> the names of the types they return, but such a breaking change is not possible now.<\/li>\n<li>They are easy to remember, and intuitively state what they are for. Indeed, &#8220;date-only and time-only values&#8221; are good descriptions for how the <code>DateOnly<\/code> and <code>TimeOnly<\/code> types should be used. Furthermore, they combine to make a <code>DateTime<\/code> so giving them similar names keeps them logically paired together.<\/li>\n<\/ul>\n<h3>What about Noda Time?<\/h3>\n<p>In introducing these two types, many have asked about using <a href=\"https:\/\/nodatime.org\/\">Noda Time<\/a> instead. Indeed, Noda Time is a great example of a high-quality, community developed .NET open source library, and you can certainly use it if desired. However, we didn&#8217;t feel that implementing a Noda-like API in .NET itself was warranted. After careful evaluation, it was decided that it would be better to <em>augment<\/em> the existing types to fill in the gaps rather than to overhaul and replace them. After all, there are many .NET applications built using the existing <code>DateTime<\/code>, <code>DateTimeOffset<\/code>, <code>TimeSpan<\/code>, and <code>TimeZoneInfo<\/code> types. The <code>DateOnly<\/code> and <code>TimeOnly<\/code> types should feel natural to use along side them.<\/p>\n<p>Additionally, support for interchanging <code>DateOnly<\/code> and <code>TimeOnly<\/code> with their equivalent Noda Time types (<code>LocalDate<\/code> and <code>LocalTime<\/code>) <a href=\"https:\/\/github.com\/nodatime\/nodatime\/issues\/1635\">has been proposed<\/a>.<\/p>\n<h2>Time Zone Conversion APIs<\/h2>\n<p>First a bit of background and history. Generally speaking, there are two sets of time zone data used in computing:<\/p>\n<ul>\n<li>The set of time zones created by Microsoft that ship with Windows.\n<ul>\n<li>Example ID: <code>\"AUS Eastern Standard Time\"<\/code><\/li>\n<\/ul>\n<\/li>\n<li>The set of time zones that everyone else uses, which are currently maintained by <a href=\"https:\/\/www.iana.org\/time-zones\">IANA<\/a>.\n<ul>\n<li>Example ID: <code>\"Australia\/Sydney\"<\/code><\/li>\n<\/ul>\n<\/li>\n<\/ul>\n<p>By <em>everyone<\/em> else, I don&#8217;t just mean Linux and macOS, but also Java, Python, Perl, Ruby, Go, JavaScript, and many others.<\/p>\n<p>Support for time zones in .NET is provided by the <code>TimeZoneInfo<\/code> class. However, this class was designed original with .NET Framework 3.5, which only ran on Windows operating systems. As such, <code>TimeZoneInfo<\/code> took its time zone data from Windows. This quickly became a problem for those that wanted to reference time zones in data passed between systems. When .NET Core came out, this problem was exacerbated because Windows time zone data was not available on non-Windows systems like Linux and macOS.<\/p>\n<p>Previously, the <code>TimeZoneInfo.FindSystemTimeZoneById<\/code> method looked up time zones available <em>on the operating system<\/em>. That means Windows time zones for Windows systems, and IANA time zones for everyone else. That is problematic, especially if one is aiming for cross-platform portability of their code and data. Until now, the way to deal with this issue has been to <em>manually<\/em> translate between one set of time zones to the other, preferably using the mappings established and maintained by the <a href=\"https:\/\/github.com\/unicode-org\/cldr\">Unicode CLDR Project<\/a>. These mappings are also surfaced by libraries such as <a href=\"https:\/\/github.com\/unicode-org\/icu\">ICU<\/a>. More commonly, .NET developers have used the <a href=\"https:\/\/github.com\/mattjohnsonpint\/TimeZoneConverter\">TimeZoneConverter<\/a> library which also uses these mappings. While any of these approaches continue to work, there is now an easier way.<\/p>\n<p>Starting with this release, the <code>TimeZoneInfo.FindSystemTimeZoneById<\/code> method will <em>automatically<\/em> convert its input to the opposite format if the requested time zone is not found on the system. That means that you can now use either IANA or Windows time zone IDs on any operating system that has time zone data installed*. It uses the same CLDR mappings, but gets them through <a href=\"https:\/\/docs.microsoft.com\/dotnet\/standard\/globalization-localization\/globalization-icu\">.NET&#8217;s ICU globalization support<\/a>, so you don&#8217;t have to use a separate library.<\/p>\n<p>A brief example:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Both of these will now work on any supported OS where ICU and time zone data are available.\r\nTimeZoneInfo tzi1 = TimeZoneInfo.FindSystemTimeZoneById(\"AUS Eastern Standard Time\");\r\nTimeZoneInfo tzi2 = TimeZoneInfo.FindSystemTimeZoneById(\"Australia\/Sydney\");<\/code><\/pre>\n<p>On Unix, the Windows time zones are not actually installed on the OS but their identifiers are recognized through the conversions and data provided by ICU. You can install <code>libicu<\/code> on your system, or you can use <a href=\"https:\/\/docs.microsoft.com\/dotnet\/standard\/globalization-localization\/globalization-icu#app-local-icu\">.NET&#8217;s App-Local ICU<\/a> feature to bundle the data with your application.<\/p>\n<p>*<em>Note, some .NET Docker images such as for Alpine Linux do not come with the <code>tzdata<\/code> package pre-installed, but you can <a href=\"https:\/\/github.com\/dotnet\/dotnet-docker\/issues\/1366\">easily add it<\/a>.<\/em><\/p>\n<p>Also with this release, we&#8217;ve added some new methods to the <code>TimeZoneInfo<\/code> class called <code>TryConvertIanaIdToWindowsId<\/code> and <code>TryConvertWindowsIdToIanaId<\/code>, for scenarios when you still need to manually convert from one form of time zone to another.<\/p>\n<p>Some example usage:<\/p>\n<pre><code class=\"language-csharp\">\/\/ Conversion from IANA to Windows\r\nstring ianaId1 = \"America\/Los_Angeles\";\r\nif (!TimeZoneInfo.TryConvertIanaIdToWindowsId(ianaId1, out string winId1))\r\n    throw new TimeZoneNotFoundException($\"No Windows time zone found for \"{ianaId1}\".\");\r\nConsole.WriteLine($\"{ianaId1} =&gt; {winId1}\");  \/\/ \"America\/Los_Angeles =&gt; Pacific Standard Time\"\r\n\r\n\/\/ Conversion from Windows to IANA when a region is unknown\r\nstring winId2 = \"Eastern Standard Time\";\r\nif (!TimeZoneInfo.TryConvertWindowsIdToIanaId(winId2, out string ianaId2))\r\n    throw new TimeZoneNotFoundException($\"No IANA time zone found for \"{winId2}\".\");\r\nConsole.WriteLine($\"{winId2} =&gt; {ianaId2}\");  \/\/ \"Eastern Standard Time =&gt; America\/New_York\"\r\n\r\n\/\/ Conversion from Windows to IANA when a region is known\r\nstring winId3 = \"Eastern Standard Time\";\r\nstring region = \"CA\"; \/\/ Canada\r\nif (!TimeZoneInfo.TryConvertWindowsIdToIanaId(winId3, region, out string ianaId3))\r\n    throw new TimeZoneNotFoundException($\"No IANA time zone found for \"{winId3}\" in \"{region}\".\");\r\nConsole.WriteLine($\"{winId3} + {region} =&gt; {ianaId3}\");  \/\/ \"Eastern Standard Time + CA =&gt; America\/Toronto\"<\/code><\/pre>\n<p>We&#8217;ve also added an instance property to <code>TimeZoneInfo<\/code> called <code>HasIanaId<\/code>, which returns <code>true<\/code> when the <code>Id<\/code> property is an IANA time zone identifier. That should help you determine whether conversion is necessary, depending on your needs. For example, perhaps you are using <code>TimeZoneInfo<\/code> objects loaded from a mix of either Windows or IANA identifiers, and then specifically need an IANA time zone identifier for some external API call. You can define a helper method as follows:<\/p>\n<pre><code class=\"language-csharp\">static string GetIanaTimeZoneId(TimeZoneInfo tzi)\r\n{\r\n    if (tzi.HasIanaId)\r\n        return tzi.Id;  \/\/ no conversion necessary\r\n\r\n    if (TimeZoneInfo.TryConvertWindowsIdToIanaId(tzi.Id, out string ianaId))\r\n        return ianaId;  \/\/ use the converted ID\r\n\r\n    throw new TimeZoneNotFoundException($\"No IANA time zone found for \"{tzi.Id}\".\");\r\n}<\/code><\/pre>\n<p>Or conversely, perhaps you are needing a Windows time zone identifier. For example, SQL Server&#8217;s <a href=\"https:\/\/docs.microsoft.com\/sql\/t-sql\/queries\/at-time-zone-transact-sql\"><code>AT TIME ZONE<\/code><\/a> function presently requires a Windows time zone identifier &#8211; even when using SQL Server on Linux. You can define a helper method as follows:<\/p>\n<pre><code class=\"language-csharp\">static string GetWindowsTimeZoneId(TimeZoneInfo tzi)\r\n{\r\n    if (!tzi.HasIanaId)\r\n        return tzi.Id;  \/\/ no conversion necessary\r\n\r\n    if (TimeZoneInfo.TryConvertIanaIdToWindowsId(tzi.Id, out string winId))\r\n        return winId;   \/\/ use the converted ID\r\n\r\n    throw new TimeZoneNotFoundException($\"No Windows time zone found for \"{tzi.Id}\".\");\r\n}<\/code><\/pre>\n<h2>Time Zone Display Names on Linux and macOS<\/h2>\n<p>Another common operation with time zones it to get a list of them, usually for purposes of asking an end-user to choose one. The <code>TimeZoneInfo.GetSystemTimeZones<\/code> method has always served this purpose well on Windows. It returns a read-only collection of <code>TimeZoneInfo<\/code> objects, and such a list can be built using the <code>Id<\/code> and <code>DisplayName<\/code> properties of each object.<\/p>\n<p>On Windows, .NET populates the display names using the resource files associated with the current OS display language. On Linux and macOS, the <a href=\"https:\/\/docs.microsoft.com\/dotnet\/standard\/globalization-localization\/globalization-icu\">ICU globalization data<\/a> is used instead. This is generally ok, except that one has to ensure that the <code>DisplayName<\/code> value is unambiguous with regard to the entire list of values, otherwise such a list becomes unusable. For example, there were 13 different time zones returned that all had the same display name of <code>\"(UTC-07:00) Mountain Standard Time\"<\/code>, making it near impossible for a user to pick the one that belonged to them &#8211; and yes, there are differences! For example, <code>America\/Denver<\/code> represents most of Mountain Time in the US, but <code>America\/Phoenix<\/code> is used in Arizona where daylight saving time is not observed.<\/p>\n<p>With this release, additional algorithms were added internally to choose better values from ICU to be used for display names. The lists are now much more usable. For example, <code>America\/Denver<\/code> is now displayed as <code>\"(UTC-07:00) Mountain Time (Denver)\"<\/code> while <code>America\/Phoenix<\/code> is displayed as <code>\"(UTC-07:00) Mountain Time (Phoenix)\"<\/code>. If you&#8217;d like to see how the rest of the list has changed, refer to the &#8220;Before&#8221; and &#8220;After&#8221; sections in <a href=\"https:\/\/github.com\/dotnet\/runtime\/pull\/48931\">the GitHub pull request<\/a>.<\/p>\n<p>Note that for now, the list of time zones and their display names on <em>Windows<\/em> remains mostly unchanged. However, a minor but related fix is that previously the display name for the UTC time zone was hard-coded to the English <code>\"Coordinated Universal Time\"<\/code>, which was a problem for other languages. It now correctly follows the same language as the rest of the time zone display names, on all operating systems.<\/p>\n<h2>TimeZoneInfo.AdjustmentRule Improvements<\/h2>\n<p>The last improvement to cover is one that is slightly lesser used, but just as important. The <code>TimeZoneInfo.AdjustmentRule<\/code> class is used as part of .NET&#8217;s in-memory representation of a time zone. A single <code>TimeZoneInfo<\/code> class can have zero to many adjustment rules. These rules keep track of how a time zone&#8217;s offset from UTC is adjusted over the course of history, so that the correct conversions can be made for a given point in time. Such changes are extremely complicated, and mostly beyond the scope of this article. However, I will describe some of the improvements that have been made.<\/p>\n<p>In the original design of <code>TimeZoneInfo<\/code>, it was assumed that the <code>BaseUtcOffset<\/code> would be a fixed value and that all the adjustment rules would simply control when daylight saving time started or stopped. Unfortunately, that design didn&#8217;t take into account that time zones have changed their <em>standard<\/em> offset at different points in history, such as when Yukon Territory, Canada <a href=\"https:\/\/www.timeanddate.com\/news\/time\/yukon-canada-permanent-dst.html\">recently decided<\/a> to stop going between UTC-8 and UTC-7, and instead stay at UTC-7 year-round. To accommodate such changes, .NET added an <em>internal<\/em> property (a long time ago) to the <code>TimeZoneInfo.AdjustmentRule<\/code> class called <code>BaseUtcOffsetDelta<\/code>. This value is used to keep track of how the <code>TimeZoneInfo.BaseUtcOffset<\/code> changes from one adjustment rule to the next.<\/p>\n<p>However, there are some advanced scenarios that occasionally require gaining access to all the raw data, and keeping one piece of the data hidden internally didn&#8217;t make much sense. So with this release, the <code>BaseUtcOffsetDelta<\/code> property on the <code>TimeZoneInfo.AdjustmentRule<\/code> class is made public. For completeness, we also took an additional step and created an overload to the <code>CreateAdjustmentRule<\/code> method, that accepts a <code>baseUtcOffsetDelta<\/code> parameter &#8211; not that we expect most developers will need or want to create custom time zones or adjustment rules.<\/p>\n<p>Two other minor improvements were made to how adjustment rules are populated from IANA data internally on non-Windows operating systems. They don&#8217;t affect external behavior, other than to ensure correctness in some edge cases.<\/p>\n<p>If this all sounds confusing, don&#8217;t worry you&#8217;re not alone. Thankfully, all of the logic to use the data correctly is already incorporated in the various methods on <code>TimeZoneInfo<\/code>, such as <code>GetUtcOffset<\/code> and <code>ConvertTime<\/code>. One generally doesn&#8217;t need to use adjustment rules.<\/p>\n<h2>Conclusion<\/h2>\n<p>Overall, things are shaping up quite a bit for date, time, and time zones in .NET 6. I&#8217;m excited to see how the new <code>DateOnly<\/code> and <code>TimeOnly<\/code> types make their way through the rest of the .NET ecosystem, especially with regard to serialization and databases. I&#8217;m also excited to see that .NET continues to make improvements to localization and usability &#8211; even in the obscure area of time zones!<\/p>\n<p>Please be sure to leave your feedback about these new features below. Thanks!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>.NET 6 Preview 4 introduces DateOnly and TimeOnly structs and improves time zone support.<\/p>\n","protected":false},"author":59397,"featured_media":33224,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685,196],"tags":[],"class_list":["post-33207","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","category-dotnet-core"],"acf":[],"blog_post_summary":"<p>.NET 6 Preview 4 introduces DateOnly and TimeOnly structs and improves time zone support.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/33207","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/59397"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=33207"}],"version-history":[{"count":0,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/33207\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/33224"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=33207"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=33207"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=33207"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}