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

Posted

技术标签:

【中文标题】为啥我不能将我的系统时间设置为接近夏令时转换的时间【英文标题】:Why can't I set my system time to a time near daylight saving transition为什么我不能将我的系统时间设置为接近夏令时转换的时间 【发布时间】:2014-10-18 19:31:19 【问题描述】:

我的时代,他们正在改变,也就是说,因为我需要他们改变。 我正在测试一些涉及我使用的调度程序的案例,这涉及到与daylight saving time 之间的转换行为。

代码

来自this post,我得到了一种工作方法,使我能够以编程方式更改系统日期(重新发布大部分代码):

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

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

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

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

现在考虑到这个代码,这可以正常工作:

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

GetNetworkTime这个方法实际上是从over here中抓取的,所以我可以在测试后将我的时钟设置回“实时”时间,为了这个问题你可以忽略它。

示例输出 #1

确实如此,正如您所期望的那样(德语 DateTime 格式,不要混淆):

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

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

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

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

现在命令行输出实际上似乎满足了我们的期望:

然后我们往下看任务栏的右侧,进入皱眉的土地,看到了一个实际上不应该存在于那一天的时间:

示例输出 #3(退出夏令时)

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

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

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

也在任务栏时钟中:

但这个故事也有一个悲伤的结局,等一秒钟过去了,你会期望时钟显示 2 点钟,但是:

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

问题

现在,我在这里缺少什么,为什么我不能在转换为夏令时之前的第二个为目标,为什么当我以这种方式以编程方式执行 DateTime 更改时,我看不到夏令时的转换?

我需要添加/设置什么?

【问题讨论】:

timeToSetuniTimeKind 是什么? @CodeCaster uniTime 类似于 UtctimeToSetUnspecified 所以只是默认值 你试过SetLocalTime 代替:Strange behaviour in SetSystemTime kernel32? @AndrewMorton 这是一个很好的指针,我只是摆弄了一下,但我也有一些奇怪的行为(一些处决导致在00:59 中显示一些01:59 [这是我猜是一种改进^^],但 dst 转换仍然没有发生,它只是从 01:5902:00) @AndrewMorton 好吧,不知道为什么我昨天没有让它那样工作,在清爽的睡眠之后它是如此简单:D,ty 【参考方案1】:

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

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

当您在处于此转换中的本地日期时间调用 ToUniversalTime 时,它是不明确的。 .Net 会假定您的意思是原始值是 标准 时间,而不是夏令时。

换句话说,时间 2:59:59 存在两次,.Net 假定 一次。

因此,一秒后确实是 3:00:00。

如果您想对此进行控制,您可以使用 DateTimeOffset 类型而不是 DateTime 类型 - 您可以在其中显式指定偏移量。您也可以使用TimeZoneInfo.IsAmbiguousTime 测试这种情况。

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

具体来说,注册表中的ActiveTimeBias 设置用于进行UTC 到本地的转换。 More in this article.

从实验来看,如果时间距离 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);

您现在可以使用您喜欢的任何日期致电SetSystemDateTimeSafely,它会弥补这种奇怪的行为。

首先设置一个在问题范围之前的值,但仅在需要时才有效。然后它会立即设置正确的值。

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

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

【讨论】:

感谢您的回答,非常有趣!我是否理解正确,对于我的示例 3,如果我将时间更改为 one hour before transition out of daylight saving(或一小时零一秒?)我也不会看到这种奇怪现象?我现在无法尝试这个,几个小时后会进一步评论。 示例 3 只是反映了 DST 的工作方式,并且 .NET 在模棱两可时选择了后面的实例。 (The chart here 可能有助于澄清)。示例 2 是一个不同的问题 - 有些人可能会将其归类为错误。过渡前一小时的事情特定于示例 2。 刚刚使用SetSystemDateTime 尝试了SetSystemDateTimeSafely 的代码(从this code 替换了我的方法,并将对SetLocalSytemDateTime 的调用替换为对SetSystemDateTimeSafely 的调用);例如#1和#2效果很好,但示例#3仍然得到与我原来的问题相同的输出,除了我不确定我是否喜欢在AddHours中拥有那个“幻数”我想我可能宁愿只打电话给SetLocalTime 两次。 SetSystemDateTimeSafely 仅解决您在 #2 中描述的错误。在 #3 中,您的代码可能不会切换到您期望的值,但它确实是一个有效值。那天有两个不同的 2:59:59 实例。如果将其设置为第一个实例,则下一秒将是 2:00:00。如果将其设置为第二个实例,则下一秒为 3:00:00。 对我来说都无所谓。我希望我提供的信息对您有所帮助,并对其他未来的读者有所帮助。【参考方案2】:

这只是一个猜测,但 SetSystemTime 上的 MSDN 文档(您正在调用的底层函数)说它在 UTC 中工作,根据定义,它没有任何夏令时概念。我认为 windows 只是“按你说的做”,而时间是“非法的”(就我们如何表达当地时间而言)这一事实并没有真正发挥作用。

SetSystemTime function

使用SetLocalTime 可能会做您想做的事,尽管该文档指出它使用“当前时区信息”(可能是用户,而不是系统)来确定夏令时,这也可能不是您想要的可重现的测试。

【讨论】:

【参考方案3】:

Andrew Morton 和 Marc 提出的建议是正确的!

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

也请点赞 Marc 的帖子,我只是在写这篇文章,所以有一个完整的代码示例来演示测试成功运行后的外观。

此代码运行 3 个测试:

    将系统时间设置为任意时间(不接近夏令时转换),等待 5 秒,然后将系统时间设置回正确时间并再次等待 5 秒。 在转换为夏令时之前将系统时间设置为一秒,等待 5 秒,然后将系统时间设置回正确的时间,然后再次等待 5 秒 在退出夏令时之前将系统时间设置为一秒,等待 5 秒,然后将系统时间设置回正确的时间,然后再次等待 5 秒

(发布一个完整的工作示例,但请注意要在您的系统上重现此内容,您可能必须使用不同的 DateTime-Values,因为 daylight saving time transition in your time zone [如果您不在柏林的时区工作],而且您可能必须 [或只是想] 在 GetNetworkTime() 中使用另一个 NTP 服务器)

// 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://***.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();
        

        // ***.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);
        
    

如果您想体验我在问题中描述的怪异,您仍然可以,只需将对 SetLocalSytemDateTime 的调用替换为 SetSytemDateTime

【讨论】:

以上是关于为啥我不能将我的系统时间设置为接近夏令时转换的时间的主要内容,如果未能解决你的问题,请参考以下文章

为啥 PDO 将我的 bool(false) 参数转换为 string('')?

将我的 FBX 文件转换为 .gltf 后,模型非常小,为啥?

为啥 Pandas 将我的 numpy float32 强制转换为 float64?

为啥 `fetch` 将我的 JSON 字符串转换为查询字符串?副作用? [复制]

为啥 QProcess 将我的参数中的“=”转换为空格

为啥扩展语法会将我的字符串转换为数组?