11

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?

Community
  • 1
  • 1
DrCopyPaste
  • 4,023
  • 1
  • 22
  • 57
  • What are `timeToSet` and `uniTime`'s `Kind`? – CodeCaster Aug 25 '14 at 15:53
  • @CodeCaster `uniTime` is of kind `Utc`, `timeToSet` is `Unspecified` so just the default value – DrCopyPaste Aug 25 '14 at 15:56
  • 1
    Have you tried `SetLocalTime` instead: [Strange behaviour in SetSystemTime kernel32](http://stackoverflow.com/a/25184149/1115360)? – Andrew Morton Aug 25 '14 at 15:56
  • @AndrewMorton that's a nice pointer, I just fiddled around with this a bit, but I got some strange behavior there as well (some executions result in showing `00:59` some in `01:59` [which is an improvment I guess^^], but the dst transition still does not occur it just goes from `01:59` to `02:00`) – DrCopyPaste Aug 25 '14 at 16:14
  • @AndrewMorton well, dunno why I didn't get it to work that way yesterday, it was sooooooo simple after a refreshing sleep :D, ty – DrCopyPaste Aug 26 '14 at 09:48

3 Answers3

5

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.

Peter O.
  • 32,158
  • 14
  • 82
  • 96
Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • Thanks for your answer, very interesting! Do I understand you correctly that for my example 3 it would also have worked if I changed time to `one hour before transition out of daylight saving` (or one hour and one second?) I would not see this weirdness either? I'm not able to try this out right now, will comment further in a few hours. – DrCopyPaste Aug 27 '14 at 05:47
  • Example 3 is just a reflection of how DST works and that .NET chooses the later instance when ambiguous. ([The chart here](http://stackoverflow.com/tags/dst/info) may help clarify). Example 2 is a different problem - one that some might classify as a bug. The one-hour-before-transition thing is specific to example 2. – Matt Johnson-Pint Aug 27 '14 at 06:04
  • Just tried your code for `SetSystemDateTimeSafely` using `SetSystemDateTime` (replaced my methods from [this code](http://stackoverflow.com/a/25502741/2186023) and replaced calls to `SetLocalSytemDateTime` with calls to `SetSystemDateTimeSafely`); that works fine for example #1 and #2, but example #3 still gets the same output as in my original question, besides that I'm not sure if I like having that "magic number" in `AddHours` I think I might prefer to just to call `SetLocalTime` twice. – DrCopyPaste Aug 27 '14 at 09:32
  • `SetSystemDateTimeSafely` only addresses the bug you described in #2. In #3, your code may not be switching to the value you expect, but it is indeed a valid value. There are two different instances of 2:59:59 on that day. If you set it to the first instance, then the next second would be 2:00:00. If you set it to the second instance, then the next second is 3:00:00. – Matt Johnson-Pint Aug 27 '14 at 23:36
  • If you call `SetLocalTime` twice, it's choosing the *first* instance. That is why you get different behavior from .NET, which chooses the *second* instance. – Matt Johnson-Pint Aug 27 '14 at 23:37
  • I updated the code to give you control over which instance you would like in ambiguous cases. – Matt Johnson-Pint Aug 28 '14 at 00:05
  • Yup, works for all three test cases now as intended, but now I am in a dilemma shall I accept your answer, because you provided more explanation as I did in [my post](http://stackoverflow.com/a/25502741/2186023)? I slightly tend towards accepting my own post, because I don't like the manuall adding/subtracting hours and would prefer calling `SetLocalTime` twice with link to the [msdn post](http://msdn.microsoft.com/en-us/library/windows/desktop/ms724936%28v=vs.85%29.aspx). – DrCopyPaste Aug 29 '14 at 12:41
  • 1
    Doesn't matter either way to me. I hope the information I provided was informative to you and helpful to other future readers. – Matt Johnson-Pint Aug 29 '14 at 16:51
2

This is just a guess, but MSDN docs on SetSystemTime (the underlying function you're calling) say that it works in UTC which by definition does not have any concept of daylight savings time. I assume that windows is just "doing what you tell it" and the fact that the time is "illegal" (in terms of how we express local times) doesn't really come into play.

SetSystemTime function

Using SetLocalTime might do what you want, although the docs on that state that it uses "current timezone information" (presumably user, not system) to determine daylight savings, which may also not be what you want for a reproducible test.

MarcE
  • 3,586
  • 1
  • 23
  • 27
1

What Andrew Morton and Marc proposed was spot on!

Though I must say I still don't understand why I would not be able to achieve the same thing using SetSystemTime (Doing conversions to universial time of course), it really DOES work using SetLocalTime.

Please upvote Marc's post as well, I am just writing this so there is a complete code example to demonstrate how the tests would look if they run successfully.

This code runs 3 tests:

  1. setting system time to an arbitrary time (not near daylight saving time transition), wait 5 seconds and then set the system time back to the correct time and wait 5 seconds again.
  2. setting system time to one second before transition to daylight saving time, wait 5 seconds and set the system time back to the correct time and wait 5 seconds again
  3. setting system time to one second before transition out of daylight saving time, wait 5 seconds and set the system time back to the correct time and wait 5 seconds again

(Posting a complete working example, but do note to reproduce this on your system you might have to use different DateTime-Values, due to the daylight saving time transition in your time zone [if you're not working in the timezone of Berlin], and also you might have to [or just like to] use another NTP-server in GetNetworkTime())

// complete example use this as Program.cs in a console application project
namespace SystemDateManipulator101
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Net;
    using System.Net.Sockets;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Threading;
    using System.Threading.Tasks;

    /// <summary>
    /// Program class.
    /// </summary>
    public class Program
    {
        #region Methods

        static void Main(string[] args)
        {
            // test one: set system time to a random time that is not near daylight savings time transition
            DateTime timeToSet = new DateTime(2014, 5, 5, 4, 59, 59, 0);
            Console.WriteLine("timeToSet Kind: {0}", timeToSet.Kind);
            Console.WriteLine("Attemting to set time to {0}", timeToSet);
            SetLocalSytemDateTime(timeToSet);
            Console.WriteLine("Now time is {0}, which is {1} (UTC)", DateTime.Now, DateTime.UtcNow);
            Thread.Sleep(TimeSpan.FromSeconds(5));
            DateTime actualSystemTime = GetNetworkTime();
            SetLocalSytemDateTime(actualSystemTime);

            Thread.Sleep(TimeSpan.FromSeconds(5));

            // test two: set system time to one second before transition to daylight savings time in Berlin
            timeToSet = new DateTime(2015, 3, 29, 1, 59, 59, 0);
            Console.WriteLine("timeToSet Kind: {0}", timeToSet.Kind);
            Console.WriteLine("Attemting to set time to {0}", timeToSet);
            SetLocalSytemDateTime(timeToSet);
            Console.WriteLine("Now time is {0}, which is {1} (UTC)", DateTime.Now, DateTime.UtcNow);
            Thread.Sleep(TimeSpan.FromSeconds(5));
            actualSystemTime = GetNetworkTime();
            SetLocalSytemDateTime(actualSystemTime);

            Thread.Sleep(TimeSpan.FromSeconds(5));

            // test three: set system time to one second before transition out of daylight savings time in Berlin
            timeToSet = new DateTime(2014, 10, 26, 2, 59, 59, 0);
            Console.WriteLine("timeToSet Kind: {0}", timeToSet.Kind);
            Console.WriteLine("Attemting to set time to {0}", timeToSet);
            SetLocalSytemDateTime(timeToSet);
            Console.WriteLine("Now time is {0}, which is {1} (UTC)", DateTime.Now, DateTime.UtcNow);
            Thread.Sleep(TimeSpan.FromSeconds(5));
            actualSystemTime = GetNetworkTime();
            SetLocalSytemDateTime(actualSystemTime);

            Console.Read();
        }

        #endregion

        // https://stackoverflow.com/a/12150289/162671
        public static DateTime GetNetworkTime()
        {
            //default Windows time server
            const string ntpServer = "time.windows.com";

            // NTP message size - 16 bytes of the digest (RFC 2030)
            var ntpData = new byte[48];

            //Setting the Leap Indicator, Version Number and Mode values
            ntpData[0] = 0x1B; //LI = 0 (no warning), VN = 3 (IPv4 only), Mode = 3 (Client Mode)

            var addresses = Dns.GetHostEntry(ntpServer).AddressList;

            //The UDP port number assigned to NTP is 123
            var ipEndPoint = new IPEndPoint(addresses[0], 123);
            //NTP uses UDP
            var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);

            socket.Connect(ipEndPoint);

            //Stops code hang if NTP is blocked
            socket.ReceiveTimeout = 3000;

            socket.Send(ntpData);
            socket.Receive(ntpData);
            socket.Close();

            //Offset to get to the "Transmit Timestamp" field (time at which the reply 
            //departed the server for the client, in 64-bit timestamp format."
            const byte serverReplyTime = 40;

            //Get the seconds part
            ulong intPart = BitConverter.ToUInt32(ntpData, serverReplyTime);

            //Get the seconds fraction
            ulong fractPart = BitConverter.ToUInt32(ntpData, serverReplyTime + 4);

            //Convert From big-endian to little-endian
            intPart = SwapEndianness(intPart);
            fractPart = SwapEndianness(fractPart);

            var milliseconds = (intPart * 1000) + ((fractPart * 1000) / 0x100000000L);

            //**UTC** time
            var networkDateTime = (new DateTime(1900, 1, 1, 0, 0, 0, DateTimeKind.Utc)).AddMilliseconds((long)milliseconds);

            return networkDateTime.ToLocalTime();
        }

        // stackoverflow.com/a/3294698/162671
        static uint SwapEndianness(ulong x)
        {
            return (uint)(((x & 0x000000ff) << 24) +
                           ((x & 0x0000ff00) << 8) +
                           ((x & 0x00ff0000) >> 8) +
                           ((x & 0xff000000) >> 24));
        }

        [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);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool SetLocalTime(ref SYSTEMTIME st);

        public static void SetSystemDateTime(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);
        }

        public static void SetLocalSytemDateTime(DateTime timeToSet)
        {
            SYSTEMTIME setTime = new SYSTEMTIME()
            {
                wYear = (short)timeToSet.Year,
                wMonth = (short)timeToSet.Month,
                wDay = (short)timeToSet.Day,
                wHour = (short)timeToSet.Hour,
                wMinute = (short)timeToSet.Minute,
                wSecond = (short)timeToSet.Second,
                wMilliseconds = (short)timeToSet.Millisecond
            };

            SetLocalTime(ref setTime);
            // yes this second call is really necessary, because the system uses the daylight saving time setting of the current time, not the new time you are setting
            // http://msdn.microsoft.com/en-us/library/windows/desktop/ms724936%28v=vs.85%29.aspx
            SetLocalTime(ref setTime);
        }
    }
}

If you want to experience the weirdness I described in my question, you still can, just replace the calls to SetLocalSytemDateTime by SetSytemDateTime.

Community
  • 1
  • 1
DrCopyPaste
  • 4,023
  • 1
  • 22
  • 57