1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Diagnostics;
6 using System.Globalization;
7 using System.Runtime.CompilerServices;
12 internal static class DateTimeParse
14 internal const Int32 MaxDateTimeNumberDigits = 8;
16 internal delegate bool MatchNumberDelegate(ref __DTString str, int digitLen, out int result);
18 internal static MatchNumberDelegate m_hebrewNumberParser = new MatchNumberDelegate(DateTimeParse.MatchHebrewDigits);
20 internal static DateTime ParseExact(ReadOnlySpan<char> s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style)
22 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
24 if (TryParseExact(s, format, dtfi, style, ref result))
26 return result.parsedDate;
30 throw GetDateTimeParseException(ref result);
34 internal static DateTime ParseExact(ReadOnlySpan<char> s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style, out TimeSpan offset)
36 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
37 offset = TimeSpan.Zero;
39 result.flags |= ParseFlags.CaptureOffset;
40 if (TryParseExact(s, format, dtfi, style, ref result))
42 offset = result.timeZoneOffset;
43 return result.parsedDate;
47 throw GetDateTimeParseException(ref result);
51 internal static bool TryParseExact(ReadOnlySpan<char> s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result)
53 result = DateTime.MinValue;
54 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
56 if (TryParseExact(s, format, dtfi, style, ref resultData))
58 result = resultData.parsedDate;
64 internal static bool TryParseExact(ReadOnlySpan<char> s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result, out TimeSpan offset)
66 result = DateTime.MinValue;
67 offset = TimeSpan.Zero;
68 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
70 resultData.flags |= ParseFlags.CaptureOffset;
71 if (TryParseExact(s, format, dtfi, style, ref resultData))
73 result = resultData.parsedDate;
74 offset = resultData.timeZoneOffset;
80 internal static bool TryParseExact(ReadOnlySpan<char> s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style, ref DateTimeResult result)
84 result.SetFailure(ParseFailureKind.ArgumentNull, nameof(SR.ArgumentNull_String), null, nameof(format));
89 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
93 if (format.Length == 0)
95 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
99 Debug.Assert(dtfi != null, "dtfi == null");
101 return DoStrictParse(s, format, style, dtfi, ref result);
104 internal static DateTime ParseExactMultiple(ReadOnlySpan<char> s, String[] formats,
105 DateTimeFormatInfo dtfi, DateTimeStyles style)
107 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
109 if (TryParseExactMultiple(s, formats, dtfi, style, ref result))
111 return result.parsedDate;
115 throw GetDateTimeParseException(ref result);
120 internal static DateTime ParseExactMultiple(ReadOnlySpan<char> s, String[] formats,
121 DateTimeFormatInfo dtfi, DateTimeStyles style, out TimeSpan offset)
123 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
124 offset = TimeSpan.Zero;
126 result.flags |= ParseFlags.CaptureOffset;
127 if (TryParseExactMultiple(s, formats, dtfi, style, ref result))
129 offset = result.timeZoneOffset;
130 return result.parsedDate;
134 throw GetDateTimeParseException(ref result);
138 internal static bool TryParseExactMultiple(ReadOnlySpan<char> s, String[] formats,
139 DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result, out TimeSpan offset)
141 result = DateTime.MinValue;
142 offset = TimeSpan.Zero;
143 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
145 resultData.flags |= ParseFlags.CaptureOffset;
146 if (TryParseExactMultiple(s, formats, dtfi, style, ref resultData))
148 result = resultData.parsedDate;
149 offset = resultData.timeZoneOffset;
156 internal static bool TryParseExactMultiple(ReadOnlySpan<char> s, String[] formats,
157 DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result)
159 result = DateTime.MinValue;
160 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
162 if (TryParseExactMultiple(s, formats, dtfi, style, ref resultData))
164 result = resultData.parsedDate;
170 internal static bool TryParseExactMultiple(ReadOnlySpan<char> s, String[] formats,
171 DateTimeFormatInfo dtfi, DateTimeStyles style, ref DateTimeResult result)
175 result.SetFailure(ParseFailureKind.ArgumentNull, nameof(SR.ArgumentNull_String), null, nameof(formats));
181 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
185 if (formats.Length == 0)
187 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
191 Debug.Assert(dtfi != null, "dtfi == null");
194 // Do a loop through the provided formats and see if we can parse succesfully in
195 // one of the formats.
197 for (int i = 0; i < formats.Length; i++)
199 if (formats[i] == null || formats[i].Length == 0)
201 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
204 // Create a new result each time to ensure the runs are independent. Carry through
205 // flags from the caller and return the result.
206 DateTimeResult innerResult = new DateTimeResult(); // The buffer to store the parsing result.
208 innerResult.flags = result.flags;
209 if (TryParseExact(s, formats[i], dtfi, style, ref innerResult))
211 result.parsedDate = innerResult.parsedDate;
212 result.timeZoneOffset = innerResult.timeZoneOffset;
216 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
220 ////////////////////////////////////////////////////////////////////////////
223 // Following is the set of tokens that can be generated from a date
224 // string. Notice that the legal set of trailing separators have been
225 // folded in with the date number, and month name tokens. This set
226 // of tokens is chosen to reduce the number of date parse states.
228 ////////////////////////////////////////////////////////////////////////////
230 internal enum DTT : int
233 NumEnd = 1, // Num[ ]*[\0]
234 NumAmpm = 2, // Num[ ]+AmPm
235 NumSpace = 3, // Num[ ]+^[Dsep|Tsep|'0\']
236 NumDatesep = 4, // Num[ ]*Dsep
237 NumTimesep = 5, // Num[ ]*Tsep
238 MonthEnd = 6, // Month[ ]*'\0'
239 MonthSpace = 7, // Month[ ]+^[Dsep|Tsep|'\0']
240 MonthDatesep = 8, // Month[ ]*Dsep
241 NumDatesuff = 9, // Month[ ]*DSuff
242 NumTimesuff = 10, // Month[ ]*TSuff
243 DayOfWeek = 11, // Day of week name
244 YearSpace = 12, // Year+^[Dsep|Tsep|'0\']
245 YearDateSep = 13, // Year+Dsep
246 YearEnd = 14, // Year+['\0']
247 TimeZone = 15, // timezone name
248 Era = 16, // era name
249 NumUTCTimeMark = 17, // Num + 'Z'
250 // When you add a new token which will be in the
251 // state table, add it after NumLocalTimeMark.
253 NumLocalTimeMark = 19, // Num + 'T'
265 ////////////////////////////////////////////////////////////////////////////
267 // DateTime parsing state enumeration (DS.*)
269 ////////////////////////////////////////////////////////////////////////////
274 N = 1, // have one number
275 NN = 2, // have two numbers
277 // The following are known to be part of a date
279 D_Nd = 3, // date string: have number followed by date separator
280 D_NN = 4, // date string: have two numbers
281 D_NNd = 5, // date string: have two numbers followed by date separator
283 D_M = 6, // date string: have a month
284 D_MN = 7, // date string: have a month and a number
285 D_NM = 8, // date string: have a number and a month
286 D_MNd = 9, // date string: have a month and number followed by date separator
287 D_NDS = 10, // date string: have one number followed a date suffix.
289 D_Y = 11, // date string: have a year.
290 D_YN = 12, // date string: have a year and a number
291 D_YNd = 13, // date string: have a year and a number and a date separator
292 D_YM = 14, // date string: have a year and a month
293 D_YMd = 15, // date string: have a year and a month and a date separator
294 D_S = 16, // have numbers followed by a date suffix.
295 T_S = 17, // have numbers followed by a time suffix.
297 // The following are known to be part of a time
299 T_Nt = 18, // have num followed by time separator
300 T_NNt = 19, // have two numbers followed by time separator
305 // The following are terminal states. These all have an action
306 // associated with them; and transition back to BEGIN.
308 DX_NN = 21, // day from two numbers
309 DX_NNN = 22, // day from three numbers
310 DX_MN = 23, // day from month and one number
311 DX_NM = 24, // day from month and one number
312 DX_MNN = 25, // day from month and two numbers
313 DX_DS = 26, // a set of date suffixed numbers.
314 DX_DSN = 27, // day from date suffixes and one number.
315 DX_NDS = 28, // day from one number and date suffixes .
316 DX_NNDS = 29, // day from one number and date suffixes .
318 DX_YNN = 30, // date string: have a year and two number
319 DX_YMN = 31, // date string: have a year, a month, and a number.
320 DX_YN = 32, // date string: have a year and one number
321 DX_YM = 33, // date string: have a year, a month.
322 TX_N = 34, // time from one number (must have ampm)
323 TX_NN = 35, // time from two numbers
324 TX_NNN = 36, // time from three numbers
325 TX_TS = 37, // a set of time suffixed numbers.
329 ////////////////////////////////////////////////////////////////////////////
331 // NOTE: The following state machine table is dependent on the order of the
332 // DS and DTT enumerations.
334 // For each non terminal state, the following table defines the next state
335 // for each given date token type.
337 ////////////////////////////////////////////////////////////////////////////
339 // End NumEnd NumAmPm NumSpace NumDaySep NumTimesep MonthEnd MonthSpace MonthDSep NumDateSuff NumTimeSuff DayOfWeek YearSpace YearDateSep YearEnd TimeZone Era UTCTimeMark
340 private static DS[][] dateParsingStates = {
341 // DS.BEGIN // DS.BEGIN
342 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},
345 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},
348 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},
350 // DS.D_Nd // DS.D_Nd
351 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},
353 // DS.D_NN // DS.D_NN
354 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},
356 // DS.D_NNd // DS.D_NNd
357 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},
360 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},
362 // DS.D_MN // DS.D_MN
363 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},
365 // DS.D_NM // DS.D_NM
366 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},
368 // DS.D_MNd // DS.D_MNd
369 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},
371 // DS.D_NDS, // DS.D_NDS,
372 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},
375 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},
377 // DS.D_YN // DS.D_YN
378 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},
380 // DS.D_YNd // DS.D_YNd
381 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},
383 // DS.D_YM // DS.D_YM
384 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},
386 // DS.D_YMd // DS.D_YMd
387 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},
390 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},
393 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},
395 // DS.T_Nt // DS.T_Nt
396 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},
398 // DS.T_NNt // DS.T_NNt
399 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},
401 // End NumEnd NumAmPm NumSpace NumDaySep NumTimesep MonthEnd MonthSpace MonthDSep NumDateSuff NumTimeSuff DayOfWeek YearSpace YearDateSep YearEnd TimeZone Era UTCMark
403 internal const String GMTName = "GMT";
404 internal const String ZuluName = "Z";
407 // Search from the index of str at str.Index to see if the target string exists in the str.
409 private static bool MatchWord(ref __DTString str, String target)
411 if (target.Length > (str.Value.Length - str.Index))
416 if (str.CompareInfo.Compare(str.Value.Slice(str.Index, target.Length), target, CompareOptions.IgnoreCase) != 0)
421 int nextCharIndex = str.Index + target.Length;
423 if (nextCharIndex < str.Value.Length)
425 char nextCh = str.Value[nextCharIndex];
426 if (Char.IsLetter(nextCh))
431 str.Index = nextCharIndex;
432 if (str.Index < str.Length)
434 str.m_current = str.Value[str.Index];
442 // Check the word at the current index to see if it matches GMT name or Zulu name.
444 private static bool GetTimeZoneName(ref __DTString str)
446 if (MatchWord(ref str, GMTName))
451 if (MatchWord(ref str, ZuluName))
459 internal static bool IsDigit(char ch) => (uint)(ch - '0') <= 9;
461 /*=================================ParseFraction==========================
462 **Action: Starting at the str.Index, which should be a decimal symbol.
463 ** if the current character is a digit, parse the remaining
464 ** numbers as fraction. For example, if the sub-string starting at str.Index is "123", then
465 ** the method will return 0.123
466 **Returns: The fraction number.
468 ** str the parsing string
470 ============================================================================*/
472 private static bool ParseFraction(ref __DTString str, out double result)
475 double decimalBase = 0.1;
479 && IsDigit(ch = str.m_current))
481 result += (ch - '0') * decimalBase;
488 /*=================================ParseTimeZone==========================
489 **Action: Parse the timezone offset in the following format:
490 ** "+8", "+08", "+0800", "+0800"
491 ** This method is used by DateTime.Parse().
492 **Returns: The TimeZone offset.
494 ** str the parsing string
496 ** FormatException if invalid timezone format is found.
497 ============================================================================*/
499 private static bool ParseTimeZone(ref __DTString str, ref TimeSpan result)
501 // The hour/minute offset for timezone.
503 int minuteOffset = 0;
506 // Consume the +/- character that has already been read
507 sub = str.GetSubString();
512 char offsetChar = sub[0];
513 if (offsetChar != '+' && offsetChar != '-')
517 str.ConsumeSubString(sub);
519 sub = str.GetSubString();
520 if (sub.type != DTSubStringType.Number)
524 int value = sub.value;
525 int length = sub.length;
526 if (length == 1 || length == 2)
528 // Parsing "+8" or "+08"
530 str.ConsumeSubString(sub);
531 // See if we have minutes
532 sub = str.GetSubString();
533 if (sub.length == 1 && sub[0] == ':')
535 // Parsing "+8:00" or "+08:00"
536 str.ConsumeSubString(sub);
537 sub = str.GetSubString();
538 if (sub.type != DTSubStringType.Number || sub.length < 1 || sub.length > 2)
542 minuteOffset = sub.value;
543 str.ConsumeSubString(sub);
546 else if (length == 3 || length == 4)
548 // Parsing "+800" or "+0800"
549 hourOffset = value / 100;
550 minuteOffset = value % 100;
551 str.ConsumeSubString(sub);
555 // Wrong number of digits
558 Debug.Assert(hourOffset >= 0 && hourOffset <= 99, "hourOffset >= 0 && hourOffset <= 99");
559 Debug.Assert(minuteOffset >= 0 && minuteOffset <= 99, "minuteOffset >= 0 && minuteOffset <= 99");
560 if (minuteOffset < 0 || minuteOffset >= 60)
565 result = new TimeSpan(hourOffset, minuteOffset, 0);
566 if (offsetChar == '-')
568 result = result.Negate();
573 // This is the helper function to handle timezone in string in the format like +/-0800
574 private static bool HandleTimeZone(ref __DTString str, ref DateTimeResult result)
576 if ((str.Index < str.Length - 1))
578 char nextCh = str.Value[str.Index];
579 // Skip whitespace, but don't update the index unless we find a time zone marker
580 int whitespaceCount = 0;
581 while (Char.IsWhiteSpace(nextCh) && str.Index + whitespaceCount < str.Length - 1)
584 nextCh = str.Value[str.Index + whitespaceCount];
586 if (nextCh == '+' || nextCh == '-')
588 str.Index += whitespaceCount;
589 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
591 // Should not have two timezone offsets.
592 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
595 result.flags |= ParseFlags.TimeZoneUsed;
596 if (!ParseTimeZone(ref str, ref result.timeZoneOffset))
598 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
607 // This is the lexer. Check the character at the current index, and put the found token in dtok and
608 // some raw date/time information in raw.
610 private static Boolean Lex(DS dps, ref __DTString str, ref DateTimeToken dtok, ref DateTimeRawInfo raw, ref DateTimeResult result, ref DateTimeFormatInfo dtfi, DateTimeStyles styles)
614 int indexBeforeSeparator;
615 char charBeforeSeparator;
618 dtok.dtt = DTT.Unk; // Assume the token is unkown.
620 str.GetRegularToken(out tokenType, out tokenValue, dtfi);
623 // Builds with _LOGGING defined (x86dbg, amd64chk, etc) support tracing
624 // Set the following internal-only/unsupported environment variables to enable DateTime tracing to the console:
626 // COMPlus_LogEnable=1
627 // COMPlus_LogToConsole=1
628 // COMPlus_LogLevel=9
629 // COMPlus_ManagedLogFacility=0x00001000
632 BCLDebug.Trace("DATETIME", "[DATETIME] Lex({0})\tpos:{1}({2}), {3}, DS.{4}", Hex(str.Value),
633 str.Index, Hex(str.m_current), tokenType, dps);
637 // Look at the regular token.
640 case TokenType.NumberToken:
641 case TokenType.YearNumberToken:
642 if (raw.numCount == 3 || tokenValue == -1)
644 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
645 LexTraceExit("0010", dps);
651 // If the previous parsing state is DS.T_NNt (like 12:01), and we got another number,
652 // so we will have a terminal state DS.TX_NNN (like 12:01:02).
653 // If the previous parsing state is DS.T_Nt (like 12:), and we got another number,
654 // so we will have a terminal state DS.TX_NN (like 12:01).
656 // Look ahead to see if the following character is a decimal point or timezone offset.
657 // This enables us to parse time in the forms of:
658 // "11:22:33.1234" or "11:22:33-08".
661 if ((str.Index < str.Length - 1))
663 char nextCh = str.Value[str.Index];
666 // While ParseFraction can fail, it just means that there were no digits after
667 // the dot. In this case ParseFraction just removes the dot. This is actually
668 // valid for cultures like Albanian, that join the time marker to the time with
669 // with a dot: e.g. "9:03.MD"
670 ParseFraction(ref str, out raw.fraction);
674 if (dps == DS.T_NNt || dps == DS.T_Nt)
676 if ((str.Index < str.Length - 1))
678 if (false == HandleTimeZone(ref str, ref result))
680 LexTraceExit("0020 (value like \"12:01\" or \"12:\" followed by a non-TZ number", dps);
686 dtok.num = tokenValue;
687 if (tokenType == TokenType.YearNumberToken)
691 raw.year = tokenValue;
693 // If we have number which has 3 or more digits (like "001" or "0001"),
694 // we assume this number is a year. Save the currnet raw.numCount in
697 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
699 case TokenType.SEP_End:
700 dtok.dtt = DTT.YearEnd;
702 case TokenType.SEP_Am:
703 case TokenType.SEP_Pm:
704 if (raw.timeMark == TM.NotSet)
706 raw.timeMark = (sep == TokenType.SEP_Am ? TM.AM : TM.PM);
707 dtok.dtt = DTT.YearSpace;
711 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
712 LexTraceExit("0030 (TM.AM/TM.PM Happened more than 1x)", dps);
715 case TokenType.SEP_Space:
716 dtok.dtt = DTT.YearSpace;
718 case TokenType.SEP_Date:
719 dtok.dtt = DTT.YearDateSep;
721 case TokenType.SEP_Time:
722 if (!raw.hasSameDateAndTimeSeparators)
724 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
725 LexTraceExit("0040 (Invalid separator after number)", dps);
729 // we have the date and time separators are same and getting a year number, then change the token to YearDateSep as
730 // we are sure we are not parsing time.
731 dtok.dtt = DTT.YearDateSep;
733 case TokenType.SEP_DateOrOffset:
734 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
735 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
736 if ((dateParsingStates[(int)dps][(int)DTT.YearDateSep] == DS.ERROR)
737 && (dateParsingStates[(int)dps][(int)DTT.YearSpace] > DS.ERROR))
739 str.Index = indexBeforeSeparator;
740 str.m_current = charBeforeSeparator;
741 dtok.dtt = DTT.YearSpace;
745 dtok.dtt = DTT.YearDateSep;
748 case TokenType.SEP_YearSuff:
749 case TokenType.SEP_MonthSuff:
750 case TokenType.SEP_DaySuff:
751 dtok.dtt = DTT.NumDatesuff;
754 case TokenType.SEP_HourSuff:
755 case TokenType.SEP_MinuteSuff:
756 case TokenType.SEP_SecondSuff:
757 dtok.dtt = DTT.NumTimesuff;
761 // Invalid separator after number number.
762 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
763 LexTraceExit("0040 (Invalid separator after number)", dps);
767 // Found the token already. Return now.
769 LexTraceExit("0050 (success)", dps);
772 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
773 LexTraceExit("0060", dps);
776 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
779 // Note here we check if the numCount is less than three.
780 // When we have more than three numbers, it will be caught as error in the state machine.
782 case TokenType.SEP_End:
783 dtok.dtt = DTT.NumEnd;
784 raw.AddNumber(dtok.num);
786 case TokenType.SEP_Am:
787 case TokenType.SEP_Pm:
788 if (raw.timeMark == TM.NotSet)
790 raw.timeMark = (sep == TokenType.SEP_Am ? TM.AM : TM.PM);
791 dtok.dtt = DTT.NumAmpm;
792 // Fix AM/PM parsing case, e.g. "1/10 5 AM"
795 if (!ProcessTerminaltState(DS.DX_NN, ref result, ref styles, ref raw, dtfi))
801 raw.AddNumber(dtok.num);
805 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
808 if (dps == DS.T_NNt || dps == DS.T_Nt)
810 if (false == HandleTimeZone(ref str, ref result))
812 LexTraceExit("0070 (HandleTimeZone returned false)", dps);
817 case TokenType.SEP_Space:
818 dtok.dtt = DTT.NumSpace;
819 raw.AddNumber(dtok.num);
821 case TokenType.SEP_Date:
822 dtok.dtt = DTT.NumDatesep;
823 raw.AddNumber(dtok.num);
825 case TokenType.SEP_DateOrOffset:
826 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
827 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
828 if ((dateParsingStates[(int)dps][(int)DTT.NumDatesep] == DS.ERROR)
829 && (dateParsingStates[(int)dps][(int)DTT.NumSpace] > DS.ERROR))
831 str.Index = indexBeforeSeparator;
832 str.m_current = charBeforeSeparator;
833 dtok.dtt = DTT.NumSpace;
837 dtok.dtt = DTT.NumDatesep;
839 raw.AddNumber(dtok.num);
841 case TokenType.SEP_Time:
842 if (raw.hasSameDateAndTimeSeparators &&
843 (dps == DS.D_Y || dps == DS.D_YN || dps == DS.D_YNd || dps == DS.D_YM || dps == DS.D_YMd))
845 // we are parsing a date and we have the time separator same as date separator, so we mark the token as date separator
846 dtok.dtt = DTT.NumDatesep;
847 raw.AddNumber(dtok.num);
850 dtok.dtt = DTT.NumTimesep;
851 raw.AddNumber(dtok.num);
853 case TokenType.SEP_YearSuff:
856 dtok.num = dtfi.Calendar.ToFourDigitYear(tokenValue);
858 catch (ArgumentOutOfRangeException e)
860 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), e);
861 LexTraceExit("0075 (Calendar.ToFourDigitYear failed)", dps);
864 dtok.dtt = DTT.NumDatesuff;
867 case TokenType.SEP_MonthSuff:
868 case TokenType.SEP_DaySuff:
869 dtok.dtt = DTT.NumDatesuff;
872 case TokenType.SEP_HourSuff:
873 case TokenType.SEP_MinuteSuff:
874 case TokenType.SEP_SecondSuff:
875 dtok.dtt = DTT.NumTimesuff;
878 case TokenType.SEP_LocalTimeMark:
879 dtok.dtt = DTT.NumLocalTimeMark;
880 raw.AddNumber(dtok.num);
883 // Invalid separator after number number.
884 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
885 LexTraceExit("0080", dps);
889 case TokenType.HebrewNumber:
890 if (tokenValue >= 100)
892 // This is a year number
895 raw.year = tokenValue;
897 // If we have number which has 3 or more digits (like "001" or "0001"),
898 // we assume this number is a year. Save the currnet raw.numCount in
901 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
903 case TokenType.SEP_End:
904 dtok.dtt = DTT.YearEnd;
906 case TokenType.SEP_Space:
907 dtok.dtt = DTT.YearSpace;
909 case TokenType.SEP_DateOrOffset:
910 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
911 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
912 if (dateParsingStates[(int)dps][(int)DTT.YearSpace] > DS.ERROR)
914 str.Index = indexBeforeSeparator;
915 str.m_current = charBeforeSeparator;
916 dtok.dtt = DTT.YearSpace;
921 // Invalid separator after number number.
922 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
923 LexTraceExit("0090", dps);
929 // Invalid separator after number number.
930 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
931 LexTraceExit("0100", dps);
937 // This is a day number
938 dtok.num = tokenValue;
939 raw.AddNumber(dtok.num);
941 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
944 // Note here we check if the numCount is less than three.
945 // When we have more than three numbers, it will be caught as error in the state machine.
947 case TokenType.SEP_End:
948 dtok.dtt = DTT.NumEnd;
950 case TokenType.SEP_Space:
951 case TokenType.SEP_Date:
952 dtok.dtt = DTT.NumDatesep;
954 case TokenType.SEP_DateOrOffset:
955 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
956 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
957 if ((dateParsingStates[(int)dps][(int)DTT.NumDatesep] == DS.ERROR)
958 && (dateParsingStates[(int)dps][(int)DTT.NumSpace] > DS.ERROR))
960 str.Index = indexBeforeSeparator;
961 str.m_current = charBeforeSeparator;
962 dtok.dtt = DTT.NumSpace;
966 dtok.dtt = DTT.NumDatesep;
970 // Invalid separator after number number.
971 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
972 LexTraceExit("0110", dps);
977 case TokenType.DayOfWeekToken:
978 if (raw.dayOfWeek == -1)
981 // This is a day of week name.
983 raw.dayOfWeek = tokenValue;
984 dtok.dtt = DTT.DayOfWeek;
988 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
989 LexTraceExit("0120 (DayOfWeek seen more than 1x)", dps);
993 case TokenType.MonthToken:
997 // This is a month name
999 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
1001 case TokenType.SEP_End:
1002 dtok.dtt = DTT.MonthEnd;
1004 case TokenType.SEP_Space:
1005 dtok.dtt = DTT.MonthSpace;
1007 case TokenType.SEP_Date:
1008 dtok.dtt = DTT.MonthDatesep;
1010 case TokenType.SEP_Time:
1011 if (!raw.hasSameDateAndTimeSeparators)
1013 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1014 LexTraceExit("0130 (Invalid separator after month name)", dps);
1018 // we have the date and time separators are same and getting a Month name, then change the token to MonthDatesep as
1019 // we are sure we are not parsing time.
1020 dtok.dtt = DTT.MonthDatesep;
1022 case TokenType.SEP_DateOrOffset:
1023 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
1024 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
1025 if ((dateParsingStates[(int)dps][(int)DTT.MonthDatesep] == DS.ERROR)
1026 && (dateParsingStates[(int)dps][(int)DTT.MonthSpace] > DS.ERROR))
1028 str.Index = indexBeforeSeparator;
1029 str.m_current = charBeforeSeparator;
1030 dtok.dtt = DTT.MonthSpace;
1034 dtok.dtt = DTT.MonthDatesep;
1038 //Invalid separator after month name
1039 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1040 LexTraceExit("0130 (Invalid separator after month name)", dps);
1043 raw.month = tokenValue;
1047 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1048 LexTraceExit("0140 (MonthToken seen more than 1x)", dps);
1052 case TokenType.EraToken:
1053 if (result.era != -1)
1055 result.era = tokenValue;
1060 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1061 LexTraceExit("0150 (EraToken seen when result.era already set)", dps);
1065 case TokenType.JapaneseEraToken:
1066 // Special case for Japanese. We allow Japanese era name to be used even if the calendar is not Japanese Calendar.
1067 result.calendar = JapaneseCalendar.GetDefaultInstance();
1068 dtfi = DateTimeFormatInfo.GetJapaneseCalendarDTFI();
1069 if (result.era != -1)
1071 result.era = tokenValue;
1076 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1077 LexTraceExit("0160 (JapaneseEraToken seen when result.era already set)", dps);
1081 case TokenType.TEraToken:
1082 result.calendar = TaiwanCalendar.GetDefaultInstance();
1083 dtfi = DateTimeFormatInfo.GetTaiwanCalendarDTFI();
1084 if (result.era != -1)
1086 result.era = tokenValue;
1091 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1092 LexTraceExit("0170 (TEraToken seen when result.era already set)", dps);
1096 case TokenType.TimeZoneToken:
1098 // This is a timezone designator
1100 // NOTENOTE : for now, we only support "GMT" and "Z" (for Zulu time).
1102 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
1104 // Should not have two timezone offsets.
1105 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1106 LexTraceExit("0180 (seen GMT or Z more than 1x)", dps);
1109 dtok.dtt = DTT.TimeZone;
1110 result.flags |= ParseFlags.TimeZoneUsed;
1111 result.timeZoneOffset = new TimeSpan(0);
1112 result.flags |= ParseFlags.TimeZoneUtc;
1114 case TokenType.EndOfString:
1117 case TokenType.DateWordToken:
1118 case TokenType.IgnorableSymbol:
1119 // Date words and ignorable symbols can just be skipped over
1123 if (raw.timeMark == TM.NotSet)
1125 raw.timeMark = (TM)tokenValue;
1129 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1130 LexTraceExit("0190 (AM/PM timeMark already set)", dps);
1134 case TokenType.UnknownToken:
1135 if (Char.IsLetter(str.m_current))
1137 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_UnknowDateTimeWord), str.Index);
1138 LexTraceExit("0200", dps);
1142 if ((str.m_current == '-' || str.m_current == '+') && ((result.flags & ParseFlags.TimeZoneUsed) == 0))
1144 Int32 originalIndex = str.Index;
1145 if (ParseTimeZone(ref str, ref result.timeZoneOffset))
1147 result.flags |= ParseFlags.TimeZoneUsed;
1148 LexTraceExit("0220 (success)", dps);
1153 // Time zone parse attempt failed. Fall through to punctuation handling.
1154 str.Index = originalIndex;
1158 // Visual Basic implements string to date conversions on top of DateTime.Parse:
1159 // CDate("#10/10/95#")
1161 if (VerifyValidPunctuation(ref str))
1163 LexTraceExit("0230 (success)", dps);
1167 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1168 LexTraceExit("0240", dps);
1172 LexTraceExit("0250 (success)", dps);
1176 private static Boolean VerifyValidPunctuation(ref __DTString str)
1178 // Compatability Behavior. Allow trailing nulls and surrounding hashes
1179 Char ch = str.Value[str.Index];
1182 bool foundStart = false;
1183 bool foundEnd = false;
1184 for (int i = 0; i < str.Length; i++)
1193 // Having more than two hashes is invalid
1206 else if (ch == '\0')
1208 // Allow nulls only at the end
1214 else if ((!Char.IsWhiteSpace(ch)))
1216 // Anthyhing other than whitespace outside hashes is invalid
1217 if (!foundStart || foundEnd)
1225 // The has was un-paired
1228 // Valid Hash usage: eat the hash and continue.
1232 else if (ch == '\0')
1234 for (int i = str.Index; i < str.Length; i++)
1236 if (str.Value[i] != '\0')
1238 // Nulls are only valid if they are the only trailing character
1242 // Move to the end of the string
1243 str.Index = str.Length;
1249 private const int ORDER_YMD = 0; // The order of date is Year/Month/Day.
1250 private const int ORDER_MDY = 1; // The order of date is Month/Day/Year.
1251 private const int ORDER_DMY = 2; // The order of date is Day/Month/Year.
1252 private const int ORDER_YDM = 3; // The order of date is Year/Day/Month
1253 private const int ORDER_YM = 4; // Year/Month order.
1254 private const int ORDER_MY = 5; // Month/Year order.
1255 private const int ORDER_MD = 6; // Month/Day order.
1256 private const int ORDER_DM = 7; // Day/Month order.
1259 // Decide the year/month/day order from the datePattern.
1261 // Return 0 for YMD, 1 for MDY, 2 for DMY, otherwise -1.
1263 private static Boolean GetYearMonthDayOrder(String datePattern, DateTimeFormatInfo dtfi, out int order)
1266 int monthOrder = -1;
1270 bool inQuote = false;
1272 for (int i = 0; i < datePattern.Length && orderCount < 3; i++)
1274 char ch = datePattern[i];
1275 if (ch == '\\' || ch == '%')
1278 continue; // Skip next character that is escaped by this backslash
1281 if (ch == '\'' || ch == '"')
1290 yearOrder = orderCount++;
1293 // Skip all year pattern charaters.
1295 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'y'; i++)
1302 monthOrder = orderCount++;
1304 // Skip all month pattern characters.
1306 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'M'; i++)
1313 int patternCount = 1;
1315 // Skip all day pattern characters.
1317 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'd'; i++)
1322 // Make sure this is not "ddd" or "dddd", which means day of week.
1324 if (patternCount <= 2)
1326 dayOrder = orderCount++;
1332 if (yearOrder == 0 && monthOrder == 1 && dayOrder == 2)
1337 if (monthOrder == 0 && dayOrder == 1 && yearOrder == 2)
1342 if (dayOrder == 0 && monthOrder == 1 && yearOrder == 2)
1347 if (yearOrder == 0 && dayOrder == 1 && monthOrder == 2)
1357 // Decide the year/month order from the pattern.
1359 // Return 0 for YM, 1 for MY, otherwise -1.
1361 private static Boolean GetYearMonthOrder(String pattern, DateTimeFormatInfo dtfi, out int order)
1364 int monthOrder = -1;
1367 bool inQuote = false;
1368 for (int i = 0; i < pattern.Length && orderCount < 2; i++)
1370 char ch = pattern[i];
1371 if (ch == '\\' || ch == '%')
1374 continue; // Skip next character that is escaped by this backslash
1377 if (ch == '\'' || ch == '"')
1386 yearOrder = orderCount++;
1389 // Skip all year pattern charaters.
1391 for (; i + 1 < pattern.Length && pattern[i + 1] == 'y'; i++)
1397 monthOrder = orderCount++;
1399 // Skip all month pattern characters.
1401 for (; i + 1 < pattern.Length && pattern[i + 1] == 'M'; i++)
1408 if (yearOrder == 0 && monthOrder == 1)
1413 if (monthOrder == 0 && yearOrder == 1)
1423 // Decide the month/day order from the pattern.
1425 // Return 0 for MD, 1 for DM, otherwise -1.
1427 private static Boolean GetMonthDayOrder(String pattern, DateTimeFormatInfo dtfi, out int order)
1429 int monthOrder = -1;
1433 bool inQuote = false;
1434 for (int i = 0; i < pattern.Length && orderCount < 2; i++)
1436 char ch = pattern[i];
1437 if (ch == '\\' || ch == '%')
1440 continue; // Skip next character that is escaped by this backslash
1443 if (ch == '\'' || ch == '"')
1452 int patternCount = 1;
1454 // Skip all day pattern charaters.
1456 for (; i + 1 < pattern.Length && pattern[i + 1] == 'd'; i++)
1462 // Make sure this is not "ddd" or "dddd", which means day of week.
1464 if (patternCount <= 2)
1466 dayOrder = orderCount++;
1471 monthOrder = orderCount++;
1473 // Skip all month pattern characters.
1475 for (; i + 1 < pattern.Length && pattern[i + 1] == 'M'; i++)
1482 if (monthOrder == 0 && dayOrder == 1)
1487 if (dayOrder == 0 && monthOrder == 1)
1497 // Adjust the two-digit year if necessary.
1499 private static bool TryAdjustYear(ref DateTimeResult result, int year, out int adjustedYear)
1505 // the Calendar classes need some real work. Many of the calendars that throw
1506 // don't implement a fast/non-allocating (and non-throwing) IsValid{Year|Day|Month} method.
1507 // we are making a targeted try/catch fix in the in-place release but will revisit this code
1508 // in the next side-by-side release.
1509 year = result.calendar.ToFourDigitYear(year);
1511 catch (ArgumentOutOfRangeException)
1517 adjustedYear = year;
1521 private static bool SetDateYMD(ref DateTimeResult result, int year, int month, int day)
1523 // Note, longer term these checks should be done at the end of the parse. This current
1524 // way of checking creates order dependence with parsing the era name.
1525 if (result.calendar.IsValidDay(year, month, day, result.era))
1527 result.SetDate(year, month, day); // YMD
1533 private static bool SetDateMDY(ref DateTimeResult result, int month, int day, int year)
1535 return (SetDateYMD(ref result, year, month, day));
1538 private static bool SetDateDMY(ref DateTimeResult result, int day, int month, int year)
1540 return (SetDateYMD(ref result, year, month, day));
1543 private static bool SetDateYDM(ref DateTimeResult result, int year, int day, int month)
1545 return (SetDateYMD(ref result, year, month, day));
1548 private static void GetDefaultYear(ref DateTimeResult result, ref DateTimeStyles styles)
1550 result.Year = result.calendar.GetYear(GetDateTimeNow(ref result, ref styles));
1551 result.flags |= ParseFlags.YearDefault;
1554 // Processing teriminal case: DS.DX_NN
1555 private static Boolean GetDayOfNN(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1557 if ((result.flags & ParseFlags.HaveDate) != 0)
1559 // Multiple dates in the input string
1560 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1564 int n1 = raw.GetNumber(0);
1565 int n2 = raw.GetNumber(1);
1567 GetDefaultYear(ref result, ref styles);
1570 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out order))
1572 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1576 if (order == ORDER_MD)
1578 if (SetDateYMD(ref result, result.Year, n1, n2)) // MD
1580 result.flags |= ParseFlags.HaveDate;
1587 if (SetDateYMD(ref result, result.Year, n2, n1)) // DM
1589 result.flags |= ParseFlags.HaveDate;
1593 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1597 // Processing teriminal case: DS.DX_NNN
1598 private static Boolean GetDayOfNNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1600 if ((result.flags & ParseFlags.HaveDate) != 0)
1602 // Multiple dates in the input string
1603 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1607 int n1 = raw.GetNumber(0);
1608 int n2 = raw.GetNumber(1); ;
1609 int n3 = raw.GetNumber(2);
1612 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
1614 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1619 if (order == ORDER_YMD)
1621 if (TryAdjustYear(ref result, n1, out year) && SetDateYMD(ref result, year, n2, n3)) // YMD
1623 result.flags |= ParseFlags.HaveDate;
1627 else if (order == ORDER_MDY)
1629 if (TryAdjustYear(ref result, n3, out year) && SetDateMDY(ref result, n1, n2, year)) // MDY
1631 result.flags |= ParseFlags.HaveDate;
1635 else if (order == ORDER_DMY)
1637 if (TryAdjustYear(ref result, n3, out year) && SetDateDMY(ref result, n1, n2, year)) // DMY
1639 result.flags |= ParseFlags.HaveDate;
1643 else if (order == ORDER_YDM)
1645 if (TryAdjustYear(ref result, n1, out year) && SetDateYDM(ref result, year, n2, n3)) // YDM
1647 result.flags |= ParseFlags.HaveDate;
1651 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1655 private static Boolean GetDayOfMN(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1657 if ((result.flags & ParseFlags.HaveDate) != 0)
1659 // Multiple dates in the input string
1660 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1664 // The interpretation is based on the MonthDayPattern and YearMonthPattern
1666 // MonthDayPattern YearMonthPattern Interpretation
1667 // --------------- ---------------- ---------------
1668 // MMMM dd MMMM yyyy Day
1669 // MMMM dd yyyy MMMM Day
1670 // dd MMMM MMMM yyyy Year
1671 // dd MMMM yyyy MMMM Day
1673 // In the first and last cases, it could be either or neither, but a day is a better default interpretation
1674 // than a 2 digit year.
1677 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder))
1679 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1682 if (monthDayOrder == ORDER_DM)
1685 if (!GetYearMonthOrder(dtfi.YearMonthPattern, dtfi, out yearMonthOrder))
1687 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.YearMonthPattern);
1690 if (yearMonthOrder == ORDER_MY)
1693 if (!TryAdjustYear(ref result, raw.GetNumber(0), out year) || !SetDateYMD(ref result, year, raw.month, 1))
1695 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1702 GetDefaultYear(ref result, ref styles);
1703 if (!SetDateYMD(ref result, result.Year, raw.month, raw.GetNumber(0)))
1705 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1711 ////////////////////////////////////////////////////////////////////////
1713 // Deal with the terminal state for Hebrew Month/Day pattern
1715 ////////////////////////////////////////////////////////////////////////
1717 private static Boolean GetHebrewDayOfNM(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1720 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder))
1722 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1725 result.Month = raw.month;
1726 if (monthDayOrder == ORDER_DM || monthDayOrder == ORDER_MD)
1728 if (result.calendar.IsValidDay(result.Year, result.Month, raw.GetNumber(0), result.era))
1730 result.Day = raw.GetNumber(0);
1734 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1738 private static Boolean GetDayOfNM(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1740 if ((result.flags & ParseFlags.HaveDate) != 0)
1742 // Multiple dates in the input string
1743 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1747 // The interpretation is based on the MonthDayPattern and YearMonthPattern
1749 // MonthDayPattern YearMonthPattern Interpretation
1750 // --------------- ---------------- ---------------
1751 // MMMM dd MMMM yyyy Day
1752 // MMMM dd yyyy MMMM Year
1753 // dd MMMM MMMM yyyy Day
1754 // dd MMMM yyyy MMMM Day
1756 // In the first and last cases, it could be either or neither, but a day is a better default interpretation
1757 // than a 2 digit year.
1760 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder))
1762 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1765 if (monthDayOrder == ORDER_MD)
1768 if (!GetYearMonthOrder(dtfi.YearMonthPattern, dtfi, out yearMonthOrder))
1770 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.YearMonthPattern);
1773 if (yearMonthOrder == ORDER_YM)
1776 if (!TryAdjustYear(ref result, raw.GetNumber(0), out year) || !SetDateYMD(ref result, year, raw.month, 1))
1778 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1785 GetDefaultYear(ref result, ref styles);
1786 if (!SetDateYMD(ref result, result.Year, raw.month, raw.GetNumber(0)))
1788 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1794 private static Boolean GetDayOfMNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1796 if ((result.flags & ParseFlags.HaveDate) != 0)
1798 // Multiple dates in the input string
1799 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1803 int n1 = raw.GetNumber(0);
1804 int n2 = raw.GetNumber(1);
1807 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
1809 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1814 if (order == ORDER_MDY)
1816 if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1818 result.SetDate(year, raw.month, n1); // MDY
1819 result.flags |= ParseFlags.HaveDate;
1822 else if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1824 result.SetDate(year, raw.month, n2); // YMD
1825 result.flags |= ParseFlags.HaveDate;
1829 else if (order == ORDER_YMD)
1831 if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1833 result.SetDate(year, raw.month, n2); // YMD
1834 result.flags |= ParseFlags.HaveDate;
1837 else if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1839 result.SetDate(year, raw.month, n1); // DMY
1840 result.flags |= ParseFlags.HaveDate;
1844 else if (order == ORDER_DMY)
1846 if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1848 result.SetDate(year, raw.month, n1); // DMY
1849 result.flags |= ParseFlags.HaveDate;
1852 else if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1854 result.SetDate(year, raw.month, n2); // YMD
1855 result.flags |= ParseFlags.HaveDate;
1860 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1864 private static Boolean GetDayOfYNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1866 if ((result.flags & ParseFlags.HaveDate) != 0)
1868 // Multiple dates in the input string
1869 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1873 int n1 = raw.GetNumber(0);
1874 int n2 = raw.GetNumber(1);
1875 String pattern = dtfi.ShortDatePattern;
1877 // For compatibility, don't throw if we can't determine the order, but default to YMD instead
1879 if (GetYearMonthDayOrder(pattern, dtfi, out order) && order == ORDER_YDM)
1881 if (SetDateYMD(ref result, raw.year, n2, n1))
1883 result.flags |= ParseFlags.HaveDate;
1884 return true; // Year + DM
1889 if (SetDateYMD(ref result, raw.year, n1, n2))
1891 result.flags |= ParseFlags.HaveDate;
1892 return true; // Year + MD
1895 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1899 private static Boolean GetDayOfNNY(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1901 if ((result.flags & ParseFlags.HaveDate) != 0)
1903 // Multiple dates in the input string
1904 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1908 int n1 = raw.GetNumber(0);
1909 int n2 = raw.GetNumber(1);
1912 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
1914 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1918 if (order == ORDER_MDY || order == ORDER_YMD)
1920 if (SetDateYMD(ref result, raw.year, n1, n2))
1922 result.flags |= ParseFlags.HaveDate;
1923 return true; // MD + Year
1928 if (SetDateYMD(ref result, raw.year, n2, n1))
1930 result.flags |= ParseFlags.HaveDate;
1931 return true; // DM + Year
1934 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1939 private static Boolean GetDayOfYMN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1941 if ((result.flags & ParseFlags.HaveDate) != 0)
1943 // Multiple dates in the input string
1944 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1948 if (SetDateYMD(ref result, raw.year, raw.month, raw.GetNumber(0)))
1950 result.flags |= ParseFlags.HaveDate;
1953 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1957 private static Boolean GetDayOfYN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1959 if ((result.flags & ParseFlags.HaveDate) != 0)
1961 // Multiple dates in the input string
1962 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1966 if (SetDateYMD(ref result, raw.year, raw.GetNumber(0), 1))
1968 result.flags |= ParseFlags.HaveDate;
1971 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1975 private static Boolean GetDayOfYM(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1977 if ((result.flags & ParseFlags.HaveDate) != 0)
1979 // Multiple dates in the input string
1980 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1984 if (SetDateYMD(ref result, raw.year, raw.month, 1))
1986 result.flags |= ParseFlags.HaveDate;
1989 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1993 private static void AdjustTimeMark(DateTimeFormatInfo dtfi, ref DateTimeRawInfo raw)
1995 // Specail case for culture which uses AM as empty string.
1996 // E.g. af-ZA (0x0436)
1999 // In this case, if we are parsing a string like "2005/09/14 12:23", we will assume this is in AM.
2001 if (raw.timeMark == TM.NotSet)
2003 if (dtfi.AMDesignator != null && dtfi.PMDesignator != null)
2005 if (dtfi.AMDesignator.Length == 0 && dtfi.PMDesignator.Length != 0)
2007 raw.timeMark = TM.AM;
2009 if (dtfi.PMDesignator.Length == 0 && dtfi.AMDesignator.Length != 0)
2011 raw.timeMark = TM.PM;
2018 // Adjust hour according to the time mark.
2020 private static Boolean AdjustHour(ref int hour, TM timeMark)
2022 if (timeMark != TM.NotSet)
2024 if (timeMark == TM.AM)
2026 if (hour < 0 || hour > 12)
2030 hour = (hour == 12) ? 0 : hour;
2034 if (hour < 0 || hour > 23)
2047 private static Boolean GetTimeOfN(DateTimeFormatInfo dtfi, ref DateTimeResult result, ref DateTimeRawInfo raw)
2049 if ((result.flags & ParseFlags.HaveTime) != 0)
2051 // Multiple times in the input string
2052 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2056 // In this case, we need a time mark. Check if so.
2058 if (raw.timeMark == TM.NotSet)
2060 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2063 result.Hour = raw.GetNumber(0);
2064 result.flags |= ParseFlags.HaveTime;
2068 private static Boolean GetTimeOfNN(DateTimeFormatInfo dtfi, ref DateTimeResult result, ref DateTimeRawInfo raw)
2070 Debug.Assert(raw.numCount >= 2, "raw.numCount >= 2");
2071 if ((result.flags & ParseFlags.HaveTime) != 0)
2073 // Multiple times in the input string
2074 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2078 result.Hour = raw.GetNumber(0);
2079 result.Minute = raw.GetNumber(1);
2080 result.flags |= ParseFlags.HaveTime;
2084 private static Boolean GetTimeOfNNN(DateTimeFormatInfo dtfi, ref DateTimeResult result, ref DateTimeRawInfo raw)
2086 if ((result.flags & ParseFlags.HaveTime) != 0)
2088 // Multiple times in the input string
2089 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2092 Debug.Assert(raw.numCount >= 3, "raw.numCount >= 3");
2093 result.Hour = raw.GetNumber(0);
2094 result.Minute = raw.GetNumber(1);
2095 result.Second = raw.GetNumber(2);
2096 result.flags |= ParseFlags.HaveTime;
2101 // Processing terminal state: A Date suffix followed by one number.
2103 private static Boolean GetDateOfDSN(ref DateTimeResult result, ref DateTimeRawInfo raw)
2105 if (raw.numCount != 1 || result.Day != -1)
2107 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2110 result.Day = raw.GetNumber(0);
2114 private static Boolean GetDateOfNDS(ref DateTimeResult result, ref DateTimeRawInfo raw)
2116 if (result.Month == -1)
2118 //Should have a month suffix
2119 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2122 if (result.Year != -1)
2124 // Aleady has a year suffix
2125 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2128 if (!TryAdjustYear(ref result, raw.GetNumber(0), out result.Year))
2130 // the year value is out of range
2131 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2138 private static Boolean GetDateOfNNDS(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2140 // For partial CJK Dates, the only valid formats are with a specified year, followed by two numbers, which
2141 // will be the Month and Day, and with a specified Month, when the numbers are either the year and day or
2142 // day and year, depending on the short date pattern.
2144 if ((result.flags & ParseFlags.HaveYear) != 0)
2146 if (((result.flags & ParseFlags.HaveMonth) == 0) && ((result.flags & ParseFlags.HaveDay) == 0))
2148 if (TryAdjustYear(ref result, raw.year, out result.Year) && SetDateYMD(ref result, result.Year, raw.GetNumber(0), raw.GetNumber(1)))
2154 else if ((result.flags & ParseFlags.HaveMonth) != 0)
2156 if (((result.flags & ParseFlags.HaveYear) == 0) && ((result.flags & ParseFlags.HaveDay) == 0))
2159 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
2161 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
2165 if (order == ORDER_YMD)
2167 if (TryAdjustYear(ref result, raw.GetNumber(0), out year) && SetDateYMD(ref result, year, result.Month, raw.GetNumber(1)))
2174 if (TryAdjustYear(ref result, raw.GetNumber(1), out year) && SetDateYMD(ref result, year, result.Month, raw.GetNumber(0)))
2181 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2186 // A date suffix is found, use this method to put the number into the result.
2188 private static bool ProcessDateTimeSuffix(ref DateTimeResult result, ref DateTimeRawInfo raw, ref DateTimeToken dtok)
2190 switch (dtok.suffix)
2192 case TokenType.SEP_YearSuff:
2193 if ((result.flags & ParseFlags.HaveYear) != 0)
2197 result.flags |= ParseFlags.HaveYear;
2198 result.Year = raw.year = dtok.num;
2200 case TokenType.SEP_MonthSuff:
2201 if ((result.flags & ParseFlags.HaveMonth) != 0)
2205 result.flags |= ParseFlags.HaveMonth;
2206 result.Month = raw.month = dtok.num;
2208 case TokenType.SEP_DaySuff:
2209 if ((result.flags & ParseFlags.HaveDay) != 0)
2213 result.flags |= ParseFlags.HaveDay;
2214 result.Day = dtok.num;
2216 case TokenType.SEP_HourSuff:
2217 if ((result.flags & ParseFlags.HaveHour) != 0)
2221 result.flags |= ParseFlags.HaveHour;
2222 result.Hour = dtok.num;
2224 case TokenType.SEP_MinuteSuff:
2225 if ((result.flags & ParseFlags.HaveMinute) != 0)
2229 result.flags |= ParseFlags.HaveMinute;
2230 result.Minute = dtok.num;
2232 case TokenType.SEP_SecondSuff:
2233 if ((result.flags & ParseFlags.HaveSecond) != 0)
2237 result.flags |= ParseFlags.HaveSecond;
2238 result.Second = dtok.num;
2244 ////////////////////////////////////////////////////////////////////////
2247 // This is used by DateTime.Parse().
2248 // Process the terminal state for the Hebrew calendar parsing.
2250 ////////////////////////////////////////////////////////////////////////
2252 internal static Boolean ProcessHebrewTerminalState(DS dps, ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2254 // The following are accepted terminal state for Hebrew date.
2258 // Deal with the default long/short date format when the year number is ambigous (i.e. year < 100).
2259 raw.year = raw.GetNumber(1);
2260 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2262 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2265 if (!GetDayOfMNN(ref result, ref raw, dtfi))
2271 // Deal with the default long/short date format when the year number is NOT ambigous (i.e. year >= 100).
2272 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2274 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2277 if (!GetDayOfYMN(ref result, ref raw, dtfi))
2283 // When formatting, we only format up to the hundred digit of the Hebrew year, although Hebrew year is now over 5000.
2284 // E.g. if the year is 5763, we only format as 763. so we do the reverse when parsing.
2285 if (raw.year < 1000)
2289 if (!GetDayOfNNY(ref result, ref raw, dtfi))
2293 if (!dtfi.YearMonthAdjustment(ref result.Year, ref raw.month, true))
2295 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2301 // Deal with Month/Day pattern.
2302 GetDefaultYear(ref result, ref styles);
2303 if (!dtfi.YearMonthAdjustment(ref result.Year, ref raw.month, true))
2305 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2308 if (!GetHebrewDayOfNM(ref result, ref raw, dtfi))
2314 // Deal with Year/Month pattern.
2315 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2317 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2320 if (!GetDayOfYM(ref result, ref raw, dtfi))
2326 // Deal hour + AM/PM
2327 if (!GetTimeOfN(dtfi, ref result, ref raw))
2333 if (!GetTimeOfNN(dtfi, ref result, ref raw))
2339 if (!GetTimeOfNNN(dtfi, ref result, ref raw))
2345 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2351 // We have reached a terminal state. Reset the raw num count.
2359 // A terminal state has been reached, call the appropriate function to fill in the parsing result.
2360 // Return true if the state is a terminal state.
2362 internal static Boolean ProcessTerminaltState(DS dps, ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2368 passed = GetDayOfNN(ref result, ref styles, ref raw, dtfi);
2371 passed = GetDayOfNNN(ref result, ref raw, dtfi);
2374 passed = GetDayOfMN(ref result, ref styles, ref raw, dtfi);
2377 passed = GetDayOfNM(ref result, ref styles, ref raw, dtfi);
2380 passed = GetDayOfMNN(ref result, ref raw, dtfi);
2383 // The result has got the correct value. No need to process.
2387 passed = GetDayOfYNN(ref result, ref raw, dtfi);
2390 passed = GetDayOfNNY(ref result, ref raw, dtfi);
2393 passed = GetDayOfYMN(ref result, ref raw, dtfi);
2396 passed = GetDayOfYN(ref result, ref raw, dtfi);
2399 passed = GetDayOfYM(ref result, ref raw, dtfi);
2402 passed = GetTimeOfN(dtfi, ref result, ref raw);
2405 passed = GetTimeOfNN(dtfi, ref result, ref raw);
2408 passed = GetTimeOfNNN(dtfi, ref result, ref raw);
2411 // The result has got the correct value. Nothing to do.
2415 passed = GetDateOfDSN(ref result, ref raw);
2418 passed = GetDateOfNDS(ref result, ref raw);
2421 passed = GetDateOfNNDS(ref result, ref raw, dtfi);
2425 PTSTraceExit(dps, passed);
2434 // We have reached a terminal state. Reset the raw num count.
2441 internal static DateTime Parse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles)
2443 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
2445 if (TryParse(s, dtfi, styles, ref result))
2447 return result.parsedDate;
2451 throw GetDateTimeParseException(ref result);
2455 internal static DateTime Parse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out TimeSpan offset)
2457 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
2459 result.flags |= ParseFlags.CaptureOffset;
2460 if (TryParse(s, dtfi, styles, ref result))
2462 offset = result.timeZoneOffset;
2463 return result.parsedDate;
2467 throw GetDateTimeParseException(ref result);
2472 internal static bool TryParse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out DateTime result)
2474 result = DateTime.MinValue;
2475 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
2477 if (TryParse(s, dtfi, styles, ref resultData))
2479 result = resultData.parsedDate;
2485 internal static bool TryParse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out DateTime result, out TimeSpan offset)
2487 result = DateTime.MinValue;
2488 offset = TimeSpan.Zero;
2489 DateTimeResult parseResult = new DateTimeResult(); // The buffer to store the parsing result.
2491 parseResult.flags |= ParseFlags.CaptureOffset;
2492 if (TryParse(s, dtfi, styles, ref parseResult))
2494 result = parseResult.parsedDate;
2495 offset = parseResult.timeZoneOffset;
2503 // This is the real method to do the parsing work.
2505 internal static bool TryParse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, ref DateTimeResult result)
2509 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2513 Debug.Assert(dtfi != null, "dtfi == null");
2521 // First try the predefined format.
2524 DS dps = DS.BEGIN; // Date Parsing State.
2525 bool reachTerminalState = false;
2527 DateTimeToken dtok = new DateTimeToken(); // The buffer to store the parsing token.
2528 dtok.suffix = TokenType.SEP_Unk;
2529 DateTimeRawInfo raw = new DateTimeRawInfo(); // The buffer to store temporary parsing information.
2532 Int32* numberPointer = stackalloc Int32[3];
2533 raw.Init(numberPointer);
2535 raw.hasSameDateAndTimeSeparators = dtfi.DateSeparator.Equals(dtfi.TimeSeparator, StringComparison.Ordinal);
2537 result.calendar = dtfi.Calendar;
2538 result.era = Calendar.CurrentEra;
2541 // The string to be parsed. Use a __DTString wrapper so that we can trace the index which
2542 // indicates the begining of next token.
2544 __DTString str = new __DTString(s, dtfi);
2549 // The following loop will break out when we reach the end of the str.
2554 // Call the lexer to get the next token.
2556 // If we find a era in Lex(), the era value will be in raw.era.
2557 if (!Lex(dps, ref str, ref dtok, ref raw, ref result, ref dtfi, styles))
2559 TPTraceExit("0000", dps);
2564 // If the token is not unknown, process it.
2565 // Otherwise, just discard it.
2567 if (dtok.dtt != DTT.Unk)
2570 // Check if we got any CJK Date/Time suffix.
2571 // Since the Date/Time suffix tells us the number belongs to year/month/day/hour/minute/second,
2572 // store the number in the appropriate field in the result.
2574 if (dtok.suffix != TokenType.SEP_Unk)
2576 if (!ProcessDateTimeSuffix(ref result, ref raw, ref dtok))
2578 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2579 TPTraceExit("0010", dps);
2583 dtok.suffix = TokenType.SEP_Unk; // Reset suffix to SEP_Unk;
2586 if (dtok.dtt == DTT.NumLocalTimeMark)
2588 if (dps == DS.D_YNd || dps == DS.D_YN)
2590 // Consider this as ISO 8601 format:
2591 // "yyyy-MM-dd'T'HH:mm:ss" 1999-10-31T02:00:00
2592 TPTraceExit("0020", dps);
2593 return (ParseISO8601(ref raw, ref str, styles, ref result));
2597 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2598 TPTraceExit("0030", dps);
2603 if (raw.hasSameDateAndTimeSeparators)
2605 if (dtok.dtt == DTT.YearEnd || dtok.dtt == DTT.YearSpace || dtok.dtt == DTT.YearDateSep)
2607 // When time and date separators are same and we are hitting a year number while the first parsed part of the string was recognized
2608 // 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
2613 if (dps == DS.T_NNt)
2619 bool atEnd = str.AtEnd();
2620 if (dateParsingStates[(int)dps][(int)dtok.dtt] == DS.ERROR || atEnd)
2624 // we have the case of Serbia have dates in forms 'd.M.yyyy.' so we can expect '.' after the date parts.
2625 // changing the token to end with space instead of Date Separator will avoid failing the parsing.
2627 case DTT.YearDateSep: dtok.dtt = atEnd ? DTT.YearEnd : DTT.YearSpace; break;
2628 case DTT.NumDatesep: dtok.dtt = atEnd ? DTT.NumEnd : DTT.NumSpace; break;
2629 case DTT.NumTimesep: dtok.dtt = atEnd ? DTT.NumEnd : DTT.NumSpace; break;
2630 case DTT.MonthDatesep: dtok.dtt = atEnd ? DTT.MonthEnd : DTT.MonthSpace; break;
2636 // Advance to the next state, and continue
2638 dps = dateParsingStates[(int)dps][(int)dtok.dtt];
2640 if (dps == DS.ERROR)
2642 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2643 TPTraceExit("0040 (invalid state transition)", dps);
2646 else if (dps > DS.ERROR)
2648 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseHebrewRule) != 0)
2650 if (!ProcessHebrewTerminalState(dps, ref result, ref styles, ref raw, dtfi))
2652 TPTraceExit("0050 (ProcessHebrewTerminalState)", dps);
2658 if (!ProcessTerminaltState(dps, ref result, ref styles, ref raw, dtfi))
2660 TPTraceExit("0060 (ProcessTerminaltState)", dps);
2664 reachTerminalState = true;
2667 // If we have reached a terminal state, start over from DS.BEGIN again.
2668 // For example, when we parsed "1999-12-23 13:30", we will reach a terminal state at "1999-12-23",
2669 // and we start over so we can continue to parse "12:30".
2674 } while (dtok.dtt != DTT.End && dtok.dtt != DTT.NumEnd && dtok.dtt != DTT.MonthEnd);
2676 if (!reachTerminalState)
2678 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2679 TPTraceExit("0070 (did not reach terminal state)", dps);
2683 AdjustTimeMark(dtfi, ref raw);
2684 if (!AdjustHour(ref result.Hour, raw.timeMark))
2686 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2687 TPTraceExit("0080 (AdjustHour)", dps);
2691 // Check if the parased string only contains hour/minute/second values.
2692 bool bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1);
2695 // Check if any year/month/day is missing in the parsing string.
2696 // If yes, get the default value from today's date.
2698 if (!CheckDefaultDateTime(ref result, ref result.calendar, styles))
2700 TPTraceExit("0090 (failed to fill in missing year/month/day defaults)", dps);
2704 if (!result.calendar.TryToDateTime(result.Year, result.Month, result.Day,
2705 result.Hour, result.Minute, result.Second, 0, result.era, out time))
2707 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2708 TPTraceExit("0100 (result.calendar.TryToDateTime)", dps);
2711 if (raw.fraction > 0)
2713 time = time.AddTicks((long)Math.Round(raw.fraction * Calendar.TicksPerSecond));
2717 // We have to check day of week before we adjust to the time zone.
2718 // Otherwise, the value of day of week may change after adjustting to the time zone.
2720 if (raw.dayOfWeek != -1)
2723 // Check if day of week is correct.
2725 if (raw.dayOfWeek != (int)result.calendar.GetDayOfWeek(time))
2727 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDayOfWeek), null);
2728 TPTraceExit("0110 (dayOfWeek check)", dps);
2733 result.parsedDate = time;
2735 if (!DetermineTimeZoneAdjustments(ref result, styles, bTimeOnly))
2737 TPTraceExit("0120 (DetermineTimeZoneAdjustments)", dps);
2740 TPTraceExit("0130 (success)", dps);
2745 // Handles time zone adjustments and sets DateTimeKind values as required by the styles
2746 private static Boolean DetermineTimeZoneAdjustments(ref DateTimeResult result, DateTimeStyles styles, Boolean bTimeOnly)
2748 if ((result.flags & ParseFlags.CaptureOffset) != 0)
2750 // This is a DateTimeOffset parse, so the offset will actually be captured directly, and
2751 // no adjustment is required in most cases
2752 return DateTimeOffsetTimeZonePostProcessing(ref result, styles);
2756 Int64 offsetTicks = result.timeZoneOffset.Ticks;
2758 // the DateTime offset must be within +- 14:00 hours.
2759 if (offsetTicks < DateTimeOffset.MinOffset || offsetTicks > DateTimeOffset.MaxOffset)
2761 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_OffsetOutOfRange), null);
2766 // The flags AssumeUniveral and AssumeLocal only apply when the input does not have a time zone
2767 if ((result.flags & ParseFlags.TimeZoneUsed) == 0)
2769 // If AssumeLocal or AssumeLocal is used, there will always be a kind specified. As in the
2770 // case when a time zone is present, it will default to being local unless AdjustToUniversal
2771 // is present. These comparisons determine whether setting the kind is sufficient, or if a
2772 // time zone adjustment is required. For consistentcy with the rest of parsing, it is desirable
2773 // to fall through to the Adjust methods below, so that there is consist handling of boundary
2774 // cases like wrapping around on time-only dates and temporarily allowing an adjusted date
2775 // to exceed DateTime.MaxValue
2776 if ((styles & DateTimeStyles.AssumeLocal) != 0)
2778 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2780 result.flags |= ParseFlags.TimeZoneUsed;
2781 result.timeZoneOffset = TimeZoneInfo.GetLocalUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime);
2785 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Local);
2789 else if ((styles & DateTimeStyles.AssumeUniversal) != 0)
2791 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2793 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Utc);
2798 result.flags |= ParseFlags.TimeZoneUsed;
2799 result.timeZoneOffset = TimeSpan.Zero;
2804 // No time zone and no Assume flags, so DateTimeKind.Unspecified is fine
2805 Debug.Assert(result.parsedDate.Kind == DateTimeKind.Unspecified, "result.parsedDate.Kind == DateTimeKind.Unspecified");
2810 if (((styles & DateTimeStyles.RoundtripKind) != 0) && ((result.flags & ParseFlags.TimeZoneUtc) != 0))
2812 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Utc);
2816 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2818 return (AdjustTimeZoneToUniversal(ref result));
2820 return (AdjustTimeZoneToLocal(ref result, bTimeOnly));
2823 // Apply validation and adjustments specific to DateTimeOffset
2824 private static Boolean DateTimeOffsetTimeZonePostProcessing(ref DateTimeResult result, DateTimeStyles styles)
2826 // For DateTimeOffset, default to the Utc or Local offset when an offset was not specified by
2827 // the input string.
2828 if ((result.flags & ParseFlags.TimeZoneUsed) == 0)
2830 if ((styles & DateTimeStyles.AssumeUniversal) != 0)
2832 // AssumeUniversal causes the offset to default to zero (0)
2833 result.timeZoneOffset = TimeSpan.Zero;
2837 // AssumeLocal causes the offset to default to Local. This flag is on by default for DateTimeOffset.
2838 result.timeZoneOffset = TimeZoneInfo.GetLocalUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime);
2842 Int64 offsetTicks = result.timeZoneOffset.Ticks;
2844 // there should be no overflow, because the offset can be no more than -+100 hours and the date already
2845 // fits within a DateTime.
2846 Int64 utcTicks = result.parsedDate.Ticks - offsetTicks;
2848 // For DateTimeOffset, both the parsed time and the corresponding UTC value must be within the boundaries
2849 // of a DateTime instance.
2850 if (utcTicks < DateTime.MinTicks || utcTicks > DateTime.MaxTicks)
2852 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_UTCOutOfRange), null);
2856 // the offset must be within +- 14:00 hours.
2857 if (offsetTicks < DateTimeOffset.MinOffset || offsetTicks > DateTimeOffset.MaxOffset)
2859 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_OffsetOutOfRange), null);
2863 // DateTimeOffset should still honor the AdjustToUniversal flag for consistency with DateTime. It means you
2864 // want to return an adjusted UTC value, so store the utcTicks in the DateTime and set the offset to zero
2865 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2867 if (((result.flags & ParseFlags.TimeZoneUsed) == 0) && ((styles & DateTimeStyles.AssumeUniversal) == 0))
2869 // Handle the special case where the timeZoneOffset was defaulted to Local
2870 Boolean toUtcResult = AdjustTimeZoneToUniversal(ref result);
2871 result.timeZoneOffset = TimeSpan.Zero;
2875 // The constructor should always succeed because of the range check earlier in the function
2876 // Althought it is UTC, internally DateTimeOffset does not use this flag
2877 result.parsedDate = new DateTime(utcTicks, DateTimeKind.Utc);
2878 result.timeZoneOffset = TimeSpan.Zero;
2886 // Adjust the specified time to universal time based on the supplied timezone.
2887 // E.g. when parsing "2001/06/08 14:00-07:00",
2888 // the time is 2001/06/08 14:00, and timeZoneOffset = -07:00.
2889 // The result will be "2001/06/08 21:00"
2891 private static Boolean AdjustTimeZoneToUniversal(ref DateTimeResult result)
2893 long resultTicks = result.parsedDate.Ticks;
2894 resultTicks -= result.timeZoneOffset.Ticks;
2895 if (resultTicks < 0)
2897 resultTicks += Calendar.TicksPerDay;
2900 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2902 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_DateOutOfRange), null);
2905 result.parsedDate = new DateTime(resultTicks, DateTimeKind.Utc);
2910 // Adjust the specified time to universal time based on the supplied timezone,
2911 // and then convert to local time.
2912 // E.g. when parsing "2001/06/08 14:00-04:00", and local timezone is GMT-7.
2913 // the time is 2001/06/08 14:00, and timeZoneOffset = -05:00.
2914 // The result will be "2001/06/08 11:00"
2916 private static Boolean AdjustTimeZoneToLocal(ref DateTimeResult result, bool bTimeOnly)
2918 long resultTicks = result.parsedDate.Ticks;
2919 // Convert to local ticks
2920 TimeZoneInfo tz = TimeZoneInfo.Local;
2921 Boolean isAmbiguousLocalDst = false;
2922 if (resultTicks < Calendar.TicksPerDay)
2925 // This is time of day.
2929 resultTicks -= result.timeZoneOffset.Ticks;
2930 // If the time is time of day, use the current timezone offset.
2931 resultTicks += tz.GetUtcOffset(bTimeOnly ? DateTime.Now : result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime).Ticks;
2933 if (resultTicks < 0)
2935 resultTicks += Calendar.TicksPerDay;
2940 // Adjust timezone to GMT.
2941 resultTicks -= result.timeZoneOffset.Ticks;
2942 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2944 // If the result ticks is greater than DateTime.MaxValue, we can not create a DateTime from this ticks.
2945 // In this case, keep using the old code.
2946 resultTicks += tz.GetUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime).Ticks;
2950 // Convert the GMT time to local time.
2951 DateTime utcDt = new DateTime(resultTicks, DateTimeKind.Utc);
2952 Boolean isDaylightSavings = false;
2953 resultTicks += TimeZoneInfo.GetUtcOffsetFromUtc(utcDt, TimeZoneInfo.Local, out isDaylightSavings, out isAmbiguousLocalDst).Ticks;
2956 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2958 result.parsedDate = DateTime.MinValue;
2959 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_DateOutOfRange), null);
2962 result.parsedDate = new DateTime(resultTicks, DateTimeKind.Local, isAmbiguousLocalDst);
2967 // Parse the ISO8601 format string found during Parse();
2970 private static bool ParseISO8601(ref DateTimeRawInfo raw, ref __DTString str, DateTimeStyles styles, ref DateTimeResult result)
2972 if (raw.year < 0 || raw.GetNumber(0) < 0 || raw.GetNumber(1) < 0)
2978 double partSecond = 0;
2980 str.SkipWhiteSpaces();
2981 if (!ParseDigits(ref str, 2, out hour))
2983 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2986 str.SkipWhiteSpaces();
2987 if (!str.Match(':'))
2989 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2992 str.SkipWhiteSpaces();
2993 if (!ParseDigits(ref str, 2, out minute))
2995 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2998 str.SkipWhiteSpaces();
3001 str.SkipWhiteSpaces();
3002 if (!ParseDigits(ref str, 2, out second))
3004 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3009 if (!ParseFraction(ref str, out partSecond))
3011 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3016 str.SkipWhiteSpaces();
3020 char ch = str.GetChar();
3021 if (ch == '+' || ch == '-')
3023 result.flags |= ParseFlags.TimeZoneUsed;
3024 if (!ParseTimeZone(ref str, ref result.timeZoneOffset))
3026 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3030 else if (ch == 'Z' || ch == 'z')
3032 result.flags |= ParseFlags.TimeZoneUsed;
3033 result.timeZoneOffset = TimeSpan.Zero;
3034 result.flags |= ParseFlags.TimeZoneUtc;
3040 str.SkipWhiteSpaces();
3043 if (!VerifyValidPunctuation(ref str))
3045 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3048 str.SkipWhiteSpaces();
3050 if (str.Match('\0'))
3052 if (!VerifyValidPunctuation(ref str))
3054 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3060 // If this is true, there were non-white space characters remaining in the DateTime
3061 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3067 Calendar calendar = GregorianCalendar.GetDefaultInstance();
3068 if (!calendar.TryToDateTime(raw.year, raw.GetNumber(0), raw.GetNumber(1),
3069 hour, minute, second, 0, result.era, out time))
3071 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
3075 time = time.AddTicks((long)Math.Round(partSecond * Calendar.TicksPerSecond));
3076 result.parsedDate = time;
3077 if (!DetermineTimeZoneAdjustments(ref result, styles, false))
3085 ////////////////////////////////////////////////////////////////////////
3088 // Parse the current word as a Hebrew number.
3089 // This is used by DateTime.ParseExact().
3091 ////////////////////////////////////////////////////////////////////////
3093 internal static bool MatchHebrewDigits(ref __DTString str, int digitLen, out int number)
3097 // Create a context object so that we can parse the Hebrew number text character by character.
3098 HebrewNumberParsingContext context = new HebrewNumberParsingContext(0);
3100 // Set this to ContinueParsing so that we will run the following while loop in the first time.
3101 HebrewNumberParsingState state = HebrewNumberParsingState.ContinueParsing;
3103 while (state == HebrewNumberParsingState.ContinueParsing && str.GetNext())
3105 state = HebrewNumber.ParseByChar(str.GetChar(), ref context);
3108 if (state == HebrewNumberParsingState.FoundEndOfHebrewNumber)
3110 // If we have reached a terminal state, update the result and returns.
3111 number = context.result;
3115 // If we run out of the character before reaching FoundEndOfHebrewNumber, or
3116 // the state is InvalidHebrewNumber or ContinueParsing, we fail to match a Hebrew number.
3121 /*=================================ParseDigits==================================
3122 **Action: Parse the number string in __DTString that are formatted using
3123 ** the following patterns:
3124 ** "0", "00", and "000..0"
3125 **Returns: the integer value
3126 **Arguments: str: a __DTString. The parsing will start from the
3127 ** next character after str.Index.
3128 **Exceptions: FormatException if error in parsing number.
3129 ==============================================================================*/
3131 internal static bool ParseDigits(ref __DTString str, int digitLen, out int result)
3135 // 1 really means 1 or 2 for this call
3136 return ParseDigits(ref str, 1, 2, out result);
3140 return ParseDigits(ref str, digitLen, digitLen, out result);
3144 internal static bool ParseDigits(ref __DTString str, int minDigitLen, int maxDigitLen, out int result)
3146 Debug.Assert(minDigitLen > 0, "minDigitLen > 0");
3147 Debug.Assert(maxDigitLen < 9, "maxDigitLen < 9");
3148 Debug.Assert(minDigitLen <= maxDigitLen, "minDigitLen <= maxDigitLen");
3149 int localResult = 0;
3150 int startingIndex = str.Index;
3151 int tokenLength = 0;
3152 while (tokenLength < maxDigitLen)
3154 if (!str.GetNextDigit())
3159 localResult = localResult * 10 + str.GetDigit();
3162 result = localResult;
3163 if (tokenLength < minDigitLen)
3165 str.Index = startingIndex;
3171 /*=================================ParseFractionExact==================================
3172 **Action: Parse the number string in __DTString that are formatted using
3173 ** the following patterns:
3174 ** "0", "00", and "000..0"
3175 **Returns: the fraction value
3176 **Arguments: str: a __DTString. The parsing will start from the
3177 ** next character after str.Index.
3178 **Exceptions: FormatException if error in parsing number.
3179 ==============================================================================*/
3181 private static bool ParseFractionExact(ref __DTString str, int maxDigitLen, ref double result)
3183 if (!str.GetNextDigit())
3188 result = str.GetDigit();
3191 for (; digitLen < maxDigitLen; digitLen++)
3193 if (!str.GetNextDigit())
3198 result = result * 10 + str.GetDigit();
3201 result /= TimeSpanParse.Pow10(digitLen);
3202 return (digitLen == maxDigitLen);
3205 /*=================================ParseSign==================================
3206 **Action: Parse a positive or a negative sign.
3207 **Returns: true if postive sign. flase if negative sign.
3208 **Arguments: str: a __DTString. The parsing will start from the
3209 ** next character after str.Index.
3210 **Exceptions: FormatException if end of string is encountered or a sign
3211 ** symbol is not found.
3212 ==============================================================================*/
3214 private static bool ParseSign(ref __DTString str, ref bool result)
3218 // A sign symbol ('+' or '-') is expected. However, end of string is encountered.
3221 char ch = str.GetChar();
3232 // A sign symbol ('+' or '-') is expected.
3236 /*=================================ParseTimeZoneOffset==================================
3237 **Action: Parse the string formatted using "z", "zz", "zzz" in DateTime.Format().
3238 **Returns: the TimeSpan for the parsed timezone offset.
3239 **Arguments: str: a __DTString. The parsing will start from the
3240 ** next character after str.Index.
3241 ** len: the repeated number of the "z"
3242 **Exceptions: FormatException if errors in parsing.
3243 ==============================================================================*/
3245 private static bool ParseTimeZoneOffset(ref __DTString str, int len, ref TimeSpan result)
3247 bool isPositive = true;
3249 int minuteOffset = 0;
3255 if (!ParseSign(ref str, ref isPositive))
3259 if (!ParseDigits(ref str, len, out hourOffset))
3265 if (!ParseSign(ref str, ref isPositive))
3270 // Parsing 1 digit will actually parse 1 or 2.
3271 if (!ParseDigits(ref str, 1, out hourOffset))
3279 if (!ParseDigits(ref str, 2, out minuteOffset))
3286 // Since we can not match ':', put the char back.
3288 if (!ParseDigits(ref str, 2, out minuteOffset))
3295 if (minuteOffset < 0 || minuteOffset >= 60)
3300 result = (new TimeSpan(hourOffset, minuteOffset, 0));
3303 result = result.Negate();
3308 /*=================================MatchAbbreviatedMonthName==================================
3309 **Action: Parse the abbreviated month name from string starting at str.Index.
3310 **Returns: A value from 1 to 12 for the first month to the twelveth month.
3311 **Arguments: str: a __DTString. The parsing will start from the
3312 ** next character after str.Index.
3313 **Exceptions: FormatException if an abbreviated month name can not be found.
3314 ==============================================================================*/
3316 private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3318 int maxMatchStrLen = 0;
3323 // Scan the month names (note that some calendars has 13 months) and find
3324 // the matching month name which has the max string length.
3325 // We need to do this because some cultures (e.g. "cs-CZ") which have
3326 // abbreviated month names with the same prefix.
3328 int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3329 for (int i = 1; i <= monthsInYear; i++)
3331 String searchStr = dtfi.GetAbbreviatedMonthName(i);
3332 int matchStrLen = searchStr.Length;
3333 if (dtfi.HasSpacesInMonthNames
3334 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3335 : str.MatchSpecifiedWord(searchStr))
3337 if (matchStrLen > maxMatchStrLen)
3339 maxMatchStrLen = matchStrLen;
3345 // Search leap year form.
3346 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0)
3348 int tempResult = str.MatchLongestWords(dtfi.internalGetLeapYearMonthNames(), ref maxMatchStrLen);
3349 // We found a longer match in the leap year month name. Use this as the result.
3350 // The result from MatchLongestWords is 0 ~ length of word array.
3351 // So we increment the result by one to become the month value.
3352 if (tempResult >= 0)
3354 result = tempResult + 1;
3360 str.Index += (maxMatchStrLen - 1);
3366 /*=================================MatchMonthName==================================
3367 **Action: Parse the month name from string starting at str.Index.
3368 **Returns: A value from 1 to 12 indicating the first month to the twelveth month.
3369 **Arguments: str: a __DTString. The parsing will start from the
3370 ** next character after str.Index.
3371 **Exceptions: FormatException if a month name can not be found.
3372 ==============================================================================*/
3374 private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3376 int maxMatchStrLen = 0;
3381 // Scan the month names (note that some calendars has 13 months) and find
3382 // the matching month name which has the max string length.
3383 // We need to do this because some cultures (e.g. "vi-VN") which have
3384 // month names with the same prefix.
3386 int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3387 for (int i = 1; i <= monthsInYear; i++)
3389 String searchStr = dtfi.GetMonthName(i);
3390 int matchStrLen = searchStr.Length;
3391 if (dtfi.HasSpacesInMonthNames
3392 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3393 : str.MatchSpecifiedWord(searchStr))
3395 if (matchStrLen > maxMatchStrLen)
3397 maxMatchStrLen = matchStrLen;
3403 // Search genitive form.
3404 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0)
3406 int tempResult = str.MatchLongestWords(dtfi.MonthGenitiveNames, ref maxMatchStrLen);
3407 // We found a longer match in the genitive month name. Use this as the result.
3408 // The result from MatchLongestWords is 0 ~ length of word array.
3409 // So we increment the result by one to become the month value.
3410 if (tempResult >= 0)
3412 result = tempResult + 1;
3416 // Search leap year form.
3417 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0)
3419 int tempResult = str.MatchLongestWords(dtfi.internalGetLeapYearMonthNames(), ref maxMatchStrLen);
3420 // We found a longer match in the leap year month name. Use this as the result.
3421 // The result from MatchLongestWords is 0 ~ length of word array.
3422 // So we increment the result by one to become the month value.
3423 if (tempResult >= 0)
3425 result = tempResult + 1;
3432 str.Index += (maxMatchStrLen - 1);
3438 /*=================================MatchAbbreviatedDayName==================================
3439 **Action: Parse the abbreviated day of week name from string starting at str.Index.
3440 **Returns: A value from 0 to 6 indicating Sunday to Saturday.
3441 **Arguments: str: a __DTString. The parsing will start from the
3442 ** next character after str.Index.
3443 **Exceptions: FormatException if a abbreviated day of week name can not be found.
3444 ==============================================================================*/
3446 private static bool MatchAbbreviatedDayName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3448 int maxMatchStrLen = 0;
3452 for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3454 String searchStr = dtfi.GetAbbreviatedDayName(i);
3455 int matchStrLen = searchStr.Length;
3456 if (dtfi.HasSpacesInDayNames
3457 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3458 : str.MatchSpecifiedWord(searchStr))
3460 if (matchStrLen > maxMatchStrLen)
3462 maxMatchStrLen = matchStrLen;
3470 str.Index += maxMatchStrLen - 1;
3476 /*=================================MatchDayName==================================
3477 **Action: Parse the day of week name from string starting at str.Index.
3478 **Returns: A value from 0 to 6 indicating Sunday to Saturday.
3479 **Arguments: str: a __DTString. The parsing will start from the
3480 ** next character after str.Index.
3481 **Exceptions: FormatException if a day of week name can not be found.
3482 ==============================================================================*/
3484 private static bool MatchDayName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3486 // Turkish (tr-TR) got day names with the same prefix.
3487 int maxMatchStrLen = 0;
3491 for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3493 String searchStr = dtfi.GetDayName(i);
3494 int matchStrLen = searchStr.Length;
3495 if (dtfi.HasSpacesInDayNames
3496 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3497 : str.MatchSpecifiedWord(searchStr))
3499 if (matchStrLen > maxMatchStrLen)
3501 maxMatchStrLen = matchStrLen;
3509 str.Index += maxMatchStrLen - 1;
3515 /*=================================MatchEraName==================================
3516 **Action: Parse era name from string starting at str.Index.
3517 **Returns: An era value.
3518 **Arguments: str: a __DTString. The parsing will start from the
3519 ** next character after str.Index.
3520 **Exceptions: FormatException if an era name can not be found.
3521 ==============================================================================*/
3523 private static bool MatchEraName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3527 int[] eras = dtfi.Calendar.Eras;
3531 for (int i = 0; i < eras.Length; i++)
3533 String searchStr = dtfi.GetEraName(eras[i]);
3534 if (str.MatchSpecifiedWord(searchStr))
3536 str.Index += (searchStr.Length - 1);
3540 searchStr = dtfi.GetAbbreviatedEraName(eras[i]);
3541 if (str.MatchSpecifiedWord(searchStr))
3543 str.Index += (searchStr.Length - 1);
3553 /*=================================MatchTimeMark==================================
3554 **Action: Parse the time mark (AM/PM) from string starting at str.Index.
3555 **Returns: TM_AM or TM_PM.
3556 **Arguments: str: a __DTString. The parsing will start from the
3557 ** next character after str.Index.
3558 **Exceptions: FormatException if a time mark can not be found.
3559 ==============================================================================*/
3561 private static bool MatchTimeMark(ref __DTString str, DateTimeFormatInfo dtfi, ref TM result)
3564 // In some cultures have empty strings in AM/PM mark. E.g. af-ZA (0x0436), the AM mark is "", and PM mark is "nm".
3565 if (dtfi.AMDesignator.Length == 0)
3569 if (dtfi.PMDesignator.Length == 0)
3576 String searchStr = dtfi.AMDesignator;
3577 if (searchStr.Length > 0)
3579 if (str.MatchSpecifiedWord(searchStr))
3581 // Found an AM timemark with length > 0.
3582 str.Index += (searchStr.Length - 1);
3587 searchStr = dtfi.PMDesignator;
3588 if (searchStr.Length > 0)
3590 if (str.MatchSpecifiedWord(searchStr))
3592 // Found a PM timemark with length > 0.
3593 str.Index += (searchStr.Length - 1);
3598 str.Index--; // Undo the GetNext call.
3600 if (result != TM.NotSet)
3602 // If one of the AM/PM marks is empty string, return the result.
3608 /*=================================MatchAbbreviatedTimeMark==================================
3609 **Action: Parse the abbreviated time mark (AM/PM) from string starting at str.Index.
3610 **Returns: TM_AM or TM_PM.
3611 **Arguments: str: a __DTString. The parsing will start from the
3612 ** next character after str.Index.
3613 **Exceptions: FormatException if a abbreviated time mark can not be found.
3614 ==============================================================================*/
3616 private static bool MatchAbbreviatedTimeMark(ref __DTString str, DateTimeFormatInfo dtfi, ref TM result)
3618 // NOTENOTE : the assumption here is that abbreviated time mark is the first
3619 // character of the AM/PM designator. If this invariant changes, we have to
3620 // change the code below.
3623 if (str.GetChar() == dtfi.AMDesignator[0])
3628 if (str.GetChar() == dtfi.PMDesignator[0])
3637 /*=================================CheckNewValue==================================
3638 **Action: Check if currentValue is initialized. If not, return the newValue.
3639 ** If yes, check if the current value is equal to newValue. Return false
3640 ** if they are not equal. This is used to check the case like "d" and "dd" are both
3641 ** used to format a string.
3642 **Returns: the correct value for currentValue.
3645 ==============================================================================*/
3647 private static bool CheckNewValue(ref int currentValue, int newValue, char patternChar, ref DateTimeResult result)
3649 if (currentValue == -1)
3651 currentValue = newValue;
3656 if (newValue != currentValue)
3658 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), patternChar);
3665 private static DateTime GetDateTimeNow(ref DateTimeResult result, ref DateTimeStyles styles)
3667 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3669 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
3671 // use the supplied offset to calculate 'Now'
3672 return new DateTime(DateTime.UtcNow.Ticks + result.timeZoneOffset.Ticks, DateTimeKind.Unspecified);
3674 else if ((styles & DateTimeStyles.AssumeUniversal) != 0)
3676 // assume the offset is Utc
3677 return DateTime.UtcNow;
3681 // assume the offset is Local
3682 return DateTime.Now;
3685 private static bool CheckDefaultDateTime(ref DateTimeResult result, ref Calendar cal, DateTimeStyles styles)
3687 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3689 // DateTimeOffset.Parse should allow dates without a year, but only if there is also no time zone marker;
3690 // e.g. "May 1 5pm" is OK, but "May 1 5pm -08:30" is not. This is somewhat pragmatic, since we would
3691 // have to rearchitect parsing completely to allow this one case to correctly handle things like leap
3692 // years and leap months. Is is an extremely corner case, and DateTime is basically incorrect in that
3695 // values like "11:00Z" or "11:00 -3:00" are also acceptable
3697 // if ((month or day is set) and (year is not set and time zone is set))
3699 if (((result.Month != -1) || (result.Day != -1))
3700 && ((result.Year == -1 || ((result.flags & ParseFlags.YearDefault) != 0)) && (result.flags & ParseFlags.TimeZoneUsed) != 0))
3702 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_MissingIncompleteDate), null);
3708 if ((result.Year == -1) || (result.Month == -1) || (result.Day == -1))
3711 The following table describes the behaviors of getting the default value
3712 when a certain year/month/day values are missing.
3714 An "X" means that the value exists. And "--" means that value is missing.
3716 Year Month Day => ResultYear ResultMonth ResultDay Note
3718 X X X Parsed year Parsed month Parsed day
3719 X X -- Parsed Year Parsed month First day If we have year and month, assume the first day of that month.
3720 X -- X Parsed year First month Parsed day If the month is missing, assume first month of that year.
3721 X -- -- Parsed year First month First day If we have only the year, assume the first day of that year.
3723 -- X X CurrentYear Parsed month Parsed day If the year is missing, assume the current year.
3724 -- X -- CurrentYear Parsed month First day If we have only a month value, assume the current year and current day.
3725 -- -- X CurrentYear First month Parsed day If we have only a day value, assume current year and first month.
3726 -- -- -- CurrentYear Current month Current day So this means that if the date string only contains time, you will get current date.
3730 DateTime now = GetDateTimeNow(ref result, ref styles);
3731 if (result.Month == -1 && result.Day == -1)
3733 if (result.Year == -1)
3735 if ((styles & DateTimeStyles.NoCurrentDateDefault) != 0)
3737 // If there is no year/month/day values, and NoCurrentDateDefault flag is used,
3738 // set the year/month/day value to the beginning year/month/day of DateTime().
3739 // Note we should be using Gregorian for the year/month/day.
3740 cal = GregorianCalendar.GetDefaultInstance();
3741 result.Year = result.Month = result.Day = 1;
3745 // Year/Month/Day are all missing.
3746 result.Year = cal.GetYear(now);
3747 result.Month = cal.GetMonth(now);
3748 result.Day = cal.GetDayOfMonth(now);
3753 // Month/Day are both missing.
3760 if (result.Year == -1)
3762 result.Year = cal.GetYear(now);
3764 if (result.Month == -1)
3768 if (result.Day == -1)
3774 // Set Hour/Minute/Second to zero if these value are not in str.
3775 if (result.Hour == -1) result.Hour = 0;
3776 if (result.Minute == -1) result.Minute = 0;
3777 if (result.Second == -1) result.Second = 0;
3778 if (result.era == -1) result.era = Calendar.CurrentEra;
3782 // Expand a pre-defined format string (like "D" for long date) to the real format that
3783 // we are going to use in the date time parsing.
3784 // This method also set the dtfi according/parseInfo to some special pre-defined
3787 private static String ExpandPredefinedFormat(String format, ref DateTimeFormatInfo dtfi, ref ParsingInfo parseInfo, ref DateTimeResult result)
3790 // Check the format to see if we need to override the dtfi to be InvariantInfo,
3791 // and see if we need to set up the userUniversalTime flag.
3796 case 'O': // Round Trip Format
3797 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3798 dtfi = DateTimeFormatInfo.InvariantInfo;
3801 case 'R': // RFC 1123 Standard. (in Universal time)
3802 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3803 dtfi = DateTimeFormatInfo.InvariantInfo;
3805 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3807 result.flags |= ParseFlags.Rfc1123Pattern;
3810 case 's': // Sortable format (in local time)
3811 dtfi = DateTimeFormatInfo.InvariantInfo;
3812 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3814 case 'u': // Universal time format in sortable format.
3815 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3816 dtfi = DateTimeFormatInfo.InvariantInfo;
3818 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3820 result.flags |= ParseFlags.UtcSortPattern;
3823 case 'U': // Universal time format with culture-dependent format.
3824 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3825 result.flags |= ParseFlags.TimeZoneUsed;
3826 result.timeZoneOffset = new TimeSpan(0);
3827 result.flags |= ParseFlags.TimeZoneUtc;
3828 if (dtfi.Calendar.GetType() != typeof(GregorianCalendar))
3830 dtfi = (DateTimeFormatInfo)dtfi.Clone();
3831 dtfi.Calendar = GregorianCalendar.GetDefaultInstance();
3837 // Expand the pre-defined format character to the real format from DateTimeFormatInfo.
3839 return (DateTimeFormat.GetRealFormat(format, dtfi));
3846 // Given a specified format character, parse and update the parsing result.
3848 private static bool ParseByFormat(
3850 ref __DTString format,
3851 ref ParsingInfo parseInfo,
3852 DateTimeFormatInfo dtfi,
3853 ref DateTimeResult result)
3856 int tempYear = 0, tempMonth = 0, tempDay = 0, tempDayOfWeek = 0, tempHour = 0, tempMinute = 0, tempSecond = 0;
3857 double tempFraction = 0;
3858 TM tempTimeMark = 0;
3860 char ch = format.GetChar();
3865 tokenLen = format.GetRepeatCount();
3867 if (dtfi.HasForceTwoDigitYears)
3869 parseResult = ParseDigits(ref str, 1, 4, out tempYear);
3875 parseInfo.fUseTwoDigitYear = true;
3877 parseResult = ParseDigits(ref str, tokenLen, out tempYear);
3879 if (!parseResult && parseInfo.fCustomNumberParser)
3881 parseResult = parseInfo.parseNumberDelegate(ref str, tokenLen, out tempYear);
3885 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3888 if (!CheckNewValue(ref result.Year, tempYear, ch, ref result))
3894 tokenLen = format.GetRepeatCount();
3897 if (!ParseDigits(ref str, tokenLen, out tempMonth))
3899 if (!parseInfo.fCustomNumberParser ||
3900 !parseInfo.parseNumberDelegate(ref str, tokenLen, out tempMonth))
3902 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3911 if (!MatchAbbreviatedMonthName(ref str, dtfi, ref tempMonth))
3913 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3919 if (!MatchMonthName(ref str, dtfi, ref tempMonth))
3921 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3925 result.flags |= ParseFlags.ParsedMonthName;
3927 if (!CheckNewValue(ref result.Month, tempMonth, ch, ref result))
3933 // Day & Day of week
3934 tokenLen = format.GetRepeatCount();
3939 if (!ParseDigits(ref str, tokenLen, out tempDay))
3941 if (!parseInfo.fCustomNumberParser ||
3942 !parseInfo.parseNumberDelegate(ref str, tokenLen, out tempDay))
3944 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3948 if (!CheckNewValue(ref result.Day, tempDay, ch, ref result))
3958 if (!MatchAbbreviatedDayName(ref str, dtfi, ref tempDayOfWeek))
3960 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3967 if (!MatchDayName(ref str, dtfi, ref tempDayOfWeek))
3969 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3973 if (!CheckNewValue(ref parseInfo.dayOfWeek, tempDayOfWeek, ch, ref result))
3980 tokenLen = format.GetRepeatCount();
3981 // Put the era value in result.era.
3982 if (!MatchEraName(ref str, dtfi, ref result.era))
3984 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3989 parseInfo.fUseHour12 = true;
3990 tokenLen = format.GetRepeatCount();
3991 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempHour))
3993 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3996 if (!CheckNewValue(ref result.Hour, tempHour, ch, ref result))
4002 tokenLen = format.GetRepeatCount();
4003 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempHour))
4005 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4008 if (!CheckNewValue(ref result.Hour, tempHour, ch, ref result))
4014 tokenLen = format.GetRepeatCount();
4015 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempMinute))
4017 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4020 if (!CheckNewValue(ref result.Minute, tempMinute, ch, ref result))
4026 tokenLen = format.GetRepeatCount();
4027 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempSecond))
4029 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4032 if (!CheckNewValue(ref result.Second, tempSecond, ch, ref result))
4039 tokenLen = format.GetRepeatCount();
4040 if (tokenLen <= DateTimeFormat.MaxSecondsFractionDigits)
4042 if (!ParseFractionExact(ref str, tokenLen, ref tempFraction))
4046 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4050 if (result.fraction < 0)
4052 result.fraction = tempFraction;
4056 if (tempFraction != result.fraction)
4058 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), ch);
4065 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4071 tokenLen = format.GetRepeatCount();
4074 if (!MatchAbbreviatedTimeMark(ref str, dtfi, ref tempTimeMark))
4076 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4082 if (!MatchTimeMark(ref str, dtfi, ref tempTimeMark))
4084 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4089 if (parseInfo.timeMark == TM.NotSet)
4091 parseInfo.timeMark = tempTimeMark;
4095 if (parseInfo.timeMark != tempTimeMark)
4097 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), ch);
4104 tokenLen = format.GetRepeatCount();
4106 TimeSpan tempTimeZoneOffset = new TimeSpan(0);
4107 if (!ParseTimeZoneOffset(ref str, tokenLen, ref tempTimeZoneOffset))
4109 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4112 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && tempTimeZoneOffset != result.timeZoneOffset)
4114 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'z');
4117 result.timeZoneOffset = tempTimeZoneOffset;
4118 result.flags |= ParseFlags.TimeZoneUsed;
4122 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && result.timeZoneOffset != TimeSpan.Zero)
4124 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'Z');
4128 result.flags |= ParseFlags.TimeZoneUsed;
4129 result.timeZoneOffset = new TimeSpan(0);
4130 result.flags |= ParseFlags.TimeZoneUtc;
4132 // The updating of the indexes is to reflect that ParseExact MatchXXX methods assume that
4133 // they need to increment the index and Parse GetXXX do not. Since we are calling a Parse
4134 // method from inside ParseExact we need to adjust this. Long term, we should try to
4135 // eliminate this discrepancy.
4137 if (!GetTimeZoneName(ref str))
4139 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4145 // This should parse either as a blank, the 'Z' character or a local offset like "-07:00"
4148 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && result.timeZoneOffset != TimeSpan.Zero)
4150 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'K');
4154 result.flags |= ParseFlags.TimeZoneUsed;
4155 result.timeZoneOffset = new TimeSpan(0);
4156 result.flags |= ParseFlags.TimeZoneUtc;
4158 else if (str.Match('+') || str.Match('-'))
4160 str.Index--; // Put the character back for the parser
4161 TimeSpan tempTimeZoneOffset = new TimeSpan(0);
4162 if (!ParseTimeZoneOffset(ref str, 3, ref tempTimeZoneOffset))
4164 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4167 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && tempTimeZoneOffset != result.timeZoneOffset)
4169 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'K');
4172 result.timeZoneOffset = tempTimeZoneOffset;
4173 result.flags |= ParseFlags.TimeZoneUsed;
4175 // Otherwise it is unspecified and we consume no characters
4178 // 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
4179 // We have to exclude the case when the time separator is more than one character and starts with ':' something like "::" for instance.
4180 if (((dtfi.TimeSeparator.Length > 1 && dtfi.TimeSeparator[0] == ':') || !str.Match(':')) &&
4181 !str.Match(dtfi.TimeSeparator))
4183 // A time separator is expected.
4184 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4189 // 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
4190 // We have to exclude the case when the date separator is more than one character and starts with '/' something like "//" for instance.
4191 if (((dtfi.DateSeparator.Length > 1 && dtfi.DateSeparator[0] == '/') || !str.Match('/')) &&
4192 !str.Match(dtfi.DateSeparator))
4194 // A date separator is expected.
4195 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4201 StringBuilder enquotedString = StringBuilderCache.Acquire();
4202 // Use ParseQuoteString so that we can handle escape characters within the quoted string.
4203 if (!TryParseQuoteString(format.Value, format.Index, enquotedString, out tokenLen))
4205 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadQuote), ch);
4206 StringBuilderCache.Release(enquotedString);
4209 format.Index += tokenLen - 1;
4211 // Some cultures uses space in the quoted string. E.g. Spanish has long date format as:
4212 // "dddd, dd' de 'MMMM' de 'yyyy". When inner spaces flag is set, we should skip whitespaces if there is space
4213 // in the quoted string.
4214 String quotedStr = StringBuilderCache.GetStringAndRelease(enquotedString);
4216 for (int i = 0; i < quotedStr.Length; i++)
4218 if (quotedStr[i] == ' ' && parseInfo.fAllowInnerWhite)
4220 str.SkipWhiteSpaces();
4222 else if (!str.Match(quotedStr[i]))
4224 // Can not find the matching quoted string.
4225 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4230 // The "r" and "u" formats incorrectly quoted 'GMT' and 'Z', respectively. We cannot
4231 // correct this mistake for DateTime.ParseExact for compatibility reasons, but we can
4232 // fix it for DateTimeOffset.ParseExact as DateTimeOffset has not been publically released
4234 if ((result.flags & ParseFlags.CaptureOffset) != 0)
4236 if ((result.flags & ParseFlags.Rfc1123Pattern) != 0 && quotedStr == GMTName)
4238 result.flags |= ParseFlags.TimeZoneUsed;
4239 result.timeZoneOffset = TimeSpan.Zero;
4241 else if ((result.flags & ParseFlags.UtcSortPattern) != 0 && quotedStr == ZuluName)
4243 result.flags |= ParseFlags.TimeZoneUsed;
4244 result.timeZoneOffset = TimeSpan.Zero;
4250 // Skip this so we can get to the next pattern character.
4251 // Used in case like "%d", "%y"
4253 // Make sure the next character is not a '%' again.
4254 if (format.Index >= format.Value.Length - 1 || format.Value[format.Index + 1] == '%')
4256 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
4261 // Escape character. For example, "\d".
4262 // Get the next character in format, and see if we can
4263 // find a match in str.
4264 if (format.GetNext())
4266 if (!str.Match(format.GetChar()))
4268 // Can not find a match for the escaped character.
4269 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4275 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
4282 if (format.GetNext())
4284 // If we encounter the pattern ".F", and the dot is not present, it is an optional
4285 // second fraction and we can skip this format.
4286 if (format.Match('F'))
4288 format.GetRepeatCount();
4292 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4299 if (parseInfo.fAllowInnerWhite)
4301 // Skip whitespaces if AllowInnerWhite.
4308 // If the space does not match, and trailing space is allowed, we do
4309 // one more step to see if the next format character can lead to
4310 // successful parsing.
4311 // This is used to deal with special case that a empty string can match
4312 // a specific pattern.
4313 // The example here is af-ZA, which has a time format like "hh:mm:ss tt". However,
4314 // its AM symbol is "" (empty string). If fAllowTrailingWhite is used, and time is in
4315 // the AM, we will trim the whitespaces at the end, which will lead to a failure
4316 // when we are trying to match the space before "tt".
4317 if (parseInfo.fAllowTrailingWhite)
4319 if (format.GetNext())
4321 if (ParseByFormat(ref str, ref format, ref parseInfo, dtfi, ref result))
4327 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4335 if (format.MatchSpecifiedWord(GMTName))
4337 format.Index += (GMTName.Length - 1);
4338 // Found GMT string in format. This means the DateTime string
4339 // is in GMT timezone.
4340 result.flags |= ParseFlags.TimeZoneUsed;
4341 result.timeZoneOffset = TimeSpan.Zero;
4342 if (!str.Match(GMTName))
4344 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4348 else if (!str.Match(ch))
4351 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4361 // The pos should point to a quote character. This method will
4362 // get the string enclosed by the quote character.
4364 internal static bool TryParseQuoteString(ReadOnlySpan<char> format, int pos, StringBuilder result, out int returnValue)
4367 // NOTE : pos will be the index of the quote character in the 'format' string.
4370 int formatLen = format.Length;
4372 char quoteChar = format[pos++]; // Get the character used to quote the following string.
4374 bool foundQuote = false;
4375 while (pos < formatLen)
4377 char ch = format[pos++];
4378 if (ch == quoteChar)
4383 else if (ch == '\\')
4385 // The following are used to support escaped character.
4386 // Escaped character is also supported in the quoted string.
4387 // Therefore, someone can use a format like "'minute:' mm\"" to display:
4389 // because the second double quote is escaped.
4390 if (pos < formatLen)
4392 result.Append(format[pos++]);
4397 // This means that '\' is at the end of the formatting string.
4410 // Here we can't find the matching quote.
4415 // Return the character count including the begin/end quote characters and enclosed string.
4417 returnValue = (pos - beginPos);
4424 /*=================================DoStrictParse==================================
4425 **Action: Do DateTime parsing using the format in formatParam.
4426 **Returns: The parsed DateTime.
4431 ** When the following general formats are used, InvariantInfo is used in dtfi:
4433 ** When the following general formats are used, the time is assumed to be in Universal time.
4436 ** Only GregarianCalendar is supported for now.
4437 ** Only support GMT timezone.
4438 ==============================================================================*/
4440 private static bool DoStrictParse(
4441 ReadOnlySpan<char> s,
4443 DateTimeStyles styles,
4444 DateTimeFormatInfo dtfi,
4445 ref DateTimeResult result)
4447 ParsingInfo parseInfo = new ParsingInfo();
4450 parseInfo.calendar = dtfi.Calendar;
4451 parseInfo.fAllowInnerWhite = ((styles & DateTimeStyles.AllowInnerWhite) != 0);
4452 parseInfo.fAllowTrailingWhite = ((styles & DateTimeStyles.AllowTrailingWhite) != 0);
4454 // We need the original values of the following two below.
4455 String originalFormat = formatParam;
4457 if (formatParam.Length == 1)
4459 if (((result.flags & ParseFlags.CaptureOffset) != 0) && formatParam[0] == 'U')
4461 // The 'U' format is not allowed for DateTimeOffset
4462 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
4465 formatParam = ExpandPredefinedFormat(formatParam, ref dtfi, ref parseInfo, ref result);
4468 bool bTimeOnly = false;
4469 result.calendar = parseInfo.calendar;
4471 if (parseInfo.calendar.ID == CalendarId.HEBREW)
4473 parseInfo.parseNumberDelegate = m_hebrewNumberParser;
4474 parseInfo.fCustomNumberParser = true;
4477 // Reset these values to negative one so that we could throw exception
4478 // if we have parsed every item twice.
4479 result.Hour = result.Minute = result.Second = -1;
4481 __DTString format = new __DTString(formatParam.AsReadOnlySpan(), dtfi, false);
4482 __DTString str = new __DTString(s, dtfi, false);
4484 if (parseInfo.fAllowTrailingWhite)
4486 // Trim trailing spaces if AllowTrailingWhite.
4488 format.RemoveTrailingInQuoteSpaces();
4492 if ((styles & DateTimeStyles.AllowLeadingWhite) != 0)
4494 format.SkipWhiteSpaces();
4495 format.RemoveLeadingInQuoteSpaces();
4496 str.SkipWhiteSpaces();
4500 // Scan every character in format and match the pattern in str.
4502 while (format.GetNext())
4504 // We trim inner spaces here, so that we will not eat trailing spaces when
4505 // AllowTrailingWhite is not used.
4506 if (parseInfo.fAllowInnerWhite)
4508 str.SkipWhiteSpaces();
4510 if (!ParseByFormat(ref str, ref format, ref parseInfo, dtfi, ref result))
4516 if (str.Index < str.Value.Length - 1)
4518 // There are still remaining character in str.
4519 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4523 if (parseInfo.fUseTwoDigitYear && ((dtfi.FormatFlags & DateTimeFormatFlags.UseHebrewRule) == 0))
4525 // A two digit year value is expected. Check if the parsed year value is valid.
4526 if (result.Year >= 100)
4528 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4533 result.Year = parseInfo.calendar.ToFourDigitYear(result.Year);
4535 catch (ArgumentOutOfRangeException e)
4537 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), e);
4542 if (parseInfo.fUseHour12)
4544 if (parseInfo.timeMark == TM.NotSet)
4546 // hh is used, but no AM/PM designator is specified.
4547 // Assume the time is AM.
4548 // Don't throw exceptions in here becasue it is very confusing for the caller.
4549 // I always got confused myself when I use "hh:mm:ss" to parse a time string,
4550 // and ParseExact() throws on me (because I didn't use the 24-hour clock 'HH').
4551 parseInfo.timeMark = TM.AM;
4553 if (result.Hour > 12)
4555 // AM/PM is used, but the value for HH is too big.
4556 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4559 if (parseInfo.timeMark == TM.AM)
4561 if (result.Hour == 12)
4568 result.Hour = (result.Hour == 12) ? 12 : result.Hour + 12;
4573 // Military (24-hour time) mode
4575 // AM cannot be set with a 24-hour time like 17:15.
4576 // PM cannot be set with a 24-hour time like 03:15.
4577 if ((parseInfo.timeMark == TM.AM && result.Hour >= 12)
4578 || (parseInfo.timeMark == TM.PM && result.Hour < 12))
4580 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4586 // Check if the parased string only contains hour/minute/second values.
4587 bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1);
4588 if (!CheckDefaultDateTime(ref result, ref parseInfo.calendar, styles))
4593 if (!bTimeOnly && dtfi.HasYearMonthAdjustment)
4595 if (!dtfi.YearMonthAdjustment(ref result.Year, ref result.Month, ((result.flags & ParseFlags.ParsedMonthName) != 0)))
4597 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
4601 if (!parseInfo.calendar.TryToDateTime(result.Year, result.Month, result.Day,
4602 result.Hour, result.Minute, result.Second, 0, result.era, out result.parsedDate))
4604 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
4607 if (result.fraction > 0)
4609 result.parsedDate = result.parsedDate.AddTicks((long)Math.Round(result.fraction * Calendar.TicksPerSecond));
4613 // We have to check day of week before we adjust to the time zone.
4614 // It is because the value of day of week may change after adjusting
4615 // to the time zone.
4617 if (parseInfo.dayOfWeek != -1)
4620 // Check if day of week is correct.
4622 if (parseInfo.dayOfWeek != (int)parseInfo.calendar.GetDayOfWeek(result.parsedDate))
4624 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDayOfWeek), null);
4630 if (!DetermineTimeZoneAdjustments(ref result, styles, bTimeOnly))
4637 private static Exception GetDateTimeParseException(ref DateTimeResult result)
4639 switch (result.failure)
4641 case ParseFailureKind.ArgumentNull:
4642 return new ArgumentNullException(result.failureArgumentName, SR.GetResourceString(result.failureMessageID));
4643 case ParseFailureKind.Format:
4644 return new FormatException(SR.GetResourceString(result.failureMessageID));
4645 case ParseFailureKind.FormatWithParameter:
4646 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), result.failureMessageFormatArgument));
4647 case ParseFailureKind.FormatBadDateTimeCalendar:
4648 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), result.calendar));
4650 Debug.Assert(false, "Unkown DateTimeParseFailure: " + result);
4655 // Builds with _LOGGING defined (x86dbg, amd64chk, etc) support tracing
4656 // Set the following internal-only/unsupported environment variables to enable DateTime tracing to the console:
4658 // COMPlus_LogEnable=1
4659 // COMPlus_LogToConsole=1
4660 // COMPlus_LogLevel=9
4661 // COMPlus_ManagedLogFacility=0x00001000
4662 [Conditional("_LOGGING")]
4663 internal static void LexTraceExit(string message, DS dps)
4666 if (!_tracingEnabled)
4668 BCLDebug.Trace("DATETIME", "[DATETIME] Lex return {0}, DS.{1}", message, dps);
4671 [Conditional("_LOGGING")]
4672 internal static void PTSTraceExit(DS dps, bool passed)
4675 if (!_tracingEnabled)
4677 BCLDebug.Trace("DATETIME", "[DATETIME] ProcessTerminalState {0} @ DS.{1}", passed ? "passed" : "failed", dps);
4680 [Conditional("_LOGGING")]
4681 internal static void TPTraceExit(string message, DS dps)
4684 if (!_tracingEnabled)
4686 BCLDebug.Trace("DATETIME", "[DATETIME] TryParse return {0}, DS.{1}", message, dps);
4689 [Conditional("_LOGGING")]
4690 internal static void DTFITrace(DateTimeFormatInfo dtfi)
4693 if (!_tracingEnabled)
4696 BCLDebug.Trace("DATETIME", "[DATETIME] DateTimeFormatInfo Properties");
4697 #if !FEATURE_COREFX_GLOBALIZATION
4698 BCLDebug.Trace("DATETIME", " NativeCalendarName {0}", Hex(dtfi.NativeCalendarName));
4700 BCLDebug.Trace("DATETIME", " AMDesignator {0}", Hex(dtfi.AMDesignator));
4701 BCLDebug.Trace("DATETIME", " PMDesignator {0}", Hex(dtfi.PMDesignator));
4702 BCLDebug.Trace("DATETIME", " TimeSeparator {0}", Hex(dtfi.TimeSeparator));
4703 BCLDebug.Trace("DATETIME", " AbbrvDayNames {0}", Hex(dtfi.AbbreviatedDayNames));
4704 BCLDebug.Trace("DATETIME", " ShortestDayNames {0}", Hex(dtfi.ShortestDayNames));
4705 BCLDebug.Trace("DATETIME", " DayNames {0}", Hex(dtfi.DayNames));
4706 BCLDebug.Trace("DATETIME", " AbbrvMonthNames {0}", Hex(dtfi.AbbreviatedMonthNames));
4707 BCLDebug.Trace("DATETIME", " MonthNames {0}", Hex(dtfi.MonthNames));
4708 BCLDebug.Trace("DATETIME", " AbbrvMonthGenNames {0}", Hex(dtfi.AbbreviatedMonthGenitiveNames));
4709 BCLDebug.Trace("DATETIME", " MonthGenNames {0}", Hex(dtfi.MonthGenitiveNames));
4713 // return a string in the form: "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
4714 internal static string Hex(string[] strs)
4716 if (strs == null || strs.Length == 0)
4717 return String.Empty;
4718 if (strs.Length == 1)
4719 return Hex(strs[0]);
4721 int curLineLength = 0;
4722 int maxLineLength = 55;
4723 int newLinePadding = 20;
4726 //invariant: strs.Length >= 2
4727 StringBuilder buffer = new StringBuilder();
4728 buffer.Append(Hex(strs[0]));
4729 curLineLength = buffer.Length;
4732 for (int i = 1; i < strs.Length - 1; i++)
4736 if (s.Length > maxLineLength || (curLineLength + s.Length + 2) > maxLineLength)
4739 buffer.Append(Environment.NewLine);
4740 buffer.Append(' ', newLinePadding);
4745 buffer.Append(", ");
4749 curLineLength += s.Length;
4753 s = Hex(strs[strs.Length - 1]);
4754 if (s.Length > maxLineLength || (curLineLength + s.Length + 6) > maxLineLength)
4756 buffer.Append(Environment.NewLine);
4757 buffer.Append(' ', newLinePadding);
4764 return buffer.ToString();
4766 // return a string in the form: "Sun"
4767 internal static string Hex(string str) => Hex(str.AsReadOnlySpan());
4768 internal static string Hex(ReadOnlySpan<char> str)
4770 StringBuilder buffer = new StringBuilder();
4771 buffer.Append("\"");
4772 for (int i = 0; i < str.Length; i++)
4774 if (str[i] <= '\x007f')
4775 buffer.Append(str[i]);
4777 buffer.Append("\\u" + ((int)str[i]).ToString("x4", CultureInfo.InvariantCulture));
4779 buffer.Append("\"");
4780 return buffer.ToString();
4782 // return an unicode escaped string form of char c
4783 internal static String Hex(char c)
4786 return c.ToString(CultureInfo.InvariantCulture);
4788 return "\\u" + ((int)c).ToString("x4", CultureInfo.InvariantCulture);
4791 internal static bool _tracingEnabled = BCLDebug.CheckEnabled("DATETIME");
4797 // This is a string parsing helper which wraps a String object.
4798 // It has a Index property which tracks
4799 // the current parsing pointer of the string.
4801 internal ref struct __DTString
4804 // Value propery: stores the real string to be parsed.
4806 internal ReadOnlySpan<char> Value;
4809 // Index property: points to the character that we are currently parsing.
4813 // The length of Value string.
4814 internal int Length => Value.Length;
4816 // The current chracter to be looked at.
4817 internal char m_current;
4819 private CompareInfo m_info;
4820 // Flag to indicate if we encouter an digit, we should check for token or not.
4821 // In some cultures, such as mn-MN, it uses "\x0031\x00a0\x0434\x04af\x0433\x044d\x044d\x0440\x00a0\x0441\x0430\x0440" in month names.
4822 private bool m_checkDigitToken;
4824 internal __DTString(ReadOnlySpan<char> str, DateTimeFormatInfo dtfi, bool checkDigitToken) : this(str, dtfi)
4826 m_checkDigitToken = checkDigitToken;
4829 internal __DTString(ReadOnlySpan<char> str, DateTimeFormatInfo dtfi)
4837 m_info = dtfi.CompareInfo;
4838 m_checkDigitToken = ((dtfi.FormatFlags & DateTimeFormatFlags.UseDigitPrefixInTokens) != 0);
4842 m_info = CultureInfo.CurrentCulture.CompareInfo;
4843 m_checkDigitToken = false;
4847 internal CompareInfo CompareInfo
4849 get { return m_info; }
4853 // Advance the Index.
4854 // Return true if Index is NOT at the end of the string.
4857 // while (str.GetNext())
4859 // char ch = str.GetChar()
4861 internal bool GetNext()
4866 m_current = Value[Index];
4872 internal bool AtEnd()
4874 return Index < Length ? false : true;
4877 internal bool Advance(int count)
4879 Debug.Assert(Index + count <= Length, "__DTString::Advance: Index + count <= len");
4883 m_current = Value[Index];
4890 // Used by DateTime.Parse() to get the next token.
4891 internal void GetRegularToken(out TokenType tokenType, out int tokenValue, DateTimeFormatInfo dtfi)
4894 if (Index >= Length)
4896 tokenType = TokenType.EndOfString;
4900 tokenType = TokenType.UnknownToken;
4903 if (DateTimeParse.IsDigit(m_current))
4906 tokenValue = m_current - '0';
4911 // Collect other digits.
4913 while (++Index < Length)
4915 m_current = Value[Index];
4916 value = m_current - '0';
4917 if (value >= 0 && value <= 9)
4919 tokenValue = tokenValue * 10 + value;
4926 if (Index - start > DateTimeParse.MaxDateTimeNumberDigits)
4928 tokenType = TokenType.NumberToken;
4931 else if (Index - start < 3)
4933 tokenType = TokenType.NumberToken;
4937 // If there are more than 3 digits, assume that it's a year value.
4938 tokenType = TokenType.YearNumberToken;
4940 if (m_checkDigitToken)
4943 char saveCh = m_current;
4944 // Re-scan using the staring Index to see if this is a token.
4945 Index = start; // To include the first digit.
4946 m_current = Value[Index];
4949 // This DTFI has tokens starting with digits.
4950 // E.g. mn-MN has month name like "\x0031\x00a0\x0434\x04af\x0433\x044d\x044d\x0440\x00a0\x0441\x0430\x0440"
4951 if (dtfi.Tokenize(TokenType.RegularTokenMask, out tempType, out tempValue, ref this))
4953 tokenType = tempType;
4954 tokenValue = tempValue;
4955 // This is a token, so the Index has been advanced propertly in DTFI.Tokenizer().
4959 // Use the number token value.
4960 // Restore the index.
4966 else if (Char.IsWhiteSpace(m_current))
4968 // Just skip to the next character.
4969 while (++Index < Length)
4971 m_current = Value[Index];
4972 if (!(Char.IsWhiteSpace(m_current)))
4977 // We have reached the end of string.
4978 tokenType = TokenType.EndOfString;
4982 dtfi.Tokenize(TokenType.RegularTokenMask, out tokenType, out tokenValue, ref this);
4986 internal TokenType GetSeparatorToken(DateTimeFormatInfo dtfi, out int indexBeforeSeparator, out char charBeforeSeparator)
4988 indexBeforeSeparator = Index;
4989 charBeforeSeparator = m_current;
4990 TokenType tokenType;
4991 if (!SkipWhiteSpaceCurrent())
4993 // Reach the end of the string.
4994 return (TokenType.SEP_End);
4996 if (!DateTimeParse.IsDigit(m_current))
4998 // Not a digit. Tokenize it.
5000 bool found = dtfi.Tokenize(TokenType.SeparatorTokenMask, out tokenType, out tokenValue, ref this);
5003 tokenType = TokenType.SEP_Space;
5008 // Do nothing here. If we see a number, it will not be a separator. There is no need wasting time trying to find the
5010 tokenType = TokenType.SEP_Space;
5015 [MethodImpl(MethodImplOptions.AggressiveInlining)]
5016 internal bool MatchSpecifiedWord(String target) =>
5017 Index + target.Length <= Length &&
5018 m_info.Compare(Value.Slice(Index, target.Length), target, CompareOptions.IgnoreCase) == 0;
5020 private static readonly Char[] WhiteSpaceChecks = new Char[] { ' ', '\u00A0' };
5022 internal bool MatchSpecifiedWords(String target, bool checkWordBoundary, ref int matchLength)
5024 int valueRemaining = Value.Length - Index;
5025 matchLength = target.Length;
5027 if (matchLength > valueRemaining || m_info.Compare(Value.Slice(Index, matchLength), target, CompareOptions.IgnoreCase) != 0)
5029 // Check word by word
5030 int targetPosition = 0; // Where we are in the target string
5031 int thisPosition = Index; // Where we are in this string
5032 int wsIndex = target.IndexOfAny(WhiteSpaceChecks, targetPosition);
5039 int segmentLength = wsIndex - targetPosition;
5040 if (thisPosition >= Value.Length - segmentLength)
5041 { // Subtraction to prevent overflow.
5044 if (segmentLength == 0)
5046 // If segmentLength == 0, it means that we have leading space in the target string.
5047 // In that case, skip the leading spaces in the target and this string.
5052 // Make sure we also have whitespace in the input string
5053 if (!Char.IsWhiteSpace(Value[thisPosition + segmentLength]))
5057 if (m_info.Compare(Value.Slice(thisPosition, segmentLength), target.AsReadOnlySpan().Slice(targetPosition, segmentLength), CompareOptions.IgnoreCase) != 0)
5061 // Advance the input string
5062 thisPosition = thisPosition + segmentLength + 1;
5064 // Advance our target string
5065 targetPosition = wsIndex + 1;
5068 // Skip past multiple whitespace
5069 while (thisPosition < Value.Length && Char.IsWhiteSpace(Value[thisPosition]))
5074 } while ((wsIndex = target.IndexOfAny(WhiteSpaceChecks, targetPosition)) >= 0);
5075 // now check the last segment;
5076 if (targetPosition < target.Length)
5078 int segmentLength = target.Length - targetPosition;
5079 if (thisPosition > Value.Length - segmentLength)
5083 if (m_info.Compare(Value.Slice(thisPosition, segmentLength), target.AsReadOnlySpan().Slice(targetPosition, segmentLength), CompareOptions.IgnoreCase) != 0)
5090 if (checkWordBoundary)
5092 int nextCharIndex = Index + matchLength;
5093 if (nextCharIndex < Value.Length)
5095 if (Char.IsLetter(Value[nextCharIndex]))
5105 // Check to see if the string starting from Index is a prefix of
5107 // If a match is found, true value is returned and Index is updated to the next character to be parsed.
5108 // Otherwise, Index is unchanged.
5110 internal bool Match(String str)
5112 if (++Index >= Length)
5117 if (str.Length > (Value.Length - Index))
5122 if (m_info.Compare(Value.Slice(Index, str.Length), str, CompareOptions.Ordinal) == 0)
5124 // Update the Index to the end of the matching string.
5125 // So the following GetNext()/Match() opeartion will get
5126 // the next character to be parsed.
5127 Index += (str.Length - 1);
5133 internal bool Match(char ch)
5135 if (++Index >= Length)
5139 if (Value[Index] == ch)
5149 // Actions: From the current position, try matching the longest word in the specified string array.
5150 // E.g. words[] = {"AB", "ABC", "ABCD"}, if the current position points to a substring like "ABC DEF",
5151 // MatchLongestWords(words, ref MaxMatchStrLen) will return 1 (the index), and maxMatchLen will be 3.
5153 // The index that contains the longest word to match
5155 // words The string array that contains words to search.
5156 // maxMatchStrLen [in/out] the initailized maximum length. This parameter can be used to
5157 // find the longest match in two string arrays.
5159 internal int MatchLongestWords(String[] words, ref int maxMatchStrLen)
5162 for (int i = 0; i < words.Length; i++)
5164 String word = words[i];
5165 int matchLength = word.Length;
5166 if (MatchSpecifiedWords(word, false, ref matchLength))
5168 if (matchLength > maxMatchStrLen)
5170 maxMatchStrLen = matchLength;
5180 // Get the number of repeat character after the current character.
5181 // For a string "hh:mm:ss" at Index of 3. GetRepeatCount() = 2, and Index
5182 // will point to the second ':'.
5184 internal int GetRepeatCount()
5186 char repeatChar = Value[Index];
5187 int pos = Index + 1;
5188 while ((pos < Length) && (Value[pos] == repeatChar))
5192 int repeatCount = (pos - Index);
5193 // Update the Index to the end of the repeated characters.
5194 // So the following GetNext() opeartion will get
5195 // the next character to be parsed.
5197 return (repeatCount);
5200 // Return false when end of string is encountered or a non-digit character is found.
5201 [MethodImpl(MethodImplOptions.AggressiveInlining)]
5202 internal bool GetNextDigit() =>
5204 DateTimeParse.IsDigit(Value[Index]);
5207 // Get the current character.
5209 internal char GetChar()
5211 Debug.Assert(Index >= 0 && Index < Length, "Index >= 0 && Index < len");
5212 return (Value[Index]);
5216 // Convert the current character to a digit, and return it.
5218 internal int GetDigit()
5220 Debug.Assert(Index >= 0 && Index < Length, "Index >= 0 && Index < len");
5221 Debug.Assert(DateTimeParse.IsDigit(Value[Index]), "IsDigit(Value[Index])");
5222 return (Value[Index] - '0');
5226 // Eat White Space ahead of the current position
5228 // Return false if end of string is encountered.
5230 internal void SkipWhiteSpaces()
5232 // Look ahead to see if the next character
5234 while (Index + 1 < Length)
5236 char ch = Value[Index + 1];
5237 if (!Char.IsWhiteSpace(ch))
5247 // Skip white spaces from the current position
5249 // Return false if end of string is encountered.
5251 internal bool SkipWhiteSpaceCurrent()
5253 if (Index >= Length)
5258 if (!Char.IsWhiteSpace(m_current))
5263 while (++Index < Length)
5265 m_current = Value[Index];
5266 if (!Char.IsWhiteSpace(m_current))
5275 internal void TrimTail()
5278 while (i >= 0 && Char.IsWhiteSpace(Value[i]))
5282 Value = Value.Slice(0, i + 1);
5285 // Trim the trailing spaces within a quoted string.
5286 // Call this after TrimTail() is done.
5287 internal void RemoveTrailingInQuoteSpaces()
5295 // Check if the last character is a quote.
5296 if (ch == '\'' || ch == '\"')
5298 if (Char.IsWhiteSpace(Value[i - 1]))
5301 while (i >= 1 && Char.IsWhiteSpace(Value[i - 1]))
5305 Value = Value.Remove(i, Value.Length - 1 - i);
5310 // Trim the leading spaces within a quoted string.
5311 // Call this after the leading spaces before quoted string are trimmed.
5312 internal void RemoveLeadingInQuoteSpaces()
5320 // Check if the last character is a quote.
5321 if (ch == '\'' || ch == '\"')
5323 while ((i + 1) < Length && Char.IsWhiteSpace(Value[i + 1]))
5329 Value = Value.Remove(1, i);
5334 internal DTSubString GetSubString()
5336 DTSubString sub = new DTSubString();
5339 while (Index + sub.length < Length)
5341 DTSubStringType currentType;
5342 Char ch = Value[Index + sub.length];
5343 if (ch >= '0' && ch <= '9')
5345 currentType = DTSubStringType.Number;
5349 currentType = DTSubStringType.Other;
5352 if (sub.length == 0)
5354 sub.type = currentType;
5358 if (sub.type != currentType)
5364 if (currentType == DTSubStringType.Number)
5366 // Incorporate the number into the value
5367 // Limit the digits to prevent overflow
5368 if (sub.length > DateTimeParse.MaxDateTimeNumberDigits)
5370 sub.type = DTSubStringType.Invalid;
5373 int number = ch - '0';
5374 Debug.Assert(number >= 0 && number <= 9, "number >= 0 && number <= 9");
5375 sub.value = sub.value * 10 + number;
5379 // For non numbers, just return this length 1 token. This should be expanded
5380 // to more types of thing if this parsing approach is used for things other
5381 // than numbers and single characters
5385 if (sub.length == 0)
5387 sub.type = DTSubStringType.End;
5394 internal void ConsumeSubString(DTSubString sub)
5396 Debug.Assert(sub.index == Index, "sub.index == Index");
5397 Debug.Assert(sub.index + sub.length <= Length, "sub.index + sub.length <= len");
5398 Index = sub.index + sub.length;
5401 m_current = Value[Index];
5406 internal enum DTSubStringType
5415 internal ref struct DTSubString
5417 internal ReadOnlySpan<char> s;
5418 internal Int32 index;
5419 internal Int32 length;
5420 internal DTSubStringType type;
5421 internal Int32 value;
5423 internal Char this[Int32 relativeIndex]
5427 return s[index + relativeIndex];
5433 // The buffer to store the parsing token.
5436 struct DateTimeToken
5438 internal DateTimeParse.DTT dtt; // Store the token
5439 internal TokenType suffix; // Store the CJK Year/Month/Day suffix (if any)
5440 internal int num; // Store the number that we are parsing (if any)
5444 // The buffer to store temporary parsing information.
5447 unsafe struct DateTimeRawInfo
5450 internal int numCount;
5453 internal int dayOfWeek;
5455 internal DateTimeParse.TM timeMark;
5456 internal double fraction;
5457 internal bool hasSameDateAndTimeSeparators;
5459 internal void Init(int* numberBuffer)
5465 timeMark = DateTimeParse.TM.NotSet;
5469 internal unsafe void AddNumber(int value)
5471 num[numCount++] = value;
5473 internal unsafe int GetNumber(int index)
5479 internal enum ParseFailureKind
5484 FormatWithParameter = 3,
5485 FormatBadDateTimeCalendar = 4, // FormatException when ArgumentOutOfRange is thrown by a Calendar.TryToDateTime().
5489 internal enum ParseFlags
5491 HaveYear = 0x00000001,
5492 HaveMonth = 0x00000002,
5493 HaveDay = 0x00000004,
5494 HaveHour = 0x00000008,
5495 HaveMinute = 0x00000010,
5496 HaveSecond = 0x00000020,
5497 HaveTime = 0x00000040,
5498 HaveDate = 0x00000080,
5499 TimeZoneUsed = 0x00000100,
5500 TimeZoneUtc = 0x00000200,
5501 ParsedMonthName = 0x00000400,
5502 CaptureOffset = 0x00000800,
5503 YearDefault = 0x00001000,
5504 Rfc1123Pattern = 0x00002000,
5505 UtcSortPattern = 0x00004000,
5509 // This will store the result of the parsing. And it will be eventually
5510 // used to construct a DateTime instance.
5513 struct DateTimeResult
5519 // Set time defualt to 00:00:00.
5522 internal int Minute;
5523 internal int Second;
5524 internal double fraction;
5528 internal ParseFlags flags;
5530 internal TimeSpan timeZoneOffset;
5532 internal Calendar calendar;
5534 internal DateTime parsedDate;
5536 internal ParseFailureKind failure;
5537 internal string failureMessageID;
5538 internal object failureMessageFormatArgument;
5539 internal string failureArgumentName;
5541 internal void Init()
5550 internal void SetDate(int year, int month, int day)
5556 internal void SetFailure(ParseFailureKind failure, string failureMessageID, object failureMessageFormatArgument)
5558 this.failure = failure;
5559 this.failureMessageID = failureMessageID;
5560 this.failureMessageFormatArgument = failureMessageFormatArgument;
5563 internal void SetFailure(ParseFailureKind failure, string failureMessageID, object failureMessageFormatArgument, string failureArgumentName)
5565 this.failure = failure;
5566 this.failureMessageID = failureMessageID;
5567 this.failureMessageFormatArgument = failureMessageFormatArgument;
5568 this.failureArgumentName = failureArgumentName;
5572 // This is the helper data structure used in ParseExact().
5573 internal struct ParsingInfo
5575 internal Calendar calendar;
5576 internal int dayOfWeek;
5577 internal DateTimeParse.TM timeMark;
5579 internal bool fUseHour12;
5580 internal bool fUseTwoDigitYear;
5581 internal bool fAllowInnerWhite;
5582 internal bool fAllowTrailingWhite;
5583 internal bool fCustomNumberParser;
5584 internal DateTimeParse.MatchNumberDelegate parseNumberDelegate;
5586 internal void Init()
5589 timeMark = DateTimeParse.TM.NotSet;
5594 // The type of token that will be returned by DateTimeFormatInfo.Tokenize().
5596 internal enum TokenType
5598 // The valid token should start from 1.
5600 // Regular tokens. The range is from 0x00 ~ 0xff.
5601 NumberToken = 1, // The number. E.g. "12"
5602 YearNumberToken = 2, // The number which is considered as year number, which has 3 or more digits. E.g. "2003"
5603 Am = 3, // AM timemark. E.g. "AM"
5604 Pm = 4, // PM timemark. E.g. "PM"
5605 MonthToken = 5, // A word (or words) that represents a month name. E.g. "March"
5606 EndOfString = 6, // End of string
5607 DayOfWeekToken = 7, // A word (or words) that represents a day of week name. E.g. "Monday" or "Mon"
5608 TimeZoneToken = 8, // A word that represents a timezone name. E.g. "GMT"
5609 EraToken = 9, // A word that represents a era name. E.g. "A.D."
5610 DateWordToken = 10, // A word that can appear in a DateTime string, but serves no parsing semantics. E.g. "de" in Spanish culture.
5611 UnknownToken = 11, // An unknown word, which signals an error in parsing.
5612 HebrewNumber = 12, // A number that is composed of Hebrew text. Hebrew calendar uses Hebrew digits for year values, month values, and day values.
5613 JapaneseEraToken = 13, // Era name for JapaneseCalendar
5614 TEraToken = 14, // Era name for TaiwanCalendar
5615 IgnorableSymbol = 15, // A separator like "," that is equivalent to whitespace
5618 // Separator tokens.
5619 SEP_Unk = 0x100, // Unknown separator.
5620 SEP_End = 0x200, // The end of the parsing string.
5621 SEP_Space = 0x300, // Whitespace (including comma).
5622 SEP_Am = 0x400, // AM timemark. E.g. "AM"
5623 SEP_Pm = 0x500, // PM timemark. E.g. "PM"
5624 SEP_Date = 0x600, // date separator. E.g. "/"
5625 SEP_Time = 0x700, // time separator. E.g. ":"
5626 SEP_YearSuff = 0x800, // Chinese/Japanese/Korean year suffix.
5627 SEP_MonthSuff = 0x900, // Chinese/Japanese/Korean month suffix.
5628 SEP_DaySuff = 0xa00, // Chinese/Japanese/Korean day suffix.
5629 SEP_HourSuff = 0xb00, // Chinese/Japanese/Korean hour suffix.
5630 SEP_MinuteSuff = 0xc00, // Chinese/Japanese/Korean minute suffix.
5631 SEP_SecondSuff = 0xd00, // Chinese/Japanese/Korean second suffix.
5632 SEP_LocalTimeMark = 0xe00, // 'T', used in ISO 8601 format.
5633 SEP_DateOrOffset = 0xf00, // '-' which could be a date separator or start of a time zone offset
5635 RegularTokenMask = 0x00ff,
5636 SeparatorTokenMask = 0xff00,