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);
625 Trace($"Lex({Hex(str.Value)})\tpos:{str.Index}({Hex(str.m_current)}), {tokenType}, DS.{dps}");
629 // Look at the regular token.
632 case TokenType.NumberToken:
633 case TokenType.YearNumberToken:
634 if (raw.numCount == 3 || tokenValue == -1)
636 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
637 LexTraceExit("0010", dps);
643 // If the previous parsing state is DS.T_NNt (like 12:01), and we got another number,
644 // so we will have a terminal state DS.TX_NNN (like 12:01:02).
645 // If the previous parsing state is DS.T_Nt (like 12:), and we got another number,
646 // so we will have a terminal state DS.TX_NN (like 12:01).
648 // Look ahead to see if the following character is a decimal point or timezone offset.
649 // This enables us to parse time in the forms of:
650 // "11:22:33.1234" or "11:22:33-08".
653 if ((str.Index < str.Length - 1))
655 char nextCh = str.Value[str.Index];
658 // While ParseFraction can fail, it just means that there were no digits after
659 // the dot. In this case ParseFraction just removes the dot. This is actually
660 // valid for cultures like Albanian, that join the time marker to the time with
661 // with a dot: e.g. "9:03.MD"
662 ParseFraction(ref str, out raw.fraction);
666 if (dps == DS.T_NNt || dps == DS.T_Nt)
668 if ((str.Index < str.Length - 1))
670 if (false == HandleTimeZone(ref str, ref result))
672 LexTraceExit("0020 (value like \"12:01\" or \"12:\" followed by a non-TZ number", dps);
678 dtok.num = tokenValue;
679 if (tokenType == TokenType.YearNumberToken)
683 raw.year = tokenValue;
685 // If we have number which has 3 or more digits (like "001" or "0001"),
686 // we assume this number is a year. Save the currnet raw.numCount in
689 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
691 case TokenType.SEP_End:
692 dtok.dtt = DTT.YearEnd;
694 case TokenType.SEP_Am:
695 case TokenType.SEP_Pm:
696 if (raw.timeMark == TM.NotSet)
698 raw.timeMark = (sep == TokenType.SEP_Am ? TM.AM : TM.PM);
699 dtok.dtt = DTT.YearSpace;
703 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
704 LexTraceExit("0030 (TM.AM/TM.PM Happened more than 1x)", dps);
707 case TokenType.SEP_Space:
708 dtok.dtt = DTT.YearSpace;
710 case TokenType.SEP_Date:
711 dtok.dtt = DTT.YearDateSep;
713 case TokenType.SEP_Time:
714 if (!raw.hasSameDateAndTimeSeparators)
716 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
717 LexTraceExit("0040 (Invalid separator after number)", dps);
721 // we have the date and time separators are same and getting a year number, then change the token to YearDateSep as
722 // we are sure we are not parsing time.
723 dtok.dtt = DTT.YearDateSep;
725 case TokenType.SEP_DateOrOffset:
726 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
727 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
728 if ((dateParsingStates[(int)dps][(int)DTT.YearDateSep] == DS.ERROR)
729 && (dateParsingStates[(int)dps][(int)DTT.YearSpace] > DS.ERROR))
731 str.Index = indexBeforeSeparator;
732 str.m_current = charBeforeSeparator;
733 dtok.dtt = DTT.YearSpace;
737 dtok.dtt = DTT.YearDateSep;
740 case TokenType.SEP_YearSuff:
741 case TokenType.SEP_MonthSuff:
742 case TokenType.SEP_DaySuff:
743 dtok.dtt = DTT.NumDatesuff;
746 case TokenType.SEP_HourSuff:
747 case TokenType.SEP_MinuteSuff:
748 case TokenType.SEP_SecondSuff:
749 dtok.dtt = DTT.NumTimesuff;
753 // Invalid separator after number number.
754 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
755 LexTraceExit("0040 (Invalid separator after number)", dps);
759 // Found the token already. Return now.
761 LexTraceExit("0050 (success)", dps);
764 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
765 LexTraceExit("0060", dps);
768 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
771 // Note here we check if the numCount is less than three.
772 // When we have more than three numbers, it will be caught as error in the state machine.
774 case TokenType.SEP_End:
775 dtok.dtt = DTT.NumEnd;
776 raw.AddNumber(dtok.num);
778 case TokenType.SEP_Am:
779 case TokenType.SEP_Pm:
780 if (raw.timeMark == TM.NotSet)
782 raw.timeMark = (sep == TokenType.SEP_Am ? TM.AM : TM.PM);
783 dtok.dtt = DTT.NumAmpm;
784 // Fix AM/PM parsing case, e.g. "1/10 5 AM"
787 if (!ProcessTerminaltState(DS.DX_NN, ref result, ref styles, ref raw, dtfi))
793 raw.AddNumber(dtok.num);
797 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
800 if (dps == DS.T_NNt || dps == DS.T_Nt)
802 if (false == HandleTimeZone(ref str, ref result))
804 LexTraceExit("0070 (HandleTimeZone returned false)", dps);
809 case TokenType.SEP_Space:
810 dtok.dtt = DTT.NumSpace;
811 raw.AddNumber(dtok.num);
813 case TokenType.SEP_Date:
814 dtok.dtt = DTT.NumDatesep;
815 raw.AddNumber(dtok.num);
817 case TokenType.SEP_DateOrOffset:
818 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
819 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
820 if ((dateParsingStates[(int)dps][(int)DTT.NumDatesep] == DS.ERROR)
821 && (dateParsingStates[(int)dps][(int)DTT.NumSpace] > DS.ERROR))
823 str.Index = indexBeforeSeparator;
824 str.m_current = charBeforeSeparator;
825 dtok.dtt = DTT.NumSpace;
829 dtok.dtt = DTT.NumDatesep;
831 raw.AddNumber(dtok.num);
833 case TokenType.SEP_Time:
834 if (raw.hasSameDateAndTimeSeparators &&
835 (dps == DS.D_Y || dps == DS.D_YN || dps == DS.D_YNd || dps == DS.D_YM || dps == DS.D_YMd))
837 // we are parsing a date and we have the time separator same as date separator, so we mark the token as date separator
838 dtok.dtt = DTT.NumDatesep;
839 raw.AddNumber(dtok.num);
842 dtok.dtt = DTT.NumTimesep;
843 raw.AddNumber(dtok.num);
845 case TokenType.SEP_YearSuff:
848 dtok.num = dtfi.Calendar.ToFourDigitYear(tokenValue);
850 catch (ArgumentOutOfRangeException e)
852 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), e);
853 LexTraceExit("0075 (Calendar.ToFourDigitYear failed)", dps);
856 dtok.dtt = DTT.NumDatesuff;
859 case TokenType.SEP_MonthSuff:
860 case TokenType.SEP_DaySuff:
861 dtok.dtt = DTT.NumDatesuff;
864 case TokenType.SEP_HourSuff:
865 case TokenType.SEP_MinuteSuff:
866 case TokenType.SEP_SecondSuff:
867 dtok.dtt = DTT.NumTimesuff;
870 case TokenType.SEP_LocalTimeMark:
871 dtok.dtt = DTT.NumLocalTimeMark;
872 raw.AddNumber(dtok.num);
875 // Invalid separator after number number.
876 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
877 LexTraceExit("0080", dps);
881 case TokenType.HebrewNumber:
882 if (tokenValue >= 100)
884 // This is a year number
887 raw.year = tokenValue;
889 // If we have number which has 3 or more digits (like "001" or "0001"),
890 // we assume this number is a year. Save the currnet raw.numCount in
893 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
895 case TokenType.SEP_End:
896 dtok.dtt = DTT.YearEnd;
898 case TokenType.SEP_Space:
899 dtok.dtt = DTT.YearSpace;
901 case TokenType.SEP_DateOrOffset:
902 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
903 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
904 if (dateParsingStates[(int)dps][(int)DTT.YearSpace] > DS.ERROR)
906 str.Index = indexBeforeSeparator;
907 str.m_current = charBeforeSeparator;
908 dtok.dtt = DTT.YearSpace;
913 // Invalid separator after number number.
914 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
915 LexTraceExit("0090", dps);
921 // Invalid separator after number number.
922 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
923 LexTraceExit("0100", dps);
929 // This is a day number
930 dtok.num = tokenValue;
931 raw.AddNumber(dtok.num);
933 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
936 // Note here we check if the numCount is less than three.
937 // When we have more than three numbers, it will be caught as error in the state machine.
939 case TokenType.SEP_End:
940 dtok.dtt = DTT.NumEnd;
942 case TokenType.SEP_Space:
943 case TokenType.SEP_Date:
944 dtok.dtt = DTT.NumDatesep;
946 case TokenType.SEP_DateOrOffset:
947 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
948 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
949 if ((dateParsingStates[(int)dps][(int)DTT.NumDatesep] == DS.ERROR)
950 && (dateParsingStates[(int)dps][(int)DTT.NumSpace] > DS.ERROR))
952 str.Index = indexBeforeSeparator;
953 str.m_current = charBeforeSeparator;
954 dtok.dtt = DTT.NumSpace;
958 dtok.dtt = DTT.NumDatesep;
962 // Invalid separator after number number.
963 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
964 LexTraceExit("0110", dps);
969 case TokenType.DayOfWeekToken:
970 if (raw.dayOfWeek == -1)
973 // This is a day of week name.
975 raw.dayOfWeek = tokenValue;
976 dtok.dtt = DTT.DayOfWeek;
980 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
981 LexTraceExit("0120 (DayOfWeek seen more than 1x)", dps);
985 case TokenType.MonthToken:
989 // This is a month name
991 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
993 case TokenType.SEP_End:
994 dtok.dtt = DTT.MonthEnd;
996 case TokenType.SEP_Space:
997 dtok.dtt = DTT.MonthSpace;
999 case TokenType.SEP_Date:
1000 dtok.dtt = DTT.MonthDatesep;
1002 case TokenType.SEP_Time:
1003 if (!raw.hasSameDateAndTimeSeparators)
1005 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1006 LexTraceExit("0130 (Invalid separator after month name)", dps);
1010 // we have the date and time separators are same and getting a Month name, then change the token to MonthDatesep as
1011 // we are sure we are not parsing time.
1012 dtok.dtt = DTT.MonthDatesep;
1014 case TokenType.SEP_DateOrOffset:
1015 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
1016 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
1017 if ((dateParsingStates[(int)dps][(int)DTT.MonthDatesep] == DS.ERROR)
1018 && (dateParsingStates[(int)dps][(int)DTT.MonthSpace] > DS.ERROR))
1020 str.Index = indexBeforeSeparator;
1021 str.m_current = charBeforeSeparator;
1022 dtok.dtt = DTT.MonthSpace;
1026 dtok.dtt = DTT.MonthDatesep;
1030 //Invalid separator after month name
1031 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1032 LexTraceExit("0130 (Invalid separator after month name)", dps);
1035 raw.month = tokenValue;
1039 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1040 LexTraceExit("0140 (MonthToken seen more than 1x)", dps);
1044 case TokenType.EraToken:
1045 if (result.era != -1)
1047 result.era = tokenValue;
1052 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1053 LexTraceExit("0150 (EraToken seen when result.era already set)", dps);
1057 case TokenType.JapaneseEraToken:
1058 // Special case for Japanese. We allow Japanese era name to be used even if the calendar is not Japanese Calendar.
1059 result.calendar = JapaneseCalendar.GetDefaultInstance();
1060 dtfi = DateTimeFormatInfo.GetJapaneseCalendarDTFI();
1061 if (result.era != -1)
1063 result.era = tokenValue;
1068 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1069 LexTraceExit("0160 (JapaneseEraToken seen when result.era already set)", dps);
1073 case TokenType.TEraToken:
1074 result.calendar = TaiwanCalendar.GetDefaultInstance();
1075 dtfi = DateTimeFormatInfo.GetTaiwanCalendarDTFI();
1076 if (result.era != -1)
1078 result.era = tokenValue;
1083 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1084 LexTraceExit("0170 (TEraToken seen when result.era already set)", dps);
1088 case TokenType.TimeZoneToken:
1090 // This is a timezone designator
1092 // NOTENOTE : for now, we only support "GMT" and "Z" (for Zulu time).
1094 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
1096 // Should not have two timezone offsets.
1097 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1098 LexTraceExit("0180 (seen GMT or Z more than 1x)", dps);
1101 dtok.dtt = DTT.TimeZone;
1102 result.flags |= ParseFlags.TimeZoneUsed;
1103 result.timeZoneOffset = new TimeSpan(0);
1104 result.flags |= ParseFlags.TimeZoneUtc;
1106 case TokenType.EndOfString:
1109 case TokenType.DateWordToken:
1110 case TokenType.IgnorableSymbol:
1111 // Date words and ignorable symbols can just be skipped over
1115 if (raw.timeMark == TM.NotSet)
1117 raw.timeMark = (TM)tokenValue;
1121 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1122 LexTraceExit("0190 (AM/PM timeMark already set)", dps);
1126 case TokenType.UnknownToken:
1127 if (Char.IsLetter(str.m_current))
1129 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_UnknowDateTimeWord), str.Index);
1130 LexTraceExit("0200", dps);
1134 if ((str.m_current == '-' || str.m_current == '+') && ((result.flags & ParseFlags.TimeZoneUsed) == 0))
1136 Int32 originalIndex = str.Index;
1137 if (ParseTimeZone(ref str, ref result.timeZoneOffset))
1139 result.flags |= ParseFlags.TimeZoneUsed;
1140 LexTraceExit("0220 (success)", dps);
1145 // Time zone parse attempt failed. Fall through to punctuation handling.
1146 str.Index = originalIndex;
1150 // Visual Basic implements string to date conversions on top of DateTime.Parse:
1151 // CDate("#10/10/95#")
1153 if (VerifyValidPunctuation(ref str))
1155 LexTraceExit("0230 (success)", dps);
1159 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1160 LexTraceExit("0240", dps);
1164 LexTraceExit("0250 (success)", dps);
1168 private static Boolean VerifyValidPunctuation(ref __DTString str)
1170 // Compatability Behavior. Allow trailing nulls and surrounding hashes
1171 Char ch = str.Value[str.Index];
1174 bool foundStart = false;
1175 bool foundEnd = false;
1176 for (int i = 0; i < str.Length; i++)
1185 // Having more than two hashes is invalid
1198 else if (ch == '\0')
1200 // Allow nulls only at the end
1206 else if ((!Char.IsWhiteSpace(ch)))
1208 // Anthyhing other than whitespace outside hashes is invalid
1209 if (!foundStart || foundEnd)
1217 // The has was un-paired
1220 // Valid Hash usage: eat the hash and continue.
1224 else if (ch == '\0')
1226 for (int i = str.Index; i < str.Length; i++)
1228 if (str.Value[i] != '\0')
1230 // Nulls are only valid if they are the only trailing character
1234 // Move to the end of the string
1235 str.Index = str.Length;
1241 private const int ORDER_YMD = 0; // The order of date is Year/Month/Day.
1242 private const int ORDER_MDY = 1; // The order of date is Month/Day/Year.
1243 private const int ORDER_DMY = 2; // The order of date is Day/Month/Year.
1244 private const int ORDER_YDM = 3; // The order of date is Year/Day/Month
1245 private const int ORDER_YM = 4; // Year/Month order.
1246 private const int ORDER_MY = 5; // Month/Year order.
1247 private const int ORDER_MD = 6; // Month/Day order.
1248 private const int ORDER_DM = 7; // Day/Month order.
1251 // Decide the year/month/day order from the datePattern.
1253 // Return 0 for YMD, 1 for MDY, 2 for DMY, otherwise -1.
1255 private static Boolean GetYearMonthDayOrder(String datePattern, DateTimeFormatInfo dtfi, out int order)
1258 int monthOrder = -1;
1262 bool inQuote = false;
1264 for (int i = 0; i < datePattern.Length && orderCount < 3; i++)
1266 char ch = datePattern[i];
1267 if (ch == '\\' || ch == '%')
1270 continue; // Skip next character that is escaped by this backslash
1273 if (ch == '\'' || ch == '"')
1282 yearOrder = orderCount++;
1285 // Skip all year pattern charaters.
1287 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'y'; i++)
1294 monthOrder = orderCount++;
1296 // Skip all month pattern characters.
1298 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'M'; i++)
1305 int patternCount = 1;
1307 // Skip all day pattern characters.
1309 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'd'; i++)
1314 // Make sure this is not "ddd" or "dddd", which means day of week.
1316 if (patternCount <= 2)
1318 dayOrder = orderCount++;
1324 if (yearOrder == 0 && monthOrder == 1 && dayOrder == 2)
1329 if (monthOrder == 0 && dayOrder == 1 && yearOrder == 2)
1334 if (dayOrder == 0 && monthOrder == 1 && yearOrder == 2)
1339 if (yearOrder == 0 && dayOrder == 1 && monthOrder == 2)
1349 // Decide the year/month order from the pattern.
1351 // Return 0 for YM, 1 for MY, otherwise -1.
1353 private static Boolean GetYearMonthOrder(String pattern, DateTimeFormatInfo dtfi, out int order)
1356 int monthOrder = -1;
1359 bool inQuote = false;
1360 for (int i = 0; i < pattern.Length && orderCount < 2; i++)
1362 char ch = pattern[i];
1363 if (ch == '\\' || ch == '%')
1366 continue; // Skip next character that is escaped by this backslash
1369 if (ch == '\'' || ch == '"')
1378 yearOrder = orderCount++;
1381 // Skip all year pattern charaters.
1383 for (; i + 1 < pattern.Length && pattern[i + 1] == 'y'; i++)
1389 monthOrder = orderCount++;
1391 // Skip all month pattern characters.
1393 for (; i + 1 < pattern.Length && pattern[i + 1] == 'M'; i++)
1400 if (yearOrder == 0 && monthOrder == 1)
1405 if (monthOrder == 0 && yearOrder == 1)
1415 // Decide the month/day order from the pattern.
1417 // Return 0 for MD, 1 for DM, otherwise -1.
1419 private static Boolean GetMonthDayOrder(String pattern, DateTimeFormatInfo dtfi, out int order)
1421 int monthOrder = -1;
1425 bool inQuote = false;
1426 for (int i = 0; i < pattern.Length && orderCount < 2; i++)
1428 char ch = pattern[i];
1429 if (ch == '\\' || ch == '%')
1432 continue; // Skip next character that is escaped by this backslash
1435 if (ch == '\'' || ch == '"')
1444 int patternCount = 1;
1446 // Skip all day pattern charaters.
1448 for (; i + 1 < pattern.Length && pattern[i + 1] == 'd'; i++)
1454 // Make sure this is not "ddd" or "dddd", which means day of week.
1456 if (patternCount <= 2)
1458 dayOrder = orderCount++;
1463 monthOrder = orderCount++;
1465 // Skip all month pattern characters.
1467 for (; i + 1 < pattern.Length && pattern[i + 1] == 'M'; i++)
1474 if (monthOrder == 0 && dayOrder == 1)
1479 if (dayOrder == 0 && monthOrder == 1)
1489 // Adjust the two-digit year if necessary.
1491 private static bool TryAdjustYear(ref DateTimeResult result, int year, out int adjustedYear)
1497 // the Calendar classes need some real work. Many of the calendars that throw
1498 // don't implement a fast/non-allocating (and non-throwing) IsValid{Year|Day|Month} method.
1499 // we are making a targeted try/catch fix in the in-place release but will revisit this code
1500 // in the next side-by-side release.
1501 year = result.calendar.ToFourDigitYear(year);
1503 catch (ArgumentOutOfRangeException)
1509 adjustedYear = year;
1513 private static bool SetDateYMD(ref DateTimeResult result, int year, int month, int day)
1515 // Note, longer term these checks should be done at the end of the parse. This current
1516 // way of checking creates order dependence with parsing the era name.
1517 if (result.calendar.IsValidDay(year, month, day, result.era))
1519 result.SetDate(year, month, day); // YMD
1525 private static bool SetDateMDY(ref DateTimeResult result, int month, int day, int year)
1527 return (SetDateYMD(ref result, year, month, day));
1530 private static bool SetDateDMY(ref DateTimeResult result, int day, int month, int year)
1532 return (SetDateYMD(ref result, year, month, day));
1535 private static bool SetDateYDM(ref DateTimeResult result, int year, int day, int month)
1537 return (SetDateYMD(ref result, year, month, day));
1540 private static void GetDefaultYear(ref DateTimeResult result, ref DateTimeStyles styles)
1542 result.Year = result.calendar.GetYear(GetDateTimeNow(ref result, ref styles));
1543 result.flags |= ParseFlags.YearDefault;
1546 // Processing teriminal case: DS.DX_NN
1547 private static Boolean GetDayOfNN(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1549 if ((result.flags & ParseFlags.HaveDate) != 0)
1551 // Multiple dates in the input string
1552 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1556 int n1 = raw.GetNumber(0);
1557 int n2 = raw.GetNumber(1);
1559 GetDefaultYear(ref result, ref styles);
1562 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out order))
1564 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1568 if (order == ORDER_MD)
1570 if (SetDateYMD(ref result, result.Year, n1, n2)) // MD
1572 result.flags |= ParseFlags.HaveDate;
1579 if (SetDateYMD(ref result, result.Year, n2, n1)) // DM
1581 result.flags |= ParseFlags.HaveDate;
1585 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1589 // Processing teriminal case: DS.DX_NNN
1590 private static Boolean GetDayOfNNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1592 if ((result.flags & ParseFlags.HaveDate) != 0)
1594 // Multiple dates in the input string
1595 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1599 int n1 = raw.GetNumber(0);
1600 int n2 = raw.GetNumber(1); ;
1601 int n3 = raw.GetNumber(2);
1604 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
1606 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1611 if (order == ORDER_YMD)
1613 if (TryAdjustYear(ref result, n1, out year) && SetDateYMD(ref result, year, n2, n3)) // YMD
1615 result.flags |= ParseFlags.HaveDate;
1619 else if (order == ORDER_MDY)
1621 if (TryAdjustYear(ref result, n3, out year) && SetDateMDY(ref result, n1, n2, year)) // MDY
1623 result.flags |= ParseFlags.HaveDate;
1627 else if (order == ORDER_DMY)
1629 if (TryAdjustYear(ref result, n3, out year) && SetDateDMY(ref result, n1, n2, year)) // DMY
1631 result.flags |= ParseFlags.HaveDate;
1635 else if (order == ORDER_YDM)
1637 if (TryAdjustYear(ref result, n1, out year) && SetDateYDM(ref result, year, n2, n3)) // YDM
1639 result.flags |= ParseFlags.HaveDate;
1643 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1647 private static Boolean GetDayOfMN(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1649 if ((result.flags & ParseFlags.HaveDate) != 0)
1651 // Multiple dates in the input string
1652 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1656 // The interpretation is based on the MonthDayPattern and YearMonthPattern
1658 // MonthDayPattern YearMonthPattern Interpretation
1659 // --------------- ---------------- ---------------
1660 // MMMM dd MMMM yyyy Day
1661 // MMMM dd yyyy MMMM Day
1662 // dd MMMM MMMM yyyy Year
1663 // dd MMMM yyyy MMMM Day
1665 // In the first and last cases, it could be either or neither, but a day is a better default interpretation
1666 // than a 2 digit year.
1669 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder))
1671 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1674 if (monthDayOrder == ORDER_DM)
1677 if (!GetYearMonthOrder(dtfi.YearMonthPattern, dtfi, out yearMonthOrder))
1679 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.YearMonthPattern);
1682 if (yearMonthOrder == ORDER_MY)
1685 if (!TryAdjustYear(ref result, raw.GetNumber(0), out year) || !SetDateYMD(ref result, year, raw.month, 1))
1687 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1694 GetDefaultYear(ref result, ref styles);
1695 if (!SetDateYMD(ref result, result.Year, raw.month, raw.GetNumber(0)))
1697 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1703 ////////////////////////////////////////////////////////////////////////
1705 // Deal with the terminal state for Hebrew Month/Day pattern
1707 ////////////////////////////////////////////////////////////////////////
1709 private static Boolean GetHebrewDayOfNM(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1712 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder))
1714 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1717 result.Month = raw.month;
1718 if (monthDayOrder == ORDER_DM || monthDayOrder == ORDER_MD)
1720 if (result.calendar.IsValidDay(result.Year, result.Month, raw.GetNumber(0), result.era))
1722 result.Day = raw.GetNumber(0);
1726 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1730 private static Boolean GetDayOfNM(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1732 if ((result.flags & ParseFlags.HaveDate) != 0)
1734 // Multiple dates in the input string
1735 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1739 // The interpretation is based on the MonthDayPattern and YearMonthPattern
1741 // MonthDayPattern YearMonthPattern Interpretation
1742 // --------------- ---------------- ---------------
1743 // MMMM dd MMMM yyyy Day
1744 // MMMM dd yyyy MMMM Year
1745 // dd MMMM MMMM yyyy Day
1746 // dd MMMM yyyy MMMM Day
1748 // In the first and last cases, it could be either or neither, but a day is a better default interpretation
1749 // than a 2 digit year.
1752 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder))
1754 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1757 if (monthDayOrder == ORDER_MD)
1760 if (!GetYearMonthOrder(dtfi.YearMonthPattern, dtfi, out yearMonthOrder))
1762 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.YearMonthPattern);
1765 if (yearMonthOrder == ORDER_YM)
1768 if (!TryAdjustYear(ref result, raw.GetNumber(0), out year) || !SetDateYMD(ref result, year, raw.month, 1))
1770 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1777 GetDefaultYear(ref result, ref styles);
1778 if (!SetDateYMD(ref result, result.Year, raw.month, raw.GetNumber(0)))
1780 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1786 private static Boolean GetDayOfMNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1788 if ((result.flags & ParseFlags.HaveDate) != 0)
1790 // Multiple dates in the input string
1791 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1795 int n1 = raw.GetNumber(0);
1796 int n2 = raw.GetNumber(1);
1799 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
1801 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1806 if (order == ORDER_MDY)
1808 if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1810 result.SetDate(year, raw.month, n1); // MDY
1811 result.flags |= ParseFlags.HaveDate;
1814 else if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1816 result.SetDate(year, raw.month, n2); // YMD
1817 result.flags |= ParseFlags.HaveDate;
1821 else if (order == ORDER_YMD)
1823 if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1825 result.SetDate(year, raw.month, n2); // YMD
1826 result.flags |= ParseFlags.HaveDate;
1829 else if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1831 result.SetDate(year, raw.month, n1); // DMY
1832 result.flags |= ParseFlags.HaveDate;
1836 else if (order == ORDER_DMY)
1838 if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1840 result.SetDate(year, raw.month, n1); // DMY
1841 result.flags |= ParseFlags.HaveDate;
1844 else if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1846 result.SetDate(year, raw.month, n2); // YMD
1847 result.flags |= ParseFlags.HaveDate;
1852 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1856 private static Boolean GetDayOfYNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1858 if ((result.flags & ParseFlags.HaveDate) != 0)
1860 // Multiple dates in the input string
1861 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1865 int n1 = raw.GetNumber(0);
1866 int n2 = raw.GetNumber(1);
1867 String pattern = dtfi.ShortDatePattern;
1869 // For compatibility, don't throw if we can't determine the order, but default to YMD instead
1871 if (GetYearMonthDayOrder(pattern, dtfi, out order) && order == ORDER_YDM)
1873 if (SetDateYMD(ref result, raw.year, n2, n1))
1875 result.flags |= ParseFlags.HaveDate;
1876 return true; // Year + DM
1881 if (SetDateYMD(ref result, raw.year, n1, n2))
1883 result.flags |= ParseFlags.HaveDate;
1884 return true; // Year + MD
1887 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1891 private static Boolean GetDayOfNNY(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1893 if ((result.flags & ParseFlags.HaveDate) != 0)
1895 // Multiple dates in the input string
1896 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1900 int n1 = raw.GetNumber(0);
1901 int n2 = raw.GetNumber(1);
1904 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
1906 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1910 if (order == ORDER_MDY || order == ORDER_YMD)
1912 if (SetDateYMD(ref result, raw.year, n1, n2))
1914 result.flags |= ParseFlags.HaveDate;
1915 return true; // MD + Year
1920 if (SetDateYMD(ref result, raw.year, n2, n1))
1922 result.flags |= ParseFlags.HaveDate;
1923 return true; // DM + Year
1926 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1931 private static Boolean GetDayOfYMN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1933 if ((result.flags & ParseFlags.HaveDate) != 0)
1935 // Multiple dates in the input string
1936 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1940 if (SetDateYMD(ref result, raw.year, raw.month, raw.GetNumber(0)))
1942 result.flags |= ParseFlags.HaveDate;
1945 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1949 private static Boolean GetDayOfYN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1951 if ((result.flags & ParseFlags.HaveDate) != 0)
1953 // Multiple dates in the input string
1954 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1958 if (SetDateYMD(ref result, raw.year, raw.GetNumber(0), 1))
1960 result.flags |= ParseFlags.HaveDate;
1963 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1967 private static Boolean GetDayOfYM(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1969 if ((result.flags & ParseFlags.HaveDate) != 0)
1971 // Multiple dates in the input string
1972 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1976 if (SetDateYMD(ref result, raw.year, raw.month, 1))
1978 result.flags |= ParseFlags.HaveDate;
1981 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1985 private static void AdjustTimeMark(DateTimeFormatInfo dtfi, ref DateTimeRawInfo raw)
1987 // Specail case for culture which uses AM as empty string.
1988 // E.g. af-ZA (0x0436)
1991 // In this case, if we are parsing a string like "2005/09/14 12:23", we will assume this is in AM.
1993 if (raw.timeMark == TM.NotSet)
1995 if (dtfi.AMDesignator != null && dtfi.PMDesignator != null)
1997 if (dtfi.AMDesignator.Length == 0 && dtfi.PMDesignator.Length != 0)
1999 raw.timeMark = TM.AM;
2001 if (dtfi.PMDesignator.Length == 0 && dtfi.AMDesignator.Length != 0)
2003 raw.timeMark = TM.PM;
2010 // Adjust hour according to the time mark.
2012 private static Boolean AdjustHour(ref int hour, TM timeMark)
2014 if (timeMark != TM.NotSet)
2016 if (timeMark == TM.AM)
2018 if (hour < 0 || hour > 12)
2022 hour = (hour == 12) ? 0 : hour;
2026 if (hour < 0 || hour > 23)
2039 private static Boolean GetTimeOfN(DateTimeFormatInfo dtfi, ref DateTimeResult result, ref DateTimeRawInfo raw)
2041 if ((result.flags & ParseFlags.HaveTime) != 0)
2043 // Multiple times in the input string
2044 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2048 // In this case, we need a time mark. Check if so.
2050 if (raw.timeMark == TM.NotSet)
2052 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2055 result.Hour = raw.GetNumber(0);
2056 result.flags |= ParseFlags.HaveTime;
2060 private static Boolean GetTimeOfNN(DateTimeFormatInfo dtfi, ref DateTimeResult result, ref DateTimeRawInfo raw)
2062 Debug.Assert(raw.numCount >= 2, "raw.numCount >= 2");
2063 if ((result.flags & ParseFlags.HaveTime) != 0)
2065 // Multiple times in the input string
2066 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2070 result.Hour = raw.GetNumber(0);
2071 result.Minute = raw.GetNumber(1);
2072 result.flags |= ParseFlags.HaveTime;
2076 private static Boolean GetTimeOfNNN(DateTimeFormatInfo dtfi, ref DateTimeResult result, ref DateTimeRawInfo raw)
2078 if ((result.flags & ParseFlags.HaveTime) != 0)
2080 // Multiple times in the input string
2081 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2084 Debug.Assert(raw.numCount >= 3, "raw.numCount >= 3");
2085 result.Hour = raw.GetNumber(0);
2086 result.Minute = raw.GetNumber(1);
2087 result.Second = raw.GetNumber(2);
2088 result.flags |= ParseFlags.HaveTime;
2093 // Processing terminal state: A Date suffix followed by one number.
2095 private static Boolean GetDateOfDSN(ref DateTimeResult result, ref DateTimeRawInfo raw)
2097 if (raw.numCount != 1 || result.Day != -1)
2099 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2102 result.Day = raw.GetNumber(0);
2106 private static Boolean GetDateOfNDS(ref DateTimeResult result, ref DateTimeRawInfo raw)
2108 if (result.Month == -1)
2110 //Should have a month suffix
2111 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2114 if (result.Year != -1)
2116 // Aleady has a year suffix
2117 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2120 if (!TryAdjustYear(ref result, raw.GetNumber(0), out result.Year))
2122 // the year value is out of range
2123 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2130 private static Boolean GetDateOfNNDS(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2132 // For partial CJK Dates, the only valid formats are with a specified year, followed by two numbers, which
2133 // will be the Month and Day, and with a specified Month, when the numbers are either the year and day or
2134 // day and year, depending on the short date pattern.
2136 if ((result.flags & ParseFlags.HaveYear) != 0)
2138 if (((result.flags & ParseFlags.HaveMonth) == 0) && ((result.flags & ParseFlags.HaveDay) == 0))
2140 if (TryAdjustYear(ref result, raw.year, out result.Year) && SetDateYMD(ref result, result.Year, raw.GetNumber(0), raw.GetNumber(1)))
2146 else if ((result.flags & ParseFlags.HaveMonth) != 0)
2148 if (((result.flags & ParseFlags.HaveYear) == 0) && ((result.flags & ParseFlags.HaveDay) == 0))
2151 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
2153 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
2157 if (order == ORDER_YMD)
2159 if (TryAdjustYear(ref result, raw.GetNumber(0), out year) && SetDateYMD(ref result, year, result.Month, raw.GetNumber(1)))
2166 if (TryAdjustYear(ref result, raw.GetNumber(1), out year) && SetDateYMD(ref result, year, result.Month, raw.GetNumber(0)))
2173 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2178 // A date suffix is found, use this method to put the number into the result.
2180 private static bool ProcessDateTimeSuffix(ref DateTimeResult result, ref DateTimeRawInfo raw, ref DateTimeToken dtok)
2182 switch (dtok.suffix)
2184 case TokenType.SEP_YearSuff:
2185 if ((result.flags & ParseFlags.HaveYear) != 0)
2189 result.flags |= ParseFlags.HaveYear;
2190 result.Year = raw.year = dtok.num;
2192 case TokenType.SEP_MonthSuff:
2193 if ((result.flags & ParseFlags.HaveMonth) != 0)
2197 result.flags |= ParseFlags.HaveMonth;
2198 result.Month = raw.month = dtok.num;
2200 case TokenType.SEP_DaySuff:
2201 if ((result.flags & ParseFlags.HaveDay) != 0)
2205 result.flags |= ParseFlags.HaveDay;
2206 result.Day = dtok.num;
2208 case TokenType.SEP_HourSuff:
2209 if ((result.flags & ParseFlags.HaveHour) != 0)
2213 result.flags |= ParseFlags.HaveHour;
2214 result.Hour = dtok.num;
2216 case TokenType.SEP_MinuteSuff:
2217 if ((result.flags & ParseFlags.HaveMinute) != 0)
2221 result.flags |= ParseFlags.HaveMinute;
2222 result.Minute = dtok.num;
2224 case TokenType.SEP_SecondSuff:
2225 if ((result.flags & ParseFlags.HaveSecond) != 0)
2229 result.flags |= ParseFlags.HaveSecond;
2230 result.Second = dtok.num;
2236 ////////////////////////////////////////////////////////////////////////
2239 // This is used by DateTime.Parse().
2240 // Process the terminal state for the Hebrew calendar parsing.
2242 ////////////////////////////////////////////////////////////////////////
2244 internal static Boolean ProcessHebrewTerminalState(DS dps, ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2246 // The following are accepted terminal state for Hebrew date.
2250 // Deal with the default long/short date format when the year number is ambigous (i.e. year < 100).
2251 raw.year = raw.GetNumber(1);
2252 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2254 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2257 if (!GetDayOfMNN(ref result, ref raw, dtfi))
2263 // Deal with the default long/short date format when the year number is NOT ambigous (i.e. year >= 100).
2264 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2266 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2269 if (!GetDayOfYMN(ref result, ref raw, dtfi))
2275 // When formatting, we only format up to the hundred digit of the Hebrew year, although Hebrew year is now over 5000.
2276 // E.g. if the year is 5763, we only format as 763. so we do the reverse when parsing.
2277 if (raw.year < 1000)
2281 if (!GetDayOfNNY(ref result, ref raw, dtfi))
2285 if (!dtfi.YearMonthAdjustment(ref result.Year, ref raw.month, true))
2287 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2293 // Deal with Month/Day pattern.
2294 GetDefaultYear(ref result, ref styles);
2295 if (!dtfi.YearMonthAdjustment(ref result.Year, ref raw.month, true))
2297 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2300 if (!GetHebrewDayOfNM(ref result, ref raw, dtfi))
2306 // Deal with Year/Month pattern.
2307 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2309 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2312 if (!GetDayOfYM(ref result, ref raw, dtfi))
2318 // Deal hour + AM/PM
2319 if (!GetTimeOfN(dtfi, ref result, ref raw))
2325 if (!GetTimeOfNN(dtfi, ref result, ref raw))
2331 if (!GetTimeOfNNN(dtfi, ref result, ref raw))
2337 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2343 // We have reached a terminal state. Reset the raw num count.
2351 // A terminal state has been reached, call the appropriate function to fill in the parsing result.
2352 // Return true if the state is a terminal state.
2354 internal static Boolean ProcessTerminaltState(DS dps, ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2360 passed = GetDayOfNN(ref result, ref styles, ref raw, dtfi);
2363 passed = GetDayOfNNN(ref result, ref raw, dtfi);
2366 passed = GetDayOfMN(ref result, ref styles, ref raw, dtfi);
2369 passed = GetDayOfNM(ref result, ref styles, ref raw, dtfi);
2372 passed = GetDayOfMNN(ref result, ref raw, dtfi);
2375 // The result has got the correct value. No need to process.
2379 passed = GetDayOfYNN(ref result, ref raw, dtfi);
2382 passed = GetDayOfNNY(ref result, ref raw, dtfi);
2385 passed = GetDayOfYMN(ref result, ref raw, dtfi);
2388 passed = GetDayOfYN(ref result, ref raw, dtfi);
2391 passed = GetDayOfYM(ref result, ref raw, dtfi);
2394 passed = GetTimeOfN(dtfi, ref result, ref raw);
2397 passed = GetTimeOfNN(dtfi, ref result, ref raw);
2400 passed = GetTimeOfNNN(dtfi, ref result, ref raw);
2403 // The result has got the correct value. Nothing to do.
2407 passed = GetDateOfDSN(ref result, ref raw);
2410 passed = GetDateOfNDS(ref result, ref raw);
2413 passed = GetDateOfNNDS(ref result, ref raw, dtfi);
2417 PTSTraceExit(dps, passed);
2426 // We have reached a terminal state. Reset the raw num count.
2433 internal static DateTime Parse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles)
2435 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
2437 if (TryParse(s, dtfi, styles, ref result))
2439 return result.parsedDate;
2443 throw GetDateTimeParseException(ref result);
2447 internal static DateTime Parse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out TimeSpan offset)
2449 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
2451 result.flags |= ParseFlags.CaptureOffset;
2452 if (TryParse(s, dtfi, styles, ref result))
2454 offset = result.timeZoneOffset;
2455 return result.parsedDate;
2459 throw GetDateTimeParseException(ref result);
2464 internal static bool TryParse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out DateTime result)
2466 result = DateTime.MinValue;
2467 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
2469 if (TryParse(s, dtfi, styles, ref resultData))
2471 result = resultData.parsedDate;
2477 internal static bool TryParse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out DateTime result, out TimeSpan offset)
2479 result = DateTime.MinValue;
2480 offset = TimeSpan.Zero;
2481 DateTimeResult parseResult = new DateTimeResult(); // The buffer to store the parsing result.
2483 parseResult.flags |= ParseFlags.CaptureOffset;
2484 if (TryParse(s, dtfi, styles, ref parseResult))
2486 result = parseResult.parsedDate;
2487 offset = parseResult.timeZoneOffset;
2495 // This is the real method to do the parsing work.
2497 internal static bool TryParse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, ref DateTimeResult result)
2501 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2505 Debug.Assert(dtfi != null, "dtfi == null");
2513 // First try the predefined format.
2516 DS dps = DS.BEGIN; // Date Parsing State.
2517 bool reachTerminalState = false;
2519 DateTimeToken dtok = new DateTimeToken(); // The buffer to store the parsing token.
2520 dtok.suffix = TokenType.SEP_Unk;
2521 DateTimeRawInfo raw = new DateTimeRawInfo(); // The buffer to store temporary parsing information.
2524 Int32* numberPointer = stackalloc Int32[3];
2525 raw.Init(numberPointer);
2527 raw.hasSameDateAndTimeSeparators = dtfi.DateSeparator.Equals(dtfi.TimeSeparator, StringComparison.Ordinal);
2529 result.calendar = dtfi.Calendar;
2530 result.era = Calendar.CurrentEra;
2533 // The string to be parsed. Use a __DTString wrapper so that we can trace the index which
2534 // indicates the begining of next token.
2536 __DTString str = new __DTString(s, dtfi);
2541 // The following loop will break out when we reach the end of the str.
2546 // Call the lexer to get the next token.
2548 // If we find a era in Lex(), the era value will be in raw.era.
2549 if (!Lex(dps, ref str, ref dtok, ref raw, ref result, ref dtfi, styles))
2551 TPTraceExit("0000", dps);
2556 // If the token is not unknown, process it.
2557 // Otherwise, just discard it.
2559 if (dtok.dtt != DTT.Unk)
2562 // Check if we got any CJK Date/Time suffix.
2563 // Since the Date/Time suffix tells us the number belongs to year/month/day/hour/minute/second,
2564 // store the number in the appropriate field in the result.
2566 if (dtok.suffix != TokenType.SEP_Unk)
2568 if (!ProcessDateTimeSuffix(ref result, ref raw, ref dtok))
2570 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2571 TPTraceExit("0010", dps);
2575 dtok.suffix = TokenType.SEP_Unk; // Reset suffix to SEP_Unk;
2578 if (dtok.dtt == DTT.NumLocalTimeMark)
2580 if (dps == DS.D_YNd || dps == DS.D_YN)
2582 // Consider this as ISO 8601 format:
2583 // "yyyy-MM-dd'T'HH:mm:ss" 1999-10-31T02:00:00
2584 TPTraceExit("0020", dps);
2585 return (ParseISO8601(ref raw, ref str, styles, ref result));
2589 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2590 TPTraceExit("0030", dps);
2595 if (raw.hasSameDateAndTimeSeparators)
2597 if (dtok.dtt == DTT.YearEnd || dtok.dtt == DTT.YearSpace || dtok.dtt == DTT.YearDateSep)
2599 // When time and date separators are same and we are hitting a year number while the first parsed part of the string was recognized
2600 // 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
2605 if (dps == DS.T_NNt)
2611 bool atEnd = str.AtEnd();
2612 if (dateParsingStates[(int)dps][(int)dtok.dtt] == DS.ERROR || atEnd)
2616 // we have the case of Serbia have dates in forms 'd.M.yyyy.' so we can expect '.' after the date parts.
2617 // changing the token to end with space instead of Date Separator will avoid failing the parsing.
2619 case DTT.YearDateSep: dtok.dtt = atEnd ? DTT.YearEnd : DTT.YearSpace; break;
2620 case DTT.NumDatesep: dtok.dtt = atEnd ? DTT.NumEnd : DTT.NumSpace; break;
2621 case DTT.NumTimesep: dtok.dtt = atEnd ? DTT.NumEnd : DTT.NumSpace; break;
2622 case DTT.MonthDatesep: dtok.dtt = atEnd ? DTT.MonthEnd : DTT.MonthSpace; break;
2628 // Advance to the next state, and continue
2630 dps = dateParsingStates[(int)dps][(int)dtok.dtt];
2632 if (dps == DS.ERROR)
2634 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2635 TPTraceExit("0040 (invalid state transition)", dps);
2638 else if (dps > DS.ERROR)
2640 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseHebrewRule) != 0)
2642 if (!ProcessHebrewTerminalState(dps, ref result, ref styles, ref raw, dtfi))
2644 TPTraceExit("0050 (ProcessHebrewTerminalState)", dps);
2650 if (!ProcessTerminaltState(dps, ref result, ref styles, ref raw, dtfi))
2652 TPTraceExit("0060 (ProcessTerminaltState)", dps);
2656 reachTerminalState = true;
2659 // If we have reached a terminal state, start over from DS.BEGIN again.
2660 // For example, when we parsed "1999-12-23 13:30", we will reach a terminal state at "1999-12-23",
2661 // and we start over so we can continue to parse "12:30".
2666 } while (dtok.dtt != DTT.End && dtok.dtt != DTT.NumEnd && dtok.dtt != DTT.MonthEnd);
2668 if (!reachTerminalState)
2670 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2671 TPTraceExit("0070 (did not reach terminal state)", dps);
2675 AdjustTimeMark(dtfi, ref raw);
2676 if (!AdjustHour(ref result.Hour, raw.timeMark))
2678 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2679 TPTraceExit("0080 (AdjustHour)", dps);
2683 // Check if the parased string only contains hour/minute/second values.
2684 bool bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1);
2687 // Check if any year/month/day is missing in the parsing string.
2688 // If yes, get the default value from today's date.
2690 if (!CheckDefaultDateTime(ref result, ref result.calendar, styles))
2692 TPTraceExit("0090 (failed to fill in missing year/month/day defaults)", dps);
2696 if (!result.calendar.TryToDateTime(result.Year, result.Month, result.Day,
2697 result.Hour, result.Minute, result.Second, 0, result.era, out time))
2699 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2700 TPTraceExit("0100 (result.calendar.TryToDateTime)", dps);
2703 if (raw.fraction > 0)
2705 time = time.AddTicks((long)Math.Round(raw.fraction * Calendar.TicksPerSecond));
2709 // We have to check day of week before we adjust to the time zone.
2710 // Otherwise, the value of day of week may change after adjustting to the time zone.
2712 if (raw.dayOfWeek != -1)
2715 // Check if day of week is correct.
2717 if (raw.dayOfWeek != (int)result.calendar.GetDayOfWeek(time))
2719 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDayOfWeek), null);
2720 TPTraceExit("0110 (dayOfWeek check)", dps);
2725 result.parsedDate = time;
2727 if (!DetermineTimeZoneAdjustments(ref result, styles, bTimeOnly))
2729 TPTraceExit("0120 (DetermineTimeZoneAdjustments)", dps);
2732 TPTraceExit("0130 (success)", dps);
2737 // Handles time zone adjustments and sets DateTimeKind values as required by the styles
2738 private static Boolean DetermineTimeZoneAdjustments(ref DateTimeResult result, DateTimeStyles styles, Boolean bTimeOnly)
2740 if ((result.flags & ParseFlags.CaptureOffset) != 0)
2742 // This is a DateTimeOffset parse, so the offset will actually be captured directly, and
2743 // no adjustment is required in most cases
2744 return DateTimeOffsetTimeZonePostProcessing(ref result, styles);
2748 Int64 offsetTicks = result.timeZoneOffset.Ticks;
2750 // the DateTime offset must be within +- 14:00 hours.
2751 if (offsetTicks < DateTimeOffset.MinOffset || offsetTicks > DateTimeOffset.MaxOffset)
2753 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_OffsetOutOfRange), null);
2758 // The flags AssumeUniveral and AssumeLocal only apply when the input does not have a time zone
2759 if ((result.flags & ParseFlags.TimeZoneUsed) == 0)
2761 // If AssumeLocal or AssumeLocal is used, there will always be a kind specified. As in the
2762 // case when a time zone is present, it will default to being local unless AdjustToUniversal
2763 // is present. These comparisons determine whether setting the kind is sufficient, or if a
2764 // time zone adjustment is required. For consistentcy with the rest of parsing, it is desirable
2765 // to fall through to the Adjust methods below, so that there is consist handling of boundary
2766 // cases like wrapping around on time-only dates and temporarily allowing an adjusted date
2767 // to exceed DateTime.MaxValue
2768 if ((styles & DateTimeStyles.AssumeLocal) != 0)
2770 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2772 result.flags |= ParseFlags.TimeZoneUsed;
2773 result.timeZoneOffset = TimeZoneInfo.GetLocalUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime);
2777 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Local);
2781 else if ((styles & DateTimeStyles.AssumeUniversal) != 0)
2783 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2785 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Utc);
2790 result.flags |= ParseFlags.TimeZoneUsed;
2791 result.timeZoneOffset = TimeSpan.Zero;
2796 // No time zone and no Assume flags, so DateTimeKind.Unspecified is fine
2797 Debug.Assert(result.parsedDate.Kind == DateTimeKind.Unspecified, "result.parsedDate.Kind == DateTimeKind.Unspecified");
2802 if (((styles & DateTimeStyles.RoundtripKind) != 0) && ((result.flags & ParseFlags.TimeZoneUtc) != 0))
2804 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Utc);
2808 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2810 return (AdjustTimeZoneToUniversal(ref result));
2812 return (AdjustTimeZoneToLocal(ref result, bTimeOnly));
2815 // Apply validation and adjustments specific to DateTimeOffset
2816 private static Boolean DateTimeOffsetTimeZonePostProcessing(ref DateTimeResult result, DateTimeStyles styles)
2818 // For DateTimeOffset, default to the Utc or Local offset when an offset was not specified by
2819 // the input string.
2820 if ((result.flags & ParseFlags.TimeZoneUsed) == 0)
2822 if ((styles & DateTimeStyles.AssumeUniversal) != 0)
2824 // AssumeUniversal causes the offset to default to zero (0)
2825 result.timeZoneOffset = TimeSpan.Zero;
2829 // AssumeLocal causes the offset to default to Local. This flag is on by default for DateTimeOffset.
2830 result.timeZoneOffset = TimeZoneInfo.GetLocalUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime);
2834 Int64 offsetTicks = result.timeZoneOffset.Ticks;
2836 // there should be no overflow, because the offset can be no more than -+100 hours and the date already
2837 // fits within a DateTime.
2838 Int64 utcTicks = result.parsedDate.Ticks - offsetTicks;
2840 // For DateTimeOffset, both the parsed time and the corresponding UTC value must be within the boundaries
2841 // of a DateTime instance.
2842 if (utcTicks < DateTime.MinTicks || utcTicks > DateTime.MaxTicks)
2844 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_UTCOutOfRange), null);
2848 // the offset must be within +- 14:00 hours.
2849 if (offsetTicks < DateTimeOffset.MinOffset || offsetTicks > DateTimeOffset.MaxOffset)
2851 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_OffsetOutOfRange), null);
2855 // DateTimeOffset should still honor the AdjustToUniversal flag for consistency with DateTime. It means you
2856 // want to return an adjusted UTC value, so store the utcTicks in the DateTime and set the offset to zero
2857 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2859 if (((result.flags & ParseFlags.TimeZoneUsed) == 0) && ((styles & DateTimeStyles.AssumeUniversal) == 0))
2861 // Handle the special case where the timeZoneOffset was defaulted to Local
2862 Boolean toUtcResult = AdjustTimeZoneToUniversal(ref result);
2863 result.timeZoneOffset = TimeSpan.Zero;
2867 // The constructor should always succeed because of the range check earlier in the function
2868 // Althought it is UTC, internally DateTimeOffset does not use this flag
2869 result.parsedDate = new DateTime(utcTicks, DateTimeKind.Utc);
2870 result.timeZoneOffset = TimeSpan.Zero;
2878 // Adjust the specified time to universal time based on the supplied timezone.
2879 // E.g. when parsing "2001/06/08 14:00-07:00",
2880 // the time is 2001/06/08 14:00, and timeZoneOffset = -07:00.
2881 // The result will be "2001/06/08 21:00"
2883 private static Boolean AdjustTimeZoneToUniversal(ref DateTimeResult result)
2885 long resultTicks = result.parsedDate.Ticks;
2886 resultTicks -= result.timeZoneOffset.Ticks;
2887 if (resultTicks < 0)
2889 resultTicks += Calendar.TicksPerDay;
2892 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2894 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_DateOutOfRange), null);
2897 result.parsedDate = new DateTime(resultTicks, DateTimeKind.Utc);
2902 // Adjust the specified time to universal time based on the supplied timezone,
2903 // and then convert to local time.
2904 // E.g. when parsing "2001/06/08 14:00-04:00", and local timezone is GMT-7.
2905 // the time is 2001/06/08 14:00, and timeZoneOffset = -05:00.
2906 // The result will be "2001/06/08 11:00"
2908 private static Boolean AdjustTimeZoneToLocal(ref DateTimeResult result, bool bTimeOnly)
2910 long resultTicks = result.parsedDate.Ticks;
2911 // Convert to local ticks
2912 TimeZoneInfo tz = TimeZoneInfo.Local;
2913 Boolean isAmbiguousLocalDst = false;
2914 if (resultTicks < Calendar.TicksPerDay)
2917 // This is time of day.
2921 resultTicks -= result.timeZoneOffset.Ticks;
2922 // If the time is time of day, use the current timezone offset.
2923 resultTicks += tz.GetUtcOffset(bTimeOnly ? DateTime.Now : result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime).Ticks;
2925 if (resultTicks < 0)
2927 resultTicks += Calendar.TicksPerDay;
2932 // Adjust timezone to GMT.
2933 resultTicks -= result.timeZoneOffset.Ticks;
2934 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2936 // If the result ticks is greater than DateTime.MaxValue, we can not create a DateTime from this ticks.
2937 // In this case, keep using the old code.
2938 resultTicks += tz.GetUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime).Ticks;
2942 // Convert the GMT time to local time.
2943 DateTime utcDt = new DateTime(resultTicks, DateTimeKind.Utc);
2944 Boolean isDaylightSavings = false;
2945 resultTicks += TimeZoneInfo.GetUtcOffsetFromUtc(utcDt, TimeZoneInfo.Local, out isDaylightSavings, out isAmbiguousLocalDst).Ticks;
2948 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2950 result.parsedDate = DateTime.MinValue;
2951 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_DateOutOfRange), null);
2954 result.parsedDate = new DateTime(resultTicks, DateTimeKind.Local, isAmbiguousLocalDst);
2959 // Parse the ISO8601 format string found during Parse();
2962 private static bool ParseISO8601(ref DateTimeRawInfo raw, ref __DTString str, DateTimeStyles styles, ref DateTimeResult result)
2964 if (raw.year < 0 || raw.GetNumber(0) < 0 || raw.GetNumber(1) < 0)
2970 double partSecond = 0;
2972 str.SkipWhiteSpaces();
2973 if (!ParseDigits(ref str, 2, out hour))
2975 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2978 str.SkipWhiteSpaces();
2979 if (!str.Match(':'))
2981 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2984 str.SkipWhiteSpaces();
2985 if (!ParseDigits(ref str, 2, out minute))
2987 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2990 str.SkipWhiteSpaces();
2993 str.SkipWhiteSpaces();
2994 if (!ParseDigits(ref str, 2, out second))
2996 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3001 if (!ParseFraction(ref str, out partSecond))
3003 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3008 str.SkipWhiteSpaces();
3012 char ch = str.GetChar();
3013 if (ch == '+' || ch == '-')
3015 result.flags |= ParseFlags.TimeZoneUsed;
3016 if (!ParseTimeZone(ref str, ref result.timeZoneOffset))
3018 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3022 else if (ch == 'Z' || ch == 'z')
3024 result.flags |= ParseFlags.TimeZoneUsed;
3025 result.timeZoneOffset = TimeSpan.Zero;
3026 result.flags |= ParseFlags.TimeZoneUtc;
3032 str.SkipWhiteSpaces();
3035 if (!VerifyValidPunctuation(ref str))
3037 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3040 str.SkipWhiteSpaces();
3042 if (str.Match('\0'))
3044 if (!VerifyValidPunctuation(ref str))
3046 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3052 // If this is true, there were non-white space characters remaining in the DateTime
3053 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3059 Calendar calendar = GregorianCalendar.GetDefaultInstance();
3060 if (!calendar.TryToDateTime(raw.year, raw.GetNumber(0), raw.GetNumber(1),
3061 hour, minute, second, 0, result.era, out time))
3063 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
3067 time = time.AddTicks((long)Math.Round(partSecond * Calendar.TicksPerSecond));
3068 result.parsedDate = time;
3069 if (!DetermineTimeZoneAdjustments(ref result, styles, false))
3077 ////////////////////////////////////////////////////////////////////////
3080 // Parse the current word as a Hebrew number.
3081 // This is used by DateTime.ParseExact().
3083 ////////////////////////////////////////////////////////////////////////
3085 internal static bool MatchHebrewDigits(ref __DTString str, int digitLen, out int number)
3089 // Create a context object so that we can parse the Hebrew number text character by character.
3090 HebrewNumberParsingContext context = new HebrewNumberParsingContext(0);
3092 // Set this to ContinueParsing so that we will run the following while loop in the first time.
3093 HebrewNumberParsingState state = HebrewNumberParsingState.ContinueParsing;
3095 while (state == HebrewNumberParsingState.ContinueParsing && str.GetNext())
3097 state = HebrewNumber.ParseByChar(str.GetChar(), ref context);
3100 if (state == HebrewNumberParsingState.FoundEndOfHebrewNumber)
3102 // If we have reached a terminal state, update the result and returns.
3103 number = context.result;
3107 // If we run out of the character before reaching FoundEndOfHebrewNumber, or
3108 // the state is InvalidHebrewNumber or ContinueParsing, we fail to match a Hebrew number.
3113 /*=================================ParseDigits==================================
3114 **Action: Parse the number string in __DTString that are formatted using
3115 ** the following patterns:
3116 ** "0", "00", and "000..0"
3117 **Returns: the integer value
3118 **Arguments: str: a __DTString. The parsing will start from the
3119 ** next character after str.Index.
3120 **Exceptions: FormatException if error in parsing number.
3121 ==============================================================================*/
3123 internal static bool ParseDigits(ref __DTString str, int digitLen, out int result)
3127 // 1 really means 1 or 2 for this call
3128 return ParseDigits(ref str, 1, 2, out result);
3132 return ParseDigits(ref str, digitLen, digitLen, out result);
3136 internal static bool ParseDigits(ref __DTString str, int minDigitLen, int maxDigitLen, out int result)
3138 Debug.Assert(minDigitLen > 0, "minDigitLen > 0");
3139 Debug.Assert(maxDigitLen < 9, "maxDigitLen < 9");
3140 Debug.Assert(minDigitLen <= maxDigitLen, "minDigitLen <= maxDigitLen");
3141 int localResult = 0;
3142 int startingIndex = str.Index;
3143 int tokenLength = 0;
3144 while (tokenLength < maxDigitLen)
3146 if (!str.GetNextDigit())
3151 localResult = localResult * 10 + str.GetDigit();
3154 result = localResult;
3155 if (tokenLength < minDigitLen)
3157 str.Index = startingIndex;
3163 /*=================================ParseFractionExact==================================
3164 **Action: Parse the number string in __DTString that are formatted using
3165 ** the following patterns:
3166 ** "0", "00", and "000..0"
3167 **Returns: the fraction value
3168 **Arguments: str: a __DTString. The parsing will start from the
3169 ** next character after str.Index.
3170 **Exceptions: FormatException if error in parsing number.
3171 ==============================================================================*/
3173 private static bool ParseFractionExact(ref __DTString str, int maxDigitLen, ref double result)
3175 if (!str.GetNextDigit())
3180 result = str.GetDigit();
3183 for (; digitLen < maxDigitLen; digitLen++)
3185 if (!str.GetNextDigit())
3190 result = result * 10 + str.GetDigit();
3193 result /= TimeSpanParse.Pow10(digitLen);
3194 return (digitLen == maxDigitLen);
3197 /*=================================ParseSign==================================
3198 **Action: Parse a positive or a negative sign.
3199 **Returns: true if postive sign. flase if negative sign.
3200 **Arguments: str: a __DTString. The parsing will start from the
3201 ** next character after str.Index.
3202 **Exceptions: FormatException if end of string is encountered or a sign
3203 ** symbol is not found.
3204 ==============================================================================*/
3206 private static bool ParseSign(ref __DTString str, ref bool result)
3210 // A sign symbol ('+' or '-') is expected. However, end of string is encountered.
3213 char ch = str.GetChar();
3224 // A sign symbol ('+' or '-') is expected.
3228 /*=================================ParseTimeZoneOffset==================================
3229 **Action: Parse the string formatted using "z", "zz", "zzz" in DateTime.Format().
3230 **Returns: the TimeSpan for the parsed timezone offset.
3231 **Arguments: str: a __DTString. The parsing will start from the
3232 ** next character after str.Index.
3233 ** len: the repeated number of the "z"
3234 **Exceptions: FormatException if errors in parsing.
3235 ==============================================================================*/
3237 private static bool ParseTimeZoneOffset(ref __DTString str, int len, ref TimeSpan result)
3239 bool isPositive = true;
3241 int minuteOffset = 0;
3247 if (!ParseSign(ref str, ref isPositive))
3251 if (!ParseDigits(ref str, len, out hourOffset))
3257 if (!ParseSign(ref str, ref isPositive))
3262 // Parsing 1 digit will actually parse 1 or 2.
3263 if (!ParseDigits(ref str, 1, out hourOffset))
3271 if (!ParseDigits(ref str, 2, out minuteOffset))
3278 // Since we can not match ':', put the char back.
3280 if (!ParseDigits(ref str, 2, out minuteOffset))
3287 if (minuteOffset < 0 || minuteOffset >= 60)
3292 result = (new TimeSpan(hourOffset, minuteOffset, 0));
3295 result = result.Negate();
3300 /*=================================MatchAbbreviatedMonthName==================================
3301 **Action: Parse the abbreviated month name from string starting at str.Index.
3302 **Returns: A value from 1 to 12 for the first month to the twelveth month.
3303 **Arguments: str: a __DTString. The parsing will start from the
3304 ** next character after str.Index.
3305 **Exceptions: FormatException if an abbreviated month name can not be found.
3306 ==============================================================================*/
3308 private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3310 int maxMatchStrLen = 0;
3315 // Scan the month names (note that some calendars has 13 months) and find
3316 // the matching month name which has the max string length.
3317 // We need to do this because some cultures (e.g. "cs-CZ") which have
3318 // abbreviated month names with the same prefix.
3320 int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3321 for (int i = 1; i <= monthsInYear; i++)
3323 String searchStr = dtfi.GetAbbreviatedMonthName(i);
3324 int matchStrLen = searchStr.Length;
3325 if (dtfi.HasSpacesInMonthNames
3326 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3327 : str.MatchSpecifiedWord(searchStr))
3329 if (matchStrLen > maxMatchStrLen)
3331 maxMatchStrLen = matchStrLen;
3337 // Search leap year form.
3338 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0)
3340 int tempResult = str.MatchLongestWords(dtfi.internalGetLeapYearMonthNames(), ref maxMatchStrLen);
3341 // We found a longer match in the leap year month name. Use this as the result.
3342 // The result from MatchLongestWords is 0 ~ length of word array.
3343 // So we increment the result by one to become the month value.
3344 if (tempResult >= 0)
3346 result = tempResult + 1;
3352 str.Index += (maxMatchStrLen - 1);
3358 /*=================================MatchMonthName==================================
3359 **Action: Parse the month name from string starting at str.Index.
3360 **Returns: A value from 1 to 12 indicating the first month to the twelveth month.
3361 **Arguments: str: a __DTString. The parsing will start from the
3362 ** next character after str.Index.
3363 **Exceptions: FormatException if a month name can not be found.
3364 ==============================================================================*/
3366 private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3368 int maxMatchStrLen = 0;
3373 // Scan the month names (note that some calendars has 13 months) and find
3374 // the matching month name which has the max string length.
3375 // We need to do this because some cultures (e.g. "vi-VN") which have
3376 // month names with the same prefix.
3378 int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3379 for (int i = 1; i <= monthsInYear; i++)
3381 String searchStr = dtfi.GetMonthName(i);
3382 int matchStrLen = searchStr.Length;
3383 if (dtfi.HasSpacesInMonthNames
3384 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3385 : str.MatchSpecifiedWord(searchStr))
3387 if (matchStrLen > maxMatchStrLen)
3389 maxMatchStrLen = matchStrLen;
3395 // Search genitive form.
3396 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0)
3398 int tempResult = str.MatchLongestWords(dtfi.MonthGenitiveNames, ref maxMatchStrLen);
3399 // We found a longer match in the genitive month name. Use this as the result.
3400 // The result from MatchLongestWords is 0 ~ length of word array.
3401 // So we increment the result by one to become the month value.
3402 if (tempResult >= 0)
3404 result = tempResult + 1;
3408 // Search leap year form.
3409 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0)
3411 int tempResult = str.MatchLongestWords(dtfi.internalGetLeapYearMonthNames(), ref maxMatchStrLen);
3412 // We found a longer match in the leap year month name. Use this as the result.
3413 // The result from MatchLongestWords is 0 ~ length of word array.
3414 // So we increment the result by one to become the month value.
3415 if (tempResult >= 0)
3417 result = tempResult + 1;
3424 str.Index += (maxMatchStrLen - 1);
3430 /*=================================MatchAbbreviatedDayName==================================
3431 **Action: Parse the abbreviated day of week name from string starting at str.Index.
3432 **Returns: A value from 0 to 6 indicating Sunday to Saturday.
3433 **Arguments: str: a __DTString. The parsing will start from the
3434 ** next character after str.Index.
3435 **Exceptions: FormatException if a abbreviated day of week name can not be found.
3436 ==============================================================================*/
3438 private static bool MatchAbbreviatedDayName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3440 int maxMatchStrLen = 0;
3444 for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3446 String searchStr = dtfi.GetAbbreviatedDayName(i);
3447 int matchStrLen = searchStr.Length;
3448 if (dtfi.HasSpacesInDayNames
3449 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3450 : str.MatchSpecifiedWord(searchStr))
3452 if (matchStrLen > maxMatchStrLen)
3454 maxMatchStrLen = matchStrLen;
3462 str.Index += maxMatchStrLen - 1;
3468 /*=================================MatchDayName==================================
3469 **Action: Parse the day of week name from string starting at str.Index.
3470 **Returns: A value from 0 to 6 indicating Sunday to Saturday.
3471 **Arguments: str: a __DTString. The parsing will start from the
3472 ** next character after str.Index.
3473 **Exceptions: FormatException if a day of week name can not be found.
3474 ==============================================================================*/
3476 private static bool MatchDayName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3478 // Turkish (tr-TR) got day names with the same prefix.
3479 int maxMatchStrLen = 0;
3483 for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3485 String searchStr = dtfi.GetDayName(i);
3486 int matchStrLen = searchStr.Length;
3487 if (dtfi.HasSpacesInDayNames
3488 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3489 : str.MatchSpecifiedWord(searchStr))
3491 if (matchStrLen > maxMatchStrLen)
3493 maxMatchStrLen = matchStrLen;
3501 str.Index += maxMatchStrLen - 1;
3507 /*=================================MatchEraName==================================
3508 **Action: Parse era name from string starting at str.Index.
3509 **Returns: An era value.
3510 **Arguments: str: a __DTString. The parsing will start from the
3511 ** next character after str.Index.
3512 **Exceptions: FormatException if an era name can not be found.
3513 ==============================================================================*/
3515 private static bool MatchEraName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3519 int[] eras = dtfi.Calendar.Eras;
3523 for (int i = 0; i < eras.Length; i++)
3525 String searchStr = dtfi.GetEraName(eras[i]);
3526 if (str.MatchSpecifiedWord(searchStr))
3528 str.Index += (searchStr.Length - 1);
3532 searchStr = dtfi.GetAbbreviatedEraName(eras[i]);
3533 if (str.MatchSpecifiedWord(searchStr))
3535 str.Index += (searchStr.Length - 1);
3545 /*=================================MatchTimeMark==================================
3546 **Action: Parse the time mark (AM/PM) from string starting at str.Index.
3547 **Returns: TM_AM or TM_PM.
3548 **Arguments: str: a __DTString. The parsing will start from the
3549 ** next character after str.Index.
3550 **Exceptions: FormatException if a time mark can not be found.
3551 ==============================================================================*/
3553 private static bool MatchTimeMark(ref __DTString str, DateTimeFormatInfo dtfi, ref TM result)
3556 // In some cultures have empty strings in AM/PM mark. E.g. af-ZA (0x0436), the AM mark is "", and PM mark is "nm".
3557 if (dtfi.AMDesignator.Length == 0)
3561 if (dtfi.PMDesignator.Length == 0)
3568 String searchStr = dtfi.AMDesignator;
3569 if (searchStr.Length > 0)
3571 if (str.MatchSpecifiedWord(searchStr))
3573 // Found an AM timemark with length > 0.
3574 str.Index += (searchStr.Length - 1);
3579 searchStr = dtfi.PMDesignator;
3580 if (searchStr.Length > 0)
3582 if (str.MatchSpecifiedWord(searchStr))
3584 // Found a PM timemark with length > 0.
3585 str.Index += (searchStr.Length - 1);
3590 str.Index--; // Undo the GetNext call.
3592 if (result != TM.NotSet)
3594 // If one of the AM/PM marks is empty string, return the result.
3600 /*=================================MatchAbbreviatedTimeMark==================================
3601 **Action: Parse the abbreviated time mark (AM/PM) from string starting at str.Index.
3602 **Returns: TM_AM or TM_PM.
3603 **Arguments: str: a __DTString. The parsing will start from the
3604 ** next character after str.Index.
3605 **Exceptions: FormatException if a abbreviated time mark can not be found.
3606 ==============================================================================*/
3608 private static bool MatchAbbreviatedTimeMark(ref __DTString str, DateTimeFormatInfo dtfi, ref TM result)
3610 // NOTENOTE : the assumption here is that abbreviated time mark is the first
3611 // character of the AM/PM designator. If this invariant changes, we have to
3612 // change the code below.
3615 if (str.GetChar() == dtfi.AMDesignator[0])
3620 if (str.GetChar() == dtfi.PMDesignator[0])
3629 /*=================================CheckNewValue==================================
3630 **Action: Check if currentValue is initialized. If not, return the newValue.
3631 ** If yes, check if the current value is equal to newValue. Return false
3632 ** if they are not equal. This is used to check the case like "d" and "dd" are both
3633 ** used to format a string.
3634 **Returns: the correct value for currentValue.
3637 ==============================================================================*/
3639 private static bool CheckNewValue(ref int currentValue, int newValue, char patternChar, ref DateTimeResult result)
3641 if (currentValue == -1)
3643 currentValue = newValue;
3648 if (newValue != currentValue)
3650 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), patternChar);
3657 private static DateTime GetDateTimeNow(ref DateTimeResult result, ref DateTimeStyles styles)
3659 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3661 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
3663 // use the supplied offset to calculate 'Now'
3664 return new DateTime(DateTime.UtcNow.Ticks + result.timeZoneOffset.Ticks, DateTimeKind.Unspecified);
3666 else if ((styles & DateTimeStyles.AssumeUniversal) != 0)
3668 // assume the offset is Utc
3669 return DateTime.UtcNow;
3673 // assume the offset is Local
3674 return DateTime.Now;
3677 private static bool CheckDefaultDateTime(ref DateTimeResult result, ref Calendar cal, DateTimeStyles styles)
3679 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3681 // DateTimeOffset.Parse should allow dates without a year, but only if there is also no time zone marker;
3682 // e.g. "May 1 5pm" is OK, but "May 1 5pm -08:30" is not. This is somewhat pragmatic, since we would
3683 // have to rearchitect parsing completely to allow this one case to correctly handle things like leap
3684 // years and leap months. Is is an extremely corner case, and DateTime is basically incorrect in that
3687 // values like "11:00Z" or "11:00 -3:00" are also acceptable
3689 // if ((month or day is set) and (year is not set and time zone is set))
3691 if (((result.Month != -1) || (result.Day != -1))
3692 && ((result.Year == -1 || ((result.flags & ParseFlags.YearDefault) != 0)) && (result.flags & ParseFlags.TimeZoneUsed) != 0))
3694 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_MissingIncompleteDate), null);
3700 if ((result.Year == -1) || (result.Month == -1) || (result.Day == -1))
3703 The following table describes the behaviors of getting the default value
3704 when a certain year/month/day values are missing.
3706 An "X" means that the value exists. And "--" means that value is missing.
3708 Year Month Day => ResultYear ResultMonth ResultDay Note
3710 X X X Parsed year Parsed month Parsed day
3711 X X -- Parsed Year Parsed month First day If we have year and month, assume the first day of that month.
3712 X -- X Parsed year First month Parsed day If the month is missing, assume first month of that year.
3713 X -- -- Parsed year First month First day If we have only the year, assume the first day of that year.
3715 -- X X CurrentYear Parsed month Parsed day If the year is missing, assume the current year.
3716 -- X -- CurrentYear Parsed month First day If we have only a month value, assume the current year and current day.
3717 -- -- X CurrentYear First month Parsed day If we have only a day value, assume current year and first month.
3718 -- -- -- CurrentYear Current month Current day So this means that if the date string only contains time, you will get current date.
3722 DateTime now = GetDateTimeNow(ref result, ref styles);
3723 if (result.Month == -1 && result.Day == -1)
3725 if (result.Year == -1)
3727 if ((styles & DateTimeStyles.NoCurrentDateDefault) != 0)
3729 // If there is no year/month/day values, and NoCurrentDateDefault flag is used,
3730 // set the year/month/day value to the beginning year/month/day of DateTime().
3731 // Note we should be using Gregorian for the year/month/day.
3732 cal = GregorianCalendar.GetDefaultInstance();
3733 result.Year = result.Month = result.Day = 1;
3737 // Year/Month/Day are all missing.
3738 result.Year = cal.GetYear(now);
3739 result.Month = cal.GetMonth(now);
3740 result.Day = cal.GetDayOfMonth(now);
3745 // Month/Day are both missing.
3752 if (result.Year == -1)
3754 result.Year = cal.GetYear(now);
3756 if (result.Month == -1)
3760 if (result.Day == -1)
3766 // Set Hour/Minute/Second to zero if these value are not in str.
3767 if (result.Hour == -1) result.Hour = 0;
3768 if (result.Minute == -1) result.Minute = 0;
3769 if (result.Second == -1) result.Second = 0;
3770 if (result.era == -1) result.era = Calendar.CurrentEra;
3774 // Expand a pre-defined format string (like "D" for long date) to the real format that
3775 // we are going to use in the date time parsing.
3776 // This method also set the dtfi according/parseInfo to some special pre-defined
3779 private static String ExpandPredefinedFormat(String format, ref DateTimeFormatInfo dtfi, ref ParsingInfo parseInfo, ref DateTimeResult result)
3782 // Check the format to see if we need to override the dtfi to be InvariantInfo,
3783 // and see if we need to set up the userUniversalTime flag.
3788 case 'O': // Round Trip Format
3789 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3790 dtfi = DateTimeFormatInfo.InvariantInfo;
3793 case 'R': // RFC 1123 Standard. (in Universal time)
3794 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3795 dtfi = DateTimeFormatInfo.InvariantInfo;
3797 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3799 result.flags |= ParseFlags.Rfc1123Pattern;
3802 case 's': // Sortable format (in local time)
3803 dtfi = DateTimeFormatInfo.InvariantInfo;
3804 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3806 case 'u': // Universal time format in sortable format.
3807 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3808 dtfi = DateTimeFormatInfo.InvariantInfo;
3810 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3812 result.flags |= ParseFlags.UtcSortPattern;
3815 case 'U': // Universal time format with culture-dependent format.
3816 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3817 result.flags |= ParseFlags.TimeZoneUsed;
3818 result.timeZoneOffset = new TimeSpan(0);
3819 result.flags |= ParseFlags.TimeZoneUtc;
3820 if (dtfi.Calendar.GetType() != typeof(GregorianCalendar))
3822 dtfi = (DateTimeFormatInfo)dtfi.Clone();
3823 dtfi.Calendar = GregorianCalendar.GetDefaultInstance();
3829 // Expand the pre-defined format character to the real format from DateTimeFormatInfo.
3831 return (DateTimeFormat.GetRealFormat(format, dtfi));
3838 // Given a specified format character, parse and update the parsing result.
3840 private static bool ParseByFormat(
3842 ref __DTString format,
3843 ref ParsingInfo parseInfo,
3844 DateTimeFormatInfo dtfi,
3845 ref DateTimeResult result)
3848 int tempYear = 0, tempMonth = 0, tempDay = 0, tempDayOfWeek = 0, tempHour = 0, tempMinute = 0, tempSecond = 0;
3849 double tempFraction = 0;
3850 TM tempTimeMark = 0;
3852 char ch = format.GetChar();
3857 tokenLen = format.GetRepeatCount();
3859 if (dtfi.HasForceTwoDigitYears)
3861 parseResult = ParseDigits(ref str, 1, 4, out tempYear);
3867 parseInfo.fUseTwoDigitYear = true;
3869 parseResult = ParseDigits(ref str, tokenLen, out tempYear);
3871 if (!parseResult && parseInfo.fCustomNumberParser)
3873 parseResult = parseInfo.parseNumberDelegate(ref str, tokenLen, out tempYear);
3877 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3880 if (!CheckNewValue(ref result.Year, tempYear, ch, ref result))
3886 tokenLen = format.GetRepeatCount();
3889 if (!ParseDigits(ref str, tokenLen, out tempMonth))
3891 if (!parseInfo.fCustomNumberParser ||
3892 !parseInfo.parseNumberDelegate(ref str, tokenLen, out tempMonth))
3894 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3903 if (!MatchAbbreviatedMonthName(ref str, dtfi, ref tempMonth))
3905 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3911 if (!MatchMonthName(ref str, dtfi, ref tempMonth))
3913 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3917 result.flags |= ParseFlags.ParsedMonthName;
3919 if (!CheckNewValue(ref result.Month, tempMonth, ch, ref result))
3925 // Day & Day of week
3926 tokenLen = format.GetRepeatCount();
3931 if (!ParseDigits(ref str, tokenLen, out tempDay))
3933 if (!parseInfo.fCustomNumberParser ||
3934 !parseInfo.parseNumberDelegate(ref str, tokenLen, out tempDay))
3936 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3940 if (!CheckNewValue(ref result.Day, tempDay, ch, ref result))
3950 if (!MatchAbbreviatedDayName(ref str, dtfi, ref tempDayOfWeek))
3952 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3959 if (!MatchDayName(ref str, dtfi, ref tempDayOfWeek))
3961 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3965 if (!CheckNewValue(ref parseInfo.dayOfWeek, tempDayOfWeek, ch, ref result))
3972 tokenLen = format.GetRepeatCount();
3973 // Put the era value in result.era.
3974 if (!MatchEraName(ref str, dtfi, ref result.era))
3976 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3981 parseInfo.fUseHour12 = true;
3982 tokenLen = format.GetRepeatCount();
3983 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempHour))
3985 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3988 if (!CheckNewValue(ref result.Hour, tempHour, ch, ref result))
3994 tokenLen = format.GetRepeatCount();
3995 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempHour))
3997 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4000 if (!CheckNewValue(ref result.Hour, tempHour, ch, ref result))
4006 tokenLen = format.GetRepeatCount();
4007 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempMinute))
4009 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4012 if (!CheckNewValue(ref result.Minute, tempMinute, ch, ref result))
4018 tokenLen = format.GetRepeatCount();
4019 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempSecond))
4021 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4024 if (!CheckNewValue(ref result.Second, tempSecond, ch, ref result))
4031 tokenLen = format.GetRepeatCount();
4032 if (tokenLen <= DateTimeFormat.MaxSecondsFractionDigits)
4034 if (!ParseFractionExact(ref str, tokenLen, ref tempFraction))
4038 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4042 if (result.fraction < 0)
4044 result.fraction = tempFraction;
4048 if (tempFraction != result.fraction)
4050 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), ch);
4057 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4063 tokenLen = format.GetRepeatCount();
4066 if (!MatchAbbreviatedTimeMark(ref str, dtfi, ref tempTimeMark))
4068 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4074 if (!MatchTimeMark(ref str, dtfi, ref tempTimeMark))
4076 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4081 if (parseInfo.timeMark == TM.NotSet)
4083 parseInfo.timeMark = tempTimeMark;
4087 if (parseInfo.timeMark != tempTimeMark)
4089 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), ch);
4096 tokenLen = format.GetRepeatCount();
4098 TimeSpan tempTimeZoneOffset = new TimeSpan(0);
4099 if (!ParseTimeZoneOffset(ref str, tokenLen, ref tempTimeZoneOffset))
4101 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4104 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && tempTimeZoneOffset != result.timeZoneOffset)
4106 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'z');
4109 result.timeZoneOffset = tempTimeZoneOffset;
4110 result.flags |= ParseFlags.TimeZoneUsed;
4114 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && result.timeZoneOffset != TimeSpan.Zero)
4116 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'Z');
4120 result.flags |= ParseFlags.TimeZoneUsed;
4121 result.timeZoneOffset = new TimeSpan(0);
4122 result.flags |= ParseFlags.TimeZoneUtc;
4124 // The updating of the indexes is to reflect that ParseExact MatchXXX methods assume that
4125 // they need to increment the index and Parse GetXXX do not. Since we are calling a Parse
4126 // method from inside ParseExact we need to adjust this. Long term, we should try to
4127 // eliminate this discrepancy.
4129 if (!GetTimeZoneName(ref str))
4131 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4137 // This should parse either as a blank, the 'Z' character or a local offset like "-07:00"
4140 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && result.timeZoneOffset != TimeSpan.Zero)
4142 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'K');
4146 result.flags |= ParseFlags.TimeZoneUsed;
4147 result.timeZoneOffset = new TimeSpan(0);
4148 result.flags |= ParseFlags.TimeZoneUtc;
4150 else if (str.Match('+') || str.Match('-'))
4152 str.Index--; // Put the character back for the parser
4153 TimeSpan tempTimeZoneOffset = new TimeSpan(0);
4154 if (!ParseTimeZoneOffset(ref str, 3, ref tempTimeZoneOffset))
4156 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4159 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && tempTimeZoneOffset != result.timeZoneOffset)
4161 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'K');
4164 result.timeZoneOffset = tempTimeZoneOffset;
4165 result.flags |= ParseFlags.TimeZoneUsed;
4167 // Otherwise it is unspecified and we consume no characters
4170 // 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
4171 // We have to exclude the case when the time separator is more than one character and starts with ':' something like "::" for instance.
4172 if (((dtfi.TimeSeparator.Length > 1 && dtfi.TimeSeparator[0] == ':') || !str.Match(':')) &&
4173 !str.Match(dtfi.TimeSeparator))
4175 // A time separator is expected.
4176 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4181 // 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
4182 // We have to exclude the case when the date separator is more than one character and starts with '/' something like "//" for instance.
4183 if (((dtfi.DateSeparator.Length > 1 && dtfi.DateSeparator[0] == '/') || !str.Match('/')) &&
4184 !str.Match(dtfi.DateSeparator))
4186 // A date separator is expected.
4187 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4193 StringBuilder enquotedString = StringBuilderCache.Acquire();
4194 // Use ParseQuoteString so that we can handle escape characters within the quoted string.
4195 if (!TryParseQuoteString(format.Value, format.Index, enquotedString, out tokenLen))
4197 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadQuote), ch);
4198 StringBuilderCache.Release(enquotedString);
4201 format.Index += tokenLen - 1;
4203 // Some cultures uses space in the quoted string. E.g. Spanish has long date format as:
4204 // "dddd, dd' de 'MMMM' de 'yyyy". When inner spaces flag is set, we should skip whitespaces if there is space
4205 // in the quoted string.
4206 String quotedStr = StringBuilderCache.GetStringAndRelease(enquotedString);
4208 for (int i = 0; i < quotedStr.Length; i++)
4210 if (quotedStr[i] == ' ' && parseInfo.fAllowInnerWhite)
4212 str.SkipWhiteSpaces();
4214 else if (!str.Match(quotedStr[i]))
4216 // Can not find the matching quoted string.
4217 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4222 // The "r" and "u" formats incorrectly quoted 'GMT' and 'Z', respectively. We cannot
4223 // correct this mistake for DateTime.ParseExact for compatibility reasons, but we can
4224 // fix it for DateTimeOffset.ParseExact as DateTimeOffset has not been publically released
4226 if ((result.flags & ParseFlags.CaptureOffset) != 0)
4228 if ((result.flags & ParseFlags.Rfc1123Pattern) != 0 && quotedStr == GMTName)
4230 result.flags |= ParseFlags.TimeZoneUsed;
4231 result.timeZoneOffset = TimeSpan.Zero;
4233 else if ((result.flags & ParseFlags.UtcSortPattern) != 0 && quotedStr == ZuluName)
4235 result.flags |= ParseFlags.TimeZoneUsed;
4236 result.timeZoneOffset = TimeSpan.Zero;
4242 // Skip this so we can get to the next pattern character.
4243 // Used in case like "%d", "%y"
4245 // Make sure the next character is not a '%' again.
4246 if (format.Index >= format.Value.Length - 1 || format.Value[format.Index + 1] == '%')
4248 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
4253 // Escape character. For example, "\d".
4254 // Get the next character in format, and see if we can
4255 // find a match in str.
4256 if (format.GetNext())
4258 if (!str.Match(format.GetChar()))
4260 // Can not find a match for the escaped character.
4261 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4267 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
4274 if (format.GetNext())
4276 // If we encounter the pattern ".F", and the dot is not present, it is an optional
4277 // second fraction and we can skip this format.
4278 if (format.Match('F'))
4280 format.GetRepeatCount();
4284 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4291 if (parseInfo.fAllowInnerWhite)
4293 // Skip whitespaces if AllowInnerWhite.
4300 // If the space does not match, and trailing space is allowed, we do
4301 // one more step to see if the next format character can lead to
4302 // successful parsing.
4303 // This is used to deal with special case that a empty string can match
4304 // a specific pattern.
4305 // The example here is af-ZA, which has a time format like "hh:mm:ss tt". However,
4306 // its AM symbol is "" (empty string). If fAllowTrailingWhite is used, and time is in
4307 // the AM, we will trim the whitespaces at the end, which will lead to a failure
4308 // when we are trying to match the space before "tt".
4309 if (parseInfo.fAllowTrailingWhite)
4311 if (format.GetNext())
4313 if (ParseByFormat(ref str, ref format, ref parseInfo, dtfi, ref result))
4319 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4327 if (format.MatchSpecifiedWord(GMTName))
4329 format.Index += (GMTName.Length - 1);
4330 // Found GMT string in format. This means the DateTime string
4331 // is in GMT timezone.
4332 result.flags |= ParseFlags.TimeZoneUsed;
4333 result.timeZoneOffset = TimeSpan.Zero;
4334 if (!str.Match(GMTName))
4336 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4340 else if (!str.Match(ch))
4343 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4353 // The pos should point to a quote character. This method will
4354 // get the string enclosed by the quote character.
4356 internal static bool TryParseQuoteString(ReadOnlySpan<char> format, int pos, StringBuilder result, out int returnValue)
4359 // NOTE : pos will be the index of the quote character in the 'format' string.
4362 int formatLen = format.Length;
4364 char quoteChar = format[pos++]; // Get the character used to quote the following string.
4366 bool foundQuote = false;
4367 while (pos < formatLen)
4369 char ch = format[pos++];
4370 if (ch == quoteChar)
4375 else if (ch == '\\')
4377 // The following are used to support escaped character.
4378 // Escaped character is also supported in the quoted string.
4379 // Therefore, someone can use a format like "'minute:' mm\"" to display:
4381 // because the second double quote is escaped.
4382 if (pos < formatLen)
4384 result.Append(format[pos++]);
4389 // This means that '\' is at the end of the formatting string.
4402 // Here we can't find the matching quote.
4407 // Return the character count including the begin/end quote characters and enclosed string.
4409 returnValue = (pos - beginPos);
4416 /*=================================DoStrictParse==================================
4417 **Action: Do DateTime parsing using the format in formatParam.
4418 **Returns: The parsed DateTime.
4423 ** When the following general formats are used, InvariantInfo is used in dtfi:
4425 ** When the following general formats are used, the time is assumed to be in Universal time.
4428 ** Only GregarianCalendar is supported for now.
4429 ** Only support GMT timezone.
4430 ==============================================================================*/
4432 private static bool DoStrictParse(
4433 ReadOnlySpan<char> s,
4435 DateTimeStyles styles,
4436 DateTimeFormatInfo dtfi,
4437 ref DateTimeResult result)
4439 ParsingInfo parseInfo = new ParsingInfo();
4442 parseInfo.calendar = dtfi.Calendar;
4443 parseInfo.fAllowInnerWhite = ((styles & DateTimeStyles.AllowInnerWhite) != 0);
4444 parseInfo.fAllowTrailingWhite = ((styles & DateTimeStyles.AllowTrailingWhite) != 0);
4446 // We need the original values of the following two below.
4447 String originalFormat = formatParam;
4449 if (formatParam.Length == 1)
4451 if (((result.flags & ParseFlags.CaptureOffset) != 0) && formatParam[0] == 'U')
4453 // The 'U' format is not allowed for DateTimeOffset
4454 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
4457 formatParam = ExpandPredefinedFormat(formatParam, ref dtfi, ref parseInfo, ref result);
4460 bool bTimeOnly = false;
4461 result.calendar = parseInfo.calendar;
4463 if (parseInfo.calendar.ID == CalendarId.HEBREW)
4465 parseInfo.parseNumberDelegate = m_hebrewNumberParser;
4466 parseInfo.fCustomNumberParser = true;
4469 // Reset these values to negative one so that we could throw exception
4470 // if we have parsed every item twice.
4471 result.Hour = result.Minute = result.Second = -1;
4473 __DTString format = new __DTString(formatParam.AsReadOnlySpan(), dtfi, false);
4474 __DTString str = new __DTString(s, dtfi, false);
4476 if (parseInfo.fAllowTrailingWhite)
4478 // Trim trailing spaces if AllowTrailingWhite.
4480 format.RemoveTrailingInQuoteSpaces();
4484 if ((styles & DateTimeStyles.AllowLeadingWhite) != 0)
4486 format.SkipWhiteSpaces();
4487 format.RemoveLeadingInQuoteSpaces();
4488 str.SkipWhiteSpaces();
4492 // Scan every character in format and match the pattern in str.
4494 while (format.GetNext())
4496 // We trim inner spaces here, so that we will not eat trailing spaces when
4497 // AllowTrailingWhite is not used.
4498 if (parseInfo.fAllowInnerWhite)
4500 str.SkipWhiteSpaces();
4502 if (!ParseByFormat(ref str, ref format, ref parseInfo, dtfi, ref result))
4508 if (str.Index < str.Value.Length - 1)
4510 // There are still remaining character in str.
4511 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4515 if (parseInfo.fUseTwoDigitYear && ((dtfi.FormatFlags & DateTimeFormatFlags.UseHebrewRule) == 0))
4517 // A two digit year value is expected. Check if the parsed year value is valid.
4518 if (result.Year >= 100)
4520 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4525 result.Year = parseInfo.calendar.ToFourDigitYear(result.Year);
4527 catch (ArgumentOutOfRangeException e)
4529 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), e);
4534 if (parseInfo.fUseHour12)
4536 if (parseInfo.timeMark == TM.NotSet)
4538 // hh is used, but no AM/PM designator is specified.
4539 // Assume the time is AM.
4540 // Don't throw exceptions in here becasue it is very confusing for the caller.
4541 // I always got confused myself when I use "hh:mm:ss" to parse a time string,
4542 // and ParseExact() throws on me (because I didn't use the 24-hour clock 'HH').
4543 parseInfo.timeMark = TM.AM;
4545 if (result.Hour > 12)
4547 // AM/PM is used, but the value for HH is too big.
4548 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4551 if (parseInfo.timeMark == TM.AM)
4553 if (result.Hour == 12)
4560 result.Hour = (result.Hour == 12) ? 12 : result.Hour + 12;
4565 // Military (24-hour time) mode
4567 // AM cannot be set with a 24-hour time like 17:15.
4568 // PM cannot be set with a 24-hour time like 03:15.
4569 if ((parseInfo.timeMark == TM.AM && result.Hour >= 12)
4570 || (parseInfo.timeMark == TM.PM && result.Hour < 12))
4572 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4578 // Check if the parased string only contains hour/minute/second values.
4579 bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1);
4580 if (!CheckDefaultDateTime(ref result, ref parseInfo.calendar, styles))
4585 if (!bTimeOnly && dtfi.HasYearMonthAdjustment)
4587 if (!dtfi.YearMonthAdjustment(ref result.Year, ref result.Month, ((result.flags & ParseFlags.ParsedMonthName) != 0)))
4589 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
4593 if (!parseInfo.calendar.TryToDateTime(result.Year, result.Month, result.Day,
4594 result.Hour, result.Minute, result.Second, 0, result.era, out result.parsedDate))
4596 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
4599 if (result.fraction > 0)
4601 result.parsedDate = result.parsedDate.AddTicks((long)Math.Round(result.fraction * Calendar.TicksPerSecond));
4605 // We have to check day of week before we adjust to the time zone.
4606 // It is because the value of day of week may change after adjusting
4607 // to the time zone.
4609 if (parseInfo.dayOfWeek != -1)
4612 // Check if day of week is correct.
4614 if (parseInfo.dayOfWeek != (int)parseInfo.calendar.GetDayOfWeek(result.parsedDate))
4616 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDayOfWeek), null);
4622 if (!DetermineTimeZoneAdjustments(ref result, styles, bTimeOnly))
4629 private static Exception GetDateTimeParseException(ref DateTimeResult result)
4631 switch (result.failure)
4633 case ParseFailureKind.ArgumentNull:
4634 return new ArgumentNullException(result.failureArgumentName, SR.GetResourceString(result.failureMessageID));
4635 case ParseFailureKind.Format:
4636 return new FormatException(SR.GetResourceString(result.failureMessageID));
4637 case ParseFailureKind.FormatWithParameter:
4638 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), result.failureMessageFormatArgument));
4639 case ParseFailureKind.FormatBadDateTimeCalendar:
4640 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), result.calendar));
4642 Debug.Fail("Unkown DateTimeParseFailure: " + result);
4647 [Conditional("_LOGGING")]
4648 private static void LexTraceExit(string message, DS dps)
4651 if (!_tracingEnabled)
4653 Trace($"Lex return {message}, DS.{dps}");
4656 [Conditional("_LOGGING")]
4657 private static void PTSTraceExit(DS dps, bool passed)
4660 if (!_tracingEnabled)
4662 Trace($"ProcessTerminalState {(passed ? "passed" : "failed")} @ DS.{dps}");
4665 [Conditional("_LOGGING")]
4666 private static void TPTraceExit(string message, DS dps)
4669 if (!_tracingEnabled)
4671 Trace($"TryParse return {message}, DS.{dps}");
4674 [Conditional("_LOGGING")]
4675 private static void DTFITrace(DateTimeFormatInfo dtfi)
4678 if (!_tracingEnabled)
4681 Trace("DateTimeFormatInfo Properties");
4682 #if !FEATURE_COREFX_GLOBALIZATION
4683 Trace($" NativeCalendarName {Hex(dtfi.NativeCalendarName)}");
4685 Trace($" AMDesignator {Hex(dtfi.AMDesignator)}");
4686 Trace($" PMDesignator {Hex(dtfi.PMDesignator)}");
4687 Trace($" TimeSeparator {Hex(dtfi.TimeSeparator)}");
4688 Trace($" AbbrvDayNames {Hex(dtfi.AbbreviatedDayNames)}");
4689 Trace($" ShortestDayNames {Hex(dtfi.ShortestDayNames)}");
4690 Trace($" DayNames {Hex(dtfi.DayNames)}");
4691 Trace($" AbbrvMonthNames {Hex(dtfi.AbbreviatedMonthNames)}");
4692 Trace($" MonthNames {Hex(dtfi.MonthNames)}");
4693 Trace($" AbbrvMonthGenNames {Hex(dtfi.AbbreviatedMonthGenitiveNames)}");
4694 Trace($" MonthGenNames {Hex(dtfi.MonthGenitiveNames)}");
4698 // return a string in the form: "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
4699 private static string Hex(string[] strs)
4701 if (strs == null || strs.Length == 0)
4702 return String.Empty;
4703 if (strs.Length == 1)
4704 return Hex(strs[0]);
4706 int curLineLength = 0;
4707 int maxLineLength = 55;
4708 int newLinePadding = 20;
4711 //invariant: strs.Length >= 2
4712 StringBuilder buffer = new StringBuilder();
4713 buffer.Append(Hex(strs[0]));
4714 curLineLength = buffer.Length;
4717 for (int i = 1; i < strs.Length - 1; i++)
4721 if (s.Length > maxLineLength || (curLineLength + s.Length + 2) > maxLineLength)
4724 buffer.Append(Environment.NewLine);
4725 buffer.Append(' ', newLinePadding);
4730 buffer.Append(", ");
4734 curLineLength += s.Length;
4738 s = Hex(strs[strs.Length - 1]);
4739 if (s.Length > maxLineLength || (curLineLength + s.Length + 6) > maxLineLength)
4741 buffer.Append(Environment.NewLine);
4742 buffer.Append(' ', newLinePadding);
4749 return buffer.ToString();
4751 // return a string in the form: "Sun"
4752 private static string Hex(string str) => Hex(str.AsReadOnlySpan());
4753 private static string Hex(ReadOnlySpan<char> str)
4755 StringBuilder buffer = new StringBuilder();
4756 buffer.Append("\"");
4757 for (int i = 0; i < str.Length; i++)
4759 if (str[i] <= '\x007f')
4760 buffer.Append(str[i]);
4762 buffer.Append("\\u" + ((int)str[i]).ToString("x4", CultureInfo.InvariantCulture));
4764 buffer.Append("\"");
4765 return buffer.ToString();
4767 // return an unicode escaped string form of char c
4768 private static String Hex(char c)
4771 return c.ToString(CultureInfo.InvariantCulture);
4773 return "\\u" + ((int)c).ToString("x4", CultureInfo.InvariantCulture);
4776 private static void Trace(string s)
4778 // Internal.Console.WriteLine(s);
4781 private static bool _tracingEnabled = false;
4787 // This is a string parsing helper which wraps a String object.
4788 // It has a Index property which tracks
4789 // the current parsing pointer of the string.
4791 internal ref struct __DTString
4794 // Value propery: stores the real string to be parsed.
4796 internal ReadOnlySpan<char> Value;
4799 // Index property: points to the character that we are currently parsing.
4803 // The length of Value string.
4804 internal int Length => Value.Length;
4806 // The current chracter to be looked at.
4807 internal char m_current;
4809 private CompareInfo m_info;
4810 // Flag to indicate if we encouter an digit, we should check for token or not.
4811 // In some cultures, such as mn-MN, it uses "\x0031\x00a0\x0434\x04af\x0433\x044d\x044d\x0440\x00a0\x0441\x0430\x0440" in month names.
4812 private bool m_checkDigitToken;
4814 internal __DTString(ReadOnlySpan<char> str, DateTimeFormatInfo dtfi, bool checkDigitToken) : this(str, dtfi)
4816 m_checkDigitToken = checkDigitToken;
4819 internal __DTString(ReadOnlySpan<char> str, DateTimeFormatInfo dtfi)
4827 m_info = dtfi.CompareInfo;
4828 m_checkDigitToken = ((dtfi.FormatFlags & DateTimeFormatFlags.UseDigitPrefixInTokens) != 0);
4832 m_info = CultureInfo.CurrentCulture.CompareInfo;
4833 m_checkDigitToken = false;
4837 internal CompareInfo CompareInfo
4839 get { return m_info; }
4843 // Advance the Index.
4844 // Return true if Index is NOT at the end of the string.
4847 // while (str.GetNext())
4849 // char ch = str.GetChar()
4851 internal bool GetNext()
4856 m_current = Value[Index];
4862 internal bool AtEnd()
4864 return Index < Length ? false : true;
4867 internal bool Advance(int count)
4869 Debug.Assert(Index + count <= Length, "__DTString::Advance: Index + count <= len");
4873 m_current = Value[Index];
4880 // Used by DateTime.Parse() to get the next token.
4881 internal void GetRegularToken(out TokenType tokenType, out int tokenValue, DateTimeFormatInfo dtfi)
4884 if (Index >= Length)
4886 tokenType = TokenType.EndOfString;
4890 tokenType = TokenType.UnknownToken;
4893 if (DateTimeParse.IsDigit(m_current))
4896 tokenValue = m_current - '0';
4901 // Collect other digits.
4903 while (++Index < Length)
4905 m_current = Value[Index];
4906 value = m_current - '0';
4907 if (value >= 0 && value <= 9)
4909 tokenValue = tokenValue * 10 + value;
4916 if (Index - start > DateTimeParse.MaxDateTimeNumberDigits)
4918 tokenType = TokenType.NumberToken;
4921 else if (Index - start < 3)
4923 tokenType = TokenType.NumberToken;
4927 // If there are more than 3 digits, assume that it's a year value.
4928 tokenType = TokenType.YearNumberToken;
4930 if (m_checkDigitToken)
4933 char saveCh = m_current;
4934 // Re-scan using the staring Index to see if this is a token.
4935 Index = start; // To include the first digit.
4936 m_current = Value[Index];
4939 // This DTFI has tokens starting with digits.
4940 // E.g. mn-MN has month name like "\x0031\x00a0\x0434\x04af\x0433\x044d\x044d\x0440\x00a0\x0441\x0430\x0440"
4941 if (dtfi.Tokenize(TokenType.RegularTokenMask, out tempType, out tempValue, ref this))
4943 tokenType = tempType;
4944 tokenValue = tempValue;
4945 // This is a token, so the Index has been advanced propertly in DTFI.Tokenizer().
4949 // Use the number token value.
4950 // Restore the index.
4956 else if (Char.IsWhiteSpace(m_current))
4958 // Just skip to the next character.
4959 while (++Index < Length)
4961 m_current = Value[Index];
4962 if (!(Char.IsWhiteSpace(m_current)))
4967 // We have reached the end of string.
4968 tokenType = TokenType.EndOfString;
4972 dtfi.Tokenize(TokenType.RegularTokenMask, out tokenType, out tokenValue, ref this);
4976 internal TokenType GetSeparatorToken(DateTimeFormatInfo dtfi, out int indexBeforeSeparator, out char charBeforeSeparator)
4978 indexBeforeSeparator = Index;
4979 charBeforeSeparator = m_current;
4980 TokenType tokenType;
4981 if (!SkipWhiteSpaceCurrent())
4983 // Reach the end of the string.
4984 return (TokenType.SEP_End);
4986 if (!DateTimeParse.IsDigit(m_current))
4988 // Not a digit. Tokenize it.
4990 bool found = dtfi.Tokenize(TokenType.SeparatorTokenMask, out tokenType, out tokenValue, ref this);
4993 tokenType = TokenType.SEP_Space;
4998 // Do nothing here. If we see a number, it will not be a separator. There is no need wasting time trying to find the
5000 tokenType = TokenType.SEP_Space;
5005 [MethodImpl(MethodImplOptions.AggressiveInlining)]
5006 internal bool MatchSpecifiedWord(String target) =>
5007 Index + target.Length <= Length &&
5008 m_info.Compare(Value.Slice(Index, target.Length), target, CompareOptions.IgnoreCase) == 0;
5010 private static readonly Char[] WhiteSpaceChecks = new Char[] { ' ', '\u00A0' };
5012 internal bool MatchSpecifiedWords(String target, bool checkWordBoundary, ref int matchLength)
5014 int valueRemaining = Value.Length - Index;
5015 matchLength = target.Length;
5017 if (matchLength > valueRemaining || m_info.Compare(Value.Slice(Index, matchLength), target, CompareOptions.IgnoreCase) != 0)
5019 // Check word by word
5020 int targetPosition = 0; // Where we are in the target string
5021 int thisPosition = Index; // Where we are in this string
5022 int wsIndex = target.IndexOfAny(WhiteSpaceChecks, targetPosition);
5029 int segmentLength = wsIndex - targetPosition;
5030 if (thisPosition >= Value.Length - segmentLength)
5031 { // Subtraction to prevent overflow.
5034 if (segmentLength == 0)
5036 // If segmentLength == 0, it means that we have leading space in the target string.
5037 // In that case, skip the leading spaces in the target and this string.
5042 // Make sure we also have whitespace in the input string
5043 if (!Char.IsWhiteSpace(Value[thisPosition + segmentLength]))
5047 if (m_info.Compare(Value.Slice(thisPosition, segmentLength), target.AsReadOnlySpan().Slice(targetPosition, segmentLength), CompareOptions.IgnoreCase) != 0)
5051 // Advance the input string
5052 thisPosition = thisPosition + segmentLength + 1;
5054 // Advance our target string
5055 targetPosition = wsIndex + 1;
5058 // Skip past multiple whitespace
5059 while (thisPosition < Value.Length && Char.IsWhiteSpace(Value[thisPosition]))
5064 } while ((wsIndex = target.IndexOfAny(WhiteSpaceChecks, targetPosition)) >= 0);
5065 // now check the last segment;
5066 if (targetPosition < target.Length)
5068 int segmentLength = target.Length - targetPosition;
5069 if (thisPosition > Value.Length - segmentLength)
5073 if (m_info.Compare(Value.Slice(thisPosition, segmentLength), target.AsReadOnlySpan().Slice(targetPosition, segmentLength), CompareOptions.IgnoreCase) != 0)
5080 if (checkWordBoundary)
5082 int nextCharIndex = Index + matchLength;
5083 if (nextCharIndex < Value.Length)
5085 if (Char.IsLetter(Value[nextCharIndex]))
5095 // Check to see if the string starting from Index is a prefix of
5097 // If a match is found, true value is returned and Index is updated to the next character to be parsed.
5098 // Otherwise, Index is unchanged.
5100 internal bool Match(String str)
5102 if (++Index >= Length)
5107 if (str.Length > (Value.Length - Index))
5112 if (m_info.Compare(Value.Slice(Index, str.Length), str, CompareOptions.Ordinal) == 0)
5114 // Update the Index to the end of the matching string.
5115 // So the following GetNext()/Match() opeartion will get
5116 // the next character to be parsed.
5117 Index += (str.Length - 1);
5123 internal bool Match(char ch)
5125 if (++Index >= Length)
5129 if (Value[Index] == ch)
5139 // Actions: From the current position, try matching the longest word in the specified string array.
5140 // E.g. words[] = {"AB", "ABC", "ABCD"}, if the current position points to a substring like "ABC DEF",
5141 // MatchLongestWords(words, ref MaxMatchStrLen) will return 1 (the index), and maxMatchLen will be 3.
5143 // The index that contains the longest word to match
5145 // words The string array that contains words to search.
5146 // maxMatchStrLen [in/out] the initailized maximum length. This parameter can be used to
5147 // find the longest match in two string arrays.
5149 internal int MatchLongestWords(String[] words, ref int maxMatchStrLen)
5152 for (int i = 0; i < words.Length; i++)
5154 String word = words[i];
5155 int matchLength = word.Length;
5156 if (MatchSpecifiedWords(word, false, ref matchLength))
5158 if (matchLength > maxMatchStrLen)
5160 maxMatchStrLen = matchLength;
5170 // Get the number of repeat character after the current character.
5171 // For a string "hh:mm:ss" at Index of 3. GetRepeatCount() = 2, and Index
5172 // will point to the second ':'.
5174 internal int GetRepeatCount()
5176 char repeatChar = Value[Index];
5177 int pos = Index + 1;
5178 while ((pos < Length) && (Value[pos] == repeatChar))
5182 int repeatCount = (pos - Index);
5183 // Update the Index to the end of the repeated characters.
5184 // So the following GetNext() opeartion will get
5185 // the next character to be parsed.
5187 return (repeatCount);
5190 // Return false when end of string is encountered or a non-digit character is found.
5191 [MethodImpl(MethodImplOptions.AggressiveInlining)]
5192 internal bool GetNextDigit() =>
5194 DateTimeParse.IsDigit(Value[Index]);
5197 // Get the current character.
5199 internal char GetChar()
5201 Debug.Assert(Index >= 0 && Index < Length, "Index >= 0 && Index < len");
5202 return (Value[Index]);
5206 // Convert the current character to a digit, and return it.
5208 internal int GetDigit()
5210 Debug.Assert(Index >= 0 && Index < Length, "Index >= 0 && Index < len");
5211 Debug.Assert(DateTimeParse.IsDigit(Value[Index]), "IsDigit(Value[Index])");
5212 return (Value[Index] - '0');
5216 // Eat White Space ahead of the current position
5218 // Return false if end of string is encountered.
5220 internal void SkipWhiteSpaces()
5222 // Look ahead to see if the next character
5224 while (Index + 1 < Length)
5226 char ch = Value[Index + 1];
5227 if (!Char.IsWhiteSpace(ch))
5237 // Skip white spaces from the current position
5239 // Return false if end of string is encountered.
5241 internal bool SkipWhiteSpaceCurrent()
5243 if (Index >= Length)
5248 if (!Char.IsWhiteSpace(m_current))
5253 while (++Index < Length)
5255 m_current = Value[Index];
5256 if (!Char.IsWhiteSpace(m_current))
5265 internal void TrimTail()
5268 while (i >= 0 && Char.IsWhiteSpace(Value[i]))
5272 Value = Value.Slice(0, i + 1);
5275 // Trim the trailing spaces within a quoted string.
5276 // Call this after TrimTail() is done.
5277 internal void RemoveTrailingInQuoteSpaces()
5285 // Check if the last character is a quote.
5286 if (ch == '\'' || ch == '\"')
5288 if (Char.IsWhiteSpace(Value[i - 1]))
5291 while (i >= 1 && Char.IsWhiteSpace(Value[i - 1]))
5295 Value = Value.Remove(i, Value.Length - 1 - i);
5300 // Trim the leading spaces within a quoted string.
5301 // Call this after the leading spaces before quoted string are trimmed.
5302 internal void RemoveLeadingInQuoteSpaces()
5310 // Check if the last character is a quote.
5311 if (ch == '\'' || ch == '\"')
5313 while ((i + 1) < Length && Char.IsWhiteSpace(Value[i + 1]))
5319 Value = Value.Remove(1, i);
5324 internal DTSubString GetSubString()
5326 DTSubString sub = new DTSubString();
5329 while (Index + sub.length < Length)
5331 DTSubStringType currentType;
5332 Char ch = Value[Index + sub.length];
5333 if (ch >= '0' && ch <= '9')
5335 currentType = DTSubStringType.Number;
5339 currentType = DTSubStringType.Other;
5342 if (sub.length == 0)
5344 sub.type = currentType;
5348 if (sub.type != currentType)
5354 if (currentType == DTSubStringType.Number)
5356 // Incorporate the number into the value
5357 // Limit the digits to prevent overflow
5358 if (sub.length > DateTimeParse.MaxDateTimeNumberDigits)
5360 sub.type = DTSubStringType.Invalid;
5363 int number = ch - '0';
5364 Debug.Assert(number >= 0 && number <= 9, "number >= 0 && number <= 9");
5365 sub.value = sub.value * 10 + number;
5369 // For non numbers, just return this length 1 token. This should be expanded
5370 // to more types of thing if this parsing approach is used for things other
5371 // than numbers and single characters
5375 if (sub.length == 0)
5377 sub.type = DTSubStringType.End;
5384 internal void ConsumeSubString(DTSubString sub)
5386 Debug.Assert(sub.index == Index, "sub.index == Index");
5387 Debug.Assert(sub.index + sub.length <= Length, "sub.index + sub.length <= len");
5388 Index = sub.index + sub.length;
5391 m_current = Value[Index];
5396 internal enum DTSubStringType
5405 internal ref struct DTSubString
5407 internal ReadOnlySpan<char> s;
5408 internal Int32 index;
5409 internal Int32 length;
5410 internal DTSubStringType type;
5411 internal Int32 value;
5413 internal Char this[Int32 relativeIndex]
5417 return s[index + relativeIndex];
5423 // The buffer to store the parsing token.
5426 struct DateTimeToken
5428 internal DateTimeParse.DTT dtt; // Store the token
5429 internal TokenType suffix; // Store the CJK Year/Month/Day suffix (if any)
5430 internal int num; // Store the number that we are parsing (if any)
5434 // The buffer to store temporary parsing information.
5437 unsafe struct DateTimeRawInfo
5440 internal int numCount;
5443 internal int dayOfWeek;
5445 internal DateTimeParse.TM timeMark;
5446 internal double fraction;
5447 internal bool hasSameDateAndTimeSeparators;
5449 internal void Init(int* numberBuffer)
5455 timeMark = DateTimeParse.TM.NotSet;
5459 internal unsafe void AddNumber(int value)
5461 num[numCount++] = value;
5463 internal unsafe int GetNumber(int index)
5469 internal enum ParseFailureKind
5474 FormatWithParameter = 3,
5475 FormatBadDateTimeCalendar = 4, // FormatException when ArgumentOutOfRange is thrown by a Calendar.TryToDateTime().
5479 internal enum ParseFlags
5481 HaveYear = 0x00000001,
5482 HaveMonth = 0x00000002,
5483 HaveDay = 0x00000004,
5484 HaveHour = 0x00000008,
5485 HaveMinute = 0x00000010,
5486 HaveSecond = 0x00000020,
5487 HaveTime = 0x00000040,
5488 HaveDate = 0x00000080,
5489 TimeZoneUsed = 0x00000100,
5490 TimeZoneUtc = 0x00000200,
5491 ParsedMonthName = 0x00000400,
5492 CaptureOffset = 0x00000800,
5493 YearDefault = 0x00001000,
5494 Rfc1123Pattern = 0x00002000,
5495 UtcSortPattern = 0x00004000,
5499 // This will store the result of the parsing. And it will be eventually
5500 // used to construct a DateTime instance.
5503 struct DateTimeResult
5509 // Set time defualt to 00:00:00.
5512 internal int Minute;
5513 internal int Second;
5514 internal double fraction;
5518 internal ParseFlags flags;
5520 internal TimeSpan timeZoneOffset;
5522 internal Calendar calendar;
5524 internal DateTime parsedDate;
5526 internal ParseFailureKind failure;
5527 internal string failureMessageID;
5528 internal object failureMessageFormatArgument;
5529 internal string failureArgumentName;
5531 internal void Init()
5540 internal void SetDate(int year, int month, int day)
5546 internal void SetFailure(ParseFailureKind failure, string failureMessageID, object failureMessageFormatArgument)
5548 this.failure = failure;
5549 this.failureMessageID = failureMessageID;
5550 this.failureMessageFormatArgument = failureMessageFormatArgument;
5553 internal void SetFailure(ParseFailureKind failure, string failureMessageID, object failureMessageFormatArgument, string failureArgumentName)
5555 this.failure = failure;
5556 this.failureMessageID = failureMessageID;
5557 this.failureMessageFormatArgument = failureMessageFormatArgument;
5558 this.failureArgumentName = failureArgumentName;
5562 // This is the helper data structure used in ParseExact().
5563 internal struct ParsingInfo
5565 internal Calendar calendar;
5566 internal int dayOfWeek;
5567 internal DateTimeParse.TM timeMark;
5569 internal bool fUseHour12;
5570 internal bool fUseTwoDigitYear;
5571 internal bool fAllowInnerWhite;
5572 internal bool fAllowTrailingWhite;
5573 internal bool fCustomNumberParser;
5574 internal DateTimeParse.MatchNumberDelegate parseNumberDelegate;
5576 internal void Init()
5579 timeMark = DateTimeParse.TM.NotSet;
5584 // The type of token that will be returned by DateTimeFormatInfo.Tokenize().
5586 internal enum TokenType
5588 // The valid token should start from 1.
5590 // Regular tokens. The range is from 0x00 ~ 0xff.
5591 NumberToken = 1, // The number. E.g. "12"
5592 YearNumberToken = 2, // The number which is considered as year number, which has 3 or more digits. E.g. "2003"
5593 Am = 3, // AM timemark. E.g. "AM"
5594 Pm = 4, // PM timemark. E.g. "PM"
5595 MonthToken = 5, // A word (or words) that represents a month name. E.g. "March"
5596 EndOfString = 6, // End of string
5597 DayOfWeekToken = 7, // A word (or words) that represents a day of week name. E.g. "Monday" or "Mon"
5598 TimeZoneToken = 8, // A word that represents a timezone name. E.g. "GMT"
5599 EraToken = 9, // A word that represents a era name. E.g. "A.D."
5600 DateWordToken = 10, // A word that can appear in a DateTime string, but serves no parsing semantics. E.g. "de" in Spanish culture.
5601 UnknownToken = 11, // An unknown word, which signals an error in parsing.
5602 HebrewNumber = 12, // A number that is composed of Hebrew text. Hebrew calendar uses Hebrew digits for year values, month values, and day values.
5603 JapaneseEraToken = 13, // Era name for JapaneseCalendar
5604 TEraToken = 14, // Era name for TaiwanCalendar
5605 IgnorableSymbol = 15, // A separator like "," that is equivalent to whitespace
5608 // Separator tokens.
5609 SEP_Unk = 0x100, // Unknown separator.
5610 SEP_End = 0x200, // The end of the parsing string.
5611 SEP_Space = 0x300, // Whitespace (including comma).
5612 SEP_Am = 0x400, // AM timemark. E.g. "AM"
5613 SEP_Pm = 0x500, // PM timemark. E.g. "PM"
5614 SEP_Date = 0x600, // date separator. E.g. "/"
5615 SEP_Time = 0x700, // time separator. E.g. ":"
5616 SEP_YearSuff = 0x800, // Chinese/Japanese/Korean year suffix.
5617 SEP_MonthSuff = 0x900, // Chinese/Japanese/Korean month suffix.
5618 SEP_DaySuff = 0xa00, // Chinese/Japanese/Korean day suffix.
5619 SEP_HourSuff = 0xb00, // Chinese/Japanese/Korean hour suffix.
5620 SEP_MinuteSuff = 0xc00, // Chinese/Japanese/Korean minute suffix.
5621 SEP_SecondSuff = 0xd00, // Chinese/Japanese/Korean second suffix.
5622 SEP_LocalTimeMark = 0xe00, // 'T', used in ISO 8601 format.
5623 SEP_DateOrOffset = 0xf00, // '-' which could be a date separator or start of a time zone offset
5625 RegularTokenMask = 0x00ff,
5626 SeparatorTokenMask = 0xff00,