dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.15k stars 4.71k forks source link

TimeZoneInfo bug changes offset twice within 1 hour #81672

Open clement911 opened 1 year ago

clement911 commented 1 year ago

Description

For some timezones, at specific times, the UTC offset behaves illogically.

Reproduction Steps

With .NET 7

var utc = DateTimeOffset.Parse("2014-12-31T14:30:00Z");
var tz = TimeZoneInfo.FindSystemTimeZoneById("North Korea Standard Time");
Console.WriteLine(TimeZoneInfo.ConvertTime(utc, tz));
Console.WriteLine(TimeZoneInfo.ConvertTime(utc.AddMinutes(30), tz));
Console.WriteLine(TimeZoneInfo.ConvertTime(utc.AddMinutes(60), tz));
Console.Read();

Expected behavior

The utc offset shouldn't change twice within the span of hour

Actual behavior

The utc offset changes twice within the span of hour

Output:

31/12/2014 11:30:00 PM +09:00
31/12/2014 11:30:00 PM +08:30
1/01/2015 12:30:00 AM +09:00

Regression?

Not sure

Known Workarounds

No response

Configuration

.NET 7, windows 11

Other information

No response

Clockwork-Muse commented 1 year ago

The (most likely relevant) official North Korean time zone change was from +0900 to +0830 on 2015-08-15, and from +0830 to +0900 on 2018-05-04 (both times giving less than two weeks notification of the change). So the observed behavior is really wrong.

The utc offset shouldn't change twice within the span of hour

While this specific instance is a bug of some sort, UTC offsets are decided by national governments. Changing the offset multiple times in an hour is a valid (if confusing) thing to do.
There's also nothing "special" about an hour in relation to UTC offsets, other than the fact that most offsets (and DST, when applicable) are in hour increments. Some offsets are +30min, as was the case here. There are some that use +45min. And there is/was a zone that used a +30min DST change.

clement911 commented 1 year ago

@Clockwork-Muse

Changing the offset multiple times in an hour is a valid (if confusing) thing to do.

Ok, so maybe it's technically valid and I didn't describe the root problem correctly, but there is a bug nonetheless.

We found several more timezones that exhibit a similar behaviour. That is, they change the UTC offset at one moment, and then revert it 1 hour later. It always happens on December 31st, which I find very suspicious.

See the following script showing 4 more occurrences, but there are a few more exhibiting the same weird behaviour.

string tzName = "Sao Tome Standard Time";
var utc = DateTimeOffset.Parse("2018-12-31T23:00:00Z");
var tz = TimeZoneInfo.FindSystemTimeZoneById(tzName);
Console.WriteLine(TimeZoneInfo.ConvertTime(utc, tz));
Console.WriteLine(TimeZoneInfo.ConvertTime(utc.AddMinutes(60), tz));
Console.WriteLine(TimeZoneInfo.ConvertTime(utc.AddMinutes(120), tz));
Console.WriteLine();

tzName = "Altai Standard Time";
utc = DateTimeOffset.Parse("2013-12-31T16:00:00Z");
tz = TimeZoneInfo.FindSystemTimeZoneById(tzName);
Console.WriteLine(TimeZoneInfo.ConvertTime(utc, tz));
Console.WriteLine(TimeZoneInfo.ConvertTime(utc.AddMinutes(60), tz));
Console.WriteLine(TimeZoneInfo.ConvertTime(utc.AddMinutes(120), tz));
Console.WriteLine();

tzName = "Saratov Standard Time";
utc = DateTimeOffset.Parse("2013-12-31T19:00:00Z");
tz = TimeZoneInfo.FindSystemTimeZoneById(tzName);
Console.WriteLine(TimeZoneInfo.ConvertTime(utc, tz));
Console.WriteLine(TimeZoneInfo.ConvertTime(utc.AddMinutes(60), tz));
Console.WriteLine(TimeZoneInfo.ConvertTime(utc.AddMinutes(120), tz));
Console.WriteLine();

tzName = "Volgograd Standard Time";
utc = DateTimeOffset.Parse("2018-12-31T19:00:00Z");
tz = TimeZoneInfo.FindSystemTimeZoneById(tzName);
Console.WriteLine(TimeZoneInfo.ConvertTime(utc, tz));
Console.WriteLine(TimeZoneInfo.ConvertTime(utc.AddMinutes(60), tz));
Console.WriteLine(TimeZoneInfo.ConvertTime(utc.AddMinutes(120), tz));
Console.WriteLine();

Console.Read();

Output!

31/12/2018 11:00:00 PM +00:00
1/01/2019 1:00:00 AM +01:00
1/01/2019 1:00:00 AM +00:00

31/12/2013 11:00:00 PM +07:00
31/12/2013 11:00:00 PM +06:00
1/01/2014 1:00:00 AM +07:00

31/12/2013 11:00:00 PM +04:00
31/12/2013 11:00:00 PM +03:00
1/01/2014 1:00:00 AM +04:00

31/12/2018 11:00:00 PM +04:00
31/12/2018 11:00:00 PM +03:00
1/01/2019 1:00:00 AM +04:00
Clockwork-Muse commented 1 year ago

Agreed that there's some sort of bug. This seems to be a Windows-only issue, so is either something in the base data or their specific format (Windows is.... special here).

My initial first guess would be that it's mishandling cases where the base offset for the timezone changes at some point during the year, and no DST, given that seems to be happening for those years for those timezones. I believe the Windows data format and/or TimeZoneInfo may have some holes in this area.

ericstj commented 1 year ago

FWIW I see the same behavior on .NETCore 3.1 and 6.0, so this doesn't appear to be a regression.

C:\scratch\timeZone>C:\scratch\timeZone\bin\Debug\netcoreapp3.1\timeZone.exe
12/31/2014 11:30:00 PM +09:00
12/31/2014 11:30:00 PM +08:30
1/1/2015 12:30:00 AM +09:00

C:\scratch\timeZone>C:\scratch\timeZone\bin\Debug\net6.0\timeZone.exe
12/31/2014 11:30:00 PM +09:00
12/31/2014 11:30:00 PM +08:30
1/1/2015 12:30:00 AM +09:00

C:\scratch\timeZone>C:\scratch\timeZone\bin\Debug\net7.0\timeZone.exe
12/31/2014 11:30:00 PM +09:00
12/31/2014 11:30:00 PM +08:30
1/1/2015 12:30:00 AM +09:00
tarekgh commented 1 year ago

This seems a bug,

According to the Windows data, we have the following North Korea Standard Time rule for year 2015.

DateStart                  : 01/01/2015 00:00:00 (Unspecified)
DateEnd                    : 12/31/2015 00:00:00 (Unspecified)
DaylightDelta              : 00:30:00
DaylightTransitionStart    : M:1, D:1, W:1, DoW:Thursday, Time:01/01/0001 00:00:00, FixedDate:False
DaylightTransitionEnd      : M:8, D:1, W:2, DoW:Friday, Time:01/01/0001 23:59:59, FixedDate:False
BaseUtcOffsetDelta         : -00:30:00
NoDaylightTransitions      : False

The problem is the daylight saving is not detected in the first half hour of the year. Calling tz.IsDaylightSavingTime(utc.AddMinutes(30)) return false while it should be true. This is why you are seeing 30:00 minutes shift in the converted time.

Clockwork-Muse commented 1 year ago

... North Korea doesn't (and didn't) observe DST. This is a base offset change only.

tarekgh commented 1 year ago

@Clockwork-Muse right but the issue is Windows TZ data cannot describe the base offset change during the year (in August 2015). It seems to me to work around the issue in the data, it created a rule with DST that starts in the beginning of the year and ends in August.