// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; namespace System.Globalization { /// /// Flags used to indicate different styles of month names. /// This is an internal flag used by internalGetMonthName(). /// Use flag here in case that we need to provide a combination of these styles /// (such as month name of a leap year in genitive form. Not likely for now, /// but would like to keep the option open). /// [Flags] internal enum MonthNameStyles { Regular = 0x00000000, Genitive = 0x00000001, LeapYear = 0x00000002, } /// /// Flags used to indicate special rule used in parsing/formatting /// for a specific DateTimeFormatInfo instance. /// This is an internal flag. /// /// This flag is different from MonthNameStyles because this flag /// can be expanded to accommodate parsing behaviors like CJK month names /// or alternative month names, etc. /// [Flags] internal enum DateTimeFormatFlags { None = 0x00000000, UseGenitiveMonth = 0x00000001, UseLeapYearMonth = 0x00000002, UseSpacesInMonthNames = 0x00000004, // Has spaces or non-breaking space in the month names. UseHebrewRule = 0x00000008, // Format/Parse using the Hebrew calendar rule. UseSpacesInDayNames = 0x00000010, // Has spaces or non-breaking space in the day names. UseDigitPrefixInTokens = 0x00000020, // Has token starting with numbers. NotInitialized = -1, } public sealed class DateTimeFormatInfo : IFormatProvider, ICloneable { // cache for the invariant culture. // invariantInfo is constant irrespective of your current culture. private static volatile DateTimeFormatInfo? s_invariantInfo; // an index which points to a record in Culture Data Table. private readonly CultureData _cultureData; // The culture name used to create this DTFI. private string? _name = null; // The language name of the culture used to create this DTFI. private string? _langName = null; // CompareInfo usually used by the parser. private CompareInfo? _compareInfo = null; // Culture matches current DTFI. mainly used for string comparisons during parsing. private CultureInfo? _cultureInfo = null; private string? amDesignator = null; private string? pmDesignator = null; private string? dateSeparator = null; // derived from short date (whidbey expects, arrowhead doesn't) private string? generalShortTimePattern = null; // short date + short time (whidbey expects, arrowhead doesn't) private string? generalLongTimePattern = null; // short date + long time (whidbey expects, arrowhead doesn't) private string? timeSeparator = null; // derived from long time (whidbey expects, arrowhead doesn't) private string? monthDayPattern = null; private string? dateTimeOffsetPattern = null; private const string rfc1123Pattern = "ddd, dd MMM yyyy HH':'mm':'ss 'GMT'"; // The sortable pattern is based on ISO 8601. private const string sortableDateTimePattern = "yyyy'-'MM'-'dd'T'HH':'mm':'ss"; private const string universalSortableDateTimePattern = "yyyy'-'MM'-'dd HH':'mm':'ss'Z'"; private Calendar calendar = null!; // initialized in helper called by ctors private int firstDayOfWeek = -1; private int calendarWeekRule = -1; private string? fullDateTimePattern = null; // long date + long time (whidbey expects, arrowhead doesn't) private string[]? abbreviatedDayNames = null; private string[]? m_superShortDayNames = null; private string[]? dayNames = null; private string[]? abbreviatedMonthNames = null; private string[]? monthNames = null; // Cache the genitive month names that we retrieve from the data table. private string[]? genitiveMonthNames = null; // Cache the abbreviated genitive month names that we retrieve from the data table. private string[]? m_genitiveAbbreviatedMonthNames = null; // Cache the month names of a leap year that we retrieve from the data table. private string[]? leapYearMonthNames = null; // For our "patterns" arrays we have 2 variables, a string and a string[] // // The string[] contains the list of patterns, EXCEPT the default may not be included. // The string contains the default pattern. // When we initially construct our string[], we set the string to string[0] // The "default" Date/time patterns private string? longDatePattern = null; private string? shortDatePattern = null; private string? yearMonthPattern = null; private string? longTimePattern = null; private string? shortTimePattern = null; private string[]? allYearMonthPatterns = null; private string[]? allShortDatePatterns = null; private string[]? allLongDatePatterns = null; private string[]? allShortTimePatterns = null; private string[]? allLongTimePatterns = null; // Cache the era names for this DateTimeFormatInfo instance. private string[]? m_eraNames = null; private string[]? m_abbrevEraNames = null; private string[]? m_abbrevEnglishEraNames = null; private CalendarId[]? optionalCalendars = null; private const int DEFAULT_ALL_DATETIMES_SIZE = 132; // CultureInfo updates this internal bool _isReadOnly = false; // This flag gives hints about if formatting/parsing should perform special code path for things like // genitive form or leap year month names. private DateTimeFormatFlags formatFlags = DateTimeFormatFlags.NotInitialized; private string CultureName => _name ??= _cultureData.CultureName; private CultureInfo Culture => _cultureInfo ??= CultureInfo.GetCultureInfo(CultureName); private string LanguageName => _langName ??= _cultureData.TwoLetterISOLanguageName; /// /// Create an array of string which contains the abbreviated day names. /// private string[] InternalGetAbbreviatedDayOfWeekNames() => abbreviatedDayNames ?? InternalGetAbbreviatedDayOfWeekNamesCore(); [MethodImpl(MethodImplOptions.NoInlining)] private string[] InternalGetAbbreviatedDayOfWeekNamesCore() { // Get the abbreviated day names for our current calendar abbreviatedDayNames = _cultureData.AbbreviatedDayNames(Calendar.ID); Debug.Assert(abbreviatedDayNames.Length == 7, "[DateTimeFormatInfo.GetAbbreviatedDayOfWeekNames] Expected 7 day names in a week"); return abbreviatedDayNames; } /// /// Returns the string array of the one-letter day of week names. /// private string[] InternalGetSuperShortDayNames() => m_superShortDayNames ?? InternalGetSuperShortDayNamesCore(); [MethodImpl(MethodImplOptions.NoInlining)] private string[] InternalGetSuperShortDayNamesCore() { // Get the super short day names for our current calendar m_superShortDayNames = _cultureData.SuperShortDayNames(Calendar.ID); Debug.Assert(m_superShortDayNames.Length == 7, "[DateTimeFormatInfo.InternalGetSuperShortDayNames] Expected 7 day names in a week"); return m_superShortDayNames; } /// /// Create an array of string which contains the day names. /// private string[] InternalGetDayOfWeekNames() => dayNames ?? InternalGetDayOfWeekNamesCore(); [MethodImpl(MethodImplOptions.NoInlining)] private string[] InternalGetDayOfWeekNamesCore() { // Get the day names for our current calendar dayNames = _cultureData.DayNames(Calendar.ID); Debug.Assert(dayNames.Length == 7, "[DateTimeFormatInfo.GetDayOfWeekNames] Expected 7 day names in a week"); return dayNames; } /// /// Create an array of string which contains the abbreviated month names. /// private string[] InternalGetAbbreviatedMonthNames() => abbreviatedMonthNames ?? InternalGetAbbreviatedMonthNamesCore(); [MethodImpl(MethodImplOptions.NoInlining)] private string[] InternalGetAbbreviatedMonthNamesCore() { // Get the month names for our current calendar abbreviatedMonthNames = _cultureData.AbbreviatedMonthNames(Calendar.ID); Debug.Assert(abbreviatedMonthNames.Length == 12 || abbreviatedMonthNames.Length == 13, "[DateTimeFormatInfo.GetAbbreviatedMonthNames] Expected 12 or 13 month names in a year"); return abbreviatedMonthNames; } /// /// Create an array of string which contains the month names. /// private string[] InternalGetMonthNames() => monthNames ?? internalGetMonthNamesCore(); [MethodImpl(MethodImplOptions.NoInlining)] private string[] internalGetMonthNamesCore() { // Get the month names for our current calendar monthNames = _cultureData.MonthNames(Calendar.ID); Debug.Assert(monthNames.Length == 12 || monthNames.Length == 13, "[DateTimeFormatInfo.GetMonthNames] Expected 12 or 13 month names in a year"); return monthNames; } // Invariant DateTimeFormatInfo doesn't have user-overriden values // Default calendar is gregorian public DateTimeFormatInfo() : this(CultureInfo.InvariantCulture._cultureData, GregorianCalendar.GetDefaultInstance()) { } internal DateTimeFormatInfo(CultureData cultureData, Calendar cal) { Debug.Assert(cultureData != null); Debug.Assert(cal != null); // Remember our culture _cultureData = cultureData; Calendar = cal; } private void InitializeOverridableProperties(CultureData cultureData, CalendarId calendarId) { Debug.Assert(cultureData != null); Debug.Assert(calendarId != CalendarId.UNINITIALIZED_VALUE, "[DateTimeFormatInfo.Populate] Expected initalized calendarId"); if (firstDayOfWeek == -1) { firstDayOfWeek = cultureData.FirstDayOfWeek; } if (calendarWeekRule == -1) { calendarWeekRule = cultureData.CalendarWeekRule; } if (amDesignator == null) { amDesignator = cultureData.AMDesignator; } if (pmDesignator == null) { pmDesignator = cultureData.PMDesignator; } if (timeSeparator == null) { timeSeparator = cultureData.TimeSeparator; } if (dateSeparator == null) { dateSeparator = cultureData.DateSeparator(calendarId); } allLongTimePatterns = _cultureData.LongTimes; Debug.Assert(allLongTimePatterns.Length > 0, "[DateTimeFormatInfo.Populate] Expected some long time patterns"); allShortTimePatterns = _cultureData.ShortTimes; Debug.Assert(allShortTimePatterns.Length > 0, "[DateTimeFormatInfo.Populate] Expected some short time patterns"); allLongDatePatterns = cultureData.LongDates(calendarId); Debug.Assert(allLongDatePatterns.Length > 0, "[DateTimeFormatInfo.Populate] Expected some long date patterns"); allShortDatePatterns = cultureData.ShortDates(calendarId); Debug.Assert(allShortDatePatterns.Length > 0, "[DateTimeFormatInfo.Populate] Expected some short date patterns"); allYearMonthPatterns = cultureData.YearMonths(calendarId); Debug.Assert(allYearMonthPatterns.Length > 0, "[DateTimeFormatInfo.Populate] Expected some year month patterns"); } /// /// Returns a default DateTimeFormatInfo that will be universally /// supported and constant irrespective of the current culture. /// public static DateTimeFormatInfo InvariantInfo { get { if (s_invariantInfo == null) { DateTimeFormatInfo info = new DateTimeFormatInfo(); info.Calendar.SetReadOnlyState(true); info._isReadOnly = true; s_invariantInfo = info; } return s_invariantInfo; } } /// /// Returns the current culture's DateTimeFormatInfo. /// public static DateTimeFormatInfo CurrentInfo { get { System.Globalization.CultureInfo culture = System.Globalization.CultureInfo.CurrentCulture; if (!culture._isInherited) { DateTimeFormatInfo? info = culture._dateTimeInfo; if (info != null) { return info; } } return (DateTimeFormatInfo)culture.GetFormat(typeof(DateTimeFormatInfo))!; } } public static DateTimeFormatInfo GetInstance(IFormatProvider? provider) => provider == null ? CurrentInfo : provider is CultureInfo cultureProvider && !cultureProvider._isInherited ? cultureProvider.DateTimeFormat : provider is DateTimeFormatInfo info ? info : provider.GetFormat(typeof(DateTimeFormatInfo)) is DateTimeFormatInfo info2 ? info2 : CurrentInfo; // Couldn't get anything, just use currentInfo as fallback public object? GetFormat(Type? formatType) { return formatType == typeof(DateTimeFormatInfo) ? this : null; } public object Clone() { DateTimeFormatInfo n = (DateTimeFormatInfo)MemberwiseClone(); // We can use the data member calendar in the setter, instead of the property Calendar, // since the cloned copy should have the same state as the original copy. n.calendar = (Calendar)Calendar.Clone(); n._isReadOnly = false; return n; } public string AMDesignator { get { if (amDesignator == null) { amDesignator = _cultureData.AMDesignator; } Debug.Assert(amDesignator != null, "DateTimeFormatInfo.AMDesignator, amDesignator != null"); return amDesignator; } set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } ClearTokenHashTable(); amDesignator = value; } } public Calendar Calendar { get { Debug.Assert(calendar != null, "DateTimeFormatInfo.Calendar: calendar != null"); return calendar; } set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } if (value == calendar) { return; } for (int i = 0; i < OptionalCalendars.Length; i++) { if (OptionalCalendars[i] == value.ID) { // We can use this one, so do so. // Clean related properties if we already had a calendar set if (calendar != null) { // clean related properties which are affected by the calendar setting, // so that they will be refreshed when they are accessed next time. // These properites are in the order as appearing in calendar.xml. m_eraNames = null; m_abbrevEraNames = null; m_abbrevEnglishEraNames = null; monthDayPattern = null; dayNames = null; abbreviatedDayNames = null; m_superShortDayNames = null; monthNames = null; abbreviatedMonthNames = null; genitiveMonthNames = null; m_genitiveAbbreviatedMonthNames = null; leapYearMonthNames = null; formatFlags = DateTimeFormatFlags.NotInitialized; allShortDatePatterns = null; allLongDatePatterns = null; allYearMonthPatterns = null; dateTimeOffsetPattern = null; // The defaults need reset as well: longDatePattern = null; shortDatePattern = null; yearMonthPattern = null; // These properies are not in the OS data, but they are dependent on the values like shortDatePattern. fullDateTimePattern = null; // Long date + long time generalShortTimePattern = null; // short date + short time generalLongTimePattern = null; // short date + long time // Derived item that changes dateSeparator = null; // We don't need to do these because they are not changed by changing calendar // amDesignator // pmDesignator // timeSeparator // longTimePattern // firstDayOfWeek // calendarWeekRule // remember to reload tokens ClearTokenHashTable(); } // Remember the new calendar calendar = value; InitializeOverridableProperties(_cultureData, calendar.ID); // We succeeded, return return; } } // The assigned calendar is not a valid calendar for this culture, throw throw new ArgumentOutOfRangeException(nameof(value), value, SR.Argument_InvalidCalendar); } } private CalendarId[] OptionalCalendars => optionalCalendars ??= _cultureData.CalendarIds; /// /// Get the era value by parsing the name of the era. /// public int GetEra(string eraName) { if (eraName == null) { throw new ArgumentNullException(nameof(eraName)); } // 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 if (eraName.Length == 0) { return -1; } // The following is based on the assumption that the era value is starting from 1, and has a // serial values. // If that ever changes, the code has to be changed. // The calls to String.Compare should use the current culture for the string comparisons, but the // invariant culture when comparing against the english names. for (int i = 0; i < EraNames.Length; i++) { // Compare the era name in a case-insensitive way for the appropriate culture. if (m_eraNames![i].Length > 0) { if (Culture.CompareInfo.Compare(eraName, m_eraNames[i], CompareOptions.IgnoreCase) == 0) { return i + 1; } } } for (int i = 0; i < AbbreviatedEraNames.Length; i++) { // Compare the abbreviated era name in a case-insensitive way for the appropriate culture. if (Culture.CompareInfo.Compare(eraName, m_abbrevEraNames![i], CompareOptions.IgnoreCase) == 0) { return i + 1; } } for (int i = 0; i < AbbreviatedEnglishEraNames.Length; i++) { // this comparison should use the InvariantCulture. The English name could have linguistically // interesting characters. if (CompareInfo.Invariant.Compare(eraName, m_abbrevEnglishEraNames![i], CompareOptions.IgnoreCase) == 0) { return i + 1; } } return -1; } internal string[] EraNames => m_eraNames ??= _cultureData.EraNames(Calendar.ID); /// /// Get the name of the era for the specified era value. /// Era names are 1 indexed /// public string GetEraName(int era) { if (era == Calendar.CurrentEra) { era = Calendar.CurrentEraValue; } // The following is based on the assumption that the era value is starting from 1, and has a // serial values. // If that ever changes, the code has to be changed. if ((--era) < EraNames.Length && (era >= 0)) { return m_eraNames![era]; } throw new ArgumentOutOfRangeException(nameof(era), era, SR.ArgumentOutOfRange_InvalidEraValue); } internal string[] AbbreviatedEraNames => m_abbrevEraNames ??= _cultureData.AbbrevEraNames(Calendar.ID); /// /// Era names are 1 indexed /// public string GetAbbreviatedEraName(int era) { if (AbbreviatedEraNames.Length == 0) { // If abbreviation era name is not used in this culture, // return the full era name. return GetEraName(era); } if (era == Calendar.CurrentEra) { era = Calendar.CurrentEraValue; } if ((--era) < m_abbrevEraNames!.Length && (era >= 0)) { return m_abbrevEraNames[era]; } throw new ArgumentOutOfRangeException(nameof(era), era, SR.ArgumentOutOfRange_InvalidEraValue); } internal string[] AbbreviatedEnglishEraNames { get { if (m_abbrevEnglishEraNames == null) { Debug.Assert(Calendar.ID > 0, "[DateTimeFormatInfo.AbbreviatedEnglishEraNames] Expected Calendar.ID > 0"); m_abbrevEnglishEraNames = _cultureData.AbbreviatedEnglishEraNames(Calendar.ID); } return m_abbrevEnglishEraNames; } } /// /// Note that cultureData derives this from the short date format (unless someone's set this previously) /// Note that this property is quite undesirable. /// public string DateSeparator { get { if (dateSeparator == null) { dateSeparator = _cultureData.DateSeparator(Calendar.ID); } Debug.Assert(dateSeparator != null, "DateTimeFormatInfo.DateSeparator, dateSeparator != null"); return dateSeparator; } set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } ClearTokenHashTable(); dateSeparator = value; } } public DayOfWeek FirstDayOfWeek { get { if (firstDayOfWeek == -1) { firstDayOfWeek = _cultureData.FirstDayOfWeek; } Debug.Assert(firstDayOfWeek != -1, "DateTimeFormatInfo.FirstDayOfWeek, firstDayOfWeek != -1"); return (DayOfWeek)firstDayOfWeek; } set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value < DayOfWeek.Sunday || value > DayOfWeek.Saturday) { throw new ArgumentOutOfRangeException( nameof(value), value, SR.Format(SR.ArgumentOutOfRange_Range, DayOfWeek.Sunday, DayOfWeek.Saturday)); } firstDayOfWeek = (int)value; } } public CalendarWeekRule CalendarWeekRule { get { if (calendarWeekRule == -1) { calendarWeekRule = _cultureData.CalendarWeekRule; } Debug.Assert(calendarWeekRule != -1, "DateTimeFormatInfo.CalendarWeekRule, calendarWeekRule != -1"); return (CalendarWeekRule)calendarWeekRule; } set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value < CalendarWeekRule.FirstDay || value > CalendarWeekRule.FirstFourDayWeek) { throw new ArgumentOutOfRangeException( nameof(value), value, SR.Format(SR.ArgumentOutOfRange_Range, CalendarWeekRule.FirstDay, CalendarWeekRule.FirstFourDayWeek)); } calendarWeekRule = (int)value; } } public string FullDateTimePattern { get => fullDateTimePattern ??= LongDatePattern + " " + LongTimePattern; set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } fullDateTimePattern = value; } } /// /// For our "patterns" arrays we have 2 variables, a string and a string[] /// The string[] contains the list of patterns, EXCEPT the default may not be included. /// The string contains the default pattern. /// When we initially construct our string[], we set the string to string[0] /// public string LongDatePattern { get => longDatePattern ??= UnclonedLongDatePatterns[0]; // initialize from the 1st array value if not set set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } // Remember the new string longDatePattern = value; // Clear the token hash table ClearTokenHashTable(); // Clean up cached values that will be affected by this property. fullDateTimePattern = null; } } /// /// For our "patterns" arrays we have 2 variables, a string and a string[] /// /// The string[] contains the list of patterns, EXCEPT the default may not be included. /// The string contains the default pattern. /// When we initially construct our string[], we set the string to string[0] /// public string LongTimePattern { get => longTimePattern ??= UnclonedLongTimePatterns[0]; // initialize from the 1st array value if not set set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } // Remember the new string longTimePattern = value; // Clear the token hash table ClearTokenHashTable(); // Clean up cached values that will be affected by this property. fullDateTimePattern = null; // Full date = long date + long Time generalLongTimePattern = null; // General long date = short date + long Time dateTimeOffsetPattern = null; } } /// /// Just to be confusing there's only 1 month day pattern, not a whole list /// public string MonthDayPattern { get { if (monthDayPattern == null) { Debug.Assert(Calendar.ID > 0, "[DateTimeFormatInfo.MonthDayPattern] Expected calID > 0"); monthDayPattern = _cultureData.MonthDay(Calendar.ID); } Debug.Assert(monthDayPattern != null, "DateTimeFormatInfo.MonthDayPattern, monthDayPattern != null"); return monthDayPattern; } set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } monthDayPattern = value; } } public string PMDesignator { get { if (pmDesignator == null) { pmDesignator = _cultureData.PMDesignator; } Debug.Assert(pmDesignator != null, "DateTimeFormatInfo.PMDesignator, pmDesignator != null"); return pmDesignator; } set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } ClearTokenHashTable(); pmDesignator = value; } } public string RFC1123Pattern => rfc1123Pattern; /// /// For our "patterns" arrays we have 2 variables, a string and a string[] /// /// The string[] contains the list of patterns, EXCEPT the default may not be included. /// The string contains the default pattern. /// When we initially construct our string[], we set the string to string[0] /// public string ShortDatePattern { get { return shortDatePattern ??= UnclonedShortDatePatterns[0]; // initialize from the 1st array value if not set } set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } // Remember the new string shortDatePattern = value; // Clear the token hash table, note that even short dates could require this ClearTokenHashTable(); // Clean up cached values that will be affected by this property. generalLongTimePattern = null; // General long time = short date + long time generalShortTimePattern = null; // General short time = short date + short Time dateTimeOffsetPattern = null; } } /// /// For our "patterns" arrays we have 2 variables, a string and a string[] /// /// The string[] contains the list of patterns, EXCEPT the default may not be included. /// The string contains the default pattern. /// When we initially construct our string[], we set the string to string[0] /// public string ShortTimePattern { get => shortTimePattern ??= UnclonedShortTimePatterns[0]; // initialize from the 1st array value if not set set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } // Remember the new string shortTimePattern = value; // Clear the token hash table, note that even short times could require this ClearTokenHashTable(); // Clean up cached values that will be affected by this property. generalShortTimePattern = null; // General short date = short date + short time. } } public string SortableDateTimePattern => sortableDateTimePattern; /// /// Return the pattern for 'g' general format: shortDate + short time /// This is used by DateTimeFormat.cs to get the pattern for 'g'. /// We put this internal property here so that we can avoid doing the /// concatation every time somebody asks for the general format. /// internal string GeneralShortTimePattern => generalShortTimePattern ??= ShortDatePattern + " " + ShortTimePattern; /// /// Return the pattern for 'g' general format: shortDate + Long time. /// We put this internal property here so that we can avoid doing the /// concatation every time somebody asks for the general format. /// internal string GeneralLongTimePattern => generalLongTimePattern ??= ShortDatePattern + " " + LongTimePattern; /// Return the default pattern DateTimeOffset : shortDate + long time + time zone offset. /// This is used by DateTimeFormat.cs to get the pattern for short Date + long time + time zone offset /// We put this internal property here so that we can avoid doing the /// concatation every time somebody uses this form. internal string DateTimeOffsetPattern { get { if (dateTimeOffsetPattern == null) { /* LongTimePattern might contain a "z" as part of the format string in which case we don't want to append a time zone offset */ bool foundZ = false; bool inQuote = false; char quote = '\''; for (int i = 0; !foundZ && i < LongTimePattern.Length; i++) { switch (LongTimePattern[i]) { case 'z': /* if we aren't in a quote, we've found a z */ foundZ = !inQuote; /* we'll fall out of the loop now because the test includes !foundZ */ break; case '\'': case '\"': if (inQuote && (quote == LongTimePattern[i])) { /* we were in a quote and found a matching exit quote, so we are outside a quote now */ inQuote = false; } else if (!inQuote) { quote = LongTimePattern[i]; inQuote = true; } else { /* we were in a quote and saw the other type of quote character, so we are still in a quote */ } break; case '%': case '\\': i++; /* skip next character that is escaped by this backslash */ break; default: break; } } dateTimeOffsetPattern = foundZ ? ShortDatePattern + " " + LongTimePattern : ShortDatePattern + " " + LongTimePattern + " zzz"; } return dateTimeOffsetPattern; } } /// /// Note that cultureData derives this from the long time format (unless someone's set this previously) /// Note that this property is quite undesirable. /// public string TimeSeparator { get { if (timeSeparator == null) { timeSeparator = _cultureData.TimeSeparator; } Debug.Assert(timeSeparator != null, "DateTimeFormatInfo.TimeSeparator, timeSeparator != null"); return timeSeparator; } set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } ClearTokenHashTable(); timeSeparator = value; } } public string UniversalSortableDateTimePattern => universalSortableDateTimePattern; /// /// For our "patterns" arrays we have 2 variables, a string and a string[] /// /// The string[] contains the list of patterns, EXCEPT the default may not be included. /// The string contains the default pattern. /// When we initially construct our string[], we set the string to string[0] /// public string YearMonthPattern { get => yearMonthPattern ??= UnclonedYearMonthPatterns[0]; // initialize from the 1st array value if not set set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } // Remember the new string yearMonthPattern = value; // Clear the token hash table, note that even short times could require this ClearTokenHashTable(); } } /// /// Check if a string array contains a null value, and throw ArgumentNullException with parameter name "value" /// private static void CheckNullValue(string[] values, int length) { Debug.Assert(values != null, "value != null"); Debug.Assert(values.Length >= length); for (int i = 0; i < length; i++) { if (values[i] == null) { throw new ArgumentNullException("value", SR.ArgumentNull_ArrayValue); } } } public string[] AbbreviatedDayNames { get => (string[])InternalGetAbbreviatedDayOfWeekNames().Clone(); set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } if (value.Length != 7) { throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 7), nameof(value)); } CheckNullValue(value, value.Length); ClearTokenHashTable(); abbreviatedDayNames = value; } } /// /// Returns the string array of the one-letter day of week names. /// public string[] ShortestDayNames { get => (string[])InternalGetSuperShortDayNames().Clone(); set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } if (value.Length != 7) { throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 7), nameof(value)); } CheckNullValue(value, value.Length); m_superShortDayNames = value; } } public string[] DayNames { get => (string[])InternalGetDayOfWeekNames().Clone(); set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } if (value.Length != 7) { throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 7), nameof(value)); } CheckNullValue(value, value.Length); ClearTokenHashTable(); dayNames = value; } } public string[] AbbreviatedMonthNames { get => (string[])InternalGetAbbreviatedMonthNames().Clone(); set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } if (value.Length != 13) { throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 13), nameof(value)); } CheckNullValue(value, value.Length - 1); ClearTokenHashTable(); abbreviatedMonthNames = value; } } public string[] MonthNames { get => (string[])InternalGetMonthNames().Clone(); set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } if (value.Length != 13) { throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 13), nameof(value)); } CheckNullValue(value, value.Length - 1); monthNames = value; ClearTokenHashTable(); } } internal bool HasSpacesInMonthNames => (FormatFlags & DateTimeFormatFlags.UseSpacesInMonthNames) != 0; internal bool HasSpacesInDayNames => (FormatFlags & DateTimeFormatFlags.UseSpacesInDayNames) != 0; /// /// Return the month name using the specified MonthNameStyles in either abbreviated form /// or full form. /// internal string InternalGetMonthName(int month, MonthNameStyles style, bool abbreviated) { string[] monthNamesArray = style switch { MonthNameStyles.Genitive => InternalGetGenitiveMonthNames(abbreviated), MonthNameStyles.LeapYear => InternalGetLeapYearMonthNames(), _ => (abbreviated ? InternalGetAbbreviatedMonthNames() : InternalGetMonthNames()), }; // The month range is from 1 ~ m_monthNames.Length // (actually is 13 right now for all cases) if ((month < 1) || (month > monthNamesArray.Length)) { throw new ArgumentOutOfRangeException( nameof(month), month, SR.Format(SR.ArgumentOutOfRange_Range, 1, monthNamesArray.Length)); } return monthNamesArray[month - 1]; } /// /// Retrieve the array which contains the month names in genitive form. /// If this culture does not use the genitive form, the normal month name is returned. /// private string[] InternalGetGenitiveMonthNames(bool abbreviated) { if (abbreviated) { if (m_genitiveAbbreviatedMonthNames == null) { m_genitiveAbbreviatedMonthNames = _cultureData.AbbreviatedGenitiveMonthNames(Calendar.ID); Debug.Assert(m_genitiveAbbreviatedMonthNames.Length == 13, "[DateTimeFormatInfo.GetGenitiveMonthNames] Expected 13 abbreviated genitive month names in a year"); } return m_genitiveAbbreviatedMonthNames; } if (genitiveMonthNames == null) { genitiveMonthNames = _cultureData.GenitiveMonthNames(Calendar.ID); Debug.Assert(genitiveMonthNames.Length == 13, "[DateTimeFormatInfo.GetGenitiveMonthNames] Expected 13 genitive month names in a year"); } return genitiveMonthNames; } /// /// Retrieve the month names used in a leap year. /// If this culture does not have different month names in a leap year, /// the normal month name is returned. /// internal string[] InternalGetLeapYearMonthNames() { if (leapYearMonthNames == null) { Debug.Assert(Calendar.ID > 0, "[DateTimeFormatInfo.InternalGetLeapYearMonthNames] Expected Calendar.ID > 0"); leapYearMonthNames = _cultureData.LeapYearMonthNames(Calendar.ID); Debug.Assert(leapYearMonthNames.Length == 13, "[DateTimeFormatInfo.InternalGetLeapYearMonthNames] Expepcted 13 leap year month names"); } return leapYearMonthNames; } public string GetAbbreviatedDayName(DayOfWeek dayofweek) { if (dayofweek < DayOfWeek.Sunday || dayofweek > DayOfWeek.Saturday) { throw new ArgumentOutOfRangeException( nameof(dayofweek), dayofweek, SR.Format(SR.ArgumentOutOfRange_Range, DayOfWeek.Sunday, DayOfWeek.Saturday)); } // Don't call the public property AbbreviatedDayNames here since a clone is needed in that // property, so it will be slower. Instead, use GetAbbreviatedDayOfWeekNames() directly. return InternalGetAbbreviatedDayOfWeekNames()[(int)dayofweek]; } /// /// Returns the super short day of week names for the specified day of week. /// public string GetShortestDayName(DayOfWeek dayOfWeek) { if (dayOfWeek < DayOfWeek.Sunday || dayOfWeek > DayOfWeek.Saturday) { throw new ArgumentOutOfRangeException( nameof(dayOfWeek), dayOfWeek, SR.Format(SR.ArgumentOutOfRange_Range, DayOfWeek.Sunday, DayOfWeek.Saturday)); } // Don't call the public property SuperShortDayNames here since a clone is needed in that // property, so it will be slower. Instead, use internalGetSuperShortDayNames() directly. return InternalGetSuperShortDayNames()[(int)dayOfWeek]; } /// /// Get all possible combination of inputs /// private static string[] GetCombinedPatterns(string[] patterns1, string[] patterns2, string connectString) { Debug.Assert(patterns1 != null); Debug.Assert(patterns2 != null); // Get array size string[] result = new string[patterns1.Length * patterns2.Length]; // Counter of actual results int k = 0; for (int i = 0; i < patterns1.Length; i++) { for (int j = 0; j < patterns2.Length; j++) { // Can't combine if null or empty result[k++] = patterns1[i] + connectString + patterns2[j]; } } // Return the combinations return result; } public string[] GetAllDateTimePatterns() { List results = new List(DEFAULT_ALL_DATETIMES_SIZE); for (int i = 0; i < DateTimeFormat.allStandardFormats.Length; i++) { string[] strings = GetAllDateTimePatterns(DateTimeFormat.allStandardFormats[i]); for (int j = 0; j < strings.Length; j++) { results.Add(strings[j]); } } return results.ToArray(); } public string[] GetAllDateTimePatterns(char format) { string[] result; switch (format) { case 'd': result = AllShortDatePatterns; break; case 'D': result = AllLongDatePatterns; break; case 'f': result = GetCombinedPatterns(AllLongDatePatterns, AllShortTimePatterns, " "); break; case 'F': case 'U': result = GetCombinedPatterns(AllLongDatePatterns, AllLongTimePatterns, " "); break; case 'g': result = GetCombinedPatterns(AllShortDatePatterns, AllShortTimePatterns, " "); break; case 'G': result = GetCombinedPatterns(AllShortDatePatterns, AllLongTimePatterns, " "); break; case 'm': case 'M': result = new string[] { MonthDayPattern }; break; case 'o': case 'O': result = new string[] { RoundtripFormat }; break; case 'r': case 'R': result = new string[] { rfc1123Pattern }; break; case 's': result = new string[] { sortableDateTimePattern }; break; case 't': result = AllShortTimePatterns; break; case 'T': result = AllLongTimePatterns; break; case 'u': result = new string[] { UniversalSortableDateTimePattern }; break; case 'y': case 'Y': result = AllYearMonthPatterns; break; default: throw new ArgumentException(SR.Format(SR.Format_BadFormatSpecifier, format), nameof(format)); } return result; } public string GetDayName(DayOfWeek dayofweek) { if ((int)dayofweek < 0 || (int)dayofweek > 6) { throw new ArgumentOutOfRangeException( nameof(dayofweek), dayofweek, SR.Format(SR.ArgumentOutOfRange_Range, DayOfWeek.Sunday, DayOfWeek.Saturday)); } // Use the internal one so that we don't clone the array unnecessarily return InternalGetDayOfWeekNames()[(int)dayofweek]; } public string GetAbbreviatedMonthName(int month) { if (month < 1 || month > 13) { throw new ArgumentOutOfRangeException( nameof(month), month, SR.Format(SR.ArgumentOutOfRange_Range, 1, 13)); } // Use the internal one so we don't clone the array unnecessarily return InternalGetAbbreviatedMonthNames()[month - 1]; } public string GetMonthName(int month) { if (month < 1 || month > 13) { throw new ArgumentOutOfRangeException( nameof(month), month, SR.Format(SR.ArgumentOutOfRange_Range, 1, 13)); } // Use the internal one so we don't clone the array unnecessarily return InternalGetMonthNames()[month - 1]; } /// /// For our "patterns" arrays we have 2 variables, a string and a string[] /// /// The string[] contains the list of patterns, EXCEPT the default may not be included. /// The string contains the default pattern. /// When we initially construct our string[], we set the string to string[0] /// /// The resulting [] can get returned to the calling app, so clone it. /// private static string[] GetMergedPatterns(string[] patterns, string defaultPattern) { Debug.Assert(patterns != null && patterns.Length > 0, "[DateTimeFormatInfo.GetMergedPatterns]Expected array of at least one pattern"); Debug.Assert(defaultPattern != null, "[DateTimeFormatInfo.GetMergedPatterns]Expected non null default string"); // If the default happens to be the first in the list just return (a cloned) copy if (defaultPattern == patterns[0]) { return (string[])patterns.Clone(); } // We either need a bigger list, or the pattern from the list. int i; for (i = 0; i < patterns.Length; i++) { // Stop if we found it if (defaultPattern == patterns[i]) break; } // Either way we're going to need a new array string[] newPatterns; // Did we find it if (i < patterns.Length) { // Found it, output will be same size newPatterns = (string[])patterns.Clone(); // Have to move [0] item to [i] so we can re-write default at [0] // (remember defaultPattern == [i] so this is OK) newPatterns[i] = newPatterns[0]; } else { // Not found, make room for it newPatterns = new string[patterns.Length + 1]; // Copy existing array Array.Copy(patterns, 0, newPatterns, 1, patterns.Length); } // Remember the default newPatterns[0] = defaultPattern; // Return the reconstructed list return newPatterns; } // Needed by DateTimeFormatInfo and DateTimeFormat 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 /// merged patterns of both /// private string[] AllYearMonthPatterns => GetMergedPatterns(UnclonedYearMonthPatterns, YearMonthPattern); private string[] AllShortDatePatterns => GetMergedPatterns(UnclonedShortDatePatterns, ShortDatePattern); private string[] AllShortTimePatterns => GetMergedPatterns(UnclonedShortTimePatterns, ShortTimePattern); private string[] AllLongDatePatterns => GetMergedPatterns(UnclonedLongDatePatterns, LongDatePattern); private string[] AllLongTimePatterns => GetMergedPatterns(UnclonedLongTimePatterns, LongTimePattern); /// /// Clone this string array if you want to return it to user. /// Otherwise, you are returning a writable cache copy. /// This won't include default, call AllYearMonthPatterns /// private string[] UnclonedYearMonthPatterns { get { if (allYearMonthPatterns == null) { Debug.Assert(Calendar.ID > 0, "[DateTimeFormatInfo.UnclonedYearMonthPatterns] Expected Calendar.ID > 0"); allYearMonthPatterns = _cultureData.YearMonths(Calendar.ID); Debug.Assert(allYearMonthPatterns.Length > 0, "[DateTimeFormatInfo.UnclonedYearMonthPatterns] Expected some year month patterns"); } return allYearMonthPatterns; } } /// /// Clone this string array if you want to return it to user. /// Otherwise, you are returning a writable cache copy. /// This won't include default, call AllShortDatePatterns /// private string[] UnclonedShortDatePatterns { get { if (allShortDatePatterns == null) { Debug.Assert(Calendar.ID > 0, "[DateTimeFormatInfo.UnclonedShortDatePatterns] Expected Calendar.ID > 0"); allShortDatePatterns = _cultureData.ShortDates(Calendar.ID); Debug.Assert(allShortDatePatterns.Length > 0, "[DateTimeFormatInfo.UnclonedShortDatePatterns] Expected some short date patterns"); } return allShortDatePatterns; } } /// /// Clone this string array if you want to return it to user. /// Otherwise, you are returning a writable cache copy. /// This won't include default, call AllLongDatePatterns /// private string[] UnclonedLongDatePatterns { get { if (allLongDatePatterns == null) { Debug.Assert(Calendar.ID > 0, "[DateTimeFormatInfo.UnclonedLongDatePatterns] Expected Calendar.ID > 0"); allLongDatePatterns = _cultureData.LongDates(Calendar.ID); Debug.Assert(allLongDatePatterns.Length > 0, "[DateTimeFormatInfo.UnclonedLongDatePatterns] Expected some long date patterns"); } return allLongDatePatterns; } } /// /// Clone this string array if you want to return it to user. /// Otherwise, you are returning a writable cache copy. /// This won't include default, call AllShortTimePatterns /// private string[] UnclonedShortTimePatterns { get { if (allShortTimePatterns == null) { allShortTimePatterns = _cultureData.ShortTimes; Debug.Assert(allShortTimePatterns.Length > 0, "[DateTimeFormatInfo.UnclonedShortTimePatterns] Expected some short time patterns"); } return allShortTimePatterns; } } /// /// Clone this string array if you want to return it to user. /// Otherwise, you are returning a writable cache copy. /// This won't include default, call AllLongTimePatterns /// private string[] UnclonedLongTimePatterns { get { if (allLongTimePatterns == null) { allLongTimePatterns = _cultureData.LongTimes; Debug.Assert(allLongTimePatterns.Length > 0, "[DateTimeFormatInfo.UnclonedLongTimePatterns] Expected some long time patterns"); } return allLongTimePatterns; } } public static DateTimeFormatInfo ReadOnly(DateTimeFormatInfo dtfi) { if (dtfi == null) { throw new ArgumentNullException(nameof(dtfi)); } if (dtfi.IsReadOnly) { return dtfi; } DateTimeFormatInfo newInfo = (DateTimeFormatInfo)(dtfi.MemberwiseClone()); // We can use the data member calendar in the setter, instead of the property Calendar, // since the cloned copy should have the same state as the original copy. newInfo.calendar = Calendar.ReadOnly(dtfi.Calendar); newInfo._isReadOnly = true; return newInfo; } public bool IsReadOnly => _isReadOnly; /// /// Return the native name for the calendar in DTFI.Calendar. The native name is referred to /// the culture used to create the DTFI. E.g. in the following example, the native language is Japanese. /// DateTimeFormatInfo dtfi = new CultureInfo("ja-JP", false).DateTimeFormat.Calendar = new JapaneseCalendar(); /// String nativeName = dtfi.NativeCalendarName; // Get the Japanese name for the Japanese calendar. /// DateTimeFormatInfo dtfi = new CultureInfo("ja-JP", false).DateTimeFormat.Calendar = new GregorianCalendar(GregorianCalendarTypes.Localized); /// String nativeName = dtfi.NativeCalendarName; // Get the Japanese name for the Gregorian calendar. /// public string NativeCalendarName => _cultureData.CalendarName(Calendar.ID); /// /// Used by custom cultures and others to set the list of available formats. Note that none of them are /// explicitly used unless someone calls GetAllDateTimePatterns and subsequently uses one of the items /// from the list. /// /// Most of the format characters that can be used in GetAllDateTimePatterns are /// not really needed since they are one of the following: /// /// r/R/s/u locale-independent constants -- cannot be changed! /// m/M/y/Y fields with a single string in them -- that can be set through props directly /// f/F/g/G/U derived fields based on combinations of various of the below formats /// /// NOTE: No special validation is done here beyond what is done when the actual respective fields /// are used (what would be the point of disallowing here what we allow in the appropriate property?) /// /// WARNING: If more validation is ever done in one place, it should be done in the other. /// public void SetAllDateTimePatterns(string[] patterns, char format) { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (patterns == null) { throw new ArgumentNullException(nameof(patterns)); } if (patterns.Length == 0) { throw new ArgumentException(SR.Arg_ArrayZeroError, nameof(patterns)); } for (int i = 0; i < patterns.Length; i++) { if (patterns[i] == null) { throw new ArgumentNullException("patterns[" + i + "]", SR.ArgumentNull_ArrayValue); } } // Remember the patterns, and use the 1st as default switch (format) { case 'd': allShortDatePatterns = patterns; shortDatePattern = allShortDatePatterns[0]; break; case 'D': allLongDatePatterns = patterns; longDatePattern = allLongDatePatterns[0]; break; case 't': allShortTimePatterns = patterns; shortTimePattern = allShortTimePatterns[0]; break; case 'T': allLongTimePatterns = patterns; longTimePattern = allLongTimePatterns[0]; break; case 'y': case 'Y': allYearMonthPatterns = patterns; yearMonthPattern = allYearMonthPatterns[0]; break; default: throw new ArgumentException(SR.Format(SR.Format_BadFormatSpecifier, format), nameof(format)); } // Clear the token hash table, note that even short dates could require this ClearTokenHashTable(); } public string[] AbbreviatedMonthGenitiveNames { get => (string[])InternalGetGenitiveMonthNames(true).Clone(); set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } if (value.Length != 13) { throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 13), nameof(value)); } CheckNullValue(value, value.Length - 1); ClearTokenHashTable(); m_genitiveAbbreviatedMonthNames = value; } } public string[] MonthGenitiveNames { get => (string[])InternalGetGenitiveMonthNames(false).Clone(); set { if (IsReadOnly) { throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); } if (value == null) { throw new ArgumentNullException(nameof(value)); } if (value.Length != 13) { throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 13), nameof(value)); } CheckNullValue(value, value.Length - 1); genitiveMonthNames = value; ClearTokenHashTable(); } } // Decimal separator used by positive TimeSpan pattern private string? _decimalSeparator; internal string DecimalSeparator => _decimalSeparator ??= new NumberFormatInfo(_cultureData.UseUserOverride ? CultureData.GetCultureData(_cultureData.CultureName, false) : _cultureData).NumberDecimalSeparator; // Positive TimeSpan Pattern private string? _fullTimeSpanPositivePattern; internal string FullTimeSpanPositivePattern => _fullTimeSpanPositivePattern ??= "d':'h':'mm':'ss'" + DecimalSeparator + "'FFFFFFF"; // Negative TimeSpan Pattern private string? _fullTimeSpanNegativePattern; internal string FullTimeSpanNegativePattern => _fullTimeSpanNegativePattern ??= "'-'" + FullTimeSpanPositivePattern; // Get suitable CompareInfo from current DTFI object. internal CompareInfo CompareInfo => // We use the regular GetCompareInfo here to make sure the created CompareInfo object is stored in the // CompareInfo cache. otherwise we would just create CompareInfo using _cultureData. _compareInfo ??= CompareInfo.GetCompareInfo(_cultureData.SortName); internal const DateTimeStyles InvalidDateTimeStyles = ~(DateTimeStyles.AllowLeadingWhite | DateTimeStyles.AllowTrailingWhite | DateTimeStyles.AllowInnerWhite | DateTimeStyles.NoCurrentDateDefault | DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeLocal | DateTimeStyles.AssumeUniversal | DateTimeStyles.RoundtripKind); internal static void ValidateStyles(DateTimeStyles style, string parameterName) { if ((style & InvalidDateTimeStyles) != 0) { throw new ArgumentException(SR.Argument_InvalidDateTimeStyles, parameterName); } if (((style & (DateTimeStyles.AssumeLocal)) != 0) && ((style & (DateTimeStyles.AssumeUniversal)) != 0)) { throw new ArgumentException(SR.Argument_ConflictingDateTimeStyles, parameterName); } if (((style & DateTimeStyles.RoundtripKind) != 0) && ((style & (DateTimeStyles.AssumeLocal | DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal)) != 0)) { throw new ArgumentException(SR.Argument_ConflictingDateTimeRoundtripStyles, parameterName); } } /// /// Return the internal flag used in formatting and parsing. /// The flag can be used to indicate things like if genitive forms is used in /// this DTFi, or if leap year gets different month names. /// internal DateTimeFormatFlags FormatFlags => formatFlags != DateTimeFormatFlags.NotInitialized ? formatFlags : InitializeFormatFlags(); [MethodImpl(MethodImplOptions.NoInlining)] private DateTimeFormatFlags InitializeFormatFlags() { // Build the format flags from the data in this DTFI formatFlags = (DateTimeFormatFlags)DateTimeFormatInfoScanner.GetFormatFlagGenitiveMonth( MonthNames, InternalGetGenitiveMonthNames(false), AbbreviatedMonthNames, InternalGetGenitiveMonthNames(true)) | (DateTimeFormatFlags)DateTimeFormatInfoScanner.GetFormatFlagUseSpaceInMonthNames( MonthNames, InternalGetGenitiveMonthNames(false), AbbreviatedMonthNames, InternalGetGenitiveMonthNames(true)) | (DateTimeFormatFlags)DateTimeFormatInfoScanner.GetFormatFlagUseSpaceInDayNames(DayNames, AbbreviatedDayNames) | (DateTimeFormatFlags)DateTimeFormatInfoScanner.GetFormatFlagUseHebrewCalendar((int)Calendar.ID); return formatFlags; } internal bool HasForceTwoDigitYears { get { switch (calendar.ID) { // Handle Japanese and Taiwan cases. // If is y/yy, do not get (year % 100). "y" will print // year without leading zero. "yy" will print year with two-digit in leading zero. // If pattern is yyy/yyyy/..., print year value with two-digit in leading zero. // So year 5 is "05", and year 125 is "125". // The reason for not doing (year % 100) is for Taiwan calendar. // If year 125, then output 125 and not 25. // Note: OS uses "yyyy" for Taiwan calendar by default. case (CalendarId.JAPAN): case (CalendarId.TAIWAN): return true; } return false; } } /// /// Returns whether the YearMonthAdjustment function has any fix-up work to do for this culture/calendar. /// internal bool HasYearMonthAdjustment => (FormatFlags & DateTimeFormatFlags.UseHebrewRule) != 0; /// /// This is a callback that the parser can make back into the DTFI to let it fiddle with special /// cases associated with that culture or calendar. Currently this only has special cases for /// the Hebrew calendar, but this could be extended to other cultures. /// /// The return value is whether the year and month are actually valid for this calendar. /// internal bool YearMonthAdjustment(ref int year, ref int month, bool parsedMonthName) { if ((FormatFlags & DateTimeFormatFlags.UseHebrewRule) != 0) { // Special rules to fix up the Hebrew year/month // When formatting, we only format up to the hundred digit of the Hebrew year, although Hebrew year is now over 5000. // E.g. if the year is 5763, we only format as 763. if (year < 1000) { year += 5000; } // Because we need to calculate leap year, we should fall out now for an invalid year. if (year < Calendar.GetYear(Calendar.MinSupportedDateTime) || year > Calendar.GetYear(Calendar.MaxSupportedDateTime)) { return false; } // To handle leap months, the set of month names in the symbol table does not always correspond to the numbers. // For non-leap years, month 7 (Adar Bet) is not present, so we need to make using this month invalid and // shuffle the other months down. if (parsedMonthName) { if (!Calendar.IsLeapYear(year)) { if (month >= 8) { month--; } else if (month == 7) { return false; } } } } return true; } // DateTimeFormatInfo tokenizer. This is used by DateTime.Parse() to break input string into tokens. private TokenHashValue[]? _dtfiTokenHash; private const int TOKEN_HASH_SIZE = 199; private const int SECOND_PRIME = 197; private const string dateSeparatorOrTimeZoneOffset = "-"; private const string invariantDateSeparator = "/"; private const string invariantTimeSeparator = ":"; // Common Ignorable Symbols internal const string IgnorablePeriod = "."; internal const string IgnorableComma = ","; // Year/Month/Day suffixes internal const string CJKYearSuff = "\u5e74"; internal const string CJKMonthSuff = "\u6708"; internal const string CJKDaySuff = "\u65e5"; internal const string KoreanYearSuff = "\ub144"; internal const string KoreanMonthSuff = "\uc6d4"; internal const string KoreanDaySuff = "\uc77c"; internal const string KoreanHourSuff = "\uc2dc"; internal const string KoreanMinuteSuff = "\ubd84"; internal const string KoreanSecondSuff = "\ucd08"; internal const string CJKHourSuff = "\u6642"; internal const string ChineseHourSuff = "\u65f6"; 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"; internal const string ZuluName = "Z"; internal const string KoreanLangName = "ko"; internal const string JapaneseLangName = "ja"; internal const string EnglishLangName = "en"; private static volatile DateTimeFormatInfo? s_jajpDTFI; private static volatile DateTimeFormatInfo? s_zhtwDTFI; /// /// Create a Japanese DTFI which uses JapaneseCalendar. This is used to parse /// date string with Japanese era name correctly even when the supplied DTFI /// does not use Japanese calendar. /// The created instance is stored in global s_jajpDTFI. /// internal static DateTimeFormatInfo GetJapaneseCalendarDTFI() { DateTimeFormatInfo? temp = s_jajpDTFI; if (temp == null) { temp = new CultureInfo("ja-JP", false).DateTimeFormat; temp.Calendar = JapaneseCalendar.GetDefaultInstance(); s_jajpDTFI = temp; } return temp; } /// /// Create a Taiwan DTFI which uses TaiwanCalendar. This is used to parse /// date string with era name correctly even when the supplied DTFI /// does not use Taiwan calendar. /// The created instance is stored in global s_zhtwDTFI. /// internal static DateTimeFormatInfo GetTaiwanCalendarDTFI() { DateTimeFormatInfo? temp = s_zhtwDTFI; if (temp == null) { temp = new CultureInfo("zh-TW", false).DateTimeFormat; temp.Calendar = TaiwanCalendar.GetDefaultInstance(); s_zhtwDTFI = temp; } return temp; } /// /// DTFI properties should call this when the setter are called. /// private void ClearTokenHashTable() { _dtfiTokenHash = null; formatFlags = DateTimeFormatFlags.NotInitialized; } internal TokenHashValue[] CreateTokenHashTable() { TokenHashValue[]? temp = _dtfiTokenHash; if (temp == null) { temp = new TokenHashValue[TOKEN_HASH_SIZE]; bool koreanLanguage = LanguageName.Equals(KoreanLangName); string sep = TimeSeparator.Trim(); if (IgnorableComma != sep) InsertHash(temp, IgnorableComma, TokenType.IgnorableSymbol, 0); if (IgnorablePeriod != sep) InsertHash(temp, IgnorablePeriod, TokenType.IgnorableSymbol, 0); if (KoreanHourSuff != sep && CJKHourSuff != sep && ChineseHourSuff != sep) { // // On the Macintosh, the default TimeSeparator is identical to the KoreanHourSuff, CJKHourSuff, or ChineseHourSuff for some cultures like // ja-JP and ko-KR. In these cases having the same symbol inserted into the hash table with multiple TokenTypes causes undesirable // DateTime.Parse behavior. For instance, the DateTimeFormatInfo.Tokenize() method might return SEP_DateOrOffset for KoreanHourSuff // instead of SEP_HourSuff. // InsertHash(temp, TimeSeparator, TokenType.SEP_Time, 0); } InsertHash(temp, AMDesignator, TokenType.SEP_Am | TokenType.Am, 0); InsertHash(temp, PMDesignator, TokenType.SEP_Pm | TokenType.Pm, 1); if (LanguageName.Equals("sq")) { // Albanian allows time formats like "12:00.PD" InsertHash(temp, IgnorablePeriod + AMDesignator, TokenType.SEP_Am | TokenType.Am, 0); InsertHash(temp, IgnorablePeriod + PMDesignator, TokenType.SEP_Pm | TokenType.Pm, 1); } // CJK suffix InsertHash(temp, CJKYearSuff, TokenType.SEP_YearSuff, 0); InsertHash(temp, KoreanYearSuff, TokenType.SEP_YearSuff, 0); InsertHash(temp, CJKMonthSuff, TokenType.SEP_MonthSuff, 0); InsertHash(temp, KoreanMonthSuff, TokenType.SEP_MonthSuff, 0); InsertHash(temp, CJKDaySuff, TokenType.SEP_DaySuff, 0); InsertHash(temp, KoreanDaySuff, TokenType.SEP_DaySuff, 0); InsertHash(temp, CJKHourSuff, TokenType.SEP_HourSuff, 0); InsertHash(temp, ChineseHourSuff, TokenType.SEP_HourSuff, 0); InsertHash(temp, CJKMinuteSuff, TokenType.SEP_MinuteSuff, 0); InsertHash(temp, CJKSecondSuff, TokenType.SEP_SecondSuff, 0); if (!LocalAppContextSwitches.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) { // Korean suffix InsertHash(temp, KoreanHourSuff, TokenType.SEP_HourSuff, 0); InsertHash(temp, KoreanMinuteSuff, TokenType.SEP_MinuteSuff, 0); InsertHash(temp, KoreanSecondSuff, TokenType.SEP_SecondSuff, 0); } if (LanguageName.Equals("ky")) { // For some cultures, the date separator works more like a comma, being allowed before or after any date part InsertHash(temp, dateSeparatorOrTimeZoneOffset, TokenType.IgnorableSymbol, 0); } else { InsertHash(temp, dateSeparatorOrTimeZoneOffset, TokenType.SEP_DateOrOffset, 0); } // We need to rescan the date words since we're always synthetic DateTimeFormatInfoScanner scanner = new DateTimeFormatInfoScanner(); string[]? dateWords = scanner.GetDateWordsOfDTFI(this); // Ensure the formatflags is initialized. _ = FormatFlags; // For some cultures, the date separator works more like a comma, being allowed before or after any date part. // In these cultures, we do not use normal date separator since we disallow date separator after a date terminal state. // This is determined in DateTimeFormatInfoScanner. Use this flag to determine if we should treat date separator as ignorable symbol. bool useDateSepAsIgnorableSymbol = false; if (dateWords != null) { // There are DateWords. It could be a real date word (such as "de"), or a monthPostfix. // The monthPostfix starts with '\xfffe' (MonthPostfixChar), followed by the real monthPostfix. for (int i = 0; i < dateWords.Length; i++) { switch (dateWords[i][0]) { // This is a month postfix case DateTimeFormatInfoScanner.MonthPostfixChar: // Get the real month postfix. ReadOnlySpan monthPostfix = dateWords[i].AsSpan(1); // Add the month name + postfix into the token. AddMonthNames(temp, monthPostfix); break; case DateTimeFormatInfoScanner.IgnorableSymbolChar: string symbol = dateWords[i].Substring(1); InsertHash(temp, symbol, TokenType.IgnorableSymbol, 0); if (DateSeparator.Trim(null).Equals(symbol)) { // The date separator is the same as the ignorable symbol. useDateSepAsIgnorableSymbol = true; } break; default: InsertHash(temp, dateWords[i], TokenType.DateWordToken, 0); // TODO: This ignores similar custom cultures if (LanguageName.Equals("eu")) { // Basque has date words with leading dots InsertHash(temp, IgnorablePeriod + dateWords[i], TokenType.DateWordToken, 0); } break; } } } if (!useDateSepAsIgnorableSymbol) { // Use the normal date separator. InsertHash(temp, DateSeparator, TokenType.SEP_Date, 0); } // Add the regular month names. AddMonthNames(temp); // Add the abbreviated month names. for (int i = 1; i <= 13; i++) { InsertHash(temp, GetAbbreviatedMonthName(i), TokenType.MonthToken, i); } if ((FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0) { string [] genitiveMonthNames = InternalGetGenitiveMonthNames(abbreviated: false); string [] abbreviatedGenitiveMonthNames = InternalGetGenitiveMonthNames(abbreviated: true); for (int i = 1; i <= 13; i++) { InsertHash(temp, genitiveMonthNames[i - 1], TokenType.MonthToken, i); InsertHash(temp, abbreviatedGenitiveMonthNames[i - 1], TokenType.MonthToken, i); } } if ((FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0) { for (int i = 1; i <= 13; i++) { string str = InternalGetMonthName(i, MonthNameStyles.LeapYear, false); InsertHash(temp, str, TokenType.MonthToken, i); } } for (int i = 0; i < 7; i++) { // We have to call public methods here to work with inherited DTFI. string str = GetDayName((DayOfWeek)i); InsertHash(temp, str, TokenType.DayOfWeekToken, i); str = GetAbbreviatedDayName((DayOfWeek)i); InsertHash(temp, str, TokenType.DayOfWeekToken, i); } int[] eras = calendar.Eras; for (int i = 1; i <= eras.Length; i++) { InsertHash(temp, GetEraName(i), TokenType.EraToken, i); InsertHash(temp, GetAbbreviatedEraName(i), TokenType.EraToken, i); } if (LanguageName.Equals(JapaneseLangName)) { // Japanese allows day of week forms like: "(Tue)" for (int i = 0; i < 7; i++) { string specialDayOfWeek = "(" + GetAbbreviatedDayName((DayOfWeek)i) + ")"; InsertHash(temp, specialDayOfWeek, TokenType.DayOfWeekToken, i); } if (Calendar.GetType() != typeof(JapaneseCalendar)) { // Special case for Japanese. If this is a Japanese DTFI, and the calendar is not Japanese calendar, // we will check Japanese Era name as well when the calendar is Gregorian. DateTimeFormatInfo jaDtfi = GetJapaneseCalendarDTFI(); for (int i = 1; i <= jaDtfi.Calendar.Eras.Length; i++) { InsertHash(temp, jaDtfi.GetEraName(i), TokenType.JapaneseEraToken, i); InsertHash(temp, jaDtfi.GetAbbreviatedEraName(i), TokenType.JapaneseEraToken, i); // m_abbrevEnglishEraNames[0] contains the name for era 1, so the token value is i+1. InsertHash(temp, jaDtfi.AbbreviatedEnglishEraNames[i - 1], TokenType.JapaneseEraToken, i); } } } // TODO: This prohibits similar custom cultures, but we hard coded the name else if (CultureName.Equals("zh-TW")) { DateTimeFormatInfo twDtfi = GetTaiwanCalendarDTFI(); for (int i = 1; i <= twDtfi.Calendar.Eras.Length; i++) { if (twDtfi.GetEraName(i).Length > 0) { InsertHash(temp, twDtfi.GetEraName(i), TokenType.TEraToken, i); } } } InsertHash(temp, InvariantInfo.AMDesignator, TokenType.SEP_Am | TokenType.Am, 0); InsertHash(temp, InvariantInfo.PMDesignator, TokenType.SEP_Pm | TokenType.Pm, 1); // Add invariant month names and day names. for (int i = 1; i <= 12; i++) { // We have to call public methods here to work with inherited DTFI. // Insert the month name first, so that they are at the front of abbreviated // month names. string str = InvariantInfo.GetMonthName(i); InsertHash(temp, str, TokenType.MonthToken, i); str = InvariantInfo.GetAbbreviatedMonthName(i); InsertHash(temp, str, TokenType.MonthToken, i); } for (int i = 0; i < 7; i++) { // We have to call public methods here to work with inherited DTFI. string str = InvariantInfo.GetDayName((DayOfWeek)i); InsertHash(temp, str, TokenType.DayOfWeekToken, i); str = InvariantInfo.GetAbbreviatedDayName((DayOfWeek)i); InsertHash(temp, str, TokenType.DayOfWeekToken, i); } for (int i = 0; i < AbbreviatedEnglishEraNames.Length; i++) { // m_abbrevEnglishEraNames[0] contains the name for era 1, so the token value is i+1. InsertHash(temp, AbbreviatedEnglishEraNames[i], TokenType.EraToken, i + 1); } InsertHash(temp, LocalTimeMark, TokenType.SEP_LocalTimeMark, 0); InsertHash(temp, GMTName, TokenType.TimeZoneToken, 0); InsertHash(temp, ZuluName, TokenType.TimeZoneToken, 0); InsertHash(temp, invariantDateSeparator, TokenType.SEP_Date, 0); InsertHash(temp, invariantTimeSeparator, TokenType.SEP_Time, 0); _dtfiTokenHash = temp; } return temp; } private void AddMonthNames(TokenHashValue[] temp, ReadOnlySpan monthPostfix = default) { for (int i = 1; i <= 13; i++) { // We have to call public methods here to work with inherited DTFI. // Insert the month name first, so that they are at the front of abbreviated // month names. string str = GetMonthName(i); if (str.Length > 0) { if (!monthPostfix.IsEmpty) { // Insert the month name with the postfix first, so it can be matched first. InsertHash(temp, string.Concat(str, monthPostfix), TokenType.MonthToken, i); } else { InsertHash(temp, str, TokenType.MonthToken, i); } } str = GetAbbreviatedMonthName(i); InsertHash(temp, str, TokenType.MonthToken, i); } } /// /// Try to parse the current word to see if it is a Hebrew number. /// Tokens will be updated accordingly. /// This is called by the Lexer of DateTime.Parse(). /// /// Unlike most of the functions in this class, the return value indicates /// whether or not it started to parse. The badFormat parameter indicates /// if parsing began, but the format was bad. /// private static bool TryParseHebrewNumber( ref __DTString str, out bool badFormat, out int number) { number = -1; badFormat = false; int i = str.Index; if (!HebrewNumber.IsDigit(str.Value[i])) { // If the current character is not a Hebrew digit, just return false. // There is no chance that we can parse a valid Hebrew number from here. return false; } // The current character is a Hebrew digit. Try to parse this word as a Hebrew number. HebrewNumberParsingContext context = new HebrewNumberParsingContext(0); HebrewNumberParsingState state; do { state = HebrewNumber.ParseByChar(str.Value[i++], ref context); switch (state) { case HebrewNumberParsingState.InvalidHebrewNumber: // Not a valid Hebrew number. case HebrewNumberParsingState.NotHebrewDigit: // The current character is not a Hebrew digit character. // Break out so that we don't continue to try parse this as a Hebrew number. return false; } } while (i < str.Value.Length && (state != HebrewNumberParsingState.FoundEndOfHebrewNumber)); // When we are here, we are either at the end of the string, or we find a valid Hebrew number. Debug.Assert(state == HebrewNumberParsingState.ContinueParsing || state == HebrewNumberParsingState.FoundEndOfHebrewNumber, "Invalid returned state from HebrewNumber.ParseByChar()"); if (state != HebrewNumberParsingState.FoundEndOfHebrewNumber) { // We reach end of the string but we can't find a terminal state in parsing Hebrew number. return false; } // We have found a valid Hebrew number. Update the index. str.Advance(i - str.Index); // Get the final Hebrew number value from the HebrewNumberParsingContext. number = context.result; return true; } private static bool IsHebrewChar(char ch) { return ch >= '\x0590' && ch <= '\x05ff'; } [MethodImpl(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 (!LocalAppContextSwitches.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) { tokenType = TokenType.UnknownToken; tokenValue = 0; TokenHashValue value; Debug.Assert(str.Index < str.Value.Length, "DateTimeFormatInfo.Tokenize(): start < value.Length"); char ch = str.m_current; bool isLetter = char.IsLetter(ch); if (isLetter) { ch = Culture.TextInfo.ToLower(ch); if (IsHebrewChar(ch) && TokenMask == TokenType.RegularTokenMask) { bool badFormat; if (TryParseHebrewNumber(ref str, out badFormat, out tokenValue)) { if (badFormat) { tokenType = TokenType.UnknownToken; return false; } // This is a Hebrew number. // Do nothing here. TryParseHebrewNumber() will update token accordingly. tokenType = TokenType.HebrewNumber; return true; } } } int hashcode = ch % TOKEN_HASH_SIZE; int hashProbe = 1 + ch % SECOND_PRIME; int remaining = str.Length - str.Index; int i = 0; TokenHashValue[] hashTable = _dtfiTokenHash ?? CreateTokenHashTable(); do { value = hashTable[hashcode]; if (value == null) { // Not found. break; } // Check this value has the right category (regular token or separator token) that we are looking for. if (((int)value.tokenType & (int)TokenMask) > 0 && value.tokenString.Length <= remaining) { bool compareStrings = true; if (isLetter) { // If this token starts with a letter, make sure that we won't allow partial match. So you can't tokenize "MarchWed" separately. // Also an optimization to avoid string comparison int nextCharIndex = str.Index + value.tokenString.Length; if (nextCharIndex > str.Length) { compareStrings = false; } else if (nextCharIndex < str.Length) { // Check word boundary. The next character should NOT be a letter. char nextCh = str.Value[nextCharIndex]; 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)) { tokenType = value.tokenType & TokenMask; tokenValue = value.tokenValue; str.Advance(value.tokenString.Length); return true; } else if ((value.tokenType == TokenType.MonthToken && HasSpacesInMonthNames) || (value.tokenType == TokenType.DayOfWeekToken && HasSpacesInDayNames)) { // For month or day token, we will match the names which have spaces. int matchStrLen = 0; if (str.MatchSpecifiedWords(value.tokenString, true, ref matchStrLen)) { tokenType = value.tokenType & TokenMask; tokenValue = value.tokenValue; str.Advance(matchStrLen); return true; } } } i++; hashcode += hashProbe; if (hashcode >= TOKEN_HASH_SIZE) hashcode -= TOKEN_HASH_SIZE; } while (i < TOKEN_HASH_SIZE); return false; } private void InsertAtCurrentHashNode(TokenHashValue[] hashTable, string str, char ch, TokenType tokenType, int tokenValue, int pos, int hashcode, int hashProbe) { // Remember the current slot. TokenHashValue previousNode = hashTable[hashcode]; // Insert the new node into the current slot. hashTable[hashcode] = new TokenHashValue(str, tokenType, tokenValue); while (++pos < TOKEN_HASH_SIZE) { hashcode += hashProbe; if (hashcode >= TOKEN_HASH_SIZE) hashcode -= TOKEN_HASH_SIZE; // Remember this slot TokenHashValue temp = hashTable[hashcode]; if (temp != null && Culture.TextInfo.ToLower(temp.tokenString[0]) != ch) { continue; } // Put the previous slot into this slot. hashTable[hashcode] = previousNode; if (temp == null) { // Done return; } previousNode = temp; } Debug.Fail("The hashtable is full. This should not happen."); } private void InsertHash(TokenHashValue[] hashTable, string str, TokenType tokenType, int tokenValue) { // The month of the 13th month is allowed to be null, so make sure that we ignore null value here. if (string.IsNullOrEmpty(str)) { return; } TokenHashValue value; int i = 0; // If there is whitespace characters in the beginning and end of the string, trim them since whitespaces are skipped by // DateTime.Parse(). if (char.IsWhiteSpace(str[0]) || char.IsWhiteSpace(str[^1])) { str = str.Trim(null); // Trim white space characters. // Could have space for separators if (str.Length == 0) { return; } } char ch = Culture.TextInfo.ToLower(str[0]); int hashcode = ch % TOKEN_HASH_SIZE; int hashProbe = 1 + ch % SECOND_PRIME; do { value = hashTable[hashcode]; if (value == null) { hashTable[hashcode] = new TokenHashValue(str, tokenType, tokenValue); return; } else { // Collision happens. Find another slot. if (str.Length >= value.tokenString.Length) { // If there are two tokens with the same prefix, we have to make sure that the longer token should be at the front of // the shorter ones. if (CompareStringIgnoreCaseOptimized(str, 0, value.tokenString.Length, value.tokenString, 0, value.tokenString.Length)) { if (str.Length > value.tokenString.Length) { // The str to be inserted has the same prefix as the current token, and str is longer. // Insert str into this node, and shift every node behind it. InsertAtCurrentHashNode(hashTable, str, ch, tokenType, tokenValue, i, hashcode, hashProbe); return; } else { // Same token. If they have different types (regular token vs separator token). Add them. // If we have the same regular token or separator token in the hash already, do NOT update the hash. // Therefore, the order of inserting token is significant here regarding what tokenType will be kept in the hash. // Check the current value of RegularToken (stored in the lower 8-bit of tokenType) , and insert the tokenType into the hash ONLY when we don't have a RegularToken yet. // Also check the current value of SeparatorToken (stored in the upper 8-bit of token), and insert the tokenType into the hash ONLY when we don't have the SeparatorToken yet. int nTokenType = (int)tokenType; int nCurrentTokenTypeInHash = (int)value.tokenType; // The folowing is the fix for the issue of throwing FormatException when "mar" is passed in string of the short date format dd/MMM/yyyy for es-MX if (((nCurrentTokenTypeInHash & (int)TokenType.RegularTokenMask) == 0) && ((nTokenType & (int)TokenType.RegularTokenMask) != 0) || ((nCurrentTokenTypeInHash & (int)TokenType.SeparatorTokenMask) == 0) && ((nTokenType & (int)TokenType.SeparatorTokenMask) != 0)) { value.tokenType |= tokenType; if (tokenValue != 0) { value.tokenValue = tokenValue; } } // The token to be inserted is already in the table. Skip it. return; } } } } i++; hashcode += hashProbe; if (hashcode >= TOKEN_HASH_SIZE) hashcode -= TOKEN_HASH_SIZE; } while (i < TOKEN_HASH_SIZE); Debug.Fail("The hashtable is full. This should not happen."); } private bool CompareStringIgnoreCaseOptimized(string string1, int offset1, int length1, string string2, int offset2, int length2) { // Optimize for one character cases which are common due to date and time separators (/ and :) if (length1 == 1 && length2 == 1 && string1[offset1] == string2[offset2]) { return true; } return Culture.CompareInfo.Compare(string1, offset1, length1, string2, offset2, length2, CompareOptions.IgnoreCase) == 0; } internal class TokenHashValue { internal string tokenString; internal TokenType tokenType; internal int tokenValue; internal TokenHashValue(string tokenString, TokenType tokenType, int tokenValue) { this.tokenString = tokenString; this.tokenType = tokenType; this.tokenValue = tokenValue; } } } }