Why UtcDateTime function is not adding the offset

2019-08-23 06:54发布

问题:

For instantaneous DateTime tracking, I am using a DateTimeOffset datatype. The following function adds the user corresponding TimeZone ID offset to the UTC DateTime property of DateTimeOffset

According to the documentation, UtcDateTime will perform both a time zone conversion and a type conversion on a DateTimeOffset. The following code does not though. Why is the conversion not taking place?

Function to add TimeSpan offset,

public static DateTimeOffset GetUtcDateTime (DateTime sourceDateTime, string timeZoneId) {
 TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById (timeZoneId);
 TimeSpan offset = timeZone.GetUtcOffset (sourceDateTime);
 DateTimeOffset utcTime = new DateTimeOffset (sourceDateTime, offset);
 return utcTime;
 }

and here where I am trying to convert,

DateTimeOffset utcDate = (DateTime.UtcNow);
DateTime fromUtc = utcDate.DateTime;
DateTimeOffset UtcDate = StaticHandlers.GetUtcDateTime (fromUtc, "America/Los_Angeles");
Console.WriteLine ("UTC now is {0} and UTC Date LA is {1} and UtcDateTime LA is {2}", utcDate, UtcDate, utcDate.UtcDateTime);

the output is,

UTC now is 5/8/18 6:43:37 AM +00:00 and and UTC Date LA is 5/8/18 6:43:37 AM -07:00 UtcDateTime LA is 5/8/18 6:43:37 AM

update,

I want to preserve both UTC and the user offset for tracking purposes. DST matters in this context. The example below shows what I am talking about.

DateTime currentDateTime = DateTime.Now;
DateTime beforeDST_LA = new DateTime (2018, 3, 11, 0, 0, 0);
DateTime afterDST_LA = new DateTime (2018, 3, 12, 0, 0, 0);
TimeSpan offsetCurrent = tzi.GetUtcOffset (currentDateTime);
TimeSpan offsetBeforeDST = tzi.GetUtcOffset (beforeDST_LA);
TimeSpan offsetAfterDST = tzi.GetUtcOffset (afterDST_LA);
Console.WriteLine ("Current offset is {0} before DST is {1} and After DST is {2}", offsetCurrent, offsetBeforeDST, offsetAfterDST);

Current offset is -07:00:00 before DST is -08:00:00 and After DST is -07:00:00

回答1:

First, I would not call your function GetUtcDateTime, because that's not what it does. It is trying to get a DateTimeOffset for a specific time zone for a specific time, so call it something like GetDateTimeOffset.

The main concept you're missing in your code is that DateTime has .Kind property, which sets a DateTimeKind value. The kind is taken into consideration by several places in your code:

  • GetUtcOffset will convert Utc or Local kinds to the zone provided before determining the offset.

  • new DateTimeOffset (the constructor) will error if the kind and the offset conflict, if you provide an offset.

  • When you assign a DateTime to a DateTimeOffset, the implicit conversion is evaluating the kind.

  • When you call .DateTime from the DateTimeOffset, the kind will always be Unspecified - regardless of the offset.

If you take all of this into account, you'll realize you need to check the kind yourself before calling GetUtcOffset. If it's not Unspecified then you'll need to convert it to the specified time zone before getting the offset.

public static DateTimeOffset GetDateTimeOffset(DateTime sourceDateTime, string timeZoneId)
{
    TimeZoneInfo timeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);

    // here, is where you need to convert
    if (sourceDateTime.Kind != DateTimeKind.Unspecified)
        sourceDateTime = TimeZoneInfo.ConvertTime(sourceDateTime, timeZone);

    TimeSpan offset = timeZone.GetUtcOffset(sourceDateTime);
    return new DateTimeOffset(sourceDateTime, offset);
}

Now that this is handled, turn to the next set of problems, which is where you call it.

DateTimeOffset utcDate = (DateTime.UtcNow);
DateTime fromUtc = utcDate.DateTime;

In line 1, the implicit cast from DateTime to DateTimeOffset sets the offset to 00:00 - because DateTime.UtcNow has .Kind == DateTimeKind.Utc.

In line 2, the call to the .DateTime property sets fromUtc.Kind == DateTimeKind.Unspecified. Essentially, you've stripped away the kind.

So instead of this, just pass DateTime.UtcNow directly into the function. The kind will persist, and it will all work - now that the Kind is recognized and the conversion is happening inside the function.

All that said, if your original values are all DateTimeOffset (example, DateTimeOffset.UtcNow) then you don't need that function at all. Just call TimeZoneInfo.ConvertTime with the DateTimeOffset directly.