71

I want to parse a string that represent a DateTime in UTC format.

My string representation includes the Zulu time specification which should indicate that the string represent a UTC time.

var myDate = DateTime.Parse("2012-09-30T23:00:00.0000000Z");    

From the above I would expect myDate.Kind to be DateTimeKind.Utc, instead it is DatetimeKind.Local.

What am I doing wrong and how to Parse a string that represents a UTC time?

Many thanks!

Giuseppe Romagnuolo
  • 3,362
  • 2
  • 30
  • 38

5 Answers5

98

I would use my Noda Time project personally. (Admittedly I'm biased as the author, but it would be cleaner...) But if you can't do that...

Either use DateTime.ParseExact specifying the exact format you expect, and include DateTimeStyles.AssumeUniversal and DateTimeStyles.AdjustToUniversal in the parse code:

using System;
using System.Globalization;

class Test
{
    static void Main()        
    {
        var date = DateTime.ParseExact("2012-09-30T23:00:00.0000000Z",
                                       "yyyy-MM-dd'T'HH:mm:ss.fffffff'Z'",
                                       CultureInfo.InvariantCulture,
                                       DateTimeStyles.AssumeUniversal |
                                       DateTimeStyles.AdjustToUniversal);
        Console.WriteLine(date);
        Console.WriteLine(date.Kind);
    }
}

(Quite why it would adjust to local by default without AdjustToUniversal is beyond me, but never mind...)

EDIT: Just to expand on my objections to mattytommo's suggestion, I aimed to prove that it would lose information. I've failed so far - but in a very peculiar way. Have a look at this - running in the Europe/London time zone, where the clocks go back on October 28th in 2012, at 2am local time (1am UTC):

DateTime local1 = DateTime.Parse("2012-10-28T00:30:00.0000000Z");
DateTime local2 = DateTime.Parse("2012-10-28T01:30:00.0000000Z");
Console.WriteLine(local1 == local2); // True

DateTime utc1 = TimeZoneInfo.ConvertTimeToUtc(local1);
DateTime utc2 = TimeZoneInfo.ConvertTimeToUtc(local2);
Console.WriteLine(utc1 == utc2); // False. Hmm.

It looks like there's a "with or without DST" flag being stored somewhere, but I'll be blowed if I can work out where. The docs for TimeZoneInfo.ConvertTimeToUtc state

If dateTime corresponds to an ambiguous time, this method assumes that it is the standard time of the source time zone.

That doesn't appear to be the case here when converting local2...

EDIT: Okay, it gets even stranger - it depends which version of the framework you're using. Consider this program:

using System;
using System.Globalization;

class Test
{
    static void Main()        
    {
        DateTime local1 = DateTime.Parse("2012-10-28T00:30:00.0000000Z");
        DateTime local2 = DateTime.Parse("2012-10-28T01:30:00.0000000Z");

        DateTime utc1 = TimeZoneInfo.ConvertTimeToUtc(local1);
        DateTime utc2 = TimeZoneInfo.ConvertTimeToUtc(local2);
        Console.WriteLine(utc1);
        Console.WriteLine(utc2);

        DateTime utc3 = local1.ToUniversalTime();
        DateTime utc4 = local2.ToUniversalTime();
        Console.WriteLine(utc3);
        Console.WriteLine(utc4);
    }
}

So this takes two different UTC values, parses them with DateTime.Parse, then converts them back to UTC in two different ways.

Results under .NET 3.5:

28/10/2012 01:30:00 // Look - we've lost information
28/10/2012 01:30:00
28/10/2012 00:30:00 // But ToUniversalTime() seems okay...
28/10/2012 01:30:00

Results under .NET 4.5 beta:

28/10/2012 00:30:00 // It's okay!
28/10/2012 01:30:00
28/10/2012 00:30:00
28/10/2012 01:30:00
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    A DateTime value stores its Kind in its most significant two bits: Unspecified (00), Utc (01), Local (10), and LocalAmbiguousDst (11). However, LocalAmbiguousDst is exposed publicly as Local. – Michael Liu Apr 05 '12 at 14:05
  • 1
    @MichaelLiu: Right. So until you convert it back to universal, you can't tell the difference. How lovely. Ick. – Jon Skeet Apr 05 '12 at 14:08
  • @JonSkeet Does that mean that my code was correct (with the exception of ParseExact) as it needs to be converted back to universal first? – Mathew Thompson Apr 05 '12 at 16:04
  • 1
    @mattytommo: No. As you can see from my .NET 3.5 repro, `ConvertTimeToUtc` doesn't seem to always honour this. And it's still *logically* wrong, even if `DateTime` tries to steer a half-way course. Basically, any conversions from local time to UTC need to consider the possibility of ambiguous or invalid dates. – Jon Skeet Apr 05 '12 at 16:07
  • 1
    You can also do DateTime.SpecifyKind(myDate,DateTimeKind.Local).ToString("0") – marcel Apr 09 '15 at 12:48
  • I would never have thought to use DateTimeStyles.AdjustToUniversal for this. – memory of a dream Jun 29 '17 at 09:03
  • @JonSkeet Etiquette note: you should disclose your affiliation with NodaTime – BobbyA Aug 10 '18 at 18:35
  • 1
    @BobbyA: Done in the first paragraph. – Jon Skeet Aug 10 '18 at 22:04
31

As usual, Jon's answer is very comprehensive. That said, nobody has yet mentioned DateTimeStyles.RoundtripKind. If you want to convert a DateTime to a string and back to the same DateTime (including preserving the DateTime.Kind setting), use the DateTimeStyles.RoundtripKind flag.

As Jon said, the correct thing to do is to use the "O" formatter when converting a DateTime object to a string. This preserves both the precision and timezone information. Again, as Jon said, use DateTime.ParseExact when converting back. But if you use DateTimeStyles.RoundtripKind, you always get back what you put in:

var now = DateTime.UtcNow;
var strNow = now.ToString("O");
var newNow = DateTime.ParseExact(strNow, "O", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);

In the above code, newNow is a exact same time as now, including the fact that it is UTC. If run the same code except substitute DateTime.Now for DateTime.UtcNow, you'll get an exact copy of now back as newNow, but this time as a local time.

For my purposes, this was the right thing since I wanted to make that sure that whatever was passed in and converted is converted back to the exact same thing.

Simon Gillbee
  • 3,932
  • 4
  • 35
  • 48
  • Thank you so much. I was looking for this flag! Jon's advice will convert all dates to UTC regardless of original timezone in the string. – irriss Mar 16 '15 at 10:02
  • Fantastic answer! – ZakiMa Aug 30 '20 at 08:11
  • Good answer; I would still opt-in for DateTime.Parse BUT with your guidance added to it; e.g. `DateTime.Parse(strNow, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind)`. For more information, please refer to: https://learn.microsoft.com/en-us/dotnet/api/system.datetime.parse?view=net-7.0#system-datetime-parse(system-string-system-iformatprovider-system-globalization-datetimestyles) – gimlichael Nov 19 '22 at 15:05
5

Use the TimeZoneInfo class using the following:

var myDate = TimeZoneInfo.ConvertTimeToUtc(DateTime.Parse("2012-09-30T23:00:00.0000000Z"));
Mathew Thompson
  • 55,877
  • 15
  • 127
  • 148
  • 4
    Bad idea IMO. If the parsed time ends up being *ambiguous* in the local time zone, you've basically lost information. Avoid the unnecessary conversion, by telling the parser to parse it as a universal time to start with. – Jon Skeet Apr 05 '12 at 13:06
  • @JonSkeet What about if ParseExact was used instead of parse, but still using the TimeZoneInfo class, would you still consider that a bad idea? – Mathew Thompson Apr 05 '12 at 13:10
  • @mattytommo: Yes, to be honest. There's no need to use `TimeZoneInfo` here - why convert it to local time and back when there's no point? It looks like actually `DateTime` secretes some information about the offset *somewhere* internally, but I'm struggling to work out where right now... – Jon Skeet Apr 05 '12 at 13:14
  • @JonSkeet but parsing the Date isn't doing the conversion to local time, we are only doing it once via the `TimeZoneInfo` class? I agree maybe the `TimeZoneInfo` class is using a sledgehammer to crack a nut here and your answer is the better of the two, but does it warrant a downvote? I'd say not :P – Mathew Thompson Apr 05 '12 at 13:20
  • @mattytommo: The parsing code is returning a local time, by converting it from the UTC value to local. You're converting *from* the local time back to UTC in the code you've presented. And yes, I *do* think it warrants a downvote, as it's at least *conceptually* losing information. (I'm not quite sure why I can't actually demonstrate it causing a problem at the moment, but it really could.) The issue of local times being potentially ambiguous is a subtle one, leading to subtle bugs which are best avoided by simply not doing any such conversion to start with. – Jon Skeet Apr 05 '12 at 13:32
  • @JonSkeet Fair enough about the parsing, but surely it's only worth the downvote if you can demonstrate it losing the information? If you can, I'll take my hat off to you :). Although in my time I have seen some peculiar edge cases when developing for multi-national applications and trying to standardise timings and dates :) – Mathew Thompson Apr 05 '12 at 13:35
  • @mattytommo: See my edited answer. It depends on which version of .NET you're using, oddly enough - on .NET 3.5 your code *does* lose information, though just using `ToUniversalTime` doesn't. But that relies on "hidden" flags in `DateTime` by the looks of it - you end up with two *equal* local times which are converted to non-equal UTC times. Do you *really* want that sort of thing happening in your application? (Yet another reason to use Noda Time, in my not-so-humble opinion...) – Jon Skeet Apr 05 '12 at 13:41
  • @JonSkeet I'll give you that one Jon, +1 for your answer. I'll just put a disclaimer on my answer saying ONLY FOR .NET 4.5! hehe. You're like the Chuck Norris of C#, minus the beard ;) – Mathew Thompson Apr 05 '12 at 13:50
4

You can use the following format for parser method: yyyy-MM-ddTHH:mm:ss.ffffffK

This shall properly process time zone information at the end (starting from .NET 2.0).

RE: ISO 8601

port443
  • 561
  • 1
  • 4
  • 12
3

Ran into a similar issue before and several hours (and pulled hairs) later ended up using DateTime.SpecifyKind:

DateTime.SpecifyKind(inputDate, DateTimeKind.Utc);

I believe someone also eluded to this in a comment above as well.

vandsh
  • 1,329
  • 15
  • 12