Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DateTime - Strange daylight savings behaviour

Tags:

.net

datetime

dst

My local timezone is (UTC+10:00) Canberra, Melbourne, Sydney

Sat 31-Mar-2012 15:59 UTC = Sun 01-Apr-2012 02:59 +11:00
Sat 31-Mar-2012 16:00 UTC = Sun 01-Apr-2012 02:00 +10:00

Daylight savings finishes at 3 AM first Sunday in April and the clock wind back 1 hour.

Given the following code ....

DateTime dt1 = DateTime.Parse("31-Mar-2012 15:59", CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal);

DateTime dt2 = DateTime.Parse("31-Mar-2012 15:59", CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal).AddMinutes(1);
DateTime dt3 = DateTime.Parse("31-Mar-2012 16:00", CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal);

Console.WriteLine("{0:yyyy-MMM-dd HH:mm:ss.ffff K}", dt1);
Console.WriteLine("{0:yyyy-MMM-dd HH:mm:ss.ffff K} ({1}) = {2:yyyy-MMM-dd HH:mm:ss.ffff K} ({3})", dt2, dt2.Kind, dt3, dt3.Kind);
Console.WriteLine("{0} : {1} : {2}", dt1.ToUniversalTime().Hour, dt2.ToUniversalTime().Hour, dt3.ToUniversalTime().Hour);

I get the following output

2012-Apr-01 02:59:00.0000 +11:00
2012-Apr-01 03:00:00.0000 +10:00 (Local) = 2012-Apr-01 02:00:00.0000 +10:00 (Local)
15 : 17 : 16

Adding 1 minute to the original datetime makes the local time 3AM but also set the offset to +10 hours. Adding 1 minute to the UTC date and parsing correctly sets the local time to 2 AM with a +10 UTC offset.

Repeating with

DateTime dt1 = new DateTime(2012, 03, 31, 15, 59, 0, DateTimeKind.Utc);

DateTime dt2 = new DateTime(2012, 03, 31, 15, 59, 0, DateTimeKind.Utc).AddMinutes(1);
DateTime dt3 = new DateTime(2012, 03, 31, 16, 0, 0, DateTimeKind.Utc);

or

DateTime dt1 = DateTime.Parse("31-Mar-2012 15:59", CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal);

DateTime dt2 = DateTime.Parse("31-Mar-2012 15:59", CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal).AddMinutes(1);
DateTime dt3 = DateTime.Parse("31-Mar-2012 16:00", CultureInfo.CurrentCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal); 

gives

2012-Mar-31 15:59:00.0000 Z
2012-Mar-31 16:00:00.0000 Z (Utc) = 2012-Mar-31 16:00:00.0000 Z (Utc)
15 : 16 : 16

as expected

Repeating again with

DateTime dt1 = new DateTime(2012, 03, 31, 15, 59, 0, DateTimeKind.Utc).ToLocalTime();

DateTime dt2 = new DateTime(2012, 03, 31, 15, 59, 0, DateTimeKind.Utc).ToLocalTime().AddMinutes(1);
DateTime dt3 = new DateTime(2012, 03, 31, 16, 0, 0, DateTimeKind.Utc).ToLocalTime();

gives the original

2012-Apr-01 02:59:00.0000 +11:00
2012-Apr-01 03:00:00.0000 +10:00 (Local) = 2012-Apr-01 02:00:00.0000 +10:00 (Local)
15 : 17 : 16

Can anyone explain this ?

Indecently if I use the TimeZoneInfo to convert from UTC to AUS Eastern Standard Time I get the correct time, but I lose the offset information in the DateTime instance as the DateTime.Kind == DateTimeKind.Unspecified

== Additional scenario to highlight

This is just simple timespan adding, starting with an NON-ambiguous UTC date, 1 minute before Daylight savings finishes.

DateTime dt1 = new DateTime(2012, 03, 31, 15, 59, 0, DateTimeKind.Utc);  
DateTime dt2 = new DateTime(2012, 03, 31, 15, 59, 0, DateTimeKind.Utc).ToLocalTime();  

Console.WriteLine("Original in UTC     : {0:yyyy-MMM-dd HH:mm:ss.ffff K}", dt1);  
Console.WriteLine("Original in Local   : {0:yyyy-MMM-dd HH:mm:ss.ffff K}", dt1.ToLocalTime());  
Console.WriteLine("+ 1 Minute in Local : {0:yyyy-MMM-dd HH:mm:ss.ffff K}", dt1.AddMinutes(1).ToLocalTime());  
Console.WriteLine("+ 1 Minute in UTC   : {0:yyyy-MMM-dd HH:mm:ss.ffff K}", dt1.AddMinutes(1));  
Console.WriteLine("=====================================================");
Console.WriteLine("Original in UTC     : {0:yyyy-MMM-dd HH:mm:ss.ffff K}", dt2.ToUniversalTime());  
Console.WriteLine("Original in Local   : {0:yyyy-MMM-dd HH:mm:ss.ffff K}", dt2);  
Console.WriteLine("+ 1 Minute in Local : {0:yyyy-MMM-dd HH:mm:ss.ffff K}", dt2.AddMinutes(1));  
Console.WriteLine("+ 1 Minute in UTC   : {0:yyyy-MMM-dd HH:mm:ss.ffff K}", dt2.AddMinutes(1).ToUniversalTime());  

gives

Original in UTC : 2012-Mar-31 15:59:00.0000 Z
Original in Local : 2012-Apr-01 02:59:00.0000 +11:00
+ 1 Minute in Local : 2012-Apr-01 02:00:00.0000 +10:00
+ 1 Minute in UTC : 2012-Mar-31 16:00:00.0000 Z

=====================================================

Original in UTC : 2012-Mar-31 15:59:00.0000 Z
Original in Local : 2012-Apr-01 02:59:00.0000 +11:00
+ 1 Minute in Local : 2012-Apr-01 03:00:00.0000 +10:00
+ 1 Minute in UTC : 2012-Mar-31 17:00:00.0000 Z

like image 275
Robert Slaney Avatar asked Mar 01 '12 23:03

Robert Slaney


1 Answers

I believe the problem is in terms of when the conversions are performed.

You're parsing assuming universal time, but then implicitly converting to a "local" kind - with a value of 2:59:59. When you ask that "local" value to add a minute, it's just adding a minute to the local value, with no consideration for time zone. When you then print the offset, the system is trying to work out the offset at the local time of 3am... which is +10.

So effectively you've got:

  • Parse step 1: treat string as universal (15:59 UTC)
  • Parse step 2: convert result to local (2:59 local)
  • Addition: in local time, no time zone values are applied (3:00 local)
  • Format step 1: offset is requested, so work out what that local time maps to (17:00 UTC)
  • Format step 2: compute offset as difference between local and universal (+10)

Yes, it's all a bit painful - DateTime is painful in general, which is the main reason I'm writing Noda Time, where there are separate types for "date/time in a zone" vs "local date/time" (or "local date" or "local time"), and it's obvious which you're using at any one point.

It's not clear to me what you're actually trying to achieve here - if you can be more specific, I can show you what you would do in Noda Time, although there may be some inherent ambiguities (conversions from local date/times to "zoned" date/times can have 0, 1 or 2 results).

EDIT: If the aim is merely to remember the time zone as well as the instant, in Noda Time you'd want ZonedDateTime, like this:

using System;
using NodaTime;

class Program
{
    static void Main(string[] args)
    {
        var zone = DateTimeZone.ForId("Australia/Melbourne");
        ZonedDateTime start = Instant.FromUtc(2012, 3, 31, 15, 59, 0)
                                     .InZone(zone);
        ZonedDateTime end = start + Duration.FromMinutes(1);

        Console.WriteLine("{0} ({1})", start.LocalDateTime, start.Offset);
        Console.WriteLine("{0} ({1})", end.LocalDateTime, end.Offset);
    }
}

See the notes on calendar arithmetic for some more information about this.

like image 88
Jon Skeet Avatar answered Oct 15 '22 08:10

Jon Skeet