如何从 UTC 获取日期时间的偏移量?

How to get a datetime's offset from UTC?

给定 “本地” 的日期时间(即没有时区信息),例如:

| Datetime            |
|---------------------|
| 2019-01-21 09:00:00 |
| 2019-02-21 09:00:00 |
| 2019-03-21 09:00:00 |
| 2019-04-21 09:00:00 |
| 2019-05-21 09:00:00 |
| 2019-06-21 09:00:00 |
| 2019-07-21 09:00:00 |
| 2019-08-21 09:00:00 |
| 2019-09-21 09:00:00 |
| 2019-10-21 09:00:00 |
| 2019-11-21 09:00:00 |
| 2019-12-21 09:00:00 |

我怎样才能得到该日期与 UTC 的偏移量? (假设机器本地时区信息)

例如,我的本地 PC 处于 东部 时区。 东部时区是:

取决于“夏令时”是否在该日期时间生效。

这意味着上面的列表:

| Datetime            | Offset from UTC (minutes)  |
|---------------------|----------------------------|
| 2019-01-21 09:00:00 | -300  (-5 hours)           |
| 2019-02-21 09:00:00 | -300  (-5 hours)           |
| 2019-03-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2019-04-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2019-05-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2019-06-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2019-07-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2019-08-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2019-09-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2019-10-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2019-11-21 09:00:00 | -300  (-5 hours)           | 
| 2019-12-21 09:00:00 | -300  (-5 hours)           | 

当然,如果日期 are from before 2007,这些偏移量也会改变,答案也会改变:

| Datetime            | Offset from UTC (minutes)  |
|---------------------|----------------------------|
| 2006-01-21 09:00:00 | -300  (-5 hours)           |
| 2006-02-21 09:00:00 | -300  (-5 hours)           |
| 2006-03-21 09:00:00 | -240  (-5 hours)           |
| 2006-04-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2006-05-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2006-06-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2006-07-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2006-08-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2006-09-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 2006-10-21 09:00:00 | -240  (-5 hours)           |
| 2006-11-21 09:00:00 | -300  (-5 hours)           | 
| 2006-12-21 09:00:00 | -300  (-5 hours)           | 

在 1977 年的能源危机期间,答案又会有所不同,因为国家 运行 全年实行夏令时:

| Datetime            | Offset from UTC (minutes)  |
|---------------------|----------------------------|
| 1977-01-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 1977-02-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 1977-03-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 1977-04-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 1977-05-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 1977-06-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 1977-07-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 1977-08-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 1977-09-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 1977-10-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 1977-11-21 09:00:00 | -240  (-4 hours)           | Daylight savings
| 1977-12-21 09:00:00 | -240  (-4 hours)           | Daylight savings

并且 before 1966 答案有一些变化。

Windows 知道所有这些事情。

所以问题是:

换句话说:

//Pesudocode. It may look like C#, but i'm using the native Win32 api
Int32 GetDateTimeMinutesOffsetFromUTC(DateTime value)
{
   //2006-03-21 09:00:00 ==> -300
   //2007-03-21 09:00:00 ==> -240
   return -1; //todo
}

function GetDateTimeMinutesOffsetFromUtc(Value: TDateTime): Integer;
begin
   //2006-03-21 09:00:00 ==> -300
   //2007-03-21 09:00:00 ==> -240
   Result := -1; //todo
end;

int GetDateTimeMinutesOffsetFromUtc(FILETIME value)
{
   //2006-03-21 09:00:00 ==> -300
   //2007-03-21 09:00:00 ==> -240
   Result := -1; //todo
}

提醒一下,我使用的是 Win32 api。

我说的是 Windows 和 Win32 API.

SQL 服务器

以上工作可以在SQL服务器中看到:

SELECT
    EventDate, 
    DATEDIFF(minute, CAST(EventDate AS datetime) AT TIME ZONE 'Eastern Standard Time', EventDate) AS MinutesOffsetFromUTC
FROM (VALUES
    ('2019-01-21 09:00:00.000'),
    ('2019-02-21 09:00:00.000'), 
    ('2019-03-21 09:00:00.000'), 
    ('2019-04-21 09:00:00.000'), 
    ('2019-05-21 09:00:00.000'), 
    ('2019-06-21 09:00:00.000'), 
    ('2019-07-21 09:00:00.000'), 
    ('2019-08-21 09:00:00.000'), 
    ('2019-09-21 09:00:00.000'), 
    ('2019-10-21 09:00:00.000'), 
    ('2019-11-21 09:00:00.000'), 
    ('2019-12-21 09:00:00.000')
) foo(EventDate)
EventDate               MinutesOffsetFromUTC
----------------------- --------------------
2019-01-21 09:00:00.000 -300
2019-02-21 09:00:00.000 -300
2019-03-21 09:00:00.000 -240
2019-04-21 09:00:00.000 -240
2019-05-21 09:00:00.000 -240
2019-06-21 09:00:00.000 -240
2019-07-21 09:00:00.000 -240
2019-08-21 09:00:00.000 -240
2019-09-21 09:00:00.000 -240
2019-10-21 09:00:00.000 -240
2019-11-21 09:00:00.000 -300
2019-12-21 09:00:00.000 -300

(12 rows affected)

研究工作

大多数 Winapi 函数用于将 "local" 转换为 "UTC",然后再返回,不要'考虑相关日期;但只使用 whether daylight savings is in effect right now:

Functions like FileTimeToLocalFileTime apply the current Daylight Savings Time (DST) bias rather than the bias that was in effect at the time in question.

其他人考虑转换的日期,但只看现在的夏令时开始和结束规则 - 而不是当时的规则。

但是 TzSpecificLocalTimeToSystemTime 理解日期时间和夏令时的一个函数:

TzSpecificLocalTimeToSystemTime takes into account whether daylight saving time (DST) is in effect for the local time to be converted.

实际上 Windows 并不知道 一切 。它在注册表中保留历史 “夏令时” 日期的数据库:

所以对我来说它真正知道的是

但这对我的用例来说已经足够好了。它是 good enough for SQL Server.

红利阅读

好的,时效已过。我给了每个人自己回答问题的机会,提供了各种提示,这样别人就可以得到甜蜜的声誉。

那个时代已经过去了。现在是回答问题的时候了because it's what Joel and Jeff would have wanted.

伪代码:

int GetDateTimeOffsetFromUtcMinutes(DateTime localDateTime)
{
   //All code on Whosebug is public domain; no attribution is ever required.

   /*
    Given a datetime, tell me how many minutes offset it was from UTC.

        2006-03-21 09:00:00 ==> -300  (before daylight savings rules changed)
        2007-03-21 09:00:00 ==> -240

    The problem is that we don't want to use the current setting of Daylight Savings or not.
    We want use if daylight savings was in effect *at the date* being supplied.

    And we can't even use the current rules:

            - Second Sunday in March: spring forward  (e.g. 3/14/2021 2:00 AM)
            - First Sunday in November: fall back     (e.g. 11/7/2021 2:00 AM)

    because those are the rules today.

    We need to use the rules that were in effect of the date we are considering.

    - e.g. the rules changed in 2007. If we have an OrderDate from 2006, we need those older rules.

    Also notice that some dates have two answers:

        11/7/2021 1:45 AM: EDT (-4 hours)
        11/7/2021 1:45 AM: EST (-5 hours, because at 2am we fallback to 1am, and encounter 1:45AM again, but this time as Standard time)

    So which one do we return? Whatever one i feel like. That's the price you pay for not using UTC or datetime's with an offset.
    */

   //Convert the date to a SYSTEM_TIME structure so we can call the Win32 API 
   SYSTEM_TIME stLocal;
   DateTimeToSystemTime(LocalDateTime, out stLocal);

   SYSTEM_TIME stUtc;
    
   if (!TzSpecificLocalTimeToSystemTime(null, stLocal, out stUtc))
       RaiseLastWin32Error();

   //We now have both "local" and "utc" as a SYSTEM_TIME.
   //Convert both to FILETIME so we can subtract them.
   FILETIME ftLocal, ftUtc;
   if (!SystemTimeToFileTime(stLocal, out ftLocal))
      RaiseLastWin32Error();
   if (!SystemTimeToFileTime(stUtc, out ftUtc))
      RaiseLastWin32Error();

   //Convert the FILETIMEs into Int64s. 
   //We do this because, as you know, you cannot access FILE_TIME structure
   //as an 64-bit integer, even though it is two 32-bit integers back to back.
   LARGE_INTEGER ulLocal, ulUtc;
   ulLocal.LowPart  = ftLocal.dwLowDateTime;
   ulLocal.HighPart = ftLocal.dwHighDatetime;

   ulUtc.LowPart  = ftUtc.dwLowDateTime;
   ulUtc.HighPart = ftUtc.dwHighDatetime;

   //Now subtract the quadparts
   Int64 delta = ulLocal.QuadPart - ulUtc.QuadPart;

   //That delta is in 100ns intervals (0.00001 sec). We want it in whole minutes;
   //         100 ns
   //       0.1 us
   //    0.0001 ms
   // 0.0000001 s
   delta = delta div 10000000; //100 ns ==> seconds  (div is integer division)
   delta = delta div 60; //seconds ==> minutes

   return delta;
}

然后,当然,没有测试用例,任何功能都是不完整的

//After the Daylight Savings rule change of of 2007
Test("2019-01-21T09:00:00", -300); //  (-5 hours)           |
Test("2019-02-21T09:00:00", -300); //  (-5 hours)           |
Test("2019-03-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2019-04-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2019-05-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2019-06-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2019-07-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2019-08-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2019-09-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2019-10-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2019-11-21T09:00:00", -300); //  (-5 hours)           |
Test("2019-12-21T09:00:00", -300); //  (-5 hours)           |

//Before the Daylight Savings rule change of 2007
Test("2006-01-21T09:00:00", -300); //  (-5 hours)           |
Test("2006-02-21T09:00:00", -300); //  (-5 hours)           |
Test("2006-03-21T09:00:00", -300); //  (-5 hours)           | What what?
Test("2006-04-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2006-05-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2006-06-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2006-07-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2006-08-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2006-09-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2006-10-21T09:00:00", -240); //  (-4 hours)           | Daylight savings
Test("2006-11-21T09:00:00", -300); //  (-5 hours)           |
Test("2006-12-21T09:00:00", -300); //  (-5 hours)           |

//Testing spring-forward. Spring forward March 14, 2021 at 2:00 AM
//The weirdness here is that the time from 2:00:00..2:59:59 doesn't exist.
Test("2021-03-14T00:00:00", -300); // EST
Test("2021-03-14T00:59:59", -300); // EST
Test("2021-03-14T01:00:00", -300); // EST
Test("2021-03-14T01:59:00", -300); // EST
Test("2021-03-14T02:00:00", -240); // There is no March 14, 2021 2am - it doesn't exist. The clock goes from 1:59:59 am --> 3:00:00 am. The function does return 240. TODO: figure out why it returns 240
Test("2021-03-14T02:59:59", -240); // There is no March 14, 2021 2:59am - it doesn't exist.
Test("2021-03-14T03:00:00", -240); // EDT

//Testing fall-back. Fall back March 14, 2021 at 2:00 AM
//The weirdness here is that 1:30 AM exists twice.
//12:00 AM -> 12:59:59 AM -> [1:00 AM -> 1:59:59 AM --> 1:00 AM -> 1:59:59 AM] -> 2:00 AM
//So there's no way to know if 11/7/2021 1:30 AM was EDT or EST - both are correct, because it actually did happen twice.
Test("2021-11-07T00:00:00", -240); // EDT
Test("2021-11-07T00:59:59", -240); // EDT
Test("2021-11-07T01:00:00", -240); // EDT (and EST!)
Test("2021-11-07T01:59:59", -240); // EDT (and EST!)
//Test("2021-11-07T01:00:00", -300); // EST (and EDT!)
//Test("2021-11-07T01:59:59", -300); // EST (and EDT!)
Test("2021-11-07T02:00:00", -300); // EST