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

Commit

Permalink
Support new Japanese calendar eras (#20727)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tarekgh authored Nov 12, 2018
1 parent e60258e commit eb90b6c
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 40 deletions.
107 changes: 67 additions & 40 deletions src/mscorlib/shared/System/Globalization/GregorianCalendarHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ internal EraInfo(int era, int startYear, int startMonth, int startDay, int yearO
}

// 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
Expand Down Expand Up @@ -87,7 +87,7 @@ internal class GregorianCalendarHelper
//
// This is the max Gregorian year can be represented by DateTime class. The limitation
// is derived from DateTime class.
//
//
internal int MaxYear
{
get
Expand Down Expand Up @@ -123,22 +123,19 @@ internal GregorianCalendarHelper(Calendar cal, EraInfo[] eraInfo)
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)
Expand All @@ -150,7 +147,38 @@ internal int GetGregorianYear(int year, int era)
{
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),
Expand All @@ -160,38 +188,37 @@ internal int GetGregorianYear(int year, int era)
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);
}

if (era == Calendar.CurrentEra)
{
era = m_Cal.CurrentEraValue;
}
return -1;
}

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;
/*=================================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)
{
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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions src/mscorlib/src/System/AppContext/AppContextSwitches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ public static bool NoAsyncCurrentCulture
}
}

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
{
Expand Down

0 comments on commit eb90b6c

Please sign in to comment.