Search code examples
c#.netdatetimetimezonedst

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?


Solution

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