Skip to content
This repository was archived by the owner on Jan 23, 2023. It is now read-only.

Commit eb90b6c

Browse files
authored
Support new Japanese calendar eras (#20727)
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.
1 parent e60258e commit eb90b6c

File tree

3 files changed

+78
-40
lines changed

3 files changed

+78
-40
lines changed

src/mscorlib/shared/System/Globalization/GregorianCalendarHelper.cs

Lines changed: 67 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@ internal EraInfo(int era, int startYear, int startMonth, int startDay, int yearO
4747
}
4848

4949
// This calendar recognizes two era values:
50-
// 0 CurrentEra (AD)
51-
// 1 BeforeCurrentEra (BC)
50+
// 0 CurrentEra (AD)
51+
// 1 BeforeCurrentEra (BC)
5252
internal class GregorianCalendarHelper
5353
{
5454
// 1 tick = 100ns = 10E-7 second
@@ -87,7 +87,7 @@ internal class GregorianCalendarHelper
8787
//
8888
// This is the max Gregorian year can be represented by DateTime class. The limitation
8989
// is derived from DateTime class.
90-
//
90+
//
9191
internal int MaxYear
9292
{
9393
get
@@ -123,22 +123,19 @@ internal GregorianCalendarHelper(Calendar cal, EraInfo[] eraInfo)
123123
m_minYear = m_EraInfo[0].minEraYear; ;
124124
}
125125

126-
/*=================================GetGregorianYear==========================
127-
**Action: Get the Gregorian year value for the specified year in an era.
128-
**Returns: The Gregorian year value.
129-
**Arguments:
130-
** year the year value in Japanese calendar
131-
** era the Japanese emperor era value.
132-
**Exceptions:
133-
** ArgumentOutOfRangeException if year value is invalid or era value is invalid.
134-
============================================================================*/
135-
136-
internal int GetGregorianYear(int year, int era)
126+
// EraInfo.yearOffset: The offset to Gregorian year when the era starts. Gregorian Year = Era Year + yearOffset
127+
// Era Year = Gregorian Year - yearOffset
128+
// EraInfo.minEraYear: Min year value in this era. Generally, this value is 1, but this may be affected by the DateTime.MinValue;
129+
// EraInfo.maxEraYear: Max year value in this era. (== the year length of the era + 1)
130+
private int GetYearOffset(int year, int era, bool throwOnError)
137131
{
138132
if (year < 0)
139133
{
140-
throw new ArgumentOutOfRangeException(nameof(year),
141-
SR.ArgumentOutOfRange_NeedNonNegNum);
134+
if (throwOnError)
135+
{
136+
throw new ArgumentOutOfRangeException(nameof(year), SR.ArgumentOutOfRange_NeedNonNegNum);
137+
}
138+
return -1;
142139
}
143140

144141
if (era == Calendar.CurrentEra)
@@ -150,7 +147,38 @@ internal int GetGregorianYear(int year, int era)
150147
{
151148
if (era == m_EraInfo[i].era)
152149
{
153-
if (year < m_EraInfo[i].minEraYear || year > m_EraInfo[i].maxEraYear)
150+
if (year >= m_EraInfo[i].minEraYear)
151+
{
152+
if (year <= m_EraInfo[i].maxEraYear)
153+
{
154+
return m_EraInfo[i].yearOffset;
155+
}
156+
else if (!AppContextSwitches.EnforceJapaneseEraYearRanges)
157+
{
158+
// If we got the year number exceeding the era max year number, this still possible be valid as the date can be created before
159+
// introducing new eras after the era we are checking. we'll loop on the eras after the era we have and ensure the year
160+
// can exist in one of these eras. otherwise, we'll throw.
161+
// Note, we always return the offset associated with the requested era.
162+
//
163+
// Here is some example:
164+
// 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
165+
// 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
166+
// the subsequent eras (e.g era 5 and up)
167+
168+
int remainingYears = year - m_EraInfo[i].maxEraYear;
169+
170+
for (int j = i - 1; j >= 0; j--)
171+
{
172+
if (remainingYears <= m_EraInfo[j].maxEraYear)
173+
{
174+
return m_EraInfo[i].yearOffset;
175+
}
176+
remainingYears -= m_EraInfo[j].maxEraYear;
177+
}
178+
}
179+
}
180+
181+
if (throwOnError)
154182
{
155183
throw new ArgumentOutOfRangeException(
156184
nameof(year),
@@ -160,38 +188,37 @@ internal int GetGregorianYear(int year, int era)
160188
m_EraInfo[i].minEraYear,
161189
m_EraInfo[i].maxEraYear));
162190
}
163-
return (m_EraInfo[i].yearOffset + year);
191+
192+
break; // no need to iterate more on eras.
164193
}
165194
}
166-
throw new ArgumentOutOfRangeException(nameof(era), SR.ArgumentOutOfRange_InvalidEraValue);
167-
}
168195

169-
internal bool IsValidYear(int year, int era)
170-
{
171-
if (year < 0)
196+
if (throwOnError)
172197
{
173-
return false;
198+
throw new ArgumentOutOfRangeException(nameof(era), SR.ArgumentOutOfRange_InvalidEraValue);
174199
}
175200

176-
if (era == Calendar.CurrentEra)
177-
{
178-
era = m_Cal.CurrentEraValue;
179-
}
201+
return -1;
202+
}
180203

181-
for (int i = 0; i < m_EraInfo.Length; i++)
182-
{
183-
if (era == m_EraInfo[i].era)
184-
{
185-
if (year < m_EraInfo[i].minEraYear || year > m_EraInfo[i].maxEraYear)
186-
{
187-
return false;
188-
}
189-
return true;
190-
}
191-
}
192-
return false;
204+
/*=================================GetGregorianYear==========================
205+
**Action: Get the Gregorian year value for the specified year in an era.
206+
**Returns: The Gregorian year value.
207+
**Arguments:
208+
** year the year value in Japanese calendar
209+
** era the Japanese emperor era value.
210+
**Exceptions:
211+
** ArgumentOutOfRangeException if year value is invalid or era value is invalid.
212+
============================================================================*/
213+
internal int GetGregorianYear(int year, int era)
214+
{
215+
return GetYearOffset(year, era, throwOnError: true) + year;
193216
}
194217

218+
internal bool IsValidYear(int year, int era)
219+
{
220+
return GetYearOffset(year, era, throwOnError: false) >= 0;
221+
}
195222

196223
// Returns a given date part of this DateTime. This method is used
197224
// to compute the year, day-of-year, month, or day part.

src/mscorlib/src/System/AppContext/AppContextDefaultValues.Defaults.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace System
99
internal static partial class AppContextDefaultValues
1010
{
1111
internal static readonly string SwitchNoAsyncCurrentCulture = "Switch.System.Globalization.NoAsyncCurrentCulture";
12+
internal static readonly string SwitchEnforceJapaneseEraYearRanges = "Switch.System.Globalization.EnforceJapaneseEraYearRanges";
1213
internal static readonly string SwitchPreserveEventListnerObjectIdentity = "Switch.System.Diagnostics.EventSource.PreserveEventListnerObjectIdentity";
1314

1415
// This is a partial method. Platforms can provide an implementation of it that will set override values

src/mscorlib/src/System/AppContext/AppContextSwitches.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ public static bool NoAsyncCurrentCulture
1919
}
2020
}
2121

22+
private static int _enforceJapaneseEraYearRanges;
23+
public static bool EnforceJapaneseEraYearRanges
24+
{
25+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
26+
get
27+
{
28+
return GetCachedSwitchValue(AppContextDefaultValues.SwitchEnforceJapaneseEraYearRanges, ref _enforceJapaneseEraYearRanges);
29+
}
30+
}
31+
2232
private static int _preserveEventListnerObjectIdentity;
2333
public static bool PreserveEventListnerObjectIdentity
2434
{

0 commit comments

Comments
 (0)