Why doesn't C# detect that 1970/1/1 was under BST? - c#

I'm working with a 3rd party API that returns Time of Day values as DateTime values filling in Jan 1, 1970 as the date part. So for 5AM, it will return something like 1969-12-31T21:03:00.000-08:00
The problem is that, when if the user was on London time, C# fails to apply BST adjustment for 1970-01-01.
For example, 1970-01-01 5AM in UTC should be 1970-01-01 6AM in London.
See conversion
But, C# doesn't seem to apply this conversion:
var utcTime = new DateTime(1970, 1, 1, 5, 0, 0, DateTimeKind.Utc);
var britishZone = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time");
var ukTime = TimeZoneInfo.ConvertTime(utcTime, britishZone);
Console.WriteLine(ukTime);
The above code will still print 5AM.
However, if I set it to a more recent date that BST was effective such as Oct 1st, 2016, the same code works correctly printing 6AM.
Why does it behave this way?
How do I workaround this? Our app needs to work across any TimeZone (i.e. hard coding timezone isn't an option - we basically use the Windows local timezone).

When you use the TimeZoneInfo class on Windows to work with a system time zone ("GMT Standard Time" in this case), you are asking for time zone data from the Windows operating system, which is stored in the registry at:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones
Windows simply does not carry historical data back that far. For "GMT Standard Time", which is the time zone in London that alternates between GMT and BST, it only knows about one set of rules - the current ones that are in effect. It does not know about anything before that (it last changed in 1996).
Note that per Microsoft's DST/TZ Support Policy, only changes from 2010 forward are guaranteed to be recorded. There are several older changes that have historically been in Windows (such as the US 2007 DST change and others), which do indeed work, but from a global perspective you may not get the best results with dates before 2010.
For that, you'll need a full implementation of the IANA TZ Database, and in .NET, one of the best ways to do that is with the Noda Time library.
Your code, transliterated to Noda Time:
var utcTime = Instant.FromUtc(1970, 1, 1, 5, 0, 0);
var britishZone = DateTimeZoneProviders.Tzdb["Europe/London"];
var ukTime = utcTime.InZone(britishZone);
Console.WriteLine(ukTime.ToDateTimeUnspecified()); // just for equivalent output
// Prints: 1/1/1970 6:00:00 AM
Also note that if you are running .NET Core on Linux or OSX, then the time zones are indeed the IANA ones, thus your original code would work by just using the IANA identifer
var britishZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/London");
See also the timezone tag wiki.

Have you looked at creating a custom time zone adjustment for the historical period(s) that are in question?
https://msdn.microsoft.com/en-us/library/bb397784(v=vs.110).aspx
which can be used in the case of:
The time zone does not have accurate information about time zone adjustments for a particular historic period.

I think the code is working correctly. BSt starts from last sunday of march until last sunday of october where the clock moves one hour forward . That is UTC+1. Since you are looking at a date in jan, london time will be same as UTC.that is utc+0
Check the wikipedia for timezone details https://en.m.wikipedia.org/wiki/British_Summer_Time

Related

TimeZoneInfo SupportsDaylightSavingTime does not return what I expect, why?

When I started working with Time Zones I tough was an easy task, but the more I dag into the topic and the more I found myself lost.
In Stack Overflow there are already dozens of similar questions, mainly attempting to solve specific problems but none of them (answers included) helped me to understand the topic as I wanted.
On top of that, as others asked, from time to time it looks like some time zone changes, and Microsoft has to rush updates distributed via Windows Update to cope with it.
In my case I noticed that at least 2 TZs are incorrect:
Altai Standard Time
Argentina Standard Time
Because as I mentioned above the TZ are taken from the local system I tried to search for updates, but I have none. So either I am really missing something important, or Microsoft doesn't care about this two time zones.
Information about DST/TZ updates
For Daylight Saving Time, Microsoft has a clear policy and states:
Microsoft makes an effort to incorporate these changes to Windows, and publishes an update through Windows Update (WU). Each DST/TZ update released through WU will have the latest time data and will also supersede any previously issued DST/TZ update
The recent updates can be found in the dedicated Microsoft Tech Community site
To help myself understand, I created a simple console application (see the code below), problem is that this has not been enough.
Overview of the most important classes and methods used in the example
private static ConsoleColor DefaultColor;
static void Main(string[] args)
{
DefaultColor = Console.ForegroundColor;
foreach (var timeZoneInfo in TimeZoneInfo.GetSystemTimeZones().OrderBy(tz => tz.Id))
{
var firstQuart = new DateTime(2019, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var secondQuart = new DateTime(2019, 4, 1, 0, 0, 0, DateTimeKind.Utc);
var thirdQuart = new DateTime(2019, 7, 1, 0, 0, 0, DateTimeKind.Utc);
var lastQuart = new DateTime(2019, 10, 1, 0, 0, 0, DateTimeKind.Utc);
if (timeZoneInfo.Id == "Altai Standard Time" ||
timeZoneInfo.Id == "Argentina Standard Time" ||
timeZoneInfo.Id == "GMT Standard Time"
)
{
Log($"{timeZoneInfo.DisplayName} (ID: {timeZoneInfo.Id})", ConsoleColor.Yellow);
Log($"StandardName: {timeZoneInfo.StandardName}");
Log($"DST: {timeZoneInfo.SupportsDaylightSavingTime}");
Log($"Daylight Name: {timeZoneInfo.DaylightName}");
Log();
Log($"UTC Offset: {timeZoneInfo.BaseUtcOffset}");
Log($"Dates for each quarter in this year");
var convertedFirstQuart = TimeZoneInfo.ConvertTimeFromUtc(firstQuart, timeZoneInfo);
var convertedSecondQuart = TimeZoneInfo.ConvertTimeFromUtc(secondQuart, timeZoneInfo);
var convertedThirdQuart = TimeZoneInfo.ConvertTimeFromUtc(thirdQuart, timeZoneInfo);
var convertedLastQuart = TimeZoneInfo.ConvertTimeFromUtc(lastQuart, timeZoneInfo);
Log();
Log($"First quarter: {TimeZoneInfo.ConvertTimeFromUtc(firstQuart, timeZoneInfo)}", ConsoleColor.Green);
Log($"DST (DateTime.IsDaylightSavingTime): {convertedFirstQuart.IsDaylightSavingTime()}");
Log($"DST (TimeInfo.IsDaylightSavingTime(DateTime): {timeZoneInfo.IsDaylightSavingTime(convertedFirstQuart)}");
Log($"Ambiguous/Invalid: {timeZoneInfo.IsAmbiguousTime(convertedFirstQuart)}/{timeZoneInfo.IsInvalidTime(convertedFirstQuart)}");
Log();
Log($"Second quarter: {TimeZoneInfo.ConvertTimeFromUtc(secondQuart, timeZoneInfo)}", ConsoleColor.Green);
Log($"DST (DateTime.IsDaylightSavingTime): {convertedSecondQuart.IsDaylightSavingTime()}");
Log($"DST (TimeInfo.IsDaylightSavingTime(DateTime): {timeZoneInfo.IsDaylightSavingTime(convertedSecondQuart)}");
Log($"Ambiguous/Invalid: {timeZoneInfo.IsAmbiguousTime(convertedSecondQuart)}/{timeZoneInfo.IsInvalidTime(convertedSecondQuart)}");
Log();
Log($"Third quarter: {TimeZoneInfo.ConvertTimeFromUtc(thirdQuart, timeZoneInfo)}", ConsoleColor.Green);
Log($"DST (DateTime.IsDaylightSavingTime): {convertedThirdQuart.IsDaylightSavingTime()}");
Log($"DST (TimeInfo.IsDaylightSavingTime(DateTime): {timeZoneInfo.IsDaylightSavingTime(convertedThirdQuart)}");
Log($"Ambiguous/Invalid: {timeZoneInfo.IsAmbiguousTime(convertedThirdQuart)}/{timeZoneInfo.IsInvalidTime(convertedThirdQuart)}");
Log();
Log($"Last quarter: {TimeZoneInfo.ConvertTimeFromUtc(lastQuart, timeZoneInfo)}", ConsoleColor.Green);
Log($"DST (DateTime.IsDaylightSavingTime): {convertedLastQuart.IsDaylightSavingTime()}");
Log($"DST (TimeInfo.IsDaylightSavingTime(DateTime): {timeZoneInfo.IsDaylightSavingTime(convertedLastQuart)}");
Log($"Ambiguous/Invalid: {timeZoneInfo.IsAmbiguousTime(convertedLastQuart)}/{timeZoneInfo.IsInvalidTime(convertedLastQuart)}");
Log("==============================================================");
Log();
}
}
Console.ReadKey();
}
private static void Log(string message = "", ConsoleColor? color = null)
{
if(color.HasValue)
Console.ForegroundColor = color.Value;
Console.WriteLine(message);
Console.ForegroundColor = DefaultColor;
}
}
Given that my local TZ is GMT, and we use DST, the output is the following:
TimeZoneInfo.SupportsDaylightSavingTime(): Official Documentation
The following example retrieves a collection of all time zones that
are available on a local system and displays the names of those that
do not support daylight saving time.
var zones = TimeZoneInfo.GetSystemTimeZones();
foreach(TimeZoneInfo zone in zones)
{
if (! zone.SupportsDaylightSavingTime)
Console.WriteLine(zone.DisplayName);
}
TimezoneInfo.IsDaylightSavingTime(DateTime): Official Documentation
Indicates whether a specified date and time falls in the range of
daylight saving time for the time zone of the current TimeZoneInfo
object.
DateTime.IsDaylightSavingTime(): Official Documentation
Indicates whether this instance of DateTime is within the daylight
saving time range for the current time zone.
It is important to understand (I didn't at first), that the method IsDaylightSavingTime on an instance of DateTime, always returns the information requested taking in consideration the Local System Time Zone.
Analyse the output
Focusing on the Argentina Standard Time, we can see that the TimeZoneInfo.SupportsDaylightSavingTime, return true, this is an incorrect information because everywhere I searched for it, I found the opposite result.
Even tough the support for DST seems to be incorrect, converting a UTC DateTime to the ART TZ, using C#, always produces the correct result.
What makes me think that I still don't understand the whole picture here, is that the TimeInfo.IsDaylightSavingTime(DateTime) return false, which is what I would expect.
Permanent daylight saving time
According to wikipedia https://en.wikipedia.org/wiki/Daylight_saving_time
A move to "permanent daylight saving time" (staying on summer hours all year with no time shifts) is sometimes advocated and is currently implemented in some jurisdictions such as Argentina, Belarus,[78]Canada (e.g. Saskatchewan), Iceland, Kyrgyzstan, Malaysia, Morocco, Namibia, Singapore, Turkey, Turkmenistan and Uzbekistan.[164]It could be a result of following the timezone of a neighbouring region, political will, or other causes.
So to summarize, my open questions are:
Why the TimezoneInfo.SupportsDaylightSavingTime() return true but the TimeInfo.IsDaylightSavingTime(DateTime) return false?
How can I make sure that I have the latest DST/TZ updates from Microsoft, beside what I explained above?
Short Answer
TimeZoneInfo.SupportsDaylightSavingTime considers all time zone data available on the system, not just that of the current year. Both Argentina Standard Time and Altai Standard Time have periods where DST was in effect, within the periods of time that Windows is tracking for them.
Longer Answer
The documentation for TimeZoneInfo.SupportsDaylightSavingTime (which you linked to in your question already) explains:
Gets a value indicating whether the time zone has any daylight saving time rules.
What it's a little less clear about is that it is referring specifically to TimeZoneInfo.AdjustmentRule objects, as returned by the TimeZoneInfo.GetAdjustmentRules method, and that these are all rules on the system, not just the ones for the current year.
Microsoft policy states that Windows keeps track of all changes from 2010 forward.
However, some time zones (such as for Argentina) were already tracking changes before the policy was written so in some cases you will see earlier data.
In the Windows registry, you can find all of the time zone data that the system knows about at the following key:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones
Each of the TimeZoneInfo.Id values corresponds to a subkey under that one, and adjustment rules (if any) will be under Dynamic DST under that.
For Argentina Standard Time\Dynamic DST, we find data from 2006 through 2010.
Without even decoding the data, we can see that there were variations between the years. Looking on timeanddate.com here gives us the detail:
It appears that daylight saving time was in effect for the 2007-08 and 2008-09 summer seasons. (Argentina, being in the southern hemisphere, has its summers split across two years.)
Indeed, we can see this from .NET:
var tz = TimeZoneInfo.FindSystemTimeZoneById("Argentina Standard Time");
var dt = new DateTime(2008, 1, 1);
var dst = tz.IsDaylightSavingTime(dt); // True
Thus, it is appropriate for TimeZoneInfo.SupportsDaylightSavingTime to return True because there are indeed valid dates that were in DST for this time zone.
The same can be found for Altai. Windows is tracking data from 2010 forward, and DST existed in 2010 there.
Notice that it also has changes to its standard time in 2014 and again in 2016. This is another reason that an adjustment rule might exist. Unfortunately, Windows doesn't have a way to model this without using transitions that are marked as either "start of DST" or "end of DST". Thus, some part of these years will return True from IsDaylightSavingTime, even though neither side of the transition was considered to be daylight saving time. This is a known issue with time zones on Windows. It is a compromise that ensures that the correct UTC offset is used, even if the transition is for a change to standard time instead of for daylight saving time.
If you absolutely need to know if a transition was related to DST or not, then you can use IANA time zone data instead, via the Noda Time library. The ZoneInterval.Savings property will tell you that. However, that gets to your point about "permanent daylight saving time". Even in the IANA database, those are usually treated as changes to standard time, rather than true daylight saving time. So, you will probably find cases where someone might say "We've been on permanent DST for years", but the data disagrees.
Regarding your last question:
How can I make sure that I have the latest DST/TZ updates from Microsoft, beside what I explained above?
It is sufficient to simply ensure you are running Windows Update. All the DST/TZ updates listed on the community site are deployed with regular updates to all supported versions of Windows, regardless of what time zone you are in.

UTC Time different on servers in different timezones?

On my dev server I save a value called "ScheduledDateUtc" date to sqlserver e.g. 24-11-14 09:00:00. I also save a value called "UtcOffset" and calculate the "ScheduledDateLocal" like so:
var ScheduledDateLocal = ScheduledDateUtc.AddHours(UtcOffset); //a negative offset would be deducted and a positive offset would be added
On my dev server this works fine and its calculates the correct ScheduledDateLocal for alle timezones/UtcOffset's. However, when I deploy to a Azure server in a different timezone this calculation is a few hours off.
Can anyone explain why? I'm guessing there is some kind of setting or system specific conversion param? Thanks!
Is it possible that the incorrect result is actually on your machine and not Azure, and is because you are initialising ScheduledDateUtc as local time and not UTC?
Consider these two lines of code:
new DateTime(2015, 6, 1, 1, 1, 1).AddHours(5).ToUniversalTime().Dump();
new DateTime(2015, 6, 1, 1, 1, 1, DateTimeKind.Utc).AddHours(5).ToUniversalTime().Dump();
Here, we are in summer time which is UTC+1. The output of the above is:
01/06/2015 05:01:01
01/06/2015 06:01:01
They're 1 hour out as I 'forgot' to specify that the input is UTC in the constructor in the first line of code.
If you don't have access to the constructor because ScheduledDateUtc is initialised by an ORM, you can use SpecifyKind:
ScheduledDateUtc = DateTime.SpecifyKind(ScheduledDateUtc, DateTimeKind.Utc)
Its unfortunate that you're storing an offset not a timezone as you may have issues with daylight savings time. If you were storing a Windows timezone name, you could use TimeZoneInfo.ConvertTimeFromUtc (no need to specify Kind here as this assumes UTC) as per this example from MSDN:
TimeZoneInfo cstZone = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time");
ScheduledDateLocal = TimeZoneInfo.ConvertTimeFromUtc(ScheduledDateUtc, cstZone);
You could instead instantiate a "custom" timezone using your offset and still use the above function.
I faced a similar issue where I had to schedule events based on a user's local time. I ended up storing the day of the week, hour, minute, and Olson timezone string, and used Noda-Time to convert to a DateTimeOffset and from there to server time.

Creating daylight savings-aware DateTime when server set to UTC

My application is hosted on Windows Azure, which has all servers set to UTC.
I need to know when any given DateTime is subject to daylight savings time. For simplicity, we can assume that my users are all in the UK (so using Greenwich Mean Time).
The code I am using to convert my DateTime objects is
public static DateTime UtcToUserTimeZone(DateTime dateTime)
{
dateTime = DateTime.SpecifyKind(dateTime, DateTimeKind.Utc);
var timeZone = TimeZoneInfo.FindSystemTimeZoneById("Greenwich Standard Time");
var userDateTime = TimeZoneInfo.ConvertTime(dateTime, timeZone);
return DateTime.SpecifyKind(userDateTime, DateTimeKind.Local);
}
However, the following test fails at the last line; the converted DateTime does not know that it should be in daylight savings time.
[Test]
public void UtcToUserTimeZoneShouldWork()
{
var utcDateTime = new DateTime(2014, 6, 1, 12, 0, 0, DateTimeKind.Utc);
Assert.IsFalse(utcDateTime.IsDaylightSavingTime());
var dateTime = Utils.UtcToUserTimeZone(utcDateTime);
Assert.IsTrue(dateTime.IsDaylightSavingTime());
}
Note that this only fails when my Windows time zone is set to (UTC) Co-ordinated Universal Time. When it is set to (UTC) Dublin, Edinburgh, Lisbon, London (or any other northern-hemisphere time zone that observes daylight savings), the test passes. If you change this setting in Windows a restart of Visual Studio is required in order for the change to fully take effect.
What am I missing?
Your last line specifies that the value is in the local time zone of the system it's running in - but it's not really... it's converted using a different time zone.
Basically DateTime is somewhat broken, in my view - which makes it hard to use correctly. There's no way of saying "This DateTime value is in time zone x" - you can perform a conversion to find a specific local time, but that doesn't remember the time zone.
For example, from the docs of DateTime.IsDaylightSavingTime (emphasis mine):
This method determines whether the current DateTime value falls within the daylight saving time range of the local time zone, which is returned by the TimeZoneInfo.Local property.
Obviously I'm biased, but I'd recommend using my Noda Time project for this instead - quite possibly using a ZonedDateTime. You can call GetZoneInterval to obtain the ZoneInterval for that ZonedDateTime, and then use ZoneInterval.Savings to check whether the current offset contains a daylight savings component:
bool daylight = zonedDateTime.GetZoneInterval().Savings != Offset.Zero;
This is slightly long-winded... I've added a feature request to consider adding an IsDaylightSavingTime() method to ZonedDateTime as well...
The main thing you are missing is that "Greenwich Standard Time" is not the TimeZoneInfo id for London. That one actually belongs to "(UTC) Monrovia, Reykjavik".
You want "GMT Standard Time", which maps to "(UTC) Dublin, Edinburgh, Lisbon, London"
Yes, Windows time zones are weird. (At least you don't live in France, which gets strangely labeled as "Romance Standard Time"!)
Also, you should not be doing this part:
return DateTime.SpecifyKind(userDateTime, DateTimeKind.Local);
That will make various other code think it came from the local machine's time zone. Leave it with the "Unspecified" kind. (Or even better, use a DateTimeOffset instead of a DateTime)
Then you also need to use the .IsDaylightSavingsTime method on the TimeZoneInfo object, rather than the one on the .DateTime object. (There are two different methods, with the same name, on different objects, with differing behavior. Ouch!)
But I wholeheartedly agree that this is way too complicated and error prone. Just use Noda Time. You'll be glad you did!

Convert from local time to GMT (World Clock application using nodatime)

I am trying to program a world clock using Nodatime, and I have searched the web for samples on how to use the library and I have read the documentation, and it says that the class Instant is simply a number of "ticks" since some arbitrary epoch the Unix epoch, which corresponds to midnight on January 1st 1970 UTC. Well, I empirically guessed that if I used as an Instant a GMT value, then I could calculate with it the time values for each time zone (creating the world clock), and it worked. The problem that I have, is that I don't know a simple way to calculate the GMT time (or GMT instant) from the local time, my time zone is "America/Mexico_City", so my question is, is there a shortcut already defined in Nodatime to get the GMT time from a local time, or in the other hand, is there a simple way to implement the "Instant GetInstantGMT()" function (the function has to take in count the day light saving time issues)?
If you are just looking for the "current" instant, such represents now, then use:
Instant now = SystemClock.Instance.Now;
Calling it a "GMT instant" is redundant, since the Instant type is representing a universal moment in time without regard to time zone. It is (mostly) equivalent to UTC - which is essentially the same as GMT. In other words, you couldn't create an Instant that wasn't GMT.
Another way to think about an Instant is as if it were a DateTime whose .Kind property was permanently fixed to DateTimeKind.Utc and could not represent anything else.
Also, depending on exactly how your application is architected, you may find it useful to use the IClock interface instead:
IClock clock = SystemClock.Instance;
Instant now = clock.Now;
This would allow you to replace the system clock with a fake clock during unit testing.
Regarding how to go from a specific local time to an Instant, you would do something like this:
LocalDateTime ldt = new LocalDateTime(2013, 1, 1, 0, 0, 0);
DateTimeZone tz = DateTimeZoneProviders.Tzdb["America/Mexico_City"];
ZonedDateTime zdt = ldt.InZoneLeniently(tz);
Instant instant = zdt.ToInstant();
Note that I used InZoneLeniently in the conversion. This makes certain assumptions about how to translate from a local time that might be invalid or ambiguous due to daylight saving time. This might be acceptable, or you might instead prefer to use InZoneStrictly which will throw exceptions, or InZone which allows you to pass a resolver function so you can provide your own logic.

Storing date/times as UTC in database

I am storing date/times in the database as UTC and computing them inside my application back to local time based on the specific timezone. Say for example I have the following date/time:
01/04/2010 00:00
Say it is for a country e.g. UK which observes DST (Daylight Savings Time) and at this particular time we are in daylight savings. When I convert this date to UTC and store it in the database it is actually stored as:
31/03/2010 23:00
As the date would be adjusted -1 hours for DST. This works fine when your observing DST at time of submission. However, what happens when the clock is adjusted back? When I pull that date from the database and convert it to local time that particular datetime would be seen as 31/03/2010 23:00 when in reality it was processed as 01/04/2010 00:00.
Correct me if I am wrong but isn't this a bit of a flaw when storing times as UTC?
Example of Timezone conversion
Basically what I am doing is storing the date/times of when information is being submitted to my system in order to allow users to do a range report. Here is how I am storing the date/times:
public DateTime LocalDateTime(string timeZoneId)
{
var tzi = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tzi).ToUniversalTime().ToLocalTime();
}
Storing as UTC:
var localDateTime = LocalDateTime("AUS Eastern Standard Time");
WriteToDB(localDateTime.ToUniversalTime());
You don't adjust the date for DST changes based on whether you're currently observing them - you adjust it based on whether DST is observed at the instant you're describing. So in the case of January, you wouldn't apply the adjustment.
There is a problem, however - some local times are ambiguous. For example, 1:30am on October 31st 2010 in the UK can either represent UTC 01:30 or UTC 02:30, because the clocks go back from 2am to 1am. You can get from any instant represented in UTC to the local time which would be displayed at that instant, but the operation isn't reversible.
Likewise it's very possible for you to have a local time which never occurs - 1:30am on March 28th 2010 didn't happen in the UK, for example - because at 1am the clocks jumped forward to 2am.
The long and the short of it is that if you're trying to represent an instant in time, you can use UTC and get an unambiguous representation. If you're trying to represent a time in a particular time zone, you'll need the time zone itself (e.g. Europe/London) and either the UTC representation of the instant or the local date and time with the offset at that particular time (to disambiguate around DST transitions). Another alternative is to only store UTC and the offset from it; that allows you to tell the local time at that instant, but it means you can't predict what the local time would be a minute later, as you don't really know the time zone. (This is what DateTimeOffset stores, basically.)
We're hoping to make this reasonably easy to handle in Noda Time, but you'll still need to be aware of it as a possibility.
EDIT:
The code you've shown is incorrect. Here's why. I've changed the structure of the code to make it easier to see, but you'll see it's performing the same calls.
var tzi = TimeZoneInfo.FindSystemTimeZoneById("AUS Eastern Standard Time");
var aussieTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tzi);
var serverLocalTime = aussieTime.ToLocalTime();
var utcTime = serverLocalTime.ToUniversalTime();
So, let's think about right now - which is 13:38 in my local time (UTC+1, in London), 12:38 UTC, 22:39 in Sydney.
Your code will give:
aussieTime = 22:39 (correct)
serverLocalTime = 23:39 (*not* correct)
utcTime = 22:39 (*not* correct)
You should not call ToLocalTime on the result of TimeZoneInfo.ConvertTimeFromUtc - it will assume that it's being called on a UTC DateTime (unless it's actually got a kind of DateTimeKind.Local, which it won't in this case).
So if you're accurately saving 22:39 in this case, you aren't accurately saving the current time in UTC.
It's good that you are attempting to store the dates and times as UTC. It is generally best and easiest to think of UTC as the actual date and time and local times are just pseudonyms for that. And UTC is absolutely critical if you need to do any math on the date/time values to get timespans. I generally manipulate dates internally as UTC, and only convert to local time when displaying the value to the user (if it's necessary).
The bug that you are experiencing is that you are incorrectly assigning the local time zone to the date/time values. In January in the UK it is incorrect to interpret a local time as being in a Summertime time zone. You should use the time zone that was in effect at the time and location that the time value represents.
Translating the time back for display depends entirely on the requirements of the system. You could either display the times as the user's local time or as the source time for the data. But either way, Daylight Saving/Summertime adjustments should be applied appropriately for the target time zone and time.
You could work around this by also storing the particular offset used when converting to UTC. In your example, you'd store the date as something like
31/12/2009 23:00 +0100
When displaying this to the user, you can use that same offset to convert, or their current local offset, as you choose.
This approach also comes with its own problems. Time is a messy thing.
The TimeZoneInfo.ConvertTimeFromUtc() method will solve your problem:
using System;
class Program {
static void Main(string[] args) {
DateTime dt1 = new DateTime(2009, 12, 31, 23, 0, 0, DateTimeKind.Utc);
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time");
Console.WriteLine(TimeZoneInfo.ConvertTimeFromUtc(dt1, tz));
DateTime dt2 = new DateTime(2010, 4, 1, 23, 0, 0, DateTimeKind.Utc);
Console.WriteLine(TimeZoneInfo.ConvertTimeFromUtc(dt2, tz));
Console.ReadLine();
}
}
Output:
12/31/2009 11:00:00 PM
4/2/2010 12:00:00 AM
You'll need .NET 3.5 or better and run on an operating system that keeps historical daylight saving time changes (Vista, Win7 or Win2008).
Correct me if I am wrong but isn't
this a bit of a flaw when storing
times as UTC?
Yes it is. Also, days of the adjustment will have either 23 or 25 hours so the idiom of the prior day at the same time being local time - 24 hours is wrong 2 days a year.
The fix is picking one standard and sticking with it. Storing dates as UTC and displaying as local is pretty standard. Just don't use a shortcut of doing calculations local (+- somthing) = new time and you are OK.
This is a huge flaw but it isn't a flaw of storing times in UTC (because that is the only reasonable thing to do -- storing local times is always a disaster). This is a flaw is the concept of daylight savings time.
The real problem is that the time zone information changes. The DST rules are dynamic and historic. They time when DST starting in USA in 2010 is not the same when it started in 2000. Until recently Windows did not even contain this historic data, so it was essentially impossible to do things correctly. You had to use the tz database to get it right. Now I just googled it and it appears that .NET 3.5 and Vista (I assume Windows 2008 too) has done some improvement and the System.TimeZoneInfo actually handles historic data. Take a look at this.
But basically DST must go.

Categories