2

Given a datetime that is "local" (i.e. has no timezone information), e.g.:

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

How can i get that date's offset from UTC? (assuming the machine local timezone information)

For example, my local PC is in the Eastern timezone. And the Eastern timezone is either:

  • 300 minutes (5 hours) behind UTC
  • 240 minutes (4 hours) behind UTC

depending on whether "daylight saving" was in effect at the time of that datetime.

Which means for the above list:

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

And, of course, those offsets change if the dates are from before 2007, the answer changes:

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

And the answer would be different again during the 1977 energy crisis, as the country ran on daylight saving year round:

| 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

And before 1966 the answers change some more.

Windows knows all these things.

So the question is:

  • given a datetime in FILETIME format
  • assumed to be of the timezone of the current PC
  • how can i get that datetime's offset from UTC
  • at the time of the datetime

In other words:

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

or

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

or

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

And, as a reminder, i'm using the Win32 api.

  • This is not C/C++ (i.e. i don't have access to the C standard library)
  • This is not C# (i.e. i don't have access to the .NET Framework Class Library)
  • This is not Java (i.e. i don't have access to the Java Class Library)
  • This is not Python
  • This is not Javascript, React, Rust, Django

I'm talking about Windows and the Win32 API.

SQL Server

You can see the above work in 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)

Research Effort

Most of the Winapi functions for converting "local" to "UTC", and back again, don't take into account the date in question; but instead only use 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.

Others take into account the date being converted, but only look at the daylight saving start and end rules as they are now - not what the rules were then.

But TzSpecificLocalTimeToSystemTime is the one function that does understand datetimes and daylight savings:

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

In reality Windows doesn't know everything. It keeps the database of historical "daylight savings" dates in the registry:

enter image description here

So for me all it really know is

  • before the great change of 2007
  • and after

But it's good enough for me in my use case. And it's good enough for SQL Server.

Bonus Reading

Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219
  • Using .NET framework? Or using the Win32 API? – Andy Mar 13 '21 at 16:44
  • @Andy Native (hence the `winapi` tag). – Ian Boyd Mar 13 '21 at 16:47
  • 1
    but impossible get any info from 8 byte *UTC* time. this is simply some value. you can call `GetTimeZoneInformation` or even may be `NtQuerySystemInformation(SystemTimeOfDayInformation,.. )` for get `SYSTEM_TIMEOFDAY_INFORMATION`. and use this info. or direct convert `FileTimeToLocalFileTime` + `LocalFileTimeToFileTime` – RbMm Mar 13 '21 at 16:52
  • [zoned_time](https://en.cppreference.com/w/cpp/chrono/zoned_time). Either use that, or copy its implementation. – IInspectable Mar 13 '21 at 17:10
  • In .NET it's one line of code. I know you want a Win32 API version, so you could "simply" [port how](https://referencesource.microsoft.com/#mscorlib/system/timezoneinfo.cs,bf901b0095a39240) [.NET does it](https://referencesource.microsoft.com/#mscorlib/system/timezoneinfo.cs,6673f58dd412a56a). – Andy Mar 13 '21 at 17:26
  • @OleV.V. Yes, it's in the registry location in the screenshot. – Ian Boyd Mar 14 '21 at 13:37
  • 1
    @OleV.V. [Certainly not](https://devblogs.microsoft.com/oldnewthing/20160308-00/?p=93123) – Ian Boyd Mar 14 '21 at 16:25

1 Answers1

1

Alright, statue of limitations has expired. I gave everyone every opportunity to answer the question themselves, providing all kinds of hints, so someone else could get the sweet sweet reputation.

That time has passed. Now it's time to answer the question because it's what Joel and Jeff would have wanted.

Pseudo-code:

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

And then, of course, no function is complete without test cases

//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
Ian Boyd
  • 246,734
  • 253
  • 869
  • 1,219