Does .NET have the history of time zone changes?

you can try NodaTime,

http://nodatime.org/

better explanation of what is does

http://blog.nodatime.org/2011/08/what-wrong-with-datetime-anyway.html


Historical time zone data is very complex and filled with many examples of small exceptions that apply in specific geographical regions that today have no easy way of being described. Not only are the offsets changing, but so are the regions to which they are applied.

There is a project to model the history of this data from around the world here:

  • http://www.iana.org/time-zones
  • http://en.wikipedia.org/wiki/Tz_database

.NET doesn't support what you want out of the box. Making a general solution to the problem will be very difficult, however if you only need it over a small set of regions, then you should be able to populate this yourself using the TimeZoneInfo class, the documentation for which states:

The members of the TimeZoneInfo class support the following operations:

  • Creating a new time zone that is not already defined by the operating system

.NET tracks some history, but it is not always accurate. You've stumbled upon one of the inaccuracies.

.NET imports all of it's time zone information from Windows via the registry, as described here and here. If you look in the registry at HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\Russian Standard Time\Dynamic DST you'll find that it only tracks information from 2010 forward for this time zone. Testing dates in year 2000 is not going to work well, as it will fall back to the earliest rule available (2010).

Base UTC offset information is tracked in the registry, but not in the AdjustmentRule class that .NET imports it into. If you check the adjustment rules for this time zone, you'll find that 2012 and 2013 are not imported at all:

var tz = TimeZoneInfo.FindSystemTimeZoneById("Russian Standard Time");
foreach (var rule in tz.GetAdjustmentRules())
{
    Console.WriteLine("{0:d} - {1:d}", rule.DateStart, rule.DateEnd);
}

OUTPUT:

1/1/0001 - 12/31/2010
1/1/2011 - 12/31/2011
1/1/2014 - 12/31/2014

Even though they exist in the Windows registry, 2012 and 2013 are not imported because they have no daylight saving time adjustments.

This creates a problem when the base offset changes - like it has for this time zone. Since it is currently +3, and the two year where it was +4 were not imported, then it will look like it was +3 for those missing years.

There is no good solution for this using TimeZoneInfo. Even if you try to create your own custom time zones, you'll have trouble fitting this kind of change into the data structures available.

Fortunately, there is another option. You can use standard IANA time zones, via the Noda Time library.

The following code uses Noda Time to match what you wrote in your original code:

DateTimeZone tz = DateTimeZoneProviders.Tzdb.GetSystemDefault();
Console.WriteLine(Instant.FromUtc(2012, 1, 1, 0, 0).InZone(tz).LocalDateTime);
Console.WriteLine(Instant.FromUtc(2012, 6, 1, 0, 0).InZone(tz).LocalDateTime);
Console.WriteLine(Instant.FromUtc(2000, 1, 1, 0, 0).InZone(tz).LocalDateTime);
Console.WriteLine(Instant.FromUtc(2000, 6, 1, 0, 0).InZone(tz).LocalDateTime);

If your local time zone is not already set for Moscow, you can change the first line to:

DateTimeZone tz = DateTimeZoneProviders.Tzdb["Europe/Moscow"];

OUTPUT:

1/1/2012 4:00:00 AM
6/1/2012 4:00:00 AM
1/1/2000 3:00:00 AM
6/1/2000 4:00:00 AM

Update

The problem described above of AdjustmentRule not tracking base offset changes was described in Microsoft Support article KB3012229, and subsequently fixed in .NET Framework 4.6 and also in .NET Core.

In the reference sources, one can see that AdjustmentRule now keeps a m_baseUtcOffsetDelta field. While this field is not exposed via a public property, it does factor in to the calculations, and it does reflect in serialization if you use the FromSerializedString and ToSerializedString methods (if anyone actually uses those).