My times, they are changing, that is, because I need them to. I am testing some cases involving a scheduler I use and this involves behavior around transitions to and from daylight saving time.
From this post I got a working method that enables me to change the system date programmatically (reposting most of the code):
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEMTIME
{
public short wYear;
public short wMonth;
public short wDayOfWeek;
public short wDay;
public short wHour;
public short wMinute;
public short wSecond;
public short wMilliseconds;
}
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool SetSystemTime(ref SYSTEMTIME st);
and for my own convenience I am just wrapping that in this function that I actually call:
public static void SetSytemDateTime(DateTime timeToSet)
{
DateTime uniTime = timeToSet.ToUniversalTime();
SYSTEMTIME setTime = new SYSTEMTIME()
{
wYear = (short)uniTime.Year,
wMonth = (short)uniTime.Month,
wDay = (short)uniTime.Day,
wHour = (short)uniTime.Hour,
wMinute = (short)uniTime.Minute,
wSecond = (short)uniTime.Second,
wMilliseconds = (short)uniTime.Millisecond
};
SetSystemTime(ref setTime);
}
The additional conversion to Universal Time is necessary, otherwise I don't get to see the date I passed to the method in my clock (down in the task bar).
Now this works fine considering this code for example:
DateTime timeToSet = new DateTime(2014, 3, 10, 1, 59, 59, 0);
Console.WriteLine("Attemting to set time to {0}", timeToSet);
SetSytemDateTime(timeToSet);
Console.WriteLine("Now time is {0}, which is {1} (UTC)", DateTime.Now, DateTime.UtcNow);
Thread.Sleep(TimeSpan.FromSeconds(5));
DateTime actualSystemTime = GetNetworkTime();
SetSytemDateTime(actualSystemTime);
The method GetNetworkTime
is actually just grabbed from over here, so I can set my clock back to the "real" time after testing, you can ignore it for this question's sake.
That does, what you'd expect (German DateTime formatting, don't get confused):
And in the task bar I also see what I expect:
But now to the weird part: Switch the first line of the calling code for
// one second before transition to daylight saving time in Berlin
DateTime timeToSet = new DateTime(2015, 3, 29, 1, 59, 59, 0);
Now the command line output actually seems to satisfy what we'd expect to see:
But then we take a look down to the right of our task bar and enter frowny land and see a time that should actually not exist for that day:
Now, the funny thing is, when I try the same thing for the second before the transition out of daylight saving time, the change gets "accepted" (switching first calling code line again):
// one second before transition out of daylight saving time in Berlin
DateTime timeToSet = new DateTime(2014, 10, 26, 2, 59, 59, 0);
We see what we'd expect in the command line output:
also in the task bar clock:
But this story also has a sad ending, let one second pass and you would expect the clock to show 2 'o clock, but instead:
Which is a time that should actually occur one hour later on that particular day (if you switch the time manually in windows this transitions as expected).
Now, what am I missing here, why can't I target the second before transition to daylight saving time and why don't I see the transition out of daylight saving time when I do the DateTime-changes programmatically this way?
What do I need to add/set so I can?
I can explain your example #3.
On October 26th 2014 in Germany, as the clock approaches 3:00 AM the hour is reset to 2:00 AM, repeating the values from 2:00:00 to 2:59:59 twice. This is known as a "fall-back" transition.
When you call ToUniversalTime
on a local date time that is in this transition, it is ambiguous. .Net will assume that you meant the original value to be in the standard time - not the daylight time.
In other words, the time 2:59:59 exists twice, and .Net assumes the second one.
Therefore, one second later is indeed 3:00:00.
If you want control over this, you would use the DateTimeOffset
type instead of the DateTime
type - where you can specify the offset explicitly. You can also test for this condition with TimeZoneInfo.IsAmbiguousTime
.
Regarding your example #2, it would appear that SetSystemTime
has the same issue that is described for SetLocalTime
in the MSDN. When you set the system time, you are correctly setting the time by UTC, but for display it is using the current settings to convert to the local time zone.
Specifically, the ActiveTimeBias
setting in the registry is used to do the UTC-to-local conversion. More in this article.
From experimentation, it would appear that if the time is more than an hour away from the DST transition, then it also triggers an update to ActiveTimeBias
and all is good.
So to recap, you'll get this behavior only if all of the following are true:
You're setting a time that is in standard time.
Your current local time is in daylight time.
You're setting a time that is no more than one hour before the spring-forward DST transition.
With that in mind, I've written this code that should work around both issues:
public static void SetSystemDateTimeSafely(DateTime timeToSet,
bool withEarlierWhenAmbiguous = true)
{
TimeZoneInfo timeZone = TimeZoneInfo.Local;
bool isAmbiguous = timeZone.IsAmbiguousTime(timeToSet);
DateTime utcTimeToSet = timeToSet.ToUniversalTime();
if (isAmbiguous && withEarlierWhenAmbiguous)
utcTimeToSet = utcTimeToSet.AddHours(-1);
TimeSpan offset = timeZone.GetUtcOffset(utcTimeToSet);
TimeSpan offsetOneHourLater = timeZone.GetUtcOffset(utcTimeToSet.AddHours(1));
if (offset != offsetOneHourLater)
{
TimeSpan currentOffset = timeZone.GetUtcOffset(DateTime.UtcNow);
if (offset != currentOffset)
{
SetSystemDateTime(utcTimeToSet.AddHours(-1));
}
}
SetSystemDateTime(utcTimeToSet);
}
private static void SetSystemDateTime(DateTime utcDateTime)
{
if (utcDateTime.Kind != DateTimeKind.Utc)
{
throw new ArgumentException();
}
SYSTEMTIME st = new SYSTEMTIME
{
wYear = (short)utcDateTime.Year,
wMonth = (short)utcDateTime.Month,
wDay = (short)utcDateTime.Day,
wHour = (short)utcDateTime.Hour,
wMinute = (short)utcDateTime.Minute,
wSecond = (short)utcDateTime.Second,
wMilliseconds = (short)utcDateTime.Millisecond
};
SetSystemTime(ref st);
}
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEMTIME
{
public short wYear;
public short wMonth;
public short wDayOfWeek;
public short wDay;
public short wHour;
public short wMinute;
public short wSecond;
public short wMilliseconds;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool SetSystemTime(ref SYSTEMTIME st);
You can now call SetSystemDateTimeSafely
with any date you like and it will compensate for this odd behavior.
This works by first setting a value that is before the problematic range, but only when needed. Then it proceeds to set the correct value immediately after.
The only downside I can think of is that it will raise two WM_TIMECHANGE
messages, which may be confusing when read in the system event logs.
If you leave the withEarlierWhenAmbiguous
parameter at it's default true
, it will have the behavior of choosing the first instance that you were expecting from your example #3. If you set it to false, it will have .NET's default behavior of choosing the second instance.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With