6

I'm stuck with a problem around parsing date and time:

I'm trying to parse a datetime string extracted from a german website. It is given in the format 'day.month.year 24hours:minutes', like:

01.01.2011 17:00

And it is always in the german timezone. But here comes the problem:

  • '01.01.2011 17:00' should be parsed to a DateTime struct with '01.01.2011 16:00' in UTC (here, the timezone is CET, without daylight saving time)
  • while '01.06.2011 17:00' should be parsed to a DateTime struct with '01.01.2011 15:00' in UTC (here, the timezone is CEST, with daylight saving time)

I have no clue how to achieve this. If I set my local clock to the german timezone, and I parse with DateTime.ParseExact and the flag DateTimeStyles.AssumeLocal and DateTimeStyles.AdjustToUniversal it is parsed correctly. However, I want any client to parse it independently from their local clock and timezone. Also, I dont want to do the timezone offset myself, because it depends on the date (summer: -2 / winter: -1).

Once I have the datetime in UTC it would be easy to convert it to any local timezone.

Philip Daubmeier
  • 14,584
  • 5
  • 41
  • 77

2 Answers2

6

It sounds like you know what time zone you should be parsing it with. Assuming .NET 3.5 (and thus TimeZoneInfo) you should logically:

  • Parse it as a "local" time (not time zone specific)
  • Convert that local time to a UTC time

Unfortunately DateTime makes that slightly tricky. EDIT: I thought you'd want to convert parse it using DateTimeStyles.AssumeUniversal - but that ends up returning a local DateTime, annoyingly. Basically you want to end up with a DateTime with the right time so that you can use:

parsed = DateTime.SpecifyKind(parsed, DateTimeKind.Unspecified);

You can then get a UTC value with:

DateTime utc = TimeZoneInfo.ConvertTimeToUtc(parsed, germanTimeZone);

Note that you really want an "unspecified" date time first, so that you can convert it to UTC in an arbitrary time zone. You should also remember the possibility that a local time is ambiguous (occurs twice) or impossible (doesn't occur at all) due to DST changes.

And yes, this will be a lot easier in Noda Time when it's finished :)

Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 1
    +1 Great, the ``ConvertTimeToUtc`` was somehow the missing piece. However, I have one last question: where do I get the ``germanTimeZone`` from? – Philip Daubmeier Aug 30 '11 at 17:23
  • 1
    @Philip: You work out the appropriate time zone ID, and use `TimeZoneInfo.FindSystemTimeZoneById`. I think you want "W. Europe Standard Time" (ignore the fact that it's called standard time; that's just the ID - it'll still handle daylight saving time). – Jon Skeet Aug 30 '11 at 17:25
  • I probably should have mentioned I am building a windows phone 7 application, where ``TimeZoneInfo`` only has the ``ConvertTime`` method and the time zones ``Utc`` and ``Local``. – Philip Daubmeier Aug 30 '11 at 17:28
  • @Philip: Ah. Yes, you should have mentioned that. So you can't get any time zones other than the local one? Eek. Noda Time isn't really production-ready yet, but if you fancy *trying* it to see if it helps you, I'd be very happy to help. We haven't got `LocalDateTime` parsing just yet, but you could parse in .NET and convert afterwards... – Jon Skeet Aug 30 '11 at 17:29
  • Thanks for your help. Without the ``SpecifyKind`` which I didnt know before, I would have wasted even more time :). I wrote a small helper (posted it here as an answer) that does the trick. Noda Time looks great, but I didnt want to include a whole library for a simple job like this. – Philip Daubmeier Aug 31 '11 at 01:18
1

After having seen that the task can not be archieved with the help of the WP7/Silverlight framework, I wrote a small helper that does the job:

public static class DateTimeHelper
{
    /// <summary>
    /// Tries to parse the given datetime string that is not annotated with a timezone 
    /// information but known to be in the CET/CEST zone and returns a DateTime struct
    /// in UTC (so it can be converted to the devices local time). If it could not be 
    /// parsed, result contains the current date/time in UTC.
    /// </summary>
    public static bool TryParseCetCest(string s, string format, IFormatProvider provider, DateTimeStyles style, out DateTime result)
    {
        // Parse datetime, knowing it is in CET/CEST timezone. Parse as universal as we fix it afterwards
        if (!DateTime.TryParseExact(s, format, provider, style, out result))
        {
            result = DateTime.UtcNow;
            return false;
        }
        result = DateTime.SpecifyKind(result, DateTimeKind.Utc);

        // The boundaries of the daylight saving time period in CET and CEST (_not_ in UTC!)
        // Both DateTime structs are of kind 'Utc', to be able to compare them with the parsing result
        DateTime DstStart = LastSundayOf(result.Year, 3).AddHours(2);
        DateTime DstEnd = LastSundayOf(result.Year, 10).AddHours(3);

        // Are we inside the daylight saving time period?
        if (DstStart.CompareTo(result) <= 0 && result.CompareTo(DstEnd) < 0)
            result = result.AddHours(-2); // CEST = UTC+2h
        else
            result = result.AddHours(-1); // CET = UTC+1h

        return true;
    }

    /// <summary>
    /// Returns the last sunday of the given month and year in UTC
    /// </summary>
    private static DateTime LastSundayOf(int year, int month)
    {
        DateTime firstOfNextMonth = new DateTime(year, month + 1, 1, 0, 0, 0, DateTimeKind.Utc);
        return firstOfNextMonth.AddDays(firstOfNextMonth.DayOfWeek == DayOfWeek.Sunday ? -7 :
                                                    (-1 * (int)firstOfNextMonth.DayOfWeek));
    }
}

The trick was to parse it without the DateTimeStyles.AssumeUniversal flag (this makes TryParseExact assume the date is UTC and returning the date converted/adjusted to local), respecifying it as UTC and then manually adjusting it to the actual UTC equivalent.

It follows the DST rules that can be found here. I tested it with all 4 boundary cases just before/after the start/end of the daylight saving time. That showed again the importance of testing: I had to change the < operator in DstStart.CompareTo(result) < 0 to <= to make it produce the correct result.

I had the feeling that I am reinventing the wheel here (which I hate to do), but did not want to use a dedicated library for this simple job. I had a look at Noda Time which is a great project, but I think its not necessary for this.

I hope I can save someone a little time with this small helper. It is intentionally not generic for all time zones (if you need this use a lib like Noda Time instead), but for these cases in which you just have one fixed single time zone, like in my case.

Philip Daubmeier
  • 14,584
  • 5
  • 41
  • 77
  • I'm surprised that you didn't use AssumeUniversal - I would have *expected* that to return it already with a UTC DateTime; apologies for misleading you on that front if it's not the case. DateTime annoys me :( – Jon Skeet Aug 31 '11 at 01:25
  • @Jon: I thought exactly the same to be honest, and wondered why it produced such strange results. Until I realized it does the right thing but different from what you might expect: If you let it parse '17:00' it assumes 5pm UTC like expected but returns a DateTime struct with 7pm (Kind = local) in my case (local timezone = german). If you call ``SpecifyKind(.., Utc)`` on this, it returns a 7pm Utc struct... – Philip Daubmeier Aug 31 '11 at 01:51
  • And yes: DateTime is indeed annoying :) – Philip Daubmeier Aug 31 '11 at 01:52
  • Weird. Ah well, thanks for letting me know. Will edit my answer on that front. Just shows why it's important to be able to parse to different types :) – Jon Skeet Aug 31 '11 at 01:52