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

时间:2014-08-25 15:36:20

标签: c# .net datetime timezone dst

我的时代,他们正在改变,也就是说,因为我需要他们。 我正在测试一些涉及我使用的调度程序的案例,这涉及到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格式化,不要混淆): cmdli output attemting to change system time 1

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

taskbar clock showing time 1

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

但现在到了奇怪的部分: 切换

的调用代码的第一行
// one second before transition to daylight saving time in Berlin
DateTime timeToSet = new DateTime(2015, 3, 29, 1, 59, 59, 0);

现在命令行输出实际上似乎满足了我们期望看到的内容: cmdli output attemting to change system time 2

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

taskbar clock showing time 2

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

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

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

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

cmdli output attemting to change system time 3

也在任务栏时钟中:

taskbar clock showing time 3

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

taskbar clock showing time 4

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

问题

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

我需要添加/设置什么?

3 个答案:

答案 0 :(得分:5)

我可以解释你的例子#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选择第二个实例的默认行为。

答案 1 :(得分:2)

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

SetSystemTime function

使用SetLocalTime可能会做你想要的,尽管那个文档说它使用了"当前时区信息" (可能是用户,而不是系统)来确定夏令时,这可能也不是您想要的可重复测试。

答案 2 :(得分:1)

提出了Andrew MortonMarc提出的内容!

虽然我必须说我仍然不明白为什么我无法使用SetSystemTime来实现同样的目标(当然,转换为普遍的时间),它确实可以使用{{3 }}

请同意upvote Marc的帖子,我只是写这个,所以有一个完整的代码示例来演示如果测试成功运行将如何看待。

此代码运行3个测试:

  1. 将系统时间设置为任意时间(不接近夏令时转换),等待5秒钟,然后将系统时间设置回正确的时间并再次等待5秒。
  2. 在转换为夏令时之前将系统时间设置为1秒,等待5秒并将系统时间设置回正确的时间并再次等待5秒
  3. 在转换为夏令时之前将系统时间设置为1秒,等待5秒并将系统时间设置回正确的时间并再次等待5秒
  4. (发布一个完整的工作示例,但请注意在您的系统上重现这一点,由于SetLocalTime [如果您不在,我可能需要使用不同的DateTime值柏林的时区],你可能还需要[或只是想]在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://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);
            }
        }
    }
    

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