diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs index 44c49e52de0b1b..b7a2f984d7b28b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeZoneInfo.Unix.cs @@ -1296,8 +1296,7 @@ private static DateTime ParseTimeOfDay(ReadOnlySpan time) { if (date[0] != 'J') { - // should be n Julian day format which we don't support. - // + // should be n Julian day format. // This specifies the Julian day, with n between 0 and 365. February 29 is counted in leap years. // // n would be a relative number from the beginning of the year. which should handle if the @@ -1313,11 +1312,30 @@ private static DateTime ParseTimeOfDay(ReadOnlySpan time) // 0 30 31 58 59 89 334 364 // |-------Jan--------|-------Feb--------|-------Mar--------|....|-------Dec--------| // - // // For example if n is specified as 60, this means in leap year the rule will start at Mar 1, // while in non leap year the rule will start at Mar 2. // - // If we need to support n format, we'll have to have a floating adjustment rule support this case. + // This n Julian day format is very uncommon and mostly used for convenience to specify dates like January 1st + // which we can support without any major modification to the Adjustment rules. We'll support this rule for day + // numbers less than 59 (up to Feb 28). Otherwise we'll skip this POSIX rule. + // We've never encountered any time zone file using this format for days beyond Feb 28. + + if (int.TryParse(date, out int julianDay) && julianDay < 59) + { + int d, m; + if (julianDay <= 30) // January + { + m = 1; + d = julianDay + 1; + } + else // February + { + m = 2; + d = julianDay - 30; + } + + return TransitionTime.CreateFixedDateRule(ParseTimeOfDay(time), m, d); + } // Since we can't support this rule, return null to indicate to skip the POSIX rule. return null; @@ -1330,7 +1348,7 @@ private static DateTime ParseTimeOfDay(ReadOnlySpan time) } /// - /// Parses a string like Jn or n into month and day values. + /// Parses a string like Jn into month and day values. /// private static void TZif_ParseJulianDay(ReadOnlySpan date, out int month, out int day) { diff --git a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs index 35861477d23883..47a8493ef22f4a 100644 --- a/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs +++ b/src/libraries/System.Runtime/tests/System/TimeZoneInfoTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Globalization; +using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.Serialization.Formatters.Binary; @@ -2353,6 +2354,79 @@ public static void GetSystemTimeZones_AllTimeZonesHaveOffsetInValidRange() } } + private static byte [] timeZoneFileContents = new byte[] + { + 0x54, 0x5A, 0x69, 0x66, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0x5A, 0x69, 0x66, + 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + 0x00, 0x00, 0x00, 0x05, 0x00, 0x00, 0x00, 0x0C, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xFF, 0xFF, 0xF8, 0xE4, 0x00, 0x00, 0x00, 0x00, 0x0E, 0x10, 0x01, 0x04, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x08, 0x00, 0x00, 0x0E, 0x10, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x4C, + 0x4D, 0x54, 0x00, 0x2B, 0x30, 0x31, 0x00, 0x2B, 0x30, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, + // POSIX Rule + // 0x0A, 0x3C, 0x2B, 0x30, 0x30, 0x3E, 0x30, 0x3C, 0x2B, 0x30, 0x31, + // 0x3E, 0x2C, 0x30, 0x2F, 0x30, 0x2C, 0x4A, 0x33, 0x36, 0x35, 0x2F, 0x32, 0x35, 0x0A + }; + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [PlatformSpecific(TestPlatforms.AnyUnix)] + [InlineData("<+00>0<+01>,0/0,J365/25", 1, 1, true)] + [InlineData("<+00>0<+01>,30/0,J365/25", 31, 1, true)] + [InlineData("<+00>0<+01>,31/0,J365/25", 1, 2, true)] + [InlineData("<+00>0<+01>,58/0,J365/25", 28, 2, true)] + [InlineData("<+00>0<+01>,59/0,J365/25", 0, 0, false)] + [InlineData("<+00>0<+01>,9999999/0,J365/25", 0, 0, false)] + [InlineData("<+00>0<+01>,A/0,J365/25", 0, 0, false)] + public static void NJulianRuleTest(string posixRule, int dayNumber, int monthNumber, bool shouldSucceed) + { + string zoneFilePath = Path.GetTempPath() + "dotnet_tz"; + using (FileStream fs = new FileStream(zoneFilePath, FileMode.Create)) + { + fs.Write(timeZoneFileContents.AsSpan()); + + // Append the POSIX rule + fs.WriteByte(0x0A); + foreach (char c in posixRule) + { + fs.WriteByte((byte) c); + } + fs.WriteByte(0x0A); + } + + try + { + ProcessStartInfo psi = new ProcessStartInfo() { UseShellExecute = false }; + psi.Environment.Add("TZ", zoneFilePath); + + RemoteExecutor.Invoke((day, month, succeed) => + { + bool expectedToSucceed = bool.Parse(succeed); + int d = int.Parse(day); + int m = int.Parse(month); + + TimeZoneInfo.AdjustmentRule [] rules = TimeZoneInfo.Local.GetAdjustmentRules(); + + if (expectedToSucceed) + { + Assert.Equal(1, rules.Length); + Assert.Equal(d, rules[0].DaylightTransitionStart.Day); + Assert.Equal(m, rules[0].DaylightTransitionStart.Month); + } + else + { + Assert.Equal(0, rules.Length); + } + }, dayNumber.ToString(), monthNumber.ToString(), shouldSucceed.ToString(), new RemoteInvokeOptions { StartInfo = psi}).Dispose(); + } + finally + { + try { File.Delete(zoneFilePath); } catch { } // don't fail the test if we couldn't delete the file. + } + } + [Fact] public static void TimeZoneInfo_DaylightDeltaIsNoMoreThan12Hours() {