为什么我不能将系统时间设置为接近夏令时转换的时间

DrC*_*ste 11 .net c# timezone datetime dst

我的时代,他们正在改变,也就是说,因为我需要他们.我正在测试一些涉及我使用的调度程序的情况,这涉及到夏令时之间转换的行为.

代码

这篇文章我得到了一个工作方法,使我能够以编程方式更改系统日期(重新发布大部分代码):

[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);
Run Code Online (Sandbox Code Playgroud)

为了我自己的方便,我只是在我实际调用的函数中包装它:

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);
}
Run Code Online (Sandbox Code Playgroud)

额外转换为通用时间是必要的,否则我无法看到我在时钟中传递给方法的日期(在任务栏中向下).

现在这个工作正常,考虑到这个代码,例如:

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);
Run Code Online (Sandbox Code Playgroud)

这个方法GetNetworkTime实际上只是从这里抓取来的,所以我可以在测试后将我的时钟设置回"真实"时间,你可以为了这个问题忽略它.

示例输出#1

那就是你所期望的(德语DateTime格式化,不要混淆): cmdli输出以改变系统时间1

在任务栏中我也看到了我的期望:

任务栏时钟显示时间1

示例输出#2(转换为夏令时)

但现在到了奇怪的部分:切换调用代码的第一行

// one second before transition to daylight saving time in Berlin
DateTime timeToSet = new DateTime(2015, 3, 29, 1, 59, 59, 0);
Run Code Online (Sandbox Code Playgroud)

现在,命令行输出实际上似乎满足了我们期望看到的内容: cmdli输出,以改变系统时间2

但是接着我们看看我们的任务栏的右边并进入皱眉的土地并看到当天实际上不存在的时间:

任务栏时钟显示时间2

示例输出#3(转换为夏令时)

现在,有趣的是,当我在夏令时转换之前第二次尝试相同的事情时,更改将被"接受"(再次切换第一个调用代码行):

// one second before transition out of daylight saving time in Berlin
DateTime timeToSet = new DateTime(2014, 10, 26, 2, 59, 59, 0);
Run Code Online (Sandbox Code Playgroud)

我们在命令行输出中看到了我们所期望的:

cmdli输出以改变系统时间3

也在任务栏时钟:

任务栏时钟显示时间3

但这个故事也有一个悲伤的结局,让一秒钟通过,你会期望时钟显示2'时钟,但相反:

任务栏时钟显示时间4

这个时间应该在该特定日期后一小时实际发生(如果您在Windows中手动切换时间,则按预期进行转换).

问题

现在,我在这里错过了什么,为什么我不能在转换到夏令时之前定位第二个,为什么当我以这种方式以编程方式进行DateTime更改时,我看不到夏令时的转换?

我需要添加/设置什么?

Mat*_*int 5

我可以解释你的例子#3.

  • 2014年10月26日在德国,当时钟接近凌晨3:00时,小时重置为凌晨2:00,重复两次从2:00:00到2:59:59的值.这被称为"后退"过渡.

  • 当您调用ToUniversalTime此转换中的本地日期时间时,它是不明确的..NET可以假设你的意思是原始值是在标准时间-而不是夏令时.

  • 换句话说,时间2:59:59存在两次,而.Net则是第二次.

  • 因此,一秒钟后确实是3点00分.

如果要控制它,可以使用DateTimeOffset类型而不是DateTime类型 - 您可以在其中明确指定偏移量.你也可以测试这个条件TimeZoneInfo.IsAmbiguousTime.

关于您的示例#2,它似乎与MSDN中SetSystemTime描述的问题相同.设置系统时间时,您正确地按UTC设置时间,但是为了显示,它使用当前设置转换为本地时区.SetLocalTime

具体而言,ActiveTimeBias注册表中的设置用于执行UTC到本地的转换.更多内容来自本文.

从实验中可以看出,如果距离DST过渡时间超过一个小时,那么它也会触发更新ActiveTimeBias,一切都很好.

所以回顾一下,只有满足以下所有条件时才会出现这种情况:

  • 您正在设置标准时间内的时间.

  • 您当前的当地时间是在白天.

  • 您设置的时间不超过春季DST过渡的一小时.

考虑到这一点,我编写了这个应该解决这两个问题的代码:

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);
Run Code Online (Sandbox Code Playgroud)

您现在可以SetSystemDateTimeSafely使用您喜欢的任何日期进行呼叫,它将弥补这种奇怪的行为.

这通过首先设置有问题范围之前的值来工作,但仅在需要时.然后它继续在之后立即设置正确的值.

我能想到的唯一缺点是它会引发两条WM_TIMECHANGE消息,这在系统事件日志中读取时可能会引起混淆.

如果将withEarlierWhenAmbiguous参数保留为默认值true,则它将具有从示例#3 中选择您期望的第一个实例的行为.如果将其设置为false,则将具有.NET选择第二个实例的默认行为.


DrC*_*ste 2

安德鲁·莫顿马克的提议是正确的!

虽然我必须说我仍然不明白为什么我无法使用SetSystemTime实现同样的事情(当然是转换为通用时间),但使用SetLocalTime确实可以工作。

也请支持 Marc 的帖子,我只是在写这篇文章,所以有一个完整的代码示例来演示如果测试成功运行,测试会是什么样子。

此代码运行 3 个测试:

  1. 将系统时间设置为任意时间(不在夏令时过渡附近),等待 5 秒,然后将系统时间设置回正确时间,然后再次等待 5 秒。
  2. 在转换为夏令时之前将系统时间设置为一秒,等待 5 秒,然后将系统时间设置回正确时间,然后再次等待 5 秒
  3. 在退出夏令时之前将系统时间设置为一秒,等待 5 秒,然后将系统时间设置回正确时间,然后再次等待 5 秒

(发布一个完整的工作示例,但请注意要在您的系统上重现此示例,由于您所在时区的夏令时转换,您可能必须使用不同的日期时间值[如果您不在柏林时区工作] ,而且您可能必须[或只是想]使用另一个 NTP 服务器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

        // /sf/answers/850520261/
        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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

如果您想体验我在问题中描述的奇怪之处,您仍然可以,只需将调用替换SetLocalSytemDateTimeSetSytemDateTime.