From c810eeab2e6c79f4f9d09e7125b194903f4a933e Mon Sep 17 00:00:00 2001 From: Alex Perovich Date: Mon, 27 Mar 2017 11:12:31 -0700 Subject: [PATCH] Add DateTimeFormat and Parse to shared and remove moved file Commit migrated from https://github.com/dotnet/coreclr/commit/6c68f33332e090d0b8cec12aeede82d6c17b1e2a --- .../shared/System.Private.CoreLib.Shared.projitems | 4 + src/coreclr/src/mscorlib/shared/System/DateTime.cs | 15 +- .../shared/System/Globalization/DateTimeFormat.cs | 1211 +++++ .../System/Globalization/DateTimeFormatInfo.cs | 3028 +++++++++++ .../Globalization/DateTimeFormatInfoScanner.cs | 742 +++ .../shared/System/Globalization/DateTimeParse.cs | 5691 ++++++++++++++++++++ 6 files changed, 10684 insertions(+), 7 deletions(-) create mode 100644 src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeFormat.cs create mode 100644 src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeFormatInfo.cs create mode 100644 src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeFormatInfoScanner.cs create mode 100644 src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeParse.cs diff --git a/src/coreclr/src/mscorlib/shared/System.Private.CoreLib.Shared.projitems b/src/coreclr/src/mscorlib/shared/System.Private.CoreLib.Shared.projitems index 98a5827..6e23cc8 100644 --- a/src/coreclr/src/mscorlib/shared/System.Private.CoreLib.Shared.projitems +++ b/src/coreclr/src/mscorlib/shared/System.Private.CoreLib.Shared.projitems @@ -123,6 +123,10 @@ + + + + diff --git a/src/coreclr/src/mscorlib/shared/System/DateTime.cs b/src/coreclr/src/mscorlib/shared/System/DateTime.cs index 1bc5fd9..ddb72da 100644 --- a/src/coreclr/src/mscorlib/shared/System/DateTime.cs +++ b/src/coreclr/src/mscorlib/shared/System/DateTime.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using System; +using System.Diagnostics; +using System.Diagnostics.Contracts; using System.Threading; using System.Globalization; using System.Runtime; @@ -11,7 +13,6 @@ using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Versioning; using System.Security; -using System.Diagnostics.Contracts; using CultureInfo = System.Globalization.CultureInfo; using Calendar = System.Globalization.Calendar; @@ -106,9 +107,9 @@ namespace System private const int DatePartMonth = 2; private const int DatePartDay = 3; - private static readonly int[] DaysToMonth365 = { + private static readonly int[] s_daysToMonth365 = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365}; - private static readonly int[] DaysToMonth366 = { + private static readonly int[] s_daysToMonth366 = { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}; public static readonly DateTime MinValue = new DateTime(MinTicks, DateTimeKind.Unspecified); @@ -559,7 +560,7 @@ namespace System { if (year >= 1 && year <= 9999 && month >= 1 && month <= 12) { - int[] days = IsLeapYear(year) ? DaysToMonth366 : DaysToMonth365; + int[] days = IsLeapYear(year) ? s_daysToMonth366 : s_daysToMonth365; if (day >= 1 && day <= days[month] - days[month - 1]) { int y = year - 1; @@ -591,7 +592,7 @@ namespace System if (month < 1 || month > 12) throw new ArgumentOutOfRangeException(nameof(month), SR.ArgumentOutOfRange_Month); Contract.EndContractBlock(); // IsLeapYear checks the year argument - int[] days = IsLeapYear(year) ? DaysToMonth366 : DaysToMonth365; + int[] days = IsLeapYear(year) ? s_daysToMonth366 : s_daysToMonth365; return days[month] - days[month - 1]; } @@ -845,7 +846,7 @@ namespace System // Leap year calculation looks different from IsLeapYear since y1, y4, // and y100 are relative to year 1, not year 0 bool leapYear = y1 == 3 && (y4 != 24 || y100 == 3); - int[] days = leapYear ? DaysToMonth366 : DaysToMonth365; + int[] days = leapYear ? s_daysToMonth366 : s_daysToMonth365; // All months have less than 32 days, so n >> 5 is a good conservative // estimate for the month int m = (n >> 5) + 1; @@ -1488,7 +1489,7 @@ namespace System { return false; } - int[] days = IsLeapYear(year) ? DaysToMonth366 : DaysToMonth365; + int[] days = IsLeapYear(year) ? s_daysToMonth366 : s_daysToMonth365; if (day < 1 || day > days[month] - days[month - 1]) { return false; diff --git a/src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeFormat.cs b/src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeFormat.cs new file mode 100644 index 0000000..d3bf255 --- /dev/null +++ b/src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeFormat.cs @@ -0,0 +1,1211 @@ +// 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.Text; +using System.Threading; +using System.Globalization; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; +using System.Security; +using System.Diagnostics; +using System.Diagnostics.Contracts; + +namespace System +{ + /* + Customized format patterns: + P.S. Format in the table below is the internal number format used to display the pattern. + + Patterns Format Description Example + ========= ========== ===================================== ======== + "h" "0" hour (12-hour clock)w/o leading zero 3 + "hh" "00" hour (12-hour clock)with leading zero 03 + "hh*" "00" hour (12-hour clock)with leading zero 03 + + "H" "0" hour (24-hour clock)w/o leading zero 8 + "HH" "00" hour (24-hour clock)with leading zero 08 + "HH*" "00" hour (24-hour clock) 08 + + "m" "0" minute w/o leading zero + "mm" "00" minute with leading zero + "mm*" "00" minute with leading zero + + "s" "0" second w/o leading zero + "ss" "00" second with leading zero + "ss*" "00" second with leading zero + + "f" "0" second fraction (1 digit) + "ff" "00" second fraction (2 digit) + "fff" "000" second fraction (3 digit) + "ffff" "0000" second fraction (4 digit) + "fffff" "00000" second fraction (5 digit) + "ffffff" "000000" second fraction (6 digit) + "fffffff" "0000000" second fraction (7 digit) + + "F" "0" second fraction (up to 1 digit) + "FF" "00" second fraction (up to 2 digit) + "FFF" "000" second fraction (up to 3 digit) + "FFFF" "0000" second fraction (up to 4 digit) + "FFFFF" "00000" second fraction (up to 5 digit) + "FFFFFF" "000000" second fraction (up to 6 digit) + "FFFFFFF" "0000000" second fraction (up to 7 digit) + + "t" first character of AM/PM designator A + "tt" AM/PM designator AM + "tt*" AM/PM designator PM + + "d" "0" day w/o leading zero 1 + "dd" "00" day with leading zero 01 + "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 + "yyyy" "D4" year 2000 + "yyyyy" "D5" year 2000 + ... + + "z" "+0;-0" timezone offset w/o leading zero -8 + "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 "" + -DateTimeOffset "zzzzz" e.g -07:30:15 + + "g*" the current era name A.D. + + ":" time separator : -- DEPRECATED - Insert separator directly into pattern (eg: "H.mm.ss") + "/" date separator /-- DEPRECATED - Insert separator directly into pattern (eg: "M-dd-yyyy") + "'" quoted string 'ABC' will insert ABC into the formatted string. + '"' 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. + + 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 + "D" long data culture-specific Sunday, October 31, 1999 + "f" full date (long date + short time) culture-specific Sunday, October 31, 1999 2:00 AM + "F" full date (long date + long time) culture-specific Sunday, October 31, 1999 2:00:00 AM + "g" general date (short date + short time) culture-specific 10/31/1999 2:00 AM + "G" general date (short date + long time) culture-specific 10/31/1999 2:00:00 AM + "m"/"M" Month/Day date culture-specific October 31 +(G) "o"/"O" Round Trip XML "yyyy-MM-ddTHH:mm:ss.fffffffK" 1999-10-31 02:00:00.0000000Z +(G) "r"/"R" RFC 1123 date, "ddd, dd MMM yyyy HH':'mm':'ss 'GMT'" Sun, 31 Oct 1999 10:00:00 GMT +(G) "s" Sortable format, based on ISO 8601. "yyyy-MM-dd'T'HH:mm:ss" 1999-10-31T02:00:00 + ('T' for local time) + "t" short time culture-specific 2:00 AM + "T" long time culture-specific 2:00:00 AM +(G) "u" Universal time with sortable format, "yyyy'-'MM'-'dd HH':'mm':'ss'Z'" 1999-10-31 10:00:00Z + based on ISO 8601. +(U) "U" Universal time with full culture-specific Sunday, October 31, 1999 10:00:00 AM + (long date + long time) format + "y"/"Y" Year/Month day culture-specific October, 1999 + + */ + + //This class contains only static members and does not require the serializable attribute. + internal static + class DateTimeFormat + { + internal const int MaxSecondsFractionDigits = 7; + internal static readonly TimeSpan NullOffset = TimeSpan.MinValue; + + internal static char[] allStandardFormats = + { + 'd', 'D', 'f', 'F', 'g', 'G', + 'm', 'M', 'o', 'O', 'r', 'R', + 's', 't', 'T', 'u', 'U', 'y', 'Y', + }; + + internal const String RoundtripFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK"; + internal const String RoundtripDateTimeUnfixed = "yyyy'-'MM'-'ddTHH':'mm':'ss zzz"; + + private const int DEFAULT_ALL_DATETIMES_SIZE = 132; + + internal static readonly DateTimeFormatInfo InvariantFormatInfo = CultureInfo.InvariantCulture.DateTimeFormat; + internal static readonly string[] InvariantAbbreviatedMonthNames = InvariantFormatInfo.AbbreviatedMonthNames; + internal static readonly string[] InvariantAbbreviatedDayNames = InvariantFormatInfo.AbbreviatedDayNames; + internal const string Gmt = "GMT"; + + internal static String[] fixedNumberFormats = new String[] { + "0", + "00", + "000", + "0000", + "00000", + "000000", + "0000000", + }; + + //////////////////////////////////////////////////////////////////////////// + // + // 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. + // If the digits of the value is greater than len, no leading zero is added. + // + // Notes: + // The function can format to Int32.MaxValue. + // + //////////////////////////////////////////////////////////////////////////// + internal static void FormatDigits(StringBuilder outputBuffer, int value, int len) + { + Debug.Assert(value >= 0, "DateTimeFormat.FormatDigits(): value >= 0"); + FormatDigits(outputBuffer, value, len, false); + } + + internal unsafe static void FormatDigits(StringBuilder outputBuffer, int value, int len, bool overrideLengthLimit) + { + Debug.Assert(value >= 0, "DateTimeFormat.FormatDigits(): value >= 0"); + + // Limit the use of this function to be two-digits, so that we have the same behavior + // as RTM bits. + if (!overrideLengthLimit && len > 2) + { + len = 2; + } + + char* buffer = stackalloc char[16]; + char* p = buffer + 16; + int n = value; + do + { + *--p = (char)(n % 10 + '0'); + n /= 10; + } while ((n != 0) && (p > buffer)); + + int digits = (int)(buffer + 16 - p); + + //If the repeat count is greater than 0, we're trying + //to emulate the "00" format, so we have to prepend + //a zero if the string only has one character. + while ((digits < len) && (p > buffer)) + { + *--p = '0'; + digits++; + } + outputBuffer.Append(p, digits); + } + + private static void HebrewFormatDigits(StringBuilder outputBuffer, int digits) + { + outputBuffer.Append(HebrewNumber.ToString(digits)); + } + + internal static int ParseRepeatPattern(String format, int pos, char patternChar) + { + int len = format.Length; + int index = pos + 1; + while ((index < len) && (format[index] == patternChar)) + { + index++; + } + return (index - pos); + } + + private static String FormatDayOfWeek(int dayOfWeek, int repeat, DateTimeFormatInfo dtfi) + { + Debug.Assert(dayOfWeek >= 0 && dayOfWeek <= 6, "dayOfWeek >= 0 && dayOfWeek <= 6"); + if (repeat == 3) + { + return (dtfi.GetAbbreviatedDayName((DayOfWeek)dayOfWeek)); + } + // Call dtfi.GetDayName() here, instead of accessing DayNames property, because we don't + // want a clone of DayNames, which will hurt perf. + return (dtfi.GetDayName((DayOfWeek)dayOfWeek)); + } + + private static String FormatMonth(int month, int repeatCount, DateTimeFormatInfo dtfi) + { + Debug.Assert(month >= 1 && month <= 12, "month >=1 && month <= 12"); + if (repeatCount == 3) + { + return (dtfi.GetAbbreviatedMonthName(month)); + } + // Call GetMonthName() here, instead of accessing MonthNames property, because we don't + // want a clone of MonthNames, which will hurt perf. + return (dtfi.GetMonthName(month)); + } + + // + // FormatHebrewMonthName + // + // Action: Return the Hebrew month name for the specified DateTime. + // Returns: The month name string for the specified DateTime. + // Arguments: + // time the time to format + // 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: + 1 Hebrew 1st Month + 2 Hebrew 2nd Month + .. ... + 6 Hebrew 6th Month + 7 Hebrew 6th Month II (used only in a leap year) + 8 Hebrew 7th Month + 9 Hebrew 8th Month + 10 Hebrew 9th Month + 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. + */ + private static String FormatHebrewMonthName(DateTime time, int month, int repeatCount, DateTimeFormatInfo dtfi) + { + Debug.Assert(repeatCount != 3 || repeatCount != 4, "repeateCount should be 3 or 4"); + if (dtfi.Calendar.IsLeapYear(dtfi.Calendar.GetYear(time))) + { + // This month is in a leap year + return (dtfi.internalGetMonthName(month, MonthNameStyles.LeapYear, (repeatCount == 3))); + } + // This is in a regular year. + if (month >= 7) + { + month++; + } + if (repeatCount == 3) + { + return (dtfi.GetAbbreviatedMonthName(month)); + } + return (dtfi.GetMonthName(month)); + } + + // + // The pos should point to a quote character. This method will + // append to the result StringBuilder the string encloed by the quote character. + // + internal static int ParseQuoteString(String format, int pos, StringBuilder result) + { + // + // NOTE : pos will be the index of the quote character in the 'format' string. + // + int formatLen = format.Length; + int beginPos = pos; + char quoteChar = format[pos++]; // Get the character used to quote the following string. + + bool foundQuote = false; + while (pos < formatLen) + { + char ch = format[pos++]; + if (ch == quoteChar) + { + foundQuote = true; + break; + } + else if (ch == '\\') + { + // The following are used to support escaped character. + // Escaped character is also supported in the quoted string. + // Therefore, someone can use a format like "'minute:' mm\"" to display: + // minute: 45" + // because the second double quote is escaped. + if (pos < formatLen) + { + result.Append(format[pos++]); + } + else + { + // + // This means that '\' is at the end of the formatting string. + // + throw new FormatException(SR.Format_InvalidString); + } + } + else + { + result.Append(ch); + } + } + + if (!foundQuote) + { + // Here we can't find the matching quote. + throw new FormatException( + String.Format( + CultureInfo.CurrentCulture, + SR.Format_BadQuote, quoteChar)); + } + + // + // Return the character count including the begin/end quote characters and enclosed string. + // + return (pos - beginPos); + } + + // + // Get the next character at the index of 'pos' in the 'format' string. + // Return value of -1 means 'pos' is already at the end of the 'format' string. + // Otherwise, return value is the int value of the next character. + // + internal static int ParseNextChar(String format, int pos) + { + if (pos >= format.Length - 1) + { + return (-1); + } + return ((int)format[pos + 1]); + } + + // + // IsUseGenitiveForm + // + // 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 + // genitive form. Genitive form is not used if there is more than two "d". + // Arguments: + // format The format string to be scanned. + // index Where we should start the scanning. This is generally where "M" starts. + // tokenLen The len of the current pattern character. This indicates how many "M" that we have. + // patternToMatch The pattern that we want to search. This generally uses "d" + // + private static bool IsUseGenitiveForm(String format, int index, int tokenLen, char patternToMatch) + { + int i; + int repeat = 0; + // + // Look back to see if we can find "d" or "ddd" + // + + // Find first "d". + for (i = index - 1; i >= 0 && format[i] != patternToMatch; i--) { /*Do nothing here */ }; + + if (i >= 0) + { + // Find a "d", so look back to see how many "d" that we can find. + while (--i >= 0 && format[i] == patternToMatch) + { + repeat++; + } + // + // repeat == 0 means that we have one (patternToMatch) + // repeat == 1 means that we have two (patternToMatch) + // + if (repeat <= 1) + { + return (true); + } + // Note that we can't just stop here. We may find "ddd" while looking back, and we have to look + // ahead to see if there is "d" or "dd". + } + + // + // If we can't find "d" or "dd" by looking back, try look ahead. + // + + // Find first "d" + for (i = index + tokenLen; i < format.Length && format[i] != patternToMatch; i++) { /* Do nothing here */ }; + + if (i < format.Length) + { + repeat = 0; + // Find a "d", so contine the walk to see how may "d" that we can find. + while (++i < format.Length && format[i] == patternToMatch) + { + repeat++; + } + // + // repeat == 0 means that we have one (patternToMatch) + // repeat == 1 means that we have two (patternToMatch) + // + if (repeat <= 1) + { + return (true); + } + } + return (false); + } + + + // + // FormatCustomized + // + // Actions: Format the DateTime instance using the specified format. + // + private static String FormatCustomized(DateTime dateTime, String format, DateTimeFormatInfo dtfi, TimeSpan offset) + { + Calendar cal = dtfi.Calendar; + StringBuilder result = StringBuilderCache.Acquire(); + // This is a flag to indicate if we are format the dates using Hebrew calendar. + + bool isHebrewCalendar = (cal.ID == CalendarId.HEBREW); + // This is a flag to indicate if we are formating hour/minute/second only. + bool bTimeOnly = true; + + int i = 0; + int tokenLen, hour12; + + while (i < format.Length) + { + char ch = format[i]; + int nextChar; + switch (ch) + { + case 'g': + tokenLen = ParseRepeatPattern(format, i, ch); + result.Append(dtfi.GetEraName(cal.GetEra(dateTime))); + break; + case 'h': + tokenLen = ParseRepeatPattern(format, i, ch); + hour12 = dateTime.Hour % 12; + if (hour12 == 0) + { + hour12 = 12; + } + FormatDigits(result, hour12, tokenLen); + break; + case 'H': + tokenLen = ParseRepeatPattern(format, i, ch); + FormatDigits(result, dateTime.Hour, tokenLen); + break; + case 'm': + tokenLen = ParseRepeatPattern(format, i, ch); + FormatDigits(result, dateTime.Minute, tokenLen); + break; + case 's': + tokenLen = ParseRepeatPattern(format, i, ch); + FormatDigits(result, dateTime.Second, tokenLen); + break; + case 'f': + case 'F': + tokenLen = ParseRepeatPattern(format, i, ch); + if (tokenLen <= MaxSecondsFractionDigits) + { + long fraction = (dateTime.Ticks % Calendar.TicksPerSecond); + fraction = fraction / (long)Math.Pow(10, 7 - tokenLen); + if (ch == 'f') + { + result.Append(((int)fraction).ToString(fixedNumberFormats[tokenLen - 1], CultureInfo.InvariantCulture)); + } + else + { + int effectiveDigits = tokenLen; + while (effectiveDigits > 0) + { + if (fraction % 10 == 0) + { + fraction = fraction / 10; + effectiveDigits--; + } + else + { + break; + } + } + if (effectiveDigits > 0) + { + result.Append(((int)fraction).ToString(fixedNumberFormats[effectiveDigits - 1], CultureInfo.InvariantCulture)); + } + else + { + // No fraction to emit, so see if we should remove decimal also. + if (result.Length > 0 && result[result.Length - 1] == '.') + { + result.Remove(result.Length - 1, 1); + } + } + } + } + else + { + throw new FormatException(SR.Format_InvalidString); + } + break; + case 't': + tokenLen = ParseRepeatPattern(format, i, ch); + if (tokenLen == 1) + { + if (dateTime.Hour < 12) + { + if (dtfi.AMDesignator.Length >= 1) + { + result.Append(dtfi.AMDesignator[0]); + } + } + else + { + if (dtfi.PMDesignator.Length >= 1) + { + result.Append(dtfi.PMDesignator[0]); + } + } + } + else + { + result.Append((dateTime.Hour < 12 ? dtfi.AMDesignator : dtfi.PMDesignator)); + } + break; + case 'd': + // + // tokenLen == 1 : Day of month as digits with no leading zero. + // tokenLen == 2 : Day of month as digits with leading zero for single-digit months. + // tokenLen == 3 : Day of week as a three-leter abbreviation. + // tokenLen >= 4 : Day of week as its full name. + // + tokenLen = ParseRepeatPattern(format, i, ch); + if (tokenLen <= 2) + { + int day = cal.GetDayOfMonth(dateTime); + if (isHebrewCalendar) + { + // For Hebrew calendar, we need to convert numbers to Hebrew text for yyyy, MM, and dd values. + HebrewFormatDigits(result, day); + } + else + { + FormatDigits(result, day, tokenLen); + } + } + else + { + int dayOfWeek = (int)cal.GetDayOfWeek(dateTime); + result.Append(FormatDayOfWeek(dayOfWeek, tokenLen, dtfi)); + } + 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. + // tokenLen >= 4 : Month as its full name. + // + tokenLen = ParseRepeatPattern(format, i, ch); + int month = cal.GetMonth(dateTime); + if (tokenLen <= 2) + { + if (isHebrewCalendar) + { + // For Hebrew calendar, we need to convert numbers to Hebrew text for yyyy, MM, and dd values. + HebrewFormatDigits(result, month); + } + else + { + FormatDigits(result, month, tokenLen); + } + } + else + { + if (isHebrewCalendar) + { + result.Append(FormatHebrewMonthName(dateTime, month, tokenLen, dtfi)); + } + else + { + if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0 && tokenLen >= 4) + { + result.Append( + dtfi.internalGetMonthName( + month, + IsUseGenitiveForm(format, i, tokenLen, 'd') ? MonthNameStyles.Genitive : MonthNameStyles.Regular, + false)); + } + else + { + result.Append(FormatMonth(month, tokenLen, dtfi)); + } + } + } + bTimeOnly = false; + break; + case 'y': + // Notes about OS behavior: + // y: Always print (year % 100). No leading zero. + // yy: Always print (year % 100) with leading zero. + // yyy/yyyy/yyyyy/... : Print year value. No leading zero. + + int year = cal.GetYear(dateTime); + tokenLen = ParseRepeatPattern(format, i, ch); + if (dtfi.HasForceTwoDigitYears) + { + FormatDigits(result, year, tokenLen <= 2 ? tokenLen : 2); + } + else if (cal.ID == CalendarId.HEBREW) + { + HebrewFormatDigits(result, year); + } + else + { + if (tokenLen <= 2) + { + FormatDigits(result, year % 100, tokenLen); + } + else + { + String fmtPattern = "D" + tokenLen.ToString(); + result.Append(year.ToString(fmtPattern, CultureInfo.InvariantCulture)); + } + } + bTimeOnly = false; + break; + case 'z': + tokenLen = ParseRepeatPattern(format, i, ch); + FormatCustomizedTimeZone(dateTime, offset, format, tokenLen, bTimeOnly, result); + break; + case 'K': + tokenLen = 1; + FormatCustomizedRoundripTimeZone(dateTime, offset, result); + break; + case ':': + result.Append(dtfi.TimeSeparator); + tokenLen = 1; + break; + case '/': + result.Append(dtfi.DateSeparator); + tokenLen = 1; + break; + case '\'': + case '\"': + tokenLen = ParseQuoteString(format, i, result); + break; + case '%': + // Optional format character. + // 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. + // Besides, we will not allow "%%" appear in the pattern. + if (nextChar >= 0 && nextChar != (int)'%') + { + result.Append(FormatCustomized(dateTime, ((char)nextChar).ToString(), dtfi, offset)); + tokenLen = 2; + } + else + { + // + // This means that '%' is at the end of the format string or + // "%%" appears in the format string. + // + throw new FormatException(SR.Format_InvalidString); + } + break; + case '\\': + // 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 + // character rule. + // That is, we ask everyone to use single quote or double quote to insert characters, + // then we can remove this character. + // + nextChar = ParseNextChar(format, i); + if (nextChar >= 0) + { + result.Append(((char)nextChar)); + tokenLen = 2; + } + else + { + // + // This means that '\' is at the end of the formatting string. + // + throw new FormatException(SR.Format_InvalidString); + } + break; + default: + // NOTENOTE : we can remove this rule if we enforce the enforced quote + // character rule. + // That is, if we ask everyone to use single quote or double quote to insert characters, + // then we can remove this default block. + result.Append(ch); + tokenLen = 1; + break; + } + i += tokenLen; + } + return StringBuilderCache.GetStringAndRelease(result); + } + + + // output the 'z' famliy of formats, which output a the offset from UTC, e.g. "-07:30" + private static void FormatCustomizedTimeZone(DateTime dateTime, TimeSpan offset, String format, Int32 tokenLen, Boolean timeOnly, StringBuilder result) + { + // See if the instance already has an offset + Boolean dateTimeFormat = (offset == NullOffset); + if (dateTimeFormat) + { + // No offset. The instance is a DateTime and the output should be the local time zone + + if (timeOnly && dateTime.Ticks < Calendar.TicksPerDay) + { + // 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); + } + else if (dateTime.Kind == DateTimeKind.Utc) + { + offset = TimeSpan.Zero; + } + else + { + offset = TimeZoneInfo.GetLocalUtcOffset(dateTime, TimeZoneInfoOptions.NoThrowOnInvalidTime); + } + } + if (offset >= TimeSpan.Zero) + { + result.Append('+'); + } + else + { + result.Append('-'); + // get a positive offset, so that you don't need a separate code path for the negative numbers. + offset = offset.Negate(); + } + + if (tokenLen <= 1) + { + // 'z' format e.g "-7" + result.AppendFormat(CultureInfo.InvariantCulture, "{0:0}", offset.Hours); + } + else + { + // 'zz' or longer format e.g "-07" + result.AppendFormat(CultureInfo.InvariantCulture, "{0:00}", offset.Hours); + if (tokenLen >= 3) + { + // 'zzz*' or longer format e.g "-07:30" + result.AppendFormat(CultureInfo.InvariantCulture, ":{0:00}", offset.Minutes); + } + } + } + + // output the 'K' format, which is for round-tripping the data + 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. + + if (offset == NullOffset) + { + // source is a date time, so behavior depends on the kind. + switch (dateTime.Kind) + { + case DateTimeKind.Local: + // This should output the local offset, e.g. "-07:30" + offset = TimeZoneInfo.GetLocalUtcOffset(dateTime, TimeZoneInfoOptions.NoThrowOnInvalidTime); + // fall through to shared time zone output code + break; + case DateTimeKind.Utc: + // The 'Z' constant is a marker for a UTC date + result.Append("Z"); + return; + default: + // If the kind is unspecified, we output nothing here + return; + } + } + if (offset >= TimeSpan.Zero) + { + result.Append('+'); + } + else + { + result.Append('-'); + // get a positive offset, so that you don't need a separate code path for the negative numbers. + offset = offset.Negate(); + } + + AppendNumber(result, offset.Hours, 2); + result.Append(':'); + AppendNumber(result, offset.Minutes, 2); + } + + + internal static String GetRealFormat(String format, DateTimeFormatInfo dtfi) + { + String realFormat = null; + + switch (format[0]) + { + case 'd': // Short Date + realFormat = dtfi.ShortDatePattern; + break; + case 'D': // Long Date + realFormat = dtfi.LongDatePattern; + break; + case 'f': // Full (long date + short time) + realFormat = dtfi.LongDatePattern + " " + dtfi.ShortTimePattern; + break; + case 'F': // Full (long date + long time) + realFormat = dtfi.FullDateTimePattern; + break; + case 'g': // General (short date + short time) + realFormat = dtfi.GeneralShortTimePattern; + break; + case 'G': // General (short date + long time) + realFormat = dtfi.GeneralLongTimePattern; + break; + case 'm': + case 'M': // Month/Day Date + realFormat = dtfi.MonthDayPattern; + break; + case 'o': + case 'O': + realFormat = RoundtripFormat; + break; + case 'r': + case 'R': // RFC 1123 Standard + realFormat = dtfi.RFC1123Pattern; + break; + case 's': // Sortable without Time Zone Info + realFormat = dtfi.SortableDateTimePattern; + break; + case 't': // Short Time + realFormat = dtfi.ShortTimePattern; + break; + case 'T': // Long Time + realFormat = dtfi.LongTimePattern; + break; + case 'u': // Universal with Sortable format + realFormat = dtfi.UniversalSortableDateTimePattern; + break; + case 'U': // Universal with Full (long date + long time) format + realFormat = dtfi.FullDateTimePattern; + break; + case 'y': + case 'Y': // Year/Month Date + realFormat = dtfi.YearMonthPattern; + break; + default: + throw new FormatException(SR.Format_InvalidString); + } + return (realFormat); + } + + + // Expand a pre-defined format string (like "D" for long date) to the real format that + // we are going to use in the date time parsing. + // This method also convert the dateTime if necessary (e.g. when the format is in Universal time), + // and change dtfi if necessary (e.g. when the format should use invariant culture). + // + private static String ExpandPredefinedFormat(String format, ref DateTime dateTime, ref DateTimeFormatInfo dtfi, ref TimeSpan offset) + { + switch (format[0]) + { + case 'o': + case 'O': // Round trip format + dtfi = DateTimeFormatInfo.InvariantInfo; + break; + case 'r': + case 'R': // RFC 1123 Standard + if (offset != NullOffset) + { + // Convert to UTC invariants mean this will be in range + dateTime = dateTime - offset; + } + else if (dateTime.Kind == DateTimeKind.Local) + { + InvalidFormatForLocal(format, dateTime); + } + dtfi = DateTimeFormatInfo.InvariantInfo; + break; + case 's': // Sortable without Time Zone Info + dtfi = DateTimeFormatInfo.InvariantInfo; + break; + case 'u': // Universal time in sortable format. + if (offset != NullOffset) + { + // Convert to UTC invariants mean this will be in range + dateTime = dateTime - offset; + } + else if (dateTime.Kind == DateTimeKind.Local) + { + InvalidFormatForLocal(format, dateTime); + } + dtfi = DateTimeFormatInfo.InvariantInfo; + break; + case 'U': // Universal time in culture dependent format. + if (offset != NullOffset) + { + // This format is not supported by DateTimeOffset + throw new FormatException(SR.Format_InvalidString); + } + // Universal time is always in Greogrian calendar. + // + // Change the Calendar to be Gregorian Calendar. + // + dtfi = (DateTimeFormatInfo)dtfi.Clone(); + if (dtfi.Calendar.GetType() != typeof(GregorianCalendar)) + { + dtfi.Calendar = GregorianCalendar.GetDefaultInstance(); + } + dateTime = dateTime.ToUniversalTime(); + break; + } + format = GetRealFormat(format, dtfi); + return (format); + } + + internal static String Format(DateTime dateTime, String format, DateTimeFormatInfo dtfi) + { + return Format(dateTime, format, dtfi, NullOffset); + } + + + internal static String Format(DateTime dateTime, String format, DateTimeFormatInfo dtfi, TimeSpan offset) + { + Contract.Requires(dtfi != null); + if (format == null || format.Length == 0) + { + Boolean timeOnlySpecialCase = false; + if (dateTime.Ticks < Calendar.TicksPerDay) + { + // 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 + // 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. + // This will pose a problem when people in VB get the time of day, and use it + // to call ToString(), which will use the general format (short date + long time). + // Since Japanese calendar does not support Gregorian year 0001, an exception will be + // thrown when we try to get the Japanese year for Gregorian year 0001. + // Therefore, the workaround allows them to call ToString() for time of day from a DateTime by + // formatting as ISO 8601 format. + switch (dtfi.Calendar.ID) + { + case CalendarId.JAPAN: + case CalendarId.TAIWAN: + case CalendarId.HIJRI: + case CalendarId.HEBREW: + case CalendarId.JULIAN: + case CalendarId.UMALQURA: + case CalendarId.PERSIAN: + timeOnlySpecialCase = true; + dtfi = DateTimeFormatInfo.InvariantInfo; + break; + } + } + if (offset == NullOffset) + { + // Default DateTime.ToString case. + if (timeOnlySpecialCase) + { + format = "s"; + } + else + { + format = "G"; + } + } + else + { + // Default DateTimeOffset.ToString case. + if (timeOnlySpecialCase) + { + format = RoundtripDateTimeUnfixed; + } + else + { + format = dtfi.DateTimeOffsetPattern; + } + } + } + + if (format.Length == 1) + { + switch (format[0]) + { + case 'O': + case 'o': + return FastFormatRoundtrip(dateTime, offset); + case 'R': + case 'r': + return FastFormatRfc1123(dateTime, offset, dtfi); + } + + format = ExpandPredefinedFormat(format, ref dateTime, ref dtfi, ref offset); + } + + return (FormatCustomized(dateTime, format, dtfi, offset)); + } + + internal static string FastFormatRfc1123(DateTime dateTime, TimeSpan offset, DateTimeFormatInfo dtfi) + { + // ddd, dd MMM yyyy HH:mm:ss GMT + const int Rfc1123FormatLength = 29; + StringBuilder result = StringBuilderCache.Acquire(Rfc1123FormatLength); + + if (offset != NullOffset) + { + // Convert to UTC invariants + dateTime = dateTime - offset; + } + + result.Append(InvariantAbbreviatedDayNames[(int)dateTime.DayOfWeek]); + result.Append(','); + result.Append(' '); + AppendNumber(result, dateTime.Day, 2); + result.Append(' '); + result.Append(InvariantAbbreviatedMonthNames[dateTime.Month - 1]); + result.Append(' '); + AppendNumber(result, dateTime.Year, 4); + result.Append(' '); + AppendHHmmssTimeOfDay(result, dateTime); + result.Append(' '); + result.Append(Gmt); + + return StringBuilderCache.GetStringAndRelease(result); + } + + internal static string FastFormatRoundtrip(DateTime dateTime, TimeSpan offset) + { + // yyyy-MM-ddTHH:mm:ss.fffffffK + const int roundTripFormatLength = 28; + StringBuilder result = StringBuilderCache.Acquire(roundTripFormatLength); + + AppendNumber(result, dateTime.Year, 4); + result.Append('-'); + AppendNumber(result, dateTime.Month, 2); + result.Append('-'); + AppendNumber(result, dateTime.Day, 2); + result.Append('T'); + AppendHHmmssTimeOfDay(result, dateTime); + result.Append('.'); + + long fraction = dateTime.Ticks % TimeSpan.TicksPerSecond; + AppendNumber(result, fraction, 7); + + FormatCustomizedRoundripTimeZone(dateTime, offset, result); + + return StringBuilderCache.GetStringAndRelease(result); + } + + private static void AppendHHmmssTimeOfDay(StringBuilder result, DateTime dateTime) + { + // HH:mm:ss + AppendNumber(result, dateTime.Hour, 2); + result.Append(':'); + AppendNumber(result, dateTime.Minute, 2); + result.Append(':'); + AppendNumber(result, dateTime.Second, 2); + } + + internal static void AppendNumber(StringBuilder builder, long val, int digits) + { + for (int i = 0; i < digits; i++) + { + builder.Append('0'); + } + + int index = 1; + while (val > 0 && index <= digits) + { + builder[builder.Length - index] = (char)('0' + (val % 10)); + val = val / 10; + index++; + } + + Debug.Assert(val == 0, "DateTimeFormat.AppendNumber(): digits less than size of val"); + } + + internal static String[] GetAllDateTimes(DateTime dateTime, char format, DateTimeFormatInfo dtfi) + { + Contract.Requires(dtfi != null); + String[] allFormats = null; + String[] results = null; + + switch (format) + { + case 'd': + case 'D': + case 'f': + case 'F': + case 'g': + case 'G': + case 'm': + case 'M': + case 't': + case 'T': + case 'y': + case 'Y': + allFormats = dtfi.GetAllDateTimePatterns(format); + results = new String[allFormats.Length]; + for (int i = 0; i < allFormats.Length; i++) + { + results[i] = Format(dateTime, allFormats[i], dtfi); + } + break; + case 'U': + DateTime universalTime = dateTime.ToUniversalTime(); + allFormats = dtfi.GetAllDateTimePatterns(format); + results = new String[allFormats.Length]; + for (int i = 0; i < allFormats.Length; i++) + { + results[i] = Format(universalTime, allFormats[i], dtfi); + } + break; + // + // The following ones are special cases because these patterns are read-only in + // DateTimeFormatInfo. + // + case 'r': + case 'R': + case 'o': + case 'O': + case 's': + case 'u': + results = new String[] { Format(dateTime, new String(format, 1), dtfi) }; + break; + default: + throw new FormatException(SR.Format_InvalidString); + } + return (results); + } + + internal static String[] GetAllDateTimes(DateTime dateTime, DateTimeFormatInfo dtfi) + { + List results = new List(DEFAULT_ALL_DATETIMES_SIZE); + + for (int i = 0; i < allStandardFormats.Length; i++) + { + String[] strings = GetAllDateTimes(dateTime, allStandardFormats[i], dtfi); + for (int j = 0; j < strings.Length; j++) + { + results.Add(strings[j]); + } + } + String[] value = new String[results.Count]; + results.CopyTo(0, value, 0, results.Count); + return (value); + } + + // This is a placeholder for an MDA to detect when the user is using a + // local DateTime with a format that will be interpreted as UTC. + internal static void InvalidFormatForLocal(String format, DateTime dateTime) + { + } + } +} diff --git a/src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeFormatInfo.cs b/src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeFormatInfo.cs new file mode 100644 index 0000000..4e24381 --- /dev/null +++ b/src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeFormatInfo.cs @@ -0,0 +1,3028 @@ +// 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.Diagnostics.Contracts; +using System.Runtime.Serialization; + +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 accomodate 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, + } + + + [Serializable] + 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. + [NonSerialized] + private CultureData _cultureData; + + // The culture name used to create this DTFI. + + [OptionalField(VersionAdded = 2)] + private String _name = null; + + // The language name of the culture used to create this DTFI. + [NonSerialized] + private String _langName = null; + + // CompareInfo usually used by the parser. + [NonSerialized] + private CompareInfo _compareInfo = null; + + // Culture matches current DTFI. mainly used for string comparisons during parsing. + [NonSerialized] + private CultureInfo _cultureInfo = null; + + // + // Caches for various properties. + // + + 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; + // added in .NET Framework Release {2.0SP1/3.0SP1/3.5RTM} + private String dateTimeOffsetPattern = null; + + // + // The following are constant values. + // + 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'"; + + // + // The following are affected by calendar settings. + // + private Calendar calendar = null; + + 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; + + [OptionalField(VersionAdded = 3)] + 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 + { + get + { + if (_name == null) + { + _name = _cultureData.CultureName; + } + return (_name); + } + } + + private CultureInfo Culture + { + get + { + if (_cultureInfo == null) + { + _cultureInfo = CultureInfo.GetCultureInfo(this.CultureName); + } + return _cultureInfo; + } + } + + // TODO: This ignores other cultures that might want to do something similar + private String LanguageName + { + get + { + if (_langName == null) + { + _langName = _cultureData.SISO639LANGNAME; + } + return (_langName); + } + } + + //////////////////////////////////////////////////////////////////////////// + // + // Create an array of string which contains the abbreviated day names. + // + //////////////////////////////////////////////////////////////////////////// + + private String[] internalGetAbbreviatedDayOfWeekNames() + { + if (this.abbreviatedDayNames == null) + { + // Get the abbreviated day names for our current calendar + this.abbreviatedDayNames = _cultureData.AbbreviatedDayNames(Calendar.ID); + Debug.Assert(this.abbreviatedDayNames.Length == 7, "[DateTimeFormatInfo.GetAbbreviatedDayOfWeekNames] Expected 7 day names in a week"); + } + return (this.abbreviatedDayNames); + } + + + //////////////////////////////////////////////////////////////////////// + // + // Action: Returns the string array of the one-letter day of week names. + // Returns: + // an array of one-letter day of week names + // Arguments: + // None + // Exceptions: + // None + // + //////////////////////////////////////////////////////////////////////// + + private String[] internalGetSuperShortDayNames() + { + if (this.m_superShortDayNames == null) + { + // Get the super short day names for our current calendar + this.m_superShortDayNames = _cultureData.SuperShortDayNames(Calendar.ID); + Debug.Assert(this.m_superShortDayNames.Length == 7, "[DateTimeFormatInfo.internalGetSuperShortDayNames] Expected 7 day names in a week"); + } + return (this.m_superShortDayNames); + } + + //////////////////////////////////////////////////////////////////////////// + // + // Create an array of string which contains the day names. + // + //////////////////////////////////////////////////////////////////////////// + + private String[] internalGetDayOfWeekNames() + { + if (this.dayNames == null) + { + // Get the day names for our current calendar + this.dayNames = _cultureData.DayNames(Calendar.ID); + Debug.Assert(this.dayNames.Length == 7, "[DateTimeFormatInfo.GetDayOfWeekNames] Expected 7 day names in a week"); + } + return (this.dayNames); + } + + //////////////////////////////////////////////////////////////////////////// + // + // Create an array of string which contains the abbreviated month names. + // + //////////////////////////////////////////////////////////////////////////// + + private String[] internalGetAbbreviatedMonthNames() + { + if (this.abbreviatedMonthNames == null) + { + // Get the month names for our current calendar + this.abbreviatedMonthNames = _cultureData.AbbreviatedMonthNames(Calendar.ID); + Debug.Assert(this.abbreviatedMonthNames.Length == 12 || this.abbreviatedMonthNames.Length == 13, + "[DateTimeFormatInfo.GetAbbreviatedMonthNames] Expected 12 or 13 month names in a year"); + } + return (this.abbreviatedMonthNames); + } + + + //////////////////////////////////////////////////////////////////////////// + // + // Create an array of string which contains the month names. + // + //////////////////////////////////////////////////////////////////////////// + + private String[] internalGetMonthNames() + { + if (this.monthNames == null) + { + // Get the month names for our current calendar + this.monthNames = _cultureData.MonthNames(Calendar.ID); + Debug.Assert(this.monthNames.Length == 12 || this.monthNames.Length == 13, + "[DateTimeFormatInfo.GetMonthNames] Expected 12 or 13 month names in a year"); + } + + return (this.monthNames); + } + + + // + // Invariant DateTimeFormatInfo doesn't have user-overriden values + // Default calendar is gregorian + public DateTimeFormatInfo() + : this(CultureInfo.InvariantCulture.m_cultureData, GregorianCalendar.GetDefaultInstance()) + { + } + + internal DateTimeFormatInfo(CultureData cultureData, Calendar cal) + { + Debug.Assert(cultureData != null); + Debug.Assert(cal != null); + + // Remember our culture + _cultureData = cultureData; + + this.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 (this.firstDayOfWeek == -1) { this.firstDayOfWeek = cultureData.IFIRSTDAYOFWEEK; } + if (this.calendarWeekRule == -1) { this.calendarWeekRule = cultureData.IFIRSTWEEKOFYEAR; } + + if (this.amDesignator == null) { this.amDesignator = cultureData.SAM1159; } + if (this.pmDesignator == null) { this.pmDesignator = cultureData.SPM2359; } + if (this.timeSeparator == null) { this.timeSeparator = cultureData.TimeSeparator; } + if (this.dateSeparator == null) { this.dateSeparator = cultureData.DateSeparator(calendarId); } + + this.allLongTimePatterns = _cultureData.LongTimes; + Debug.Assert(this.allLongTimePatterns.Length > 0, "[DateTimeFormatInfo.Populate] Expected some long time patterns"); + + this.allShortTimePatterns = _cultureData.ShortTimes; + Debug.Assert(this.allShortTimePatterns.Length > 0, "[DateTimeFormatInfo.Populate] Expected some short time patterns"); + + this.allLongDatePatterns = cultureData.LongDates(calendarId); + Debug.Assert(this.allLongDatePatterns.Length > 0, "[DateTimeFormatInfo.Populate] Expected some long date patterns"); + + this.allShortDatePatterns = cultureData.ShortDates(calendarId); + Debug.Assert(this.allShortDatePatterns.Length > 0, "[DateTimeFormatInfo.Populate] Expected some short date patterns"); + + this.allYearMonthPatterns = cultureData.YearMonths(calendarId); + Debug.Assert(this.allYearMonthPatterns.Length > 0, "[DateTimeFormatInfo.Populate] Expected some year month patterns"); + } + + [OptionalField(VersionAdded = 1)] + private bool _useUserOverride; + + // This was synthesized by Whidbey so we knew what words might appear in the middle of a date string + // Now we always synthesize so its not helpful + + internal String[] m_dateWords = null; + + [OnSerializing] + private void OnSerializing(StreamingContext ctx) + { + _name = this.CultureName; // make sure the _name is initialized. + _useUserOverride = _cultureData.UseUserOverride; + + // Important to initialize these fields otherwise we may run into exception when deserializing on Whidbey + // because Whidbey try to initialize some of these fields using calendar data which could be null values + // and then we get exceptions. So we call the accessors to force the caches to get loaded. + Object o; + o = this.LongTimePattern; + o = this.LongDatePattern; + o = this.ShortTimePattern; + o = this.ShortDatePattern; + o = this.YearMonthPattern; + o = this.AllLongTimePatterns; + o = this.AllLongDatePatterns; + o = this.AllShortTimePatterns; + o = this.AllShortDatePatterns; + o = this.AllYearMonthPatterns; + } + + [OnDeserialized] + private void OnDeserialized(StreamingContext ctx) + { + if (_name != null) + { + _cultureData = CultureData.GetCultureData(_name, _useUserOverride); + if (_cultureData == null) + { + throw new CultureNotFoundException("_name", _name, SR.Argument_CultureNotSupported); + } + } + + if (calendar == null) + { + calendar = (Calendar)GregorianCalendar.GetDefaultInstance().Clone(); + calendar.SetReadOnlyState(_isReadOnly); + } + + InitializeOverridableProperties(_cultureData, calendar.ID); + + // + // turn off read only state till we finish initializing all fields and then store read only state after we are done. + // + bool isReadOnly = _isReadOnly; + _isReadOnly = false; + + // If we deserialized defaults ala Whidbey, make sure they're still defaults + // Whidbey's arrays could get a bit mixed up. + if (longDatePattern != null) this.LongDatePattern = longDatePattern; + if (shortDatePattern != null) this.ShortDatePattern = shortDatePattern; + if (yearMonthPattern != null) this.YearMonthPattern = yearMonthPattern; + if (longTimePattern != null) this.LongTimePattern = longTimePattern; + if (shortTimePattern != null) this.ShortTimePattern = shortTimePattern; + + _isReadOnly = isReadOnly; + } + + // Returns a default DateTimeFormatInfo that will be universally + // supported and constant irrespective of the current culture. + // Used by FromString methods. + // + + public static DateTimeFormatInfo InvariantInfo + { + get + { + Contract.Ensures(Contract.Result() != null); + 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. Used by Parse methods. + // + + public static DateTimeFormatInfo CurrentInfo + { + get + { + Contract.Ensures(Contract.Result() != null); + System.Globalization.CultureInfo culture = System.Globalization.CultureInfo.CurrentCulture; + if (!culture.m_isInherited) + { + DateTimeFormatInfo info = culture.dateTimeInfo; + if (info != null) + { + return info; + } + } + return (DateTimeFormatInfo)culture.GetFormat(typeof(DateTimeFormatInfo)); + } + } + + + public static DateTimeFormatInfo GetInstance(IFormatProvider provider) + { + // Fast case for a regular CultureInfo + DateTimeFormatInfo info; + CultureInfo cultureProvider = provider as CultureInfo; + if (cultureProvider != null && !cultureProvider.m_isInherited) + { + return cultureProvider.DateTimeFormat; + } + // Fast case for a DTFI; + info = provider as DateTimeFormatInfo; + if (info != null) + { + return info; + } + // Wasn't cultureInfo or DTFI, do it the slower way + if (provider != null) + { + info = provider.GetFormat(typeof(DateTimeFormatInfo)) as DateTimeFormatInfo; + if (info != null) + { + return info; + } + } + // Couldn't get anything, just use currentInfo as fallback + return CurrentInfo; + } + + + 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)this.Calendar.Clone(); + n._isReadOnly = false; + return n; + } + + + public String AMDesignator + { + get + { + if (this.amDesignator == null) + { + this.amDesignator = _cultureData.SAM1159; + } + Debug.Assert(this.amDesignator != null, "DateTimeFormatInfo.AMDesignator, amDesignator != null"); + return (this.amDesignator); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_String); + } + Contract.EndContractBlock(); + ClearTokenHashTable(); + amDesignator = value; + } + } + + + public Calendar Calendar + { + get + { + Contract.Ensures(Contract.Result() != null); + + Debug.Assert(this.calendar != null, "DateTimeFormatInfo.Calendar: calendar != null"); + return (this.calendar); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), SR.ArgumentNull_Obj); + } + Contract.EndContractBlock(); + if (value == calendar) + { + return; + } + + for (int i = 0; i < this.OptionalCalendars.Length; i++) + { + if (this.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), SR.Argument_InvalidCalendar); + } + } + + private CalendarId[] OptionalCalendars + { + get + { + if (this.optionalCalendars == null) + { + this.optionalCalendars = _cultureData.CalendarIds; + } + return (this.optionalCalendars); + } + } + + /*=================================GetEra========================== + **Action: Get the era value by parsing the name of the era. + **Returns: The era value for the specified era name. + ** -1 if the name of the era is not valid or not supported. + **Arguments: eraName the name of the era. + **Exceptions: None. + ============================================================================*/ + + + public int GetEra(String eraName) + { + if (eraName == null) + { + throw new ArgumentNullException(nameof(eraName), + SR.ArgumentNull_String); + } + Contract.EndContractBlock(); + + // 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 (this.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 (this.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 (CultureInfo.InvariantCulture.CompareInfo.Compare(eraName, m_abbrevEnglishEraNames[i], CompareOptions.IgnoreCase) == 0) + { + return (i + 1); + } + } + return (-1); + } + + + internal String[] EraNames + { + get + { + if (this.m_eraNames == null) + { + this.m_eraNames = _cultureData.EraNames(Calendar.ID); ; + } + return (this.m_eraNames); + } + } + + /*=================================GetEraName========================== + **Action: Get the name of the era for the specified era value. + **Returns: The name of the specified era. + **Arguments: + ** era the era value. + **Exceptions: + ** ArguementException if the era valie is invalid. + ============================================================================*/ + + // 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), SR.ArgumentOutOfRange_InvalidEraValue); + } + + internal String[] AbbreviatedEraNames + { + get + { + if (this.m_abbrevEraNames == null) + { + this.m_abbrevEraNames = _cultureData.AbbrevEraNames(Calendar.ID); + } + return (this.m_abbrevEraNames); + } + } + + // 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), SR.ArgumentOutOfRange_InvalidEraValue); + } + + internal String[] AbbreviatedEnglishEraNames + { + get + { + if (this.m_abbrevEnglishEraNames == null) + { + Debug.Assert(Calendar.ID > 0, "[DateTimeFormatInfo.AbbreviatedEnglishEraNames] Expected Calendar.ID > 0"); + this.m_abbrevEnglishEraNames = _cultureData.AbbreviatedEnglishEraNames(Calendar.ID); + } + return (this.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(this.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), SR.ArgumentNull_String); + } + Contract.EndContractBlock(); + ClearTokenHashTable(); + dateSeparator = value; + } + } + + public DayOfWeek FirstDayOfWeek + { + get + { + if (this.firstDayOfWeek == -1) + { + this.firstDayOfWeek = _cultureData.IFIRSTDAYOFWEEK; + } + Debug.Assert(this.firstDayOfWeek != -1, "DateTimeFormatInfo.FirstDayOfWeek, firstDayOfWeek != -1"); + + return ((DayOfWeek)this.firstDayOfWeek); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value >= DayOfWeek.Sunday && value <= DayOfWeek.Saturday) + { + firstDayOfWeek = (int)value; + } + else + { + throw new ArgumentOutOfRangeException( + nameof(value), SR.Format(SR.ArgumentOutOfRange_Range, + DayOfWeek.Sunday, DayOfWeek.Saturday)); + } + } + } + + public CalendarWeekRule CalendarWeekRule + { + get + { + if (this.calendarWeekRule == -1) + { + this.calendarWeekRule = _cultureData.IFIRSTWEEKOFYEAR; + } + Debug.Assert(this.calendarWeekRule != -1, "DateTimeFormatInfo.CalendarWeekRule, calendarWeekRule != -1"); + return ((CalendarWeekRule)this.calendarWeekRule); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value >= CalendarWeekRule.FirstDay && value <= CalendarWeekRule.FirstFourDayWeek) + { + calendarWeekRule = (int)value; + } + else + { + throw new ArgumentOutOfRangeException( + nameof(value), SR.Format(SR.ArgumentOutOfRange_Range, + CalendarWeekRule.FirstDay, CalendarWeekRule.FirstFourDayWeek)); + } + } + } + + public String FullDateTimePattern + { + get + { + if (fullDateTimePattern == null) + { + fullDateTimePattern = LongDatePattern + " " + LongTimePattern; + } + return (fullDateTimePattern); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_String); + } + Contract.EndContractBlock(); + 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 + { + // Initialize our long date pattern from the 1st array value if not set + if (this.longDatePattern == null) + { + // Initialize our data + this.longDatePattern = this.UnclonedLongDatePatterns[0]; + } + + return this.longDatePattern; + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_String); + } + Contract.EndContractBlock(); + + // Remember the new string + this.longDatePattern = value; + + // Clear the token hash table + ClearTokenHashTable(); + + // Clean up cached values that will be affected by this property. + this.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 + { + // Initialize our long time pattern from the 1st array value if not set + if (this.longTimePattern == null) + { + // Initialize our data + this.longTimePattern = this.UnclonedLongTimePatterns[0]; + } + + return this.longTimePattern; + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_String); + } + Contract.EndContractBlock(); + + // Remember the new string + this.longTimePattern = value; + + // Clear the token hash table + ClearTokenHashTable(); + + // Clean up cached values that will be affected by this property. + this.fullDateTimePattern = null; // Full date = long date + long Time + this.generalLongTimePattern = null; // General long date = short date + long Time + this.dateTimeOffsetPattern = null; + } + } + + + // Note: just to be confusing there's only 1 month day pattern, not a whole list + public String MonthDayPattern + { + get + { + if (this.monthDayPattern == null) + { + Debug.Assert(Calendar.ID > 0, "[DateTimeFormatInfo.MonthDayPattern] Expected calID > 0"); + this.monthDayPattern = _cultureData.MonthDay(Calendar.ID); + } + Debug.Assert(this.monthDayPattern != null, "DateTimeFormatInfo.MonthDayPattern, monthDayPattern != null"); + return (this.monthDayPattern); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_String); + } + Contract.EndContractBlock(); + + this.monthDayPattern = value; + } + } + + + public String PMDesignator + { + get + { + if (this.pmDesignator == null) + { + this.pmDesignator = _cultureData.SPM2359; + } + Debug.Assert(this.pmDesignator != null, "DateTimeFormatInfo.PMDesignator, pmDesignator != null"); + return (this.pmDesignator); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_String); + } + Contract.EndContractBlock(); + ClearTokenHashTable(); + + pmDesignator = value; + } + } + + + public String RFC1123Pattern + { + get + { + return (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 + { + // Initialize our short date pattern from the 1st array value if not set + if (this.shortDatePattern == null) + { + // Initialize our data + this.shortDatePattern = this.UnclonedShortDatePatterns[0]; + } + + return this.shortDatePattern; + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_String); + Contract.EndContractBlock(); + + // Remember the new string + this.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 + { + // Initialize our short time pattern from the 1st array value if not set + if (this.shortTimePattern == null) + { + // Initialize our data + this.shortTimePattern = this.UnclonedShortTimePatterns[0]; + } + return this.shortTimePattern; + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_String); + } + Contract.EndContractBlock(); + + // Remember the new string + this.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 + { + get + { + return (sortableDateTimePattern); + } + } + + /*=================================GeneralShortTimePattern===================== + **Property: Return the pattern for 'g' general format: shortDate + short time + **Note: 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 + { + get + { + if (generalShortTimePattern == null) + { + generalShortTimePattern = ShortDatePattern + " " + ShortTimePattern; + } + return (generalShortTimePattern); + } + } + + /*=================================GeneralLongTimePattern===================== + **Property: Return the pattern for 'g' general format: shortDate + Long time + **Note: 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 GeneralLongTimePattern + { + get + { + if (generalLongTimePattern == null) + { + generalLongTimePattern = ShortDatePattern + " " + LongTimePattern; + } + return (generalLongTimePattern); + } + } + + /*=================================DateTimeOffsetPattern========================== + **Property: Return the default pattern DateTimeOffset : shortDate + long time + time zone offset + **Note: 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 + ==============================================================================*/ + + /*=================================DateTimeOffsetPattern========================== + **Property: Return the default pattern DateTimeOffset : shortDate + long time + time zone offset + **Note: 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) + { + string dateTimePattern = ShortDatePattern + " " + LongTimePattern; + + /* 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; + } + } + + if (!foundZ) + { + dateTimePattern = dateTimePattern + " zzz"; + } + + dateTimeOffsetPattern = dateTimePattern; + } + 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(this.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), SR.ArgumentNull_String); + } + + Contract.EndContractBlock(); + ClearTokenHashTable(); + + timeSeparator = value; + } + } + + public String UniversalSortableDateTimePattern + { + get + { + return (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 + { + // Initialize our year/month pattern from the 1st array value if not set + if (this.yearMonthPattern == null) + { + // Initialize our data + this.yearMonthPattern = this.UnclonedYearMonthPatterns[0]; + } + return this.yearMonthPattern; + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_String); + } + Contract.EndContractBlock(); + + // Remember the new string + this.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 + { + return ((String[])internalGetAbbreviatedDayOfWeekNames().Clone()); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_Array); + } + if (value.Length != 7) + { + throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 7), nameof(value)); + } + Contract.EndContractBlock(); + CheckNullValue(value, value.Length); + ClearTokenHashTable(); + + abbreviatedDayNames = value; + } + } + + // Returns the string array of the one-letter day of week names. + public String[] ShortestDayNames + { + get + { + return ((String[])internalGetSuperShortDayNames().Clone()); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_Array); + } + if (value.Length != 7) + { + throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 7), nameof(value)); + } + Contract.EndContractBlock(); + CheckNullValue(value, value.Length); + this.m_superShortDayNames = value; + } + } + + + public String[] DayNames + { + get + { + return ((String[])internalGetDayOfWeekNames().Clone()); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_Array); + } + if (value.Length != 7) + { + throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 7), nameof(value)); + } + Contract.EndContractBlock(); + CheckNullValue(value, value.Length); + ClearTokenHashTable(); + + dayNames = value; + } + } + + + public String[] AbbreviatedMonthNames + { + get + { + return ((String[])internalGetAbbreviatedMonthNames().Clone()); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_Array); + } + if (value.Length != 13) + { + throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 13), nameof(value)); + } + Contract.EndContractBlock(); + CheckNullValue(value, value.Length - 1); + ClearTokenHashTable(); + abbreviatedMonthNames = value; + } + } + + + public String[] MonthNames + { + get + { + return ((String[])internalGetMonthNames().Clone()); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_Array); + } + if (value.Length != 13) + { + throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 13), nameof(value)); + } + Contract.EndContractBlock(); + CheckNullValue(value, value.Length - 1); + monthNames = value; + ClearTokenHashTable(); + } + } + + // Whitespaces that we allow in the month names. + // U+00a0 is non-breaking space. + private static readonly char[] s_monthSpaces = { ' ', '\u00a0' }; + + internal bool HasSpacesInMonthNames + { + get + { + return (FormatFlags & DateTimeFormatFlags.UseSpacesInMonthNames) != 0; + } + } + + internal bool HasSpacesInDayNames + { + get + { + return (FormatFlags & DateTimeFormatFlags.UseSpacesInDayNames) != 0; + } + } + + + // + // internalGetMonthName + // + // Actions: Return the month name using the specified MonthNameStyles in either abbreviated form + // or full form. + // Arguments: + // month + // style To indicate a form like regular/genitive/month name in a leap year. + // abbreviated When true, return abbreviated form. Otherwise, return a full form. + // Exceptions: + // ArgumentOutOfRangeException When month name is invalid. + // + internal String internalGetMonthName(int month, MonthNameStyles style, bool abbreviated) + { + // + // Right now, style is mutual exclusive, but I make the style to be flag so that + // maybe we can combine flag if there is such a need. + // + String[] monthNamesArray = null; + switch (style) + { + case MonthNameStyles.Genitive: + monthNamesArray = internalGetGenitiveMonthNames(abbreviated); + break; + case MonthNameStyles.LeapYear: + monthNamesArray = internalGetLeapYearMonthNames(/*abbreviated*/); + break; + default: + monthNamesArray = (abbreviated ? internalGetAbbreviatedMonthNames() : internalGetMonthNames()); + break; + } + // The month range is from 1 ~ this.m_monthNames.Length + // (actually is 13 right now for all cases) + if ((month < 1) || (month > monthNamesArray.Length)) + { + throw new ArgumentOutOfRangeException( + nameof(month), SR.Format(SR.ArgumentOutOfRange_Range, + 1, monthNamesArray.Length)); + } + return (monthNamesArray[month - 1]); + } + + // + // internalGetGenitiveMonthNames + // + // Action: Retrieve the array which contains the month names in genitive form. + // If this culture does not use the gentive form, the normal month name is returned. + // Arguments: + // abbreviated When true, return abbreviated form. Otherwise, return a full form. + // + private String[] internalGetGenitiveMonthNames(bool abbreviated) + { + if (abbreviated) + { + if (this.m_genitiveAbbreviatedMonthNames == null) + { + this.m_genitiveAbbreviatedMonthNames = _cultureData.AbbreviatedGenitiveMonthNames(this.Calendar.ID); + Debug.Assert(this.m_genitiveAbbreviatedMonthNames.Length == 13, + "[DateTimeFormatInfo.GetGenitiveMonthNames] Expected 13 abbreviated genitive month names in a year"); + } + return (this.m_genitiveAbbreviatedMonthNames); + } + + if (this.genitiveMonthNames == null) + { + this.genitiveMonthNames = _cultureData.GenitiveMonthNames(this.Calendar.ID); + Debug.Assert(this.genitiveMonthNames.Length == 13, + "[DateTimeFormatInfo.GetGenitiveMonthNames] Expected 13 genitive month names in a year"); + } + return (this.genitiveMonthNames); + } + + // + // internalGetLeapYearMonthNames + // + // Actions: 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. + // Agruments: None. (can use abbreviated later if needed) + // + internal String[] internalGetLeapYearMonthNames(/*bool abbreviated*/) + { + if (this.leapYearMonthNames == null) + { + Debug.Assert(Calendar.ID > 0, "[DateTimeFormatInfo.internalGetLeapYearMonthNames] Expected Calendar.ID > 0"); + this.leapYearMonthNames = _cultureData.LeapYearMonthNames(Calendar.ID); + Debug.Assert(this.leapYearMonthNames.Length == 13, + "[DateTimeFormatInfo.internalGetLeapYearMonthNames] Expepcted 13 leap year month names"); + } + return (leapYearMonthNames); + } + + + public String GetAbbreviatedDayName(DayOfWeek dayofweek) + { + if ((int)dayofweek < 0 || (int)dayofweek > 6) + { + throw new ArgumentOutOfRangeException( + nameof(dayofweek), SR.Format(SR.ArgumentOutOfRange_Range, + DayOfWeek.Sunday, DayOfWeek.Saturday)); + } + Contract.EndContractBlock(); + // + // 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 ((int)dayOfWeek < 0 || (int)dayOfWeek > 6) + { + throw new ArgumentOutOfRangeException( + nameof(dayOfWeek), SR.Format(SR.ArgumentOutOfRange_Range, + DayOfWeek.Sunday, DayOfWeek.Saturday)); + } + Contract.EndContractBlock(); + // + // 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) + { + Contract.Ensures(Contract.Result() != null); + String[] result = null; + + switch (format) + { + case 'd': + result = this.AllShortDatePatterns; + break; + case 'D': + result = this.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 = this.AllShortTimePatterns; + break; + case 'T': + result = this.AllLongTimePatterns; + break; + case 'u': + result = new String[] { UniversalSortableDateTimePattern }; + break; + case 'y': + case 'Y': + result = this.AllYearMonthPatterns; + break; + default: + throw new ArgumentException(SR.Format_BadFormatSpecifier, nameof(format)); + } + return (result); + } + + + public String GetDayName(DayOfWeek dayofweek) + { + if ((int)dayofweek < 0 || (int)dayofweek > 6) + { + throw new ArgumentOutOfRangeException( + nameof(dayofweek), SR.Format(SR.ArgumentOutOfRange_Range, + DayOfWeek.Sunday, DayOfWeek.Saturday)); + } + Contract.EndContractBlock(); + + // 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), SR.Format(SR.ArgumentOutOfRange_Range, + 1, 13)); + } + Contract.EndContractBlock(); + // 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), SR.Format(SR.ArgumentOutOfRange_Range, + 1, 13)); + } + Contract.EndContractBlock(); + // 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 + { + get + { + return GetMergedPatterns(this.UnclonedYearMonthPatterns, this.YearMonthPattern); + } + } + + private String[] AllShortDatePatterns + { + get + { + return GetMergedPatterns(this.UnclonedShortDatePatterns, this.ShortDatePattern); + } + } + + private String[] AllShortTimePatterns + { + get + { + return GetMergedPatterns(this.UnclonedShortTimePatterns, this.ShortTimePattern); + } + } + + private String[] AllLongDatePatterns + { + get + { + return GetMergedPatterns(this.UnclonedLongDatePatterns, this.LongDatePattern); + } + } + + private String[] AllLongTimePatterns + { + get + { + return GetMergedPatterns(this.UnclonedLongTimePatterns, this.LongTimePattern); + } + } + + // NOTE: 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"); + this.allYearMonthPatterns = _cultureData.YearMonths(this.Calendar.ID); + Debug.Assert(this.allYearMonthPatterns.Length > 0, + "[DateTimeFormatInfo.UnclonedYearMonthPatterns] Expected some year month patterns"); + } + + return allYearMonthPatterns; + } + } + + + // NOTE: 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"); + this.allShortDatePatterns = _cultureData.ShortDates(this.Calendar.ID); + Debug.Assert(this.allShortDatePatterns.Length > 0, + "[DateTimeFormatInfo.UnclonedShortDatePatterns] Expected some short date patterns"); + } + + return this.allShortDatePatterns; + } + } + + // NOTE: 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"); + this.allLongDatePatterns = _cultureData.LongDates(this.Calendar.ID); + Debug.Assert(this.allLongDatePatterns.Length > 0, + "[DateTimeFormatInfo.UnclonedLongDatePatterns] Expected some long date patterns"); + } + + return this.allLongDatePatterns; + } + } + + // NOTE: 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 (this.allShortTimePatterns == null) + { + this.allShortTimePatterns = _cultureData.ShortTimes; + Debug.Assert(this.allShortTimePatterns.Length > 0, + "[DateTimeFormatInfo.UnclonedShortTimePatterns] Expected some short time patterns"); + } + + return this.allShortTimePatterns; + } + } + + // NOTE: 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 (this.allLongTimePatterns == null) + { + this.allLongTimePatterns = _cultureData.LongTimes; + Debug.Assert(this.allLongTimePatterns.Length > 0, + "[DateTimeFormatInfo.UnclonedLongTimePatterns] Expected some long time patterns"); + } + + return this.allLongTimePatterns; + } + } + + public static DateTimeFormatInfo ReadOnly(DateTimeFormatInfo dtfi) + { + if (dtfi == null) + { + throw new ArgumentNullException(nameof(dtfi), + SR.ArgumentNull_Obj); + } + Contract.EndContractBlock(); + 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 + { + get + { + return (_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 + { + get + { + return _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), SR.ArgumentNull_Array); + } + + if (patterns.Length == 0) + { + throw new ArgumentException(SR.Arg_ArrayZeroError, nameof(patterns)); + } + + Contract.EndContractBlock(); + + 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_BadFormatSpecifier, nameof(format)); + } + + // Clear the token hash table, note that even short dates could require this + ClearTokenHashTable(); + } + + public String[] AbbreviatedMonthGenitiveNames + { + get + { + return ((String[])internalGetGenitiveMonthNames(true).Clone()); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_Array); + } + if (value.Length != 13) + { + throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 13), nameof(value)); + } + Contract.EndContractBlock(); + CheckNullValue(value, value.Length - 1); + ClearTokenHashTable(); + this.m_genitiveAbbreviatedMonthNames = value; + } + } + + public String[] MonthGenitiveNames + { + get + { + return ((String[])internalGetGenitiveMonthNames(false).Clone()); + } + + set + { + if (IsReadOnly) + throw new InvalidOperationException(SR.InvalidOperation_ReadOnly); + if (value == null) + { + throw new ArgumentNullException(nameof(value), + SR.ArgumentNull_Array); + } + if (value.Length != 13) + { + throw new ArgumentException(SR.Format(SR.Argument_InvalidArrayLength, 13), nameof(value)); + } + Contract.EndContractBlock(); + CheckNullValue(value, value.Length - 1); + genitiveMonthNames = value; + ClearTokenHashTable(); + } + } + + // + // Positive TimeSpan Pattern + // + [NonSerialized] + private string _fullTimeSpanPositivePattern; + internal String FullTimeSpanPositivePattern + { + get + { + if (_fullTimeSpanPositivePattern == null) + { + CultureData cultureDataWithoutUserOverrides; + if (_cultureData.UseUserOverride) + cultureDataWithoutUserOverrides = CultureData.GetCultureData(_cultureData.CultureName, false); + else + cultureDataWithoutUserOverrides = _cultureData; + String decimalSeparator = new NumberFormatInfo(cultureDataWithoutUserOverrides).NumberDecimalSeparator; + + _fullTimeSpanPositivePattern = "d':'h':'mm':'ss'" + decimalSeparator + "'FFFFFFF"; + } + return _fullTimeSpanPositivePattern; + } + } + + // + // Negative TimeSpan Pattern + // + [NonSerialized] + private string _fullTimeSpanNegativePattern; + internal String FullTimeSpanNegativePattern + { + get + { + if (_fullTimeSpanNegativePattern == null) + _fullTimeSpanNegativePattern = "'-'" + FullTimeSpanPositivePattern; + return _fullTimeSpanNegativePattern; + } + } + + // + // Get suitable CompareInfo from current DTFI object. + // + internal CompareInfo CompareInfo + { + get + { + if (_compareInfo == null) + { + // 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.SCOMPAREINFO); + } + + return _compareInfo; + } + } + + + 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); + } + Contract.EndContractBlock(); + if (((style & DateTimeStyles.RoundtripKind) != 0) + && ((style & (DateTimeStyles.AssumeLocal | DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal)) != 0)) + { + throw new ArgumentException(SR.Argument_ConflictingDateTimeRoundtripStyles, parameterName); + } + } + + // + // Actions: 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 + { + get + { + if (formatFlags == DateTimeFormatFlags.NotInitialized) + { + // Build the format flags from the data in this DTFI + formatFlags = DateTimeFormatFlags.None; + formatFlags |= (DateTimeFormatFlags)DateTimeFormatInfoScanner.GetFormatFlagGenitiveMonth( + MonthNames, internalGetGenitiveMonthNames(false), AbbreviatedMonthNames, internalGetGenitiveMonthNames(true)); + formatFlags |= (DateTimeFormatFlags)DateTimeFormatInfoScanner.GetFormatFlagUseSpaceInMonthNames( + MonthNames, internalGetGenitiveMonthNames(false), AbbreviatedMonthNames, internalGetGenitiveMonthNames(true)); + formatFlags |= (DateTimeFormatFlags)DateTimeFormatInfoScanner.GetFormatFlagUseSpaceInDayNames(DayNames, AbbreviatedDayNames); + formatFlags |= (DateTimeFormatFlags)DateTimeFormatInfoScanner.GetFormatFlagUseHebrewCalendar((int)Calendar.ID); + } + return (formatFlags); + } + } + + internal Boolean 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 Boolean HasYearMonthAdjustment + { + get + { + return ((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 Boolean YearMonthAdjustment(ref int year, ref int month, Boolean 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. + // + [NonSerialized] + 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 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 = this.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, this.TimeSeparator, TokenType.SEP_Time, 0); + } + + InsertHash(temp, this.AMDesignator, TokenType.SEP_Am | TokenType.Am, 0); + InsertHash(temp, this.PMDesignator, TokenType.SEP_Pm | TokenType.Pm, 1); + + // TODO: This ignores similar custom cultures + if (LanguageName.Equals("sq")) + { + // Albanian allows time formats like "12:00.PD" + InsertHash(temp, IgnorablePeriod + this.AMDesignator, TokenType.SEP_Am | TokenType.Am, 0); + InsertHash(temp, IgnorablePeriod + this.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); + + // 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); + } + + String[] dateWords = null; + DateTimeFormatInfoScanner scanner = null; + + // We need to rescan the date words since we're always synthetic + scanner = new DateTimeFormatInfoScanner(); + m_dateWords = dateWords = scanner.GetDateWordsOfDTFI(this); + // Ensure the formatflags is initialized. + DateTimeFormatFlags flag = 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; + + String monthPostfix = null; + 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. + monthPostfix = dateWords[i].Substring(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 (this.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, this.DateSeparator, TokenType.SEP_Date, 0); + } + // Add the regular month names. + AddMonthNames(temp, null); + + // Add the abbreviated month names. + for (int i = 1; i <= 13; i++) + { + InsertHash(temp, GetAbbreviatedMonthName(i), TokenType.MonthToken, i); + } + + + if ((FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0) + { + for (int i = 1; i <= 13; i++) + { + String str; + str = internalGetMonthName(i, MonthNameStyles.Genitive, false); + InsertHash(temp, str, TokenType.MonthToken, i); + } + } + + if ((FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0) + { + for (int i = 1; i <= 13; i++) + { + String str; + str = internalGetMonthName(i, MonthNameStyles.LeapYear, false); + InsertHash(temp, str, TokenType.MonthToken, i); + } + } + + for (int i = 0; i < 7; i++) + { + //String str = GetDayOfWeekNames()[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); + } + + // TODO: This ignores other cultures that might want to do something similar + 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 (this.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++) + { + String str; + // 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 abbrevaited + // month names. + 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, String monthPostfix) + { + for (int i = 1; i <= 13; i++) + { + String str; + //str = internalGetMonthName(i, MonthNameStyles.Regular, false); + // 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 abbrevaited + // month names. + str = GetMonthName(i); + if (str.Length > 0) + { + if (monthPostfix != null) + { + // Insert the month name with the postfix first, so it can be matched first. + InsertHash(temp, str + monthPostfix, TokenType.MonthToken, i); + } + else + { + InsertHash(temp, str, TokenType.MonthToken, i); + } + } + str = GetAbbreviatedMonthName(i); + InsertHash(temp, str, TokenType.MonthToken, i); + } + } + + //////////////////////////////////////////////////////////////////////// + // + // Actions: + // 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 Boolean 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'); + } + + 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 = this.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.len - str.Index; + int i = 0; + + TokenHashValue[] hashTable = _dtfiTokenHash; + if (hashTable == null) + { + hashTable = 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.len) + { + compareStrings = false; + } + else if (nextCharIndex < str.len) + { + // Check word boundary. The next character should NOT be a letter. + char nextCh = str.Value[nextCharIndex]; + compareStrings = !(Char.IsLetter(nextCh)); + } + } + if (compareStrings && CompareStringIgnoreCaseOptimized(str.Value, str.Index, value.tokenString.Length, value.tokenString, 0, value.tokenString.Length)) + { + 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]; + + //// Console.WriteLine(" Insert Key: {0} in {1}", str, slotToInsert); + // 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 && this.Culture.TextInfo.ToLower(temp.tokenString[0]) != ch) + { + continue; + } + // Put the previous slot into this slot. + hashTable[hashcode] = previousNode; + //// Console.WriteLine(" Move {0} to slot {1}", previousNode.tokenString, hashcode); + if (temp == null) + { + // Done + return; + } + previousNode = temp; + }; + Debug.Assert(false, "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 (str == null || str.Length == 0) + { + 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[str.Length - 1])) + { + str = str.Trim(null); // Trim white space characters. + // Could have space for separators + if (str.Length == 0) + return; + } + char ch = this.Culture.TextInfo.ToLower(str[0]); + int hashcode = ch % TOKEN_HASH_SIZE; + int hashProbe = 1 + ch % SECOND_PRIME; + do + { + value = hashTable[hashcode]; + if (value == null) + { + //// Console.WriteLine(" Put Key: {0} in {1}", str, hashcode); + 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 (this.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; + } + } + } + } + //// Console.WriteLine(" COLLISION. Old Key: {0}, New Key: {1}", hashTable[hashcode].tokenString, str); + i++; + hashcode += hashProbe; + if (hashcode >= TOKEN_HASH_SIZE) hashcode -= TOKEN_HASH_SIZE; + } while (i < TOKEN_HASH_SIZE); + Debug.Assert(false, "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 (this.Culture.CompareInfo.Compare(string1, offset1, length1, string2, offset2, length2, CompareOptions.IgnoreCase) == 0); + } + + // class DateTimeFormatInfo + + 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; + } + } + } +} diff --git a/src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeFormatInfoScanner.cs b/src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeFormatInfoScanner.cs new file mode 100644 index 0000000..ddf7d7e --- /dev/null +++ b/src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeFormatInfoScanner.cs @@ -0,0 +1,742 @@ +// 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. + +//////////////////////////////////////////////////////////////////////////// +// +// DateTimeFormatInfoScanner +// +// Scan a specified DateTimeFormatInfo to search for data used in DateTime.Parse() +// +// The data includes: +// +// DateWords: such as "de" used in es-ES (Spanish) LongDatePattern. +// Postfix: such as "ta" used in fi-FI after the month name. +// +// This class is shared among mscorlib.dll and sysglobl.dll. +// Use conditional CULTURE_AND_REGIONINFO_BUILDER_ONLY to differentiate between +// methods for mscorlib.dll and sysglobl.dll. +// +//////////////////////////////////////////////////////////////////////////// + +using System; +using System.Globalization; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace System.Globalization +{ + +#if CORECLR + using StringStringDictionary = Dictionary; + using StringList = List; +#else + using StringStringDictionary = LowLevelDictionary; + using StringList = LowLevelList; +#endif + + // + // from LocaleEx.txt header + // + //; IFORMATFLAGS + //; Parsing/formatting flags. + internal enum FORMATFLAGS + { + None = 0x00000000, + UseGenitiveMonth = 0x00000001, + UseLeapYearMonth = 0x00000002, + UseSpacesInMonthNames = 0x00000004, + UseHebrewParsing = 0x00000008, + UseSpacesInDayNames = 0x00000010, // Has spaces or non-breaking space in the day names. + UseDigitPrefixInTokens = 0x00000020, // Has token starting with numbers. + } + + internal enum CalendarId : ushort + { + UNINITIALIZED_VALUE = 0, + GREGORIAN = 1, // Gregorian (localized) calendar + GREGORIAN_US = 2, // Gregorian (U.S.) calendar + JAPAN = 3, // Japanese Emperor Era calendar + /* SSS_WARNINGS_OFF */ + TAIWAN = 4, // Taiwan Era calendar /* SSS_WARNINGS_ON */ + KOREA = 5, // Korean Tangun Era calendar + HIJRI = 6, // Hijri (Arabic Lunar) calendar + THAI = 7, // Thai calendar + HEBREW = 8, // Hebrew (Lunar) calendar + GREGORIAN_ME_FRENCH = 9, // Gregorian Middle East French calendar + GREGORIAN_ARABIC = 10, // Gregorian Arabic calendar + GREGORIAN_XLIT_ENGLISH = 11, // Gregorian Transliterated English calendar + GREGORIAN_XLIT_FRENCH = 12, + // Note that all calendars after this point are MANAGED ONLY for now. + JULIAN = 13, + JAPANESELUNISOLAR = 14, + CHINESELUNISOLAR = 15, + SAKA = 16, // reserved to match Office but not implemented in our code + LUNAR_ETO_CHN = 17, // reserved to match Office but not implemented in our code + LUNAR_ETO_KOR = 18, // reserved to match Office but not implemented in our code + LUNAR_ETO_ROKUYOU = 19, // reserved to match Office but not implemented in our code + KOREANLUNISOLAR = 20, + TAIWANLUNISOLAR = 21, + PERSIAN = 22, + UMALQURA = 23, + LAST_CALENDAR = 23 // Last calendar ID + } + + internal class DateTimeFormatInfoScanner + { + // Special prefix-like flag char in DateWord array. + + // Use char in PUA area since we won't be using them in real data. + // The char used to tell a read date word or a month postfix. A month postfix + // is "ta" in the long date pattern like "d. MMMM'ta 'yyyy" for fi-FI. + // In this case, it will be stored as "\xfffeta" in the date word array. + internal const char MonthPostfixChar = '\xe000'; + + // Add ignorable symbol in a DateWord array. + + // hu-HU has: + // shrot date pattern: yyyy. MM. dd.;yyyy-MM-dd;yy-MM-dd + // long date pattern: yyyy. MMMM d. + // Here, "." is the date separator (derived from short date pattern). However, + // "." also appear at the end of long date pattern. In this case, we just + // "." as ignorable symbol so that the DateTime.Parse() state machine will not + // treat the additional date separator at the end of y,m,d pattern as an error + // condition. + internal const char IgnorableSymbolChar = '\xe001'; + + // Known CJK suffix + 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"; + + // The collection fo date words & postfix. + internal StringList m_dateWords = new StringList(); + // Hashtable for the known words. + private static volatile StringStringDictionary s_knownWords; + + static StringStringDictionary KnownWords + { + get + { + if (s_knownWords == null) + { + StringStringDictionary temp = new StringStringDictionary(); + // Add known words into the hash table. + + // Skip these special symbols. + temp.Add("/", String.Empty); + temp.Add("-", String.Empty); + temp.Add(".", String.Empty); + // Skip known CJK suffixes. + temp.Add(CJKYearSuff, String.Empty); + temp.Add(CJKMonthSuff, String.Empty); + temp.Add(CJKDaySuff, String.Empty); + temp.Add(KoreanYearSuff, String.Empty); + temp.Add(KoreanMonthSuff, String.Empty); + temp.Add(KoreanDaySuff, String.Empty); + temp.Add(KoreanHourSuff, String.Empty); + temp.Add(KoreanMinuteSuff, String.Empty); + temp.Add(KoreanSecondSuff, String.Empty); + temp.Add(CJKHourSuff, String.Empty); + temp.Add(ChineseHourSuff, String.Empty); + temp.Add(CJKMinuteSuff, String.Empty); + temp.Add(CJKSecondSuff, String.Empty); + + s_knownWords = temp; + } + return (s_knownWords); + } + } + + //////////////////////////////////////////////////////////////////////////// + // + // Parameters: + // pattern: The pattern to be scanned. + // currentIndex: the current index to start the scan. + // + // Returns: + // Return the index with the first character that is a letter, which will + // be the start of a date word. + // Note that the index can be pattern.Length if we reach the end of the string. + // + //////////////////////////////////////////////////////////////////////////// + internal static int SkipWhiteSpacesAndNonLetter(String pattern, int currentIndex) + { + while (currentIndex < pattern.Length) + { + char ch = pattern[currentIndex]; + if (ch == '\\') + { + // Escaped character. Look ahead one character. + currentIndex++; + if (currentIndex < pattern.Length) + { + ch = pattern[currentIndex]; + if (ch == '\'') + { + // Skip the leading single quote. We will + // stop at the first letter. + continue; + } + // Fall thru to check if this is a letter. + } + else + { + // End of string + break; + } + } + if (Char.IsLetter(ch) || ch == '\'' || ch == '.') + { + break; + } + // Skip the current char since it is not a letter. + currentIndex++; + } + return (currentIndex); + } + + //////////////////////////////////////////////////////////////////////////// + // + // A helper to add the found date word or month postfix into ArrayList for date words. + // + // Parameters: + // formatPostfix: What kind of postfix this is. + // Possible values: + // null: This is a regular date word + // "MMMM": month postfix + // word: The date word or postfix to be added. + // + //////////////////////////////////////////////////////////////////////////// + internal void AddDateWordOrPostfix(String formatPostfix, String str) + { + if (str.Length > 0) + { + // Some cultures use . like an abbreviation + if (str.Equals(".")) + { + AddIgnorableSymbols("."); + return; + } + String words; + if (KnownWords.TryGetValue(str, out words) == false) + { + if (m_dateWords == null) + { + m_dateWords = new StringList(); + } + if (formatPostfix == "MMMM") + { + // Add the word into the ArrayList as "\xfffe" + real month postfix. + String temp = MonthPostfixChar + str; + if (!m_dateWords.Contains(temp)) + { + m_dateWords.Add(temp); + } + } + else + { + if (!m_dateWords.Contains(str)) + { + m_dateWords.Add(str); + } + if (str[str.Length - 1] == '.') + { + // Old version ignore the trialing dot in the date words. Support this as well. + String strWithoutDot = str.Substring(0, str.Length - 1); + if (!m_dateWords.Contains(strWithoutDot)) + { + m_dateWords.Add(strWithoutDot); + } + } + } + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // + // Scan the pattern from the specified index and add the date word/postfix + // when appropriate. + // + // Parameters: + // pattern: The pattern to be scanned. + // index: The starting index to be scanned. + // formatPostfix: The kind of postfix to be scanned. + // Possible values: + // null: This is a regular date word + // "MMMM": month postfix + // + // + //////////////////////////////////////////////////////////////////////////// + internal int AddDateWords(String pattern, int index, String formatPostfix) + { + // Skip any whitespaces so we will start from a letter. + int newIndex = SkipWhiteSpacesAndNonLetter(pattern, index); + if (newIndex != index && formatPostfix != null) + { + // There are whitespaces. This will not be a postfix. + formatPostfix = null; + } + index = newIndex; + + // This is the first char added into dateWord. + // Skip all non-letter character. We will add the first letter into DateWord. + StringBuilder dateWord = new StringBuilder(); + // We assume that date words should start with a letter. + // Skip anything until we see a letter. + + while (index < pattern.Length) + { + char ch = pattern[index]; + if (ch == '\'') + { + // We have seen the end of quote. Add the word if we do not see it before, + // and break the while loop. + AddDateWordOrPostfix(formatPostfix, dateWord.ToString()); + index++; + break; + } + else if (ch == '\\') + { + // + // Escaped character. Look ahead one character + // + + // Skip escaped backslash. + index++; + if (index < pattern.Length) + { + dateWord.Append(pattern[index]); + index++; + } + } + else if (Char.IsWhiteSpace(ch)) + { + // Found a whitespace. We have to add the current date word/postfix. + AddDateWordOrPostfix(formatPostfix, dateWord.ToString()); + if (formatPostfix != null) + { + // Done with postfix. The rest will be regular date word. + formatPostfix = null; + } + // Reset the dateWord. + dateWord.Length = 0; + index++; + } + else + { + dateWord.Append(ch); + index++; + } + } + return (index); + } + + //////////////////////////////////////////////////////////////////////////// + // + // A simple helper to find the repeat count for a specified char. + // + //////////////////////////////////////////////////////////////////////////// + internal static int ScanRepeatChar(String pattern, char ch, int index, out int count) + { + count = 1; + while (++index < pattern.Length && pattern[index] == ch) + { + count++; + } + // Return the updated position. + return (index); + } + + //////////////////////////////////////////////////////////////////////////// + // + // Add the text that is a date separator but is treated like ignroable symbol. + // E.g. + // hu-HU has: + // shrot date pattern: yyyy. MM. dd.;yyyy-MM-dd;yy-MM-dd + // long date pattern: yyyy. MMMM d. + // Here, "." is the date separator (derived from short date pattern). However, + // "." also appear at the end of long date pattern. In this case, we just + // "." as ignorable symbol so that the DateTime.Parse() state machine will not + // treat the additional date separator at the end of y,m,d pattern as an error + // condition. + // + //////////////////////////////////////////////////////////////////////////// + + internal void AddIgnorableSymbols(String text) + { + if (m_dateWords == null) + { + // Create the date word array. + m_dateWords = new StringList(); + } + // Add the ignorable symbol into the ArrayList. + String temp = IgnorableSymbolChar + text; + if (!m_dateWords.Contains(temp)) + { + m_dateWords.Add(temp); + } + } + + + // + // Flag used to trace the date patterns (yy/yyyyy/M/MM/MMM/MMM/d/dd) that we have seen. + // + private enum FoundDatePattern + { + None = 0x0000, + FoundYearPatternFlag = 0x0001, + FoundMonthPatternFlag = 0x0002, + FoundDayPatternFlag = 0x0004, + FoundYMDPatternFlag = 0x0007, // FoundYearPatternFlag | FoundMonthPatternFlag | FoundDayPatternFlag; + } + + // Check if we have found all of the year/month/day pattern. + private FoundDatePattern _ymdFlags = FoundDatePattern.None; + + + //////////////////////////////////////////////////////////////////////////// + // + // Given a date format pattern, scan for date word or postfix. + // + // A date word should be always put in a single quoted string. And it will + // start from a letter, so whitespace and symbols will be ignored before + // the first letter. + // + // Examples of date word: + // 'de' in es-SP: dddd, dd' de 'MMMM' de 'yyyy + // "\x0443." in bg-BG: dd.M.yyyy '\x0433.' + // + // Example of postfix: + // month postfix: + // "ta" in fi-FI: d. MMMM'ta 'yyyy + // Currently, only month postfix is supported. + // + // Usage: + // Always call this with Framework-style pattern, instead of Windows style pattern. + // Windows style pattern uses '' for single quote, while .NET uses \' + // + //////////////////////////////////////////////////////////////////////////// + internal void ScanDateWord(String pattern) + { + // Check if we have found all of the year/month/day pattern. + _ymdFlags = FoundDatePattern.None; + + int i = 0; + while (i < pattern.Length) + { + char ch = pattern[i]; + int chCount; + + switch (ch) + { + case '\'': + // Find a beginning quote. Search until the end quote. + i = AddDateWords(pattern, i + 1, null); + break; + case 'M': + i = ScanRepeatChar(pattern, 'M', i, out chCount); + if (chCount >= 4) + { + if (i < pattern.Length && pattern[i] == '\'') + { + i = AddDateWords(pattern, i + 1, "MMMM"); + } + } + _ymdFlags |= FoundDatePattern.FoundMonthPatternFlag; + break; + case 'y': + i = ScanRepeatChar(pattern, 'y', i, out chCount); + _ymdFlags |= FoundDatePattern.FoundYearPatternFlag; + break; + case 'd': + i = ScanRepeatChar(pattern, 'd', i, out chCount); + if (chCount <= 2) + { + // Only count "d" & "dd". + // ddd, dddd are day names. Do not count them. + _ymdFlags |= FoundDatePattern.FoundDayPatternFlag; + } + break; + case '\\': + // Found a escaped char not in a quoted string. Skip the current backslash + // and its next character. + i += 2; + break; + case '.': + if (_ymdFlags == FoundDatePattern.FoundYMDPatternFlag) + { + // If we find a dot immediately after the we have seen all of the y, m, d pattern. + // treat it as a ignroable symbol. Check for comments in AddIgnorableSymbols for + // more details. + AddIgnorableSymbols("."); + _ymdFlags = FoundDatePattern.None; + } + i++; + break; + default: + if (_ymdFlags == FoundDatePattern.FoundYMDPatternFlag && !Char.IsWhiteSpace(ch)) + { + // We are not seeing "." after YMD. Clear the flag. + _ymdFlags = FoundDatePattern.None; + } + // We are not in quote. Skip the current character. + i++; + break; + } + } + } + + //////////////////////////////////////////////////////////////////////////// + // + // Given a DTFI, get all of the date words from date patterns and time patterns. + // + //////////////////////////////////////////////////////////////////////////// + + internal String[] GetDateWordsOfDTFI(DateTimeFormatInfo dtfi) + { + // Enumarate all LongDatePatterns, and get the DateWords and scan for month postfix. + String[] datePatterns = dtfi.GetAllDateTimePatterns('D'); + int i; + + // Scan the long date patterns + for (i = 0; i < datePatterns.Length; i++) + { + ScanDateWord(datePatterns[i]); + } + + // Scan the short date patterns + datePatterns = dtfi.GetAllDateTimePatterns('d'); + for (i = 0; i < datePatterns.Length; i++) + { + ScanDateWord(datePatterns[i]); + } + // Scan the YearMonth patterns. + datePatterns = dtfi.GetAllDateTimePatterns('y'); + for (i = 0; i < datePatterns.Length; i++) + { + ScanDateWord(datePatterns[i]); + } + + // Scan the month/day pattern + ScanDateWord(dtfi.MonthDayPattern); + + // Scan the long time patterns. + datePatterns = dtfi.GetAllDateTimePatterns('T'); + for (i = 0; i < datePatterns.Length; i++) + { + ScanDateWord(datePatterns[i]); + } + + // Scan the short time patterns. + datePatterns = dtfi.GetAllDateTimePatterns('t'); + for (i = 0; i < datePatterns.Length; i++) + { + ScanDateWord(datePatterns[i]); + } + + String[] result = null; + if (m_dateWords != null && m_dateWords.Count > 0) + { + result = new String[m_dateWords.Count]; + for (i = 0; i < m_dateWords.Count; i++) + { + result[i] = m_dateWords[i]; + } + } + return (result); + } + + + //////////////////////////////////////////////////////////////////////////// + // + // Scan the month names to see if genitive month names are used, and return + // the format flag. + // + //////////////////////////////////////////////////////////////////////////// + internal static FORMATFLAGS GetFormatFlagGenitiveMonth(String[] monthNames, String[] genitveMonthNames, String[] abbrevMonthNames, String[] genetiveAbbrevMonthNames) + { + // If we have different names in regular and genitive month names, use genitive month flag. + return ((!EqualStringArrays(monthNames, genitveMonthNames) || !EqualStringArrays(abbrevMonthNames, genetiveAbbrevMonthNames)) + ? FORMATFLAGS.UseGenitiveMonth : 0); + } + + //////////////////////////////////////////////////////////////////////////// + // + // Scan the month names to see if spaces are used or start with a digit, and return the format flag + // + //////////////////////////////////////////////////////////////////////////// + internal static FORMATFLAGS GetFormatFlagUseSpaceInMonthNames(String[] monthNames, String[] genitveMonthNames, String[] abbrevMonthNames, String[] genetiveAbbrevMonthNames) + { + FORMATFLAGS formatFlags = 0; + formatFlags |= (ArrayElementsBeginWithDigit(monthNames) || + ArrayElementsBeginWithDigit(genitveMonthNames) || + ArrayElementsBeginWithDigit(abbrevMonthNames) || + ArrayElementsBeginWithDigit(genetiveAbbrevMonthNames) + ? FORMATFLAGS.UseDigitPrefixInTokens : 0); + + formatFlags |= (ArrayElementsHaveSpace(monthNames) || + ArrayElementsHaveSpace(genitveMonthNames) || + ArrayElementsHaveSpace(abbrevMonthNames) || + ArrayElementsHaveSpace(genetiveAbbrevMonthNames) + ? FORMATFLAGS.UseSpacesInMonthNames : 0); + return (formatFlags); + } + + //////////////////////////////////////////////////////////////////////////// + // + // Scan the day names and set the correct format flag. + // + //////////////////////////////////////////////////////////////////////////// + internal static FORMATFLAGS GetFormatFlagUseSpaceInDayNames(String[] dayNames, String[] abbrevDayNames) + { + return ((ArrayElementsHaveSpace(dayNames) || + ArrayElementsHaveSpace(abbrevDayNames)) + ? FORMATFLAGS.UseSpacesInDayNames : 0); + } + + //////////////////////////////////////////////////////////////////////////// + // + // Check the calendar to see if it is HebrewCalendar and set the Hebrew format flag if necessary. + // + //////////////////////////////////////////////////////////////////////////// + internal static FORMATFLAGS GetFormatFlagUseHebrewCalendar(int calID) + { + return (calID == (int)CalendarId.HEBREW ? + FORMATFLAGS.UseHebrewParsing | FORMATFLAGS.UseLeapYearMonth : 0); + } + + + //----------------------------------------------------------------------------- + // EqualStringArrays + // compares two string arrays and return true if all elements of the first + // array equals to all elmentsof the second array. + // otherwise it returns false. + //----------------------------------------------------------------------------- + + private static bool EqualStringArrays(string[] array1, string[] array2) + { + // Shortcut if they're the same array + if (array1 == array2) + { + return true; + } + + // This is effectively impossible + if (array1.Length != array2.Length) + { + return false; + } + + // Check each string + for (int i = 0; i < array1.Length; i++) + { + if (!array1[i].Equals(array2[i])) + { + return false; + } + } + + return true; + } + + //----------------------------------------------------------------------------- + // ArrayElementsHaveSpace + // It checks all input array elements if any of them has space character + // returns true if found space character in one of the array elements. + // otherwise returns false. + //----------------------------------------------------------------------------- + + private static bool ArrayElementsHaveSpace(string[] array) + { + for (int i = 0; i < array.Length; i++) + { + // it is faster to check for space character manually instead of calling IndexOf + // so we don't have to go to native code side. + for (int j = 0; j < array[i].Length; j++) + { + if (Char.IsWhiteSpace(array[i][j])) + { + return true; + } + } + } + + return false; + } + + + //////////////////////////////////////////////////////////////////////////// + // + // Check if any element of the array start with a digit. + // + //////////////////////////////////////////////////////////////////////////// + private static bool ArrayElementsBeginWithDigit(string[] array) + { + for (int i = 0; i < array.Length; i++) + { + // it is faster to check for space character manually instead of calling IndexOf + // so we don't have to go to native code side. + if (array[i].Length > 0 && + array[i][0] >= '0' && array[i][0] <= '9') + { + int index = 1; + while (index < array[i].Length && array[i][index] >= '0' && array[i][index] <= '9') + { + // Skip other digits. + index++; + } + if (index == array[i].Length) + { + return (false); + } + + if (index == array[i].Length - 1) + { + // Skip known CJK month suffix. + // CJK uses month name like "1\x6708", since \x6708 is a known month suffix, + // we don't need the UseDigitPrefixInTokens since it is slower. + switch (array[i][index]) + { + case '\x6708': // CJKMonthSuff + case '\xc6d4': // KoreanMonthSuff + return (false); + } + } + + if (index == array[i].Length - 4) + { + // Skip known CJK month suffix. + // Starting with Windows 8, the CJK months for some cultures looks like: "1' \x6708'" + // instead of just "1\x6708" + if (array[i][index] == '\'' && array[i][index + 1] == ' ' && + array[i][index + 2] == '\x6708' && array[i][index + 3] == '\'') + { + return (false); + } + } + return (true); + } + } + + return false; + } + } +} + diff --git a/src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeParse.cs b/src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeParse.cs new file mode 100644 index 0000000..857ffc6 --- /dev/null +++ b/src/coreclr/src/mscorlib/shared/System/Globalization/DateTimeParse.cs @@ -0,0 +1,5691 @@ +// 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. + +//////////////////////////////////////////////////////////////////////////// +// +// +// Purpose: This class is called by DateTime to parse a date/time string. +// +//////////////////////////////////////////////////////////////////////////// + +namespace System +{ + using System; + using System.Text; + using System.Globalization; + using System.Threading; + using System.Collections; + using System.Runtime.CompilerServices; + using System.Runtime.InteropServices; + using System.Runtime.Versioning; + using System.Security; + using System.Diagnostics; + using System.Diagnostics.Contracts; + + //////////////////////////////////////////////////////////////////////// + + //This class contains only static members + + internal static + class DateTimeParse + { + internal const Int32 MaxDateTimeNumberDigits = 8; + + internal delegate bool MatchNumberDelegate(ref __DTString str, int digitLen, out int result); + + internal static MatchNumberDelegate m_hebrewNumberParser = new MatchNumberDelegate(DateTimeParse.MatchHebrewDigits); + + internal static DateTime ParseExact(String s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style) + { + DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result. + result.Init(); + if (TryParseExact(s, format, dtfi, style, ref result)) + { + return result.parsedDate; + } + else + { + throw GetDateTimeParseException(ref result); + } + } + + internal static DateTime ParseExact(String s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style, out TimeSpan offset) + { + DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result. + offset = TimeSpan.Zero; + result.Init(); + result.flags |= ParseFlags.CaptureOffset; + if (TryParseExact(s, format, dtfi, style, ref result)) + { + offset = result.timeZoneOffset; + return result.parsedDate; + } + else + { + throw GetDateTimeParseException(ref result); + } + } + + internal static bool TryParseExact(String s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result) + { + result = DateTime.MinValue; + DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result. + resultData.Init(); + if (TryParseExact(s, format, dtfi, style, ref resultData)) + { + result = resultData.parsedDate; + return true; + } + return false; + } + + internal static bool TryParseExact(String s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result, out TimeSpan offset) + { + result = DateTime.MinValue; + offset = TimeSpan.Zero; + DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result. + resultData.Init(); + resultData.flags |= ParseFlags.CaptureOffset; + if (TryParseExact(s, format, dtfi, style, ref resultData)) + { + result = resultData.parsedDate; + offset = resultData.timeZoneOffset; + return true; + } + return false; + } + + internal static bool TryParseExact(String s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style, ref DateTimeResult result) + { + if (s == null) + { + result.SetFailure(ParseFailureKind.ArgumentNull, "ArgumentNull_String", null, nameof(s)); + return false; + } + if (format == null) + { + result.SetFailure(ParseFailureKind.ArgumentNull, "ArgumentNull_String", null, nameof(format)); + return false; + } + if (s.Length == 0) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + if (format.Length == 0) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadFormatSpecifier", null); + return false; + } + + Debug.Assert(dtfi != null, "dtfi == null"); + + return DoStrictParse(s, format, style, dtfi, ref result); + } + + internal static DateTime ParseExactMultiple(String s, String[] formats, + DateTimeFormatInfo dtfi, DateTimeStyles style) + { + DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result. + result.Init(); + if (TryParseExactMultiple(s, formats, dtfi, style, ref result)) + { + return result.parsedDate; + } + else + { + throw GetDateTimeParseException(ref result); + } + } + + + internal static DateTime ParseExactMultiple(String s, String[] formats, + DateTimeFormatInfo dtfi, DateTimeStyles style, out TimeSpan offset) + { + DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result. + offset = TimeSpan.Zero; + result.Init(); + result.flags |= ParseFlags.CaptureOffset; + if (TryParseExactMultiple(s, formats, dtfi, style, ref result)) + { + offset = result.timeZoneOffset; + return result.parsedDate; + } + else + { + throw GetDateTimeParseException(ref result); + } + } + + internal static bool TryParseExactMultiple(String s, String[] formats, + DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result, out TimeSpan offset) + { + result = DateTime.MinValue; + offset = TimeSpan.Zero; + DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result. + resultData.Init(); + resultData.flags |= ParseFlags.CaptureOffset; + if (TryParseExactMultiple(s, formats, dtfi, style, ref resultData)) + { + result = resultData.parsedDate; + offset = resultData.timeZoneOffset; + return true; + } + return false; + } + + + internal static bool TryParseExactMultiple(String s, String[] formats, + DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result) + { + result = DateTime.MinValue; + DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result. + resultData.Init(); + if (TryParseExactMultiple(s, formats, dtfi, style, ref resultData)) + { + result = resultData.parsedDate; + return true; + } + return false; + } + + internal static bool TryParseExactMultiple(String s, String[] formats, + DateTimeFormatInfo dtfi, DateTimeStyles style, ref DateTimeResult result) + { + if (s == null) + { + result.SetFailure(ParseFailureKind.ArgumentNull, "ArgumentNull_String", null, nameof(s)); + return false; + } + if (formats == null) + { + result.SetFailure(ParseFailureKind.ArgumentNull, "ArgumentNull_String", null, nameof(formats)); + return false; + } + + if (s.Length == 0) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + if (formats.Length == 0) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadFormatSpecifier", null); + return false; + } + + Debug.Assert(dtfi != null, "dtfi == null"); + + // + // Do a loop through the provided formats and see if we can parse succesfully in + // one of the formats. + // + for (int i = 0; i < formats.Length; i++) + { + if (formats[i] == null || formats[i].Length == 0) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadFormatSpecifier", null); + return false; + } + // Create a new result each time to ensure the runs are independent. Carry through + // flags from the caller and return the result. + DateTimeResult innerResult = new DateTimeResult(); // The buffer to store the parsing result. + innerResult.Init(); + innerResult.flags = result.flags; + if (TryParseExact(s, formats[i], dtfi, style, ref innerResult)) + { + result.parsedDate = innerResult.parsedDate; + result.timeZoneOffset = innerResult.timeZoneOffset; + return (true); + } + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + + //////////////////////////////////////////////////////////////////////////// + // Date Token Types + // + // Following is the set of tokens that can be generated from a date + // string. Notice that the legal set of trailing separators have been + // folded in with the date number, and month name tokens. This set + // of tokens is chosen to reduce the number of date parse states. + // + //////////////////////////////////////////////////////////////////////////// + + internal enum DTT : int + { + End = 0, // '\0' + NumEnd = 1, // Num[ ]*[\0] + NumAmpm = 2, // Num[ ]+AmPm + NumSpace = 3, // Num[ ]+^[Dsep|Tsep|'0\'] + NumDatesep = 4, // Num[ ]*Dsep + NumTimesep = 5, // Num[ ]*Tsep + MonthEnd = 6, // Month[ ]*'\0' + MonthSpace = 7, // Month[ ]+^[Dsep|Tsep|'\0'] + MonthDatesep = 8, // Month[ ]*Dsep + NumDatesuff = 9, // Month[ ]*DSuff + NumTimesuff = 10, // Month[ ]*TSuff + DayOfWeek = 11, // Day of week name + YearSpace = 12, // Year+^[Dsep|Tsep|'0\'] + YearDateSep = 13, // Year+Dsep + YearEnd = 14, // Year+['\0'] + TimeZone = 15, // timezone name + Era = 16, // era name + NumUTCTimeMark = 17, // Num + 'Z' + // When you add a new token which will be in the + // state table, add it after NumLocalTimeMark. + Unk = 18, // unknown + NumLocalTimeMark = 19, // Num + 'T' + Max = 20, // marker + } + + internal enum TM + { + NotSet = -1, + AM = 0, + PM = 1, + } + + + //////////////////////////////////////////////////////////////////////////// + // + // DateTime parsing state enumeration (DS.*) + // + //////////////////////////////////////////////////////////////////////////// + + internal enum DS + { + BEGIN = 0, + N = 1, // have one number + NN = 2, // have two numbers + + // The following are known to be part of a date + + D_Nd = 3, // date string: have number followed by date separator + D_NN = 4, // date string: have two numbers + D_NNd = 5, // date string: have two numbers followed by date separator + + D_M = 6, // date string: have a month + D_MN = 7, // date string: have a month and a number + D_NM = 8, // date string: have a number and a month + D_MNd = 9, // date string: have a month and number followed by date separator + D_NDS = 10, // date string: have one number followed a date suffix. + + D_Y = 11, // date string: have a year. + D_YN = 12, // date string: have a year and a number + D_YNd = 13, // date string: have a year and a number and a date separator + D_YM = 14, // date string: have a year and a month + D_YMd = 15, // date string: have a year and a month and a date separator + D_S = 16, // have numbers followed by a date suffix. + T_S = 17, // have numbers followed by a time suffix. + + // The following are known to be part of a time + + T_Nt = 18, // have num followed by time separator + T_NNt = 19, // have two numbers followed by time separator + + + ERROR = 20, + + // The following are terminal states. These all have an action + // associated with them; and transition back to BEGIN. + + DX_NN = 21, // day from two numbers + DX_NNN = 22, // day from three numbers + DX_MN = 23, // day from month and one number + DX_NM = 24, // day from month and one number + DX_MNN = 25, // day from month and two numbers + DX_DS = 26, // a set of date suffixed numbers. + DX_DSN = 27, // day from date suffixes and one number. + DX_NDS = 28, // day from one number and date suffixes . + DX_NNDS = 29, // day from one number and date suffixes . + + DX_YNN = 30, // date string: have a year and two number + DX_YMN = 31, // date string: have a year, a month, and a number. + DX_YN = 32, // date string: have a year and one number + DX_YM = 33, // date string: have a year, a month. + TX_N = 34, // time from one number (must have ampm) + TX_NN = 35, // time from two numbers + TX_NNN = 36, // time from three numbers + TX_TS = 37, // a set of time suffixed numbers. + DX_NNY = 38, + } + + //////////////////////////////////////////////////////////////////////////// + // + // NOTE: The following state machine table is dependent on the order of the + // DS and DTT enumerations. + // + // For each non terminal state, the following table defines the next state + // for each given date token type. + // + //////////////////////////////////////////////////////////////////////////// + + // End NumEnd NumAmPm NumSpace NumDaySep NumTimesep MonthEnd MonthSpace MonthDSep NumDateSuff NumTimeSuff DayOfWeek YearSpace YearDateSep YearEnd TimeZone Era UTCTimeMark + private static DS[][] dateParsingStates = { +// DS.BEGIN // DS.BEGIN +new DS[] { DS.BEGIN, DS.ERROR, DS.TX_N, DS.N, DS.D_Nd, DS.T_Nt, DS.ERROR, DS.D_M, DS.D_M, DS.D_S, DS.T_S, DS.BEGIN, DS.D_Y, DS.D_Y, DS.ERROR, DS.BEGIN, DS.BEGIN, DS.ERROR}, + +// DS.N // DS.N +new DS[] { DS.ERROR, DS.DX_NN, DS.ERROR, DS.NN, DS.D_NNd, DS.ERROR, DS.DX_NM, DS.D_NM, DS.D_MNd, DS.D_NDS, DS.ERROR, DS.N, DS.D_YN, DS.D_YNd, DS.DX_YN, DS.N, DS.N, DS.ERROR}, + +// DS.NN // DS.NN +new DS[] { DS.DX_NN, DS.DX_NNN, DS.TX_N, DS.DX_NNN, DS.ERROR, DS.T_Nt, DS.DX_MNN, DS.DX_MNN, DS.ERROR, DS.ERROR, DS.T_S, DS.NN, DS.DX_NNY, DS.ERROR, DS.DX_NNY, DS.NN, DS.NN, DS.ERROR}, + +// DS.D_Nd // DS.D_Nd +new DS[] { DS.ERROR, DS.DX_NN, DS.ERROR, DS.D_NN, DS.D_NNd, DS.ERROR, DS.DX_NM, DS.D_MN, DS.D_MNd, DS.ERROR, DS.ERROR, DS.D_Nd, DS.D_YN, DS.D_YNd, DS.DX_YN, DS.ERROR, DS.D_Nd, DS.ERROR}, + +// DS.D_NN // DS.D_NN +new DS[] { DS.DX_NN, DS.DX_NNN, DS.TX_N, DS.DX_NNN, DS.ERROR, DS.T_Nt, DS.DX_MNN, DS.DX_MNN, DS.ERROR, DS.DX_DS, DS.T_S, DS.D_NN, DS.DX_NNY, DS.ERROR, DS.DX_NNY, DS.ERROR, DS.D_NN, DS.ERROR}, + +// DS.D_NNd // DS.D_NNd +new DS[] { DS.ERROR, DS.DX_NNN, DS.DX_NNN, DS.DX_NNN, DS.ERROR, DS.ERROR, DS.DX_MNN, DS.DX_MNN, DS.ERROR, DS.DX_DS, DS.ERROR, DS.D_NNd, DS.DX_NNY, DS.ERROR, DS.DX_NNY, DS.ERROR, DS.D_NNd, DS.ERROR}, + +// DS.D_M // DS.D_M +new DS[] { DS.ERROR, DS.DX_MN, DS.ERROR, DS.D_MN, DS.D_MNd, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_M, DS.D_YM, DS.D_YMd, DS.DX_YM, DS.ERROR, DS.D_M, DS.ERROR}, + +// DS.D_MN // DS.D_MN +new DS[] { DS.DX_MN, DS.DX_MNN, DS.DX_MNN, DS.DX_MNN, DS.ERROR, DS.T_Nt, DS.ERROR, DS.ERROR, DS.ERROR, DS.DX_DS, DS.T_S, DS.D_MN, DS.DX_YMN, DS.ERROR, DS.DX_YMN, DS.ERROR, DS.D_MN, DS.ERROR}, + +// DS.D_NM // DS.D_NM +new DS[] { DS.DX_NM, DS.DX_MNN, DS.DX_MNN, DS.DX_MNN, DS.ERROR, DS.T_Nt, DS.ERROR, DS.ERROR, DS.ERROR, DS.DX_DS, DS.T_S, DS.D_NM, DS.DX_YMN, DS.ERROR, DS.DX_YMN, DS.ERROR, DS.D_NM, DS.ERROR}, + +// DS.D_MNd // DS.D_MNd +new DS[] { DS.ERROR, DS.DX_MNN, DS.ERROR, DS.DX_MNN, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_MNd, DS.DX_YMN, DS.ERROR, DS.DX_YMN, DS.ERROR, DS.D_MNd, DS.ERROR}, + +// DS.D_NDS, // DS.D_NDS, +new DS[] { DS.DX_NDS,DS.DX_NNDS, DS.DX_NNDS, DS.DX_NNDS, DS.ERROR, DS.T_Nt, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_NDS, DS.T_S, DS.D_NDS, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_NDS, DS.ERROR}, + +// DS.D_Y // DS.D_Y +new DS[] { DS.ERROR, DS.DX_YN, DS.ERROR, DS.D_YN, DS.D_YNd, DS.ERROR, DS.DX_YM, DS.D_YM, DS.D_YMd, DS.D_YM, DS.ERROR, DS.D_Y, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_Y, DS.ERROR}, + +// DS.D_YN // DS.D_YN +new DS[] { DS.DX_YN, DS.DX_YNN, DS.DX_YNN, DS.DX_YNN, DS.ERROR, DS.ERROR, DS.DX_YMN, DS.DX_YMN, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_YN, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_YN, DS.ERROR}, + +// DS.D_YNd // DS.D_YNd +new DS[] { DS.ERROR, DS.DX_YNN, DS.DX_YNN, DS.DX_YNN, DS.ERROR, DS.ERROR, DS.DX_YMN, DS.DX_YMN, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_YN, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_YN, DS.ERROR}, + +// DS.D_YM // DS.D_YM +new DS[] { DS.DX_YM, DS.DX_YMN, DS.DX_YMN, DS.DX_YMN, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_YM, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_YM, DS.ERROR}, + +// DS.D_YMd // DS.D_YMd +new DS[] { DS.ERROR, DS.DX_YMN, DS.DX_YMN, DS.DX_YMN, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_YM, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_YM, DS.ERROR}, + +// DS.D_S // DS.D_S +new DS[] { DS.DX_DS, DS.DX_DSN, DS.TX_N, DS.T_Nt, DS.ERROR, DS.T_Nt, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_S, DS.T_S, DS.D_S, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_S, DS.ERROR}, + +// DS.T_S // DS.T_S +new DS[] { DS.TX_TS, DS.TX_TS, DS.TX_TS, DS.T_Nt, DS.D_Nd, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.D_S, DS.T_S, DS.T_S, DS.ERROR, DS.ERROR, DS.ERROR, DS.T_S, DS.T_S, DS.ERROR}, + +// DS.T_Nt // DS.T_Nt +new DS[] { DS.ERROR, DS.TX_NN, DS.TX_NN, DS.TX_NN, DS.ERROR, DS.T_NNt, DS.DX_NM, DS.D_NM, DS.ERROR, DS.ERROR, DS.T_S, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.T_Nt, DS.T_Nt, DS.TX_NN}, + +// DS.T_NNt // DS.T_NNt +new DS[] { DS.ERROR, DS.TX_NNN, DS.TX_NNN, DS.TX_NNN, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.ERROR, DS.T_S, DS.T_NNt, DS.ERROR, DS.ERROR, DS.ERROR, DS.T_NNt, DS.T_NNt, DS.TX_NNN}, +}; + // End NumEnd NumAmPm NumSpace NumDaySep NumTimesep MonthEnd MonthSpace MonthDSep NumDateSuff NumTimeSuff DayOfWeek YearSpace YearDateSep YearEnd TimeZone Era UTCMark + + internal const String GMTName = "GMT"; + internal const String ZuluName = "Z"; + + // + // Search from the index of str at str.Index to see if the target string exists in the str. + // + private static bool MatchWord(ref __DTString str, String target) + { + int length = target.Length; + if (length > (str.Value.Length - str.Index)) + { + return false; + } + + if (str.CompareInfo.Compare(str.Value, str.Index, length, + target, 0, length, CompareOptions.IgnoreCase) != 0) + { + return (false); + } + + int nextCharIndex = str.Index + target.Length; + + if (nextCharIndex < str.Value.Length) + { + char nextCh = str.Value[nextCharIndex]; + if (Char.IsLetter(nextCh)) + { + return (false); + } + } + str.Index = nextCharIndex; + if (str.Index < str.len) + { + str.m_current = str.Value[str.Index]; + } + + return (true); + } + + + // + // Check the word at the current index to see if it matches GMT name or Zulu name. + // + private static bool GetTimeZoneName(ref __DTString str) + { + if (MatchWord(ref str, GMTName)) + { + return (true); + } + + if (MatchWord(ref str, ZuluName)) + { + return (true); + } + + return (false); + } + + internal static bool IsDigit(char ch) + { + return (ch >= '0' && ch <= '9'); + } + + + /*=================================ParseFraction========================== + **Action: Starting at the str.Index, which should be a decimal symbol. + ** if the current character is a digit, parse the remaining + ** numbers as fraction. For example, if the sub-string starting at str.Index is "123", then + ** the method will return 0.123 + **Returns: The fraction number. + **Arguments: + ** str the parsing string + **Exceptions: + ============================================================================*/ + + private static bool ParseFraction(ref __DTString str, out double result) + { + result = 0; + double decimalBase = 0.1; + int digits = 0; + char ch; + while (str.GetNext() + && IsDigit(ch = str.m_current)) + { + result += (ch - '0') * decimalBase; + decimalBase *= 0.1; + digits++; + } + return (digits > 0); + } + + /*=================================ParseTimeZone========================== + **Action: Parse the timezone offset in the following format: + ** "+8", "+08", "+0800", "+0800" + ** This method is used by DateTime.Parse(). + **Returns: The TimeZone offset. + **Arguments: + ** str the parsing string + **Exceptions: + ** FormatException if invalid timezone format is found. + ============================================================================*/ + + private static bool ParseTimeZone(ref __DTString str, ref TimeSpan result) + { + // The hour/minute offset for timezone. + int hourOffset = 0; + int minuteOffset = 0; + DTSubString sub; + + // Consume the +/- character that has already been read + sub = str.GetSubString(); + if (sub.length != 1) + { + return false; + } + char offsetChar = sub[0]; + if (offsetChar != '+' && offsetChar != '-') + { + return false; + } + str.ConsumeSubString(sub); + + sub = str.GetSubString(); + if (sub.type != DTSubStringType.Number) + { + return false; + } + int value = sub.value; + int length = sub.length; + if (length == 1 || length == 2) + { + // Parsing "+8" or "+08" + hourOffset = value; + str.ConsumeSubString(sub); + // See if we have minutes + sub = str.GetSubString(); + if (sub.length == 1 && sub[0] == ':') + { + // Parsing "+8:00" or "+08:00" + str.ConsumeSubString(sub); + sub = str.GetSubString(); + if (sub.type != DTSubStringType.Number || sub.length < 1 || sub.length > 2) + { + return false; + } + minuteOffset = sub.value; + str.ConsumeSubString(sub); + } + } + else if (length == 3 || length == 4) + { + // Parsing "+800" or "+0800" + hourOffset = value / 100; + minuteOffset = value % 100; + str.ConsumeSubString(sub); + } + else + { + // Wrong number of digits + return false; + } + Debug.Assert(hourOffset >= 0 && hourOffset <= 99, "hourOffset >= 0 && hourOffset <= 99"); + Debug.Assert(minuteOffset >= 0 && minuteOffset <= 99, "minuteOffset >= 0 && minuteOffset <= 99"); + if (minuteOffset < 0 || minuteOffset >= 60) + { + return false; + } + + result = new TimeSpan(hourOffset, minuteOffset, 0); + if (offsetChar == '-') + { + result = result.Negate(); + } + return true; + } + + // This is the helper function to handle timezone in string in the format like +/-0800 + private static bool HandleTimeZone(ref __DTString str, ref DateTimeResult result) + { + if ((str.Index < str.len - 1)) + { + char nextCh = str.Value[str.Index]; + // Skip whitespace, but don't update the index unless we find a time zone marker + int whitespaceCount = 0; + while (Char.IsWhiteSpace(nextCh) && str.Index + whitespaceCount < str.len - 1) + { + whitespaceCount++; + nextCh = str.Value[str.Index + whitespaceCount]; + } + if (nextCh == '+' || nextCh == '-') + { + str.Index += whitespaceCount; + if ((result.flags & ParseFlags.TimeZoneUsed) != 0) + { + // Should not have two timezone offsets. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + result.flags |= ParseFlags.TimeZoneUsed; + if (!ParseTimeZone(ref str, ref result.timeZoneOffset)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + } + } + return true; + } + + // + // This is the lexer. Check the character at the current index, and put the found token in dtok and + // some raw date/time information in raw. + // + private static Boolean Lex(DS dps, ref __DTString str, ref DateTimeToken dtok, ref DateTimeRawInfo raw, ref DateTimeResult result, ref DateTimeFormatInfo dtfi, DateTimeStyles styles) + { + TokenType tokenType; + int tokenValue; + int indexBeforeSeparator; + char charBeforeSeparator; + + TokenType sep; + dtok.dtt = DTT.Unk; // Assume the token is unkown. + + str.GetRegularToken(out tokenType, out tokenValue, dtfi); + +#if _LOGGING + // Builds with _LOGGING defined (x86dbg, amd64chk, etc) support tracing + // Set the following internal-only/unsupported environment variables to enable DateTime tracing to the console: + // + // COMPlus_LogEnable=1 + // COMPlus_LogToConsole=1 + // COMPlus_LogLevel=9 + // COMPlus_ManagedLogFacility=0x00001000 + if (_tracingEnabled) + { + BCLDebug.Trace("DATETIME", "[DATETIME] Lex({0})\tpos:{1}({2}), {3}, DS.{4}", Hex(str.Value), + str.Index, Hex(str.m_current), tokenType, dps); + } +#endif // _LOGGING + + // Look at the regular token. + switch (tokenType) + { + case TokenType.NumberToken: + case TokenType.YearNumberToken: + if (raw.numCount == 3 || tokenValue == -1) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0010", dps); + return false; + } + // + // This is a digit. + // + // If the previous parsing state is DS.T_NNt (like 12:01), and we got another number, + // so we will have a terminal state DS.TX_NNN (like 12:01:02). + // If the previous parsing state is DS.T_Nt (like 12:), and we got another number, + // so we will have a terminal state DS.TX_NN (like 12:01). + // + // Look ahead to see if the following character is a decimal point or timezone offset. + // This enables us to parse time in the forms of: + // "11:22:33.1234" or "11:22:33-08". + if (dps == DS.T_NNt) + { + if ((str.Index < str.len - 1)) + { + char nextCh = str.Value[str.Index]; + if (nextCh == '.') + { + // While ParseFraction can fail, it just means that there were no digits after + // the dot. In this case ParseFraction just removes the dot. This is actually + // valid for cultures like Albanian, that join the time marker to the time with + // with a dot: e.g. "9:03.MD" + ParseFraction(ref str, out raw.fraction); + } + } + } + if (dps == DS.T_NNt || dps == DS.T_Nt) + { + if ((str.Index < str.len - 1)) + { + if (false == HandleTimeZone(ref str, ref result)) + { + LexTraceExit("0020 (value like \"12:01\" or \"12:\" followed by a non-TZ number", dps); + return false; + } + } + } + + dtok.num = tokenValue; + if (tokenType == TokenType.YearNumberToken) + { + if (raw.year == -1) + { + raw.year = tokenValue; + // + // If we have number which has 3 or more digits (like "001" or "0001"), + // we assume this number is a year. Save the currnet raw.numCount in + // raw.year. + // + switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator)) + { + case TokenType.SEP_End: + dtok.dtt = DTT.YearEnd; + break; + case TokenType.SEP_Am: + case TokenType.SEP_Pm: + if (raw.timeMark == TM.NotSet) + { + raw.timeMark = (sep == TokenType.SEP_Am ? TM.AM : TM.PM); + dtok.dtt = DTT.YearSpace; + } + else + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0030 (TM.AM/TM.PM Happened more than 1x)", dps); + } + break; + case TokenType.SEP_Space: + dtok.dtt = DTT.YearSpace; + break; + case TokenType.SEP_Date: + dtok.dtt = DTT.YearDateSep; + break; + case TokenType.SEP_Time: + if (!raw.hasSameDateAndTimeSeparators) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0040 (Invalid separator after number)", dps); + return false; + } + + // we have the date and time separators are same and getting a year number, then change the token to YearDateSep as + // we are sure we are not parsing time. + dtok.dtt = DTT.YearDateSep; + break; + case TokenType.SEP_DateOrOffset: + // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then + // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset. + if ((dateParsingStates[(int)dps][(int)DTT.YearDateSep] == DS.ERROR) + && (dateParsingStates[(int)dps][(int)DTT.YearSpace] > DS.ERROR)) + { + str.Index = indexBeforeSeparator; + str.m_current = charBeforeSeparator; + dtok.dtt = DTT.YearSpace; + } + else + { + dtok.dtt = DTT.YearDateSep; + } + break; + case TokenType.SEP_YearSuff: + case TokenType.SEP_MonthSuff: + case TokenType.SEP_DaySuff: + dtok.dtt = DTT.NumDatesuff; + dtok.suffix = sep; + break; + case TokenType.SEP_HourSuff: + case TokenType.SEP_MinuteSuff: + case TokenType.SEP_SecondSuff: + dtok.dtt = DTT.NumTimesuff; + dtok.suffix = sep; + break; + default: + // Invalid separator after number number. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0040 (Invalid separator after number)", dps); + return false; + } + // + // Found the token already. Return now. + // + LexTraceExit("0050 (success)", dps); + return true; + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0060", dps); + return false; + } + switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator)) + { + // + // Note here we check if the numCount is less than three. + // When we have more than three numbers, it will be caught as error in the state machine. + // + case TokenType.SEP_End: + dtok.dtt = DTT.NumEnd; + raw.AddNumber(dtok.num); + break; + case TokenType.SEP_Am: + case TokenType.SEP_Pm: + if (raw.timeMark == TM.NotSet) + { + raw.timeMark = (sep == TokenType.SEP_Am ? TM.AM : TM.PM); + dtok.dtt = DTT.NumAmpm; + // Fix AM/PM parsing case, e.g. "1/10 5 AM" + if (dps == DS.D_NN) + { + if (!ProcessTerminaltState(DS.DX_NN, ref result, ref styles, ref raw, dtfi)) + { + return false; + } + } + + raw.AddNumber(dtok.num); + } + else + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + break; + } + if (dps == DS.T_NNt || dps == DS.T_Nt) + { + if (false == HandleTimeZone(ref str, ref result)) + { + LexTraceExit("0070 (HandleTimeZone returned false)", dps); + return false; + } + } + break; + case TokenType.SEP_Space: + dtok.dtt = DTT.NumSpace; + raw.AddNumber(dtok.num); + break; + case TokenType.SEP_Date: + dtok.dtt = DTT.NumDatesep; + raw.AddNumber(dtok.num); + break; + case TokenType.SEP_DateOrOffset: + // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then + // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset. + if ((dateParsingStates[(int)dps][(int)DTT.NumDatesep] == DS.ERROR) + && (dateParsingStates[(int)dps][(int)DTT.NumSpace] > DS.ERROR)) + { + str.Index = indexBeforeSeparator; + str.m_current = charBeforeSeparator; + dtok.dtt = DTT.NumSpace; + } + else + { + dtok.dtt = DTT.NumDatesep; + } + raw.AddNumber(dtok.num); + break; + case TokenType.SEP_Time: + if (raw.hasSameDateAndTimeSeparators && + (dps == DS.D_Y || dps == DS.D_YN || dps == DS.D_YNd || dps == DS.D_YM || dps == DS.D_YMd)) + { + // we are parsing a date and we have the time separator same as date separator, so we mark the token as date separator + dtok.dtt = DTT.NumDatesep; + raw.AddNumber(dtok.num); + break; + } + dtok.dtt = DTT.NumTimesep; + raw.AddNumber(dtok.num); + break; + case TokenType.SEP_YearSuff: + try + { + dtok.num = dtfi.Calendar.ToFourDigitYear(tokenValue); + } + catch (ArgumentOutOfRangeException e) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", e); + LexTraceExit("0075 (Calendar.ToFourDigitYear failed)", dps); + return false; + } + dtok.dtt = DTT.NumDatesuff; + dtok.suffix = sep; + break; + case TokenType.SEP_MonthSuff: + case TokenType.SEP_DaySuff: + dtok.dtt = DTT.NumDatesuff; + dtok.suffix = sep; + break; + case TokenType.SEP_HourSuff: + case TokenType.SEP_MinuteSuff: + case TokenType.SEP_SecondSuff: + dtok.dtt = DTT.NumTimesuff; + dtok.suffix = sep; + break; + case TokenType.SEP_LocalTimeMark: + dtok.dtt = DTT.NumLocalTimeMark; + raw.AddNumber(dtok.num); + break; + default: + // Invalid separator after number number. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0080", dps); + return false; + } + break; + case TokenType.HebrewNumber: + if (tokenValue >= 100) + { + // This is a year number + if (raw.year == -1) + { + raw.year = tokenValue; + // + // If we have number which has 3 or more digits (like "001" or "0001"), + // we assume this number is a year. Save the currnet raw.numCount in + // raw.year. + // + switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator)) + { + case TokenType.SEP_End: + dtok.dtt = DTT.YearEnd; + break; + case TokenType.SEP_Space: + dtok.dtt = DTT.YearSpace; + break; + case TokenType.SEP_DateOrOffset: + // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then + // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset. + if (dateParsingStates[(int)dps][(int)DTT.YearSpace] > DS.ERROR) + { + str.Index = indexBeforeSeparator; + str.m_current = charBeforeSeparator; + dtok.dtt = DTT.YearSpace; + break; + } + goto default; + default: + // Invalid separator after number number. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0090", dps); + return false; + } + } + else + { + // Invalid separator after number number. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0100", dps); + return false; + } + } + else + { + // This is a day number + dtok.num = tokenValue; + raw.AddNumber(dtok.num); + + switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator)) + { + // + // Note here we check if the numCount is less than three. + // When we have more than three numbers, it will be caught as error in the state machine. + // + case TokenType.SEP_End: + dtok.dtt = DTT.NumEnd; + break; + case TokenType.SEP_Space: + case TokenType.SEP_Date: + dtok.dtt = DTT.NumDatesep; + break; + case TokenType.SEP_DateOrOffset: + // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then + // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset. + if ((dateParsingStates[(int)dps][(int)DTT.NumDatesep] == DS.ERROR) + && (dateParsingStates[(int)dps][(int)DTT.NumSpace] > DS.ERROR)) + { + str.Index = indexBeforeSeparator; + str.m_current = charBeforeSeparator; + dtok.dtt = DTT.NumSpace; + } + else + { + dtok.dtt = DTT.NumDatesep; + } + break; + default: + // Invalid separator after number number. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0110", dps); + return false; + } + } + break; + case TokenType.DayOfWeekToken: + if (raw.dayOfWeek == -1) + { + // + // This is a day of week name. + // + raw.dayOfWeek = tokenValue; + dtok.dtt = DTT.DayOfWeek; + } + else + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0120 (DayOfWeek seen more than 1x)", dps); + return false; + } + break; + case TokenType.MonthToken: + if (raw.month == -1) + { + // + // This is a month name + // + switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator)) + { + case TokenType.SEP_End: + dtok.dtt = DTT.MonthEnd; + break; + case TokenType.SEP_Space: + dtok.dtt = DTT.MonthSpace; + break; + case TokenType.SEP_Date: + dtok.dtt = DTT.MonthDatesep; + break; + case TokenType.SEP_Time: + if (!raw.hasSameDateAndTimeSeparators) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0130 (Invalid separator after month name)", dps); + return false; + } + + // we have the date and time separators are same and getting a Month name, then change the token to MonthDatesep as + // we are sure we are not parsing time. + dtok.dtt = DTT.MonthDatesep; + break; + case TokenType.SEP_DateOrOffset: + // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then + // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset. + if ((dateParsingStates[(int)dps][(int)DTT.MonthDatesep] == DS.ERROR) + && (dateParsingStates[(int)dps][(int)DTT.MonthSpace] > DS.ERROR)) + { + str.Index = indexBeforeSeparator; + str.m_current = charBeforeSeparator; + dtok.dtt = DTT.MonthSpace; + } + else + { + dtok.dtt = DTT.MonthDatesep; + } + break; + default: + //Invalid separator after month name + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0130 (Invalid separator after month name)", dps); + return false; + } + raw.month = tokenValue; + } + else + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0140 (MonthToken seen more than 1x)", dps); + return false; + } + break; + case TokenType.EraToken: + if (result.era != -1) + { + result.era = tokenValue; + dtok.dtt = DTT.Era; + } + else + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0150 (EraToken seen when result.era already set)", dps); + return false; + } + break; + case TokenType.JapaneseEraToken: + // Special case for Japanese. We allow Japanese era name to be used even if the calendar is not Japanese Calendar. + result.calendar = JapaneseCalendar.GetDefaultInstance(); + dtfi = DateTimeFormatInfo.GetJapaneseCalendarDTFI(); + if (result.era != -1) + { + result.era = tokenValue; + dtok.dtt = DTT.Era; + } + else + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0160 (JapaneseEraToken seen when result.era already set)", dps); + return false; + } + break; + case TokenType.TEraToken: + result.calendar = TaiwanCalendar.GetDefaultInstance(); + dtfi = DateTimeFormatInfo.GetTaiwanCalendarDTFI(); + if (result.era != -1) + { + result.era = tokenValue; + dtok.dtt = DTT.Era; + } + else + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0170 (TEraToken seen when result.era already set)", dps); + return false; + } + break; + case TokenType.TimeZoneToken: + // + // This is a timezone designator + // + // NOTENOTE : for now, we only support "GMT" and "Z" (for Zulu time). + // + if ((result.flags & ParseFlags.TimeZoneUsed) != 0) + { + // Should not have two timezone offsets. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0180 (seen GMT or Z more than 1x)", dps); + return false; + } + dtok.dtt = DTT.TimeZone; + result.flags |= ParseFlags.TimeZoneUsed; + result.timeZoneOffset = new TimeSpan(0); + result.flags |= ParseFlags.TimeZoneUtc; + break; + case TokenType.EndOfString: + dtok.dtt = DTT.End; + break; + case TokenType.DateWordToken: + case TokenType.IgnorableSymbol: + // Date words and ignorable symbols can just be skipped over + break; + case TokenType.Am: + case TokenType.Pm: + if (raw.timeMark == TM.NotSet) + { + raw.timeMark = (TM)tokenValue; + } + else + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0190 (AM/PM timeMark already set)", dps); + return false; + } + break; + case TokenType.UnknownToken: + if (Char.IsLetter(str.m_current)) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_UnknowDateTimeWord", str.Index); + LexTraceExit("0200", dps); + return (false); + } + + if ((str.m_current == '-' || str.m_current == '+') && ((result.flags & ParseFlags.TimeZoneUsed) == 0)) + { + Int32 originalIndex = str.Index; + if (ParseTimeZone(ref str, ref result.timeZoneOffset)) + { + result.flags |= ParseFlags.TimeZoneUsed; + LexTraceExit("0220 (success)", dps); + return true; + } + else + { + // Time zone parse attempt failed. Fall through to punctuation handling. + str.Index = originalIndex; + } + } + + // Visual Basic implements string to date conversions on top of DateTime.Parse: + // CDate("#10/10/95#") + // + if (VerifyValidPunctuation(ref str)) + { + LexTraceExit("0230 (success)", dps); + return true; + } + + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + LexTraceExit("0240", dps); + return false; + } + + LexTraceExit("0250 (success)", dps); + return true; + } + + private static Boolean VerifyValidPunctuation(ref __DTString str) + { + // Compatability Behavior. Allow trailing nulls and surrounding hashes + Char ch = str.Value[str.Index]; + if (ch == '#') + { + bool foundStart = false; + bool foundEnd = false; + for (int i = 0; i < str.len; i++) + { + ch = str.Value[i]; + if (ch == '#') + { + if (foundStart) + { + if (foundEnd) + { + // Having more than two hashes is invalid + return false; + } + else + { + foundEnd = true; + } + } + else + { + foundStart = true; + } + } + else if (ch == '\0') + { + // Allow nulls only at the end + if (!foundEnd) + { + return false; + } + } + else if ((!Char.IsWhiteSpace(ch))) + { + // Anthyhing other than whitespace outside hashes is invalid + if (!foundStart || foundEnd) + { + return false; + } + } + } + if (!foundEnd) + { + // The has was un-paired + return false; + } + // Valid Hash usage: eat the hash and continue. + str.GetNext(); + return true; + } + else if (ch == '\0') + { + for (int i = str.Index; i < str.len; i++) + { + if (str.Value[i] != '\0') + { + // Nulls are only valid if they are the only trailing character + return false; + } + } + // Move to the end of the string + str.Index = str.len; + return true; + } + return false; + } + + private const int ORDER_YMD = 0; // The order of date is Year/Month/Day. + private const int ORDER_MDY = 1; // The order of date is Month/Day/Year. + private const int ORDER_DMY = 2; // The order of date is Day/Month/Year. + private const int ORDER_YDM = 3; // The order of date is Year/Day/Month + private const int ORDER_YM = 4; // Year/Month order. + private const int ORDER_MY = 5; // Month/Year order. + private const int ORDER_MD = 6; // Month/Day order. + private const int ORDER_DM = 7; // Day/Month order. + + // + // Decide the year/month/day order from the datePattern. + // + // Return 0 for YMD, 1 for MDY, 2 for DMY, otherwise -1. + // + private static Boolean GetYearMonthDayOrder(String datePattern, DateTimeFormatInfo dtfi, out int order) + { + int yearOrder = -1; + int monthOrder = -1; + int dayOrder = -1; + int orderCount = 0; + + bool inQuote = false; + + for (int i = 0; i < datePattern.Length && orderCount < 3; i++) + { + char ch = datePattern[i]; + if (ch == '\\' || ch == '%') + { + i++; + continue; // Skip next character that is escaped by this backslash + } + + if (ch == '\'' || ch == '"') + { + inQuote = !inQuote; + } + + if (!inQuote) + { + if (ch == 'y') + { + yearOrder = orderCount++; + + // + // Skip all year pattern charaters. + // + for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'y'; i++) + { + // Do nothing here. + } + } + else if (ch == 'M') + { + monthOrder = orderCount++; + // + // Skip all month pattern characters. + // + for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'M'; i++) + { + // Do nothing here. + } + } + else if (ch == 'd') + { + int patternCount = 1; + // + // Skip all day pattern characters. + // + for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'd'; i++) + { + patternCount++; + } + // + // Make sure this is not "ddd" or "dddd", which means day of week. + // + if (patternCount <= 2) + { + dayOrder = orderCount++; + } + } + } + } + + if (yearOrder == 0 && monthOrder == 1 && dayOrder == 2) + { + order = ORDER_YMD; + return true; + } + if (monthOrder == 0 && dayOrder == 1 && yearOrder == 2) + { + order = ORDER_MDY; + return true; + } + if (dayOrder == 0 && monthOrder == 1 && yearOrder == 2) + { + order = ORDER_DMY; + return true; + } + if (yearOrder == 0 && dayOrder == 1 && monthOrder == 2) + { + order = ORDER_YDM; + return true; + } + order = -1; + return false; + } + + // + // Decide the year/month order from the pattern. + // + // Return 0 for YM, 1 for MY, otherwise -1. + // + private static Boolean GetYearMonthOrder(String pattern, DateTimeFormatInfo dtfi, out int order) + { + int yearOrder = -1; + int monthOrder = -1; + int orderCount = 0; + + bool inQuote = false; + for (int i = 0; i < pattern.Length && orderCount < 2; i++) + { + char ch = pattern[i]; + if (ch == '\\' || ch == '%') + { + i++; + continue; // Skip next character that is escaped by this backslash + } + + if (ch == '\'' || ch == '"') + { + inQuote = !inQuote; + } + + if (!inQuote) + { + if (ch == 'y') + { + yearOrder = orderCount++; + + // + // Skip all year pattern charaters. + // + for (; i + 1 < pattern.Length && pattern[i + 1] == 'y'; i++) + { + } + } + else if (ch == 'M') + { + monthOrder = orderCount++; + // + // Skip all month pattern characters. + // + for (; i + 1 < pattern.Length && pattern[i + 1] == 'M'; i++) + { + } + } + } + } + + if (yearOrder == 0 && monthOrder == 1) + { + order = ORDER_YM; + return true; + } + if (monthOrder == 0 && yearOrder == 1) + { + order = ORDER_MY; + return true; + } + order = -1; + return false; + } + + // + // Decide the month/day order from the pattern. + // + // Return 0 for MD, 1 for DM, otherwise -1. + // + private static Boolean GetMonthDayOrder(String pattern, DateTimeFormatInfo dtfi, out int order) + { + int monthOrder = -1; + int dayOrder = -1; + int orderCount = 0; + + bool inQuote = false; + for (int i = 0; i < pattern.Length && orderCount < 2; i++) + { + char ch = pattern[i]; + if (ch == '\\' || ch == '%') + { + i++; + continue; // Skip next character that is escaped by this backslash + } + + if (ch == '\'' || ch == '"') + { + inQuote = !inQuote; + } + + if (!inQuote) + { + if (ch == 'd') + { + int patternCount = 1; + // + // Skip all day pattern charaters. + // + for (; i + 1 < pattern.Length && pattern[i + 1] == 'd'; i++) + { + patternCount++; + } + + // + // Make sure this is not "ddd" or "dddd", which means day of week. + // + if (patternCount <= 2) + { + dayOrder = orderCount++; + } + } + else if (ch == 'M') + { + monthOrder = orderCount++; + // + // Skip all month pattern characters. + // + for (; i + 1 < pattern.Length && pattern[i + 1] == 'M'; i++) + { + } + } + } + } + + if (monthOrder == 0 && dayOrder == 1) + { + order = ORDER_MD; + return true; + } + if (dayOrder == 0 && monthOrder == 1) + { + order = ORDER_DM; + return true; + } + order = -1; + return false; + } + + // + // Adjust the two-digit year if necessary. + // + private static bool TryAdjustYear(ref DateTimeResult result, int year, out int adjustedYear) + { + if (year < 100) + { + try + { + // the Calendar classes need some real work. Many of the calendars that throw + // don't implement a fast/non-allocating (and non-throwing) IsValid{Year|Day|Month} method. + // we are making a targeted try/catch fix in the in-place release but will revisit this code + // in the next side-by-side release. + year = result.calendar.ToFourDigitYear(year); + } + catch (ArgumentOutOfRangeException) + { + adjustedYear = -1; + return false; + } + } + adjustedYear = year; + return true; + } + + private static bool SetDateYMD(ref DateTimeResult result, int year, int month, int day) + { + // Note, longer term these checks should be done at the end of the parse. This current + // way of checking creates order dependence with parsing the era name. + if (result.calendar.IsValidDay(year, month, day, result.era)) + { + result.SetDate(year, month, day); // YMD + return (true); + } + return (false); + } + + private static bool SetDateMDY(ref DateTimeResult result, int month, int day, int year) + { + return (SetDateYMD(ref result, year, month, day)); + } + + private static bool SetDateDMY(ref DateTimeResult result, int day, int month, int year) + { + return (SetDateYMD(ref result, year, month, day)); + } + + private static bool SetDateYDM(ref DateTimeResult result, int year, int day, int month) + { + return (SetDateYMD(ref result, year, month, day)); + } + + private static void GetDefaultYear(ref DateTimeResult result, ref DateTimeStyles styles) + { + result.Year = result.calendar.GetYear(GetDateTimeNow(ref result, ref styles)); + result.flags |= ParseFlags.YearDefault; + } + + // Processing teriminal case: DS.DX_NN + private static Boolean GetDayOfNN(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + if ((result.flags & ParseFlags.HaveDate) != 0) + { + // Multiple dates in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + int n1 = raw.GetNumber(0); + int n2 = raw.GetNumber(1); + + GetDefaultYear(ref result, ref styles); + + int order; + if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out order)) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_BadDatePattern", dtfi.MonthDayPattern); + return false; + } + + if (order == ORDER_MD) + { + if (SetDateYMD(ref result, result.Year, n1, n2)) // MD + { + result.flags |= ParseFlags.HaveDate; + return true; + } + } + else + { + // ORDER_DM + if (SetDateYMD(ref result, result.Year, n2, n1)) // DM + { + result.flags |= ParseFlags.HaveDate; + return true; + } + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + // Processing teriminal case: DS.DX_NNN + private static Boolean GetDayOfNNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + if ((result.flags & ParseFlags.HaveDate) != 0) + { + // Multiple dates in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + int n1 = raw.GetNumber(0); + int n2 = raw.GetNumber(1); ; + int n3 = raw.GetNumber(2); + + int order; + if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order)) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_BadDatePattern", dtfi.ShortDatePattern); + return false; + } + int year; + + if (order == ORDER_YMD) + { + if (TryAdjustYear(ref result, n1, out year) && SetDateYMD(ref result, year, n2, n3)) // YMD + { + result.flags |= ParseFlags.HaveDate; + return true; + } + } + else if (order == ORDER_MDY) + { + if (TryAdjustYear(ref result, n3, out year) && SetDateMDY(ref result, n1, n2, year)) // MDY + { + result.flags |= ParseFlags.HaveDate; + return true; + } + } + else if (order == ORDER_DMY) + { + if (TryAdjustYear(ref result, n3, out year) && SetDateDMY(ref result, n1, n2, year)) // DMY + { + result.flags |= ParseFlags.HaveDate; + return true; + } + } + else if (order == ORDER_YDM) + { + if (TryAdjustYear(ref result, n1, out year) && SetDateYDM(ref result, year, n2, n3)) // YDM + { + result.flags |= ParseFlags.HaveDate; + return true; + } + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + private static Boolean GetDayOfMN(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + if ((result.flags & ParseFlags.HaveDate) != 0) + { + // Multiple dates in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + // The interpretation is based on the MonthDayPattern and YearMonthPattern + // + // MonthDayPattern YearMonthPattern Interpretation + // --------------- ---------------- --------------- + // MMMM dd MMMM yyyy Day + // MMMM dd yyyy MMMM Day + // dd MMMM MMMM yyyy Year + // dd MMMM yyyy MMMM Day + // + // In the first and last cases, it could be either or neither, but a day is a better default interpretation + // than a 2 digit year. + + int monthDayOrder; + if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder)) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_BadDatePattern", dtfi.MonthDayPattern); + return false; + } + if (monthDayOrder == ORDER_DM) + { + int yearMonthOrder; + if (!GetYearMonthOrder(dtfi.YearMonthPattern, dtfi, out yearMonthOrder)) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_BadDatePattern", dtfi.YearMonthPattern); + return false; + } + if (yearMonthOrder == ORDER_MY) + { + int year; + if (!TryAdjustYear(ref result, raw.GetNumber(0), out year) || !SetDateYMD(ref result, year, raw.month, 1)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + return true; + } + } + + GetDefaultYear(ref result, ref styles); + if (!SetDateYMD(ref result, result.Year, raw.month, raw.GetNumber(0))) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + return true; + } + + //////////////////////////////////////////////////////////////////////// + // Actions: + // Deal with the terminal state for Hebrew Month/Day pattern + // + //////////////////////////////////////////////////////////////////////// + + private static Boolean GetHebrewDayOfNM(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + int monthDayOrder; + if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder)) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_BadDatePattern", dtfi.MonthDayPattern); + return false; + } + result.Month = raw.month; + if (monthDayOrder == ORDER_DM || monthDayOrder == ORDER_MD) + { + if (result.calendar.IsValidDay(result.Year, result.Month, raw.GetNumber(0), result.era)) + { + result.Day = raw.GetNumber(0); + return true; + } + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + private static Boolean GetDayOfNM(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + if ((result.flags & ParseFlags.HaveDate) != 0) + { + // Multiple dates in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + // The interpretation is based on the MonthDayPattern and YearMonthPattern + // + // MonthDayPattern YearMonthPattern Interpretation + // --------------- ---------------- --------------- + // MMMM dd MMMM yyyy Day + // MMMM dd yyyy MMMM Year + // dd MMMM MMMM yyyy Day + // dd MMMM yyyy MMMM Day + // + // In the first and last cases, it could be either or neither, but a day is a better default interpretation + // than a 2 digit year. + + int monthDayOrder; + if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder)) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_BadDatePattern", dtfi.MonthDayPattern); + return false; + } + if (monthDayOrder == ORDER_MD) + { + int yearMonthOrder; + if (!GetYearMonthOrder(dtfi.YearMonthPattern, dtfi, out yearMonthOrder)) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_BadDatePattern", dtfi.YearMonthPattern); + return false; + } + if (yearMonthOrder == ORDER_YM) + { + int year; + if (!TryAdjustYear(ref result, raw.GetNumber(0), out year) || !SetDateYMD(ref result, year, raw.month, 1)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + return true; + } + } + + GetDefaultYear(ref result, ref styles); + if (!SetDateYMD(ref result, result.Year, raw.month, raw.GetNumber(0))) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + return true; + } + + private static Boolean GetDayOfMNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + if ((result.flags & ParseFlags.HaveDate) != 0) + { + // Multiple dates in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + int n1 = raw.GetNumber(0); + int n2 = raw.GetNumber(1); + + int order; + if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order)) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_BadDatePattern", dtfi.ShortDatePattern); + return false; + } + int year; + + if (order == ORDER_MDY) + { + if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era)) + { + result.SetDate(year, raw.month, n1); // MDY + result.flags |= ParseFlags.HaveDate; + return true; + } + else if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era)) + { + result.SetDate(year, raw.month, n2); // YMD + result.flags |= ParseFlags.HaveDate; + return true; + } + } + else if (order == ORDER_YMD) + { + if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era)) + { + result.SetDate(year, raw.month, n2); // YMD + result.flags |= ParseFlags.HaveDate; + return true; + } + else if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era)) + { + result.SetDate(year, raw.month, n1); // DMY + result.flags |= ParseFlags.HaveDate; + return true; + } + } + else if (order == ORDER_DMY) + { + if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era)) + { + result.SetDate(year, raw.month, n1); // DMY + result.flags |= ParseFlags.HaveDate; + return true; + } + else if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era)) + { + result.SetDate(year, raw.month, n2); // YMD + result.flags |= ParseFlags.HaveDate; + return true; + } + } + + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + private static Boolean GetDayOfYNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + if ((result.flags & ParseFlags.HaveDate) != 0) + { + // Multiple dates in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + int n1 = raw.GetNumber(0); + int n2 = raw.GetNumber(1); + String pattern = dtfi.ShortDatePattern; + + // For compatibility, don't throw if we can't determine the order, but default to YMD instead + int order; + if (GetYearMonthDayOrder(pattern, dtfi, out order) && order == ORDER_YDM) + { + if (SetDateYMD(ref result, raw.year, n2, n1)) + { + result.flags |= ParseFlags.HaveDate; + return true; // Year + DM + } + } + else + { + if (SetDateYMD(ref result, raw.year, n1, n2)) + { + result.flags |= ParseFlags.HaveDate; + return true; // Year + MD + } + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + private static Boolean GetDayOfNNY(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + if ((result.flags & ParseFlags.HaveDate) != 0) + { + // Multiple dates in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + int n1 = raw.GetNumber(0); + int n2 = raw.GetNumber(1); + + int order; + if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order)) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_BadDatePattern", dtfi.ShortDatePattern); + return false; + } + + if (order == ORDER_MDY || order == ORDER_YMD) + { + if (SetDateYMD(ref result, raw.year, n1, n2)) + { + result.flags |= ParseFlags.HaveDate; + return true; // MD + Year + } + } + else + { + if (SetDateYMD(ref result, raw.year, n2, n1)) + { + result.flags |= ParseFlags.HaveDate; + return true; // DM + Year + } + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + + private static Boolean GetDayOfYMN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + if ((result.flags & ParseFlags.HaveDate) != 0) + { + // Multiple dates in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + if (SetDateYMD(ref result, raw.year, raw.month, raw.GetNumber(0))) + { + result.flags |= ParseFlags.HaveDate; + return true; + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + private static Boolean GetDayOfYN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + if ((result.flags & ParseFlags.HaveDate) != 0) + { + // Multiple dates in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + if (SetDateYMD(ref result, raw.year, raw.GetNumber(0), 1)) + { + result.flags |= ParseFlags.HaveDate; + return true; + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + private static Boolean GetDayOfYM(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + if ((result.flags & ParseFlags.HaveDate) != 0) + { + // Multiple dates in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + if (SetDateYMD(ref result, raw.year, raw.month, 1)) + { + result.flags |= ParseFlags.HaveDate; + return true; + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + private static void AdjustTimeMark(DateTimeFormatInfo dtfi, ref DateTimeRawInfo raw) + { + // Specail case for culture which uses AM as empty string. + // E.g. af-ZA (0x0436) + // S1159 \x0000 + // S2359 nm + // In this case, if we are parsing a string like "2005/09/14 12:23", we will assume this is in AM. + + if (raw.timeMark == TM.NotSet) + { + if (dtfi.AMDesignator != null && dtfi.PMDesignator != null) + { + if (dtfi.AMDesignator.Length == 0 && dtfi.PMDesignator.Length != 0) + { + raw.timeMark = TM.AM; + } + if (dtfi.PMDesignator.Length == 0 && dtfi.AMDesignator.Length != 0) + { + raw.timeMark = TM.PM; + } + } + } + } + + // + // Adjust hour according to the time mark. + // + private static Boolean AdjustHour(ref int hour, TM timeMark) + { + if (timeMark != TM.NotSet) + { + if (timeMark == TM.AM) + { + if (hour < 0 || hour > 12) + { + return false; + } + hour = (hour == 12) ? 0 : hour; + } + else + { + if (hour < 0 || hour > 23) + { + return false; + } + if (hour < 12) + { + hour += 12; + } + } + } + return true; + } + + private static Boolean GetTimeOfN(DateTimeFormatInfo dtfi, ref DateTimeResult result, ref DateTimeRawInfo raw) + { + if ((result.flags & ParseFlags.HaveTime) != 0) + { + // Multiple times in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + // + // In this case, we need a time mark. Check if so. + // + if (raw.timeMark == TM.NotSet) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + result.Hour = raw.GetNumber(0); + result.flags |= ParseFlags.HaveTime; + return true; + } + + private static Boolean GetTimeOfNN(DateTimeFormatInfo dtfi, ref DateTimeResult result, ref DateTimeRawInfo raw) + { + Debug.Assert(raw.numCount >= 2, "raw.numCount >= 2"); + if ((result.flags & ParseFlags.HaveTime) != 0) + { + // Multiple times in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + result.Hour = raw.GetNumber(0); + result.Minute = raw.GetNumber(1); + result.flags |= ParseFlags.HaveTime; + return true; + } + + private static Boolean GetTimeOfNNN(DateTimeFormatInfo dtfi, ref DateTimeResult result, ref DateTimeRawInfo raw) + { + if ((result.flags & ParseFlags.HaveTime) != 0) + { + // Multiple times in the input string + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + Debug.Assert(raw.numCount >= 3, "raw.numCount >= 3"); + result.Hour = raw.GetNumber(0); + result.Minute = raw.GetNumber(1); + result.Second = raw.GetNumber(2); + result.flags |= ParseFlags.HaveTime; + return true; + } + + // + // Processing terminal state: A Date suffix followed by one number. + // + private static Boolean GetDateOfDSN(ref DateTimeResult result, ref DateTimeRawInfo raw) + { + if (raw.numCount != 1 || result.Day != -1) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + result.Day = raw.GetNumber(0); + return true; + } + + private static Boolean GetDateOfNDS(ref DateTimeResult result, ref DateTimeRawInfo raw) + { + if (result.Month == -1) + { + //Should have a month suffix + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + if (result.Year != -1) + { + // Aleady has a year suffix + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + if (!TryAdjustYear(ref result, raw.GetNumber(0), out result.Year)) + { + // the year value is out of range + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + result.Day = 1; + return true; + } + + private static Boolean GetDateOfNNDS(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + // For partial CJK Dates, the only valid formats are with a specified year, followed by two numbers, which + // will be the Month and Day, and with a specified Month, when the numbers are either the year and day or + // day and year, depending on the short date pattern. + + if ((result.flags & ParseFlags.HaveYear) != 0) + { + if (((result.flags & ParseFlags.HaveMonth) == 0) && ((result.flags & ParseFlags.HaveDay) == 0)) + { + if (TryAdjustYear(ref result, raw.year, out result.Year) && SetDateYMD(ref result, result.Year, raw.GetNumber(0), raw.GetNumber(1))) + { + return true; + } + } + } + else if ((result.flags & ParseFlags.HaveMonth) != 0) + { + if (((result.flags & ParseFlags.HaveYear) == 0) && ((result.flags & ParseFlags.HaveDay) == 0)) + { + int order; + if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order)) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_BadDatePattern", dtfi.ShortDatePattern); + return false; + } + int year; + if (order == ORDER_YMD) + { + if (TryAdjustYear(ref result, raw.GetNumber(0), out year) && SetDateYMD(ref result, year, result.Month, raw.GetNumber(1))) + { + return true; + } + } + else + { + if (TryAdjustYear(ref result, raw.GetNumber(1), out year) && SetDateYMD(ref result, year, result.Month, raw.GetNumber(0))) + { + return true; + } + } + } + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + // + // A date suffix is found, use this method to put the number into the result. + // + private static bool ProcessDateTimeSuffix(ref DateTimeResult result, ref DateTimeRawInfo raw, ref DateTimeToken dtok) + { + switch (dtok.suffix) + { + case TokenType.SEP_YearSuff: + if ((result.flags & ParseFlags.HaveYear) != 0) + { + return false; + } + result.flags |= ParseFlags.HaveYear; + result.Year = raw.year = dtok.num; + break; + case TokenType.SEP_MonthSuff: + if ((result.flags & ParseFlags.HaveMonth) != 0) + { + return false; + } + result.flags |= ParseFlags.HaveMonth; + result.Month = raw.month = dtok.num; + break; + case TokenType.SEP_DaySuff: + if ((result.flags & ParseFlags.HaveDay) != 0) + { + return false; + } + result.flags |= ParseFlags.HaveDay; + result.Day = dtok.num; + break; + case TokenType.SEP_HourSuff: + if ((result.flags & ParseFlags.HaveHour) != 0) + { + return false; + } + result.flags |= ParseFlags.HaveHour; + result.Hour = dtok.num; + break; + case TokenType.SEP_MinuteSuff: + if ((result.flags & ParseFlags.HaveMinute) != 0) + { + return false; + } + result.flags |= ParseFlags.HaveMinute; + result.Minute = dtok.num; + break; + case TokenType.SEP_SecondSuff: + if ((result.flags & ParseFlags.HaveSecond) != 0) + { + return false; + } + result.flags |= ParseFlags.HaveSecond; + result.Second = dtok.num; + break; + } + return true; + } + + //////////////////////////////////////////////////////////////////////// + // + // Actions: + // This is used by DateTime.Parse(). + // Process the terminal state for the Hebrew calendar parsing. + // + //////////////////////////////////////////////////////////////////////// + + internal static Boolean ProcessHebrewTerminalState(DS dps, ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + // The following are accepted terminal state for Hebrew date. + switch (dps) + { + case DS.DX_MNN: + // Deal with the default long/short date format when the year number is ambigous (i.e. year < 100). + raw.year = raw.GetNumber(1); + if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true)) + { + result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, "Format_BadDateTimeCalendar", null); + return false; + } + if (!GetDayOfMNN(ref result, ref raw, dtfi)) + { + return false; + } + break; + case DS.DX_YMN: + // Deal with the default long/short date format when the year number is NOT ambigous (i.e. year >= 100). + if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true)) + { + result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, "Format_BadDateTimeCalendar", null); + return false; + } + if (!GetDayOfYMN(ref result, ref raw, dtfi)) + { + return false; + } + break; + case DS.DX_NM: + case DS.DX_MN: + // Deal with Month/Day pattern. + GetDefaultYear(ref result, ref styles); + if (!dtfi.YearMonthAdjustment(ref result.Year, ref raw.month, true)) + { + result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, "Format_BadDateTimeCalendar", null); + return false; + } + if (!GetHebrewDayOfNM(ref result, ref raw, dtfi)) + { + return false; + } + break; + case DS.DX_YM: + // Deal with Year/Month pattern. + if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true)) + { + result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, "Format_BadDateTimeCalendar", null); + return false; + } + if (!GetDayOfYM(ref result, ref raw, dtfi)) + { + return false; + } + break; + case DS.TX_N: + // Deal hour + AM/PM + if (!GetTimeOfN(dtfi, ref result, ref raw)) + { + return false; + } + break; + case DS.TX_NN: + if (!GetTimeOfNN(dtfi, ref result, ref raw)) + { + return false; + } + break; + case DS.TX_NNN: + if (!GetTimeOfNNN(dtfi, ref result, ref raw)) + { + return false; + } + break; + default: + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + if (dps > DS.ERROR) + { + // + // We have reached a terminal state. Reset the raw num count. + // + raw.numCount = 0; + } + return true; + } + + // + // A terminal state has been reached, call the appropriate function to fill in the parsing result. + // Return true if the state is a terminal state. + // + internal static Boolean ProcessTerminaltState(DS dps, ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi) + { + bool passed = true; + switch (dps) + { + case DS.DX_NN: + passed = GetDayOfNN(ref result, ref styles, ref raw, dtfi); + break; + case DS.DX_NNN: + passed = GetDayOfNNN(ref result, ref raw, dtfi); + break; + case DS.DX_MN: + passed = GetDayOfMN(ref result, ref styles, ref raw, dtfi); + break; + case DS.DX_NM: + passed = GetDayOfNM(ref result, ref styles, ref raw, dtfi); + break; + case DS.DX_MNN: + passed = GetDayOfMNN(ref result, ref raw, dtfi); + break; + case DS.DX_DS: + // The result has got the correct value. No need to process. + passed = true; + break; + case DS.DX_YNN: + passed = GetDayOfYNN(ref result, ref raw, dtfi); + break; + case DS.DX_NNY: + passed = GetDayOfNNY(ref result, ref raw, dtfi); + break; + case DS.DX_YMN: + passed = GetDayOfYMN(ref result, ref raw, dtfi); + break; + case DS.DX_YN: + passed = GetDayOfYN(ref result, ref raw, dtfi); + break; + case DS.DX_YM: + passed = GetDayOfYM(ref result, ref raw, dtfi); + break; + case DS.TX_N: + passed = GetTimeOfN(dtfi, ref result, ref raw); + break; + case DS.TX_NN: + passed = GetTimeOfNN(dtfi, ref result, ref raw); + break; + case DS.TX_NNN: + passed = GetTimeOfNNN(dtfi, ref result, ref raw); + break; + case DS.TX_TS: + // The result has got the correct value. Nothing to do. + passed = true; + break; + case DS.DX_DSN: + passed = GetDateOfDSN(ref result, ref raw); + break; + case DS.DX_NDS: + passed = GetDateOfNDS(ref result, ref raw); + break; + case DS.DX_NNDS: + passed = GetDateOfNNDS(ref result, ref raw, dtfi); + break; + } + + PTSTraceExit(dps, passed); + if (!passed) + { + return false; + } + + if (dps > DS.ERROR) + { + // + // We have reached a terminal state. Reset the raw num count. + // + raw.numCount = 0; + } + return true; + } + + internal static DateTime Parse(String s, DateTimeFormatInfo dtfi, DateTimeStyles styles) + { + DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result. + result.Init(); + if (TryParse(s, dtfi, styles, ref result)) + { + return result.parsedDate; + } + else + { + throw GetDateTimeParseException(ref result); + } + } + + internal static DateTime Parse(String s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out TimeSpan offset) + { + DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result. + result.Init(); + result.flags |= ParseFlags.CaptureOffset; + if (TryParse(s, dtfi, styles, ref result)) + { + offset = result.timeZoneOffset; + return result.parsedDate; + } + else + { + throw GetDateTimeParseException(ref result); + } + } + + + internal static bool TryParse(String s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out DateTime result) + { + result = DateTime.MinValue; + DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result. + resultData.Init(); + if (TryParse(s, dtfi, styles, ref resultData)) + { + result = resultData.parsedDate; + return true; + } + return false; + } + + internal static bool TryParse(String s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out DateTime result, out TimeSpan offset) + { + result = DateTime.MinValue; + offset = TimeSpan.Zero; + DateTimeResult parseResult = new DateTimeResult(); // The buffer to store the parsing result. + parseResult.Init(); + parseResult.flags |= ParseFlags.CaptureOffset; + if (TryParse(s, dtfi, styles, ref parseResult)) + { + result = parseResult.parsedDate; + offset = parseResult.timeZoneOffset; + return true; + } + return false; + } + + + // + // This is the real method to do the parsing work. + // + internal static bool TryParse(String s, DateTimeFormatInfo dtfi, DateTimeStyles styles, ref DateTimeResult result) + { + if (s == null) + { + result.SetFailure(ParseFailureKind.ArgumentNull, "ArgumentNull_String", null, nameof(s)); + return false; + } + if (s.Length == 0) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + Debug.Assert(dtfi != null, "dtfi == null"); + +#if _LOGGING + DTFITrace(dtfi); +#endif + + DateTime time; + // + // First try the predefined format. + // + + DS dps = DS.BEGIN; // Date Parsing State. + bool reachTerminalState = false; + + DateTimeToken dtok = new DateTimeToken(); // The buffer to store the parsing token. + dtok.suffix = TokenType.SEP_Unk; + DateTimeRawInfo raw = new DateTimeRawInfo(); // The buffer to store temporary parsing information. + unsafe + { + Int32* numberPointer = stackalloc Int32[3]; + raw.Init(numberPointer); + } + raw.hasSameDateAndTimeSeparators = dtfi.DateSeparator.Equals(dtfi.TimeSeparator, StringComparison.Ordinal); + + result.calendar = dtfi.Calendar; + result.era = Calendar.CurrentEra; + + // + // The string to be parsed. Use a __DTString wrapper so that we can trace the index which + // indicates the begining of next token. + // + __DTString str = new __DTString(s, dtfi); + + str.GetNext(); + + // + // The following loop will break out when we reach the end of the str. + // + do + { + // + // Call the lexer to get the next token. + // + // If we find a era in Lex(), the era value will be in raw.era. + if (!Lex(dps, ref str, ref dtok, ref raw, ref result, ref dtfi, styles)) + { + TPTraceExit("0000", dps); + return false; + } + + // + // If the token is not unknown, process it. + // Otherwise, just discard it. + // + if (dtok.dtt != DTT.Unk) + { + // + // Check if we got any CJK Date/Time suffix. + // Since the Date/Time suffix tells us the number belongs to year/month/day/hour/minute/second, + // store the number in the appropriate field in the result. + // + if (dtok.suffix != TokenType.SEP_Unk) + { + if (!ProcessDateTimeSuffix(ref result, ref raw, ref dtok)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + TPTraceExit("0010", dps); + return false; + } + + dtok.suffix = TokenType.SEP_Unk; // Reset suffix to SEP_Unk; + } + + if (dtok.dtt == DTT.NumLocalTimeMark) + { + if (dps == DS.D_YNd || dps == DS.D_YN) + { + // Consider this as ISO 8601 format: + // "yyyy-MM-dd'T'HH:mm:ss" 1999-10-31T02:00:00 + TPTraceExit("0020", dps); + return (ParseISO8601(ref raw, ref str, styles, ref result)); + } + else + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + TPTraceExit("0030", dps); + return false; + } + } + + if (raw.hasSameDateAndTimeSeparators) + { + if (dtok.dtt == DTT.YearEnd || dtok.dtt == DTT.YearSpace || dtok.dtt == DTT.YearDateSep) + { + // When time and date separators are same and we are hitting a year number while the first parsed part of the string was recognized + // as part of time (and not a date) DS.T_Nt, DS.T_NNt then change the state to be a date so we try to parse it as a date instead + if (dps == DS.T_Nt) + { + dps = DS.D_Nd; + } + if (dps == DS.T_NNt) + { + dps = DS.D_NNd; + } + } + + bool atEnd = str.AtEnd(); + if (dateParsingStates[(int)dps][(int)dtok.dtt] == DS.ERROR || atEnd) + { + switch (dtok.dtt) + { + // we have the case of Serbia have dates in forms 'd.M.yyyy.' so we can expect '.' after the date parts. + // changing the token to end with space instead of Date Separator will avoid failing the parsing. + + case DTT.YearDateSep: dtok.dtt = atEnd ? DTT.YearEnd : DTT.YearSpace; break; + case DTT.NumDatesep: dtok.dtt = atEnd ? DTT.NumEnd : DTT.NumSpace; break; + case DTT.NumTimesep: dtok.dtt = atEnd ? DTT.NumEnd : DTT.NumSpace; break; + case DTT.MonthDatesep: dtok.dtt = atEnd ? DTT.MonthEnd : DTT.MonthSpace; break; + } + } + } + + // + // Advance to the next state, and continue + // + dps = dateParsingStates[(int)dps][(int)dtok.dtt]; + + if (dps == DS.ERROR) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + TPTraceExit("0040 (invalid state transition)", dps); + return false; + } + else if (dps > DS.ERROR) + { + if ((dtfi.FormatFlags & DateTimeFormatFlags.UseHebrewRule) != 0) + { + if (!ProcessHebrewTerminalState(dps, ref result, ref styles, ref raw, dtfi)) + { + TPTraceExit("0050 (ProcessHebrewTerminalState)", dps); + return false; + } + } + else + { + if (!ProcessTerminaltState(dps, ref result, ref styles, ref raw, dtfi)) + { + TPTraceExit("0060 (ProcessTerminaltState)", dps); + return false; + } + } + reachTerminalState = true; + + // + // If we have reached a terminal state, start over from DS.BEGIN again. + // For example, when we parsed "1999-12-23 13:30", we will reach a terminal state at "1999-12-23", + // and we start over so we can continue to parse "12:30". + // + dps = DS.BEGIN; + } + } + } while (dtok.dtt != DTT.End && dtok.dtt != DTT.NumEnd && dtok.dtt != DTT.MonthEnd); + + if (!reachTerminalState) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + TPTraceExit("0070 (did not reach terminal state)", dps); + return false; + } + + AdjustTimeMark(dtfi, ref raw); + if (!AdjustHour(ref result.Hour, raw.timeMark)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + TPTraceExit("0080 (AdjustHour)", dps); + return false; + } + + // Check if the parased string only contains hour/minute/second values. + bool bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1); + + // + // Check if any year/month/day is missing in the parsing string. + // If yes, get the default value from today's date. + // + if (!CheckDefaultDateTime(ref result, ref result.calendar, styles)) + { + TPTraceExit("0090 (failed to fill in missing year/month/day defaults)", dps); + return false; + } + + if (!result.calendar.TryToDateTime(result.Year, result.Month, result.Day, + result.Hour, result.Minute, result.Second, 0, result.era, out time)) + { + result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, "Format_BadDateTimeCalendar", null); + TPTraceExit("0100 (result.calendar.TryToDateTime)", dps); + return false; + } + if (raw.fraction > 0) + { + time = time.AddTicks((long)Math.Round(raw.fraction * Calendar.TicksPerSecond)); + } + + // + // We have to check day of week before we adjust to the time zone. + // Otherwise, the value of day of week may change after adjustting to the time zone. + // + if (raw.dayOfWeek != -1) + { + // + // Check if day of week is correct. + // + if (raw.dayOfWeek != (int)result.calendar.GetDayOfWeek(time)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDayOfWeek", null); + TPTraceExit("0110 (dayOfWeek check)", dps); + return false; + } + } + + result.parsedDate = time; + + if (!DetermineTimeZoneAdjustments(ref result, styles, bTimeOnly)) + { + TPTraceExit("0120 (DetermineTimeZoneAdjustments)", dps); + return false; + } + TPTraceExit("0130 (success)", dps); + return true; + } + + + // Handles time zone adjustments and sets DateTimeKind values as required by the styles + private static Boolean DetermineTimeZoneAdjustments(ref DateTimeResult result, DateTimeStyles styles, Boolean bTimeOnly) + { + if ((result.flags & ParseFlags.CaptureOffset) != 0) + { + // This is a DateTimeOffset parse, so the offset will actually be captured directly, and + // no adjustment is required in most cases + return DateTimeOffsetTimeZonePostProcessing(ref result, styles); + } + else + { + Int64 offsetTicks = result.timeZoneOffset.Ticks; + + // the DateTime offset must be within +- 14:00 hours. + if (offsetTicks < DateTimeOffset.MinOffset || offsetTicks > DateTimeOffset.MaxOffset) + { + result.SetFailure(ParseFailureKind.Format, "Format_OffsetOutOfRange", null); + return false; + } + } + + // The flags AssumeUniveral and AssumeLocal only apply when the input does not have a time zone + if ((result.flags & ParseFlags.TimeZoneUsed) == 0) + { + // If AssumeLocal or AssumeLocal is used, there will always be a kind specified. As in the + // case when a time zone is present, it will default to being local unless AdjustToUniversal + // is present. These comparisons determine whether setting the kind is sufficient, or if a + // time zone adjustment is required. For consistentcy with the rest of parsing, it is desirable + // to fall through to the Adjust methods below, so that there is consist handling of boundary + // cases like wrapping around on time-only dates and temporarily allowing an adjusted date + // to exceed DateTime.MaxValue + if ((styles & DateTimeStyles.AssumeLocal) != 0) + { + if ((styles & DateTimeStyles.AdjustToUniversal) != 0) + { + result.flags |= ParseFlags.TimeZoneUsed; + result.timeZoneOffset = TimeZoneInfo.GetLocalUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime); + } + else + { + result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Local); + return true; + } + } + else if ((styles & DateTimeStyles.AssumeUniversal) != 0) + { + if ((styles & DateTimeStyles.AdjustToUniversal) != 0) + { + result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Utc); + return true; + } + else + { + result.flags |= ParseFlags.TimeZoneUsed; + result.timeZoneOffset = TimeSpan.Zero; + } + } + else + { + // No time zone and no Assume flags, so DateTimeKind.Unspecified is fine + Debug.Assert(result.parsedDate.Kind == DateTimeKind.Unspecified, "result.parsedDate.Kind == DateTimeKind.Unspecified"); + return true; + } + } + + if (((styles & DateTimeStyles.RoundtripKind) != 0) && ((result.flags & ParseFlags.TimeZoneUtc) != 0)) + { + result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Utc); + return true; + } + + if ((styles & DateTimeStyles.AdjustToUniversal) != 0) + { + return (AdjustTimeZoneToUniversal(ref result)); + } + return (AdjustTimeZoneToLocal(ref result, bTimeOnly)); + } + + // Apply validation and adjustments specific to DateTimeOffset + private static Boolean DateTimeOffsetTimeZonePostProcessing(ref DateTimeResult result, DateTimeStyles styles) + { + // For DateTimeOffset, default to the Utc or Local offset when an offset was not specified by + // the input string. + if ((result.flags & ParseFlags.TimeZoneUsed) == 0) + { + if ((styles & DateTimeStyles.AssumeUniversal) != 0) + { + // AssumeUniversal causes the offset to default to zero (0) + result.timeZoneOffset = TimeSpan.Zero; + } + else + { + // AssumeLocal causes the offset to default to Local. This flag is on by default for DateTimeOffset. + result.timeZoneOffset = TimeZoneInfo.GetLocalUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime); + } + } + + Int64 offsetTicks = result.timeZoneOffset.Ticks; + + // there should be no overflow, because the offset can be no more than -+100 hours and the date already + // fits within a DateTime. + Int64 utcTicks = result.parsedDate.Ticks - offsetTicks; + + // For DateTimeOffset, both the parsed time and the corresponding UTC value must be within the boundaries + // of a DateTime instance. + if (utcTicks < DateTime.MinTicks || utcTicks > DateTime.MaxTicks) + { + result.SetFailure(ParseFailureKind.Format, "Format_UTCOutOfRange", null); + return false; + } + + // the offset must be within +- 14:00 hours. + if (offsetTicks < DateTimeOffset.MinOffset || offsetTicks > DateTimeOffset.MaxOffset) + { + result.SetFailure(ParseFailureKind.Format, "Format_OffsetOutOfRange", null); + return false; + } + + // DateTimeOffset should still honor the AdjustToUniversal flag for consistency with DateTime. It means you + // want to return an adjusted UTC value, so store the utcTicks in the DateTime and set the offset to zero + if ((styles & DateTimeStyles.AdjustToUniversal) != 0) + { + if (((result.flags & ParseFlags.TimeZoneUsed) == 0) && ((styles & DateTimeStyles.AssumeUniversal) == 0)) + { + // Handle the special case where the timeZoneOffset was defaulted to Local + Boolean toUtcResult = AdjustTimeZoneToUniversal(ref result); + result.timeZoneOffset = TimeSpan.Zero; + return toUtcResult; + } + + // The constructor should always succeed because of the range check earlier in the function + // Althought it is UTC, internally DateTimeOffset does not use this flag + result.parsedDate = new DateTime(utcTicks, DateTimeKind.Utc); + result.timeZoneOffset = TimeSpan.Zero; + } + + return true; + } + + + // + // Adjust the specified time to universal time based on the supplied timezone. + // E.g. when parsing "2001/06/08 14:00-07:00", + // the time is 2001/06/08 14:00, and timeZoneOffset = -07:00. + // The result will be "2001/06/08 21:00" + // + private static Boolean AdjustTimeZoneToUniversal(ref DateTimeResult result) + { + long resultTicks = result.parsedDate.Ticks; + resultTicks -= result.timeZoneOffset.Ticks; + if (resultTicks < 0) + { + resultTicks += Calendar.TicksPerDay; + } + + if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks) + { + result.SetFailure(ParseFailureKind.Format, "Format_DateOutOfRange", null); + return false; + } + result.parsedDate = new DateTime(resultTicks, DateTimeKind.Utc); + return true; + } + + // + // Adjust the specified time to universal time based on the supplied timezone, + // and then convert to local time. + // E.g. when parsing "2001/06/08 14:00-04:00", and local timezone is GMT-7. + // the time is 2001/06/08 14:00, and timeZoneOffset = -05:00. + // The result will be "2001/06/08 11:00" + // + private static Boolean AdjustTimeZoneToLocal(ref DateTimeResult result, bool bTimeOnly) + { + long resultTicks = result.parsedDate.Ticks; + // Convert to local ticks + TimeZoneInfo tz = TimeZoneInfo.Local; + Boolean isAmbiguousLocalDst = false; + if (resultTicks < Calendar.TicksPerDay) + { + // + // This is time of day. + // + + // Adjust timezone. + resultTicks -= result.timeZoneOffset.Ticks; + // If the time is time of day, use the current timezone offset. + resultTicks += tz.GetUtcOffset(bTimeOnly ? DateTime.Now : result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime).Ticks; + + if (resultTicks < 0) + { + resultTicks += Calendar.TicksPerDay; + } + } + else + { + // Adjust timezone to GMT. + resultTicks -= result.timeZoneOffset.Ticks; + if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks) + { + // If the result ticks is greater than DateTime.MaxValue, we can not create a DateTime from this ticks. + // In this case, keep using the old code. + resultTicks += tz.GetUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime).Ticks; + } + else + { + // Convert the GMT time to local time. + DateTime utcDt = new DateTime(resultTicks, DateTimeKind.Utc); + Boolean isDaylightSavings = false; + resultTicks += TimeZoneInfo.GetUtcOffsetFromUtc(utcDt, TimeZoneInfo.Local, out isDaylightSavings, out isAmbiguousLocalDst).Ticks; + } + } + if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks) + { + result.parsedDate = DateTime.MinValue; + result.SetFailure(ParseFailureKind.Format, "Format_DateOutOfRange", null); + return false; + } + result.parsedDate = new DateTime(resultTicks, DateTimeKind.Local, isAmbiguousLocalDst); + return true; + } + + // + // Parse the ISO8601 format string found during Parse(); + // + // + private static bool ParseISO8601(ref DateTimeRawInfo raw, ref __DTString str, DateTimeStyles styles, ref DateTimeResult result) + { + if (raw.year < 0 || raw.GetNumber(0) < 0 || raw.GetNumber(1) < 0) + { + } + str.Index--; + int hour, minute; + int second = 0; + double partSecond = 0; + + str.SkipWhiteSpaces(); + if (!ParseDigits(ref str, 2, out hour)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + str.SkipWhiteSpaces(); + if (!str.Match(':')) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + str.SkipWhiteSpaces(); + if (!ParseDigits(ref str, 2, out minute)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + str.SkipWhiteSpaces(); + if (str.Match(':')) + { + str.SkipWhiteSpaces(); + if (!ParseDigits(ref str, 2, out second)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + if (str.Match('.')) + { + if (!ParseFraction(ref str, out partSecond)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + str.Index--; + } + str.SkipWhiteSpaces(); + } + if (str.GetNext()) + { + char ch = str.GetChar(); + if (ch == '+' || ch == '-') + { + result.flags |= ParseFlags.TimeZoneUsed; + if (!ParseTimeZone(ref str, ref result.timeZoneOffset)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + } + else if (ch == 'Z' || ch == 'z') + { + result.flags |= ParseFlags.TimeZoneUsed; + result.timeZoneOffset = TimeSpan.Zero; + result.flags |= ParseFlags.TimeZoneUtc; + } + else + { + str.Index--; + } + str.SkipWhiteSpaces(); + if (str.Match('#')) + { + if (!VerifyValidPunctuation(ref str)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + str.SkipWhiteSpaces(); + } + if (str.Match('\0')) + { + if (!VerifyValidPunctuation(ref str)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + } + if (str.GetNext()) + { + // If this is true, there were non-white space characters remaining in the DateTime + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + } + + DateTime time; + Calendar calendar = GregorianCalendar.GetDefaultInstance(); + if (!calendar.TryToDateTime(raw.year, raw.GetNumber(0), raw.GetNumber(1), + hour, minute, second, 0, result.era, out time)) + { + result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, "Format_BadDateTimeCalendar", null); + return false; + } + + time = time.AddTicks((long)Math.Round(partSecond * Calendar.TicksPerSecond)); + result.parsedDate = time; + if (!DetermineTimeZoneAdjustments(ref result, styles, false)) + { + return false; + } + return true; + } + + + //////////////////////////////////////////////////////////////////////// + // + // Actions: + // Parse the current word as a Hebrew number. + // This is used by DateTime.ParseExact(). + // + //////////////////////////////////////////////////////////////////////// + + internal static bool MatchHebrewDigits(ref __DTString str, int digitLen, out int number) + { + number = 0; + + // Create a context object so that we can parse the Hebrew number text character by character. + HebrewNumberParsingContext context = new HebrewNumberParsingContext(0); + + // Set this to ContinueParsing so that we will run the following while loop in the first time. + HebrewNumberParsingState state = HebrewNumberParsingState.ContinueParsing; + + while (state == HebrewNumberParsingState.ContinueParsing && str.GetNext()) + { + state = HebrewNumber.ParseByChar(str.GetChar(), ref context); + } + + if (state == HebrewNumberParsingState.FoundEndOfHebrewNumber) + { + // If we have reached a terminal state, update the result and returns. + number = context.result; + return (true); + } + + // If we run out of the character before reaching FoundEndOfHebrewNumber, or + // the state is InvalidHebrewNumber or ContinueParsing, we fail to match a Hebrew number. + // Return an error. + return false; + } + + /*=================================ParseDigits================================== + **Action: Parse the number string in __DTString that are formatted using + ** the following patterns: + ** "0", "00", and "000..0" + **Returns: the integer value + **Arguments: str: a __DTString. The parsing will start from the + ** next character after str.Index. + **Exceptions: FormatException if error in parsing number. + ==============================================================================*/ + + internal static bool ParseDigits(ref __DTString str, int digitLen, out int result) + { + if (digitLen == 1) + { + // 1 really means 1 or 2 for this call + return ParseDigits(ref str, 1, 2, out result); + } + else + { + return ParseDigits(ref str, digitLen, digitLen, out result); + } + } + + internal static bool ParseDigits(ref __DTString str, int minDigitLen, int maxDigitLen, out int result) + { + Debug.Assert(minDigitLen > 0, "minDigitLen > 0"); + Debug.Assert(maxDigitLen < 9, "maxDigitLen < 9"); + Debug.Assert(minDigitLen <= maxDigitLen, "minDigitLen <= maxDigitLen"); + result = 0; + int startingIndex = str.Index; + int tokenLength = 0; + while (tokenLength < maxDigitLen) + { + if (!str.GetNextDigit()) + { + str.Index--; + break; + } + result = result * 10 + str.GetDigit(); + tokenLength++; + } + if (tokenLength < minDigitLen) + { + str.Index = startingIndex; + return false; + } + return true; + } + + /*=================================ParseFractionExact================================== + **Action: Parse the number string in __DTString that are formatted using + ** the following patterns: + ** "0", "00", and "000..0" + **Returns: the fraction value + **Arguments: str: a __DTString. The parsing will start from the + ** next character after str.Index. + **Exceptions: FormatException if error in parsing number. + ==============================================================================*/ + + private static bool ParseFractionExact(ref __DTString str, int maxDigitLen, ref double result) + { + if (!str.GetNextDigit()) + { + str.Index--; + return false; + } + result = str.GetDigit(); + + int digitLen = 1; + for (; digitLen < maxDigitLen; digitLen++) + { + if (!str.GetNextDigit()) + { + str.Index--; + break; + } + result = result * 10 + str.GetDigit(); + } + + result = ((double)result / Math.Pow(10, digitLen)); + return (digitLen == maxDigitLen); + } + + /*=================================ParseSign================================== + **Action: Parse a positive or a negative sign. + **Returns: true if postive sign. flase if negative sign. + **Arguments: str: a __DTString. The parsing will start from the + ** next character after str.Index. + **Exceptions: FormatException if end of string is encountered or a sign + ** symbol is not found. + ==============================================================================*/ + + private static bool ParseSign(ref __DTString str, ref bool result) + { + if (!str.GetNext()) + { + // A sign symbol ('+' or '-') is expected. However, end of string is encountered. + return false; + } + char ch = str.GetChar(); + if (ch == '+') + { + result = true; + return (true); + } + else if (ch == '-') + { + result = false; + return (true); + } + // A sign symbol ('+' or '-') is expected. + return false; + } + + /*=================================ParseTimeZoneOffset================================== + **Action: Parse the string formatted using "z", "zz", "zzz" in DateTime.Format(). + **Returns: the TimeSpan for the parsed timezone offset. + **Arguments: str: a __DTString. The parsing will start from the + ** next character after str.Index. + ** len: the repeated number of the "z" + **Exceptions: FormatException if errors in parsing. + ==============================================================================*/ + + private static bool ParseTimeZoneOffset(ref __DTString str, int len, ref TimeSpan result) + { + bool isPositive = true; + int hourOffset; + int minuteOffset = 0; + + switch (len) + { + case 1: + case 2: + if (!ParseSign(ref str, ref isPositive)) + { + return (false); + } + if (!ParseDigits(ref str, len, out hourOffset)) + { + return (false); + } + break; + default: + if (!ParseSign(ref str, ref isPositive)) + { + return (false); + } + + // Parsing 1 digit will actually parse 1 or 2. + if (!ParseDigits(ref str, 1, out hourOffset)) + { + return (false); + } + // ':' is optional. + if (str.Match(":")) + { + // Found ':' + if (!ParseDigits(ref str, 2, out minuteOffset)) + { + return (false); + } + } + else + { + // Since we can not match ':', put the char back. + str.Index--; + if (!ParseDigits(ref str, 2, out minuteOffset)) + { + return (false); + } + } + break; + } + if (minuteOffset < 0 || minuteOffset >= 60) + { + return false; + } + + result = (new TimeSpan(hourOffset, minuteOffset, 0)); + if (!isPositive) + { + result = result.Negate(); + } + return (true); + } + + /*=================================MatchAbbreviatedMonthName================================== + **Action: Parse the abbreviated month name from string starting at str.Index. + **Returns: A value from 1 to 12 for the first month to the twelveth month. + **Arguments: str: a __DTString. The parsing will start from the + ** next character after str.Index. + **Exceptions: FormatException if an abbreviated month name can not be found. + ==============================================================================*/ + + private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result) + { + int maxMatchStrLen = 0; + result = -1; + if (str.GetNext()) + { + // + // Scan the month names (note that some calendars has 13 months) and find + // the matching month name which has the max string length. + // We need to do this because some cultures (e.g. "cs-CZ") which have + // abbreviated month names with the same prefix. + // + int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13); + for (int i = 1; i <= monthsInYear; i++) + { + String searchStr = dtfi.GetAbbreviatedMonthName(i); + int matchStrLen = searchStr.Length; + if (dtfi.HasSpacesInMonthNames + ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen) + : str.MatchSpecifiedWord(searchStr)) + { + if (matchStrLen > maxMatchStrLen) + { + maxMatchStrLen = matchStrLen; + result = i; + } + } + } + + // Search leap year form. + if ((dtfi.FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0) + { + int tempResult = str.MatchLongestWords(dtfi.internalGetLeapYearMonthNames(), ref maxMatchStrLen); + // We found a longer match in the leap year month name. Use this as the result. + // The result from MatchLongestWords is 0 ~ length of word array. + // So we increment the result by one to become the month value. + if (tempResult >= 0) + { + result = tempResult + 1; + } + } + } + if (result > 0) + { + str.Index += (maxMatchStrLen - 1); + return (true); + } + return false; + } + + /*=================================MatchMonthName================================== + **Action: Parse the month name from string starting at str.Index. + **Returns: A value from 1 to 12 indicating the first month to the twelveth month. + **Arguments: str: a __DTString. The parsing will start from the + ** next character after str.Index. + **Exceptions: FormatException if a month name can not be found. + ==============================================================================*/ + + private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result) + { + int maxMatchStrLen = 0; + result = -1; + if (str.GetNext()) + { + // + // Scan the month names (note that some calendars has 13 months) and find + // the matching month name which has the max string length. + // We need to do this because some cultures (e.g. "vi-VN") which have + // month names with the same prefix. + // + int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13); + for (int i = 1; i <= monthsInYear; i++) + { + String searchStr = dtfi.GetMonthName(i); + int matchStrLen = searchStr.Length; + if (dtfi.HasSpacesInMonthNames + ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen) + : str.MatchSpecifiedWord(searchStr)) + { + if (matchStrLen > maxMatchStrLen) + { + maxMatchStrLen = matchStrLen; + result = i; + } + } + } + + // Search genitive form. + if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0) + { + int tempResult = str.MatchLongestWords(dtfi.MonthGenitiveNames, ref maxMatchStrLen); + // We found a longer match in the genitive month name. Use this as the result. + // The result from MatchLongestWords is 0 ~ length of word array. + // So we increment the result by one to become the month value. + if (tempResult >= 0) + { + result = tempResult + 1; + } + } + + // Search leap year form. + if ((dtfi.FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0) + { + int tempResult = str.MatchLongestWords(dtfi.internalGetLeapYearMonthNames(), ref maxMatchStrLen); + // We found a longer match in the leap year month name. Use this as the result. + // The result from MatchLongestWords is 0 ~ length of word array. + // So we increment the result by one to become the month value. + if (tempResult >= 0) + { + result = tempResult + 1; + } + } + } + + if (result > 0) + { + str.Index += (maxMatchStrLen - 1); + return (true); + } + return false; + } + + /*=================================MatchAbbreviatedDayName================================== + **Action: Parse the abbreviated day of week name from string starting at str.Index. + **Returns: A value from 0 to 6 indicating Sunday to Saturday. + **Arguments: str: a __DTString. The parsing will start from the + ** next character after str.Index. + **Exceptions: FormatException if a abbreviated day of week name can not be found. + ==============================================================================*/ + + private static bool MatchAbbreviatedDayName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result) + { + int maxMatchStrLen = 0; + result = -1; + if (str.GetNext()) + { + for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++) + { + String searchStr = dtfi.GetAbbreviatedDayName(i); + int matchStrLen = searchStr.Length; + if (dtfi.HasSpacesInDayNames + ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen) + : str.MatchSpecifiedWord(searchStr)) + { + if (matchStrLen > maxMatchStrLen) + { + maxMatchStrLen = matchStrLen; + result = (int)i; + } + } + } + } + if (result >= 0) + { + str.Index += maxMatchStrLen - 1; + return (true); + } + return false; + } + + /*=================================MatchDayName================================== + **Action: Parse the day of week name from string starting at str.Index. + **Returns: A value from 0 to 6 indicating Sunday to Saturday. + **Arguments: str: a __DTString. The parsing will start from the + ** next character after str.Index. + **Exceptions: FormatException if a day of week name can not be found. + ==============================================================================*/ + + private static bool MatchDayName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result) + { + // Turkish (tr-TR) got day names with the same prefix. + int maxMatchStrLen = 0; + result = -1; + if (str.GetNext()) + { + for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++) + { + String searchStr = dtfi.GetDayName(i); + int matchStrLen = searchStr.Length; + if (dtfi.HasSpacesInDayNames + ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen) + : str.MatchSpecifiedWord(searchStr)) + { + if (matchStrLen > maxMatchStrLen) + { + maxMatchStrLen = matchStrLen; + result = (int)i; + } + } + } + } + if (result >= 0) + { + str.Index += maxMatchStrLen - 1; + return (true); + } + return false; + } + + /*=================================MatchEraName================================== + **Action: Parse era name from string starting at str.Index. + **Returns: An era value. + **Arguments: str: a __DTString. The parsing will start from the + ** next character after str.Index. + **Exceptions: FormatException if an era name can not be found. + ==============================================================================*/ + + private static bool MatchEraName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result) + { + if (str.GetNext()) + { + int[] eras = dtfi.Calendar.Eras; + + if (eras != null) + { + for (int i = 0; i < eras.Length; i++) + { + String searchStr = dtfi.GetEraName(eras[i]); + if (str.MatchSpecifiedWord(searchStr)) + { + str.Index += (searchStr.Length - 1); + result = eras[i]; + return (true); + } + searchStr = dtfi.GetAbbreviatedEraName(eras[i]); + if (str.MatchSpecifiedWord(searchStr)) + { + str.Index += (searchStr.Length - 1); + result = eras[i]; + return (true); + } + } + } + } + return false; + } + + /*=================================MatchTimeMark================================== + **Action: Parse the time mark (AM/PM) from string starting at str.Index. + **Returns: TM_AM or TM_PM. + **Arguments: str: a __DTString. The parsing will start from the + ** next character after str.Index. + **Exceptions: FormatException if a time mark can not be found. + ==============================================================================*/ + + private static bool MatchTimeMark(ref __DTString str, DateTimeFormatInfo dtfi, ref TM result) + { + result = TM.NotSet; + // In some cultures have empty strings in AM/PM mark. E.g. af-ZA (0x0436), the AM mark is "", and PM mark is "nm". + if (dtfi.AMDesignator.Length == 0) + { + result = TM.AM; + } + if (dtfi.PMDesignator.Length == 0) + { + result = TM.PM; + } + + if (str.GetNext()) + { + String searchStr = dtfi.AMDesignator; + if (searchStr.Length > 0) + { + if (str.MatchSpecifiedWord(searchStr)) + { + // Found an AM timemark with length > 0. + str.Index += (searchStr.Length - 1); + result = TM.AM; + return (true); + } + } + searchStr = dtfi.PMDesignator; + if (searchStr.Length > 0) + { + if (str.MatchSpecifiedWord(searchStr)) + { + // Found a PM timemark with length > 0. + str.Index += (searchStr.Length - 1); + result = TM.PM; + return (true); + } + } + str.Index--; // Undo the GetNext call. + } + if (result != TM.NotSet) + { + // If one of the AM/PM marks is empty string, return the result. + return (true); + } + return false; + } + + /*=================================MatchAbbreviatedTimeMark================================== + **Action: Parse the abbreviated time mark (AM/PM) from string starting at str.Index. + **Returns: TM_AM or TM_PM. + **Arguments: str: a __DTString. The parsing will start from the + ** next character after str.Index. + **Exceptions: FormatException if a abbreviated time mark can not be found. + ==============================================================================*/ + + private static bool MatchAbbreviatedTimeMark(ref __DTString str, DateTimeFormatInfo dtfi, ref TM result) + { + // NOTENOTE : the assumption here is that abbreviated time mark is the first + // character of the AM/PM designator. If this invariant changes, we have to + // change the code below. + if (str.GetNext()) + { + if (str.GetChar() == dtfi.AMDesignator[0]) + { + result = TM.AM; + return (true); + } + if (str.GetChar() == dtfi.PMDesignator[0]) + { + result = TM.PM; + return (true); + } + } + return false; + } + + /*=================================CheckNewValue================================== + **Action: Check if currentValue is initialized. If not, return the newValue. + ** If yes, check if the current value is equal to newValue. Return false + ** if they are not equal. This is used to check the case like "d" and "dd" are both + ** used to format a string. + **Returns: the correct value for currentValue. + **Arguments: + **Exceptions: + ==============================================================================*/ + + private static bool CheckNewValue(ref int currentValue, int newValue, char patternChar, ref DateTimeResult result) + { + if (currentValue == -1) + { + currentValue = newValue; + return (true); + } + else + { + if (newValue != currentValue) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_RepeatDateTimePattern", patternChar); + return (false); + } + } + return (true); + } + + private static DateTime GetDateTimeNow(ref DateTimeResult result, ref DateTimeStyles styles) + { + if ((result.flags & ParseFlags.CaptureOffset) != 0) + { + if ((result.flags & ParseFlags.TimeZoneUsed) != 0) + { + // use the supplied offset to calculate 'Now' + return new DateTime(DateTime.UtcNow.Ticks + result.timeZoneOffset.Ticks, DateTimeKind.Unspecified); + } + else if ((styles & DateTimeStyles.AssumeUniversal) != 0) + { + // assume the offset is Utc + return DateTime.UtcNow; + } + } + + // assume the offset is Local + return DateTime.Now; + } + + private static bool CheckDefaultDateTime(ref DateTimeResult result, ref Calendar cal, DateTimeStyles styles) + { + if ((result.flags & ParseFlags.CaptureOffset) != 0) + { + // DateTimeOffset.Parse should allow dates without a year, but only if there is also no time zone marker; + // e.g. "May 1 5pm" is OK, but "May 1 5pm -08:30" is not. This is somewhat pragmatic, since we would + // have to rearchitect parsing completely to allow this one case to correctly handle things like leap + // years and leap months. Is is an extremely corner case, and DateTime is basically incorrect in that + // case today. + // + // values like "11:00Z" or "11:00 -3:00" are also acceptable + // + // if ((month or day is set) and (year is not set and time zone is set)) + // + if (((result.Month != -1) || (result.Day != -1)) + && ((result.Year == -1 || ((result.flags & ParseFlags.YearDefault) != 0)) && (result.flags & ParseFlags.TimeZoneUsed) != 0)) + { + result.SetFailure(ParseFailureKind.Format, "Format_MissingIncompleteDate", null); + return false; + } + } + + + if ((result.Year == -1) || (result.Month == -1) || (result.Day == -1)) + { + /* + The following table describes the behaviors of getting the default value + when a certain year/month/day values are missing. + + An "X" means that the value exists. And "--" means that value is missing. + + Year Month Day => ResultYear ResultMonth ResultDay Note + + X X X Parsed year Parsed month Parsed day + X X -- Parsed Year Parsed month First day If we have year and month, assume the first day of that month. + X -- X Parsed year First month Parsed day If the month is missing, assume first month of that year. + X -- -- Parsed year First month First day If we have only the year, assume the first day of that year. + + -- X X CurrentYear Parsed month Parsed day If the year is missing, assume the current year. + -- X -- CurrentYear Parsed month First day If we have only a month value, assume the current year and current day. + -- -- X CurrentYear First month Parsed day If we have only a day value, assume current year and first month. + -- -- -- CurrentYear Current month Current day So this means that if the date string only contains time, you will get current date. + + */ + + DateTime now = GetDateTimeNow(ref result, ref styles); + if (result.Month == -1 && result.Day == -1) + { + if (result.Year == -1) + { + if ((styles & DateTimeStyles.NoCurrentDateDefault) != 0) + { + // If there is no year/month/day values, and NoCurrentDateDefault flag is used, + // set the year/month/day value to the beginning year/month/day of DateTime(). + // Note we should be using Gregorian for the year/month/day. + cal = GregorianCalendar.GetDefaultInstance(); + result.Year = result.Month = result.Day = 1; + } + else + { + // Year/Month/Day are all missing. + result.Year = cal.GetYear(now); + result.Month = cal.GetMonth(now); + result.Day = cal.GetDayOfMonth(now); + } + } + else + { + // Month/Day are both missing. + result.Month = 1; + result.Day = 1; + } + } + else + { + if (result.Year == -1) + { + result.Year = cal.GetYear(now); + } + if (result.Month == -1) + { + result.Month = 1; + } + if (result.Day == -1) + { + result.Day = 1; + } + } + } + // Set Hour/Minute/Second to zero if these value are not in str. + if (result.Hour == -1) result.Hour = 0; + if (result.Minute == -1) result.Minute = 0; + if (result.Second == -1) result.Second = 0; + if (result.era == -1) result.era = Calendar.CurrentEra; + return true; + } + + // Expand a pre-defined format string (like "D" for long date) to the real format that + // we are going to use in the date time parsing. + // This method also set the dtfi according/parseInfo to some special pre-defined + // formats. + // + private static String ExpandPredefinedFormat(String format, ref DateTimeFormatInfo dtfi, ref ParsingInfo parseInfo, ref DateTimeResult result) + { + // + // Check the format to see if we need to override the dtfi to be InvariantInfo, + // and see if we need to set up the userUniversalTime flag. + // + switch (format[0]) + { + case 'o': + case 'O': // Round Trip Format + parseInfo.calendar = GregorianCalendar.GetDefaultInstance(); + dtfi = DateTimeFormatInfo.InvariantInfo; + break; + case 'r': + case 'R': // RFC 1123 Standard. (in Universal time) + parseInfo.calendar = GregorianCalendar.GetDefaultInstance(); + dtfi = DateTimeFormatInfo.InvariantInfo; + + if ((result.flags & ParseFlags.CaptureOffset) != 0) + { + result.flags |= ParseFlags.Rfc1123Pattern; + } + break; + case 's': // Sortable format (in local time) + dtfi = DateTimeFormatInfo.InvariantInfo; + parseInfo.calendar = GregorianCalendar.GetDefaultInstance(); + break; + case 'u': // Universal time format in sortable format. + parseInfo.calendar = GregorianCalendar.GetDefaultInstance(); + dtfi = DateTimeFormatInfo.InvariantInfo; + + if ((result.flags & ParseFlags.CaptureOffset) != 0) + { + result.flags |= ParseFlags.UtcSortPattern; + } + break; + case 'U': // Universal time format with culture-dependent format. + parseInfo.calendar = GregorianCalendar.GetDefaultInstance(); + result.flags |= ParseFlags.TimeZoneUsed; + result.timeZoneOffset = new TimeSpan(0); + result.flags |= ParseFlags.TimeZoneUtc; + if (dtfi.Calendar.GetType() != typeof(GregorianCalendar)) + { + dtfi = (DateTimeFormatInfo)dtfi.Clone(); + dtfi.Calendar = GregorianCalendar.GetDefaultInstance(); + } + break; + } + + // + // Expand the pre-defined format character to the real format from DateTimeFormatInfo. + // + return (DateTimeFormat.GetRealFormat(format, dtfi)); + } + + + + + + // Given a specified format character, parse and update the parsing result. + // + private static bool ParseByFormat( + ref __DTString str, + ref __DTString format, + ref ParsingInfo parseInfo, + DateTimeFormatInfo dtfi, + ref DateTimeResult result) + { + int tokenLen = 0; + int tempYear = 0, tempMonth = 0, tempDay = 0, tempDayOfWeek = 0, tempHour = 0, tempMinute = 0, tempSecond = 0; + double tempFraction = 0; + TM tempTimeMark = 0; + + char ch = format.GetChar(); + + switch (ch) + { + case 'y': + tokenLen = format.GetRepeatCount(); + bool parseResult; + if (dtfi.HasForceTwoDigitYears) + { + parseResult = ParseDigits(ref str, 1, 4, out tempYear); + } + else + { + if (tokenLen <= 2) + { + parseInfo.fUseTwoDigitYear = true; + } + parseResult = ParseDigits(ref str, tokenLen, out tempYear); + } + if (!parseResult && parseInfo.fCustomNumberParser) + { + parseResult = parseInfo.parseNumberDelegate(ref str, tokenLen, out tempYear); + } + if (!parseResult) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + if (!CheckNewValue(ref result.Year, tempYear, ch, ref result)) + { + return (false); + } + break; + case 'M': + tokenLen = format.GetRepeatCount(); + if (tokenLen <= 2) + { + if (!ParseDigits(ref str, tokenLen, out tempMonth)) + { + if (!parseInfo.fCustomNumberParser || + !parseInfo.parseNumberDelegate(ref str, tokenLen, out tempMonth)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + } + } + else + { + if (tokenLen == 3) + { + if (!MatchAbbreviatedMonthName(ref str, dtfi, ref tempMonth)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + } + else + { + if (!MatchMonthName(ref str, dtfi, ref tempMonth)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + } + result.flags |= ParseFlags.ParsedMonthName; + } + if (!CheckNewValue(ref result.Month, tempMonth, ch, ref result)) + { + return (false); + } + break; + case 'd': + // Day & Day of week + tokenLen = format.GetRepeatCount(); + if (tokenLen <= 2) + { + // "d" & "dd" + + if (!ParseDigits(ref str, tokenLen, out tempDay)) + { + if (!parseInfo.fCustomNumberParser || + !parseInfo.parseNumberDelegate(ref str, tokenLen, out tempDay)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + } + if (!CheckNewValue(ref result.Day, tempDay, ch, ref result)) + { + return (false); + } + } + else + { + if (tokenLen == 3) + { + // "ddd" + if (!MatchAbbreviatedDayName(ref str, dtfi, ref tempDayOfWeek)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + } + else + { + // "dddd*" + if (!MatchDayName(ref str, dtfi, ref tempDayOfWeek)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + } + if (!CheckNewValue(ref parseInfo.dayOfWeek, tempDayOfWeek, ch, ref result)) + { + return (false); + } + } + break; + case 'g': + tokenLen = format.GetRepeatCount(); + // Put the era value in result.era. + if (!MatchEraName(ref str, dtfi, ref result.era)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + break; + case 'h': + parseInfo.fUseHour12 = true; + tokenLen = format.GetRepeatCount(); + if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempHour)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + if (!CheckNewValue(ref result.Hour, tempHour, ch, ref result)) + { + return (false); + } + break; + case 'H': + tokenLen = format.GetRepeatCount(); + if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempHour)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + if (!CheckNewValue(ref result.Hour, tempHour, ch, ref result)) + { + return (false); + } + break; + case 'm': + tokenLen = format.GetRepeatCount(); + if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempMinute)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + if (!CheckNewValue(ref result.Minute, tempMinute, ch, ref result)) + { + return (false); + } + break; + case 's': + tokenLen = format.GetRepeatCount(); + if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempSecond)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + if (!CheckNewValue(ref result.Second, tempSecond, ch, ref result)) + { + return (false); + } + break; + case 'f': + case 'F': + tokenLen = format.GetRepeatCount(); + if (tokenLen <= DateTimeFormat.MaxSecondsFractionDigits) + { + if (!ParseFractionExact(ref str, tokenLen, ref tempFraction)) + { + if (ch == 'f') + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + } + if (result.fraction < 0) + { + result.fraction = tempFraction; + } + else + { + if (tempFraction != result.fraction) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_RepeatDateTimePattern", ch); + return (false); + } + } + } + else + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + break; + case 't': + // AM/PM designator + tokenLen = format.GetRepeatCount(); + if (tokenLen == 1) + { + if (!MatchAbbreviatedTimeMark(ref str, dtfi, ref tempTimeMark)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + } + else + { + if (!MatchTimeMark(ref str, dtfi, ref tempTimeMark)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + } + + if (parseInfo.timeMark == TM.NotSet) + { + parseInfo.timeMark = tempTimeMark; + } + else + { + if (parseInfo.timeMark != tempTimeMark) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_RepeatDateTimePattern", ch); + return (false); + } + } + break; + case 'z': + // timezone offset + tokenLen = format.GetRepeatCount(); + { + TimeSpan tempTimeZoneOffset = new TimeSpan(0); + if (!ParseTimeZoneOffset(ref str, tokenLen, ref tempTimeZoneOffset)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && tempTimeZoneOffset != result.timeZoneOffset) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_RepeatDateTimePattern", 'z'); + return (false); + } + result.timeZoneOffset = tempTimeZoneOffset; + result.flags |= ParseFlags.TimeZoneUsed; + } + break; + case 'Z': + if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && result.timeZoneOffset != TimeSpan.Zero) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_RepeatDateTimePattern", 'Z'); + return (false); + } + + result.flags |= ParseFlags.TimeZoneUsed; + result.timeZoneOffset = new TimeSpan(0); + result.flags |= ParseFlags.TimeZoneUtc; + + // The updating of the indexes is to reflect that ParseExact MatchXXX methods assume that + // they need to increment the index and Parse GetXXX do not. Since we are calling a Parse + // method from inside ParseExact we need to adjust this. Long term, we should try to + // eliminate this discrepancy. + str.Index++; + if (!GetTimeZoneName(ref str)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + str.Index--; + break; + case 'K': + // This should parse either as a blank, the 'Z' character or a local offset like "-07:00" + if (str.Match('Z')) + { + if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && result.timeZoneOffset != TimeSpan.Zero) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_RepeatDateTimePattern", 'K'); + return (false); + } + + result.flags |= ParseFlags.TimeZoneUsed; + result.timeZoneOffset = new TimeSpan(0); + result.flags |= ParseFlags.TimeZoneUtc; + } + else if (str.Match('+') || str.Match('-')) + { + str.Index--; // Put the character back for the parser + TimeSpan tempTimeZoneOffset = new TimeSpan(0); + if (!ParseTimeZoneOffset(ref str, 3, ref tempTimeZoneOffset)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return (false); + } + if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && tempTimeZoneOffset != result.timeZoneOffset) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_RepeatDateTimePattern", 'K'); + return (false); + } + result.timeZoneOffset = tempTimeZoneOffset; + result.flags |= ParseFlags.TimeZoneUsed; + } + // Otherwise it is unspecified and we consume no characters + break; + case ':': + // We match the separator in time pattern with the character in the time string if both equal to ':' or the date separator is matching the characters in the date string + // We have to exclude the case when the time separator is more than one character and starts with ':' something like "::" for instance. + if (((dtfi.TimeSeparator.Length > 1 && dtfi.TimeSeparator[0] == ':') || !str.Match(':')) && + !str.Match(dtfi.TimeSeparator)) + { + // A time separator is expected. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + break; + case '/': + // We match the separator in date pattern with the character in the date string if both equal to '/' or the date separator is matching the characters in the date string + // We have to exclude the case when the date separator is more than one character and starts with '/' something like "//" for instance. + if (((dtfi.DateSeparator.Length > 1 && dtfi.DateSeparator[0] == '/') || !str.Match('/')) && + !str.Match(dtfi.DateSeparator)) + { + // A date separator is expected. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + break; + case '\"': + case '\'': + StringBuilder enquotedString = new StringBuilder(); + // Use ParseQuoteString so that we can handle escape characters within the quoted string. + if (!TryParseQuoteString(format.Value, format.Index, enquotedString, out tokenLen)) + { + result.SetFailure(ParseFailureKind.FormatWithParameter, "Format_BadQuote", ch); + return (false); + } + format.Index += tokenLen - 1; + + // Some cultures uses space in the quoted string. E.g. Spanish has long date format as: + // "dddd, dd' de 'MMMM' de 'yyyy". When inner spaces flag is set, we should skip whitespaces if there is space + // in the quoted string. + String quotedStr = enquotedString.ToString(); + + for (int i = 0; i < quotedStr.Length; i++) + { + if (quotedStr[i] == ' ' && parseInfo.fAllowInnerWhite) + { + str.SkipWhiteSpaces(); + } + else if (!str.Match(quotedStr[i])) + { + // Can not find the matching quoted string. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + } + + // The "r" and "u" formats incorrectly quoted 'GMT' and 'Z', respectively. We cannot + // correct this mistake for DateTime.ParseExact for compatibility reasons, but we can + // fix it for DateTimeOffset.ParseExact as DateTimeOffset has not been publically released + // with this issue. + if ((result.flags & ParseFlags.CaptureOffset) != 0) + { + if ((result.flags & ParseFlags.Rfc1123Pattern) != 0 && quotedStr == GMTName) + { + result.flags |= ParseFlags.TimeZoneUsed; + result.timeZoneOffset = TimeSpan.Zero; + } + else if ((result.flags & ParseFlags.UtcSortPattern) != 0 && quotedStr == ZuluName) + { + result.flags |= ParseFlags.TimeZoneUsed; + result.timeZoneOffset = TimeSpan.Zero; + } + } + + break; + case '%': + // Skip this so we can get to the next pattern character. + // Used in case like "%d", "%y" + + // Make sure the next character is not a '%' again. + if (format.Index >= format.Value.Length - 1 || format.Value[format.Index + 1] == '%') + { + result.SetFailure(ParseFailureKind.Format, "Format_BadFormatSpecifier", null); + return false; + } + break; + case '\\': + // Escape character. For example, "\d". + // Get the next character in format, and see if we can + // find a match in str. + if (format.GetNext()) + { + if (!str.Match(format.GetChar())) + { + // Can not find a match for the escaped character. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + } + else + { + result.SetFailure(ParseFailureKind.Format, "Format_BadFormatSpecifier", null); + return false; + } + break; + case '.': + if (!str.Match(ch)) + { + if (format.GetNext()) + { + // If we encounter the pattern ".F", and the dot is not present, it is an optional + // second fraction and we can skip this format. + if (format.Match('F')) + { + format.GetRepeatCount(); + break; + } + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + break; + default: + if (ch == ' ') + { + if (parseInfo.fAllowInnerWhite) + { + // Skip whitespaces if AllowInnerWhite. + // Do nothing here. + } + else + { + if (!str.Match(ch)) + { + // If the space does not match, and trailing space is allowed, we do + // one more step to see if the next format character can lead to + // successful parsing. + // This is used to deal with special case that a empty string can match + // a specific pattern. + // The example here is af-ZA, which has a time format like "hh:mm:ss tt". However, + // its AM symbol is "" (empty string). If fAllowTrailingWhite is used, and time is in + // the AM, we will trim the whitespaces at the end, which will lead to a failure + // when we are trying to match the space before "tt". + if (parseInfo.fAllowTrailingWhite) + { + if (format.GetNext()) + { + if (ParseByFormat(ref str, ref format, ref parseInfo, dtfi, ref result)) + { + return (true); + } + } + } + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + // Found a macth. + } + } + else + { + if (format.MatchSpecifiedWord(GMTName)) + { + format.Index += (GMTName.Length - 1); + // Found GMT string in format. This means the DateTime string + // is in GMT timezone. + result.flags |= ParseFlags.TimeZoneUsed; + result.timeZoneOffset = TimeSpan.Zero; + if (!str.Match(GMTName)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + } + else if (!str.Match(ch)) + { + // ch is expected. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + } + break; + } // switch + return (true); + } + + // + // The pos should point to a quote character. This method will + // get the string enclosed by the quote character. + // + internal static bool TryParseQuoteString(String format, int pos, StringBuilder result, out int returnValue) + { + // + // NOTE : pos will be the index of the quote character in the 'format' string. + // + returnValue = 0; + int formatLen = format.Length; + int beginPos = pos; + char quoteChar = format[pos++]; // Get the character used to quote the following string. + + bool foundQuote = false; + while (pos < formatLen) + { + char ch = format[pos++]; + if (ch == quoteChar) + { + foundQuote = true; + break; + } + else if (ch == '\\') + { + // The following are used to support escaped character. + // Escaped character is also supported in the quoted string. + // Therefore, someone can use a format like "'minute:' mm\"" to display: + // minute: 45" + // because the second double quote is escaped. + if (pos < formatLen) + { + result.Append(format[pos++]); + } + else + { + // + // This means that '\' is at the end of the formatting string. + // + return false; + } + } + else + { + result.Append(ch); + } + } + + if (!foundQuote) + { + // Here we can't find the matching quote. + return false; + } + + // + // Return the character count including the begin/end quote characters and enclosed string. + // + returnValue = (pos - beginPos); + return true; + } + + + + + /*=================================DoStrictParse================================== + **Action: Do DateTime parsing using the format in formatParam. + **Returns: The parsed DateTime. + **Arguments: + **Exceptions: + ** + **Notes: + ** When the following general formats are used, InvariantInfo is used in dtfi: + ** 'r', 'R', 's'. + ** When the following general formats are used, the time is assumed to be in Universal time. + ** + **Limitations: + ** Only GregarianCalendar is supported for now. + ** Only support GMT timezone. + ==============================================================================*/ + + private static bool DoStrictParse( + String s, + String formatParam, + DateTimeStyles styles, + DateTimeFormatInfo dtfi, + ref DateTimeResult result) + { + ParsingInfo parseInfo = new ParsingInfo(); + parseInfo.Init(); + + parseInfo.calendar = dtfi.Calendar; + parseInfo.fAllowInnerWhite = ((styles & DateTimeStyles.AllowInnerWhite) != 0); + parseInfo.fAllowTrailingWhite = ((styles & DateTimeStyles.AllowTrailingWhite) != 0); + + // We need the original values of the following two below. + String originalFormat = formatParam; + + if (formatParam.Length == 1) + { + if (((result.flags & ParseFlags.CaptureOffset) != 0) && formatParam[0] == 'U') + { + // The 'U' format is not allowed for DateTimeOffset + result.SetFailure(ParseFailureKind.Format, "Format_BadFormatSpecifier", null); + return false; + } + formatParam = ExpandPredefinedFormat(formatParam, ref dtfi, ref parseInfo, ref result); + } + + bool bTimeOnly = false; + result.calendar = parseInfo.calendar; + + if (parseInfo.calendar.ID == CalendarId.HEBREW) + { + parseInfo.parseNumberDelegate = m_hebrewNumberParser; + parseInfo.fCustomNumberParser = true; + } + + // Reset these values to negative one so that we could throw exception + // if we have parsed every item twice. + result.Hour = result.Minute = result.Second = -1; + + __DTString format = new __DTString(formatParam, dtfi, false); + __DTString str = new __DTString(s, dtfi, false); + + if (parseInfo.fAllowTrailingWhite) + { + // Trim trailing spaces if AllowTrailingWhite. + format.TrimTail(); + format.RemoveTrailingInQuoteSpaces(); + str.TrimTail(); + } + + if ((styles & DateTimeStyles.AllowLeadingWhite) != 0) + { + format.SkipWhiteSpaces(); + format.RemoveLeadingInQuoteSpaces(); + str.SkipWhiteSpaces(); + } + + // + // Scan every character in format and match the pattern in str. + // + while (format.GetNext()) + { + // We trim inner spaces here, so that we will not eat trailing spaces when + // AllowTrailingWhite is not used. + if (parseInfo.fAllowInnerWhite) + { + str.SkipWhiteSpaces(); + } + if (!ParseByFormat(ref str, ref format, ref parseInfo, dtfi, ref result)) + { + return (false); + } + } + + if (str.Index < str.Value.Length - 1) + { + // There are still remaining character in str. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + + if (parseInfo.fUseTwoDigitYear && ((dtfi.FormatFlags & DateTimeFormatFlags.UseHebrewRule) == 0)) + { + // A two digit year value is expected. Check if the parsed year value is valid. + if (result.Year >= 100) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + try + { + result.Year = parseInfo.calendar.ToFourDigitYear(result.Year); + } + catch (ArgumentOutOfRangeException e) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", e); + return false; + } + } + + if (parseInfo.fUseHour12) + { + if (parseInfo.timeMark == TM.NotSet) + { + // hh is used, but no AM/PM designator is specified. + // Assume the time is AM. + // Don't throw exceptions in here becasue it is very confusing for the caller. + // I always got confused myself when I use "hh:mm:ss" to parse a time string, + // and ParseExact() throws on me (because I didn't use the 24-hour clock 'HH'). + parseInfo.timeMark = TM.AM; + } + if (result.Hour > 12) + { + // AM/PM is used, but the value for HH is too big. + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + if (parseInfo.timeMark == TM.AM) + { + if (result.Hour == 12) + { + result.Hour = 0; + } + } + else + { + result.Hour = (result.Hour == 12) ? 12 : result.Hour + 12; + } + } + else + { + // Military (24-hour time) mode + // + // AM cannot be set with a 24-hour time like 17:15. + // PM cannot be set with a 24-hour time like 03:15. + if ((parseInfo.timeMark == TM.AM && result.Hour >= 12) + || (parseInfo.timeMark == TM.PM && result.Hour < 12)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDateTime", null); + return false; + } + } + + + // Check if the parased 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)) + { + return false; + } + + if (!bTimeOnly && dtfi.HasYearMonthAdjustment) + { + if (!dtfi.YearMonthAdjustment(ref result.Year, ref result.Month, ((result.flags & ParseFlags.ParsedMonthName) != 0))) + { + result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, "Format_BadDateTimeCalendar", null); + return false; + } + } + if (!parseInfo.calendar.TryToDateTime(result.Year, result.Month, result.Day, + result.Hour, result.Minute, result.Second, 0, result.era, out result.parsedDate)) + { + result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, "Format_BadDateTimeCalendar", null); + return false; + } + if (result.fraction > 0) + { + result.parsedDate = result.parsedDate.AddTicks((long)Math.Round(result.fraction * Calendar.TicksPerSecond)); + } + + // + // We have to check day of week before we adjust to the time zone. + // It is because the value of day of week may change after adjusting + // to the time zone. + // + if (parseInfo.dayOfWeek != -1) + { + // + // Check if day of week is correct. + // + if (parseInfo.dayOfWeek != (int)parseInfo.calendar.GetDayOfWeek(result.parsedDate)) + { + result.SetFailure(ParseFailureKind.Format, "Format_BadDayOfWeek", null); + return false; + } + } + + + if (!DetermineTimeZoneAdjustments(ref result, styles, bTimeOnly)) + { + return false; + } + return true; + } + + private static Exception GetDateTimeParseException(ref DateTimeResult result) + { + switch (result.failure) + { + case ParseFailureKind.ArgumentNull: + return new ArgumentNullException(result.failureArgumentName, SR.GetResourceString(result.failureMessageID)); + case ParseFailureKind.Format: + return new FormatException(SR.GetResourceString(result.failureMessageID)); + case ParseFailureKind.FormatWithParameter: + return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), result.failureMessageFormatArgument)); + case ParseFailureKind.FormatBadDateTimeCalendar: + return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), result.calendar)); + default: + Debug.Assert(false, "Unkown DateTimeParseFailure: " + result); + return null; + } + } + + // Builds with _LOGGING defined (x86dbg, amd64chk, etc) support tracing + // Set the following internal-only/unsupported environment variables to enable DateTime tracing to the console: + // + // COMPlus_LogEnable=1 + // COMPlus_LogToConsole=1 + // COMPlus_LogLevel=9 + // COMPlus_ManagedLogFacility=0x00001000 + [Pure] + [Conditional("_LOGGING")] + internal static void LexTraceExit(string message, DS dps) + { +#if _LOGGING + if (!_tracingEnabled) + return; + BCLDebug.Trace("DATETIME", "[DATETIME] Lex return {0}, DS.{1}", message, dps); +#endif // _LOGGING + } + [Pure] + [Conditional("_LOGGING")] + internal static void PTSTraceExit(DS dps, bool passed) + { +#if _LOGGING + if (!_tracingEnabled) + return; + BCLDebug.Trace("DATETIME", "[DATETIME] ProcessTerminalState {0} @ DS.{1}", passed ? "passed" : "failed", dps); +#endif // _LOGGING + } + [Pure] + [Conditional("_LOGGING")] + internal static void TPTraceExit(string message, DS dps) + { +#if _LOGGING + if (!_tracingEnabled) + return; + BCLDebug.Trace("DATETIME", "[DATETIME] TryParse return {0}, DS.{1}", message, dps); +#endif // _LOGGING + } + [Pure] + [Conditional("_LOGGING")] + internal static void DTFITrace(DateTimeFormatInfo dtfi) + { +#if _LOGGING + if (!_tracingEnabled) + return; + + BCLDebug.Trace("DATETIME", "[DATETIME] DateTimeFormatInfo Properties"); +#if !FEATURE_COREFX_GLOBALIZATION + BCLDebug.Trace("DATETIME", " NativeCalendarName {0}", Hex(dtfi.NativeCalendarName)); +#endif + BCLDebug.Trace("DATETIME", " AMDesignator {0}", Hex(dtfi.AMDesignator)); + BCLDebug.Trace("DATETIME", " PMDesignator {0}", Hex(dtfi.PMDesignator)); + BCLDebug.Trace("DATETIME", " TimeSeparator {0}", Hex(dtfi.TimeSeparator)); + BCLDebug.Trace("DATETIME", " AbbrvDayNames {0}", Hex(dtfi.AbbreviatedDayNames)); + BCLDebug.Trace("DATETIME", " ShortestDayNames {0}", Hex(dtfi.ShortestDayNames)); + BCLDebug.Trace("DATETIME", " DayNames {0}", Hex(dtfi.DayNames)); + BCLDebug.Trace("DATETIME", " AbbrvMonthNames {0}", Hex(dtfi.AbbreviatedMonthNames)); + BCLDebug.Trace("DATETIME", " MonthNames {0}", Hex(dtfi.MonthNames)); + BCLDebug.Trace("DATETIME", " AbbrvMonthGenNames {0}", Hex(dtfi.AbbreviatedMonthGenitiveNames)); + BCLDebug.Trace("DATETIME", " MonthGenNames {0}", Hex(dtfi.MonthGenitiveNames)); +#endif // _LOGGING + } +#if _LOGGING + [Pure] + // return a string in the form: "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" + internal static string Hex(string[] strs) + { + if (strs == null || strs.Length == 0) + return String.Empty; + if (strs.Length == 1) + return Hex(strs[0]); + + int curLineLength = 0; + int maxLineLength = 55; + int newLinePadding = 20; + + + //invariant: strs.Length >= 2 + StringBuilder buffer = new StringBuilder(); + buffer.Append(Hex(strs[0])); + curLineLength = buffer.Length; + String s; + + for (int i = 1; i < strs.Length - 1; i++) + { + s = Hex(strs[i]); + + if (s.Length > maxLineLength || (curLineLength + s.Length + 2) > maxLineLength) + { + buffer.Append(','); + buffer.Append(Environment.NewLine); + buffer.Append(' ', newLinePadding); + curLineLength = 0; + } + else + { + buffer.Append(", "); + curLineLength += 2; + } + buffer.Append(s); + curLineLength += s.Length; + } + + buffer.Append(','); + s = Hex(strs[strs.Length - 1]); + if (s.Length > maxLineLength || (curLineLength + s.Length + 6) > maxLineLength) + { + buffer.Append(Environment.NewLine); + buffer.Append(' ', newLinePadding); + } + else + { + buffer.Append(' '); + } + buffer.Append(s); + return buffer.ToString(); + } + [Pure] + // return a string in the form: "Sun" + internal static string Hex(string str) + { + StringBuilder buffer = new StringBuilder(); + buffer.Append("\""); + for (int i = 0; i < str.Length; i++) + { + if (str[i] <= '\x007f') + buffer.Append(str[i]); + else + buffer.Append("\\u" + ((int)str[i]).ToString("x4", CultureInfo.InvariantCulture)); + } + buffer.Append("\""); + return buffer.ToString(); + } + [Pure] + // return an unicode escaped string form of char c + internal static String Hex(char c) + { + if (c <= '\x007f') + return c.ToString(CultureInfo.InvariantCulture); + else + return "\\u" + ((int)c).ToString("x4", CultureInfo.InvariantCulture); + } + + internal static bool _tracingEnabled = BCLDebug.CheckEnabled("DATETIME"); +#endif // _LOGGING + } + + + // + // This is a string parsing helper which wraps a String object. + // It has a Index property which tracks + // the current parsing pointer of the string. + // + internal + struct __DTString + { + // + // Value propery: stores the real string to be parsed. + // + internal String Value; + + // + // Index property: points to the character that we are currently parsing. + // + internal int Index; + + // The length of Value string. + internal int len; + + // The current chracter to be looked at. + internal char m_current; + + private CompareInfo m_info; + // Flag to indicate if we encouter an digit, we should check for token or not. + // In some cultures, such as mn-MN, it uses "\x0031\x00a0\x0434\x04af\x0433\x044d\x044d\x0440\x00a0\x0441\x0430\x0440" in month names. + private bool m_checkDigitToken; + + internal __DTString(String str, DateTimeFormatInfo dtfi, bool checkDigitToken) : this(str, dtfi) + { + m_checkDigitToken = checkDigitToken; + } + + internal __DTString(String str, DateTimeFormatInfo dtfi) + { + Index = -1; + Value = str; + len = Value.Length; + + m_current = '\0'; + if (dtfi != null) + { + m_info = dtfi.CompareInfo; + m_checkDigitToken = ((dtfi.FormatFlags & DateTimeFormatFlags.UseDigitPrefixInTokens) != 0); + } + else + { + m_info = CultureInfo.CurrentCulture.CompareInfo; + m_checkDigitToken = false; + } + } + + internal CompareInfo CompareInfo + { + get { return m_info; } + } + + // + // Advance the Index. + // Return true if Index is NOT at the end of the string. + // + // Typical usage: + // while (str.GetNext()) + // { + // char ch = str.GetChar() + // } + internal bool GetNext() + { + Index++; + if (Index < len) + { + m_current = Value[Index]; + return (true); + } + return (false); + } + + internal bool AtEnd() + { + return Index < len ? false : true; + } + + internal bool Advance(int count) + { + Debug.Assert(Index + count <= len, "__DTString::Advance: Index + count <= len"); + Index += count; + if (Index < len) + { + m_current = Value[Index]; + return (true); + } + return (false); + } + + + // Used by DateTime.Parse() to get the next token. + internal void GetRegularToken(out TokenType tokenType, out int tokenValue, DateTimeFormatInfo dtfi) + { + tokenValue = 0; + if (Index >= len) + { + tokenType = TokenType.EndOfString; + return; + } + + tokenType = TokenType.UnknownToken; + + Start: + if (DateTimeParse.IsDigit(m_current)) + { + // This is a digit. + tokenValue = m_current - '0'; + int value; + int start = Index; + + // + // Collect other digits. + // + while (++Index < len) + { + m_current = Value[Index]; + value = m_current - '0'; + if (value >= 0 && value <= 9) + { + tokenValue = tokenValue * 10 + value; + } + else + { + break; + } + } + if (Index - start > DateTimeParse.MaxDateTimeNumberDigits) + { + tokenType = TokenType.NumberToken; + tokenValue = -1; + } + else if (Index - start < 3) + { + tokenType = TokenType.NumberToken; + } + else + { + // If there are more than 3 digits, assume that it's a year value. + tokenType = TokenType.YearNumberToken; + } + if (m_checkDigitToken) + { + int save = Index; + char saveCh = m_current; + // Re-scan using the staring Index to see if this is a token. + Index = start; // To include the first digit. + m_current = Value[Index]; + TokenType tempType; + int tempValue; + // This DTFI has tokens starting with digits. + // E.g. mn-MN has month name like "\x0031\x00a0\x0434\x04af\x0433\x044d\x044d\x0440\x00a0\x0441\x0430\x0440" + if (dtfi.Tokenize(TokenType.RegularTokenMask, out tempType, out tempValue, ref this)) + { + tokenType = tempType; + tokenValue = tempValue; + // This is a token, so the Index has been advanced propertly in DTFI.Tokenizer(). + } + else + { + // Use the number token value. + // Restore the index. + Index = save; + m_current = saveCh; + } + } + } + else if (Char.IsWhiteSpace(m_current)) + { + // Just skip to the next character. + while (++Index < len) + { + m_current = Value[Index]; + if (!(Char.IsWhiteSpace(m_current))) + { + goto Start; + } + } + // We have reached the end of string. + tokenType = TokenType.EndOfString; + } + else + { + dtfi.Tokenize(TokenType.RegularTokenMask, out tokenType, out tokenValue, ref this); + } + } + + internal TokenType GetSeparatorToken(DateTimeFormatInfo dtfi, out int indexBeforeSeparator, out char charBeforeSeparator) + { + indexBeforeSeparator = Index; + charBeforeSeparator = m_current; + TokenType tokenType; + if (!SkipWhiteSpaceCurrent()) + { + // Reach the end of the string. + return (TokenType.SEP_End); + } + if (!DateTimeParse.IsDigit(m_current)) + { + // Not a digit. Tokenize it. + int tokenValue; + bool found = dtfi.Tokenize(TokenType.SeparatorTokenMask, out tokenType, out tokenValue, ref this); + if (!found) + { + tokenType = TokenType.SEP_Space; + } + } + else + { + // Do nothing here. If we see a number, it will not be a separator. There is no need wasting time trying to find the + // separator token. + tokenType = TokenType.SEP_Space; + } + return (tokenType); + } + + internal bool MatchSpecifiedWord(String target) + { + return MatchSpecifiedWord(target, target.Length + Index); + } + + internal bool MatchSpecifiedWord(String target, int endIndex) + { + int count = endIndex - Index; + + if (count != target.Length) + { + return false; + } + + if (Index + count > len) + { + return false; + } + + return (m_info.Compare(Value, Index, count, target, 0, count, CompareOptions.IgnoreCase) == 0); + } + + private static Char[] WhiteSpaceChecks = new Char[] { ' ', '\u00A0' }; + + internal bool MatchSpecifiedWords(String target, bool checkWordBoundary, ref int matchLength) + { + int valueRemaining = Value.Length - Index; + matchLength = target.Length; + + if (matchLength > valueRemaining || m_info.Compare(Value, Index, matchLength, target, 0, matchLength, CompareOptions.IgnoreCase) != 0) + { + // Check word by word + int targetPosition = 0; // Where we are in the target string + int thisPosition = Index; // Where we are in this string + int wsIndex = target.IndexOfAny(WhiteSpaceChecks, targetPosition); + if (wsIndex == -1) + { + return false; + } + do + { + int segmentLength = wsIndex - targetPosition; + if (thisPosition >= Value.Length - segmentLength) + { // Subtraction to prevent overflow. + return false; + } + if (segmentLength == 0) + { + // If segmentLength == 0, it means that we have leading space in the target string. + // In that case, skip the leading spaces in the target and this string. + matchLength--; + } + else + { + // Make sure we also have whitespace in the input string + if (!Char.IsWhiteSpace(Value[thisPosition + segmentLength])) + { + return false; + } + if (m_info.Compare(Value, thisPosition, segmentLength, target, targetPosition, segmentLength, CompareOptions.IgnoreCase) != 0) + { + return false; + } + // Advance the input string + thisPosition = thisPosition + segmentLength + 1; + } + // Advance our target string + targetPosition = wsIndex + 1; + + + // Skip past multiple whitespace + while (thisPosition < Value.Length && Char.IsWhiteSpace(Value[thisPosition])) + { + thisPosition++; + matchLength++; + } + } while ((wsIndex = target.IndexOfAny(WhiteSpaceChecks, targetPosition)) >= 0); + // now check the last segment; + if (targetPosition < target.Length) + { + int segmentLength = target.Length - targetPosition; + if (thisPosition > Value.Length - segmentLength) + { + return false; + } + if (m_info.Compare(Value, thisPosition, segmentLength, target, targetPosition, segmentLength, CompareOptions.IgnoreCase) != 0) + { + return false; + } + } + } + + if (checkWordBoundary) + { + int nextCharIndex = Index + matchLength; + if (nextCharIndex < Value.Length) + { + if (Char.IsLetter(Value[nextCharIndex])) + { + return (false); + } + } + } + return (true); + } + + // + // Check to see if the string starting from Index is a prefix of + // str. + // If a match is found, true value is returned and Index is updated to the next character to be parsed. + // Otherwise, Index is unchanged. + // + internal bool Match(String str) + { + if (++Index >= len) + { + return (false); + } + + if (str.Length > (Value.Length - Index)) + { + return false; + } + + if (m_info.Compare(Value, Index, str.Length, str, 0, str.Length, CompareOptions.Ordinal) == 0) + { + // Update the Index to the end of the matching string. + // So the following GetNext()/Match() opeartion will get + // the next character to be parsed. + Index += (str.Length - 1); + return (true); + } + return (false); + } + + internal bool Match(char ch) + { + if (++Index >= len) + { + return (false); + } + if (Value[Index] == ch) + { + m_current = ch; + return (true); + } + Index--; + return (false); + } + + // + // Actions: From the current position, try matching the longest word in the specified string array. + // E.g. words[] = {"AB", "ABC", "ABCD"}, if the current position points to a substring like "ABC DEF", + // MatchLongestWords(words, ref MaxMatchStrLen) will return 1 (the index), and maxMatchLen will be 3. + // Returns: + // 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 + // find the longest match in two string arrays. + // + internal int MatchLongestWords(String[] words, ref int maxMatchStrLen) + { + int result = -1; + for (int i = 0; i < words.Length; i++) + { + String word = words[i]; + int matchLength = word.Length; + if (MatchSpecifiedWords(word, false, ref matchLength)) + { + if (matchLength > maxMatchStrLen) + { + maxMatchStrLen = matchLength; + result = i; + } + } + } + + return (result); + } + + // + // Get the number of repeat character after the current character. + // For a string "hh:mm:ss" at Index of 3. GetRepeatCount() = 2, and Index + // will point to the second ':'. + // + internal int GetRepeatCount() + { + char repeatChar = Value[Index]; + int pos = Index + 1; + while ((pos < len) && (Value[pos] == repeatChar)) + { + pos++; + } + int repeatCount = (pos - Index); + // Update the Index to the end of the repeated characters. + // So the following GetNext() opeartion will get + // the next character to be parsed. + Index = pos - 1; + return (repeatCount); + } + + // Return false when end of string is encountered or a non-digit character is found. + internal bool GetNextDigit() + { + if (++Index >= len) + { + return (false); + } + return (DateTimeParse.IsDigit(Value[Index])); + } + + // + // Get the current character. + // + internal char GetChar() + { + Debug.Assert(Index >= 0 && Index < len, "Index >= 0 && Index < len"); + return (Value[Index]); + } + + // + // Convert the current character to a digit, and return it. + // + internal int GetDigit() + { + Debug.Assert(Index >= 0 && Index < len, "Index >= 0 && Index < len"); + Debug.Assert(DateTimeParse.IsDigit(Value[Index]), "IsDigit(Value[Index])"); + return (Value[Index] - '0'); + } + + // + // Eat White Space ahead of the current position + // + // Return false if end of string is encountered. + // + internal void SkipWhiteSpaces() + { + // Look ahead to see if the next character + // is a whitespace. + while (Index + 1 < len) + { + char ch = Value[Index + 1]; + if (!Char.IsWhiteSpace(ch)) + { + return; + } + Index++; + } + return; + } + + // + // Skip white spaces from the current position + // + // Return false if end of string is encountered. + // + internal bool SkipWhiteSpaceCurrent() + { + if (Index >= len) + { + return (false); + } + + if (!Char.IsWhiteSpace(m_current)) + { + return (true); + } + + while (++Index < len) + { + m_current = Value[Index]; + if (!Char.IsWhiteSpace(m_current)) + { + return (true); + } + // Nothing here. + } + return (false); + } + + internal void TrimTail() + { + int i = len - 1; + while (i >= 0 && Char.IsWhiteSpace(Value[i])) + { + i--; + } + Value = Value.Substring(0, i + 1); + len = Value.Length; + } + + // Trim the trailing spaces within a quoted string. + // Call this after TrimTail() is done. + internal void RemoveTrailingInQuoteSpaces() + { + int i = len - 1; + if (i <= 1) + { + return; + } + char ch = Value[i]; + // Check if the last character is a quote. + if (ch == '\'' || ch == '\"') + { + if (Char.IsWhiteSpace(Value[i - 1])) + { + i--; + while (i >= 1 && Char.IsWhiteSpace(Value[i - 1])) + { + i--; + } + Value = Value.Remove(i, Value.Length - 1 - i); + len = Value.Length; + } + } + } + + // Trim the leading spaces within a quoted string. + // Call this after the leading spaces before quoted string are trimmed. + internal void RemoveLeadingInQuoteSpaces() + { + if (len <= 2) + { + return; + } + int i = 0; + char ch = Value[i]; + // Check if the last character is a quote. + if (ch == '\'' || ch == '\"') + { + while ((i + 1) < len && Char.IsWhiteSpace(Value[i + 1])) + { + i++; + } + if (i != 0) + { + Value = Value.Remove(1, i); + len = Value.Length; + } + } + } + + internal DTSubString GetSubString() + { + DTSubString sub = new DTSubString(); + sub.index = Index; + sub.s = Value; + while (Index + sub.length < len) + { + DTSubStringType currentType; + Char ch = Value[Index + sub.length]; + if (ch >= '0' && ch <= '9') + { + currentType = DTSubStringType.Number; + } + else + { + currentType = DTSubStringType.Other; + } + + if (sub.length == 0) + { + sub.type = currentType; + } + else + { + if (sub.type != currentType) + { + break; + } + } + sub.length++; + if (currentType == DTSubStringType.Number) + { + // Incorporate the number into the value + // Limit the digits to prevent overflow + if (sub.length > DateTimeParse.MaxDateTimeNumberDigits) + { + sub.type = DTSubStringType.Invalid; + return sub; + } + int number = ch - '0'; + Debug.Assert(number >= 0 && number <= 9, "number >= 0 && number <= 9"); + sub.value = sub.value * 10 + number; + } + else + { + // For non numbers, just return this length 1 token. This should be expanded + // to more types of thing if this parsing approach is used for things other + // than numbers and single characters + break; + } + } + if (sub.length == 0) + { + sub.type = DTSubStringType.End; + return sub; + } + + return sub; + } + + internal void ConsumeSubString(DTSubString sub) + { + Debug.Assert(sub.index == Index, "sub.index == Index"); + Debug.Assert(sub.index + sub.length <= len, "sub.index + sub.length <= len"); + Index = sub.index + sub.length; + if (Index < len) + { + m_current = Value[Index]; + } + } + } + + internal enum DTSubStringType + { + Unknown = 0, + Invalid = 1, + Number = 2, + End = 3, + Other = 4, + } + + internal struct DTSubString + { + internal String s; + internal Int32 index; + internal Int32 length; + internal DTSubStringType type; + internal Int32 value; + + internal Char this[Int32 relativeIndex] + { + get + { + return s[index + relativeIndex]; + } + } + } + + // + // The buffer to store the parsing token. + // + internal + struct DateTimeToken + { + internal DateTimeParse.DTT dtt; // Store the token + internal TokenType suffix; // Store the CJK Year/Month/Day suffix (if any) + internal int num; // Store the number that we are parsing (if any) + } + + // + // The buffer to store temporary parsing information. + // + internal + unsafe struct DateTimeRawInfo + { + private int* num; + internal int numCount; + internal int month; + internal int year; + internal int dayOfWeek; + internal int era; + internal DateTimeParse.TM timeMark; + internal double fraction; + internal bool hasSameDateAndTimeSeparators; + + internal void Init(int* numberBuffer) + { + month = -1; + year = -1; + dayOfWeek = -1; + era = -1; + timeMark = DateTimeParse.TM.NotSet; + fraction = -1; + num = numberBuffer; + } + internal unsafe void AddNumber(int value) + { + num[numCount++] = value; + } + internal unsafe int GetNumber(int index) + { + return num[index]; + } + } + + internal enum ParseFailureKind + { + None = 0, + ArgumentNull = 1, + Format = 2, + FormatWithParameter = 3, + FormatBadDateTimeCalendar = 4, // FormatException when ArgumentOutOfRange is thrown by a Calendar.TryToDateTime(). + }; + + [Flags] + internal enum ParseFlags + { + HaveYear = 0x00000001, + HaveMonth = 0x00000002, + HaveDay = 0x00000004, + HaveHour = 0x00000008, + HaveMinute = 0x00000010, + HaveSecond = 0x00000020, + HaveTime = 0x00000040, + HaveDate = 0x00000080, + TimeZoneUsed = 0x00000100, + TimeZoneUtc = 0x00000200, + ParsedMonthName = 0x00000400, + CaptureOffset = 0x00000800, + YearDefault = 0x00001000, + Rfc1123Pattern = 0x00002000, + UtcSortPattern = 0x00004000, + } + + // + // This will store the result of the parsing. And it will be eventually + // used to construct a DateTime instance. + // + internal + struct DateTimeResult + { + internal int Year; + internal int Month; + internal int Day; + // + // Set time defualt to 00:00:00. + // + internal int Hour; + internal int Minute; + internal int Second; + internal double fraction; + + internal int era; + + internal ParseFlags flags; + + internal TimeSpan timeZoneOffset; + + internal Calendar calendar; + + internal DateTime parsedDate; + + internal ParseFailureKind failure; + internal string failureMessageID; + internal object failureMessageFormatArgument; + internal string failureArgumentName; + + internal void Init() + { + Year = -1; + Month = -1; + Day = -1; + fraction = -1; + era = -1; + } + + internal void SetDate(int year, int month, int day) + { + Year = year; + Month = month; + Day = day; + } + internal void SetFailure(ParseFailureKind failure, string failureMessageID, object failureMessageFormatArgument) + { + this.failure = failure; + this.failureMessageID = failureMessageID; + this.failureMessageFormatArgument = failureMessageFormatArgument; + } + + internal void SetFailure(ParseFailureKind failure, string failureMessageID, object failureMessageFormatArgument, string failureArgumentName) + { + this.failure = failure; + this.failureMessageID = failureMessageID; + this.failureMessageFormatArgument = failureMessageFormatArgument; + this.failureArgumentName = failureArgumentName; + } + } + + // This is the helper data structure used in ParseExact(). + internal struct ParsingInfo + { + internal Calendar calendar; + internal int dayOfWeek; + internal DateTimeParse.TM timeMark; + + internal bool fUseHour12; + internal bool fUseTwoDigitYear; + internal bool fAllowInnerWhite; + internal bool fAllowTrailingWhite; + internal bool fCustomNumberParser; + internal DateTimeParse.MatchNumberDelegate parseNumberDelegate; + + internal void Init() + { + dayOfWeek = -1; + timeMark = DateTimeParse.TM.NotSet; + } + } + + // + // The type of token that will be returned by DateTimeFormatInfo.Tokenize(). + // + internal enum TokenType + { + // The valid token should start from 1. + + // Regular tokens. The range is from 0x00 ~ 0xff. + NumberToken = 1, // The number. E.g. "12" + YearNumberToken = 2, // The number which is considered as year number, which has 3 or more digits. E.g. "2003" + Am = 3, // AM timemark. E.g. "AM" + Pm = 4, // PM timemark. E.g. "PM" + MonthToken = 5, // A word (or words) that represents a month name. E.g. "March" + EndOfString = 6, // End of string + DayOfWeekToken = 7, // A word (or words) that represents a day of week name. E.g. "Monday" or "Mon" + TimeZoneToken = 8, // A word that represents a timezone name. E.g. "GMT" + EraToken = 9, // A word that represents a era name. E.g. "A.D." + DateWordToken = 10, // A word that can appear in a DateTime string, but serves no parsing semantics. E.g. "de" in Spanish culture. + UnknownToken = 11, // An unknown word, which signals an error in parsing. + HebrewNumber = 12, // A number that is composed of Hebrew text. Hebrew calendar uses Hebrew digits for year values, month values, and day values. + JapaneseEraToken = 13, // Era name for JapaneseCalendar + TEraToken = 14, // Era name for TaiwanCalendar + IgnorableSymbol = 15, // A separator like "," that is equivalent to whitespace + + + // Separator tokens. + SEP_Unk = 0x100, // Unknown separator. + SEP_End = 0x200, // The end of the parsing string. + SEP_Space = 0x300, // Whitespace (including comma). + SEP_Am = 0x400, // AM timemark. E.g. "AM" + SEP_Pm = 0x500, // PM timemark. E.g. "PM" + SEP_Date = 0x600, // date separator. E.g. "/" + SEP_Time = 0x700, // time separator. E.g. ":" + SEP_YearSuff = 0x800, // Chinese/Japanese/Korean year suffix. + SEP_MonthSuff = 0x900, // Chinese/Japanese/Korean month suffix. + SEP_DaySuff = 0xa00, // Chinese/Japanese/Korean day suffix. + SEP_HourSuff = 0xb00, // Chinese/Japanese/Korean hour suffix. + SEP_MinuteSuff = 0xc00, // Chinese/Japanese/Korean minute suffix. + SEP_SecondSuff = 0xd00, // Chinese/Japanese/Korean second suffix. + SEP_LocalTimeMark = 0xe00, // 'T', used in ISO 8601 format. + SEP_DateOrOffset = 0xf00, // '-' which could be a date separator or start of a time zone offset + + RegularTokenMask = 0x00ff, + SeparatorTokenMask = 0xff00, + } +} -- 2.7.4