Support new Japanese calendar eras (#18209)
authorTarek Mahmoud Sayed <tarekms@microsoft.com>
Thu, 31 May 2018 17:31:11 +0000 (10:31 -0700)
committerGitHub <noreply@github.com>
Thu, 31 May 2018 17:31:11 +0000 (10:31 -0700)
Japan is going to introduce the new era next year 2019, this new era will be added to the Japanese calendar. This new era would affect anyone converting, formatting or parsing dates using the Japanese calendar.
Users who formatted future dates before introducing the new era and then try to parse these dates after introducing the new era will fail and get parsing exception. The reason is the year number will not be valid in the old era anymore because the new era set a year limit to the old era.

Here is an example:

Format a date like "平成 32年2月1日" which saying year 32 in the era "平成". after we introduce the new era, the old era "平成" will be limited up to and including year 31 so year 32 is exceeding the era end.

The fix is to allow the parser succeeds with such dates and have a config switch which can be used to for anyone want the old behavior.

src/System.Private.CoreLib/shared/System/Globalization/GregorianCalendarHelper.cs
src/System.Private.CoreLib/src/System/AppContext/AppContextDefaultValues.Defaults.cs
src/System.Private.CoreLib/src/System/AppContext/AppContextSwitches.cs

index 26bb700..34206b3 100644 (file)
@@ -47,8 +47,8 @@ namespace System.Globalization
     }
 
     // This calendar recognizes two era values:
-    // 0 CurrentEra (AD) 
-    // 1 BeforeCurrentEra (BC) 
+    // 0 CurrentEra (AD)
+    // 1 BeforeCurrentEra (BC)
     internal class GregorianCalendarHelper
     {
         // 1 tick = 100ns = 10E-7 second
@@ -87,7 +87,7 @@ namespace System.Globalization
         //
         // This is the max Gregorian year can be represented by DateTime class.  The limitation
         // is derived from DateTime class.
-        // 
+        //
         internal int MaxYear
         {
             get
@@ -123,22 +123,19 @@ namespace System.Globalization
             m_minYear = m_EraInfo[0].minEraYear; ;
         }
 
-        /*=================================GetGregorianYear==========================
-        **Action: Get the Gregorian year value for the specified year in an era.
-        **Returns: The Gregorian year value.
-        **Arguments:
-        **      year    the year value in Japanese calendar
-        **      era     the Japanese emperor era value.
-        **Exceptions:
-        **      ArgumentOutOfRangeException if year value is invalid or era value is invalid.
-        ============================================================================*/
-
-        internal int GetGregorianYear(int year, int era)
+        // EraInfo.yearOffset:  The offset to Gregorian year when the era starts. Gregorian Year = Era Year + yearOffset
+        //                      Era Year = Gregorian Year - yearOffset
+        // EraInfo.minEraYear:  Min year value in this era. Generally, this value is 1, but this may be affected by the DateTime.MinValue;
+        // EraInfo.maxEraYear:  Max year value in this era. (== the year length of the era + 1)
+        private int GetYearOffset(int year, int era, bool throwOnError)
         {
             if (year < 0)
             {
-                throw new ArgumentOutOfRangeException(nameof(year),
-                    SR.ArgumentOutOfRange_NeedNonNegNum);
+                if (throwOnError)
+                {
+                    throw new ArgumentOutOfRangeException(nameof(year), SR.ArgumentOutOfRange_NeedNonNegNum);
+                }
+                return -1;
             }
 
             if (era == Calendar.CurrentEra)
@@ -150,7 +147,38 @@ namespace System.Globalization
             {
                 if (era == m_EraInfo[i].era)
                 {
-                    if (year < m_EraInfo[i].minEraYear || year > m_EraInfo[i].maxEraYear)
+                    if (year >= m_EraInfo[i].minEraYear)
+                    {
+                        if (year <= m_EraInfo[i].maxEraYear)
+                        {
+                            return m_EraInfo[i].yearOffset;
+                        }
+                        else if (!AppContextSwitches.EnforceJapaneseEraYearRanges)
+                        {
+                            // If we got the year number exceeding the era max year number, this still possible be valid as the date can be created before
+                            // introducing new eras after the era we are checking. we'll loop on the eras after the era we have and ensure the year
+                            // can exist in one of these eras. otherwise, we'll throw.
+                            // Note, we always return the offset associated with the requested era.
+                            //
+                            // Here is some example:
+                            // if we are getting the era number 4 (Heisei) and getting the year number 32. if the era 4 has year range from 1 to 31
+                            // then year 32 exceeded the range of era 4 and we'll try to find out if the years difference (32 - 31 = 1) would lay in
+                            // the subsequent eras (e.g era 5 and up)
+
+                            int remainingYears = year - m_EraInfo[i].maxEraYear;
+
+                            for (int j = i - 1; j >= 0; j--)
+                            {
+                                if (remainingYears <= m_EraInfo[j].maxEraYear)
+                                {
+                                    return m_EraInfo[i].yearOffset;
+                                }
+                                remainingYears -= m_EraInfo[j].maxEraYear;
+                            }
+                        }
+                    }
+
+                    if (throwOnError)
                     {
                         throw new ArgumentOutOfRangeException(
                                     nameof(year),
@@ -160,38 +188,37 @@ namespace System.Globalization
                                         m_EraInfo[i].minEraYear,
                                         m_EraInfo[i].maxEraYear));
                     }
-                    return (m_EraInfo[i].yearOffset + year);
+
+                    break; // no need to iterate more on eras.
                 }
             }
-            throw new ArgumentOutOfRangeException(nameof(era), SR.ArgumentOutOfRange_InvalidEraValue);
-        }
 
-        internal bool IsValidYear(int year, int era)
-        {
-            if (year < 0)
+            if (throwOnError)
             {
-                return false;
+                throw new ArgumentOutOfRangeException(nameof(era), SR.ArgumentOutOfRange_InvalidEraValue);
             }
+            return -1;
+        }
 
-            if (era == Calendar.CurrentEra)
-            {
-                era = m_Cal.CurrentEraValue;
-            }
+        /*=================================GetGregorianYear==========================
+        **Action: Get the Gregorian year value for the specified year in an era.
+        **Returns: The Gregorian year value.
+        **Arguments:
+        **      year    the year value in Japanese calendar
+        **      era     the Japanese emperor era value.
+        **Exceptions:
+        **      ArgumentOutOfRangeException if year value is invalid or era value is invalid.
+        ============================================================================*/
 
-            for (int i = 0; i < m_EraInfo.Length; i++)
-            {
-                if (era == m_EraInfo[i].era)
-                {
-                    if (year < m_EraInfo[i].minEraYear || year > m_EraInfo[i].maxEraYear)
-                    {
-                        return false;
-                    }
-                    return true;
-                }
-            }
-            return false;
+        internal int GetGregorianYear(int year, int era)
+        {
+            return GetYearOffset(year, era, throwOnError: true) + year;
         }
 
+        internal bool IsValidYear(int year, int era)
+        {
+            return GetYearOffset(year, era, throwOnError: false) >= 0;
+        }
 
         // Returns a given date part of this DateTime. This method is used
         // to compute the year, day-of-year, month, or day part.
index a624353..5de87d5 100644 (file)
@@ -9,6 +9,7 @@ namespace System
     internal static partial class AppContextDefaultValues
     {
         internal static readonly string SwitchNoAsyncCurrentCulture = "Switch.System.Globalization.NoAsyncCurrentCulture";
+        internal static readonly string SwitchEnforceJapaneseEraYearRanges = "Switch.System.Globalization.EnforceJapaneseEraYearRanges";
         internal static readonly string SwitchPreserveEventListnerObjectIdentity = "Switch.System.Diagnostics.EventSource.PreserveEventListnerObjectIdentity";
 
         // This is a partial method. Platforms can provide an implementation of it that will set override values
index b62d253..17c4084 100644 (file)
@@ -19,6 +19,16 @@ namespace System
             }
         }
 
+        private static int _enforceJapaneseEraYearRanges;
+        public static bool EnforceJapaneseEraYearRanges
+        {
+            [MethodImpl(MethodImplOptions.AggressiveInlining)]
+            get
+            {
+                return GetCachedSwitchValue(AppContextDefaultValues.SwitchEnforceJapaneseEraYearRanges, ref _enforceJapaneseEraYearRanges);
+            }
+        }
+
         private static int _preserveEventListnerObjectIdentity;
         public static bool PreserveEventListnerObjectIdentity
         {