diff --git a/src/mscorlib/shared/System/Globalization/CultureData.cs b/src/mscorlib/shared/System/Globalization/CultureData.cs index fda239c5180b..a70b41f271e3 100644 --- a/src/mscorlib/shared/System/Globalization/CultureData.cs +++ b/src/mscorlib/shared/System/Globalization/CultureData.cs @@ -2134,6 +2134,17 @@ internal string TimeSeparator // Date separator (derived from short date format) internal string DateSeparator(CalendarId calendarId) { + if (calendarId == CalendarId.JAPAN && !AppContextSwitches.EnforceLegacyJapaneseDateParsing) + { + // The date separator is derived from the default short date pattern. So far this pattern is using + // '/' as date separator when using the Japanese calendar which make the formatting and parsing work fine. + // changing the default pattern is likely will happen in the near future which can easily break formatting + // and parsing. + // We are forcing here the date separator to '/' to ensure the parsing is not going to break when changing + // the default short date pattern. The application still can override this in the code by DateTimeFormatInfo.DateSeparartor. + return "/"; + } + return GetDateSeparator(ShortDates(calendarId)[0]); } diff --git a/src/mscorlib/shared/System/Globalization/DateTimeFormat.cs b/src/mscorlib/shared/System/Globalization/DateTimeFormat.cs index 092ad0365d5d..7c96d3a10bd1 100644 --- a/src/mscorlib/shared/System/Globalization/DateTimeFormat.cs +++ b/src/mscorlib/shared/System/Globalization/DateTimeFormat.cs @@ -11,7 +11,7 @@ namespace System { - /* + /* Customized format patterns: P.S. Format in the table below is the internal number format used to display the pattern. @@ -58,14 +58,14 @@ Patterns Format Description Example "ddd" short weekday name (abbreviation) Mon "dddd" full weekday name Monday "dddd*" full weekday name Monday - + "M" "0" month w/o leading zero 2 "MM" "00" month with leading zero 02 "MMM" short month name (abbreviation) Feb "MMMM" full month name Febuary "MMMM*" full month name Febuary - + "y" "0" two digit year (year % 100) w/o leading zero 0 "yy" "00" two digit year (year % 100) with leading zero 00 "yyy" "D3" year 2000 @@ -77,10 +77,10 @@ Patterns Format Description Example "zz" "+00;-00" timezone offset with leading zero -08 "zzz" "+00;-00" for hour offset, "00" for minute offset full timezone offset -07:30 "zzz*" "+00;-00" for hour offset, "00" for minute offset full timezone offset -08:00 - + "K" -Local "zzz", e.g. -08:00 -Utc "'Z'", representing UTC - -Unspecified "" + -Unspecified "" -DateTimeOffset "zzzzz" e.g -07:30:15 "g*" the current era name A.D. @@ -91,12 +91,12 @@ Patterns Format Description Example '"' quoted string "ABC" will insert ABC into the formatted string. "%" used to quote a single pattern characters E.g.The format character "%y" is to print two digit year. "\" escaped character E.g. '\d' insert the character 'd' into the format string. - other characters insert the character into the format string. + other characters insert the character into the format string. - Pre-defined format characters: + Pre-defined format characters: (U) to indicate Universal time is used. (G) to indicate Gregorian calendar is used. - + Format Description Real format Example ========= ================================= ====================== ======================= "d" short date culture-specific 10/31/1999 @@ -120,7 +120,7 @@ based on ISO 8601. */ - //This class contains only static members and does not require the serializable attribute. + //This class contains only static members and does not require the serializable attribute. internal static class DateTimeFormat { @@ -155,16 +155,16 @@ class DateTimeFormat }; //////////////////////////////////////////////////////////////////////////// - // - // Format the positive integer value to a string and perfix with assigned + // + // Format the positive integer value to a string and perfix with assigned // length of leading zero. // // Parameters: // value: The value to format - // len: The maximum length for leading zero. + // len: The maximum length for leading zero. // If the digits of the value is greater than len, no leading zero is added. // - // Notes: + // Notes: // The function can format to Int32.MaxValue. // //////////////////////////////////////////////////////////////////////////// @@ -252,16 +252,16 @@ private static String FormatMonth(int month, int repeatCount, DateTimeFormatInfo // // Action: Return the Hebrew month name for the specified DateTime. // Returns: The month name string for the specified DateTime. - // Arguments: + // Arguments: // time the time to format - // month The month is the value of HebrewCalendar.GetMonth(time). + // month The month is the value of HebrewCalendar.GetMonth(time). // repeat Return abbreviated month name if repeat=3, or full month name if repeat=4 // dtfi The DateTimeFormatInfo which uses the Hebrew calendars as its calendar. // Exceptions: None. - // + // /* Note: - If DTFI is using Hebrew calendar, GetMonthName()/GetAbbreviatedMonthName() will return month names like this: + If DTFI is using Hebrew calendar, GetMonthName()/GetAbbreviatedMonthName() will return month names like this: 1 Hebrew 1st Month 2 Hebrew 2nd Month .. ... @@ -274,7 +274,7 @@ 11 Hebrew 10th Month 12 Hebrew 11th Month 13 Hebrew 12th Month - Therefore, if we are in a regular year, we have to increment the month name if moth is greater or eqaul to 7. + Therefore, if we are in a regular year, we have to increment the month name if moth is greater or eqaul to 7. */ private static String FormatHebrewMonthName(DateTime time, int month, int repeatCount, DateTimeFormatInfo dtfi) { @@ -377,7 +377,7 @@ internal static int ParseNextChar(ReadOnlySpan format, int pos) // // Actions: Check the format to see if we should use genitive month in the formatting. // Starting at the position (index) in the (format) string, look back and look ahead to - // see if there is "d" or "dd". In the case like "d MMMM" or "MMMM dd", we can use + // see if there is "d" or "dd". In the case like "d MMMM" or "MMMM dd", we can use // genitive form. Genitive form is not used if there is more than two "d". // Arguments: // format The format string to be scanned. @@ -447,7 +447,7 @@ private static bool IsUseGenitiveForm(ReadOnlySpan format, int index, int // FormatCustomized // // Actions: Format the DateTime instance using the specified format. - // + // private static StringBuilder FormatCustomized( DateTime dateTime, ReadOnlySpan format, DateTimeFormatInfo dtfi, TimeSpan offset, StringBuilder result) { @@ -459,9 +459,11 @@ private static StringBuilder FormatCustomized( resultBuilderIsPooled = true; result = StringBuilderCache.Acquire(); } - + // This is a flag to indicate if we are format the dates using Hebrew calendar. bool isHebrewCalendar = (cal.ID == CalendarId.HEBREW); + bool isJapaneseCalendar = (cal.ID == CalendarId.JAPAN); + // This is a flag to indicate if we are formating hour/minute/second only. bool bTimeOnly = true; @@ -601,7 +603,7 @@ private static StringBuilder FormatCustomized( bTimeOnly = false; break; case 'M': - // + // // tokenLen == 1 : Month as digits with no leading zero. // tokenLen == 2 : Month as digits with leading zero for single-digit months. // tokenLen == 3 : Month as a three-letter abbreviation. @@ -653,7 +655,19 @@ private static StringBuilder FormatCustomized( int year = cal.GetYear(dateTime); tokenLen = ParseRepeatPattern(format, i, ch); - if (dtfi.HasForceTwoDigitYears) + if (isJapaneseCalendar && + !AppContextSwitches.FormatJapaneseFirstYearAsANumber && + year == 1 && + i + tokenLen < format.Length - 1 && + format[i + tokenLen] == '\'' && + format[i + tokenLen + 1] == DateTimeFormatInfoScanner.CJKYearSuff[0]) + { + // We are formatting a Japanese date with year equals 1 and the year number is followed by the year sign \u5e74 + // In Japanese dates, the first year in the era is not formatted as a number 1 instead it is formatted as \u5143 which means + // first or beginning of the era. + result.Append(DateTimeFormatInfo.JapaneseEraStart[0]); + } + else if (dtfi.HasForceTwoDigitYears) { FormatDigits(result, year, tokenLen <= 2 ? tokenLen : 2); } @@ -697,7 +711,7 @@ private static StringBuilder FormatCustomized( break; case '%': // Optional format character. - // For example, format string "%d" will print day of month + // For example, format string "%d" will print day of month // without leading zero. Most of the cases, "%" can be ignored. nextChar = ParseNextChar(format, i); // nextChar will be -1 if we already reach the end of the format string. @@ -726,7 +740,7 @@ private static StringBuilder FormatCustomized( // Escaped character. Can be used to insert character into the format string. // For exmple, "\d" will insert the character 'd' into the string. // - // NOTENOTE : we can remove this format character if we enforce the enforced quote + // NOTENOTE : we can remove this format character if we enforce the enforced quote // character rule. // That is, we ask everyone to use single quote or double quote to insert characters, // then we can remove this character. @@ -775,7 +789,7 @@ private static void FormatCustomizedTimeZone(DateTime dateTime, TimeSpan offset, if (timeOnly && dateTime.Ticks < Calendar.TicksPerDay) { - // For time only format and a time only input, the time offset on 0001/01/01 is less + // For time only format and a time only input, the time offset on 0001/01/01 is less // accurate than the system's current offset because of daylight saving time. offset = TimeZoneInfo.GetLocalUtcOffset(DateTime.Now, TimeZoneInfoOptions.NoThrowOnInvalidTime); } @@ -820,8 +834,8 @@ private static void FormatCustomizedTimeZone(DateTime dateTime, TimeSpan offset, private static void FormatCustomizedRoundripTimeZone(DateTime dateTime, TimeSpan offset, StringBuilder result) { // The objective of this format is to round trip the data in the type - // For DateTime it should round-trip the Kind value and preserve the time zone. - // DateTimeOffset instance, it should do so by using the internal time zone. + // For DateTime it should round-trip the Kind value and preserve the time zone. + // DateTimeOffset instance, it should do so by using the internal time zone. if (offset == NullOffset) { @@ -897,7 +911,7 @@ internal static String GetRealFormat(ReadOnlySpan format, DateTimeFormatIn realFormat = RoundtripFormat; break; case 'r': - case 'R': // RFC 1123 Standard + case 'R': // RFC 1123 Standard realFormat = dtfi.RFC1123Pattern; break; case 's': // Sortable without Time Zone Info @@ -940,7 +954,7 @@ private static String ExpandPredefinedFormat(ReadOnlySpan format, ref Date dtfi = DateTimeFormatInfo.InvariantInfo; break; case 'r': - case 'R': // RFC 1123 Standard + case 'R': // RFC 1123 Standard if (offset != NullOffset) { // Convert to UTC invariants mean this will be in range @@ -952,7 +966,7 @@ private static String ExpandPredefinedFormat(ReadOnlySpan format, ref Date } dtfi = DateTimeFormatInfo.InvariantInfo; break; - case 's': // Sortable without Time Zone Info + case 's': // Sortable without Time Zone Info dtfi = DateTimeFormatInfo.InvariantInfo; break; case 'u': // Universal time in sortable format. @@ -1075,7 +1089,7 @@ private static StringBuilder FormatStringBuilder(DateTime dateTime, ReadOnlySpan // If the time is less than 1 day, consider it as time of day. // Just print out the short time format. // - // This is a workaround for VB, since they use ticks less then one day to be + // This is a workaround for VB, since they use ticks less then one day to be // time of day. In cultures which use calendar other than Gregorian calendar, these // alternative calendar may not support ticks less than a day. // For example, Japanese calendar only supports date after 1868/9/8. diff --git a/src/mscorlib/shared/System/Globalization/DateTimeFormatInfo.cs b/src/mscorlib/shared/System/Globalization/DateTimeFormatInfo.cs index edec75ac85bb..2045042608be 100644 --- a/src/mscorlib/shared/System/Globalization/DateTimeFormatInfo.cs +++ b/src/mscorlib/shared/System/Globalization/DateTimeFormatInfo.cs @@ -133,7 +133,7 @@ public sealed class DateTimeFormatInfo : IFormatProvider, ICloneable // The string contains the default pattern. // When we initially construct our string[], we set the string to string[0] - // The "default" Date/time patterns + // The "default" Date/time patterns private String longDatePattern = null; private String shortDatePattern = null; private String yearMonthPattern = null; @@ -554,8 +554,8 @@ public int GetEra(String eraName) SR.ArgumentNull_String); } - // The Era Name and Abbreviated Era Name - // for Taiwan Calendar on non-Taiwan SKU returns empty string (which + // The Era Name and Abbreviated Era Name + // for Taiwan Calendar on non-Taiwan SKU returns empty string (which // would be matched below) but we don't want the empty string to give // us an Era number // confer 85900 DTFI.GetEra("") should fail on all cultures @@ -583,7 +583,7 @@ public int GetEra(String eraName) } for (int i = 0; i < AbbreviatedEraNames.Length; i++) { - // Compare the abbreviated era name in a case-insensitive way for the appropriate culture. + // Compare the abbreviated era name in a case-insensitive way for the appropriate culture. if (this.Culture.CompareInfo.Compare(eraName, m_abbrevEraNames[i], CompareOptions.IgnoreCase) == 0) { return (i + 1); @@ -1707,7 +1707,7 @@ private static string[] GetMergedPatterns(string[] patterns, string defaultPatte internal const String RoundtripFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK"; internal const String RoundtripDateTimeUnfixed = "yyyy'-'MM'-'ddTHH':'mm':'ss zzz"; - // Default string isn't necessarily in our string array, so get the + // Default string isn't necessarily in our string array, so get the // merged patterns of both private String[] AllYearMonthPatterns { @@ -2096,7 +2096,7 @@ internal static void ValidateStyles(DateTimeStyles style, String parameterName) private DateTimeFormatFlags InitializeFormatFlags() { // Build the format flags from the data in this DTFI - formatFlags = + formatFlags = (DateTimeFormatFlags)DateTimeFormatInfoScanner.GetFormatFlagGenitiveMonth( MonthNames, internalGetGenitiveMonthNames(false), AbbreviatedMonthNames, internalGetGenitiveMonthNames(true)) | (DateTimeFormatFlags)DateTimeFormatInfoScanner.GetFormatFlagUseSpaceInMonthNames( @@ -2220,6 +2220,8 @@ internal Boolean YearMonthAdjustment(ref int year, ref int month, Boolean parsed internal const String CJKMinuteSuff = "\u5206"; internal const String CJKSecondSuff = "\u79d2"; + internal const string JapaneseEraStart = "\u5143"; + internal const String LocalTimeMark = "T"; internal const String GMTName = "GMT"; @@ -2322,6 +2324,15 @@ internal TokenHashValue[] CreateTokenHashTable() InsertHash(temp, CJKMinuteSuff, TokenType.SEP_MinuteSuff, 0); InsertHash(temp, CJKSecondSuff, TokenType.SEP_SecondSuff, 0); + if (!AppContextSwitches.EnforceLegacyJapaneseDateParsing && Calendar.ID == CalendarId.JAPAN) + { + // We need to support parsing the dates has the start of era symbol which means it is year 1 in the era. + // The start of era symbol has to be followed by the year symbol suffix, otherwise it would be invalid date. + InsertHash(temp, JapaneseEraStart, TokenType.YearNumberToken, 1); + InsertHash(temp, "(", TokenType.IgnorableSymbol, 0); + InsertHash(temp, ")", TokenType.IgnorableSymbol, 0); + } + // TODO: This ignores other custom cultures that might want to do something similar if (koreanLanguage) { @@ -2621,6 +2632,25 @@ private static bool IsHebrewChar(char ch) return (ch >= '\x0590' && ch <= '\x05ff'); } + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + private bool IsAllowedJapaneseTokenFollowedByNonSpaceLetter(string tokenString, char nextCh) + { + // Allow the parser to recognize the case when having some date part followed by JapaneseEraStart "\u5143" + // without spaces in between. e.g. Era name followed by \u5143 in the date formats ggy. + // Also, allow recognizing the year suffix symbol "\u5e74" followed the JapaneseEraStart "\u5143" + if (!AppContextSwitches.EnforceLegacyJapaneseDateParsing && Calendar.ID == CalendarId.JAPAN && + ( + // something like ggy, era followed by year and the year is specified using the JapaneseEraStart "\u5143" + nextCh == JapaneseEraStart[0] || + // JapaneseEraStart followed by year suffix "\u5143" + (tokenString == JapaneseEraStart && nextCh == CJKYearSuff[0]) + )) + { + return true; + } + return false; + } + internal bool Tokenize(TokenType TokenMask, out TokenType tokenType, out int tokenValue, ref __DTString str) { @@ -2687,12 +2717,12 @@ internal bool Tokenize(TokenType TokenMask, out TokenType tokenType, out int tok } else if (nextCharIndex < str.Length) { - // Check word boundary. The next character should NOT be a letter. + // Check word boundary. The next character should NOT be a letter. char nextCh = str.Value[nextCharIndex]; - compareStrings = !(Char.IsLetter(nextCh)); + compareStrings = !(char.IsLetter(nextCh)) || IsAllowedJapaneseTokenFollowedByNonSpaceLetter(value.tokenString, nextCh); } } - + if (compareStrings && ((value.tokenString.Length == 1 && str.Value[str.Index] == value.tokenString[0]) || Culture.CompareInfo.Compare(str.Value.Slice(str.Index, value.tokenString.Length), value.tokenString, CompareOptions.IgnoreCase) == 0)) diff --git a/src/mscorlib/shared/System/Globalization/DateTimeParse.cs b/src/mscorlib/shared/System/Globalization/DateTimeParse.cs index 970d1765bb32..3a9f7be80678 100644 --- a/src/mscorlib/shared/System/Globalization/DateTimeParse.cs +++ b/src/mscorlib/shared/System/Globalization/DateTimeParse.cs @@ -186,7 +186,7 @@ internal static bool TryParseExactMultiple(ReadOnlySpan s, String[] format Debug.Assert(dtfi != null, "dtfi == null"); // - // Do a loop through the provided formats and see if we can parse succesfully in + // Do a loop through the provided formats and see if we can parse successfully in // one of the formats. // for (int i = 0; i < formats.Length; i++) @@ -2675,7 +2675,7 @@ internal static bool TryParse(ReadOnlySpan s, DateTimeFormatInfo dtfi, Dat return false; } - // Check if the parased string only contains hour/minute/second values. + // Check if the parsed string only contains hour/minute/second values. bool bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1); // @@ -3829,9 +3829,23 @@ private static String ExpandPredefinedFormat(ReadOnlySpan format, ref Date return (DateTimeFormat.GetRealFormat(format, dtfi)); } + [MethodImplAttribute(MethodImplOptions.AggressiveInlining)] + private static bool ParseJapaneseEraStart(ref __DTString str, DateTimeFormatInfo dtfi) + { + // ParseJapaneseEraStart will be called when parsing the year number. We can have dates which not listing + // the year as a number and listing it as JapaneseEraStart symbol (which means year 1). + // This will be legitimate date to recognize. + if (AppContextSwitches.EnforceLegacyJapaneseDateParsing || dtfi.Calendar.ID != CalendarId.JAPAN || !str.GetNext()) + return false; + if (str.m_current != DateTimeFormatInfo.JapaneseEraStart[0]) + { + str.Index--; + return false; + } - + return true; + } // Given a specified format character, parse and update the parsing result. // @@ -3854,7 +3868,12 @@ private static bool ParseByFormat( case 'y': tokenLen = format.GetRepeatCount(); bool parseResult; - if (dtfi.HasForceTwoDigitYears) + if (ParseJapaneseEraStart(ref str, dtfi)) + { + tempYear = 1; + parseResult = true; + } + else if (dtfi.HasForceTwoDigitYears) { parseResult = ParseDigits(ref str, 1, 4, out tempYear); } @@ -4423,7 +4442,7 @@ internal static bool TryParseQuoteString(ReadOnlySpan format, int pos, Str ** When the following general formats are used, the time is assumed to be in Universal time. ** **Limitations: - ** Only GregarianCalendar is supported for now. + ** Only GregorianCalendar is supported for now. ** Only support GMT timezone. ==============================================================================*/ @@ -4570,7 +4589,7 @@ private static bool DoStrictParse( } - // Check if the parased string only contains hour/minute/second values. + // Check if the parsed string only contains hour/minute/second values. bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1); if (!CheckDefaultDateTime(ref result, ref parseInfo.calendar, styles)) { @@ -4792,7 +4811,7 @@ private static void Trace(string s) internal ref struct __DTString { // - // Value propery: stores the real string to be parsed. + // Value property: stores the real string to be parsed. // internal ReadOnlySpan Value; @@ -4804,7 +4823,7 @@ internal ref struct __DTString // The length of Value string. internal int Length => Value.Length; - // The current chracter to be looked at. + // The current character to be looked at. internal char m_current; private CompareInfo m_info; @@ -5144,7 +5163,7 @@ internal bool Match(char ch) // The index that contains the longest word to match // Arguments: // words The string array that contains words to search. - // maxMatchStrLen [in/out] the initailized maximum length. This parameter can be used to + // maxMatchStrLen [in/out] the initialized maximum length. This parameter can be used to // find the longest match in two string arrays. // internal int MatchLongestWords(String[] words, ref int maxMatchStrLen) diff --git a/src/mscorlib/src/System/AppContext/AppContextDefaultValues.Defaults.cs b/src/mscorlib/src/System/AppContext/AppContextDefaultValues.Defaults.cs index 5de87d5dfd6b..dc64aef9a8e2 100644 --- a/src/mscorlib/src/System/AppContext/AppContextDefaultValues.Defaults.cs +++ b/src/mscorlib/src/System/AppContext/AppContextDefaultValues.Defaults.cs @@ -9,6 +9,8 @@ namespace System internal static partial class AppContextDefaultValues { internal static readonly string SwitchNoAsyncCurrentCulture = "Switch.System.Globalization.NoAsyncCurrentCulture"; + internal static readonly string SwitchFormatJapaneseFirstYearAsANumber = "Switch.System.Globalization.FormatJapaneseFirstYearAsANumber"; + internal static readonly string SwitchEnforceLegacyJapaneseDateParsing = "Switch.System.Globalization.EnforceLegacyJapaneseDateParsing"; internal static readonly string SwitchEnforceJapaneseEraYearRanges = "Switch.System.Globalization.EnforceJapaneseEraYearRanges"; internal static readonly string SwitchPreserveEventListnerObjectIdentity = "Switch.System.Diagnostics.EventSource.PreserveEventListnerObjectIdentity"; diff --git a/src/mscorlib/src/System/AppContext/AppContextSwitches.cs b/src/mscorlib/src/System/AppContext/AppContextSwitches.cs index 17c408428a43..c3d595db74c9 100644 --- a/src/mscorlib/src/System/AppContext/AppContextSwitches.cs +++ b/src/mscorlib/src/System/AppContext/AppContextSwitches.cs @@ -19,6 +19,16 @@ public static bool NoAsyncCurrentCulture } } + private static int _formatJapaneseFirstYearAsANumber; + public static bool FormatJapaneseFirstYearAsANumber + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return GetCachedSwitchValue(AppContextDefaultValues.SwitchFormatJapaneseFirstYearAsANumber, ref _formatJapaneseFirstYearAsANumber); + } + } + private static int _enforceJapaneseEraYearRanges; public static bool EnforceJapaneseEraYearRanges { @@ -29,6 +39,16 @@ public static bool EnforceJapaneseEraYearRanges } } + private static int _enforceLegacyJapaneseDateParsing; + public static bool EnforceLegacyJapaneseDateParsing + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + return GetCachedSwitchValue(AppContextDefaultValues.SwitchEnforceLegacyJapaneseDateParsing, ref _enforceLegacyJapaneseDateParsing); + } + } + private static int _preserveEventListnerObjectIdentity; public static bool PreserveEventListnerObjectIdentity {