Fix reading Time zone rules using Julian days (#17672)
authorTarek Mahmoud Sayed <tarekms@microsoft.com>
Thu, 19 Apr 2018 20:14:12 +0000 (13:14 -0700)
committerGitHub <noreply@github.com>
Thu, 19 Apr 2018 20:14:12 +0000 (13:14 -0700)
src/mscorlib/Resources/Strings.resx
src/mscorlib/shared/System/TimeZoneInfo.Unix.cs

index 5d37b50..49c8e93 100644 (file)
@@ -1,17 +1,17 @@
 <?xml version="1.0" encoding="utf-8"?>
 <root>
-  <!-- 
-    Microsoft ResX Schema 
-    
+  <!--
+    Microsoft ResX Schema
+
     Version 2.0
-    
-    The primary goals of this format is to allow a simple XML format 
-    that is mostly human readable. The generation and parsing of the 
-    various data types are done through the TypeConverter classes 
+
+    The primary goals of this format is to allow a simple XML format
+    that is mostly human readable. The generation and parsing of the
+    various data types are done through the TypeConverter classes
     associated with the data types.
-    
+
     Example:
-    
+
     ... ado.net/XML headers & schema ...
     <resheader name="resmimetype">text/microsoft-resx</resheader>
     <resheader name="version">2.0</resheader>
         <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
         <comment>This is a comment</comment>
     </data>
-                
-    There are any number of "resheader" rows that contain simple 
+
+    There are any number of "resheader" rows that contain simple
     name/value pairs.
-    
-    Each data row contains a name, and value. The row also contains a 
-    type or mimetype. Type corresponds to a .NET class that support 
-    text/value conversion through the TypeConverter architecture. 
-    Classes that don't support this are serialized and stored with the 
+
+    Each data row contains a name, and value. The row also contains a
+    type or mimetype. Type corresponds to a .NET class that support
+    text/value conversion through the TypeConverter architecture.
+    Classes that don't support this are serialized and stored with the
     mimetype set.
-    
-    The mimetype is used for serialized objects, and tells the 
-    ResXResourceReader how to depersist the object. This is currently not 
+
+    The mimetype is used for serialized objects, and tells the
+    ResXResourceReader how to depersist the object. This is currently not
     extensible. For a given mimetype the value must be set accordingly:
-    
-    Note - application/x-microsoft.net.object.binary.base64 is the format 
-    that the ResXResourceWriter will generate, however the reader can 
+
+    Note - application/x-microsoft.net.object.binary.base64 is the format
+    that the ResXResourceWriter will generate, however the reader can
     read any of the formats listed below.
-    
+
     mimetype: application/x-microsoft.net.object.binary.base64
-    value   : The object must be serialized with 
+    value   : The object must be serialized with
             : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
             : and then encoded with base64 encoding.
-    
+
     mimetype: application/x-microsoft.net.object.soap.base64
-    value   : The object must be serialized with 
+    value   : The object must be serialized with
             : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
             : and then encoded with base64 encoding.
 
     mimetype: application/x-microsoft.net.object.bytearray.base64
-    value   : The object must be serialized into a byte array 
+    value   : The object must be serialized into a byte array
             : using a System.ComponentModel.TypeConverter
             : and then encoded with base64 encoding.
     -->
   <data name="InvalidTimeZone_InvalidRegistryData" xml:space="preserve">
     <value>The time zone ID '{0}' was found on the local computer, but the registry information was corrupt.</value>
   </data>
-  <data name="InvalidTimeZone_JulianDayNotSupported" xml:space="preserve">
-    <value>Julian dates in POSIX strings are unsupported.</value>
+  <data name="InvalidTimeZone_InvalidJulianDay" xml:space="preserve">
+    <value>Invalid Julian day in POSIX strings.</value>
+  </data>
+  <data name="InvalidTimeZone_NJulianDayNotSupported" xml:space="preserve">
+    <value>Julian n day in POSIX strings is not supported.</value>
   </data>
   <data name="InvalidTimeZone_NoTTInfoStructures" xml:space="preserve">
     <value>There are no ttinfo structures in the tzfile.  At least one ttinfo structure is required in order to construct a TimeZoneInfo object.</value>
index 2dcaf67..cc386a1 100644 (file)
@@ -1117,6 +1117,37 @@ namespace System
             return result;
         }
 
+        private static DateTime ParseTimeOfDay(string time)
+        {
+            DateTime timeOfDay;
+            TimeSpan? timeOffset = TZif_ParseOffsetString(time);
+            if (timeOffset.HasValue)
+            {
+                // This logic isn't correct and can't be corrected until https://github.com/dotnet/corefx/issues/2618 is fixed.
+                // Some time zones use time values like, "26", "144", or "-2".
+                // This allows the week to sometimes be week 4 and sometimes week 5 in the month.
+                // For now, strip off any 'days' in the offset, and just get the time of day correct
+                timeOffset = new TimeSpan(timeOffset.Value.Hours, timeOffset.Value.Minutes, timeOffset.Value.Seconds);
+                if (timeOffset.Value < TimeSpan.Zero)
+                {
+                    timeOfDay = new DateTime(1, 1, 2, 0, 0, 0);
+                }
+                else
+                {
+                    timeOfDay = new DateTime(1, 1, 1, 0, 0, 0);
+                }
+
+                timeOfDay += timeOffset.Value;
+            }
+            else
+            {
+                // default to 2AM.
+                timeOfDay = new DateTime(1, 1, 1, 2, 0, 0);
+            }
+
+            return timeOfDay;
+        }
+
         private static TransitionTime TZif_CreateTransitionTimeFromPosixRule(string date, string time)
         {
             if (string.IsNullOrEmpty(date))
@@ -1138,48 +1169,90 @@ namespace System
                     throw new InvalidTimeZoneException(SR.Format(SR.InvalidTimeZone_UnparseablePosixMDateString, date));
                 }
 
-                DateTime timeOfDay;
-                TimeSpan? timeOffset = TZif_ParseOffsetString(time);
-                if (timeOffset.HasValue)
-                {
-                    // This logic isn't correct and can't be corrected until https://github.com/dotnet/corefx/issues/2618 is fixed.
-                    // Some time zones use time values like, "26", "144", or "-2".
-                    // This allows the week to sometimes be week 4 and sometimes week 5 in the month.
-                    // For now, strip off any 'days' in the offset, and just get the time of day correct
-                    timeOffset = new TimeSpan(timeOffset.Value.Hours, timeOffset.Value.Minutes, timeOffset.Value.Seconds);
-                    if (timeOffset.Value < TimeSpan.Zero)
-                    {
-                        timeOfDay = new DateTime(1, 1, 2, 0, 0, 0);
-                    }
-                    else
-                    {
-                        timeOfDay = new DateTime(1, 1, 1, 0, 0, 0);
-                    }
-
-                    timeOfDay += timeOffset.Value;
-                }
-                else
+                return TransitionTime.CreateFloatingDateRule(ParseTimeOfDay(time), month, week, day);
+            }
+            else
+            {
+                if (date[0] != 'J')
                 {
-                    // default to 2AM.
-                    timeOfDay = new DateTime(1, 1, 1, 2, 0, 0);
+                    // should be n Julian day format which we don't support.
+                    //
+                    // 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 begining of the year. which should handle if the
+                    // the year is a leap year or not.
+                    //
+                    // In leap year, n would be counted as:
+                    //
+                    // 0                30 31              59 60              90      335            365
+                    // |-------Jan--------|-------Feb--------|-------Mar--------|....|-------Dec--------|
+                    //
+                    // while in non leap year we'll have
+                    //
+                    // 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.
+
+                    throw new InvalidTimeZoneException(SR.InvalidTimeZone_NJulianDayNotSupported);
                 }
 
-                return TransitionTime.CreateFloatingDateRule(timeOfDay, month, week, day);
+                // Julian day
+                TZif_ParseJulianDay(date, out int month, out int day);
+                return TransitionTime.CreateFixedDateRule(ParseTimeOfDay(time), month, day);
             }
-            else
+        }
+
+        /// <summary>
+        /// Parses a string like Jn or n into month and day values.
+        /// </summary>
+        /// <returns>
+        /// true if the parsing succeeded; otherwise, false.
+        /// </returns>
+        private static void TZif_ParseJulianDay(string date, out int month, out int day)
+        {
+            // Jn
+            // This specifies the Julian day, with n between 1 and 365.February 29 is never counted, even in leap years.
+            Debug.Assert(date[0] == 'J');
+            Debug.Assert(!String.IsNullOrEmpty(date));
+            month = day = 0;
+
+            int index = 1;
+
+            if (index >= date.Length || ((uint)(date[index] - '0') > '9'-'0'))
             {
-                // Jn
-                // This specifies the Julian day, with n between 1 and 365.February 29 is never counted, even in leap years.
+                throw new InvalidTimeZoneException(SR.InvalidTimeZone_InvalidJulianDay);
+            }
+
+            int julianDay = 0;
 
-                // n
-                // This specifies the Julian day, with n between 0 and 365.February 29 is counted in leap years.
+            do
+            {
+                julianDay = julianDay * 10 + (int) (date[index] - '0');
+                index++;
+            } while (index < date.Length && ((uint)(date[index] - '0') <= '9'-'0'));
 
-                // These two rules cannot be expressed with the current AdjustmentRules
-                // One of them *could* be supported if we relaxed the TransitionTime validation rules, and allowed
-                // "IsFixedDateRule = true, Month = 0, Day = n" to mean the nth day of the year, picking one of the rules above
+            int[] days = GregorianCalendarHelper.DaysToMonth365;
 
-                throw new InvalidTimeZoneException(SR.InvalidTimeZone_JulianDayNotSupported);
+            if (julianDay == 0 || julianDay > days[days.Length - 1])
+            {
+                throw new InvalidTimeZoneException(SR.InvalidTimeZone_InvalidJulianDay);
             }
+
+            int i = 1;
+            while (i < days.Length && julianDay > days[i])
+            {
+                i++;
+            }
+
+            Debug.Assert(i > 0 && i < days.Length);
+
+            month = i;
+            day = julianDay - days[i - 1];
         }
 
         /// <summary>