Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't I set my system time to a time near daylight saving transition

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.

The Code

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.

Example output #1

That does, what you'd expect (German DateTime formatting, don't get confused): cmdli output attemting to change system time 1

And in the task bar I also see what I expect:

taskbar clock showing time 1

Example output #2 (Transitioning to daylight saving time)

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: cmdli output attemting to change system time 2

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:

taskbar clock showing time 2

Example output #3 (Transitioning out of daylight saving time)

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:

cmdli output attemting to change system time 3

also in the task bar clock:

taskbar clock showing time 3

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:

taskbar clock showing time 4

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).

The Question

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?

like image 565
DrCopyPaste Avatar asked Aug 25 '14 15:08

DrCopyPaste


1 Answers

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.

like image 136
Matt Johnson-Pint Avatar answered Sep 19 '22 12:09

Matt Johnson-Pint