2

I am attempting to get a correctly formatted ISO 8601 date, and I keep hitting issues. My initial code seemed to be working, but I found that DST dates were not returning as expected. I made a .NET fiddle to ask about this issue here on stackoverflow, but it seems the way the "system" timezone works is going to cause me further problems when I deploy my code.

Here is a dotnet fiddle that displays something completely wrong:

using System;
                
public class Program
{
    public static void Main()
    {
        var val3 = TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2021, 10, 13, 18, 0, 0), TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"));
        Console.WriteLine(val3.ToString("yyyy-MM-ddTHH:mm:sszzz"));
    
    }
}

If I run this, I get the following:

2021-10-13T13:00:00+00:00

So the time is correct for CST, but the offset looks like it is reflecting the "system" timezone on the server. Taken altogether, the date is completely different than the input date.

If I run this code on my development system where the local timezone is CST, I get yet another different answer:

2021-10-13T08:00:00-06:00

Given that the date in question was in DST, I expect both of the above to return the following:

2021-10-13T13:00:00-05:00

What I am doing wrong?

lopass
  • 158
  • 10
  • Not to distract from the question too much, but my reason for all this work is that I am maintaining an old SOAP API, and I have verified that I can return datetimes with the offset if I make them Kind.Local. I am hoping I can get a solution that will still work when I deploy. – lopass Feb 02 '22 at 23:42
  • 1
    IMHO, don't use datetime. Convert to `DateTimeOffset` like this; https://stackoverflow.com/a/70647111/4139809 – Jeremy Lakeman Feb 02 '22 at 23:48
  • 1
    Soap is the problem here: https://stackoverflow.com/questions/33108000/how-to-add-timezone-information-to-datetime-of-a-soap-request – lopass Feb 02 '22 at 23:51
  • remember that the date you are initially constructing is in the timezone of the server , not UTC. Use DateTimeOffet – pm100 Feb 02 '22 at 23:53
  • I suspected the timezone of the server is going to be a problem, but what you said technically means I would not be able to deploy this code at all. I will need to check if it is possible to specify a timezone for an AWS lambda. I know it can be done in docker/linux. – lopass Feb 02 '22 at 23:57
  • IMHO `DateTime` is only useful for UTC. In the age of cloud computing, it's completely broken and should be deprecated. – Jeremy Lakeman Feb 03 '22 at 00:14
  • @JeremyLakeman this is exactly correct, without subclassing the XMLSerializer, my only choice would be to setup a server per timezone to make the code work as is. We have decided as an org to return local timestamps only since SOAP is deprecated anyway. We were trying to accommodate the request from a partner, but have directed them to reintegrate using REST instead. – lopass Feb 04 '22 at 18:16

2 Answers2

1

Let me see if my understanding is correct (I'm still not entirely sure when the SOAP comes into play in the question).

You have a UTC DateTime:

var dt = new DateTime(2021, 10, 13, 18, 0, 0, DateTimeKind.Utc);

And you'd like to format that in CST -- which, on October 13, is actually CDT (-05:00)?

If that is correct, then you want to utilise DateTimeOffset, which has a better understanding of.. well, time offsets.

// Your original value. This will have an offset of "00:00" 
// as the DateTime above was created as `DateTimeKind.Utc`
var timeAtUtc = new DateTimeOffset(dt);

// Alternatively:
// var timeAtUtc = new DateTimeOffset(2021, 10, 13, 18, 0, 0, TimeSpan.Zero);

// Find out the correct offset on that day (-5:00)
var cstOffsetAtTheTime = TimeZoneInfo
    .FindSystemTimeZoneById("Central Standard Time")
    .GetUtcOffset(timeAtUtc);

// And now apply the offset to your original value
var timeAtCst = timeAtUtc.ToOffset(cstOffsetAtTheTime);

// You will now get "2021-10-13T13:00:00-05:00" 
// no matter where you run this from.
Console.WriteLine(timeAtCst.ToString("yyyy-MM-ddTHH:mm:sszzz"));

// Edit: If you pass it a date in Feb, you'll see that it will correctly be at -06:00.


Edit 2022-03-07 based on comment below. If you don't need to care about the offset value, you can simply do:

var timeAtUtc = ...;
var timeAtCst = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(
  timeAtUtc, 
  "Central Standard Time");
NPras
  • 3,135
  • 15
  • 29
  • I added this to the fiddle and can confirm it works. The real test will be whether I can get SOAP to take this value and print out the correct information – lopass Feb 03 '22 at 19:52
  • This approach is fine, but just FYI it can be simplified using either `TimeZoneInfo.ConvertTime` or `TimeZoneInfo.ConvertTimeBySystemTimeZoneId` instead of the `GetUtcOffset`+`ToOffset` approach. – Matt Johnson-Pint Mar 04 '22 at 17:19
  • @MattJohnson-Pint, You're absolutely correct. I used to work on a system that logged events with their UTC timestamp and offset in separate columns, so this was the first thing that came to mind. – NPras Mar 07 '22 at 00:21
1

NPras's answer is good, but just to answer the question about what was wrong with the original code:

  • A DateTime object does not store time zone offset information. It only stores a Kind property, which can be one of three DateTimeKind values, either Utc, Local, or Unspecified.

  • Since your input was a DateTime, the output also has to be a DateTime, which has no time zone offset. Thus the offset related to your target time zone is discarded. Using a DateTimeOffset allows the target offset to persist.

  • In the final console output, you used zzz to show the offset. The .NET documentation explains why this used the server's time zone:

    With DateTime values, the "z" custom format specifier represents the signed offset of the local operating system's time zone from Coordinated Universal Time (UTC), measured in hours. It doesn't reflect the value of an instance's DateTime.Kind property. For this reason, the "z" format specifier is not recommended for use with DateTime values.

    With DateTimeOffset values, this format specifier represents the DateTimeOffset value's offset from UTC in hours.

As an aside, if you actually need to show an offset for a DateTime whose Kind is either Utc or Local, you should use the K specifier (the K is for Kind) instead of zzz. With the K specifier, Utc kinds will display a "Z", Local values will display the local offset such as "-05:00", and Unspecified values will display neither (an empty string).

Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575