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, ReadOnlySpan<char> 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, ReadOnlySpan<char> 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, ReadOnlySpan<char> 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, ReadOnlySpan<char> 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, ReadOnlySpan<char> format, DateTimeFormatInfo dtfi, DateTimeStyles style, ref DateTimeResult result)
84 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDateTime));
88 if (format.Length == 0)
90 result.SetBadFormatSpecifierFailure();
94 Debug.Assert(dtfi != null, "dtfi == null");
96 return DoStrictParse(s, format, style, dtfi, ref result);
99 internal static DateTime ParseExactMultiple(ReadOnlySpan<char> s, String[] formats,
100 DateTimeFormatInfo dtfi, DateTimeStyles style)
102 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
104 if (TryParseExactMultiple(s, formats, dtfi, style, ref result))
106 return result.parsedDate;
110 throw GetDateTimeParseException(ref result);
115 internal static DateTime ParseExactMultiple(ReadOnlySpan<char> s, String[] formats,
116 DateTimeFormatInfo dtfi, DateTimeStyles style, out TimeSpan offset)
118 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
119 offset = TimeSpan.Zero;
121 result.flags |= ParseFlags.CaptureOffset;
122 if (TryParseExactMultiple(s, formats, dtfi, style, ref result))
124 offset = result.timeZoneOffset;
125 return result.parsedDate;
129 throw GetDateTimeParseException(ref result);
133 internal static bool TryParseExactMultiple(ReadOnlySpan<char> s, String[] formats,
134 DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result, out TimeSpan offset)
136 result = DateTime.MinValue;
137 offset = TimeSpan.Zero;
138 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
140 resultData.flags |= ParseFlags.CaptureOffset;
141 if (TryParseExactMultiple(s, formats, dtfi, style, ref resultData))
143 result = resultData.parsedDate;
144 offset = resultData.timeZoneOffset;
151 internal static bool TryParseExactMultiple(ReadOnlySpan<char> s, String[] formats,
152 DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result)
154 result = DateTime.MinValue;
155 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
157 if (TryParseExactMultiple(s, formats, dtfi, style, ref resultData))
159 result = resultData.parsedDate;
165 internal static bool TryParseExactMultiple(ReadOnlySpan<char> s, String[] formats,
166 DateTimeFormatInfo dtfi, DateTimeStyles style, ref DateTimeResult result)
170 result.SetFailure(ParseFailureKind.ArgumentNull, nameof(SR.ArgumentNull_String), null, nameof(formats));
176 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDateTime));
180 if (formats.Length == 0)
182 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_NoFormatSpecifier));
186 Debug.Assert(dtfi != null, "dtfi == null");
189 // Do a loop through the provided formats and see if we can parse succesfully in
190 // one of the formats.
192 for (int i = 0; i < formats.Length; i++)
194 if (formats[i] == null || formats[i].Length == 0)
196 result.SetBadFormatSpecifierFailure();
199 // Create a new result each time to ensure the runs are independent. Carry through
200 // flags from the caller and return the result.
201 DateTimeResult innerResult = new DateTimeResult(); // The buffer to store the parsing result.
203 innerResult.flags = result.flags;
204 if (TryParseExact(s, formats[i], dtfi, style, ref innerResult))
206 result.parsedDate = innerResult.parsedDate;
207 result.timeZoneOffset = innerResult.timeZoneOffset;
211 result.SetBadDateTimeFailure();
215 ////////////////////////////////////////////////////////////////////////////
218 // Following is the set of tokens that can be generated from a date
219 // string. Notice that the legal set of trailing separators have been
220 // folded in with the date number, and month name tokens. This set
221 // of tokens is chosen to reduce the number of date parse states.
223 ////////////////////////////////////////////////////////////////////////////
225 internal enum DTT : int
228 NumEnd = 1, // Num[ ]*[\0]
229 NumAmpm = 2, // Num[ ]+AmPm
230 NumSpace = 3, // Num[ ]+^[Dsep|Tsep|'0\']
231 NumDatesep = 4, // Num[ ]*Dsep
232 NumTimesep = 5, // Num[ ]*Tsep
233 MonthEnd = 6, // Month[ ]*'\0'
234 MonthSpace = 7, // Month[ ]+^[Dsep|Tsep|'\0']
235 MonthDatesep = 8, // Month[ ]*Dsep
236 NumDatesuff = 9, // Month[ ]*DSuff
237 NumTimesuff = 10, // Month[ ]*TSuff
238 DayOfWeek = 11, // Day of week name
239 YearSpace = 12, // Year+^[Dsep|Tsep|'0\']
240 YearDateSep = 13, // Year+Dsep
241 YearEnd = 14, // Year+['\0']
242 TimeZone = 15, // timezone name
243 Era = 16, // era name
244 NumUTCTimeMark = 17, // Num + 'Z'
245 // When you add a new token which will be in the
246 // state table, add it after NumLocalTimeMark.
248 NumLocalTimeMark = 19, // Num + 'T'
260 ////////////////////////////////////////////////////////////////////////////
262 // DateTime parsing state enumeration (DS.*)
264 ////////////////////////////////////////////////////////////////////////////
269 N = 1, // have one number
270 NN = 2, // have two numbers
272 // The following are known to be part of a date
274 D_Nd = 3, // date string: have number followed by date separator
275 D_NN = 4, // date string: have two numbers
276 D_NNd = 5, // date string: have two numbers followed by date separator
278 D_M = 6, // date string: have a month
279 D_MN = 7, // date string: have a month and a number
280 D_NM = 8, // date string: have a number and a month
281 D_MNd = 9, // date string: have a month and number followed by date separator
282 D_NDS = 10, // date string: have one number followed a date suffix.
284 D_Y = 11, // date string: have a year.
285 D_YN = 12, // date string: have a year and a number
286 D_YNd = 13, // date string: have a year and a number and a date separator
287 D_YM = 14, // date string: have a year and a month
288 D_YMd = 15, // date string: have a year and a month and a date separator
289 D_S = 16, // have numbers followed by a date suffix.
290 T_S = 17, // have numbers followed by a time suffix.
292 // The following are known to be part of a time
294 T_Nt = 18, // have num followed by time separator
295 T_NNt = 19, // have two numbers followed by time separator
300 // The following are terminal states. These all have an action
301 // associated with them; and transition back to BEGIN.
303 DX_NN = 21, // day from two numbers
304 DX_NNN = 22, // day from three numbers
305 DX_MN = 23, // day from month and one number
306 DX_NM = 24, // day from month and one number
307 DX_MNN = 25, // day from month and two numbers
308 DX_DS = 26, // a set of date suffixed numbers.
309 DX_DSN = 27, // day from date suffixes and one number.
310 DX_NDS = 28, // day from one number and date suffixes .
311 DX_NNDS = 29, // day from one number and date suffixes .
313 DX_YNN = 30, // date string: have a year and two number
314 DX_YMN = 31, // date string: have a year, a month, and a number.
315 DX_YN = 32, // date string: have a year and one number
316 DX_YM = 33, // date string: have a year, a month.
317 TX_N = 34, // time from one number (must have ampm)
318 TX_NN = 35, // time from two numbers
319 TX_NNN = 36, // time from three numbers
320 TX_TS = 37, // a set of time suffixed numbers.
324 ////////////////////////////////////////////////////////////////////////////
326 // NOTE: The following state machine table is dependent on the order of the
327 // DS and DTT enumerations.
329 // For each non terminal state, the following table defines the next state
330 // for each given date token type.
332 ////////////////////////////////////////////////////////////////////////////
334 // End NumEnd NumAmPm NumSpace NumDaySep NumTimesep MonthEnd MonthSpace MonthDSep NumDateSuff NumTimeSuff DayOfWeek YearSpace YearDateSep YearEnd TimeZone Era UTCTimeMark
335 private static DS[][] dateParsingStates = {
336 // DS.BEGIN // DS.BEGIN
337 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},
340 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},
343 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},
345 // DS.D_Nd // DS.D_Nd
346 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},
348 // DS.D_NN // DS.D_NN
349 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},
351 // DS.D_NNd // DS.D_NNd
352 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},
355 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},
357 // DS.D_MN // DS.D_MN
358 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},
360 // DS.D_NM // DS.D_NM
361 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},
363 // DS.D_MNd // DS.D_MNd
364 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},
366 // DS.D_NDS, // DS.D_NDS,
367 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},
370 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},
372 // DS.D_YN // DS.D_YN
373 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},
375 // DS.D_YNd // DS.D_YNd
376 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},
378 // DS.D_YM // DS.D_YM
379 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},
381 // DS.D_YMd // DS.D_YMd
382 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},
385 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},
388 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},
390 // DS.T_Nt // DS.T_Nt
391 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},
393 // DS.T_NNt // DS.T_NNt
394 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},
396 // End NumEnd NumAmPm NumSpace NumDaySep NumTimesep MonthEnd MonthSpace MonthDSep NumDateSuff NumTimeSuff DayOfWeek YearSpace YearDateSep YearEnd TimeZone Era UTCMark
398 internal const String GMTName = "GMT";
399 internal const String ZuluName = "Z";
402 // Search from the index of str at str.Index to see if the target string exists in the str.
404 private static bool MatchWord(ref __DTString str, String target)
406 if (target.Length > (str.Value.Length - str.Index))
411 if (str.CompareInfo.Compare(str.Value.Slice(str.Index, target.Length), target, CompareOptions.IgnoreCase) != 0)
416 int nextCharIndex = str.Index + target.Length;
418 if (nextCharIndex < str.Value.Length)
420 char nextCh = str.Value[nextCharIndex];
421 if (Char.IsLetter(nextCh))
426 str.Index = nextCharIndex;
427 if (str.Index < str.Length)
429 str.m_current = str.Value[str.Index];
437 // Check the word at the current index to see if it matches GMT name or Zulu name.
439 private static bool GetTimeZoneName(ref __DTString str)
441 if (MatchWord(ref str, GMTName))
446 if (MatchWord(ref str, ZuluName))
454 internal static bool IsDigit(char ch) => (uint)(ch - '0') <= 9;
456 /*=================================ParseFraction==========================
457 **Action: Starting at the str.Index, which should be a decimal symbol.
458 ** if the current character is a digit, parse the remaining
459 ** numbers as fraction. For example, if the sub-string starting at str.Index is "123", then
460 ** the method will return 0.123
461 **Returns: The fraction number.
463 ** str the parsing string
465 ============================================================================*/
467 private static bool ParseFraction(ref __DTString str, out double result)
470 double decimalBase = 0.1;
474 && IsDigit(ch = str.m_current))
476 result += (ch - '0') * decimalBase;
483 /*=================================ParseTimeZone==========================
484 **Action: Parse the timezone offset in the following format:
485 ** "+8", "+08", "+0800", "+0800"
486 ** This method is used by DateTime.Parse().
487 **Returns: The TimeZone offset.
489 ** str the parsing string
491 ** FormatException if invalid timezone format is found.
492 ============================================================================*/
494 private static bool ParseTimeZone(ref __DTString str, ref TimeSpan result)
496 // The hour/minute offset for timezone.
498 int minuteOffset = 0;
501 // Consume the +/- character that has already been read
502 sub = str.GetSubString();
507 char offsetChar = sub[0];
508 if (offsetChar != '+' && offsetChar != '-')
512 str.ConsumeSubString(sub);
514 sub = str.GetSubString();
515 if (sub.type != DTSubStringType.Number)
519 int value = sub.value;
520 int length = sub.length;
521 if (length == 1 || length == 2)
523 // Parsing "+8" or "+08"
525 str.ConsumeSubString(sub);
526 // See if we have minutes
527 sub = str.GetSubString();
528 if (sub.length == 1 && sub[0] == ':')
530 // Parsing "+8:00" or "+08:00"
531 str.ConsumeSubString(sub);
532 sub = str.GetSubString();
533 if (sub.type != DTSubStringType.Number || sub.length < 1 || sub.length > 2)
537 minuteOffset = sub.value;
538 str.ConsumeSubString(sub);
541 else if (length == 3 || length == 4)
543 // Parsing "+800" or "+0800"
544 hourOffset = value / 100;
545 minuteOffset = value % 100;
546 str.ConsumeSubString(sub);
550 // Wrong number of digits
553 Debug.Assert(hourOffset >= 0 && hourOffset <= 99, "hourOffset >= 0 && hourOffset <= 99");
554 Debug.Assert(minuteOffset >= 0 && minuteOffset <= 99, "minuteOffset >= 0 && minuteOffset <= 99");
555 if (minuteOffset < 0 || minuteOffset >= 60)
560 result = new TimeSpan(hourOffset, minuteOffset, 0);
561 if (offsetChar == '-')
563 result = result.Negate();
568 // This is the helper function to handle timezone in string in the format like +/-0800
569 private static bool HandleTimeZone(ref __DTString str, ref DateTimeResult result)
571 if ((str.Index < str.Length - 1))
573 char nextCh = str.Value[str.Index];
574 // Skip whitespace, but don't update the index unless we find a time zone marker
575 int whitespaceCount = 0;
576 while (Char.IsWhiteSpace(nextCh) && str.Index + whitespaceCount < str.Length - 1)
579 nextCh = str.Value[str.Index + whitespaceCount];
581 if (nextCh == '+' || nextCh == '-')
583 str.Index += whitespaceCount;
584 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
586 // Should not have two timezone offsets.
587 result.SetBadDateTimeFailure();
590 result.flags |= ParseFlags.TimeZoneUsed;
591 if (!ParseTimeZone(ref str, ref result.timeZoneOffset))
593 result.SetBadDateTimeFailure();
602 // This is the lexer. Check the character at the current index, and put the found token in dtok and
603 // some raw date/time information in raw.
605 private static Boolean Lex(DS dps, ref __DTString str, ref DateTimeToken dtok, ref DateTimeRawInfo raw, ref DateTimeResult result, ref DateTimeFormatInfo dtfi, DateTimeStyles styles)
609 int indexBeforeSeparator;
610 char charBeforeSeparator;
613 dtok.dtt = DTT.Unk; // Assume the token is unkown.
615 str.GetRegularToken(out tokenType, out tokenValue, dtfi);
620 Trace($"Lex({Hex(str.Value)})\tpos:{str.Index}({Hex(str.m_current)}), {tokenType}, DS.{dps}");
624 // Look at the regular token.
627 case TokenType.NumberToken:
628 case TokenType.YearNumberToken:
629 if (raw.numCount == 3 || tokenValue == -1)
631 result.SetBadDateTimeFailure();
632 LexTraceExit("0010", dps);
638 // If the previous parsing state is DS.T_NNt (like 12:01), and we got another number,
639 // so we will have a terminal state DS.TX_NNN (like 12:01:02).
640 // If the previous parsing state is DS.T_Nt (like 12:), and we got another number,
641 // so we will have a terminal state DS.TX_NN (like 12:01).
643 // Look ahead to see if the following character is a decimal point or timezone offset.
644 // This enables us to parse time in the forms of:
645 // "11:22:33.1234" or "11:22:33-08".
648 if ((str.Index < str.Length - 1))
650 char nextCh = str.Value[str.Index];
653 // While ParseFraction can fail, it just means that there were no digits after
654 // the dot. In this case ParseFraction just removes the dot. This is actually
655 // valid for cultures like Albanian, that join the time marker to the time with
656 // with a dot: e.g. "9:03.MD"
657 ParseFraction(ref str, out raw.fraction);
661 if (dps == DS.T_NNt || dps == DS.T_Nt)
663 if ((str.Index < str.Length - 1))
665 if (false == HandleTimeZone(ref str, ref result))
667 LexTraceExit("0020 (value like \"12:01\" or \"12:\" followed by a non-TZ number", dps);
673 dtok.num = tokenValue;
674 if (tokenType == TokenType.YearNumberToken)
678 raw.year = tokenValue;
680 // If we have number which has 3 or more digits (like "001" or "0001"),
681 // we assume this number is a year. Save the currnet raw.numCount in
684 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
686 case TokenType.SEP_End:
687 dtok.dtt = DTT.YearEnd;
689 case TokenType.SEP_Am:
690 case TokenType.SEP_Pm:
691 if (raw.timeMark == TM.NotSet)
693 raw.timeMark = (sep == TokenType.SEP_Am ? TM.AM : TM.PM);
694 dtok.dtt = DTT.YearSpace;
698 result.SetBadDateTimeFailure();
699 LexTraceExit("0030 (TM.AM/TM.PM Happened more than 1x)", dps);
702 case TokenType.SEP_Space:
703 dtok.dtt = DTT.YearSpace;
705 case TokenType.SEP_Date:
706 dtok.dtt = DTT.YearDateSep;
708 case TokenType.SEP_Time:
709 if (!raw.hasSameDateAndTimeSeparators)
711 result.SetBadDateTimeFailure();
712 LexTraceExit("0040 (Invalid separator after number)", dps);
716 // we have the date and time separators are same and getting a year number, then change the token to YearDateSep as
717 // we are sure we are not parsing time.
718 dtok.dtt = DTT.YearDateSep;
720 case TokenType.SEP_DateOrOffset:
721 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
722 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
723 if ((dateParsingStates[(int)dps][(int)DTT.YearDateSep] == DS.ERROR)
724 && (dateParsingStates[(int)dps][(int)DTT.YearSpace] > DS.ERROR))
726 str.Index = indexBeforeSeparator;
727 str.m_current = charBeforeSeparator;
728 dtok.dtt = DTT.YearSpace;
732 dtok.dtt = DTT.YearDateSep;
735 case TokenType.SEP_YearSuff:
736 case TokenType.SEP_MonthSuff:
737 case TokenType.SEP_DaySuff:
738 dtok.dtt = DTT.NumDatesuff;
741 case TokenType.SEP_HourSuff:
742 case TokenType.SEP_MinuteSuff:
743 case TokenType.SEP_SecondSuff:
744 dtok.dtt = DTT.NumTimesuff;
748 // Invalid separator after number number.
749 result.SetBadDateTimeFailure();
750 LexTraceExit("0040 (Invalid separator after number)", dps);
754 // Found the token already. Return now.
756 LexTraceExit("0050 (success)", dps);
759 result.SetBadDateTimeFailure();
760 LexTraceExit("0060", dps);
763 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
766 // Note here we check if the numCount is less than three.
767 // When we have more than three numbers, it will be caught as error in the state machine.
769 case TokenType.SEP_End:
770 dtok.dtt = DTT.NumEnd;
771 raw.AddNumber(dtok.num);
773 case TokenType.SEP_Am:
774 case TokenType.SEP_Pm:
775 if (raw.timeMark == TM.NotSet)
777 raw.timeMark = (sep == TokenType.SEP_Am ? TM.AM : TM.PM);
778 dtok.dtt = DTT.NumAmpm;
779 // Fix AM/PM parsing case, e.g. "1/10 5 AM"
782 if (!ProcessTerminalState(DS.DX_NN, ref str, ref result, ref styles, ref raw, dtfi))
788 raw.AddNumber(dtok.num);
792 result.SetBadDateTimeFailure();
795 if (dps == DS.T_NNt || dps == DS.T_Nt)
797 if (false == HandleTimeZone(ref str, ref result))
799 LexTraceExit("0070 (HandleTimeZone returned false)", dps);
804 case TokenType.SEP_Space:
805 dtok.dtt = DTT.NumSpace;
806 raw.AddNumber(dtok.num);
808 case TokenType.SEP_Date:
809 dtok.dtt = DTT.NumDatesep;
810 raw.AddNumber(dtok.num);
812 case TokenType.SEP_DateOrOffset:
813 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
814 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
815 if ((dateParsingStates[(int)dps][(int)DTT.NumDatesep] == DS.ERROR)
816 && (dateParsingStates[(int)dps][(int)DTT.NumSpace] > DS.ERROR))
818 str.Index = indexBeforeSeparator;
819 str.m_current = charBeforeSeparator;
820 dtok.dtt = DTT.NumSpace;
824 dtok.dtt = DTT.NumDatesep;
826 raw.AddNumber(dtok.num);
828 case TokenType.SEP_Time:
829 if (raw.hasSameDateAndTimeSeparators &&
830 (dps == DS.D_Y || dps == DS.D_YN || dps == DS.D_YNd || dps == DS.D_YM || dps == DS.D_YMd))
832 // we are parsing a date and we have the time separator same as date separator, so we mark the token as date separator
833 dtok.dtt = DTT.NumDatesep;
834 raw.AddNumber(dtok.num);
837 dtok.dtt = DTT.NumTimesep;
838 raw.AddNumber(dtok.num);
840 case TokenType.SEP_YearSuff:
843 dtok.num = dtfi.Calendar.ToFourDigitYear(tokenValue);
845 catch (ArgumentOutOfRangeException)
847 result.SetBadDateTimeFailure();
848 LexTraceExit("0075 (Calendar.ToFourDigitYear failed)", dps);
851 dtok.dtt = DTT.NumDatesuff;
854 case TokenType.SEP_MonthSuff:
855 case TokenType.SEP_DaySuff:
856 dtok.dtt = DTT.NumDatesuff;
859 case TokenType.SEP_HourSuff:
860 case TokenType.SEP_MinuteSuff:
861 case TokenType.SEP_SecondSuff:
862 dtok.dtt = DTT.NumTimesuff;
865 case TokenType.SEP_LocalTimeMark:
866 dtok.dtt = DTT.NumLocalTimeMark;
867 raw.AddNumber(dtok.num);
870 // Invalid separator after number number.
871 result.SetBadDateTimeFailure();
872 LexTraceExit("0080", dps);
876 case TokenType.HebrewNumber:
877 if (tokenValue >= 100)
879 // This is a year number
882 raw.year = tokenValue;
884 // If we have number which has 3 or more digits (like "001" or "0001"),
885 // we assume this number is a year. Save the currnet raw.numCount in
888 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
890 case TokenType.SEP_End:
891 dtok.dtt = DTT.YearEnd;
893 case TokenType.SEP_Space:
894 dtok.dtt = DTT.YearSpace;
896 case TokenType.SEP_DateOrOffset:
897 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
898 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
899 if (dateParsingStates[(int)dps][(int)DTT.YearSpace] > DS.ERROR)
901 str.Index = indexBeforeSeparator;
902 str.m_current = charBeforeSeparator;
903 dtok.dtt = DTT.YearSpace;
908 // Invalid separator after number number.
909 result.SetBadDateTimeFailure();
910 LexTraceExit("0090", dps);
916 // Invalid separator after number number.
917 result.SetBadDateTimeFailure();
918 LexTraceExit("0100", dps);
924 // This is a day number
925 dtok.num = tokenValue;
926 raw.AddNumber(dtok.num);
928 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
931 // Note here we check if the numCount is less than three.
932 // When we have more than three numbers, it will be caught as error in the state machine.
934 case TokenType.SEP_End:
935 dtok.dtt = DTT.NumEnd;
937 case TokenType.SEP_Space:
938 case TokenType.SEP_Date:
939 dtok.dtt = DTT.NumDatesep;
941 case TokenType.SEP_DateOrOffset:
942 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
943 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
944 if ((dateParsingStates[(int)dps][(int)DTT.NumDatesep] == DS.ERROR)
945 && (dateParsingStates[(int)dps][(int)DTT.NumSpace] > DS.ERROR))
947 str.Index = indexBeforeSeparator;
948 str.m_current = charBeforeSeparator;
949 dtok.dtt = DTT.NumSpace;
953 dtok.dtt = DTT.NumDatesep;
957 // Invalid separator after number number.
958 result.SetBadDateTimeFailure();
959 LexTraceExit("0110", dps);
964 case TokenType.DayOfWeekToken:
965 if (raw.dayOfWeek == -1)
968 // This is a day of week name.
970 raw.dayOfWeek = tokenValue;
971 dtok.dtt = DTT.DayOfWeek;
975 result.SetBadDateTimeFailure();
976 LexTraceExit("0120 (DayOfWeek seen more than 1x)", dps);
980 case TokenType.MonthToken:
984 // This is a month name
986 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
988 case TokenType.SEP_End:
989 dtok.dtt = DTT.MonthEnd;
991 case TokenType.SEP_Space:
992 dtok.dtt = DTT.MonthSpace;
994 case TokenType.SEP_Date:
995 dtok.dtt = DTT.MonthDatesep;
997 case TokenType.SEP_Time:
998 if (!raw.hasSameDateAndTimeSeparators)
1000 result.SetBadDateTimeFailure();
1001 LexTraceExit("0130 (Invalid separator after month name)", dps);
1005 // we have the date and time separators are same and getting a Month name, then change the token to MonthDatesep as
1006 // we are sure we are not parsing time.
1007 dtok.dtt = DTT.MonthDatesep;
1009 case TokenType.SEP_DateOrOffset:
1010 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
1011 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
1012 if ((dateParsingStates[(int)dps][(int)DTT.MonthDatesep] == DS.ERROR)
1013 && (dateParsingStates[(int)dps][(int)DTT.MonthSpace] > DS.ERROR))
1015 str.Index = indexBeforeSeparator;
1016 str.m_current = charBeforeSeparator;
1017 dtok.dtt = DTT.MonthSpace;
1021 dtok.dtt = DTT.MonthDatesep;
1025 //Invalid separator after month name
1026 result.SetBadDateTimeFailure();
1027 LexTraceExit("0130 (Invalid separator after month name)", dps);
1030 raw.month = tokenValue;
1034 result.SetBadDateTimeFailure();
1035 LexTraceExit("0140 (MonthToken seen more than 1x)", dps);
1039 case TokenType.EraToken:
1040 if (result.era != -1)
1042 result.era = tokenValue;
1047 result.SetBadDateTimeFailure();
1048 LexTraceExit("0150 (EraToken seen when result.era already set)", dps);
1052 case TokenType.JapaneseEraToken:
1053 // Special case for Japanese. We allow Japanese era name to be used even if the calendar is not Japanese Calendar.
1054 result.calendar = JapaneseCalendar.GetDefaultInstance();
1055 dtfi = DateTimeFormatInfo.GetJapaneseCalendarDTFI();
1056 if (result.era != -1)
1058 result.era = tokenValue;
1063 result.SetBadDateTimeFailure();
1064 LexTraceExit("0160 (JapaneseEraToken seen when result.era already set)", dps);
1068 case TokenType.TEraToken:
1069 result.calendar = TaiwanCalendar.GetDefaultInstance();
1070 dtfi = DateTimeFormatInfo.GetTaiwanCalendarDTFI();
1071 if (result.era != -1)
1073 result.era = tokenValue;
1078 result.SetBadDateTimeFailure();
1079 LexTraceExit("0170 (TEraToken seen when result.era already set)", dps);
1083 case TokenType.TimeZoneToken:
1085 // This is a timezone designator
1087 // NOTENOTE : for now, we only support "GMT" and "Z" (for Zulu time).
1089 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
1091 // Should not have two timezone offsets.
1092 result.SetBadDateTimeFailure();
1093 LexTraceExit("0180 (seen GMT or Z more than 1x)", dps);
1096 dtok.dtt = DTT.TimeZone;
1097 result.flags |= ParseFlags.TimeZoneUsed;
1098 result.timeZoneOffset = new TimeSpan(0);
1099 result.flags |= ParseFlags.TimeZoneUtc;
1101 case TokenType.EndOfString:
1104 case TokenType.DateWordToken:
1105 case TokenType.IgnorableSymbol:
1106 // Date words and ignorable symbols can just be skipped over
1110 if (raw.timeMark == TM.NotSet)
1112 raw.timeMark = (TM)tokenValue;
1116 result.SetBadDateTimeFailure();
1117 LexTraceExit("0190 (AM/PM timeMark already set)", dps);
1121 case TokenType.UnknownToken:
1122 if (Char.IsLetter(str.m_current))
1124 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTimeAndParameter, nameof(SR.Format_UnknownDateTimeWord), str.Index);
1125 LexTraceExit("0200", dps);
1129 if ((str.m_current == '-' || str.m_current == '+') && ((result.flags & ParseFlags.TimeZoneUsed) == 0))
1131 Int32 originalIndex = str.Index;
1132 if (ParseTimeZone(ref str, ref result.timeZoneOffset))
1134 result.flags |= ParseFlags.TimeZoneUsed;
1135 LexTraceExit("0220 (success)", dps);
1140 // Time zone parse attempt failed. Fall through to punctuation handling.
1141 str.Index = originalIndex;
1145 // Visual Basic implements string to date conversions on top of DateTime.Parse:
1146 // CDate("#10/10/95#")
1148 if (VerifyValidPunctuation(ref str))
1150 LexTraceExit("0230 (success)", dps);
1154 result.SetBadDateTimeFailure();
1155 LexTraceExit("0240", dps);
1159 LexTraceExit("0250 (success)", dps);
1163 private static Boolean VerifyValidPunctuation(ref __DTString str)
1165 // Compatability Behavior. Allow trailing nulls and surrounding hashes
1166 Char ch = str.Value[str.Index];
1169 bool foundStart = false;
1170 bool foundEnd = false;
1171 for (int i = 0; i < str.Length; i++)
1180 // Having more than two hashes is invalid
1193 else if (ch == '\0')
1195 // Allow nulls only at the end
1201 else if ((!Char.IsWhiteSpace(ch)))
1203 // Anything other than whitespace outside hashes is invalid
1204 if (!foundStart || foundEnd)
1212 // The has was un-paired
1215 // Valid Hash usage: eat the hash and continue.
1219 else if (ch == '\0')
1221 for (int i = str.Index; i < str.Length; i++)
1223 if (str.Value[i] != '\0')
1225 // Nulls are only valid if they are the only trailing character
1229 // Move to the end of the string
1230 str.Index = str.Length;
1236 private const int ORDER_YMD = 0; // The order of date is Year/Month/Day.
1237 private const int ORDER_MDY = 1; // The order of date is Month/Day/Year.
1238 private const int ORDER_DMY = 2; // The order of date is Day/Month/Year.
1239 private const int ORDER_YDM = 3; // The order of date is Year/Day/Month
1240 private const int ORDER_YM = 4; // Year/Month order.
1241 private const int ORDER_MY = 5; // Month/Year order.
1242 private const int ORDER_MD = 6; // Month/Day order.
1243 private const int ORDER_DM = 7; // Day/Month order.
1246 // Decide the year/month/day order from the datePattern.
1248 // Return 0 for YMD, 1 for MDY, 2 for DMY, otherwise -1.
1250 private static Boolean GetYearMonthDayOrder(String datePattern, DateTimeFormatInfo dtfi, out int order)
1253 int monthOrder = -1;
1257 bool inQuote = false;
1259 for (int i = 0; i < datePattern.Length && orderCount < 3; i++)
1261 char ch = datePattern[i];
1262 if (ch == '\\' || ch == '%')
1265 continue; // Skip next character that is escaped by this backslash
1268 if (ch == '\'' || ch == '"')
1277 yearOrder = orderCount++;
1280 // Skip all year pattern charaters.
1282 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'y'; i++)
1289 monthOrder = orderCount++;
1291 // Skip all month pattern characters.
1293 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'M'; i++)
1300 int patternCount = 1;
1302 // Skip all day pattern characters.
1304 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'd'; i++)
1309 // Make sure this is not "ddd" or "dddd", which means day of week.
1311 if (patternCount <= 2)
1313 dayOrder = orderCount++;
1319 if (yearOrder == 0 && monthOrder == 1 && dayOrder == 2)
1324 if (monthOrder == 0 && dayOrder == 1 && yearOrder == 2)
1329 if (dayOrder == 0 && monthOrder == 1 && yearOrder == 2)
1334 if (yearOrder == 0 && dayOrder == 1 && monthOrder == 2)
1344 // Decide the year/month order from the pattern.
1346 // Return 0 for YM, 1 for MY, otherwise -1.
1348 private static Boolean GetYearMonthOrder(String pattern, DateTimeFormatInfo dtfi, out int order)
1351 int monthOrder = -1;
1354 bool inQuote = false;
1355 for (int i = 0; i < pattern.Length && orderCount < 2; i++)
1357 char ch = pattern[i];
1358 if (ch == '\\' || ch == '%')
1361 continue; // Skip next character that is escaped by this backslash
1364 if (ch == '\'' || ch == '"')
1373 yearOrder = orderCount++;
1376 // Skip all year pattern charaters.
1378 for (; i + 1 < pattern.Length && pattern[i + 1] == 'y'; i++)
1384 monthOrder = orderCount++;
1386 // Skip all month pattern characters.
1388 for (; i + 1 < pattern.Length && pattern[i + 1] == 'M'; i++)
1395 if (yearOrder == 0 && monthOrder == 1)
1400 if (monthOrder == 0 && yearOrder == 1)
1410 // Decide the month/day order from the pattern.
1412 // Return 0 for MD, 1 for DM, otherwise -1.
1414 private static Boolean GetMonthDayOrder(String pattern, DateTimeFormatInfo dtfi, out int order)
1416 int monthOrder = -1;
1420 bool inQuote = false;
1421 for (int i = 0; i < pattern.Length && orderCount < 2; i++)
1423 char ch = pattern[i];
1424 if (ch == '\\' || ch == '%')
1427 continue; // Skip next character that is escaped by this backslash
1430 if (ch == '\'' || ch == '"')
1439 int patternCount = 1;
1441 // Skip all day pattern charaters.
1443 for (; i + 1 < pattern.Length && pattern[i + 1] == 'd'; i++)
1449 // Make sure this is not "ddd" or "dddd", which means day of week.
1451 if (patternCount <= 2)
1453 dayOrder = orderCount++;
1458 monthOrder = orderCount++;
1460 // Skip all month pattern characters.
1462 for (; i + 1 < pattern.Length && pattern[i + 1] == 'M'; i++)
1469 if (monthOrder == 0 && dayOrder == 1)
1474 if (dayOrder == 0 && monthOrder == 1)
1484 // Adjust the two-digit year if necessary.
1486 private static bool TryAdjustYear(ref DateTimeResult result, int year, out int adjustedYear)
1492 // the Calendar classes need some real work. Many of the calendars that throw
1493 // don't implement a fast/non-allocating (and non-throwing) IsValid{Year|Day|Month} method.
1494 // we are making a targeted try/catch fix in the in-place release but will revisit this code
1495 // in the next side-by-side release.
1496 year = result.calendar.ToFourDigitYear(year);
1498 catch (ArgumentOutOfRangeException)
1504 adjustedYear = year;
1508 private static bool SetDateYMD(ref DateTimeResult result, int year, int month, int day)
1510 // Note, longer term these checks should be done at the end of the parse. This current
1511 // way of checking creates order dependence with parsing the era name.
1512 if (result.calendar.IsValidDay(year, month, day, result.era))
1514 result.SetDate(year, month, day); // YMD
1520 private static bool SetDateMDY(ref DateTimeResult result, int month, int day, int year)
1522 return (SetDateYMD(ref result, year, month, day));
1525 private static bool SetDateDMY(ref DateTimeResult result, int day, int month, int year)
1527 return (SetDateYMD(ref result, year, month, day));
1530 private static bool SetDateYDM(ref DateTimeResult result, int year, int day, int month)
1532 return (SetDateYMD(ref result, year, month, day));
1535 private static void GetDefaultYear(ref DateTimeResult result, ref DateTimeStyles styles)
1537 result.Year = result.calendar.GetYear(GetDateTimeNow(ref result, ref styles));
1538 result.flags |= ParseFlags.YearDefault;
1541 // Processing teriminal case: DS.DX_NN
1542 private static Boolean GetDayOfNN(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1544 if ((result.flags & ParseFlags.HaveDate) != 0)
1546 // Multiple dates in the input string
1547 result.SetBadDateTimeFailure();
1551 int n1 = raw.GetNumber(0);
1552 int n2 = raw.GetNumber(1);
1554 GetDefaultYear(ref result, ref styles);
1557 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out order))
1559 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1563 if (order == ORDER_MD)
1565 if (SetDateYMD(ref result, result.Year, n1, n2)) // MD
1567 result.flags |= ParseFlags.HaveDate;
1574 if (SetDateYMD(ref result, result.Year, n2, n1)) // DM
1576 result.flags |= ParseFlags.HaveDate;
1580 result.SetBadDateTimeFailure();
1584 // Processing teriminal case: DS.DX_NNN
1585 private static Boolean GetDayOfNNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1587 if ((result.flags & ParseFlags.HaveDate) != 0)
1589 // Multiple dates in the input string
1590 result.SetBadDateTimeFailure();
1594 int n1 = raw.GetNumber(0);
1595 int n2 = raw.GetNumber(1); ;
1596 int n3 = raw.GetNumber(2);
1599 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
1601 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1606 if (order == ORDER_YMD)
1608 if (TryAdjustYear(ref result, n1, out year) && SetDateYMD(ref result, year, n2, n3)) // YMD
1610 result.flags |= ParseFlags.HaveDate;
1614 else if (order == ORDER_MDY)
1616 if (TryAdjustYear(ref result, n3, out year) && SetDateMDY(ref result, n1, n2, year)) // MDY
1618 result.flags |= ParseFlags.HaveDate;
1622 else if (order == ORDER_DMY)
1624 if (TryAdjustYear(ref result, n3, out year) && SetDateDMY(ref result, n1, n2, year)) // DMY
1626 result.flags |= ParseFlags.HaveDate;
1630 else if (order == ORDER_YDM)
1632 if (TryAdjustYear(ref result, n1, out year) && SetDateYDM(ref result, year, n2, n3)) // YDM
1634 result.flags |= ParseFlags.HaveDate;
1638 result.SetBadDateTimeFailure();
1642 private static Boolean GetDayOfMN(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1644 if ((result.flags & ParseFlags.HaveDate) != 0)
1646 // Multiple dates in the input string
1647 result.SetBadDateTimeFailure();
1651 // The interpretation is based on the MonthDayPattern and YearMonthPattern
1653 // MonthDayPattern YearMonthPattern Interpretation
1654 // --------------- ---------------- ---------------
1655 // MMMM dd MMMM yyyy Day
1656 // MMMM dd yyyy MMMM Day
1657 // dd MMMM MMMM yyyy Year
1658 // dd MMMM yyyy MMMM Day
1660 // In the first and last cases, it could be either or neither, but a day is a better default interpretation
1661 // than a 2 digit year.
1664 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder))
1666 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1669 if (monthDayOrder == ORDER_DM)
1672 if (!GetYearMonthOrder(dtfi.YearMonthPattern, dtfi, out yearMonthOrder))
1674 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.YearMonthPattern);
1677 if (yearMonthOrder == ORDER_MY)
1680 if (!TryAdjustYear(ref result, raw.GetNumber(0), out year) || !SetDateYMD(ref result, year, raw.month, 1))
1682 result.SetBadDateTimeFailure();
1689 GetDefaultYear(ref result, ref styles);
1690 if (!SetDateYMD(ref result, result.Year, raw.month, raw.GetNumber(0)))
1692 result.SetBadDateTimeFailure();
1698 ////////////////////////////////////////////////////////////////////////
1700 // Deal with the terminal state for Hebrew Month/Day pattern
1702 ////////////////////////////////////////////////////////////////////////
1704 private static Boolean GetHebrewDayOfNM(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1707 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder))
1709 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1712 result.Month = raw.month;
1713 if (monthDayOrder == ORDER_DM || monthDayOrder == ORDER_MD)
1715 if (result.calendar.IsValidDay(result.Year, result.Month, raw.GetNumber(0), result.era))
1717 result.Day = raw.GetNumber(0);
1721 result.SetBadDateTimeFailure();
1725 private static Boolean GetDayOfNM(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1727 if ((result.flags & ParseFlags.HaveDate) != 0)
1729 // Multiple dates in the input string
1730 result.SetBadDateTimeFailure();
1734 // The interpretation is based on the MonthDayPattern and YearMonthPattern
1736 // MonthDayPattern YearMonthPattern Interpretation
1737 // --------------- ---------------- ---------------
1738 // MMMM dd MMMM yyyy Day
1739 // MMMM dd yyyy MMMM Year
1740 // dd MMMM MMMM yyyy Day
1741 // dd MMMM yyyy MMMM Day
1743 // In the first and last cases, it could be either or neither, but a day is a better default interpretation
1744 // than a 2 digit year.
1747 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder))
1749 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1752 if (monthDayOrder == ORDER_MD)
1755 if (!GetYearMonthOrder(dtfi.YearMonthPattern, dtfi, out yearMonthOrder))
1757 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.YearMonthPattern);
1760 if (yearMonthOrder == ORDER_YM)
1763 if (!TryAdjustYear(ref result, raw.GetNumber(0), out year) || !SetDateYMD(ref result, year, raw.month, 1))
1765 result.SetBadDateTimeFailure();
1772 GetDefaultYear(ref result, ref styles);
1773 if (!SetDateYMD(ref result, result.Year, raw.month, raw.GetNumber(0)))
1775 result.SetBadDateTimeFailure();
1781 private static Boolean GetDayOfMNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1783 if ((result.flags & ParseFlags.HaveDate) != 0)
1785 // Multiple dates in the input string
1786 result.SetBadDateTimeFailure();
1790 int n1 = raw.GetNumber(0);
1791 int n2 = raw.GetNumber(1);
1794 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
1796 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1801 if (order == ORDER_MDY)
1803 if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1805 result.SetDate(year, raw.month, n1); // MDY
1806 result.flags |= ParseFlags.HaveDate;
1809 else if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1811 result.SetDate(year, raw.month, n2); // YMD
1812 result.flags |= ParseFlags.HaveDate;
1816 else if (order == ORDER_YMD)
1818 if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1820 result.SetDate(year, raw.month, n2); // YMD
1821 result.flags |= ParseFlags.HaveDate;
1824 else if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1826 result.SetDate(year, raw.month, n1); // DMY
1827 result.flags |= ParseFlags.HaveDate;
1831 else if (order == ORDER_DMY)
1833 if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1835 result.SetDate(year, raw.month, n1); // DMY
1836 result.flags |= ParseFlags.HaveDate;
1839 else if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1841 result.SetDate(year, raw.month, n2); // YMD
1842 result.flags |= ParseFlags.HaveDate;
1847 result.SetBadDateTimeFailure();
1851 private static Boolean GetDayOfYNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1853 if ((result.flags & ParseFlags.HaveDate) != 0)
1855 // Multiple dates in the input string
1856 result.SetBadDateTimeFailure();
1860 int n1 = raw.GetNumber(0);
1861 int n2 = raw.GetNumber(1);
1862 String pattern = dtfi.ShortDatePattern;
1864 // For compatibility, don't throw if we can't determine the order, but default to YMD instead
1866 if (GetYearMonthDayOrder(pattern, dtfi, out order) && order == ORDER_YDM)
1868 if (SetDateYMD(ref result, raw.year, n2, n1))
1870 result.flags |= ParseFlags.HaveDate;
1871 return true; // Year + DM
1876 if (SetDateYMD(ref result, raw.year, n1, n2))
1878 result.flags |= ParseFlags.HaveDate;
1879 return true; // Year + MD
1882 result.SetBadDateTimeFailure();
1886 private static Boolean GetDayOfNNY(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1888 if ((result.flags & ParseFlags.HaveDate) != 0)
1890 // Multiple dates in the input string
1891 result.SetBadDateTimeFailure();
1895 int n1 = raw.GetNumber(0);
1896 int n2 = raw.GetNumber(1);
1899 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
1901 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1905 if (order == ORDER_MDY || order == ORDER_YMD)
1907 if (SetDateYMD(ref result, raw.year, n1, n2))
1909 result.flags |= ParseFlags.HaveDate;
1910 return true; // MD + Year
1915 if (SetDateYMD(ref result, raw.year, n2, n1))
1917 result.flags |= ParseFlags.HaveDate;
1918 return true; // DM + Year
1921 result.SetBadDateTimeFailure();
1926 private static Boolean GetDayOfYMN(ref DateTimeResult result, ref DateTimeRawInfo raw)
1928 if ((result.flags & ParseFlags.HaveDate) != 0)
1930 // Multiple dates in the input string
1931 result.SetBadDateTimeFailure();
1935 if (SetDateYMD(ref result, raw.year, raw.month, raw.GetNumber(0)))
1937 result.flags |= ParseFlags.HaveDate;
1940 result.SetBadDateTimeFailure();
1944 private static Boolean GetDayOfYN(ref DateTimeResult result, ref DateTimeRawInfo raw)
1946 if ((result.flags & ParseFlags.HaveDate) != 0)
1948 // Multiple dates in the input string
1949 result.SetBadDateTimeFailure();
1953 if (SetDateYMD(ref result, raw.year, raw.GetNumber(0), 1))
1955 result.flags |= ParseFlags.HaveDate;
1958 result.SetBadDateTimeFailure();
1962 private static Boolean GetDayOfYM(ref DateTimeResult result, ref DateTimeRawInfo raw)
1964 if ((result.flags & ParseFlags.HaveDate) != 0)
1966 // Multiple dates in the input string
1967 result.SetBadDateTimeFailure();
1971 if (SetDateYMD(ref result, raw.year, raw.month, 1))
1973 result.flags |= ParseFlags.HaveDate;
1976 result.SetBadDateTimeFailure();
1980 private static void AdjustTimeMark(DateTimeFormatInfo dtfi, ref DateTimeRawInfo raw)
1982 // Specail case for culture which uses AM as empty string.
1983 // E.g. af-ZA (0x0436)
1986 // In this case, if we are parsing a string like "2005/09/14 12:23", we will assume this is in AM.
1988 if (raw.timeMark == TM.NotSet)
1990 if (dtfi.AMDesignator != null && dtfi.PMDesignator != null)
1992 if (dtfi.AMDesignator.Length == 0 && dtfi.PMDesignator.Length != 0)
1994 raw.timeMark = TM.AM;
1996 if (dtfi.PMDesignator.Length == 0 && dtfi.AMDesignator.Length != 0)
1998 raw.timeMark = TM.PM;
2005 // Adjust hour according to the time mark.
2007 private static Boolean AdjustHour(ref int hour, TM timeMark)
2009 if (timeMark != TM.NotSet)
2011 if (timeMark == TM.AM)
2013 if (hour < 0 || hour > 12)
2017 hour = (hour == 12) ? 0 : hour;
2021 if (hour < 0 || hour > 23)
2034 private static Boolean GetTimeOfN(ref DateTimeResult result, ref DateTimeRawInfo raw)
2036 if ((result.flags & ParseFlags.HaveTime) != 0)
2038 // Multiple times in the input string
2039 result.SetBadDateTimeFailure();
2043 // In this case, we need a time mark. Check if so.
2045 if (raw.timeMark == TM.NotSet)
2047 result.SetBadDateTimeFailure();
2050 result.Hour = raw.GetNumber(0);
2051 result.flags |= ParseFlags.HaveTime;
2055 private static Boolean GetTimeOfNN(ref DateTimeResult result, ref DateTimeRawInfo raw)
2057 Debug.Assert(raw.numCount >= 2, "raw.numCount >= 2");
2058 if ((result.flags & ParseFlags.HaveTime) != 0)
2060 // Multiple times in the input string
2061 result.SetBadDateTimeFailure();
2065 result.Hour = raw.GetNumber(0);
2066 result.Minute = raw.GetNumber(1);
2067 result.flags |= ParseFlags.HaveTime;
2071 private static Boolean GetTimeOfNNN(ref DateTimeResult result, ref DateTimeRawInfo raw)
2073 if ((result.flags & ParseFlags.HaveTime) != 0)
2075 // Multiple times in the input string
2076 result.SetBadDateTimeFailure();
2079 Debug.Assert(raw.numCount >= 3, "raw.numCount >= 3");
2080 result.Hour = raw.GetNumber(0);
2081 result.Minute = raw.GetNumber(1);
2082 result.Second = raw.GetNumber(2);
2083 result.flags |= ParseFlags.HaveTime;
2088 // Processing terminal state: A Date suffix followed by one number.
2090 private static Boolean GetDateOfDSN(ref DateTimeResult result, ref DateTimeRawInfo raw)
2092 if (raw.numCount != 1 || result.Day != -1)
2094 result.SetBadDateTimeFailure();
2097 result.Day = raw.GetNumber(0);
2101 private static Boolean GetDateOfNDS(ref DateTimeResult result, ref DateTimeRawInfo raw)
2103 if (result.Month == -1)
2105 //Should have a month suffix
2106 result.SetBadDateTimeFailure();
2109 if (result.Year != -1)
2111 // Already has a year suffix
2112 result.SetBadDateTimeFailure();
2115 if (!TryAdjustYear(ref result, raw.GetNumber(0), out result.Year))
2117 // the year value is out of range
2118 result.SetBadDateTimeFailure();
2125 private static Boolean GetDateOfNNDS(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2127 // For partial CJK Dates, the only valid formats are with a specified year, followed by two numbers, which
2128 // will be the Month and Day, and with a specified Month, when the numbers are either the year and day or
2129 // day and year, depending on the short date pattern.
2131 if ((result.flags & ParseFlags.HaveYear) != 0)
2133 if (((result.flags & ParseFlags.HaveMonth) == 0) && ((result.flags & ParseFlags.HaveDay) == 0))
2135 if (TryAdjustYear(ref result, raw.year, out result.Year) && SetDateYMD(ref result, result.Year, raw.GetNumber(0), raw.GetNumber(1)))
2141 else if ((result.flags & ParseFlags.HaveMonth) != 0)
2143 if (((result.flags & ParseFlags.HaveYear) == 0) && ((result.flags & ParseFlags.HaveDay) == 0))
2146 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
2148 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
2152 if (order == ORDER_YMD)
2154 if (TryAdjustYear(ref result, raw.GetNumber(0), out year) && SetDateYMD(ref result, year, result.Month, raw.GetNumber(1)))
2161 if (TryAdjustYear(ref result, raw.GetNumber(1), out year) && SetDateYMD(ref result, year, result.Month, raw.GetNumber(0)))
2168 result.SetBadDateTimeFailure();
2173 // A date suffix is found, use this method to put the number into the result.
2175 private static bool ProcessDateTimeSuffix(ref DateTimeResult result, ref DateTimeRawInfo raw, ref DateTimeToken dtok)
2177 switch (dtok.suffix)
2179 case TokenType.SEP_YearSuff:
2180 if ((result.flags & ParseFlags.HaveYear) != 0)
2184 result.flags |= ParseFlags.HaveYear;
2185 result.Year = raw.year = dtok.num;
2187 case TokenType.SEP_MonthSuff:
2188 if ((result.flags & ParseFlags.HaveMonth) != 0)
2192 result.flags |= ParseFlags.HaveMonth;
2193 result.Month = raw.month = dtok.num;
2195 case TokenType.SEP_DaySuff:
2196 if ((result.flags & ParseFlags.HaveDay) != 0)
2200 result.flags |= ParseFlags.HaveDay;
2201 result.Day = dtok.num;
2203 case TokenType.SEP_HourSuff:
2204 if ((result.flags & ParseFlags.HaveHour) != 0)
2208 result.flags |= ParseFlags.HaveHour;
2209 result.Hour = dtok.num;
2211 case TokenType.SEP_MinuteSuff:
2212 if ((result.flags & ParseFlags.HaveMinute) != 0)
2216 result.flags |= ParseFlags.HaveMinute;
2217 result.Minute = dtok.num;
2219 case TokenType.SEP_SecondSuff:
2220 if ((result.flags & ParseFlags.HaveSecond) != 0)
2224 result.flags |= ParseFlags.HaveSecond;
2225 result.Second = dtok.num;
2231 ////////////////////////////////////////////////////////////////////////
2234 // This is used by DateTime.Parse().
2235 // Process the terminal state for the Hebrew calendar parsing.
2237 ////////////////////////////////////////////////////////////////////////
2239 internal static Boolean ProcessHebrewTerminalState(DS dps, ref __DTString str, ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2241 // The following are accepted terminal state for Hebrew date.
2245 // Deal with the default long/short date format when the year number is ambigous (i.e. year < 100).
2246 raw.year = raw.GetNumber(1);
2247 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2249 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
2252 if (!GetDayOfMNN(ref result, ref raw, dtfi))
2258 // Deal with the default long/short date format when the year number is NOT ambigous (i.e. year >= 100).
2259 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2261 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
2264 if (!GetDayOfYMN(ref result, ref raw))
2270 // When formatting, we only format up to the hundred digit of the Hebrew year, although Hebrew year is now over 5000.
2271 // E.g. if the year is 5763, we only format as 763. so we do the reverse when parsing.
2272 if (raw.year < 1000)
2276 if (!GetDayOfNNY(ref result, ref raw, dtfi))
2280 if (!dtfi.YearMonthAdjustment(ref result.Year, ref raw.month, true))
2282 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
2288 // Deal with Month/Day pattern.
2289 GetDefaultYear(ref result, ref styles);
2290 if (!dtfi.YearMonthAdjustment(ref result.Year, ref raw.month, true))
2292 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
2295 if (!GetHebrewDayOfNM(ref result, ref raw, dtfi))
2301 // Deal with Year/Month pattern.
2302 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2304 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
2307 if (!GetDayOfYM(ref result, ref raw))
2313 // Deal hour + AM/PM
2314 if (!GetTimeOfN(ref result, ref raw))
2320 if (!GetTimeOfNN(ref result, ref raw))
2326 if (!GetTimeOfNNN(ref result, ref raw))
2332 result.SetBadDateTimeFailure();
2338 // We have reached a terminal state. Reset the raw num count.
2346 // A terminal state has been reached, call the appropriate function to fill in the parsing result.
2347 // Return true if the state is a terminal state.
2349 internal static Boolean ProcessTerminalState(DS dps, ref __DTString str, ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2355 passed = GetDayOfNN(ref result, ref styles, ref raw, dtfi);
2358 passed = GetDayOfNNN(ref result, ref raw, dtfi);
2361 passed = GetDayOfMN(ref result, ref styles, ref raw, dtfi);
2364 passed = GetDayOfNM(ref result, ref styles, ref raw, dtfi);
2367 passed = GetDayOfMNN(ref result, ref raw, dtfi);
2370 // The result has got the correct value. No need to process.
2374 passed = GetDayOfYNN(ref result, ref raw, dtfi);
2377 passed = GetDayOfNNY(ref result, ref raw, dtfi);
2380 passed = GetDayOfYMN(ref result, ref raw);
2383 passed = GetDayOfYN(ref result, ref raw);
2386 passed = GetDayOfYM(ref result, ref raw);
2389 passed = GetTimeOfN(ref result, ref raw);
2392 passed = GetTimeOfNN(ref result, ref raw);
2395 passed = GetTimeOfNNN(ref result, ref raw);
2398 // The result has got the correct value. Nothing to do.
2402 passed = GetDateOfDSN(ref result, ref raw);
2405 passed = GetDateOfNDS(ref result, ref raw);
2408 passed = GetDateOfNNDS(ref result, ref raw, dtfi);
2412 PTSTraceExit(dps, passed);
2421 // We have reached a terminal state. Reset the raw num count.
2428 internal static DateTime Parse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles)
2430 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
2432 if (TryParse(s, dtfi, styles, ref result))
2434 return result.parsedDate;
2438 throw GetDateTimeParseException(ref result);
2442 internal static DateTime Parse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out TimeSpan offset)
2444 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
2446 result.flags |= ParseFlags.CaptureOffset;
2447 if (TryParse(s, dtfi, styles, ref result))
2449 offset = result.timeZoneOffset;
2450 return result.parsedDate;
2454 throw GetDateTimeParseException(ref result);
2459 internal static bool TryParse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out DateTime result)
2461 result = DateTime.MinValue;
2462 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
2464 if (TryParse(s, dtfi, styles, ref resultData))
2466 result = resultData.parsedDate;
2472 internal static bool TryParse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out DateTime result, out TimeSpan offset)
2474 result = DateTime.MinValue;
2475 offset = TimeSpan.Zero;
2476 DateTimeResult parseResult = new DateTimeResult(); // The buffer to store the parsing result.
2477 parseResult.Init(s);
2478 parseResult.flags |= ParseFlags.CaptureOffset;
2479 if (TryParse(s, dtfi, styles, ref parseResult))
2481 result = parseResult.parsedDate;
2482 offset = parseResult.timeZoneOffset;
2490 // This is the real method to do the parsing work.
2492 internal static bool TryParse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, ref DateTimeResult result)
2496 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDateTime));
2500 Debug.Assert(dtfi != null, "dtfi == null");
2508 // First try the predefined format.
2511 DS dps = DS.BEGIN; // Date Parsing State.
2512 bool reachTerminalState = false;
2514 DateTimeToken dtok = new DateTimeToken(); // The buffer to store the parsing token.
2515 dtok.suffix = TokenType.SEP_Unk;
2516 DateTimeRawInfo raw = new DateTimeRawInfo(); // The buffer to store temporary parsing information.
2519 Int32* numberPointer = stackalloc Int32[3];
2520 raw.Init(numberPointer);
2522 raw.hasSameDateAndTimeSeparators = dtfi.DateSeparator.Equals(dtfi.TimeSeparator, StringComparison.Ordinal);
2524 result.calendar = dtfi.Calendar;
2525 result.era = Calendar.CurrentEra;
2528 // The string to be parsed. Use a __DTString wrapper so that we can trace the index which
2529 // indicates the begining of next token.
2531 __DTString str = new __DTString(s, dtfi);
2536 // The following loop will break out when we reach the end of the str.
2541 // Call the lexer to get the next token.
2543 // If we find a era in Lex(), the era value will be in raw.era.
2544 if (!Lex(dps, ref str, ref dtok, ref raw, ref result, ref dtfi, styles))
2546 TPTraceExit("0000", dps);
2551 // If the token is not unknown, process it.
2552 // Otherwise, just discard it.
2554 if (dtok.dtt != DTT.Unk)
2557 // Check if we got any CJK Date/Time suffix.
2558 // Since the Date/Time suffix tells us the number belongs to year/month/day/hour/minute/second,
2559 // store the number in the appropriate field in the result.
2561 if (dtok.suffix != TokenType.SEP_Unk)
2563 if (!ProcessDateTimeSuffix(ref result, ref raw, ref dtok))
2565 result.SetBadDateTimeFailure();
2566 TPTraceExit("0010", dps);
2570 dtok.suffix = TokenType.SEP_Unk; // Reset suffix to SEP_Unk;
2573 if (dtok.dtt == DTT.NumLocalTimeMark)
2575 if (dps == DS.D_YNd || dps == DS.D_YN)
2577 // Consider this as ISO 8601 format:
2578 // "yyyy-MM-dd'T'HH:mm:ss" 1999-10-31T02:00:00
2579 TPTraceExit("0020", dps);
2580 return (ParseISO8601(ref raw, ref str, styles, ref result));
2584 result.SetBadDateTimeFailure();
2585 TPTraceExit("0030", dps);
2590 if (raw.hasSameDateAndTimeSeparators)
2592 if (dtok.dtt == DTT.YearEnd || dtok.dtt == DTT.YearSpace || dtok.dtt == DTT.YearDateSep)
2594 // When time and date separators are same and we are hitting a year number while the first parsed part of the string was recognized
2595 // 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
2600 if (dps == DS.T_NNt)
2606 bool atEnd = str.AtEnd();
2607 if (dateParsingStates[(int)dps][(int)dtok.dtt] == DS.ERROR || atEnd)
2611 // we have the case of Serbia have dates in forms 'd.M.yyyy.' so we can expect '.' after the date parts.
2612 // changing the token to end with space instead of Date Separator will avoid failing the parsing.
2614 case DTT.YearDateSep: dtok.dtt = atEnd ? DTT.YearEnd : DTT.YearSpace; break;
2615 case DTT.NumDatesep: dtok.dtt = atEnd ? DTT.NumEnd : DTT.NumSpace; break;
2616 case DTT.NumTimesep: dtok.dtt = atEnd ? DTT.NumEnd : DTT.NumSpace; break;
2617 case DTT.MonthDatesep: dtok.dtt = atEnd ? DTT.MonthEnd : DTT.MonthSpace; break;
2623 // Advance to the next state, and continue
2625 dps = dateParsingStates[(int)dps][(int)dtok.dtt];
2627 if (dps == DS.ERROR)
2629 result.SetBadDateTimeFailure();
2630 TPTraceExit("0040 (invalid state transition)", dps);
2633 else if (dps > DS.ERROR)
2635 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseHebrewRule) != 0)
2637 if (!ProcessHebrewTerminalState(dps, ref str, ref result, ref styles, ref raw, dtfi))
2639 TPTraceExit("0050 (ProcessHebrewTerminalState)", dps);
2645 if (!ProcessTerminalState(dps, ref str, ref result, ref styles, ref raw, dtfi))
2647 TPTraceExit("0060 (ProcessTerminaltState)", dps);
2651 reachTerminalState = true;
2654 // If we have reached a terminal state, start over from DS.BEGIN again.
2655 // For example, when we parsed "1999-12-23 13:30", we will reach a terminal state at "1999-12-23",
2656 // and we start over so we can continue to parse "12:30".
2661 } while (dtok.dtt != DTT.End && dtok.dtt != DTT.NumEnd && dtok.dtt != DTT.MonthEnd);
2663 if (!reachTerminalState)
2665 result.SetBadDateTimeFailure();
2666 TPTraceExit("0070 (did not reach terminal state)", dps);
2670 AdjustTimeMark(dtfi, ref raw);
2671 if (!AdjustHour(ref result.Hour, raw.timeMark))
2673 result.SetBadDateTimeFailure();
2674 TPTraceExit("0080 (AdjustHour)", dps);
2678 // Check if the parased string only contains hour/minute/second values.
2679 bool bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1);
2682 // Check if any year/month/day is missing in the parsing string.
2683 // If yes, get the default value from today's date.
2685 if (!CheckDefaultDateTime(ref result, ref result.calendar, styles))
2687 TPTraceExit("0090 (failed to fill in missing year/month/day defaults)", dps);
2691 if (!result.calendar.TryToDateTime(result.Year, result.Month, result.Day,
2692 result.Hour, result.Minute, result.Second, 0, result.era, out time))
2694 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
2695 TPTraceExit("0100 (result.calendar.TryToDateTime)", dps);
2698 if (raw.fraction > 0)
2700 time = time.AddTicks((long)Math.Round(raw.fraction * Calendar.TicksPerSecond));
2704 // We have to check day of week before we adjust to the time zone.
2705 // Otherwise, the value of day of week may change after adjusting to the time zone.
2707 if (raw.dayOfWeek != -1)
2710 // Check if day of week is correct.
2712 if (raw.dayOfWeek != (int)result.calendar.GetDayOfWeek(time))
2714 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_BadDayOfWeek));
2715 TPTraceExit("0110 (dayOfWeek check)", dps);
2720 result.parsedDate = time;
2722 if (!DetermineTimeZoneAdjustments(ref str, ref result, styles, bTimeOnly))
2724 TPTraceExit("0120 (DetermineTimeZoneAdjustments)", dps);
2727 TPTraceExit("0130 (success)", dps);
2732 // Handles time zone adjustments and sets DateTimeKind values as required by the styles
2733 private static Boolean DetermineTimeZoneAdjustments(ref __DTString str, ref DateTimeResult result, DateTimeStyles styles, Boolean bTimeOnly)
2735 if ((result.flags & ParseFlags.CaptureOffset) != 0)
2737 // This is a DateTimeOffset parse, so the offset will actually be captured directly, and
2738 // no adjustment is required in most cases
2739 return DateTimeOffsetTimeZonePostProcessing(ref str, ref result, styles);
2743 Int64 offsetTicks = result.timeZoneOffset.Ticks;
2745 // the DateTime offset must be within +- 14:00 hours.
2746 if (offsetTicks < DateTimeOffset.MinOffset || offsetTicks > DateTimeOffset.MaxOffset)
2748 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_OffsetOutOfRange));
2753 // The flags AssumeUniveral and AssumeLocal only apply when the input does not have a time zone
2754 if ((result.flags & ParseFlags.TimeZoneUsed) == 0)
2756 // If AssumeLocal or AssumeLocal is used, there will always be a kind specified. As in the
2757 // case when a time zone is present, it will default to being local unless AdjustToUniversal
2758 // is present. These comparisons determine whether setting the kind is sufficient, or if a
2759 // time zone adjustment is required. For consistentcy with the rest of parsing, it is desirable
2760 // to fall through to the Adjust methods below, so that there is consist handling of boundary
2761 // cases like wrapping around on time-only dates and temporarily allowing an adjusted date
2762 // to exceed DateTime.MaxValue
2763 if ((styles & DateTimeStyles.AssumeLocal) != 0)
2765 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2767 result.flags |= ParseFlags.TimeZoneUsed;
2768 result.timeZoneOffset = TimeZoneInfo.GetLocalUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime);
2772 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Local);
2776 else if ((styles & DateTimeStyles.AssumeUniversal) != 0)
2778 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2780 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Utc);
2785 result.flags |= ParseFlags.TimeZoneUsed;
2786 result.timeZoneOffset = TimeSpan.Zero;
2791 // No time zone and no Assume flags, so DateTimeKind.Unspecified is fine
2792 Debug.Assert(result.parsedDate.Kind == DateTimeKind.Unspecified, "result.parsedDate.Kind == DateTimeKind.Unspecified");
2797 if (((styles & DateTimeStyles.RoundtripKind) != 0) && ((result.flags & ParseFlags.TimeZoneUtc) != 0))
2799 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Utc);
2803 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2805 return (AdjustTimeZoneToUniversal(ref result));
2807 return (AdjustTimeZoneToLocal(ref result, bTimeOnly));
2810 // Apply validation and adjustments specific to DateTimeOffset
2811 private static Boolean DateTimeOffsetTimeZonePostProcessing(ref __DTString str, ref DateTimeResult result, DateTimeStyles styles)
2813 // For DateTimeOffset, default to the Utc or Local offset when an offset was not specified by
2814 // the input string.
2815 if ((result.flags & ParseFlags.TimeZoneUsed) == 0)
2817 if ((styles & DateTimeStyles.AssumeUniversal) != 0)
2819 // AssumeUniversal causes the offset to default to zero (0)
2820 result.timeZoneOffset = TimeSpan.Zero;
2824 // AssumeLocal causes the offset to default to Local. This flag is on by default for DateTimeOffset.
2825 result.timeZoneOffset = TimeZoneInfo.GetLocalUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime);
2829 Int64 offsetTicks = result.timeZoneOffset.Ticks;
2831 // there should be no overflow, because the offset can be no more than -+100 hours and the date already
2832 // fits within a DateTime.
2833 Int64 utcTicks = result.parsedDate.Ticks - offsetTicks;
2835 // For DateTimeOffset, both the parsed time and the corresponding UTC value must be within the boundaries
2836 // of a DateTime instance.
2837 if (utcTicks < DateTime.MinTicks || utcTicks > DateTime.MaxTicks)
2839 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_UTCOutOfRange));
2843 // the offset must be within +- 14:00 hours.
2844 if (offsetTicks < DateTimeOffset.MinOffset || offsetTicks > DateTimeOffset.MaxOffset)
2846 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_OffsetOutOfRange));
2850 // DateTimeOffset should still honor the AdjustToUniversal flag for consistency with DateTime. It means you
2851 // want to return an adjusted UTC value, so store the utcTicks in the DateTime and set the offset to zero
2852 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2854 if (((result.flags & ParseFlags.TimeZoneUsed) == 0) && ((styles & DateTimeStyles.AssumeUniversal) == 0))
2856 // Handle the special case where the timeZoneOffset was defaulted to Local
2857 Boolean toUtcResult = AdjustTimeZoneToUniversal(ref result);
2858 result.timeZoneOffset = TimeSpan.Zero;
2862 // The constructor should always succeed because of the range check earlier in the function
2863 // Although it is UTC, internally DateTimeOffset does not use this flag
2864 result.parsedDate = new DateTime(utcTicks, DateTimeKind.Utc);
2865 result.timeZoneOffset = TimeSpan.Zero;
2873 // Adjust the specified time to universal time based on the supplied timezone.
2874 // E.g. when parsing "2001/06/08 14:00-07:00",
2875 // the time is 2001/06/08 14:00, and timeZoneOffset = -07:00.
2876 // The result will be "2001/06/08 21:00"
2878 private static Boolean AdjustTimeZoneToUniversal(ref DateTimeResult result)
2880 long resultTicks = result.parsedDate.Ticks;
2881 resultTicks -= result.timeZoneOffset.Ticks;
2882 if (resultTicks < 0)
2884 resultTicks += Calendar.TicksPerDay;
2887 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2889 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_DateOutOfRange));
2892 result.parsedDate = new DateTime(resultTicks, DateTimeKind.Utc);
2897 // Adjust the specified time to universal time based on the supplied timezone,
2898 // and then convert to local time.
2899 // E.g. when parsing "2001/06/08 14:00-04:00", and local timezone is GMT-7.
2900 // the time is 2001/06/08 14:00, and timeZoneOffset = -05:00.
2901 // The result will be "2001/06/08 11:00"
2903 private static Boolean AdjustTimeZoneToLocal(ref DateTimeResult result, bool bTimeOnly)
2905 long resultTicks = result.parsedDate.Ticks;
2906 // Convert to local ticks
2907 TimeZoneInfo tz = TimeZoneInfo.Local;
2908 Boolean isAmbiguousLocalDst = false;
2909 if (resultTicks < Calendar.TicksPerDay)
2912 // This is time of day.
2916 resultTicks -= result.timeZoneOffset.Ticks;
2917 // If the time is time of day, use the current timezone offset.
2918 resultTicks += tz.GetUtcOffset(bTimeOnly ? DateTime.Now : result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime).Ticks;
2920 if (resultTicks < 0)
2922 resultTicks += Calendar.TicksPerDay;
2927 // Adjust timezone to GMT.
2928 resultTicks -= result.timeZoneOffset.Ticks;
2929 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2931 // If the result ticks is greater than DateTime.MaxValue, we can not create a DateTime from this ticks.
2932 // In this case, keep using the old code.
2933 resultTicks += tz.GetUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime).Ticks;
2937 // Convert the GMT time to local time.
2938 DateTime utcDt = new DateTime(resultTicks, DateTimeKind.Utc);
2939 Boolean isDaylightSavings = false;
2940 resultTicks += TimeZoneInfo.GetUtcOffsetFromUtc(utcDt, TimeZoneInfo.Local, out isDaylightSavings, out isAmbiguousLocalDst).Ticks;
2943 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2945 result.parsedDate = DateTime.MinValue;
2946 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_DateOutOfRange));
2949 result.parsedDate = new DateTime(resultTicks, DateTimeKind.Local, isAmbiguousLocalDst);
2954 // Parse the ISO8601 format string found during Parse();
2957 private static bool ParseISO8601(ref DateTimeRawInfo raw, ref __DTString str, DateTimeStyles styles, ref DateTimeResult result)
2959 if (raw.year < 0 || raw.GetNumber(0) < 0 || raw.GetNumber(1) < 0)
2965 double partSecond = 0;
2967 str.SkipWhiteSpaces();
2968 if (!ParseDigits(ref str, 2, out hour))
2970 result.SetBadDateTimeFailure();
2973 str.SkipWhiteSpaces();
2974 if (!str.Match(':'))
2976 result.SetBadDateTimeFailure();
2979 str.SkipWhiteSpaces();
2980 if (!ParseDigits(ref str, 2, out minute))
2982 result.SetBadDateTimeFailure();
2985 str.SkipWhiteSpaces();
2988 str.SkipWhiteSpaces();
2989 if (!ParseDigits(ref str, 2, out second))
2991 result.SetBadDateTimeFailure();
2996 if (!ParseFraction(ref str, out partSecond))
2998 result.SetBadDateTimeFailure();
3003 str.SkipWhiteSpaces();
3007 char ch = str.GetChar();
3008 if (ch == '+' || ch == '-')
3010 result.flags |= ParseFlags.TimeZoneUsed;
3011 if (!ParseTimeZone(ref str, ref result.timeZoneOffset))
3013 result.SetBadDateTimeFailure();
3017 else if (ch == 'Z' || ch == 'z')
3019 result.flags |= ParseFlags.TimeZoneUsed;
3020 result.timeZoneOffset = TimeSpan.Zero;
3021 result.flags |= ParseFlags.TimeZoneUtc;
3027 str.SkipWhiteSpaces();
3030 if (!VerifyValidPunctuation(ref str))
3032 result.SetBadDateTimeFailure();
3035 str.SkipWhiteSpaces();
3037 if (str.Match('\0'))
3039 if (!VerifyValidPunctuation(ref str))
3041 result.SetBadDateTimeFailure();
3047 // If this is true, there were non-white space characters remaining in the DateTime
3048 result.SetBadDateTimeFailure();
3054 Calendar calendar = GregorianCalendar.GetDefaultInstance();
3055 if (!calendar.TryToDateTime(raw.year, raw.GetNumber(0), raw.GetNumber(1),
3056 hour, minute, second, 0, result.era, out time))
3058 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
3062 time = time.AddTicks((long)Math.Round(partSecond * Calendar.TicksPerSecond));
3063 result.parsedDate = time;
3064 if (!DetermineTimeZoneAdjustments(ref str, ref result, styles, false))
3072 ////////////////////////////////////////////////////////////////////////
3075 // Parse the current word as a Hebrew number.
3076 // This is used by DateTime.ParseExact().
3078 ////////////////////////////////////////////////////////////////////////
3080 internal static bool MatchHebrewDigits(ref __DTString str, int digitLen, out int number)
3084 // Create a context object so that we can parse the Hebrew number text character by character.
3085 HebrewNumberParsingContext context = new HebrewNumberParsingContext(0);
3087 // Set this to ContinueParsing so that we will run the following while loop in the first time.
3088 HebrewNumberParsingState state = HebrewNumberParsingState.ContinueParsing;
3090 while (state == HebrewNumberParsingState.ContinueParsing && str.GetNext())
3092 state = HebrewNumber.ParseByChar(str.GetChar(), ref context);
3095 if (state == HebrewNumberParsingState.FoundEndOfHebrewNumber)
3097 // If we have reached a terminal state, update the result and returns.
3098 number = context.result;
3102 // If we run out of the character before reaching FoundEndOfHebrewNumber, or
3103 // the state is InvalidHebrewNumber or ContinueParsing, we fail to match a Hebrew number.
3108 /*=================================ParseDigits==================================
3109 **Action: Parse the number string in __DTString that are formatted using
3110 ** the following patterns:
3111 ** "0", "00", and "000..0"
3112 **Returns: the integer value
3113 **Arguments: str: a __DTString. The parsing will start from the
3114 ** next character after str.Index.
3115 **Exceptions: FormatException if error in parsing number.
3116 ==============================================================================*/
3118 internal static bool ParseDigits(ref __DTString str, int digitLen, out int result)
3122 // 1 really means 1 or 2 for this call
3123 return ParseDigits(ref str, 1, 2, out result);
3127 return ParseDigits(ref str, digitLen, digitLen, out result);
3131 internal static bool ParseDigits(ref __DTString str, int minDigitLen, int maxDigitLen, out int result)
3133 Debug.Assert(minDigitLen > 0, "minDigitLen > 0");
3134 Debug.Assert(maxDigitLen < 9, "maxDigitLen < 9");
3135 Debug.Assert(minDigitLen <= maxDigitLen, "minDigitLen <= maxDigitLen");
3136 int localResult = 0;
3137 int startingIndex = str.Index;
3138 int tokenLength = 0;
3139 while (tokenLength < maxDigitLen)
3141 if (!str.GetNextDigit())
3146 localResult = localResult * 10 + str.GetDigit();
3149 result = localResult;
3150 if (tokenLength < minDigitLen)
3152 str.Index = startingIndex;
3158 /*=================================ParseFractionExact==================================
3159 **Action: Parse the number string in __DTString that are formatted using
3160 ** the following patterns:
3161 ** "0", "00", and "000..0"
3162 **Returns: the fraction value
3163 **Arguments: str: a __DTString. The parsing will start from the
3164 ** next character after str.Index.
3165 **Exceptions: FormatException if error in parsing number.
3166 ==============================================================================*/
3168 private static bool ParseFractionExact(ref __DTString str, int maxDigitLen, ref double result)
3170 if (!str.GetNextDigit())
3175 result = str.GetDigit();
3178 for (; digitLen < maxDigitLen; digitLen++)
3180 if (!str.GetNextDigit())
3185 result = result * 10 + str.GetDigit();
3188 result /= TimeSpanParse.Pow10(digitLen);
3189 return (digitLen == maxDigitLen);
3192 /*=================================ParseSign==================================
3193 **Action: Parse a positive or a negative sign.
3194 **Returns: true if postive sign. flase if negative sign.
3195 **Arguments: str: a __DTString. The parsing will start from the
3196 ** next character after str.Index.
3197 **Exceptions: FormatException if end of string is encountered or a sign
3198 ** symbol is not found.
3199 ==============================================================================*/
3201 private static bool ParseSign(ref __DTString str, ref bool result)
3205 // A sign symbol ('+' or '-') is expected. However, end of string is encountered.
3208 char ch = str.GetChar();
3219 // A sign symbol ('+' or '-') is expected.
3223 /*=================================ParseTimeZoneOffset==================================
3224 **Action: Parse the string formatted using "z", "zz", "zzz" in DateTime.Format().
3225 **Returns: the TimeSpan for the parsed timezone offset.
3226 **Arguments: str: a __DTString. The parsing will start from the
3227 ** next character after str.Index.
3228 ** len: the repeated number of the "z"
3229 **Exceptions: FormatException if errors in parsing.
3230 ==============================================================================*/
3232 private static bool ParseTimeZoneOffset(ref __DTString str, int len, ref TimeSpan result)
3234 bool isPositive = true;
3236 int minuteOffset = 0;
3242 if (!ParseSign(ref str, ref isPositive))
3246 if (!ParseDigits(ref str, len, out hourOffset))
3252 if (!ParseSign(ref str, ref isPositive))
3257 // Parsing 1 digit will actually parse 1 or 2.
3258 if (!ParseDigits(ref str, 1, out hourOffset))
3266 if (!ParseDigits(ref str, 2, out minuteOffset))
3273 // Since we can not match ':', put the char back.
3275 if (!ParseDigits(ref str, 2, out minuteOffset))
3282 if (minuteOffset < 0 || minuteOffset >= 60)
3287 result = (new TimeSpan(hourOffset, minuteOffset, 0));
3290 result = result.Negate();
3295 /*=================================MatchAbbreviatedMonthName==================================
3296 **Action: Parse the abbreviated month name from string starting at str.Index.
3297 **Returns: A value from 1 to 12 for the first month to the twelveth month.
3298 **Arguments: str: a __DTString. The parsing will start from the
3299 ** next character after str.Index.
3300 **Exceptions: FormatException if an abbreviated month name can not be found.
3301 ==============================================================================*/
3303 private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3305 int maxMatchStrLen = 0;
3310 // Scan the month names (note that some calendars has 13 months) and find
3311 // the matching month name which has the max string length.
3312 // We need to do this because some cultures (e.g. "cs-CZ") which have
3313 // abbreviated month names with the same prefix.
3315 int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3316 for (int i = 1; i <= monthsInYear; i++)
3318 String searchStr = dtfi.GetAbbreviatedMonthName(i);
3319 int matchStrLen = searchStr.Length;
3320 if (dtfi.HasSpacesInMonthNames
3321 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3322 : str.MatchSpecifiedWord(searchStr))
3324 if (matchStrLen > maxMatchStrLen)
3326 maxMatchStrLen = matchStrLen;
3332 // Search leap year form.
3333 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0)
3335 int tempResult = str.MatchLongestWords(dtfi.internalGetLeapYearMonthNames(), ref maxMatchStrLen);
3336 // We found a longer match in the leap year month name. Use this as the result.
3337 // The result from MatchLongestWords is 0 ~ length of word array.
3338 // So we increment the result by one to become the month value.
3339 if (tempResult >= 0)
3341 result = tempResult + 1;
3347 str.Index += (maxMatchStrLen - 1);
3353 /*=================================MatchMonthName==================================
3354 **Action: Parse the month name from string starting at str.Index.
3355 **Returns: A value from 1 to 12 indicating the first month to the twelveth month.
3356 **Arguments: str: a __DTString. The parsing will start from the
3357 ** next character after str.Index.
3358 **Exceptions: FormatException if a month name can not be found.
3359 ==============================================================================*/
3361 private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3363 int maxMatchStrLen = 0;
3368 // Scan the month names (note that some calendars has 13 months) and find
3369 // the matching month name which has the max string length.
3370 // We need to do this because some cultures (e.g. "vi-VN") which have
3371 // month names with the same prefix.
3373 int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3374 for (int i = 1; i <= monthsInYear; i++)
3376 String searchStr = dtfi.GetMonthName(i);
3377 int matchStrLen = searchStr.Length;
3378 if (dtfi.HasSpacesInMonthNames
3379 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3380 : str.MatchSpecifiedWord(searchStr))
3382 if (matchStrLen > maxMatchStrLen)
3384 maxMatchStrLen = matchStrLen;
3390 // Search genitive form.
3391 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0)
3393 int tempResult = str.MatchLongestWords(dtfi.MonthGenitiveNames, ref maxMatchStrLen);
3394 // We found a longer match in the genitive month name. Use this as the result.
3395 // The result from MatchLongestWords is 0 ~ length of word array.
3396 // So we increment the result by one to become the month value.
3397 if (tempResult >= 0)
3399 result = tempResult + 1;
3403 // Search leap year form.
3404 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0)
3406 int tempResult = str.MatchLongestWords(dtfi.internalGetLeapYearMonthNames(), ref maxMatchStrLen);
3407 // We found a longer match in the leap year month name. Use this as the result.
3408 // The result from MatchLongestWords is 0 ~ length of word array.
3409 // So we increment the result by one to become the month value.
3410 if (tempResult >= 0)
3412 result = tempResult + 1;
3419 str.Index += (maxMatchStrLen - 1);
3425 /*=================================MatchAbbreviatedDayName==================================
3426 **Action: Parse the abbreviated day of week name from string starting at str.Index.
3427 **Returns: A value from 0 to 6 indicating Sunday to Saturday.
3428 **Arguments: str: a __DTString. The parsing will start from the
3429 ** next character after str.Index.
3430 **Exceptions: FormatException if a abbreviated day of week name can not be found.
3431 ==============================================================================*/
3433 private static bool MatchAbbreviatedDayName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3435 int maxMatchStrLen = 0;
3439 for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3441 String searchStr = dtfi.GetAbbreviatedDayName(i);
3442 int matchStrLen = searchStr.Length;
3443 if (dtfi.HasSpacesInDayNames
3444 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3445 : str.MatchSpecifiedWord(searchStr))
3447 if (matchStrLen > maxMatchStrLen)
3449 maxMatchStrLen = matchStrLen;
3457 str.Index += maxMatchStrLen - 1;
3463 /*=================================MatchDayName==================================
3464 **Action: Parse the day of week name from string starting at str.Index.
3465 **Returns: A value from 0 to 6 indicating Sunday to Saturday.
3466 **Arguments: str: a __DTString. The parsing will start from the
3467 ** next character after str.Index.
3468 **Exceptions: FormatException if a day of week name can not be found.
3469 ==============================================================================*/
3471 private static bool MatchDayName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3473 // Turkish (tr-TR) got day names with the same prefix.
3474 int maxMatchStrLen = 0;
3478 for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3480 String searchStr = dtfi.GetDayName(i);
3481 int matchStrLen = searchStr.Length;
3482 if (dtfi.HasSpacesInDayNames
3483 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3484 : str.MatchSpecifiedWord(searchStr))
3486 if (matchStrLen > maxMatchStrLen)
3488 maxMatchStrLen = matchStrLen;
3496 str.Index += maxMatchStrLen - 1;
3502 /*=================================MatchEraName==================================
3503 **Action: Parse era name from string starting at str.Index.
3504 **Returns: An era value.
3505 **Arguments: str: a __DTString. The parsing will start from the
3506 ** next character after str.Index.
3507 **Exceptions: FormatException if an era name can not be found.
3508 ==============================================================================*/
3510 private static bool MatchEraName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3514 int[] eras = dtfi.Calendar.Eras;
3518 for (int i = 0; i < eras.Length; i++)
3520 String searchStr = dtfi.GetEraName(eras[i]);
3521 if (str.MatchSpecifiedWord(searchStr))
3523 str.Index += (searchStr.Length - 1);
3527 searchStr = dtfi.GetAbbreviatedEraName(eras[i]);
3528 if (str.MatchSpecifiedWord(searchStr))
3530 str.Index += (searchStr.Length - 1);
3540 /*=================================MatchTimeMark==================================
3541 **Action: Parse the time mark (AM/PM) from string starting at str.Index.
3542 **Returns: TM_AM or TM_PM.
3543 **Arguments: str: a __DTString. The parsing will start from the
3544 ** next character after str.Index.
3545 **Exceptions: FormatException if a time mark can not be found.
3546 ==============================================================================*/
3548 private static bool MatchTimeMark(ref __DTString str, DateTimeFormatInfo dtfi, ref TM result)
3551 // In some cultures have empty strings in AM/PM mark. E.g. af-ZA (0x0436), the AM mark is "", and PM mark is "nm".
3552 if (dtfi.AMDesignator.Length == 0)
3556 if (dtfi.PMDesignator.Length == 0)
3563 String searchStr = dtfi.AMDesignator;
3564 if (searchStr.Length > 0)
3566 if (str.MatchSpecifiedWord(searchStr))
3568 // Found an AM timemark with length > 0.
3569 str.Index += (searchStr.Length - 1);
3574 searchStr = dtfi.PMDesignator;
3575 if (searchStr.Length > 0)
3577 if (str.MatchSpecifiedWord(searchStr))
3579 // Found a PM timemark with length > 0.
3580 str.Index += (searchStr.Length - 1);
3585 str.Index--; // Undo the GetNext call.
3587 if (result != TM.NotSet)
3589 // If one of the AM/PM marks is empty string, return the result.
3595 /*=================================MatchAbbreviatedTimeMark==================================
3596 **Action: Parse the abbreviated time mark (AM/PM) from string starting at str.Index.
3597 **Returns: TM_AM or TM_PM.
3598 **Arguments: str: a __DTString. The parsing will start from the
3599 ** next character after str.Index.
3600 **Exceptions: FormatException if a abbreviated time mark can not be found.
3601 ==============================================================================*/
3603 private static bool MatchAbbreviatedTimeMark(ref __DTString str, DateTimeFormatInfo dtfi, ref TM result)
3605 // NOTENOTE : the assumption here is that abbreviated time mark is the first
3606 // character of the AM/PM designator. If this invariant changes, we have to
3607 // change the code below.
3610 string amDesignator = dtfi.AMDesignator;
3611 if (amDesignator.Length > 0 && str.GetChar() == amDesignator[0])
3617 string pmDesignator = dtfi.PMDesignator;
3618 if (pmDesignator.Length > 0 && str.GetChar() == pmDesignator[0])
3627 /*=================================CheckNewValue==================================
3628 **Action: Check if currentValue is initialized. If not, return the newValue.
3629 ** If yes, check if the current value is equal to newValue. Return false
3630 ** if they are not equal. This is used to check the case like "d" and "dd" are both
3631 ** used to format a string.
3632 **Returns: the correct value for currentValue.
3635 ==============================================================================*/
3637 private static bool CheckNewValue(ref int currentValue, int newValue, char patternChar, ref DateTimeResult result)
3639 if (currentValue == -1)
3641 currentValue = newValue;
3646 if (newValue != currentValue)
3648 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), patternChar);
3655 private static DateTime GetDateTimeNow(ref DateTimeResult result, ref DateTimeStyles styles)
3657 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3659 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
3661 // use the supplied offset to calculate 'Now'
3662 return new DateTime(DateTime.UtcNow.Ticks + result.timeZoneOffset.Ticks, DateTimeKind.Unspecified);
3664 else if ((styles & DateTimeStyles.AssumeUniversal) != 0)
3666 // assume the offset is Utc
3667 return DateTime.UtcNow;
3671 // assume the offset is Local
3672 return DateTime.Now;
3675 private static bool CheckDefaultDateTime(ref DateTimeResult result, ref Calendar cal, DateTimeStyles styles)
3677 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3679 // DateTimeOffset.Parse should allow dates without a year, but only if there is also no time zone marker;
3680 // e.g. "May 1 5pm" is OK, but "May 1 5pm -08:30" is not. This is somewhat pragmatic, since we would
3681 // have to rearchitect parsing completely to allow this one case to correctly handle things like leap
3682 // years and leap months. Is is an extremely corner case, and DateTime is basically incorrect in that
3685 // values like "11:00Z" or "11:00 -3:00" are also acceptable
3687 // if ((month or day is set) and (year is not set and time zone is set))
3689 if (((result.Month != -1) || (result.Day != -1))
3690 && ((result.Year == -1 || ((result.flags & ParseFlags.YearDefault) != 0)) && (result.flags & ParseFlags.TimeZoneUsed) != 0))
3692 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_MissingIncompleteDate));
3698 if ((result.Year == -1) || (result.Month == -1) || (result.Day == -1))
3701 The following table describes the behaviors of getting the default value
3702 when a certain year/month/day values are missing.
3704 An "X" means that the value exists. And "--" means that value is missing.
3706 Year Month Day => ResultYear ResultMonth ResultDay Note
3708 X X X Parsed year Parsed month Parsed day
3709 X X -- Parsed Year Parsed month First day If we have year and month, assume the first day of that month.
3710 X -- X Parsed year First month Parsed day If the month is missing, assume first month of that year.
3711 X -- -- Parsed year First month First day If we have only the year, assume the first day of that year.
3713 -- X X CurrentYear Parsed month Parsed day If the year is missing, assume the current year.
3714 -- X -- CurrentYear Parsed month First day If we have only a month value, assume the current year and current day.
3715 -- -- X CurrentYear First month Parsed day If we have only a day value, assume current year and first month.
3716 -- -- -- CurrentYear Current month Current day So this means that if the date string only contains time, you will get current date.
3720 DateTime now = GetDateTimeNow(ref result, ref styles);
3721 if (result.Month == -1 && result.Day == -1)
3723 if (result.Year == -1)
3725 if ((styles & DateTimeStyles.NoCurrentDateDefault) != 0)
3727 // If there is no year/month/day values, and NoCurrentDateDefault flag is used,
3728 // set the year/month/day value to the beginning year/month/day of DateTime().
3729 // Note we should be using Gregorian for the year/month/day.
3730 cal = GregorianCalendar.GetDefaultInstance();
3731 result.Year = result.Month = result.Day = 1;
3735 // Year/Month/Day are all missing.
3736 result.Year = cal.GetYear(now);
3737 result.Month = cal.GetMonth(now);
3738 result.Day = cal.GetDayOfMonth(now);
3743 // Month/Day are both missing.
3750 if (result.Year == -1)
3752 result.Year = cal.GetYear(now);
3754 if (result.Month == -1)
3758 if (result.Day == -1)
3764 // Set Hour/Minute/Second to zero if these value are not in str.
3765 if (result.Hour == -1) result.Hour = 0;
3766 if (result.Minute == -1) result.Minute = 0;
3767 if (result.Second == -1) result.Second = 0;
3768 if (result.era == -1) result.era = Calendar.CurrentEra;
3772 // Expand a pre-defined format string (like "D" for long date) to the real format that
3773 // we are going to use in the date time parsing.
3774 // This method also set the dtfi according/parseInfo to some special pre-defined
3777 private static String ExpandPredefinedFormat(ReadOnlySpan<char> format, ref DateTimeFormatInfo dtfi, ref ParsingInfo parseInfo, ref DateTimeResult result)
3780 // Check the format to see if we need to override the dtfi to be InvariantInfo,
3781 // and see if we need to set up the userUniversalTime flag.
3786 case 'O': // Round Trip Format
3787 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3788 dtfi = DateTimeFormatInfo.InvariantInfo;
3791 case 'R': // RFC 1123 Standard. (in Universal time)
3792 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3793 dtfi = DateTimeFormatInfo.InvariantInfo;
3795 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3797 result.flags |= ParseFlags.Rfc1123Pattern;
3800 case 's': // Sortable format (in local time)
3801 dtfi = DateTimeFormatInfo.InvariantInfo;
3802 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3804 case 'u': // Universal time format in sortable format.
3805 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3806 dtfi = DateTimeFormatInfo.InvariantInfo;
3808 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3810 result.flags |= ParseFlags.UtcSortPattern;
3813 case 'U': // Universal time format with culture-dependent format.
3814 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3815 result.flags |= ParseFlags.TimeZoneUsed;
3816 result.timeZoneOffset = new TimeSpan(0);
3817 result.flags |= ParseFlags.TimeZoneUtc;
3818 if (dtfi.Calendar.GetType() != typeof(GregorianCalendar))
3820 dtfi = (DateTimeFormatInfo)dtfi.Clone();
3821 dtfi.Calendar = GregorianCalendar.GetDefaultInstance();
3827 // Expand the pre-defined format character to the real format from DateTimeFormatInfo.
3829 return (DateTimeFormat.GetRealFormat(format, dtfi));
3836 // Given a specified format character, parse and update the parsing result.
3838 private static bool ParseByFormat(
3840 ref __DTString format,
3841 ref ParsingInfo parseInfo,
3842 DateTimeFormatInfo dtfi,
3843 ref DateTimeResult result)
3846 int tempYear = 0, tempMonth = 0, tempDay = 0, tempDayOfWeek = 0, tempHour = 0, tempMinute = 0, tempSecond = 0;
3847 double tempFraction = 0;
3848 TM tempTimeMark = 0;
3850 char ch = format.GetChar();
3855 tokenLen = format.GetRepeatCount();
3857 if (dtfi.HasForceTwoDigitYears)
3859 parseResult = ParseDigits(ref str, 1, 4, out tempYear);
3865 parseInfo.fUseTwoDigitYear = true;
3867 parseResult = ParseDigits(ref str, tokenLen, out tempYear);
3869 if (!parseResult && parseInfo.fCustomNumberParser)
3871 parseResult = parseInfo.parseNumberDelegate(ref str, tokenLen, out tempYear);
3875 result.SetBadDateTimeFailure();
3878 if (!CheckNewValue(ref result.Year, tempYear, ch, ref result))
3884 tokenLen = format.GetRepeatCount();
3887 if (!ParseDigits(ref str, tokenLen, out tempMonth))
3889 if (!parseInfo.fCustomNumberParser ||
3890 !parseInfo.parseNumberDelegate(ref str, tokenLen, out tempMonth))
3892 result.SetBadDateTimeFailure();
3901 if (!MatchAbbreviatedMonthName(ref str, dtfi, ref tempMonth))
3903 result.SetBadDateTimeFailure();
3909 if (!MatchMonthName(ref str, dtfi, ref tempMonth))
3911 result.SetBadDateTimeFailure();
3915 result.flags |= ParseFlags.ParsedMonthName;
3917 if (!CheckNewValue(ref result.Month, tempMonth, ch, ref result))
3923 // Day & Day of week
3924 tokenLen = format.GetRepeatCount();
3929 if (!ParseDigits(ref str, tokenLen, out tempDay))
3931 if (!parseInfo.fCustomNumberParser ||
3932 !parseInfo.parseNumberDelegate(ref str, tokenLen, out tempDay))
3934 result.SetBadDateTimeFailure();
3938 if (!CheckNewValue(ref result.Day, tempDay, ch, ref result))
3948 if (!MatchAbbreviatedDayName(ref str, dtfi, ref tempDayOfWeek))
3950 result.SetBadDateTimeFailure();
3957 if (!MatchDayName(ref str, dtfi, ref tempDayOfWeek))
3959 result.SetBadDateTimeFailure();
3963 if (!CheckNewValue(ref parseInfo.dayOfWeek, tempDayOfWeek, ch, ref result))
3970 tokenLen = format.GetRepeatCount();
3971 // Put the era value in result.era.
3972 if (!MatchEraName(ref str, dtfi, ref result.era))
3974 result.SetBadDateTimeFailure();
3979 parseInfo.fUseHour12 = true;
3980 tokenLen = format.GetRepeatCount();
3981 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempHour))
3983 result.SetBadDateTimeFailure();
3986 if (!CheckNewValue(ref result.Hour, tempHour, ch, ref result))
3992 tokenLen = format.GetRepeatCount();
3993 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempHour))
3995 result.SetBadDateTimeFailure();
3998 if (!CheckNewValue(ref result.Hour, tempHour, ch, ref result))
4004 tokenLen = format.GetRepeatCount();
4005 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempMinute))
4007 result.SetBadDateTimeFailure();
4010 if (!CheckNewValue(ref result.Minute, tempMinute, ch, ref result))
4016 tokenLen = format.GetRepeatCount();
4017 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempSecond))
4019 result.SetBadDateTimeFailure();
4022 if (!CheckNewValue(ref result.Second, tempSecond, ch, ref result))
4029 tokenLen = format.GetRepeatCount();
4030 if (tokenLen <= DateTimeFormat.MaxSecondsFractionDigits)
4032 if (!ParseFractionExact(ref str, tokenLen, ref tempFraction))
4036 result.SetBadDateTimeFailure();
4040 if (result.fraction < 0)
4042 result.fraction = tempFraction;
4046 if (tempFraction != result.fraction)
4048 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), ch);
4055 result.SetBadDateTimeFailure();
4061 tokenLen = format.GetRepeatCount();
4064 if (!MatchAbbreviatedTimeMark(ref str, dtfi, ref tempTimeMark))
4066 result.SetBadDateTimeFailure();
4072 if (!MatchTimeMark(ref str, dtfi, ref tempTimeMark))
4074 result.SetBadDateTimeFailure();
4079 if (parseInfo.timeMark == TM.NotSet)
4081 parseInfo.timeMark = tempTimeMark;
4085 if (parseInfo.timeMark != tempTimeMark)
4087 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), ch);
4094 tokenLen = format.GetRepeatCount();
4096 TimeSpan tempTimeZoneOffset = new TimeSpan(0);
4097 if (!ParseTimeZoneOffset(ref str, tokenLen, ref tempTimeZoneOffset))
4099 result.SetBadDateTimeFailure();
4102 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && tempTimeZoneOffset != result.timeZoneOffset)
4104 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'z');
4107 result.timeZoneOffset = tempTimeZoneOffset;
4108 result.flags |= ParseFlags.TimeZoneUsed;
4112 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && result.timeZoneOffset != TimeSpan.Zero)
4114 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'Z');
4118 result.flags |= ParseFlags.TimeZoneUsed;
4119 result.timeZoneOffset = new TimeSpan(0);
4120 result.flags |= ParseFlags.TimeZoneUtc;
4122 // The updating of the indexes is to reflect that ParseExact MatchXXX methods assume that
4123 // they need to increment the index and Parse GetXXX do not. Since we are calling a Parse
4124 // method from inside ParseExact we need to adjust this. Long term, we should try to
4125 // eliminate this discrepancy.
4127 if (!GetTimeZoneName(ref str))
4129 result.SetBadDateTimeFailure();
4135 // This should parse either as a blank, the 'Z' character or a local offset like "-07:00"
4138 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && result.timeZoneOffset != TimeSpan.Zero)
4140 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'K');
4144 result.flags |= ParseFlags.TimeZoneUsed;
4145 result.timeZoneOffset = new TimeSpan(0);
4146 result.flags |= ParseFlags.TimeZoneUtc;
4148 else if (str.Match('+') || str.Match('-'))
4150 str.Index--; // Put the character back for the parser
4151 TimeSpan tempTimeZoneOffset = new TimeSpan(0);
4152 if (!ParseTimeZoneOffset(ref str, 3, ref tempTimeZoneOffset))
4154 result.SetBadDateTimeFailure();
4157 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && tempTimeZoneOffset != result.timeZoneOffset)
4159 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'K');
4162 result.timeZoneOffset = tempTimeZoneOffset;
4163 result.flags |= ParseFlags.TimeZoneUsed;
4165 // Otherwise it is unspecified and we consume no characters
4168 // 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
4169 // We have to exclude the case when the time separator is more than one character and starts with ':' something like "::" for instance.
4170 if (((dtfi.TimeSeparator.Length > 1 && dtfi.TimeSeparator[0] == ':') || !str.Match(':')) &&
4171 !str.Match(dtfi.TimeSeparator))
4173 // A time separator is expected.
4174 result.SetBadDateTimeFailure();
4179 // 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
4180 // We have to exclude the case when the date separator is more than one character and starts with '/' something like "//" for instance.
4181 if (((dtfi.DateSeparator.Length > 1 && dtfi.DateSeparator[0] == '/') || !str.Match('/')) &&
4182 !str.Match(dtfi.DateSeparator))
4184 // A date separator is expected.
4185 result.SetBadDateTimeFailure();
4191 StringBuilder enquotedString = StringBuilderCache.Acquire();
4192 // Use ParseQuoteString so that we can handle escape characters within the quoted string.
4193 if (!TryParseQuoteString(format.Value, format.Index, enquotedString, out tokenLen))
4195 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadQuote), ch);
4196 StringBuilderCache.Release(enquotedString);
4199 format.Index += tokenLen - 1;
4201 // Some cultures uses space in the quoted string. E.g. Spanish has long date format as:
4202 // "dddd, dd' de 'MMMM' de 'yyyy". When inner spaces flag is set, we should skip whitespaces if there is space
4203 // in the quoted string.
4204 String quotedStr = StringBuilderCache.GetStringAndRelease(enquotedString);
4206 for (int i = 0; i < quotedStr.Length; i++)
4208 if (quotedStr[i] == ' ' && parseInfo.fAllowInnerWhite)
4210 str.SkipWhiteSpaces();
4212 else if (!str.Match(quotedStr[i]))
4214 // Can not find the matching quoted string.
4215 result.SetBadDateTimeFailure();
4220 // The "r" and "u" formats incorrectly quoted 'GMT' and 'Z', respectively. We cannot
4221 // correct this mistake for DateTime.ParseExact for compatibility reasons, but we can
4222 // fix it for DateTimeOffset.ParseExact as DateTimeOffset has not been publically released
4224 if ((result.flags & ParseFlags.CaptureOffset) != 0)
4226 if ((result.flags & ParseFlags.Rfc1123Pattern) != 0 && quotedStr == GMTName)
4228 result.flags |= ParseFlags.TimeZoneUsed;
4229 result.timeZoneOffset = TimeSpan.Zero;
4231 else if ((result.flags & ParseFlags.UtcSortPattern) != 0 && quotedStr == ZuluName)
4233 result.flags |= ParseFlags.TimeZoneUsed;
4234 result.timeZoneOffset = TimeSpan.Zero;
4240 // Skip this so we can get to the next pattern character.
4241 // Used in case like "%d", "%y"
4243 // Make sure the next character is not a '%' again.
4244 if (format.Index >= format.Value.Length - 1 || format.Value[format.Index + 1] == '%')
4246 result.SetBadFormatSpecifierFailure(format.Value);
4251 // Escape character. For example, "\d".
4252 // Get the next character in format, and see if we can
4253 // find a match in str.
4254 if (format.GetNext())
4256 if (!str.Match(format.GetChar()))
4258 // Can not find a match for the escaped character.
4259 result.SetBadDateTimeFailure();
4265 result.SetBadFormatSpecifierFailure(format.Value);
4272 if (format.GetNext())
4274 // If we encounter the pattern ".F", and the dot is not present, it is an optional
4275 // second fraction and we can skip this format.
4276 if (format.Match('F'))
4278 format.GetRepeatCount();
4282 result.SetBadDateTimeFailure();
4289 if (parseInfo.fAllowInnerWhite)
4291 // Skip whitespaces if AllowInnerWhite.
4298 // If the space does not match, and trailing space is allowed, we do
4299 // one more step to see if the next format character can lead to
4300 // successful parsing.
4301 // This is used to deal with special case that a empty string can match
4302 // a specific pattern.
4303 // The example here is af-ZA, which has a time format like "hh:mm:ss tt". However,
4304 // its AM symbol is "" (empty string). If fAllowTrailingWhite is used, and time is in
4305 // the AM, we will trim the whitespaces at the end, which will lead to a failure
4306 // when we are trying to match the space before "tt".
4307 if (parseInfo.fAllowTrailingWhite)
4309 if (format.GetNext())
4311 if (ParseByFormat(ref str, ref format, ref parseInfo, dtfi, ref result))
4317 result.SetBadDateTimeFailure();
4325 if (format.MatchSpecifiedWord(GMTName))
4327 format.Index += (GMTName.Length - 1);
4328 // Found GMT string in format. This means the DateTime string
4329 // is in GMT timezone.
4330 result.flags |= ParseFlags.TimeZoneUsed;
4331 result.timeZoneOffset = TimeSpan.Zero;
4332 if (!str.Match(GMTName))
4334 result.SetBadDateTimeFailure();
4338 else if (!str.Match(ch))
4341 result.SetBadDateTimeFailure();
4351 // The pos should point to a quote character. This method will
4352 // get the string enclosed by the quote character.
4354 internal static bool TryParseQuoteString(ReadOnlySpan<char> format, int pos, StringBuilder result, out int returnValue)
4357 // NOTE : pos will be the index of the quote character in the 'format' string.
4360 int formatLen = format.Length;
4362 char quoteChar = format[pos++]; // Get the character used to quote the following string.
4364 bool foundQuote = false;
4365 while (pos < formatLen)
4367 char ch = format[pos++];
4368 if (ch == quoteChar)
4373 else if (ch == '\\')
4375 // The following are used to support escaped character.
4376 // Escaped character is also supported in the quoted string.
4377 // Therefore, someone can use a format like "'minute:' mm\"" to display:
4379 // because the second double quote is escaped.
4380 if (pos < formatLen)
4382 result.Append(format[pos++]);
4387 // This means that '\' is at the end of the formatting string.
4400 // Here we can't find the matching quote.
4405 // Return the character count including the begin/end quote characters and enclosed string.
4407 returnValue = (pos - beginPos);
4414 /*=================================DoStrictParse==================================
4415 **Action: Do DateTime parsing using the format in formatParam.
4416 **Returns: The parsed DateTime.
4421 ** When the following general formats are used, InvariantInfo is used in dtfi:
4423 ** When the following general formats are used, the time is assumed to be in Universal time.
4426 ** Only GregarianCalendar is supported for now.
4427 ** Only support GMT timezone.
4428 ==============================================================================*/
4430 private static bool DoStrictParse(
4431 ReadOnlySpan<char> s,
4432 ReadOnlySpan<char> formatParam,
4433 DateTimeStyles styles,
4434 DateTimeFormatInfo dtfi,
4435 ref DateTimeResult result)
4437 ParsingInfo parseInfo = new ParsingInfo();
4440 parseInfo.calendar = dtfi.Calendar;
4441 parseInfo.fAllowInnerWhite = ((styles & DateTimeStyles.AllowInnerWhite) != 0);
4442 parseInfo.fAllowTrailingWhite = ((styles & DateTimeStyles.AllowTrailingWhite) != 0);
4444 if (formatParam.Length == 1)
4446 if (((result.flags & ParseFlags.CaptureOffset) != 0) && formatParam[0] == 'U')
4448 // The 'U' format is not allowed for DateTimeOffset
4449 result.SetBadFormatSpecifierFailure(formatParam);
4452 formatParam = ExpandPredefinedFormat(formatParam, ref dtfi, ref parseInfo, ref result);
4455 bool bTimeOnly = false;
4456 result.calendar = parseInfo.calendar;
4458 if (parseInfo.calendar.ID == CalendarId.HEBREW)
4460 parseInfo.parseNumberDelegate = m_hebrewNumberParser;
4461 parseInfo.fCustomNumberParser = true;
4464 // Reset these values to negative one so that we could throw exception
4465 // if we have parsed every item twice.
4466 result.Hour = result.Minute = result.Second = -1;
4468 __DTString format = new __DTString(formatParam, dtfi, false);
4469 __DTString str = new __DTString(s, dtfi, false);
4471 if (parseInfo.fAllowTrailingWhite)
4473 // Trim trailing spaces if AllowTrailingWhite.
4475 format.RemoveTrailingInQuoteSpaces();
4479 if ((styles & DateTimeStyles.AllowLeadingWhite) != 0)
4481 format.SkipWhiteSpaces();
4482 format.RemoveLeadingInQuoteSpaces();
4483 str.SkipWhiteSpaces();
4487 // Scan every character in format and match the pattern in str.
4489 while (format.GetNext())
4491 // We trim inner spaces here, so that we will not eat trailing spaces when
4492 // AllowTrailingWhite is not used.
4493 if (parseInfo.fAllowInnerWhite)
4495 str.SkipWhiteSpaces();
4497 if (!ParseByFormat(ref str, ref format, ref parseInfo, dtfi, ref result))
4503 if (str.Index < str.Value.Length - 1)
4505 // There are still remaining character in str.
4506 result.SetBadDateTimeFailure();
4510 if (parseInfo.fUseTwoDigitYear && ((dtfi.FormatFlags & DateTimeFormatFlags.UseHebrewRule) == 0))
4512 // A two digit year value is expected. Check if the parsed year value is valid.
4513 if (result.Year >= 100)
4515 result.SetBadDateTimeFailure();
4520 result.Year = parseInfo.calendar.ToFourDigitYear(result.Year);
4522 catch (ArgumentOutOfRangeException)
4524 result.SetBadDateTimeFailure();
4529 if (parseInfo.fUseHour12)
4531 if (parseInfo.timeMark == TM.NotSet)
4533 // hh is used, but no AM/PM designator is specified.
4534 // Assume the time is AM.
4535 // Don't throw exceptions in here becasue it is very confusing for the caller.
4536 // I always got confused myself when I use "hh:mm:ss" to parse a time string,
4537 // and ParseExact() throws on me (because I didn't use the 24-hour clock 'HH').
4538 parseInfo.timeMark = TM.AM;
4540 if (result.Hour > 12)
4542 // AM/PM is used, but the value for HH is too big.
4543 result.SetBadDateTimeFailure();
4546 if (parseInfo.timeMark == TM.AM)
4548 if (result.Hour == 12)
4555 result.Hour = (result.Hour == 12) ? 12 : result.Hour + 12;
4560 // Military (24-hour time) mode
4562 // AM cannot be set with a 24-hour time like 17:15.
4563 // PM cannot be set with a 24-hour time like 03:15.
4564 if ((parseInfo.timeMark == TM.AM && result.Hour >= 12)
4565 || (parseInfo.timeMark == TM.PM && result.Hour < 12))
4567 result.SetBadDateTimeFailure();
4573 // Check if the parased string only contains hour/minute/second values.
4574 bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1);
4575 if (!CheckDefaultDateTime(ref result, ref parseInfo.calendar, styles))
4580 if (!bTimeOnly && dtfi.HasYearMonthAdjustment)
4582 if (!dtfi.YearMonthAdjustment(ref result.Year, ref result.Month, ((result.flags & ParseFlags.ParsedMonthName) != 0)))
4584 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
4588 if (!parseInfo.calendar.TryToDateTime(result.Year, result.Month, result.Day,
4589 result.Hour, result.Minute, result.Second, 0, result.era, out result.parsedDate))
4591 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
4594 if (result.fraction > 0)
4596 result.parsedDate = result.parsedDate.AddTicks((long)Math.Round(result.fraction * Calendar.TicksPerSecond));
4600 // We have to check day of week before we adjust to the time zone.
4601 // It is because the value of day of week may change after adjusting
4602 // to the time zone.
4604 if (parseInfo.dayOfWeek != -1)
4607 // Check if day of week is correct.
4609 if (parseInfo.dayOfWeek != (int)parseInfo.calendar.GetDayOfWeek(result.parsedDate))
4611 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_BadDayOfWeek));
4617 if (!DetermineTimeZoneAdjustments(ref str, ref result, styles, bTimeOnly))
4624 private static Exception GetDateTimeParseException(ref DateTimeResult result)
4626 switch (result.failure)
4628 case ParseFailureKind.ArgumentNull:
4629 return new ArgumentNullException(result.failureArgumentName, SR.GetResourceString(result.failureMessageID));
4630 case ParseFailureKind.Format:
4631 return new FormatException(SR.GetResourceString(result.failureMessageID));
4632 case ParseFailureKind.FormatWithParameter:
4633 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), result.failureMessageFormatArgument));
4634 case ParseFailureKind.FormatBadDateTimeCalendar:
4635 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), new string(result.originalDateTimeString), result.calendar));
4636 case ParseFailureKind.FormatWithOriginalDateTime:
4637 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), new string(result.originalDateTimeString)));
4638 case ParseFailureKind.FormatWithFormatSpecifier:
4639 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), new string(result.failedFormatSpecifier)));
4640 case ParseFailureKind.FormatWithOriginalDateTimeAndParameter:
4641 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), new string(result.originalDateTimeString), result.failureMessageFormatArgument));
4643 Debug.Fail("Unknown DateTimeParseFailure: " + result.failure.ToString());
4648 [Conditional("_LOGGING")]
4649 private static void LexTraceExit(string message, DS dps)
4652 if (!_tracingEnabled)
4654 Trace($"Lex return {message}, DS.{dps}");
4657 [Conditional("_LOGGING")]
4658 private static void PTSTraceExit(DS dps, bool passed)
4661 if (!_tracingEnabled)
4663 Trace($"ProcessTerminalState {(passed ? "passed" : "failed")} @ DS.{dps}");
4666 [Conditional("_LOGGING")]
4667 private static void TPTraceExit(string message, DS dps)
4670 if (!_tracingEnabled)
4672 Trace($"TryParse return {message}, DS.{dps}");
4675 [Conditional("_LOGGING")]
4676 private static void DTFITrace(DateTimeFormatInfo dtfi)
4679 if (!_tracingEnabled)
4682 Trace("DateTimeFormatInfo Properties");
4683 #if !FEATURE_COREFX_GLOBALIZATION
4684 Trace($" NativeCalendarName {Hex(dtfi.NativeCalendarName)}");
4686 Trace($" AMDesignator {Hex(dtfi.AMDesignator)}");
4687 Trace($" PMDesignator {Hex(dtfi.PMDesignator)}");
4688 Trace($" TimeSeparator {Hex(dtfi.TimeSeparator)}");
4689 Trace($" AbbrvDayNames {Hex(dtfi.AbbreviatedDayNames)}");
4690 Trace($" ShortestDayNames {Hex(dtfi.ShortestDayNames)}");
4691 Trace($" DayNames {Hex(dtfi.DayNames)}");
4692 Trace($" AbbrvMonthNames {Hex(dtfi.AbbreviatedMonthNames)}");
4693 Trace($" MonthNames {Hex(dtfi.MonthNames)}");
4694 Trace($" AbbrvMonthGenNames {Hex(dtfi.AbbreviatedMonthGenitiveNames)}");
4695 Trace($" MonthGenNames {Hex(dtfi.MonthGenitiveNames)}");
4699 // return a string in the form: "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
4700 private static string Hex(string[] strs)
4702 if (strs == null || strs.Length == 0)
4703 return String.Empty;
4704 if (strs.Length == 1)
4705 return Hex(strs[0]);
4707 int curLineLength = 0;
4708 int maxLineLength = 55;
4709 int newLinePadding = 20;
4712 //invariant: strs.Length >= 2
4713 StringBuilder buffer = new StringBuilder();
4714 buffer.Append(Hex(strs[0]));
4715 curLineLength = buffer.Length;
4718 for (int i = 1; i < strs.Length - 1; i++)
4722 if (s.Length > maxLineLength || (curLineLength + s.Length + 2) > maxLineLength)
4725 buffer.Append(Environment.NewLine);
4726 buffer.Append(' ', newLinePadding);
4731 buffer.Append(", ");
4735 curLineLength += s.Length;
4739 s = Hex(strs[strs.Length - 1]);
4740 if (s.Length > maxLineLength || (curLineLength + s.Length + 6) > maxLineLength)
4742 buffer.Append(Environment.NewLine);
4743 buffer.Append(' ', newLinePadding);
4750 return buffer.ToString();
4752 // return a string in the form: "Sun"
4753 private static string Hex(string str) => Hex((ReadOnlySpan<char>)str);
4754 private static string Hex(ReadOnlySpan<char> str)
4756 StringBuilder buffer = new StringBuilder();
4757 buffer.Append("\"");
4758 for (int i = 0; i < str.Length; i++)
4760 if (str[i] <= '\x007f')
4761 buffer.Append(str[i]);
4763 buffer.Append("\\u" + ((int)str[i]).ToString("x4", CultureInfo.InvariantCulture));
4765 buffer.Append("\"");
4766 return buffer.ToString();
4768 // return an unicode escaped string form of char c
4769 private static String Hex(char c)
4772 return c.ToString(CultureInfo.InvariantCulture);
4774 return "\\u" + ((int)c).ToString("x4", CultureInfo.InvariantCulture);
4777 private static void Trace(string s)
4779 // Internal.Console.WriteLine(s);
4782 private static bool _tracingEnabled = false;
4788 // This is a string parsing helper which wraps a String object.
4789 // It has a Index property which tracks
4790 // the current parsing pointer of the string.
4792 internal ref struct __DTString
4795 // Value propery: stores the real string to be parsed.
4797 internal ReadOnlySpan<char> Value;
4800 // Index property: points to the character that we are currently parsing.
4804 // The length of Value string.
4805 internal int Length => Value.Length;
4807 // The current chracter to be looked at.
4808 internal char m_current;
4810 private CompareInfo m_info;
4811 // Flag to indicate if we encouter an digit, we should check for token or not.
4812 // In some cultures, such as mn-MN, it uses "\x0031\x00a0\x0434\x04af\x0433\x044d\x044d\x0440\x00a0\x0441\x0430\x0440" in month names.
4813 private bool m_checkDigitToken;
4815 internal __DTString(ReadOnlySpan<char> str, DateTimeFormatInfo dtfi, bool checkDigitToken) : this(str, dtfi)
4817 m_checkDigitToken = checkDigitToken;
4820 internal __DTString(ReadOnlySpan<char> str, DateTimeFormatInfo dtfi)
4828 m_info = dtfi.CompareInfo;
4829 m_checkDigitToken = ((dtfi.FormatFlags & DateTimeFormatFlags.UseDigitPrefixInTokens) != 0);
4833 m_info = CultureInfo.CurrentCulture.CompareInfo;
4834 m_checkDigitToken = false;
4838 internal CompareInfo CompareInfo
4840 get { return m_info; }
4844 // Advance the Index.
4845 // Return true if Index is NOT at the end of the string.
4848 // while (str.GetNext())
4850 // char ch = str.GetChar()
4852 internal bool GetNext()
4857 m_current = Value[Index];
4863 internal bool AtEnd()
4865 return Index < Length ? false : true;
4868 internal bool Advance(int count)
4870 Debug.Assert(Index + count <= Length, "__DTString::Advance: Index + count <= len");
4874 m_current = Value[Index];
4881 // Used by DateTime.Parse() to get the next token.
4882 internal void GetRegularToken(out TokenType tokenType, out int tokenValue, DateTimeFormatInfo dtfi)
4885 if (Index >= Length)
4887 tokenType = TokenType.EndOfString;
4891 tokenType = TokenType.UnknownToken;
4894 if (DateTimeParse.IsDigit(m_current))
4897 tokenValue = m_current - '0';
4902 // Collect other digits.
4904 while (++Index < Length)
4906 m_current = Value[Index];
4907 value = m_current - '0';
4908 if (value >= 0 && value <= 9)
4910 tokenValue = tokenValue * 10 + value;
4917 if (Index - start > DateTimeParse.MaxDateTimeNumberDigits)
4919 tokenType = TokenType.NumberToken;
4922 else if (Index - start < 3)
4924 tokenType = TokenType.NumberToken;
4928 // If there are more than 3 digits, assume that it's a year value.
4929 tokenType = TokenType.YearNumberToken;
4931 if (m_checkDigitToken)
4934 char saveCh = m_current;
4935 // Re-scan using the staring Index to see if this is a token.
4936 Index = start; // To include the first digit.
4937 m_current = Value[Index];
4940 // This DTFI has tokens starting with digits.
4941 // E.g. mn-MN has month name like "\x0031\x00a0\x0434\x04af\x0433\x044d\x044d\x0440\x00a0\x0441\x0430\x0440"
4942 if (dtfi.Tokenize(TokenType.RegularTokenMask, out tempType, out tempValue, ref this))
4944 tokenType = tempType;
4945 tokenValue = tempValue;
4946 // This is a token, so the Index has been advanced propertly in DTFI.Tokenizer().
4950 // Use the number token value.
4951 // Restore the index.
4957 else if (Char.IsWhiteSpace(m_current))
4959 // Just skip to the next character.
4960 while (++Index < Length)
4962 m_current = Value[Index];
4963 if (!(Char.IsWhiteSpace(m_current)))
4968 // We have reached the end of string.
4969 tokenType = TokenType.EndOfString;
4973 dtfi.Tokenize(TokenType.RegularTokenMask, out tokenType, out tokenValue, ref this);
4977 internal TokenType GetSeparatorToken(DateTimeFormatInfo dtfi, out int indexBeforeSeparator, out char charBeforeSeparator)
4979 indexBeforeSeparator = Index;
4980 charBeforeSeparator = m_current;
4981 TokenType tokenType;
4982 if (!SkipWhiteSpaceCurrent())
4984 // Reach the end of the string.
4985 return (TokenType.SEP_End);
4987 if (!DateTimeParse.IsDigit(m_current))
4989 // Not a digit. Tokenize it.
4991 bool found = dtfi.Tokenize(TokenType.SeparatorTokenMask, out tokenType, out tokenValue, ref this);
4994 tokenType = TokenType.SEP_Space;
4999 // Do nothing here. If we see a number, it will not be a separator. There is no need wasting time trying to find the
5001 tokenType = TokenType.SEP_Space;
5006 [MethodImpl(MethodImplOptions.AggressiveInlining)]
5007 internal bool MatchSpecifiedWord(String target) =>
5008 Index + target.Length <= Length &&
5009 m_info.Compare(Value.Slice(Index, target.Length), target, CompareOptions.IgnoreCase) == 0;
5011 private static readonly Char[] WhiteSpaceChecks = new Char[] { ' ', '\u00A0' };
5013 internal bool MatchSpecifiedWords(String target, bool checkWordBoundary, ref int matchLength)
5015 int valueRemaining = Value.Length - Index;
5016 matchLength = target.Length;
5018 if (matchLength > valueRemaining || m_info.Compare(Value.Slice(Index, matchLength), target, CompareOptions.IgnoreCase) != 0)
5020 // Check word by word
5021 int targetPosition = 0; // Where we are in the target string
5022 int thisPosition = Index; // Where we are in this string
5023 int wsIndex = target.IndexOfAny(WhiteSpaceChecks, targetPosition);
5030 int segmentLength = wsIndex - targetPosition;
5031 if (thisPosition >= Value.Length - segmentLength)
5032 { // Subtraction to prevent overflow.
5035 if (segmentLength == 0)
5037 // If segmentLength == 0, it means that we have leading space in the target string.
5038 // In that case, skip the leading spaces in the target and this string.
5043 // Make sure we also have whitespace in the input string
5044 if (!Char.IsWhiteSpace(Value[thisPosition + segmentLength]))
5048 if (m_info.CompareOptionIgnoreCase(Value.Slice(thisPosition, segmentLength), target.AsSpan(targetPosition, segmentLength)) != 0)
5052 // Advance the input string
5053 thisPosition = thisPosition + segmentLength + 1;
5055 // Advance our target string
5056 targetPosition = wsIndex + 1;
5059 // Skip past multiple whitespace
5060 while (thisPosition < Value.Length && Char.IsWhiteSpace(Value[thisPosition]))
5065 } while ((wsIndex = target.IndexOfAny(WhiteSpaceChecks, targetPosition)) >= 0);
5066 // now check the last segment;
5067 if (targetPosition < target.Length)
5069 int segmentLength = target.Length - targetPosition;
5070 if (thisPosition > Value.Length - segmentLength)
5074 if (m_info.CompareOptionIgnoreCase(Value.Slice(thisPosition, segmentLength), target.AsSpan(targetPosition, segmentLength)) != 0)
5081 if (checkWordBoundary)
5083 int nextCharIndex = Index + matchLength;
5084 if (nextCharIndex < Value.Length)
5086 if (Char.IsLetter(Value[nextCharIndex]))
5096 // Check to see if the string starting from Index is a prefix of
5098 // If a match is found, true value is returned and Index is updated to the next character to be parsed.
5099 // Otherwise, Index is unchanged.
5101 internal bool Match(String str)
5103 if (++Index >= Length)
5108 if (str.Length > (Value.Length - Index))
5113 if (m_info.Compare(Value.Slice(Index, str.Length), str, CompareOptions.Ordinal) == 0)
5115 // Update the Index to the end of the matching string.
5116 // So the following GetNext()/Match() opeartion will get
5117 // the next character to be parsed.
5118 Index += (str.Length - 1);
5124 internal bool Match(char ch)
5126 if (++Index >= Length)
5130 if (Value[Index] == ch)
5140 // Actions: From the current position, try matching the longest word in the specified string array.
5141 // E.g. words[] = {"AB", "ABC", "ABCD"}, if the current position points to a substring like "ABC DEF",
5142 // MatchLongestWords(words, ref MaxMatchStrLen) will return 1 (the index), and maxMatchLen will be 3.
5144 // The index that contains the longest word to match
5146 // words The string array that contains words to search.
5147 // maxMatchStrLen [in/out] the initailized maximum length. This parameter can be used to
5148 // find the longest match in two string arrays.
5150 internal int MatchLongestWords(String[] words, ref int maxMatchStrLen)
5153 for (int i = 0; i < words.Length; i++)
5155 String word = words[i];
5156 int matchLength = word.Length;
5157 if (MatchSpecifiedWords(word, false, ref matchLength))
5159 if (matchLength > maxMatchStrLen)
5161 maxMatchStrLen = matchLength;
5171 // Get the number of repeat character after the current character.
5172 // For a string "hh:mm:ss" at Index of 3. GetRepeatCount() = 2, and Index
5173 // will point to the second ':'.
5175 internal int GetRepeatCount()
5177 char repeatChar = Value[Index];
5178 int pos = Index + 1;
5179 while ((pos < Length) && (Value[pos] == repeatChar))
5183 int repeatCount = (pos - Index);
5184 // Update the Index to the end of the repeated characters.
5185 // So the following GetNext() opeartion will get
5186 // the next character to be parsed.
5188 return (repeatCount);
5191 // Return false when end of string is encountered or a non-digit character is found.
5192 [MethodImpl(MethodImplOptions.AggressiveInlining)]
5193 internal bool GetNextDigit() =>
5195 DateTimeParse.IsDigit(Value[Index]);
5198 // Get the current character.
5200 internal char GetChar()
5202 Debug.Assert(Index >= 0 && Index < Length, "Index >= 0 && Index < len");
5203 return (Value[Index]);
5207 // Convert the current character to a digit, and return it.
5209 internal int GetDigit()
5211 Debug.Assert(Index >= 0 && Index < Length, "Index >= 0 && Index < len");
5212 Debug.Assert(DateTimeParse.IsDigit(Value[Index]), "IsDigit(Value[Index])");
5213 return (Value[Index] - '0');
5217 // Eat White Space ahead of the current position
5219 // Return false if end of string is encountered.
5221 internal void SkipWhiteSpaces()
5223 // Look ahead to see if the next character
5225 while (Index + 1 < Length)
5227 char ch = Value[Index + 1];
5228 if (!Char.IsWhiteSpace(ch))
5238 // Skip white spaces from the current position
5240 // Return false if end of string is encountered.
5242 internal bool SkipWhiteSpaceCurrent()
5244 if (Index >= Length)
5249 if (!Char.IsWhiteSpace(m_current))
5254 while (++Index < Length)
5256 m_current = Value[Index];
5257 if (!Char.IsWhiteSpace(m_current))
5266 internal void TrimTail()
5269 while (i >= 0 && Char.IsWhiteSpace(Value[i]))
5273 Value = Value.Slice(0, i + 1);
5276 // Trim the trailing spaces within a quoted string.
5277 // Call this after TrimTail() is done.
5278 internal void RemoveTrailingInQuoteSpaces()
5286 // Check if the last character is a quote.
5287 if (ch == '\'' || ch == '\"')
5289 if (char.IsWhiteSpace(Value[i - 1]))
5292 while (i >= 1 && char.IsWhiteSpace(Value[i - 1]))
5296 Span<char> result = new char[i + 1];
5298 Value.Slice(0, i).CopyTo(result);
5304 // Trim the leading spaces within a quoted string.
5305 // Call this after the leading spaces before quoted string are trimmed.
5306 internal void RemoveLeadingInQuoteSpaces()
5314 // Check if the last character is a quote.
5315 if (ch == '\'' || ch == '\"')
5317 while ((i + 1) < Length && char.IsWhiteSpace(Value[i + 1]))
5323 Span<char> result = new char[Value.Length - i];
5325 Value.Slice(i + 1).CopyTo(result.Slice(1));
5331 internal DTSubString GetSubString()
5333 DTSubString sub = new DTSubString();
5336 while (Index + sub.length < Length)
5338 DTSubStringType currentType;
5339 Char ch = Value[Index + sub.length];
5340 if (ch >= '0' && ch <= '9')
5342 currentType = DTSubStringType.Number;
5346 currentType = DTSubStringType.Other;
5349 if (sub.length == 0)
5351 sub.type = currentType;
5355 if (sub.type != currentType)
5361 if (currentType == DTSubStringType.Number)
5363 // Incorporate the number into the value
5364 // Limit the digits to prevent overflow
5365 if (sub.length > DateTimeParse.MaxDateTimeNumberDigits)
5367 sub.type = DTSubStringType.Invalid;
5370 int number = ch - '0';
5371 Debug.Assert(number >= 0 && number <= 9, "number >= 0 && number <= 9");
5372 sub.value = sub.value * 10 + number;
5376 // For non numbers, just return this length 1 token. This should be expanded
5377 // to more types of thing if this parsing approach is used for things other
5378 // than numbers and single characters
5382 if (sub.length == 0)
5384 sub.type = DTSubStringType.End;
5391 internal void ConsumeSubString(DTSubString sub)
5393 Debug.Assert(sub.index == Index, "sub.index == Index");
5394 Debug.Assert(sub.index + sub.length <= Length, "sub.index + sub.length <= len");
5395 Index = sub.index + sub.length;
5398 m_current = Value[Index];
5403 internal enum DTSubStringType
5412 internal ref struct DTSubString
5414 internal ReadOnlySpan<char> s;
5415 internal Int32 index;
5416 internal Int32 length;
5417 internal DTSubStringType type;
5418 internal Int32 value;
5420 internal Char this[Int32 relativeIndex]
5424 return s[index + relativeIndex];
5430 // The buffer to store the parsing token.
5433 struct DateTimeToken
5435 internal DateTimeParse.DTT dtt; // Store the token
5436 internal TokenType suffix; // Store the CJK Year/Month/Day suffix (if any)
5437 internal int num; // Store the number that we are parsing (if any)
5441 // The buffer to store temporary parsing information.
5444 unsafe struct DateTimeRawInfo
5447 internal int numCount;
5450 internal int dayOfWeek;
5452 internal DateTimeParse.TM timeMark;
5453 internal double fraction;
5454 internal bool hasSameDateAndTimeSeparators;
5456 internal void Init(int* numberBuffer)
5462 timeMark = DateTimeParse.TM.NotSet;
5466 internal unsafe void AddNumber(int value)
5468 num[numCount++] = value;
5470 internal unsafe int GetNumber(int index)
5476 internal enum ParseFailureKind
5481 FormatWithParameter = 3,
5482 FormatWithOriginalDateTime = 4,
5483 FormatWithFormatSpecifier = 5,
5484 FormatWithOriginalDateTimeAndParameter = 6,
5485 FormatBadDateTimeCalendar = 7, // FormatException when ArgumentOutOfRange is thrown by a Calendar.TryToDateTime().
5489 internal enum ParseFlags
5491 HaveYear = 0x00000001,
5492 HaveMonth = 0x00000002,
5493 HaveDay = 0x00000004,
5494 HaveHour = 0x00000008,
5495 HaveMinute = 0x00000010,
5496 HaveSecond = 0x00000020,
5497 HaveTime = 0x00000040,
5498 HaveDate = 0x00000080,
5499 TimeZoneUsed = 0x00000100,
5500 TimeZoneUtc = 0x00000200,
5501 ParsedMonthName = 0x00000400,
5502 CaptureOffset = 0x00000800,
5503 YearDefault = 0x00001000,
5504 Rfc1123Pattern = 0x00002000,
5505 UtcSortPattern = 0x00004000,
5509 // This will store the result of the parsing. And it will be eventually
5510 // used to construct a DateTime instance.
5513 ref struct DateTimeResult
5519 // Set time defualt to 00:00:00.
5522 internal int Minute;
5523 internal int Second;
5524 internal double fraction;
5528 internal ParseFlags flags;
5530 internal TimeSpan timeZoneOffset;
5532 internal Calendar calendar;
5534 internal DateTime parsedDate;
5536 internal ParseFailureKind failure;
5537 internal string failureMessageID;
5538 internal object failureMessageFormatArgument;
5539 internal string failureArgumentName;
5540 internal ReadOnlySpan<char> originalDateTimeString;
5541 internal ReadOnlySpan<char> failedFormatSpecifier;
5543 internal void Init(ReadOnlySpan<char> originalDateTimeString)
5545 this.originalDateTimeString = originalDateTimeString;
5553 internal void SetDate(int year, int month, int day)
5560 internal void SetBadFormatSpecifierFailure()
5562 SetBadFormatSpecifierFailure(ReadOnlySpan<char>.Empty);
5565 internal void SetBadFormatSpecifierFailure(ReadOnlySpan<char> failedFormatSpecifier)
5567 this.failure = ParseFailureKind.FormatWithFormatSpecifier;
5568 this.failureMessageID = nameof(SR.Format_BadFormatSpecifier);
5569 this.failedFormatSpecifier = failedFormatSpecifier;
5572 internal void SetBadDateTimeFailure()
5574 this.failure = ParseFailureKind.FormatWithOriginalDateTime;
5575 this.failureMessageID = nameof(SR.Format_BadDateTime);
5576 this.failureMessageFormatArgument = null;
5579 internal void SetFailure(ParseFailureKind failure, string failureMessageID)
5581 this.failure = failure;
5582 this.failureMessageID = failureMessageID;
5583 this.failureMessageFormatArgument = null;
5586 internal void SetFailure(ParseFailureKind failure, string failureMessageID, object failureMessageFormatArgument)
5588 this.failure = failure;
5589 this.failureMessageID = failureMessageID;
5590 this.failureMessageFormatArgument = failureMessageFormatArgument;
5593 internal void SetFailure(ParseFailureKind failure, string failureMessageID, object failureMessageFormatArgument, string failureArgumentName)
5595 this.failure = failure;
5596 this.failureMessageID = failureMessageID;
5597 this.failureMessageFormatArgument = failureMessageFormatArgument;
5598 this.failureArgumentName = failureArgumentName;
5602 // This is the helper data structure used in ParseExact().
5603 internal struct ParsingInfo
5605 internal Calendar calendar;
5606 internal int dayOfWeek;
5607 internal DateTimeParse.TM timeMark;
5609 internal bool fUseHour12;
5610 internal bool fUseTwoDigitYear;
5611 internal bool fAllowInnerWhite;
5612 internal bool fAllowTrailingWhite;
5613 internal bool fCustomNumberParser;
5614 internal DateTimeParse.MatchNumberDelegate parseNumberDelegate;
5616 internal void Init()
5619 timeMark = DateTimeParse.TM.NotSet;
5624 // The type of token that will be returned by DateTimeFormatInfo.Tokenize().
5626 internal enum TokenType
5628 // The valid token should start from 1.
5630 // Regular tokens. The range is from 0x00 ~ 0xff.
5631 NumberToken = 1, // The number. E.g. "12"
5632 YearNumberToken = 2, // The number which is considered as year number, which has 3 or more digits. E.g. "2003"
5633 Am = 3, // AM timemark. E.g. "AM"
5634 Pm = 4, // PM timemark. E.g. "PM"
5635 MonthToken = 5, // A word (or words) that represents a month name. E.g. "March"
5636 EndOfString = 6, // End of string
5637 DayOfWeekToken = 7, // A word (or words) that represents a day of week name. E.g. "Monday" or "Mon"
5638 TimeZoneToken = 8, // A word that represents a timezone name. E.g. "GMT"
5639 EraToken = 9, // A word that represents a era name. E.g. "A.D."
5640 DateWordToken = 10, // A word that can appear in a DateTime string, but serves no parsing semantics. E.g. "de" in Spanish culture.
5641 UnknownToken = 11, // An unknown word, which signals an error in parsing.
5642 HebrewNumber = 12, // A number that is composed of Hebrew text. Hebrew calendar uses Hebrew digits for year values, month values, and day values.
5643 JapaneseEraToken = 13, // Era name for JapaneseCalendar
5644 TEraToken = 14, // Era name for TaiwanCalendar
5645 IgnorableSymbol = 15, // A separator like "," that is equivalent to whitespace
5648 // Separator tokens.
5649 SEP_Unk = 0x100, // Unknown separator.
5650 SEP_End = 0x200, // The end of the parsing string.
5651 SEP_Space = 0x300, // Whitespace (including comma).
5652 SEP_Am = 0x400, // AM timemark. E.g. "AM"
5653 SEP_Pm = 0x500, // PM timemark. E.g. "PM"
5654 SEP_Date = 0x600, // date separator. E.g. "/"
5655 SEP_Time = 0x700, // time separator. E.g. ":"
5656 SEP_YearSuff = 0x800, // Chinese/Japanese/Korean year suffix.
5657 SEP_MonthSuff = 0x900, // Chinese/Japanese/Korean month suffix.
5658 SEP_DaySuff = 0xa00, // Chinese/Japanese/Korean day suffix.
5659 SEP_HourSuff = 0xb00, // Chinese/Japanese/Korean hour suffix.
5660 SEP_MinuteSuff = 0xc00, // Chinese/Japanese/Korean minute suffix.
5661 SEP_SecondSuff = 0xd00, // Chinese/Japanese/Korean second suffix.
5662 SEP_LocalTimeMark = 0xe00, // 'T', used in ISO 8601 format.
5663 SEP_DateOrOffset = 0xf00, // '-' which could be a date separator or start of a time zone offset
5665 RegularTokenMask = 0x00ff,
5666 SeparatorTokenMask = 0xff00,