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

时间:2021-03-13 16:37:23

标签: winapi timezone

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

| 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 位于东部时区。而东部时区是:

  • 300 分钟(5 小时)落后 UTC
  • 240 分钟(4 小时)落后 UTC

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

以上列表的含义:

| 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 知道所有这些。

所以问题是:

  • FILETIME 格式给出日期时间
  • 假定为当前 PC 的时区
  • 如何从 UTC 获取该日期时间的偏移量
  • 在日期时间的时候

换句话说:

//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。

  • 这不是 C/C++(即我无权访问 C 标准库)
  • 这不是 C#(即我无权访问 .NET Framework 类库)
  • 这不是 Java(即我无权访问 Java 类库)
  • 这不是 Python
  • 这不是 Javascript、React、Rust、Django

我说的是 Windows 和 Win32 API。

SQL Server

可以在 SQL Server 中看到以上工作:

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)

研究工作

大多数用于将 "local" 转换为 "UTC" 并返回的 Winapi 函数不考虑相关日期;而是只使用是否 daylight savings is in effect right now:

<块引用>

FileTimeToLocalFileTime 之类的函数应用当前夏令时 (DST) 偏差,而不是相关时间有效的偏差。

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

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

<块引用>

TzSpecificLocalTimeToSystemTime 考虑夏令时 (DST) 是否对要转换的本地时间生效。

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

enter image description here

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

  • 2007 年巨变之前
  • 之后

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

奖励阅读

1 个答案:

答案 0 :(得分:0)

好的,限制条件已过期。我给了每个人自己回答问题的机会,提供各种提示,让别人得到甜甜的名声。

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

伪代码:

int GetDateTimeOffsetFromUtcMinutes(DateTime localDateTime)
{
   //All code on Stackoverflow 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