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;
11 internal static class DateTimeParse
13 internal const Int32 MaxDateTimeNumberDigits = 8;
15 internal delegate bool MatchNumberDelegate(ref __DTString str, int digitLen, out int result);
17 internal static MatchNumberDelegate m_hebrewNumberParser = new MatchNumberDelegate(DateTimeParse.MatchHebrewDigits);
19 internal static DateTime ParseExact(String s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style)
21 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
23 if (TryParseExact(s, format, dtfi, style, ref result))
25 return result.parsedDate;
29 throw GetDateTimeParseException(ref result);
33 internal static DateTime ParseExact(String s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style, out TimeSpan offset)
35 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
36 offset = TimeSpan.Zero;
38 result.flags |= ParseFlags.CaptureOffset;
39 if (TryParseExact(s, format, dtfi, style, ref result))
41 offset = result.timeZoneOffset;
42 return result.parsedDate;
46 throw GetDateTimeParseException(ref result);
50 internal static bool TryParseExact(String s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result)
52 result = DateTime.MinValue;
53 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
55 if (TryParseExact(s, format, dtfi, style, ref resultData))
57 result = resultData.parsedDate;
63 internal static bool TryParseExact(String s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result, out TimeSpan offset)
65 result = DateTime.MinValue;
66 offset = TimeSpan.Zero;
67 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
69 resultData.flags |= ParseFlags.CaptureOffset;
70 if (TryParseExact(s, format, dtfi, style, ref resultData))
72 result = resultData.parsedDate;
73 offset = resultData.timeZoneOffset;
79 internal static bool TryParseExact(String s, String format, DateTimeFormatInfo dtfi, DateTimeStyles style, ref DateTimeResult result)
83 result.SetFailure(ParseFailureKind.ArgumentNull, nameof(SR.ArgumentNull_String), null, nameof(s));
88 result.SetFailure(ParseFailureKind.ArgumentNull, nameof(SR.ArgumentNull_String), null, nameof(format));
93 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
97 if (format.Length == 0)
99 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
103 Debug.Assert(dtfi != null, "dtfi == null");
105 return DoStrictParse(s, format, style, dtfi, ref result);
108 internal static DateTime ParseExactMultiple(String s, String[] formats,
109 DateTimeFormatInfo dtfi, DateTimeStyles style)
111 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
113 if (TryParseExactMultiple(s, formats, dtfi, style, ref result))
115 return result.parsedDate;
119 throw GetDateTimeParseException(ref result);
124 internal static DateTime ParseExactMultiple(String s, String[] formats,
125 DateTimeFormatInfo dtfi, DateTimeStyles style, out TimeSpan offset)
127 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
128 offset = TimeSpan.Zero;
130 result.flags |= ParseFlags.CaptureOffset;
131 if (TryParseExactMultiple(s, formats, dtfi, style, ref result))
133 offset = result.timeZoneOffset;
134 return result.parsedDate;
138 throw GetDateTimeParseException(ref result);
142 internal static bool TryParseExactMultiple(String s, String[] formats,
143 DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result, out TimeSpan offset)
145 result = DateTime.MinValue;
146 offset = TimeSpan.Zero;
147 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
149 resultData.flags |= ParseFlags.CaptureOffset;
150 if (TryParseExactMultiple(s, formats, dtfi, style, ref resultData))
152 result = resultData.parsedDate;
153 offset = resultData.timeZoneOffset;
160 internal static bool TryParseExactMultiple(String s, String[] formats,
161 DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result)
163 result = DateTime.MinValue;
164 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
166 if (TryParseExactMultiple(s, formats, dtfi, style, ref resultData))
168 result = resultData.parsedDate;
174 internal static bool TryParseExactMultiple(String s, String[] formats,
175 DateTimeFormatInfo dtfi, DateTimeStyles style, ref DateTimeResult result)
179 result.SetFailure(ParseFailureKind.ArgumentNull, nameof(SR.ArgumentNull_String), null, nameof(s));
184 result.SetFailure(ParseFailureKind.ArgumentNull, nameof(SR.ArgumentNull_String), null, nameof(formats));
190 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
194 if (formats.Length == 0)
196 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
200 Debug.Assert(dtfi != null, "dtfi == null");
203 // Do a loop through the provided formats and see if we can parse succesfully in
204 // one of the formats.
206 for (int i = 0; i < formats.Length; i++)
208 if (formats[i] == null || formats[i].Length == 0)
210 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
213 // Create a new result each time to ensure the runs are independent. Carry through
214 // flags from the caller and return the result.
215 DateTimeResult innerResult = new DateTimeResult(); // The buffer to store the parsing result.
217 innerResult.flags = result.flags;
218 if (TryParseExact(s, formats[i], dtfi, style, ref innerResult))
220 result.parsedDate = innerResult.parsedDate;
221 result.timeZoneOffset = innerResult.timeZoneOffset;
225 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
229 ////////////////////////////////////////////////////////////////////////////
232 // Following is the set of tokens that can be generated from a date
233 // string. Notice that the legal set of trailing separators have been
234 // folded in with the date number, and month name tokens. This set
235 // of tokens is chosen to reduce the number of date parse states.
237 ////////////////////////////////////////////////////////////////////////////
239 internal enum DTT : int
242 NumEnd = 1, // Num[ ]*[\0]
243 NumAmpm = 2, // Num[ ]+AmPm
244 NumSpace = 3, // Num[ ]+^[Dsep|Tsep|'0\']
245 NumDatesep = 4, // Num[ ]*Dsep
246 NumTimesep = 5, // Num[ ]*Tsep
247 MonthEnd = 6, // Month[ ]*'\0'
248 MonthSpace = 7, // Month[ ]+^[Dsep|Tsep|'\0']
249 MonthDatesep = 8, // Month[ ]*Dsep
250 NumDatesuff = 9, // Month[ ]*DSuff
251 NumTimesuff = 10, // Month[ ]*TSuff
252 DayOfWeek = 11, // Day of week name
253 YearSpace = 12, // Year+^[Dsep|Tsep|'0\']
254 YearDateSep = 13, // Year+Dsep
255 YearEnd = 14, // Year+['\0']
256 TimeZone = 15, // timezone name
257 Era = 16, // era name
258 NumUTCTimeMark = 17, // Num + 'Z'
259 // When you add a new token which will be in the
260 // state table, add it after NumLocalTimeMark.
262 NumLocalTimeMark = 19, // Num + 'T'
274 ////////////////////////////////////////////////////////////////////////////
276 // DateTime parsing state enumeration (DS.*)
278 ////////////////////////////////////////////////////////////////////////////
283 N = 1, // have one number
284 NN = 2, // have two numbers
286 // The following are known to be part of a date
288 D_Nd = 3, // date string: have number followed by date separator
289 D_NN = 4, // date string: have two numbers
290 D_NNd = 5, // date string: have two numbers followed by date separator
292 D_M = 6, // date string: have a month
293 D_MN = 7, // date string: have a month and a number
294 D_NM = 8, // date string: have a number and a month
295 D_MNd = 9, // date string: have a month and number followed by date separator
296 D_NDS = 10, // date string: have one number followed a date suffix.
298 D_Y = 11, // date string: have a year.
299 D_YN = 12, // date string: have a year and a number
300 D_YNd = 13, // date string: have a year and a number and a date separator
301 D_YM = 14, // date string: have a year and a month
302 D_YMd = 15, // date string: have a year and a month and a date separator
303 D_S = 16, // have numbers followed by a date suffix.
304 T_S = 17, // have numbers followed by a time suffix.
306 // The following are known to be part of a time
308 T_Nt = 18, // have num followed by time separator
309 T_NNt = 19, // have two numbers followed by time separator
314 // The following are terminal states. These all have an action
315 // associated with them; and transition back to BEGIN.
317 DX_NN = 21, // day from two numbers
318 DX_NNN = 22, // day from three numbers
319 DX_MN = 23, // day from month and one number
320 DX_NM = 24, // day from month and one number
321 DX_MNN = 25, // day from month and two numbers
322 DX_DS = 26, // a set of date suffixed numbers.
323 DX_DSN = 27, // day from date suffixes and one number.
324 DX_NDS = 28, // day from one number and date suffixes .
325 DX_NNDS = 29, // day from one number and date suffixes .
327 DX_YNN = 30, // date string: have a year and two number
328 DX_YMN = 31, // date string: have a year, a month, and a number.
329 DX_YN = 32, // date string: have a year and one number
330 DX_YM = 33, // date string: have a year, a month.
331 TX_N = 34, // time from one number (must have ampm)
332 TX_NN = 35, // time from two numbers
333 TX_NNN = 36, // time from three numbers
334 TX_TS = 37, // a set of time suffixed numbers.
338 ////////////////////////////////////////////////////////////////////////////
340 // NOTE: The following state machine table is dependent on the order of the
341 // DS and DTT enumerations.
343 // For each non terminal state, the following table defines the next state
344 // for each given date token type.
346 ////////////////////////////////////////////////////////////////////////////
348 // End NumEnd NumAmPm NumSpace NumDaySep NumTimesep MonthEnd MonthSpace MonthDSep NumDateSuff NumTimeSuff DayOfWeek YearSpace YearDateSep YearEnd TimeZone Era UTCTimeMark
349 private static DS[][] dateParsingStates = {
350 // DS.BEGIN // DS.BEGIN
351 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},
354 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},
357 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},
359 // DS.D_Nd // DS.D_Nd
360 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},
362 // DS.D_NN // DS.D_NN
363 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},
365 // DS.D_NNd // DS.D_NNd
366 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},
369 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},
371 // DS.D_MN // DS.D_MN
372 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},
374 // DS.D_NM // DS.D_NM
375 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},
377 // DS.D_MNd // DS.D_MNd
378 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},
380 // DS.D_NDS, // DS.D_NDS,
381 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},
384 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},
386 // DS.D_YN // DS.D_YN
387 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},
389 // DS.D_YNd // DS.D_YNd
390 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},
392 // DS.D_YM // DS.D_YM
393 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},
395 // DS.D_YMd // DS.D_YMd
396 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},
399 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},
402 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},
404 // DS.T_Nt // DS.T_Nt
405 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},
407 // DS.T_NNt // DS.T_NNt
408 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},
410 // End NumEnd NumAmPm NumSpace NumDaySep NumTimesep MonthEnd MonthSpace MonthDSep NumDateSuff NumTimeSuff DayOfWeek YearSpace YearDateSep YearEnd TimeZone Era UTCMark
412 internal const String GMTName = "GMT";
413 internal const String ZuluName = "Z";
416 // Search from the index of str at str.Index to see if the target string exists in the str.
418 private static bool MatchWord(ref __DTString str, String target)
420 int length = target.Length;
421 if (length > (str.Value.Length - str.Index))
426 if (str.CompareInfo.Compare(str.Value, str.Index, length,
427 target, 0, length, CompareOptions.IgnoreCase) != 0)
432 int nextCharIndex = str.Index + target.Length;
434 if (nextCharIndex < str.Value.Length)
436 char nextCh = str.Value[nextCharIndex];
437 if (Char.IsLetter(nextCh))
442 str.Index = nextCharIndex;
443 if (str.Index < str.len)
445 str.m_current = str.Value[str.Index];
453 // Check the word at the current index to see if it matches GMT name or Zulu name.
455 private static bool GetTimeZoneName(ref __DTString str)
457 if (MatchWord(ref str, GMTName))
462 if (MatchWord(ref str, ZuluName))
470 internal static bool IsDigit(char ch)
472 return (ch >= '0' && ch <= '9');
476 /*=================================ParseFraction==========================
477 **Action: Starting at the str.Index, which should be a decimal symbol.
478 ** if the current character is a digit, parse the remaining
479 ** numbers as fraction. For example, if the sub-string starting at str.Index is "123", then
480 ** the method will return 0.123
481 **Returns: The fraction number.
483 ** str the parsing string
485 ============================================================================*/
487 private static bool ParseFraction(ref __DTString str, out double result)
490 double decimalBase = 0.1;
494 && IsDigit(ch = str.m_current))
496 result += (ch - '0') * decimalBase;
503 /*=================================ParseTimeZone==========================
504 **Action: Parse the timezone offset in the following format:
505 ** "+8", "+08", "+0800", "+0800"
506 ** This method is used by DateTime.Parse().
507 **Returns: The TimeZone offset.
509 ** str the parsing string
511 ** FormatException if invalid timezone format is found.
512 ============================================================================*/
514 private static bool ParseTimeZone(ref __DTString str, ref TimeSpan result)
516 // The hour/minute offset for timezone.
518 int minuteOffset = 0;
521 // Consume the +/- character that has already been read
522 sub = str.GetSubString();
527 char offsetChar = sub[0];
528 if (offsetChar != '+' && offsetChar != '-')
532 str.ConsumeSubString(sub);
534 sub = str.GetSubString();
535 if (sub.type != DTSubStringType.Number)
539 int value = sub.value;
540 int length = sub.length;
541 if (length == 1 || length == 2)
543 // Parsing "+8" or "+08"
545 str.ConsumeSubString(sub);
546 // See if we have minutes
547 sub = str.GetSubString();
548 if (sub.length == 1 && sub[0] == ':')
550 // Parsing "+8:00" or "+08:00"
551 str.ConsumeSubString(sub);
552 sub = str.GetSubString();
553 if (sub.type != DTSubStringType.Number || sub.length < 1 || sub.length > 2)
557 minuteOffset = sub.value;
558 str.ConsumeSubString(sub);
561 else if (length == 3 || length == 4)
563 // Parsing "+800" or "+0800"
564 hourOffset = value / 100;
565 minuteOffset = value % 100;
566 str.ConsumeSubString(sub);
570 // Wrong number of digits
573 Debug.Assert(hourOffset >= 0 && hourOffset <= 99, "hourOffset >= 0 && hourOffset <= 99");
574 Debug.Assert(minuteOffset >= 0 && minuteOffset <= 99, "minuteOffset >= 0 && minuteOffset <= 99");
575 if (minuteOffset < 0 || minuteOffset >= 60)
580 result = new TimeSpan(hourOffset, minuteOffset, 0);
581 if (offsetChar == '-')
583 result = result.Negate();
588 // This is the helper function to handle timezone in string in the format like +/-0800
589 private static bool HandleTimeZone(ref __DTString str, ref DateTimeResult result)
591 if ((str.Index < str.len - 1))
593 char nextCh = str.Value[str.Index];
594 // Skip whitespace, but don't update the index unless we find a time zone marker
595 int whitespaceCount = 0;
596 while (Char.IsWhiteSpace(nextCh) && str.Index + whitespaceCount < str.len - 1)
599 nextCh = str.Value[str.Index + whitespaceCount];
601 if (nextCh == '+' || nextCh == '-')
603 str.Index += whitespaceCount;
604 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
606 // Should not have two timezone offsets.
607 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
610 result.flags |= ParseFlags.TimeZoneUsed;
611 if (!ParseTimeZone(ref str, ref result.timeZoneOffset))
613 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
622 // This is the lexer. Check the character at the current index, and put the found token in dtok and
623 // some raw date/time information in raw.
625 private static Boolean Lex(DS dps, ref __DTString str, ref DateTimeToken dtok, ref DateTimeRawInfo raw, ref DateTimeResult result, ref DateTimeFormatInfo dtfi, DateTimeStyles styles)
629 int indexBeforeSeparator;
630 char charBeforeSeparator;
633 dtok.dtt = DTT.Unk; // Assume the token is unkown.
635 str.GetRegularToken(out tokenType, out tokenValue, dtfi);
638 // Builds with _LOGGING defined (x86dbg, amd64chk, etc) support tracing
639 // Set the following internal-only/unsupported environment variables to enable DateTime tracing to the console:
641 // COMPlus_LogEnable=1
642 // COMPlus_LogToConsole=1
643 // COMPlus_LogLevel=9
644 // COMPlus_ManagedLogFacility=0x00001000
647 BCLDebug.Trace("DATETIME", "[DATETIME] Lex({0})\tpos:{1}({2}), {3}, DS.{4}", Hex(str.Value),
648 str.Index, Hex(str.m_current), tokenType, dps);
652 // Look at the regular token.
655 case TokenType.NumberToken:
656 case TokenType.YearNumberToken:
657 if (raw.numCount == 3 || tokenValue == -1)
659 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
660 LexTraceExit("0010", dps);
666 // If the previous parsing state is DS.T_NNt (like 12:01), and we got another number,
667 // so we will have a terminal state DS.TX_NNN (like 12:01:02).
668 // If the previous parsing state is DS.T_Nt (like 12:), and we got another number,
669 // so we will have a terminal state DS.TX_NN (like 12:01).
671 // Look ahead to see if the following character is a decimal point or timezone offset.
672 // This enables us to parse time in the forms of:
673 // "11:22:33.1234" or "11:22:33-08".
676 if ((str.Index < str.len - 1))
678 char nextCh = str.Value[str.Index];
681 // While ParseFraction can fail, it just means that there were no digits after
682 // the dot. In this case ParseFraction just removes the dot. This is actually
683 // valid for cultures like Albanian, that join the time marker to the time with
684 // with a dot: e.g. "9:03.MD"
685 ParseFraction(ref str, out raw.fraction);
689 if (dps == DS.T_NNt || dps == DS.T_Nt)
691 if ((str.Index < str.len - 1))
693 if (false == HandleTimeZone(ref str, ref result))
695 LexTraceExit("0020 (value like \"12:01\" or \"12:\" followed by a non-TZ number", dps);
701 dtok.num = tokenValue;
702 if (tokenType == TokenType.YearNumberToken)
706 raw.year = tokenValue;
708 // If we have number which has 3 or more digits (like "001" or "0001"),
709 // we assume this number is a year. Save the currnet raw.numCount in
712 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
714 case TokenType.SEP_End:
715 dtok.dtt = DTT.YearEnd;
717 case TokenType.SEP_Am:
718 case TokenType.SEP_Pm:
719 if (raw.timeMark == TM.NotSet)
721 raw.timeMark = (sep == TokenType.SEP_Am ? TM.AM : TM.PM);
722 dtok.dtt = DTT.YearSpace;
726 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
727 LexTraceExit("0030 (TM.AM/TM.PM Happened more than 1x)", dps);
730 case TokenType.SEP_Space:
731 dtok.dtt = DTT.YearSpace;
733 case TokenType.SEP_Date:
734 dtok.dtt = DTT.YearDateSep;
736 case TokenType.SEP_Time:
737 if (!raw.hasSameDateAndTimeSeparators)
739 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
740 LexTraceExit("0040 (Invalid separator after number)", dps);
744 // we have the date and time separators are same and getting a year number, then change the token to YearDateSep as
745 // we are sure we are not parsing time.
746 dtok.dtt = DTT.YearDateSep;
748 case TokenType.SEP_DateOrOffset:
749 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
750 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
751 if ((dateParsingStates[(int)dps][(int)DTT.YearDateSep] == DS.ERROR)
752 && (dateParsingStates[(int)dps][(int)DTT.YearSpace] > DS.ERROR))
754 str.Index = indexBeforeSeparator;
755 str.m_current = charBeforeSeparator;
756 dtok.dtt = DTT.YearSpace;
760 dtok.dtt = DTT.YearDateSep;
763 case TokenType.SEP_YearSuff:
764 case TokenType.SEP_MonthSuff:
765 case TokenType.SEP_DaySuff:
766 dtok.dtt = DTT.NumDatesuff;
769 case TokenType.SEP_HourSuff:
770 case TokenType.SEP_MinuteSuff:
771 case TokenType.SEP_SecondSuff:
772 dtok.dtt = DTT.NumTimesuff;
776 // Invalid separator after number number.
777 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
778 LexTraceExit("0040 (Invalid separator after number)", dps);
782 // Found the token already. Return now.
784 LexTraceExit("0050 (success)", dps);
787 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
788 LexTraceExit("0060", dps);
791 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
794 // Note here we check if the numCount is less than three.
795 // When we have more than three numbers, it will be caught as error in the state machine.
797 case TokenType.SEP_End:
798 dtok.dtt = DTT.NumEnd;
799 raw.AddNumber(dtok.num);
801 case TokenType.SEP_Am:
802 case TokenType.SEP_Pm:
803 if (raw.timeMark == TM.NotSet)
805 raw.timeMark = (sep == TokenType.SEP_Am ? TM.AM : TM.PM);
806 dtok.dtt = DTT.NumAmpm;
807 // Fix AM/PM parsing case, e.g. "1/10 5 AM"
810 if (!ProcessTerminaltState(DS.DX_NN, ref result, ref styles, ref raw, dtfi))
816 raw.AddNumber(dtok.num);
820 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
823 if (dps == DS.T_NNt || dps == DS.T_Nt)
825 if (false == HandleTimeZone(ref str, ref result))
827 LexTraceExit("0070 (HandleTimeZone returned false)", dps);
832 case TokenType.SEP_Space:
833 dtok.dtt = DTT.NumSpace;
834 raw.AddNumber(dtok.num);
836 case TokenType.SEP_Date:
837 dtok.dtt = DTT.NumDatesep;
838 raw.AddNumber(dtok.num);
840 case TokenType.SEP_DateOrOffset:
841 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
842 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
843 if ((dateParsingStates[(int)dps][(int)DTT.NumDatesep] == DS.ERROR)
844 && (dateParsingStates[(int)dps][(int)DTT.NumSpace] > DS.ERROR))
846 str.Index = indexBeforeSeparator;
847 str.m_current = charBeforeSeparator;
848 dtok.dtt = DTT.NumSpace;
852 dtok.dtt = DTT.NumDatesep;
854 raw.AddNumber(dtok.num);
856 case TokenType.SEP_Time:
857 if (raw.hasSameDateAndTimeSeparators &&
858 (dps == DS.D_Y || dps == DS.D_YN || dps == DS.D_YNd || dps == DS.D_YM || dps == DS.D_YMd))
860 // we are parsing a date and we have the time separator same as date separator, so we mark the token as date separator
861 dtok.dtt = DTT.NumDatesep;
862 raw.AddNumber(dtok.num);
865 dtok.dtt = DTT.NumTimesep;
866 raw.AddNumber(dtok.num);
868 case TokenType.SEP_YearSuff:
871 dtok.num = dtfi.Calendar.ToFourDigitYear(tokenValue);
873 catch (ArgumentOutOfRangeException e)
875 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), e);
876 LexTraceExit("0075 (Calendar.ToFourDigitYear failed)", dps);
879 dtok.dtt = DTT.NumDatesuff;
882 case TokenType.SEP_MonthSuff:
883 case TokenType.SEP_DaySuff:
884 dtok.dtt = DTT.NumDatesuff;
887 case TokenType.SEP_HourSuff:
888 case TokenType.SEP_MinuteSuff:
889 case TokenType.SEP_SecondSuff:
890 dtok.dtt = DTT.NumTimesuff;
893 case TokenType.SEP_LocalTimeMark:
894 dtok.dtt = DTT.NumLocalTimeMark;
895 raw.AddNumber(dtok.num);
898 // Invalid separator after number number.
899 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
900 LexTraceExit("0080", dps);
904 case TokenType.HebrewNumber:
905 if (tokenValue >= 100)
907 // This is a year number
910 raw.year = tokenValue;
912 // If we have number which has 3 or more digits (like "001" or "0001"),
913 // we assume this number is a year. Save the currnet raw.numCount in
916 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
918 case TokenType.SEP_End:
919 dtok.dtt = DTT.YearEnd;
921 case TokenType.SEP_Space:
922 dtok.dtt = DTT.YearSpace;
924 case TokenType.SEP_DateOrOffset:
925 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
926 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
927 if (dateParsingStates[(int)dps][(int)DTT.YearSpace] > DS.ERROR)
929 str.Index = indexBeforeSeparator;
930 str.m_current = charBeforeSeparator;
931 dtok.dtt = DTT.YearSpace;
936 // Invalid separator after number number.
937 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
938 LexTraceExit("0090", dps);
944 // Invalid separator after number number.
945 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
946 LexTraceExit("0100", dps);
952 // This is a day number
953 dtok.num = tokenValue;
954 raw.AddNumber(dtok.num);
956 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
959 // Note here we check if the numCount is less than three.
960 // When we have more than three numbers, it will be caught as error in the state machine.
962 case TokenType.SEP_End:
963 dtok.dtt = DTT.NumEnd;
965 case TokenType.SEP_Space:
966 case TokenType.SEP_Date:
967 dtok.dtt = DTT.NumDatesep;
969 case TokenType.SEP_DateOrOffset:
970 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
971 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
972 if ((dateParsingStates[(int)dps][(int)DTT.NumDatesep] == DS.ERROR)
973 && (dateParsingStates[(int)dps][(int)DTT.NumSpace] > DS.ERROR))
975 str.Index = indexBeforeSeparator;
976 str.m_current = charBeforeSeparator;
977 dtok.dtt = DTT.NumSpace;
981 dtok.dtt = DTT.NumDatesep;
985 // Invalid separator after number number.
986 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
987 LexTraceExit("0110", dps);
992 case TokenType.DayOfWeekToken:
993 if (raw.dayOfWeek == -1)
996 // This is a day of week name.
998 raw.dayOfWeek = tokenValue;
999 dtok.dtt = DTT.DayOfWeek;
1003 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1004 LexTraceExit("0120 (DayOfWeek seen more than 1x)", dps);
1008 case TokenType.MonthToken:
1009 if (raw.month == -1)
1012 // This is a month name
1014 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
1016 case TokenType.SEP_End:
1017 dtok.dtt = DTT.MonthEnd;
1019 case TokenType.SEP_Space:
1020 dtok.dtt = DTT.MonthSpace;
1022 case TokenType.SEP_Date:
1023 dtok.dtt = DTT.MonthDatesep;
1025 case TokenType.SEP_Time:
1026 if (!raw.hasSameDateAndTimeSeparators)
1028 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1029 LexTraceExit("0130 (Invalid separator after month name)", dps);
1033 // we have the date and time separators are same and getting a Month name, then change the token to MonthDatesep as
1034 // we are sure we are not parsing time.
1035 dtok.dtt = DTT.MonthDatesep;
1037 case TokenType.SEP_DateOrOffset:
1038 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
1039 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
1040 if ((dateParsingStates[(int)dps][(int)DTT.MonthDatesep] == DS.ERROR)
1041 && (dateParsingStates[(int)dps][(int)DTT.MonthSpace] > DS.ERROR))
1043 str.Index = indexBeforeSeparator;
1044 str.m_current = charBeforeSeparator;
1045 dtok.dtt = DTT.MonthSpace;
1049 dtok.dtt = DTT.MonthDatesep;
1053 //Invalid separator after month name
1054 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1055 LexTraceExit("0130 (Invalid separator after month name)", dps);
1058 raw.month = tokenValue;
1062 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1063 LexTraceExit("0140 (MonthToken seen more than 1x)", dps);
1067 case TokenType.EraToken:
1068 if (result.era != -1)
1070 result.era = tokenValue;
1075 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1076 LexTraceExit("0150 (EraToken seen when result.era already set)", dps);
1080 case TokenType.JapaneseEraToken:
1081 // Special case for Japanese. We allow Japanese era name to be used even if the calendar is not Japanese Calendar.
1082 result.calendar = JapaneseCalendar.GetDefaultInstance();
1083 dtfi = DateTimeFormatInfo.GetJapaneseCalendarDTFI();
1084 if (result.era != -1)
1086 result.era = tokenValue;
1091 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1092 LexTraceExit("0160 (JapaneseEraToken seen when result.era already set)", dps);
1096 case TokenType.TEraToken:
1097 result.calendar = TaiwanCalendar.GetDefaultInstance();
1098 dtfi = DateTimeFormatInfo.GetTaiwanCalendarDTFI();
1099 if (result.era != -1)
1101 result.era = tokenValue;
1106 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1107 LexTraceExit("0170 (TEraToken seen when result.era already set)", dps);
1111 case TokenType.TimeZoneToken:
1113 // This is a timezone designator
1115 // NOTENOTE : for now, we only support "GMT" and "Z" (for Zulu time).
1117 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
1119 // Should not have two timezone offsets.
1120 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1121 LexTraceExit("0180 (seen GMT or Z more than 1x)", dps);
1124 dtok.dtt = DTT.TimeZone;
1125 result.flags |= ParseFlags.TimeZoneUsed;
1126 result.timeZoneOffset = new TimeSpan(0);
1127 result.flags |= ParseFlags.TimeZoneUtc;
1129 case TokenType.EndOfString:
1132 case TokenType.DateWordToken:
1133 case TokenType.IgnorableSymbol:
1134 // Date words and ignorable symbols can just be skipped over
1138 if (raw.timeMark == TM.NotSet)
1140 raw.timeMark = (TM)tokenValue;
1144 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1145 LexTraceExit("0190 (AM/PM timeMark already set)", dps);
1149 case TokenType.UnknownToken:
1150 if (Char.IsLetter(str.m_current))
1152 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_UnknowDateTimeWord), str.Index);
1153 LexTraceExit("0200", dps);
1157 if ((str.m_current == '-' || str.m_current == '+') && ((result.flags & ParseFlags.TimeZoneUsed) == 0))
1159 Int32 originalIndex = str.Index;
1160 if (ParseTimeZone(ref str, ref result.timeZoneOffset))
1162 result.flags |= ParseFlags.TimeZoneUsed;
1163 LexTraceExit("0220 (success)", dps);
1168 // Time zone parse attempt failed. Fall through to punctuation handling.
1169 str.Index = originalIndex;
1173 // Visual Basic implements string to date conversions on top of DateTime.Parse:
1174 // CDate("#10/10/95#")
1176 if (VerifyValidPunctuation(ref str))
1178 LexTraceExit("0230 (success)", dps);
1182 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1183 LexTraceExit("0240", dps);
1187 LexTraceExit("0250 (success)", dps);
1191 private static Boolean VerifyValidPunctuation(ref __DTString str)
1193 // Compatability Behavior. Allow trailing nulls and surrounding hashes
1194 Char ch = str.Value[str.Index];
1197 bool foundStart = false;
1198 bool foundEnd = false;
1199 for (int i = 0; i < str.len; i++)
1208 // Having more than two hashes is invalid
1221 else if (ch == '\0')
1223 // Allow nulls only at the end
1229 else if ((!Char.IsWhiteSpace(ch)))
1231 // Anthyhing other than whitespace outside hashes is invalid
1232 if (!foundStart || foundEnd)
1240 // The has was un-paired
1243 // Valid Hash usage: eat the hash and continue.
1247 else if (ch == '\0')
1249 for (int i = str.Index; i < str.len; i++)
1251 if (str.Value[i] != '\0')
1253 // Nulls are only valid if they are the only trailing character
1257 // Move to the end of the string
1258 str.Index = str.len;
1264 private const int ORDER_YMD = 0; // The order of date is Year/Month/Day.
1265 private const int ORDER_MDY = 1; // The order of date is Month/Day/Year.
1266 private const int ORDER_DMY = 2; // The order of date is Day/Month/Year.
1267 private const int ORDER_YDM = 3; // The order of date is Year/Day/Month
1268 private const int ORDER_YM = 4; // Year/Month order.
1269 private const int ORDER_MY = 5; // Month/Year order.
1270 private const int ORDER_MD = 6; // Month/Day order.
1271 private const int ORDER_DM = 7; // Day/Month order.
1274 // Decide the year/month/day order from the datePattern.
1276 // Return 0 for YMD, 1 for MDY, 2 for DMY, otherwise -1.
1278 private static Boolean GetYearMonthDayOrder(String datePattern, DateTimeFormatInfo dtfi, out int order)
1281 int monthOrder = -1;
1285 bool inQuote = false;
1287 for (int i = 0; i < datePattern.Length && orderCount < 3; i++)
1289 char ch = datePattern[i];
1290 if (ch == '\\' || ch == '%')
1293 continue; // Skip next character that is escaped by this backslash
1296 if (ch == '\'' || ch == '"')
1305 yearOrder = orderCount++;
1308 // Skip all year pattern charaters.
1310 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'y'; i++)
1317 monthOrder = orderCount++;
1319 // Skip all month pattern characters.
1321 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'M'; i++)
1328 int patternCount = 1;
1330 // Skip all day pattern characters.
1332 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'd'; i++)
1337 // Make sure this is not "ddd" or "dddd", which means day of week.
1339 if (patternCount <= 2)
1341 dayOrder = orderCount++;
1347 if (yearOrder == 0 && monthOrder == 1 && dayOrder == 2)
1352 if (monthOrder == 0 && dayOrder == 1 && yearOrder == 2)
1357 if (dayOrder == 0 && monthOrder == 1 && yearOrder == 2)
1362 if (yearOrder == 0 && dayOrder == 1 && monthOrder == 2)
1372 // Decide the year/month order from the pattern.
1374 // Return 0 for YM, 1 for MY, otherwise -1.
1376 private static Boolean GetYearMonthOrder(String pattern, DateTimeFormatInfo dtfi, out int order)
1379 int monthOrder = -1;
1382 bool inQuote = false;
1383 for (int i = 0; i < pattern.Length && orderCount < 2; i++)
1385 char ch = pattern[i];
1386 if (ch == '\\' || ch == '%')
1389 continue; // Skip next character that is escaped by this backslash
1392 if (ch == '\'' || ch == '"')
1401 yearOrder = orderCount++;
1404 // Skip all year pattern charaters.
1406 for (; i + 1 < pattern.Length && pattern[i + 1] == 'y'; i++)
1412 monthOrder = orderCount++;
1414 // Skip all month pattern characters.
1416 for (; i + 1 < pattern.Length && pattern[i + 1] == 'M'; i++)
1423 if (yearOrder == 0 && monthOrder == 1)
1428 if (monthOrder == 0 && yearOrder == 1)
1438 // Decide the month/day order from the pattern.
1440 // Return 0 for MD, 1 for DM, otherwise -1.
1442 private static Boolean GetMonthDayOrder(String pattern, DateTimeFormatInfo dtfi, out int order)
1444 int monthOrder = -1;
1448 bool inQuote = false;
1449 for (int i = 0; i < pattern.Length && orderCount < 2; i++)
1451 char ch = pattern[i];
1452 if (ch == '\\' || ch == '%')
1455 continue; // Skip next character that is escaped by this backslash
1458 if (ch == '\'' || ch == '"')
1467 int patternCount = 1;
1469 // Skip all day pattern charaters.
1471 for (; i + 1 < pattern.Length && pattern[i + 1] == 'd'; i++)
1477 // Make sure this is not "ddd" or "dddd", which means day of week.
1479 if (patternCount <= 2)
1481 dayOrder = orderCount++;
1486 monthOrder = orderCount++;
1488 // Skip all month pattern characters.
1490 for (; i + 1 < pattern.Length && pattern[i + 1] == 'M'; i++)
1497 if (monthOrder == 0 && dayOrder == 1)
1502 if (dayOrder == 0 && monthOrder == 1)
1512 // Adjust the two-digit year if necessary.
1514 private static bool TryAdjustYear(ref DateTimeResult result, int year, out int adjustedYear)
1520 // the Calendar classes need some real work. Many of the calendars that throw
1521 // don't implement a fast/non-allocating (and non-throwing) IsValid{Year|Day|Month} method.
1522 // we are making a targeted try/catch fix in the in-place release but will revisit this code
1523 // in the next side-by-side release.
1524 year = result.calendar.ToFourDigitYear(year);
1526 catch (ArgumentOutOfRangeException)
1532 adjustedYear = year;
1536 private static bool SetDateYMD(ref DateTimeResult result, int year, int month, int day)
1538 // Note, longer term these checks should be done at the end of the parse. This current
1539 // way of checking creates order dependence with parsing the era name.
1540 if (result.calendar.IsValidDay(year, month, day, result.era))
1542 result.SetDate(year, month, day); // YMD
1548 private static bool SetDateMDY(ref DateTimeResult result, int month, int day, int year)
1550 return (SetDateYMD(ref result, year, month, day));
1553 private static bool SetDateDMY(ref DateTimeResult result, int day, int month, int year)
1555 return (SetDateYMD(ref result, year, month, day));
1558 private static bool SetDateYDM(ref DateTimeResult result, int year, int day, int month)
1560 return (SetDateYMD(ref result, year, month, day));
1563 private static void GetDefaultYear(ref DateTimeResult result, ref DateTimeStyles styles)
1565 result.Year = result.calendar.GetYear(GetDateTimeNow(ref result, ref styles));
1566 result.flags |= ParseFlags.YearDefault;
1569 // Processing teriminal case: DS.DX_NN
1570 private static Boolean GetDayOfNN(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1572 if ((result.flags & ParseFlags.HaveDate) != 0)
1574 // Multiple dates in the input string
1575 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1579 int n1 = raw.GetNumber(0);
1580 int n2 = raw.GetNumber(1);
1582 GetDefaultYear(ref result, ref styles);
1585 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out order))
1587 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1591 if (order == ORDER_MD)
1593 if (SetDateYMD(ref result, result.Year, n1, n2)) // MD
1595 result.flags |= ParseFlags.HaveDate;
1602 if (SetDateYMD(ref result, result.Year, n2, n1)) // DM
1604 result.flags |= ParseFlags.HaveDate;
1608 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1612 // Processing teriminal case: DS.DX_NNN
1613 private static Boolean GetDayOfNNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1615 if ((result.flags & ParseFlags.HaveDate) != 0)
1617 // Multiple dates in the input string
1618 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1622 int n1 = raw.GetNumber(0);
1623 int n2 = raw.GetNumber(1); ;
1624 int n3 = raw.GetNumber(2);
1627 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
1629 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1634 if (order == ORDER_YMD)
1636 if (TryAdjustYear(ref result, n1, out year) && SetDateYMD(ref result, year, n2, n3)) // YMD
1638 result.flags |= ParseFlags.HaveDate;
1642 else if (order == ORDER_MDY)
1644 if (TryAdjustYear(ref result, n3, out year) && SetDateMDY(ref result, n1, n2, year)) // MDY
1646 result.flags |= ParseFlags.HaveDate;
1650 else if (order == ORDER_DMY)
1652 if (TryAdjustYear(ref result, n3, out year) && SetDateDMY(ref result, n1, n2, year)) // DMY
1654 result.flags |= ParseFlags.HaveDate;
1658 else if (order == ORDER_YDM)
1660 if (TryAdjustYear(ref result, n1, out year) && SetDateYDM(ref result, year, n2, n3)) // YDM
1662 result.flags |= ParseFlags.HaveDate;
1666 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1670 private static Boolean GetDayOfMN(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1672 if ((result.flags & ParseFlags.HaveDate) != 0)
1674 // Multiple dates in the input string
1675 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1679 // The interpretation is based on the MonthDayPattern and YearMonthPattern
1681 // MonthDayPattern YearMonthPattern Interpretation
1682 // --------------- ---------------- ---------------
1683 // MMMM dd MMMM yyyy Day
1684 // MMMM dd yyyy MMMM Day
1685 // dd MMMM MMMM yyyy Year
1686 // dd MMMM yyyy MMMM Day
1688 // In the first and last cases, it could be either or neither, but a day is a better default interpretation
1689 // than a 2 digit year.
1692 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder))
1694 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1697 if (monthDayOrder == ORDER_DM)
1700 if (!GetYearMonthOrder(dtfi.YearMonthPattern, dtfi, out yearMonthOrder))
1702 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.YearMonthPattern);
1705 if (yearMonthOrder == ORDER_MY)
1708 if (!TryAdjustYear(ref result, raw.GetNumber(0), out year) || !SetDateYMD(ref result, year, raw.month, 1))
1710 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1717 GetDefaultYear(ref result, ref styles);
1718 if (!SetDateYMD(ref result, result.Year, raw.month, raw.GetNumber(0)))
1720 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1726 ////////////////////////////////////////////////////////////////////////
1728 // Deal with the terminal state for Hebrew Month/Day pattern
1730 ////////////////////////////////////////////////////////////////////////
1732 private static Boolean GetHebrewDayOfNM(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1735 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder))
1737 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1740 result.Month = raw.month;
1741 if (monthDayOrder == ORDER_DM || monthDayOrder == ORDER_MD)
1743 if (result.calendar.IsValidDay(result.Year, result.Month, raw.GetNumber(0), result.era))
1745 result.Day = raw.GetNumber(0);
1749 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1753 private static Boolean GetDayOfNM(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1755 if ((result.flags & ParseFlags.HaveDate) != 0)
1757 // Multiple dates in the input string
1758 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1762 // The interpretation is based on the MonthDayPattern and YearMonthPattern
1764 // MonthDayPattern YearMonthPattern Interpretation
1765 // --------------- ---------------- ---------------
1766 // MMMM dd MMMM yyyy Day
1767 // MMMM dd yyyy MMMM Year
1768 // dd MMMM MMMM yyyy Day
1769 // dd MMMM yyyy MMMM Day
1771 // In the first and last cases, it could be either or neither, but a day is a better default interpretation
1772 // than a 2 digit year.
1775 if (!GetMonthDayOrder(dtfi.MonthDayPattern, dtfi, out monthDayOrder))
1777 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1780 if (monthDayOrder == ORDER_MD)
1783 if (!GetYearMonthOrder(dtfi.YearMonthPattern, dtfi, out yearMonthOrder))
1785 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.YearMonthPattern);
1788 if (yearMonthOrder == ORDER_YM)
1791 if (!TryAdjustYear(ref result, raw.GetNumber(0), out year) || !SetDateYMD(ref result, year, raw.month, 1))
1793 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1800 GetDefaultYear(ref result, ref styles);
1801 if (!SetDateYMD(ref result, result.Year, raw.month, raw.GetNumber(0)))
1803 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1809 private static Boolean GetDayOfMNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1811 if ((result.flags & ParseFlags.HaveDate) != 0)
1813 // Multiple dates in the input string
1814 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1818 int n1 = raw.GetNumber(0);
1819 int n2 = raw.GetNumber(1);
1822 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
1824 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1829 if (order == ORDER_MDY)
1831 if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1833 result.SetDate(year, raw.month, n1); // MDY
1834 result.flags |= ParseFlags.HaveDate;
1837 else if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1839 result.SetDate(year, raw.month, n2); // YMD
1840 result.flags |= ParseFlags.HaveDate;
1844 else if (order == ORDER_YMD)
1846 if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1848 result.SetDate(year, raw.month, n2); // YMD
1849 result.flags |= ParseFlags.HaveDate;
1852 else if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1854 result.SetDate(year, raw.month, n1); // DMY
1855 result.flags |= ParseFlags.HaveDate;
1859 else if (order == ORDER_DMY)
1861 if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1863 result.SetDate(year, raw.month, n1); // DMY
1864 result.flags |= ParseFlags.HaveDate;
1867 else if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1869 result.SetDate(year, raw.month, n2); // YMD
1870 result.flags |= ParseFlags.HaveDate;
1875 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1879 private static Boolean GetDayOfYNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1881 if ((result.flags & ParseFlags.HaveDate) != 0)
1883 // Multiple dates in the input string
1884 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1888 int n1 = raw.GetNumber(0);
1889 int n2 = raw.GetNumber(1);
1890 String pattern = dtfi.ShortDatePattern;
1892 // For compatibility, don't throw if we can't determine the order, but default to YMD instead
1894 if (GetYearMonthDayOrder(pattern, dtfi, out order) && order == ORDER_YDM)
1896 if (SetDateYMD(ref result, raw.year, n2, n1))
1898 result.flags |= ParseFlags.HaveDate;
1899 return true; // Year + DM
1904 if (SetDateYMD(ref result, raw.year, n1, n2))
1906 result.flags |= ParseFlags.HaveDate;
1907 return true; // Year + MD
1910 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1914 private static Boolean GetDayOfNNY(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1916 if ((result.flags & ParseFlags.HaveDate) != 0)
1918 // Multiple dates in the input string
1919 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1923 int n1 = raw.GetNumber(0);
1924 int n2 = raw.GetNumber(1);
1927 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
1929 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1933 if (order == ORDER_MDY || order == ORDER_YMD)
1935 if (SetDateYMD(ref result, raw.year, n1, n2))
1937 result.flags |= ParseFlags.HaveDate;
1938 return true; // MD + Year
1943 if (SetDateYMD(ref result, raw.year, n2, n1))
1945 result.flags |= ParseFlags.HaveDate;
1946 return true; // DM + Year
1949 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1954 private static Boolean GetDayOfYMN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1956 if ((result.flags & ParseFlags.HaveDate) != 0)
1958 // Multiple dates in the input string
1959 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1963 if (SetDateYMD(ref result, raw.year, raw.month, raw.GetNumber(0)))
1965 result.flags |= ParseFlags.HaveDate;
1968 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1972 private static Boolean GetDayOfYN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1974 if ((result.flags & ParseFlags.HaveDate) != 0)
1976 // Multiple dates in the input string
1977 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1981 if (SetDateYMD(ref result, raw.year, raw.GetNumber(0), 1))
1983 result.flags |= ParseFlags.HaveDate;
1986 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1990 private static Boolean GetDayOfYM(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1992 if ((result.flags & ParseFlags.HaveDate) != 0)
1994 // Multiple dates in the input string
1995 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
1999 if (SetDateYMD(ref result, raw.year, raw.month, 1))
2001 result.flags |= ParseFlags.HaveDate;
2004 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2008 private static void AdjustTimeMark(DateTimeFormatInfo dtfi, ref DateTimeRawInfo raw)
2010 // Specail case for culture which uses AM as empty string.
2011 // E.g. af-ZA (0x0436)
2014 // In this case, if we are parsing a string like "2005/09/14 12:23", we will assume this is in AM.
2016 if (raw.timeMark == TM.NotSet)
2018 if (dtfi.AMDesignator != null && dtfi.PMDesignator != null)
2020 if (dtfi.AMDesignator.Length == 0 && dtfi.PMDesignator.Length != 0)
2022 raw.timeMark = TM.AM;
2024 if (dtfi.PMDesignator.Length == 0 && dtfi.AMDesignator.Length != 0)
2026 raw.timeMark = TM.PM;
2033 // Adjust hour according to the time mark.
2035 private static Boolean AdjustHour(ref int hour, TM timeMark)
2037 if (timeMark != TM.NotSet)
2039 if (timeMark == TM.AM)
2041 if (hour < 0 || hour > 12)
2045 hour = (hour == 12) ? 0 : hour;
2049 if (hour < 0 || hour > 23)
2062 private static Boolean GetTimeOfN(DateTimeFormatInfo dtfi, ref DateTimeResult result, ref DateTimeRawInfo raw)
2064 if ((result.flags & ParseFlags.HaveTime) != 0)
2066 // Multiple times in the input string
2067 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2071 // In this case, we need a time mark. Check if so.
2073 if (raw.timeMark == TM.NotSet)
2075 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2078 result.Hour = raw.GetNumber(0);
2079 result.flags |= ParseFlags.HaveTime;
2083 private static Boolean GetTimeOfNN(DateTimeFormatInfo dtfi, ref DateTimeResult result, ref DateTimeRawInfo raw)
2085 Debug.Assert(raw.numCount >= 2, "raw.numCount >= 2");
2086 if ((result.flags & ParseFlags.HaveTime) != 0)
2088 // Multiple times in the input string
2089 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2093 result.Hour = raw.GetNumber(0);
2094 result.Minute = raw.GetNumber(1);
2095 result.flags |= ParseFlags.HaveTime;
2099 private static Boolean GetTimeOfNNN(DateTimeFormatInfo dtfi, ref DateTimeResult result, ref DateTimeRawInfo raw)
2101 if ((result.flags & ParseFlags.HaveTime) != 0)
2103 // Multiple times in the input string
2104 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2107 Debug.Assert(raw.numCount >= 3, "raw.numCount >= 3");
2108 result.Hour = raw.GetNumber(0);
2109 result.Minute = raw.GetNumber(1);
2110 result.Second = raw.GetNumber(2);
2111 result.flags |= ParseFlags.HaveTime;
2116 // Processing terminal state: A Date suffix followed by one number.
2118 private static Boolean GetDateOfDSN(ref DateTimeResult result, ref DateTimeRawInfo raw)
2120 if (raw.numCount != 1 || result.Day != -1)
2122 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2125 result.Day = raw.GetNumber(0);
2129 private static Boolean GetDateOfNDS(ref DateTimeResult result, ref DateTimeRawInfo raw)
2131 if (result.Month == -1)
2133 //Should have a month suffix
2134 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2137 if (result.Year != -1)
2139 // Aleady has a year suffix
2140 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2143 if (!TryAdjustYear(ref result, raw.GetNumber(0), out result.Year))
2145 // the year value is out of range
2146 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2153 private static Boolean GetDateOfNNDS(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2155 // For partial CJK Dates, the only valid formats are with a specified year, followed by two numbers, which
2156 // will be the Month and Day, and with a specified Month, when the numbers are either the year and day or
2157 // day and year, depending on the short date pattern.
2159 if ((result.flags & ParseFlags.HaveYear) != 0)
2161 if (((result.flags & ParseFlags.HaveMonth) == 0) && ((result.flags & ParseFlags.HaveDay) == 0))
2163 if (TryAdjustYear(ref result, raw.year, out result.Year) && SetDateYMD(ref result, result.Year, raw.GetNumber(0), raw.GetNumber(1)))
2169 else if ((result.flags & ParseFlags.HaveMonth) != 0)
2171 if (((result.flags & ParseFlags.HaveYear) == 0) && ((result.flags & ParseFlags.HaveDay) == 0))
2174 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, dtfi, out order))
2176 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
2180 if (order == ORDER_YMD)
2182 if (TryAdjustYear(ref result, raw.GetNumber(0), out year) && SetDateYMD(ref result, year, result.Month, raw.GetNumber(1)))
2189 if (TryAdjustYear(ref result, raw.GetNumber(1), out year) && SetDateYMD(ref result, year, result.Month, raw.GetNumber(0)))
2196 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2201 // A date suffix is found, use this method to put the number into the result.
2203 private static bool ProcessDateTimeSuffix(ref DateTimeResult result, ref DateTimeRawInfo raw, ref DateTimeToken dtok)
2205 switch (dtok.suffix)
2207 case TokenType.SEP_YearSuff:
2208 if ((result.flags & ParseFlags.HaveYear) != 0)
2212 result.flags |= ParseFlags.HaveYear;
2213 result.Year = raw.year = dtok.num;
2215 case TokenType.SEP_MonthSuff:
2216 if ((result.flags & ParseFlags.HaveMonth) != 0)
2220 result.flags |= ParseFlags.HaveMonth;
2221 result.Month = raw.month = dtok.num;
2223 case TokenType.SEP_DaySuff:
2224 if ((result.flags & ParseFlags.HaveDay) != 0)
2228 result.flags |= ParseFlags.HaveDay;
2229 result.Day = dtok.num;
2231 case TokenType.SEP_HourSuff:
2232 if ((result.flags & ParseFlags.HaveHour) != 0)
2236 result.flags |= ParseFlags.HaveHour;
2237 result.Hour = dtok.num;
2239 case TokenType.SEP_MinuteSuff:
2240 if ((result.flags & ParseFlags.HaveMinute) != 0)
2244 result.flags |= ParseFlags.HaveMinute;
2245 result.Minute = dtok.num;
2247 case TokenType.SEP_SecondSuff:
2248 if ((result.flags & ParseFlags.HaveSecond) != 0)
2252 result.flags |= ParseFlags.HaveSecond;
2253 result.Second = dtok.num;
2259 ////////////////////////////////////////////////////////////////////////
2262 // This is used by DateTime.Parse().
2263 // Process the terminal state for the Hebrew calendar parsing.
2265 ////////////////////////////////////////////////////////////////////////
2267 internal static Boolean ProcessHebrewTerminalState(DS dps, ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2269 // The following are accepted terminal state for Hebrew date.
2273 // Deal with the default long/short date format when the year number is ambigous (i.e. year < 100).
2274 raw.year = raw.GetNumber(1);
2275 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2277 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2280 if (!GetDayOfMNN(ref result, ref raw, dtfi))
2286 // Deal with the default long/short date format when the year number is NOT ambigous (i.e. year >= 100).
2287 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2289 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2292 if (!GetDayOfYMN(ref result, ref raw, dtfi))
2298 // When formatting, we only format up to the hundred digit of the Hebrew year, although Hebrew year is now over 5000.
2299 // E.g. if the year is 5763, we only format as 763. so we do the reverse when parsing.
2300 if (raw.year < 1000)
2304 if (!GetDayOfNNY(ref result, ref raw, dtfi))
2308 if (!dtfi.YearMonthAdjustment(ref result.Year, ref raw.month, true))
2310 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2316 // Deal with Month/Day pattern.
2317 GetDefaultYear(ref result, ref styles);
2318 if (!dtfi.YearMonthAdjustment(ref result.Year, ref raw.month, true))
2320 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2323 if (!GetHebrewDayOfNM(ref result, ref raw, dtfi))
2329 // Deal with Year/Month pattern.
2330 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2332 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2335 if (!GetDayOfYM(ref result, ref raw, dtfi))
2341 // Deal hour + AM/PM
2342 if (!GetTimeOfN(dtfi, ref result, ref raw))
2348 if (!GetTimeOfNN(dtfi, ref result, ref raw))
2354 if (!GetTimeOfNNN(dtfi, ref result, ref raw))
2360 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2366 // We have reached a terminal state. Reset the raw num count.
2374 // A terminal state has been reached, call the appropriate function to fill in the parsing result.
2375 // Return true if the state is a terminal state.
2377 internal static Boolean ProcessTerminaltState(DS dps, ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2383 passed = GetDayOfNN(ref result, ref styles, ref raw, dtfi);
2386 passed = GetDayOfNNN(ref result, ref raw, dtfi);
2389 passed = GetDayOfMN(ref result, ref styles, ref raw, dtfi);
2392 passed = GetDayOfNM(ref result, ref styles, ref raw, dtfi);
2395 passed = GetDayOfMNN(ref result, ref raw, dtfi);
2398 // The result has got the correct value. No need to process.
2402 passed = GetDayOfYNN(ref result, ref raw, dtfi);
2405 passed = GetDayOfNNY(ref result, ref raw, dtfi);
2408 passed = GetDayOfYMN(ref result, ref raw, dtfi);
2411 passed = GetDayOfYN(ref result, ref raw, dtfi);
2414 passed = GetDayOfYM(ref result, ref raw, dtfi);
2417 passed = GetTimeOfN(dtfi, ref result, ref raw);
2420 passed = GetTimeOfNN(dtfi, ref result, ref raw);
2423 passed = GetTimeOfNNN(dtfi, ref result, ref raw);
2426 // The result has got the correct value. Nothing to do.
2430 passed = GetDateOfDSN(ref result, ref raw);
2433 passed = GetDateOfNDS(ref result, ref raw);
2436 passed = GetDateOfNNDS(ref result, ref raw, dtfi);
2440 PTSTraceExit(dps, passed);
2449 // We have reached a terminal state. Reset the raw num count.
2456 internal static DateTime Parse(String s, DateTimeFormatInfo dtfi, DateTimeStyles styles)
2458 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
2460 if (TryParse(s, dtfi, styles, ref result))
2462 return result.parsedDate;
2466 throw GetDateTimeParseException(ref result);
2470 internal static DateTime Parse(String s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out TimeSpan offset)
2472 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
2474 result.flags |= ParseFlags.CaptureOffset;
2475 if (TryParse(s, dtfi, styles, ref result))
2477 offset = result.timeZoneOffset;
2478 return result.parsedDate;
2482 throw GetDateTimeParseException(ref result);
2487 internal static bool TryParse(String s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out DateTime result)
2489 result = DateTime.MinValue;
2490 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
2492 if (TryParse(s, dtfi, styles, ref resultData))
2494 result = resultData.parsedDate;
2500 internal static bool TryParse(String s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out DateTime result, out TimeSpan offset)
2502 result = DateTime.MinValue;
2503 offset = TimeSpan.Zero;
2504 DateTimeResult parseResult = new DateTimeResult(); // The buffer to store the parsing result.
2506 parseResult.flags |= ParseFlags.CaptureOffset;
2507 if (TryParse(s, dtfi, styles, ref parseResult))
2509 result = parseResult.parsedDate;
2510 offset = parseResult.timeZoneOffset;
2518 // This is the real method to do the parsing work.
2520 internal static bool TryParse(String s, DateTimeFormatInfo dtfi, DateTimeStyles styles, ref DateTimeResult result)
2524 result.SetFailure(ParseFailureKind.ArgumentNull, nameof(SR.ArgumentNull_String), null, nameof(s));
2529 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2533 Debug.Assert(dtfi != null, "dtfi == null");
2541 // First try the predefined format.
2544 DS dps = DS.BEGIN; // Date Parsing State.
2545 bool reachTerminalState = false;
2547 DateTimeToken dtok = new DateTimeToken(); // The buffer to store the parsing token.
2548 dtok.suffix = TokenType.SEP_Unk;
2549 DateTimeRawInfo raw = new DateTimeRawInfo(); // The buffer to store temporary parsing information.
2552 Int32* numberPointer = stackalloc Int32[3];
2553 raw.Init(numberPointer);
2555 raw.hasSameDateAndTimeSeparators = dtfi.DateSeparator.Equals(dtfi.TimeSeparator, StringComparison.Ordinal);
2557 result.calendar = dtfi.Calendar;
2558 result.era = Calendar.CurrentEra;
2561 // The string to be parsed. Use a __DTString wrapper so that we can trace the index which
2562 // indicates the begining of next token.
2564 __DTString str = new __DTString(s, dtfi);
2569 // The following loop will break out when we reach the end of the str.
2574 // Call the lexer to get the next token.
2576 // If we find a era in Lex(), the era value will be in raw.era.
2577 if (!Lex(dps, ref str, ref dtok, ref raw, ref result, ref dtfi, styles))
2579 TPTraceExit("0000", dps);
2584 // If the token is not unknown, process it.
2585 // Otherwise, just discard it.
2587 if (dtok.dtt != DTT.Unk)
2590 // Check if we got any CJK Date/Time suffix.
2591 // Since the Date/Time suffix tells us the number belongs to year/month/day/hour/minute/second,
2592 // store the number in the appropriate field in the result.
2594 if (dtok.suffix != TokenType.SEP_Unk)
2596 if (!ProcessDateTimeSuffix(ref result, ref raw, ref dtok))
2598 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2599 TPTraceExit("0010", dps);
2603 dtok.suffix = TokenType.SEP_Unk; // Reset suffix to SEP_Unk;
2606 if (dtok.dtt == DTT.NumLocalTimeMark)
2608 if (dps == DS.D_YNd || dps == DS.D_YN)
2610 // Consider this as ISO 8601 format:
2611 // "yyyy-MM-dd'T'HH:mm:ss" 1999-10-31T02:00:00
2612 TPTraceExit("0020", dps);
2613 return (ParseISO8601(ref raw, ref str, styles, ref result));
2617 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2618 TPTraceExit("0030", dps);
2623 if (raw.hasSameDateAndTimeSeparators)
2625 if (dtok.dtt == DTT.YearEnd || dtok.dtt == DTT.YearSpace || dtok.dtt == DTT.YearDateSep)
2627 // When time and date separators are same and we are hitting a year number while the first parsed part of the string was recognized
2628 // 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
2633 if (dps == DS.T_NNt)
2639 bool atEnd = str.AtEnd();
2640 if (dateParsingStates[(int)dps][(int)dtok.dtt] == DS.ERROR || atEnd)
2644 // we have the case of Serbia have dates in forms 'd.M.yyyy.' so we can expect '.' after the date parts.
2645 // changing the token to end with space instead of Date Separator will avoid failing the parsing.
2647 case DTT.YearDateSep: dtok.dtt = atEnd ? DTT.YearEnd : DTT.YearSpace; break;
2648 case DTT.NumDatesep: dtok.dtt = atEnd ? DTT.NumEnd : DTT.NumSpace; break;
2649 case DTT.NumTimesep: dtok.dtt = atEnd ? DTT.NumEnd : DTT.NumSpace; break;
2650 case DTT.MonthDatesep: dtok.dtt = atEnd ? DTT.MonthEnd : DTT.MonthSpace; break;
2656 // Advance to the next state, and continue
2658 dps = dateParsingStates[(int)dps][(int)dtok.dtt];
2660 if (dps == DS.ERROR)
2662 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2663 TPTraceExit("0040 (invalid state transition)", dps);
2666 else if (dps > DS.ERROR)
2668 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseHebrewRule) != 0)
2670 if (!ProcessHebrewTerminalState(dps, ref result, ref styles, ref raw, dtfi))
2672 TPTraceExit("0050 (ProcessHebrewTerminalState)", dps);
2678 if (!ProcessTerminaltState(dps, ref result, ref styles, ref raw, dtfi))
2680 TPTraceExit("0060 (ProcessTerminaltState)", dps);
2684 reachTerminalState = true;
2687 // If we have reached a terminal state, start over from DS.BEGIN again.
2688 // For example, when we parsed "1999-12-23 13:30", we will reach a terminal state at "1999-12-23",
2689 // and we start over so we can continue to parse "12:30".
2694 } while (dtok.dtt != DTT.End && dtok.dtt != DTT.NumEnd && dtok.dtt != DTT.MonthEnd);
2696 if (!reachTerminalState)
2698 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2699 TPTraceExit("0070 (did not reach terminal state)", dps);
2703 AdjustTimeMark(dtfi, ref raw);
2704 if (!AdjustHour(ref result.Hour, raw.timeMark))
2706 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
2707 TPTraceExit("0080 (AdjustHour)", dps);
2711 // Check if the parased string only contains hour/minute/second values.
2712 bool bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1);
2715 // Check if any year/month/day is missing in the parsing string.
2716 // If yes, get the default value from today's date.
2718 if (!CheckDefaultDateTime(ref result, ref result.calendar, styles))
2720 TPTraceExit("0090 (failed to fill in missing year/month/day defaults)", dps);
2724 if (!result.calendar.TryToDateTime(result.Year, result.Month, result.Day,
2725 result.Hour, result.Minute, result.Second, 0, result.era, out time))
2727 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
2728 TPTraceExit("0100 (result.calendar.TryToDateTime)", dps);
2731 if (raw.fraction > 0)
2733 time = time.AddTicks((long)Math.Round(raw.fraction * Calendar.TicksPerSecond));
2737 // We have to check day of week before we adjust to the time zone.
2738 // Otherwise, the value of day of week may change after adjustting to the time zone.
2740 if (raw.dayOfWeek != -1)
2743 // Check if day of week is correct.
2745 if (raw.dayOfWeek != (int)result.calendar.GetDayOfWeek(time))
2747 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDayOfWeek), null);
2748 TPTraceExit("0110 (dayOfWeek check)", dps);
2753 result.parsedDate = time;
2755 if (!DetermineTimeZoneAdjustments(ref result, styles, bTimeOnly))
2757 TPTraceExit("0120 (DetermineTimeZoneAdjustments)", dps);
2760 TPTraceExit("0130 (success)", dps);
2765 // Handles time zone adjustments and sets DateTimeKind values as required by the styles
2766 private static Boolean DetermineTimeZoneAdjustments(ref DateTimeResult result, DateTimeStyles styles, Boolean bTimeOnly)
2768 if ((result.flags & ParseFlags.CaptureOffset) != 0)
2770 // This is a DateTimeOffset parse, so the offset will actually be captured directly, and
2771 // no adjustment is required in most cases
2772 return DateTimeOffsetTimeZonePostProcessing(ref result, styles);
2776 Int64 offsetTicks = result.timeZoneOffset.Ticks;
2778 // the DateTime offset must be within +- 14:00 hours.
2779 if (offsetTicks < DateTimeOffset.MinOffset || offsetTicks > DateTimeOffset.MaxOffset)
2781 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_OffsetOutOfRange), null);
2786 // The flags AssumeUniveral and AssumeLocal only apply when the input does not have a time zone
2787 if ((result.flags & ParseFlags.TimeZoneUsed) == 0)
2789 // If AssumeLocal or AssumeLocal is used, there will always be a kind specified. As in the
2790 // case when a time zone is present, it will default to being local unless AdjustToUniversal
2791 // is present. These comparisons determine whether setting the kind is sufficient, or if a
2792 // time zone adjustment is required. For consistentcy with the rest of parsing, it is desirable
2793 // to fall through to the Adjust methods below, so that there is consist handling of boundary
2794 // cases like wrapping around on time-only dates and temporarily allowing an adjusted date
2795 // to exceed DateTime.MaxValue
2796 if ((styles & DateTimeStyles.AssumeLocal) != 0)
2798 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2800 result.flags |= ParseFlags.TimeZoneUsed;
2801 result.timeZoneOffset = TimeZoneInfo.GetLocalUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime);
2805 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Local);
2809 else if ((styles & DateTimeStyles.AssumeUniversal) != 0)
2811 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2813 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Utc);
2818 result.flags |= ParseFlags.TimeZoneUsed;
2819 result.timeZoneOffset = TimeSpan.Zero;
2824 // No time zone and no Assume flags, so DateTimeKind.Unspecified is fine
2825 Debug.Assert(result.parsedDate.Kind == DateTimeKind.Unspecified, "result.parsedDate.Kind == DateTimeKind.Unspecified");
2830 if (((styles & DateTimeStyles.RoundtripKind) != 0) && ((result.flags & ParseFlags.TimeZoneUtc) != 0))
2832 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Utc);
2836 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2838 return (AdjustTimeZoneToUniversal(ref result));
2840 return (AdjustTimeZoneToLocal(ref result, bTimeOnly));
2843 // Apply validation and adjustments specific to DateTimeOffset
2844 private static Boolean DateTimeOffsetTimeZonePostProcessing(ref DateTimeResult result, DateTimeStyles styles)
2846 // For DateTimeOffset, default to the Utc or Local offset when an offset was not specified by
2847 // the input string.
2848 if ((result.flags & ParseFlags.TimeZoneUsed) == 0)
2850 if ((styles & DateTimeStyles.AssumeUniversal) != 0)
2852 // AssumeUniversal causes the offset to default to zero (0)
2853 result.timeZoneOffset = TimeSpan.Zero;
2857 // AssumeLocal causes the offset to default to Local. This flag is on by default for DateTimeOffset.
2858 result.timeZoneOffset = TimeZoneInfo.GetLocalUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime);
2862 Int64 offsetTicks = result.timeZoneOffset.Ticks;
2864 // there should be no overflow, because the offset can be no more than -+100 hours and the date already
2865 // fits within a DateTime.
2866 Int64 utcTicks = result.parsedDate.Ticks - offsetTicks;
2868 // For DateTimeOffset, both the parsed time and the corresponding UTC value must be within the boundaries
2869 // of a DateTime instance.
2870 if (utcTicks < DateTime.MinTicks || utcTicks > DateTime.MaxTicks)
2872 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_UTCOutOfRange), null);
2876 // the offset must be within +- 14:00 hours.
2877 if (offsetTicks < DateTimeOffset.MinOffset || offsetTicks > DateTimeOffset.MaxOffset)
2879 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_OffsetOutOfRange), null);
2883 // DateTimeOffset should still honor the AdjustToUniversal flag for consistency with DateTime. It means you
2884 // want to return an adjusted UTC value, so store the utcTicks in the DateTime and set the offset to zero
2885 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2887 if (((result.flags & ParseFlags.TimeZoneUsed) == 0) && ((styles & DateTimeStyles.AssumeUniversal) == 0))
2889 // Handle the special case where the timeZoneOffset was defaulted to Local
2890 Boolean toUtcResult = AdjustTimeZoneToUniversal(ref result);
2891 result.timeZoneOffset = TimeSpan.Zero;
2895 // The constructor should always succeed because of the range check earlier in the function
2896 // Althought it is UTC, internally DateTimeOffset does not use this flag
2897 result.parsedDate = new DateTime(utcTicks, DateTimeKind.Utc);
2898 result.timeZoneOffset = TimeSpan.Zero;
2906 // Adjust the specified time to universal time based on the supplied timezone.
2907 // E.g. when parsing "2001/06/08 14:00-07:00",
2908 // the time is 2001/06/08 14:00, and timeZoneOffset = -07:00.
2909 // The result will be "2001/06/08 21:00"
2911 private static Boolean AdjustTimeZoneToUniversal(ref DateTimeResult result)
2913 long resultTicks = result.parsedDate.Ticks;
2914 resultTicks -= result.timeZoneOffset.Ticks;
2915 if (resultTicks < 0)
2917 resultTicks += Calendar.TicksPerDay;
2920 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2922 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_DateOutOfRange), null);
2925 result.parsedDate = new DateTime(resultTicks, DateTimeKind.Utc);
2930 // Adjust the specified time to universal time based on the supplied timezone,
2931 // and then convert to local time.
2932 // E.g. when parsing "2001/06/08 14:00-04:00", and local timezone is GMT-7.
2933 // the time is 2001/06/08 14:00, and timeZoneOffset = -05:00.
2934 // The result will be "2001/06/08 11:00"
2936 private static Boolean AdjustTimeZoneToLocal(ref DateTimeResult result, bool bTimeOnly)
2938 long resultTicks = result.parsedDate.Ticks;
2939 // Convert to local ticks
2940 TimeZoneInfo tz = TimeZoneInfo.Local;
2941 Boolean isAmbiguousLocalDst = false;
2942 if (resultTicks < Calendar.TicksPerDay)
2945 // This is time of day.
2949 resultTicks -= result.timeZoneOffset.Ticks;
2950 // If the time is time of day, use the current timezone offset.
2951 resultTicks += tz.GetUtcOffset(bTimeOnly ? DateTime.Now : result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime).Ticks;
2953 if (resultTicks < 0)
2955 resultTicks += Calendar.TicksPerDay;
2960 // Adjust timezone to GMT.
2961 resultTicks -= result.timeZoneOffset.Ticks;
2962 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2964 // If the result ticks is greater than DateTime.MaxValue, we can not create a DateTime from this ticks.
2965 // In this case, keep using the old code.
2966 resultTicks += tz.GetUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime).Ticks;
2970 // Convert the GMT time to local time.
2971 DateTime utcDt = new DateTime(resultTicks, DateTimeKind.Utc);
2972 Boolean isDaylightSavings = false;
2973 resultTicks += TimeZoneInfo.GetUtcOffsetFromUtc(utcDt, TimeZoneInfo.Local, out isDaylightSavings, out isAmbiguousLocalDst).Ticks;
2976 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2978 result.parsedDate = DateTime.MinValue;
2979 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_DateOutOfRange), null);
2982 result.parsedDate = new DateTime(resultTicks, DateTimeKind.Local, isAmbiguousLocalDst);
2987 // Parse the ISO8601 format string found during Parse();
2990 private static bool ParseISO8601(ref DateTimeRawInfo raw, ref __DTString str, DateTimeStyles styles, ref DateTimeResult result)
2992 if (raw.year < 0 || raw.GetNumber(0) < 0 || raw.GetNumber(1) < 0)
2998 double partSecond = 0;
3000 str.SkipWhiteSpaces();
3001 if (!ParseDigits(ref str, 2, out hour))
3003 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3006 str.SkipWhiteSpaces();
3007 if (!str.Match(':'))
3009 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3012 str.SkipWhiteSpaces();
3013 if (!ParseDigits(ref str, 2, out minute))
3015 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3018 str.SkipWhiteSpaces();
3021 str.SkipWhiteSpaces();
3022 if (!ParseDigits(ref str, 2, out second))
3024 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3029 if (!ParseFraction(ref str, out partSecond))
3031 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3036 str.SkipWhiteSpaces();
3040 char ch = str.GetChar();
3041 if (ch == '+' || ch == '-')
3043 result.flags |= ParseFlags.TimeZoneUsed;
3044 if (!ParseTimeZone(ref str, ref result.timeZoneOffset))
3046 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3050 else if (ch == 'Z' || ch == 'z')
3052 result.flags |= ParseFlags.TimeZoneUsed;
3053 result.timeZoneOffset = TimeSpan.Zero;
3054 result.flags |= ParseFlags.TimeZoneUtc;
3060 str.SkipWhiteSpaces();
3063 if (!VerifyValidPunctuation(ref str))
3065 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3068 str.SkipWhiteSpaces();
3070 if (str.Match('\0'))
3072 if (!VerifyValidPunctuation(ref str))
3074 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3080 // If this is true, there were non-white space characters remaining in the DateTime
3081 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3087 Calendar calendar = GregorianCalendar.GetDefaultInstance();
3088 if (!calendar.TryToDateTime(raw.year, raw.GetNumber(0), raw.GetNumber(1),
3089 hour, minute, second, 0, result.era, out time))
3091 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
3095 time = time.AddTicks((long)Math.Round(partSecond * Calendar.TicksPerSecond));
3096 result.parsedDate = time;
3097 if (!DetermineTimeZoneAdjustments(ref result, styles, false))
3105 ////////////////////////////////////////////////////////////////////////
3108 // Parse the current word as a Hebrew number.
3109 // This is used by DateTime.ParseExact().
3111 ////////////////////////////////////////////////////////////////////////
3113 internal static bool MatchHebrewDigits(ref __DTString str, int digitLen, out int number)
3117 // Create a context object so that we can parse the Hebrew number text character by character.
3118 HebrewNumberParsingContext context = new HebrewNumberParsingContext(0);
3120 // Set this to ContinueParsing so that we will run the following while loop in the first time.
3121 HebrewNumberParsingState state = HebrewNumberParsingState.ContinueParsing;
3123 while (state == HebrewNumberParsingState.ContinueParsing && str.GetNext())
3125 state = HebrewNumber.ParseByChar(str.GetChar(), ref context);
3128 if (state == HebrewNumberParsingState.FoundEndOfHebrewNumber)
3130 // If we have reached a terminal state, update the result and returns.
3131 number = context.result;
3135 // If we run out of the character before reaching FoundEndOfHebrewNumber, or
3136 // the state is InvalidHebrewNumber or ContinueParsing, we fail to match a Hebrew number.
3141 /*=================================ParseDigits==================================
3142 **Action: Parse the number string in __DTString that are formatted using
3143 ** the following patterns:
3144 ** "0", "00", and "000..0"
3145 **Returns: the integer value
3146 **Arguments: str: a __DTString. The parsing will start from the
3147 ** next character after str.Index.
3148 **Exceptions: FormatException if error in parsing number.
3149 ==============================================================================*/
3151 internal static bool ParseDigits(ref __DTString str, int digitLen, out int result)
3155 // 1 really means 1 or 2 for this call
3156 return ParseDigits(ref str, 1, 2, out result);
3160 return ParseDigits(ref str, digitLen, digitLen, out result);
3164 internal static bool ParseDigits(ref __DTString str, int minDigitLen, int maxDigitLen, out int result)
3166 Debug.Assert(minDigitLen > 0, "minDigitLen > 0");
3167 Debug.Assert(maxDigitLen < 9, "maxDigitLen < 9");
3168 Debug.Assert(minDigitLen <= maxDigitLen, "minDigitLen <= maxDigitLen");
3170 int startingIndex = str.Index;
3171 int tokenLength = 0;
3172 while (tokenLength < maxDigitLen)
3174 if (!str.GetNextDigit())
3179 result = result * 10 + str.GetDigit();
3182 if (tokenLength < minDigitLen)
3184 str.Index = startingIndex;
3190 /*=================================ParseFractionExact==================================
3191 **Action: Parse the number string in __DTString that are formatted using
3192 ** the following patterns:
3193 ** "0", "00", and "000..0"
3194 **Returns: the fraction value
3195 **Arguments: str: a __DTString. The parsing will start from the
3196 ** next character after str.Index.
3197 **Exceptions: FormatException if error in parsing number.
3198 ==============================================================================*/
3200 private static bool ParseFractionExact(ref __DTString str, int maxDigitLen, ref double result)
3202 if (!str.GetNextDigit())
3207 result = str.GetDigit();
3210 for (; digitLen < maxDigitLen; digitLen++)
3212 if (!str.GetNextDigit())
3217 result = result * 10 + str.GetDigit();
3220 result = ((double)result / Math.Pow(10, digitLen));
3221 return (digitLen == maxDigitLen);
3224 /*=================================ParseSign==================================
3225 **Action: Parse a positive or a negative sign.
3226 **Returns: true if postive sign. flase if negative sign.
3227 **Arguments: str: a __DTString. The parsing will start from the
3228 ** next character after str.Index.
3229 **Exceptions: FormatException if end of string is encountered or a sign
3230 ** symbol is not found.
3231 ==============================================================================*/
3233 private static bool ParseSign(ref __DTString str, ref bool result)
3237 // A sign symbol ('+' or '-') is expected. However, end of string is encountered.
3240 char ch = str.GetChar();
3251 // A sign symbol ('+' or '-') is expected.
3255 /*=================================ParseTimeZoneOffset==================================
3256 **Action: Parse the string formatted using "z", "zz", "zzz" in DateTime.Format().
3257 **Returns: the TimeSpan for the parsed timezone offset.
3258 **Arguments: str: a __DTString. The parsing will start from the
3259 ** next character after str.Index.
3260 ** len: the repeated number of the "z"
3261 **Exceptions: FormatException if errors in parsing.
3262 ==============================================================================*/
3264 private static bool ParseTimeZoneOffset(ref __DTString str, int len, ref TimeSpan result)
3266 bool isPositive = true;
3268 int minuteOffset = 0;
3274 if (!ParseSign(ref str, ref isPositive))
3278 if (!ParseDigits(ref str, len, out hourOffset))
3284 if (!ParseSign(ref str, ref isPositive))
3289 // Parsing 1 digit will actually parse 1 or 2.
3290 if (!ParseDigits(ref str, 1, out hourOffset))
3298 if (!ParseDigits(ref str, 2, out minuteOffset))
3305 // Since we can not match ':', put the char back.
3307 if (!ParseDigits(ref str, 2, out minuteOffset))
3314 if (minuteOffset < 0 || minuteOffset >= 60)
3319 result = (new TimeSpan(hourOffset, minuteOffset, 0));
3322 result = result.Negate();
3327 /*=================================MatchAbbreviatedMonthName==================================
3328 **Action: Parse the abbreviated month name from string starting at str.Index.
3329 **Returns: A value from 1 to 12 for the first month to the twelveth month.
3330 **Arguments: str: a __DTString. The parsing will start from the
3331 ** next character after str.Index.
3332 **Exceptions: FormatException if an abbreviated month name can not be found.
3333 ==============================================================================*/
3335 private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3337 int maxMatchStrLen = 0;
3342 // Scan the month names (note that some calendars has 13 months) and find
3343 // the matching month name which has the max string length.
3344 // We need to do this because some cultures (e.g. "cs-CZ") which have
3345 // abbreviated month names with the same prefix.
3347 int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3348 for (int i = 1; i <= monthsInYear; i++)
3350 String searchStr = dtfi.GetAbbreviatedMonthName(i);
3351 int matchStrLen = searchStr.Length;
3352 if (dtfi.HasSpacesInMonthNames
3353 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3354 : str.MatchSpecifiedWord(searchStr))
3356 if (matchStrLen > maxMatchStrLen)
3358 maxMatchStrLen = matchStrLen;
3364 // Search leap year form.
3365 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0)
3367 int tempResult = str.MatchLongestWords(dtfi.internalGetLeapYearMonthNames(), ref maxMatchStrLen);
3368 // We found a longer match in the leap year month name. Use this as the result.
3369 // The result from MatchLongestWords is 0 ~ length of word array.
3370 // So we increment the result by one to become the month value.
3371 if (tempResult >= 0)
3373 result = tempResult + 1;
3379 str.Index += (maxMatchStrLen - 1);
3385 /*=================================MatchMonthName==================================
3386 **Action: Parse the month name from string starting at str.Index.
3387 **Returns: A value from 1 to 12 indicating the first month to the twelveth month.
3388 **Arguments: str: a __DTString. The parsing will start from the
3389 ** next character after str.Index.
3390 **Exceptions: FormatException if a month name can not be found.
3391 ==============================================================================*/
3393 private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3395 int maxMatchStrLen = 0;
3400 // Scan the month names (note that some calendars has 13 months) and find
3401 // the matching month name which has the max string length.
3402 // We need to do this because some cultures (e.g. "vi-VN") which have
3403 // month names with the same prefix.
3405 int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3406 for (int i = 1; i <= monthsInYear; i++)
3408 String searchStr = dtfi.GetMonthName(i);
3409 int matchStrLen = searchStr.Length;
3410 if (dtfi.HasSpacesInMonthNames
3411 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3412 : str.MatchSpecifiedWord(searchStr))
3414 if (matchStrLen > maxMatchStrLen)
3416 maxMatchStrLen = matchStrLen;
3422 // Search genitive form.
3423 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0)
3425 int tempResult = str.MatchLongestWords(dtfi.MonthGenitiveNames, ref maxMatchStrLen);
3426 // We found a longer match in the genitive month name. Use this as the result.
3427 // The result from MatchLongestWords is 0 ~ length of word array.
3428 // So we increment the result by one to become the month value.
3429 if (tempResult >= 0)
3431 result = tempResult + 1;
3435 // Search leap year form.
3436 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0)
3438 int tempResult = str.MatchLongestWords(dtfi.internalGetLeapYearMonthNames(), ref maxMatchStrLen);
3439 // We found a longer match in the leap year month name. Use this as the result.
3440 // The result from MatchLongestWords is 0 ~ length of word array.
3441 // So we increment the result by one to become the month value.
3442 if (tempResult >= 0)
3444 result = tempResult + 1;
3451 str.Index += (maxMatchStrLen - 1);
3457 /*=================================MatchAbbreviatedDayName==================================
3458 **Action: Parse the abbreviated day of week name from string starting at str.Index.
3459 **Returns: A value from 0 to 6 indicating Sunday to Saturday.
3460 **Arguments: str: a __DTString. The parsing will start from the
3461 ** next character after str.Index.
3462 **Exceptions: FormatException if a abbreviated day of week name can not be found.
3463 ==============================================================================*/
3465 private static bool MatchAbbreviatedDayName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3467 int maxMatchStrLen = 0;
3471 for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3473 String searchStr = dtfi.GetAbbreviatedDayName(i);
3474 int matchStrLen = searchStr.Length;
3475 if (dtfi.HasSpacesInDayNames
3476 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3477 : str.MatchSpecifiedWord(searchStr))
3479 if (matchStrLen > maxMatchStrLen)
3481 maxMatchStrLen = matchStrLen;
3489 str.Index += maxMatchStrLen - 1;
3495 /*=================================MatchDayName==================================
3496 **Action: Parse the day of week name from string starting at str.Index.
3497 **Returns: A value from 0 to 6 indicating Sunday to Saturday.
3498 **Arguments: str: a __DTString. The parsing will start from the
3499 ** next character after str.Index.
3500 **Exceptions: FormatException if a day of week name can not be found.
3501 ==============================================================================*/
3503 private static bool MatchDayName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3505 // Turkish (tr-TR) got day names with the same prefix.
3506 int maxMatchStrLen = 0;
3510 for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3512 String searchStr = dtfi.GetDayName(i);
3513 int matchStrLen = searchStr.Length;
3514 if (dtfi.HasSpacesInDayNames
3515 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3516 : str.MatchSpecifiedWord(searchStr))
3518 if (matchStrLen > maxMatchStrLen)
3520 maxMatchStrLen = matchStrLen;
3528 str.Index += maxMatchStrLen - 1;
3534 /*=================================MatchEraName==================================
3535 **Action: Parse era name from string starting at str.Index.
3536 **Returns: An era value.
3537 **Arguments: str: a __DTString. The parsing will start from the
3538 ** next character after str.Index.
3539 **Exceptions: FormatException if an era name can not be found.
3540 ==============================================================================*/
3542 private static bool MatchEraName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3546 int[] eras = dtfi.Calendar.Eras;
3550 for (int i = 0; i < eras.Length; i++)
3552 String searchStr = dtfi.GetEraName(eras[i]);
3553 if (str.MatchSpecifiedWord(searchStr))
3555 str.Index += (searchStr.Length - 1);
3559 searchStr = dtfi.GetAbbreviatedEraName(eras[i]);
3560 if (str.MatchSpecifiedWord(searchStr))
3562 str.Index += (searchStr.Length - 1);
3572 /*=================================MatchTimeMark==================================
3573 **Action: Parse the time mark (AM/PM) from string starting at str.Index.
3574 **Returns: TM_AM or TM_PM.
3575 **Arguments: str: a __DTString. The parsing will start from the
3576 ** next character after str.Index.
3577 **Exceptions: FormatException if a time mark can not be found.
3578 ==============================================================================*/
3580 private static bool MatchTimeMark(ref __DTString str, DateTimeFormatInfo dtfi, ref TM result)
3583 // In some cultures have empty strings in AM/PM mark. E.g. af-ZA (0x0436), the AM mark is "", and PM mark is "nm".
3584 if (dtfi.AMDesignator.Length == 0)
3588 if (dtfi.PMDesignator.Length == 0)
3595 String searchStr = dtfi.AMDesignator;
3596 if (searchStr.Length > 0)
3598 if (str.MatchSpecifiedWord(searchStr))
3600 // Found an AM timemark with length > 0.
3601 str.Index += (searchStr.Length - 1);
3606 searchStr = dtfi.PMDesignator;
3607 if (searchStr.Length > 0)
3609 if (str.MatchSpecifiedWord(searchStr))
3611 // Found a PM timemark with length > 0.
3612 str.Index += (searchStr.Length - 1);
3617 str.Index--; // Undo the GetNext call.
3619 if (result != TM.NotSet)
3621 // If one of the AM/PM marks is empty string, return the result.
3627 /*=================================MatchAbbreviatedTimeMark==================================
3628 **Action: Parse the abbreviated time mark (AM/PM) from string starting at str.Index.
3629 **Returns: TM_AM or TM_PM.
3630 **Arguments: str: a __DTString. The parsing will start from the
3631 ** next character after str.Index.
3632 **Exceptions: FormatException if a abbreviated time mark can not be found.
3633 ==============================================================================*/
3635 private static bool MatchAbbreviatedTimeMark(ref __DTString str, DateTimeFormatInfo dtfi, ref TM result)
3637 // NOTENOTE : the assumption here is that abbreviated time mark is the first
3638 // character of the AM/PM designator. If this invariant changes, we have to
3639 // change the code below.
3642 if (str.GetChar() == dtfi.AMDesignator[0])
3647 if (str.GetChar() == dtfi.PMDesignator[0])
3656 /*=================================CheckNewValue==================================
3657 **Action: Check if currentValue is initialized. If not, return the newValue.
3658 ** If yes, check if the current value is equal to newValue. Return false
3659 ** if they are not equal. This is used to check the case like "d" and "dd" are both
3660 ** used to format a string.
3661 **Returns: the correct value for currentValue.
3664 ==============================================================================*/
3666 private static bool CheckNewValue(ref int currentValue, int newValue, char patternChar, ref DateTimeResult result)
3668 if (currentValue == -1)
3670 currentValue = newValue;
3675 if (newValue != currentValue)
3677 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), patternChar);
3684 private static DateTime GetDateTimeNow(ref DateTimeResult result, ref DateTimeStyles styles)
3686 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3688 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
3690 // use the supplied offset to calculate 'Now'
3691 return new DateTime(DateTime.UtcNow.Ticks + result.timeZoneOffset.Ticks, DateTimeKind.Unspecified);
3693 else if ((styles & DateTimeStyles.AssumeUniversal) != 0)
3695 // assume the offset is Utc
3696 return DateTime.UtcNow;
3700 // assume the offset is Local
3701 return DateTime.Now;
3704 private static bool CheckDefaultDateTime(ref DateTimeResult result, ref Calendar cal, DateTimeStyles styles)
3706 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3708 // DateTimeOffset.Parse should allow dates without a year, but only if there is also no time zone marker;
3709 // e.g. "May 1 5pm" is OK, but "May 1 5pm -08:30" is not. This is somewhat pragmatic, since we would
3710 // have to rearchitect parsing completely to allow this one case to correctly handle things like leap
3711 // years and leap months. Is is an extremely corner case, and DateTime is basically incorrect in that
3714 // values like "11:00Z" or "11:00 -3:00" are also acceptable
3716 // if ((month or day is set) and (year is not set and time zone is set))
3718 if (((result.Month != -1) || (result.Day != -1))
3719 && ((result.Year == -1 || ((result.flags & ParseFlags.YearDefault) != 0)) && (result.flags & ParseFlags.TimeZoneUsed) != 0))
3721 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_MissingIncompleteDate), null);
3727 if ((result.Year == -1) || (result.Month == -1) || (result.Day == -1))
3730 The following table describes the behaviors of getting the default value
3731 when a certain year/month/day values are missing.
3733 An "X" means that the value exists. And "--" means that value is missing.
3735 Year Month Day => ResultYear ResultMonth ResultDay Note
3737 X X X Parsed year Parsed month Parsed day
3738 X X -- Parsed Year Parsed month First day If we have year and month, assume the first day of that month.
3739 X -- X Parsed year First month Parsed day If the month is missing, assume first month of that year.
3740 X -- -- Parsed year First month First day If we have only the year, assume the first day of that year.
3742 -- X X CurrentYear Parsed month Parsed day If the year is missing, assume the current year.
3743 -- X -- CurrentYear Parsed month First day If we have only a month value, assume the current year and current day.
3744 -- -- X CurrentYear First month Parsed day If we have only a day value, assume current year and first month.
3745 -- -- -- CurrentYear Current month Current day So this means that if the date string only contains time, you will get current date.
3749 DateTime now = GetDateTimeNow(ref result, ref styles);
3750 if (result.Month == -1 && result.Day == -1)
3752 if (result.Year == -1)
3754 if ((styles & DateTimeStyles.NoCurrentDateDefault) != 0)
3756 // If there is no year/month/day values, and NoCurrentDateDefault flag is used,
3757 // set the year/month/day value to the beginning year/month/day of DateTime().
3758 // Note we should be using Gregorian for the year/month/day.
3759 cal = GregorianCalendar.GetDefaultInstance();
3760 result.Year = result.Month = result.Day = 1;
3764 // Year/Month/Day are all missing.
3765 result.Year = cal.GetYear(now);
3766 result.Month = cal.GetMonth(now);
3767 result.Day = cal.GetDayOfMonth(now);
3772 // Month/Day are both missing.
3779 if (result.Year == -1)
3781 result.Year = cal.GetYear(now);
3783 if (result.Month == -1)
3787 if (result.Day == -1)
3793 // Set Hour/Minute/Second to zero if these value are not in str.
3794 if (result.Hour == -1) result.Hour = 0;
3795 if (result.Minute == -1) result.Minute = 0;
3796 if (result.Second == -1) result.Second = 0;
3797 if (result.era == -1) result.era = Calendar.CurrentEra;
3801 // Expand a pre-defined format string (like "D" for long date) to the real format that
3802 // we are going to use in the date time parsing.
3803 // This method also set the dtfi according/parseInfo to some special pre-defined
3806 private static String ExpandPredefinedFormat(String format, ref DateTimeFormatInfo dtfi, ref ParsingInfo parseInfo, ref DateTimeResult result)
3809 // Check the format to see if we need to override the dtfi to be InvariantInfo,
3810 // and see if we need to set up the userUniversalTime flag.
3815 case 'O': // Round Trip Format
3816 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3817 dtfi = DateTimeFormatInfo.InvariantInfo;
3820 case 'R': // RFC 1123 Standard. (in Universal time)
3821 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3822 dtfi = DateTimeFormatInfo.InvariantInfo;
3824 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3826 result.flags |= ParseFlags.Rfc1123Pattern;
3829 case 's': // Sortable format (in local time)
3830 dtfi = DateTimeFormatInfo.InvariantInfo;
3831 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3833 case 'u': // Universal time format in sortable format.
3834 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3835 dtfi = DateTimeFormatInfo.InvariantInfo;
3837 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3839 result.flags |= ParseFlags.UtcSortPattern;
3842 case 'U': // Universal time format with culture-dependent format.
3843 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3844 result.flags |= ParseFlags.TimeZoneUsed;
3845 result.timeZoneOffset = new TimeSpan(0);
3846 result.flags |= ParseFlags.TimeZoneUtc;
3847 if (dtfi.Calendar.GetType() != typeof(GregorianCalendar))
3849 dtfi = (DateTimeFormatInfo)dtfi.Clone();
3850 dtfi.Calendar = GregorianCalendar.GetDefaultInstance();
3856 // Expand the pre-defined format character to the real format from DateTimeFormatInfo.
3858 return (DateTimeFormat.GetRealFormat(format, dtfi));
3865 // Given a specified format character, parse and update the parsing result.
3867 private static bool ParseByFormat(
3869 ref __DTString format,
3870 ref ParsingInfo parseInfo,
3871 DateTimeFormatInfo dtfi,
3872 ref DateTimeResult result)
3875 int tempYear = 0, tempMonth = 0, tempDay = 0, tempDayOfWeek = 0, tempHour = 0, tempMinute = 0, tempSecond = 0;
3876 double tempFraction = 0;
3877 TM tempTimeMark = 0;
3879 char ch = format.GetChar();
3884 tokenLen = format.GetRepeatCount();
3886 if (dtfi.HasForceTwoDigitYears)
3888 parseResult = ParseDigits(ref str, 1, 4, out tempYear);
3894 parseInfo.fUseTwoDigitYear = true;
3896 parseResult = ParseDigits(ref str, tokenLen, out tempYear);
3898 if (!parseResult && parseInfo.fCustomNumberParser)
3900 parseResult = parseInfo.parseNumberDelegate(ref str, tokenLen, out tempYear);
3904 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3907 if (!CheckNewValue(ref result.Year, tempYear, ch, ref result))
3913 tokenLen = format.GetRepeatCount();
3916 if (!ParseDigits(ref str, tokenLen, out tempMonth))
3918 if (!parseInfo.fCustomNumberParser ||
3919 !parseInfo.parseNumberDelegate(ref str, tokenLen, out tempMonth))
3921 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3930 if (!MatchAbbreviatedMonthName(ref str, dtfi, ref tempMonth))
3932 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3938 if (!MatchMonthName(ref str, dtfi, ref tempMonth))
3940 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3944 result.flags |= ParseFlags.ParsedMonthName;
3946 if (!CheckNewValue(ref result.Month, tempMonth, ch, ref result))
3952 // Day & Day of week
3953 tokenLen = format.GetRepeatCount();
3958 if (!ParseDigits(ref str, tokenLen, out tempDay))
3960 if (!parseInfo.fCustomNumberParser ||
3961 !parseInfo.parseNumberDelegate(ref str, tokenLen, out tempDay))
3963 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3967 if (!CheckNewValue(ref result.Day, tempDay, ch, ref result))
3977 if (!MatchAbbreviatedDayName(ref str, dtfi, ref tempDayOfWeek))
3979 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3986 if (!MatchDayName(ref str, dtfi, ref tempDayOfWeek))
3988 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
3992 if (!CheckNewValue(ref parseInfo.dayOfWeek, tempDayOfWeek, ch, ref result))
3999 tokenLen = format.GetRepeatCount();
4000 // Put the era value in result.era.
4001 if (!MatchEraName(ref str, dtfi, ref result.era))
4003 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4008 parseInfo.fUseHour12 = true;
4009 tokenLen = format.GetRepeatCount();
4010 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempHour))
4012 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4015 if (!CheckNewValue(ref result.Hour, tempHour, ch, ref result))
4021 tokenLen = format.GetRepeatCount();
4022 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempHour))
4024 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4027 if (!CheckNewValue(ref result.Hour, tempHour, ch, ref result))
4033 tokenLen = format.GetRepeatCount();
4034 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempMinute))
4036 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4039 if (!CheckNewValue(ref result.Minute, tempMinute, ch, ref result))
4045 tokenLen = format.GetRepeatCount();
4046 if (!ParseDigits(ref str, (tokenLen < 2 ? 1 : 2), out tempSecond))
4048 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4051 if (!CheckNewValue(ref result.Second, tempSecond, ch, ref result))
4058 tokenLen = format.GetRepeatCount();
4059 if (tokenLen <= DateTimeFormat.MaxSecondsFractionDigits)
4061 if (!ParseFractionExact(ref str, tokenLen, ref tempFraction))
4065 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4069 if (result.fraction < 0)
4071 result.fraction = tempFraction;
4075 if (tempFraction != result.fraction)
4077 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), ch);
4084 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4090 tokenLen = format.GetRepeatCount();
4093 if (!MatchAbbreviatedTimeMark(ref str, dtfi, ref tempTimeMark))
4095 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4101 if (!MatchTimeMark(ref str, dtfi, ref tempTimeMark))
4103 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4108 if (parseInfo.timeMark == TM.NotSet)
4110 parseInfo.timeMark = tempTimeMark;
4114 if (parseInfo.timeMark != tempTimeMark)
4116 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), ch);
4123 tokenLen = format.GetRepeatCount();
4125 TimeSpan tempTimeZoneOffset = new TimeSpan(0);
4126 if (!ParseTimeZoneOffset(ref str, tokenLen, ref tempTimeZoneOffset))
4128 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4131 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && tempTimeZoneOffset != result.timeZoneOffset)
4133 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'z');
4136 result.timeZoneOffset = tempTimeZoneOffset;
4137 result.flags |= ParseFlags.TimeZoneUsed;
4141 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && result.timeZoneOffset != TimeSpan.Zero)
4143 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'Z');
4147 result.flags |= ParseFlags.TimeZoneUsed;
4148 result.timeZoneOffset = new TimeSpan(0);
4149 result.flags |= ParseFlags.TimeZoneUtc;
4151 // The updating of the indexes is to reflect that ParseExact MatchXXX methods assume that
4152 // they need to increment the index and Parse GetXXX do not. Since we are calling a Parse
4153 // method from inside ParseExact we need to adjust this. Long term, we should try to
4154 // eliminate this discrepancy.
4156 if (!GetTimeZoneName(ref str))
4158 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4164 // This should parse either as a blank, the 'Z' character or a local offset like "-07:00"
4167 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && result.timeZoneOffset != TimeSpan.Zero)
4169 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'K');
4173 result.flags |= ParseFlags.TimeZoneUsed;
4174 result.timeZoneOffset = new TimeSpan(0);
4175 result.flags |= ParseFlags.TimeZoneUtc;
4177 else if (str.Match('+') || str.Match('-'))
4179 str.Index--; // Put the character back for the parser
4180 TimeSpan tempTimeZoneOffset = new TimeSpan(0);
4181 if (!ParseTimeZoneOffset(ref str, 3, ref tempTimeZoneOffset))
4183 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4186 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && tempTimeZoneOffset != result.timeZoneOffset)
4188 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'K');
4191 result.timeZoneOffset = tempTimeZoneOffset;
4192 result.flags |= ParseFlags.TimeZoneUsed;
4194 // Otherwise it is unspecified and we consume no characters
4197 // 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
4198 // We have to exclude the case when the time separator is more than one character and starts with ':' something like "::" for instance.
4199 if (((dtfi.TimeSeparator.Length > 1 && dtfi.TimeSeparator[0] == ':') || !str.Match(':')) &&
4200 !str.Match(dtfi.TimeSeparator))
4202 // A time separator is expected.
4203 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4208 // 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
4209 // We have to exclude the case when the date separator is more than one character and starts with '/' something like "//" for instance.
4210 if (((dtfi.DateSeparator.Length > 1 && dtfi.DateSeparator[0] == '/') || !str.Match('/')) &&
4211 !str.Match(dtfi.DateSeparator))
4213 // A date separator is expected.
4214 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4220 StringBuilder enquotedString = new StringBuilder();
4221 // Use ParseQuoteString so that we can handle escape characters within the quoted string.
4222 if (!TryParseQuoteString(format.Value, format.Index, enquotedString, out tokenLen))
4224 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadQuote), ch);
4227 format.Index += tokenLen - 1;
4229 // Some cultures uses space in the quoted string. E.g. Spanish has long date format as:
4230 // "dddd, dd' de 'MMMM' de 'yyyy". When inner spaces flag is set, we should skip whitespaces if there is space
4231 // in the quoted string.
4232 String quotedStr = enquotedString.ToString();
4234 for (int i = 0; i < quotedStr.Length; i++)
4236 if (quotedStr[i] == ' ' && parseInfo.fAllowInnerWhite)
4238 str.SkipWhiteSpaces();
4240 else if (!str.Match(quotedStr[i]))
4242 // Can not find the matching quoted string.
4243 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4248 // The "r" and "u" formats incorrectly quoted 'GMT' and 'Z', respectively. We cannot
4249 // correct this mistake for DateTime.ParseExact for compatibility reasons, but we can
4250 // fix it for DateTimeOffset.ParseExact as DateTimeOffset has not been publically released
4252 if ((result.flags & ParseFlags.CaptureOffset) != 0)
4254 if ((result.flags & ParseFlags.Rfc1123Pattern) != 0 && quotedStr == GMTName)
4256 result.flags |= ParseFlags.TimeZoneUsed;
4257 result.timeZoneOffset = TimeSpan.Zero;
4259 else if ((result.flags & ParseFlags.UtcSortPattern) != 0 && quotedStr == ZuluName)
4261 result.flags |= ParseFlags.TimeZoneUsed;
4262 result.timeZoneOffset = TimeSpan.Zero;
4268 // Skip this so we can get to the next pattern character.
4269 // Used in case like "%d", "%y"
4271 // Make sure the next character is not a '%' again.
4272 if (format.Index >= format.Value.Length - 1 || format.Value[format.Index + 1] == '%')
4274 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
4279 // Escape character. For example, "\d".
4280 // Get the next character in format, and see if we can
4281 // find a match in str.
4282 if (format.GetNext())
4284 if (!str.Match(format.GetChar()))
4286 // Can not find a match for the escaped character.
4287 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4293 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
4300 if (format.GetNext())
4302 // If we encounter the pattern ".F", and the dot is not present, it is an optional
4303 // second fraction and we can skip this format.
4304 if (format.Match('F'))
4306 format.GetRepeatCount();
4310 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4317 if (parseInfo.fAllowInnerWhite)
4319 // Skip whitespaces if AllowInnerWhite.
4326 // If the space does not match, and trailing space is allowed, we do
4327 // one more step to see if the next format character can lead to
4328 // successful parsing.
4329 // This is used to deal with special case that a empty string can match
4330 // a specific pattern.
4331 // The example here is af-ZA, which has a time format like "hh:mm:ss tt". However,
4332 // its AM symbol is "" (empty string). If fAllowTrailingWhite is used, and time is in
4333 // the AM, we will trim the whitespaces at the end, which will lead to a failure
4334 // when we are trying to match the space before "tt".
4335 if (parseInfo.fAllowTrailingWhite)
4337 if (format.GetNext())
4339 if (ParseByFormat(ref str, ref format, ref parseInfo, dtfi, ref result))
4345 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4353 if (format.MatchSpecifiedWord(GMTName))
4355 format.Index += (GMTName.Length - 1);
4356 // Found GMT string in format. This means the DateTime string
4357 // is in GMT timezone.
4358 result.flags |= ParseFlags.TimeZoneUsed;
4359 result.timeZoneOffset = TimeSpan.Zero;
4360 if (!str.Match(GMTName))
4362 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4366 else if (!str.Match(ch))
4369 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4379 // The pos should point to a quote character. This method will
4380 // get the string enclosed by the quote character.
4382 internal static bool TryParseQuoteString(String format, int pos, StringBuilder result, out int returnValue)
4385 // NOTE : pos will be the index of the quote character in the 'format' string.
4388 int formatLen = format.Length;
4390 char quoteChar = format[pos++]; // Get the character used to quote the following string.
4392 bool foundQuote = false;
4393 while (pos < formatLen)
4395 char ch = format[pos++];
4396 if (ch == quoteChar)
4401 else if (ch == '\\')
4403 // The following are used to support escaped character.
4404 // Escaped character is also supported in the quoted string.
4405 // Therefore, someone can use a format like "'minute:' mm\"" to display:
4407 // because the second double quote is escaped.
4408 if (pos < formatLen)
4410 result.Append(format[pos++]);
4415 // This means that '\' is at the end of the formatting string.
4428 // Here we can't find the matching quote.
4433 // Return the character count including the begin/end quote characters and enclosed string.
4435 returnValue = (pos - beginPos);
4442 /*=================================DoStrictParse==================================
4443 **Action: Do DateTime parsing using the format in formatParam.
4444 **Returns: The parsed DateTime.
4449 ** When the following general formats are used, InvariantInfo is used in dtfi:
4451 ** When the following general formats are used, the time is assumed to be in Universal time.
4454 ** Only GregarianCalendar is supported for now.
4455 ** Only support GMT timezone.
4456 ==============================================================================*/
4458 private static bool DoStrictParse(
4461 DateTimeStyles styles,
4462 DateTimeFormatInfo dtfi,
4463 ref DateTimeResult result)
4465 ParsingInfo parseInfo = new ParsingInfo();
4468 parseInfo.calendar = dtfi.Calendar;
4469 parseInfo.fAllowInnerWhite = ((styles & DateTimeStyles.AllowInnerWhite) != 0);
4470 parseInfo.fAllowTrailingWhite = ((styles & DateTimeStyles.AllowTrailingWhite) != 0);
4472 // We need the original values of the following two below.
4473 String originalFormat = formatParam;
4475 if (formatParam.Length == 1)
4477 if (((result.flags & ParseFlags.CaptureOffset) != 0) && formatParam[0] == 'U')
4479 // The 'U' format is not allowed for DateTimeOffset
4480 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier), null);
4483 formatParam = ExpandPredefinedFormat(formatParam, ref dtfi, ref parseInfo, ref result);
4486 bool bTimeOnly = false;
4487 result.calendar = parseInfo.calendar;
4489 if (parseInfo.calendar.ID == CalendarId.HEBREW)
4491 parseInfo.parseNumberDelegate = m_hebrewNumberParser;
4492 parseInfo.fCustomNumberParser = true;
4495 // Reset these values to negative one so that we could throw exception
4496 // if we have parsed every item twice.
4497 result.Hour = result.Minute = result.Second = -1;
4499 __DTString format = new __DTString(formatParam, dtfi, false);
4500 __DTString str = new __DTString(s, dtfi, false);
4502 if (parseInfo.fAllowTrailingWhite)
4504 // Trim trailing spaces if AllowTrailingWhite.
4506 format.RemoveTrailingInQuoteSpaces();
4510 if ((styles & DateTimeStyles.AllowLeadingWhite) != 0)
4512 format.SkipWhiteSpaces();
4513 format.RemoveLeadingInQuoteSpaces();
4514 str.SkipWhiteSpaces();
4518 // Scan every character in format and match the pattern in str.
4520 while (format.GetNext())
4522 // We trim inner spaces here, so that we will not eat trailing spaces when
4523 // AllowTrailingWhite is not used.
4524 if (parseInfo.fAllowInnerWhite)
4526 str.SkipWhiteSpaces();
4528 if (!ParseByFormat(ref str, ref format, ref parseInfo, dtfi, ref result))
4534 if (str.Index < str.Value.Length - 1)
4536 // There are still remaining character in str.
4537 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4541 if (parseInfo.fUseTwoDigitYear && ((dtfi.FormatFlags & DateTimeFormatFlags.UseHebrewRule) == 0))
4543 // A two digit year value is expected. Check if the parsed year value is valid.
4544 if (result.Year >= 100)
4546 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4551 result.Year = parseInfo.calendar.ToFourDigitYear(result.Year);
4553 catch (ArgumentOutOfRangeException e)
4555 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), e);
4560 if (parseInfo.fUseHour12)
4562 if (parseInfo.timeMark == TM.NotSet)
4564 // hh is used, but no AM/PM designator is specified.
4565 // Assume the time is AM.
4566 // Don't throw exceptions in here becasue it is very confusing for the caller.
4567 // I always got confused myself when I use "hh:mm:ss" to parse a time string,
4568 // and ParseExact() throws on me (because I didn't use the 24-hour clock 'HH').
4569 parseInfo.timeMark = TM.AM;
4571 if (result.Hour > 12)
4573 // AM/PM is used, but the value for HH is too big.
4574 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4577 if (parseInfo.timeMark == TM.AM)
4579 if (result.Hour == 12)
4586 result.Hour = (result.Hour == 12) ? 12 : result.Hour + 12;
4591 // Military (24-hour time) mode
4593 // AM cannot be set with a 24-hour time like 17:15.
4594 // PM cannot be set with a 24-hour time like 03:15.
4595 if ((parseInfo.timeMark == TM.AM && result.Hour >= 12)
4596 || (parseInfo.timeMark == TM.PM && result.Hour < 12))
4598 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDateTime), null);
4604 // Check if the parased string only contains hour/minute/second values.
4605 bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1);
4606 if (!CheckDefaultDateTime(ref result, ref parseInfo.calendar, styles))
4611 if (!bTimeOnly && dtfi.HasYearMonthAdjustment)
4613 if (!dtfi.YearMonthAdjustment(ref result.Year, ref result.Month, ((result.flags & ParseFlags.ParsedMonthName) != 0)))
4615 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
4619 if (!parseInfo.calendar.TryToDateTime(result.Year, result.Month, result.Day,
4620 result.Hour, result.Minute, result.Second, 0, result.era, out result.parsedDate))
4622 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar), null);
4625 if (result.fraction > 0)
4627 result.parsedDate = result.parsedDate.AddTicks((long)Math.Round(result.fraction * Calendar.TicksPerSecond));
4631 // We have to check day of week before we adjust to the time zone.
4632 // It is because the value of day of week may change after adjusting
4633 // to the time zone.
4635 if (parseInfo.dayOfWeek != -1)
4638 // Check if day of week is correct.
4640 if (parseInfo.dayOfWeek != (int)parseInfo.calendar.GetDayOfWeek(result.parsedDate))
4642 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadDayOfWeek), null);
4648 if (!DetermineTimeZoneAdjustments(ref result, styles, bTimeOnly))
4655 private static Exception GetDateTimeParseException(ref DateTimeResult result)
4657 switch (result.failure)
4659 case ParseFailureKind.ArgumentNull:
4660 return new ArgumentNullException(result.failureArgumentName, SR.GetResourceString(result.failureMessageID));
4661 case ParseFailureKind.Format:
4662 return new FormatException(SR.GetResourceString(result.failureMessageID));
4663 case ParseFailureKind.FormatWithParameter:
4664 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), result.failureMessageFormatArgument));
4665 case ParseFailureKind.FormatBadDateTimeCalendar:
4666 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID), result.calendar));
4668 Debug.Assert(false, "Unkown DateTimeParseFailure: " + result);
4673 // Builds with _LOGGING defined (x86dbg, amd64chk, etc) support tracing
4674 // Set the following internal-only/unsupported environment variables to enable DateTime tracing to the console:
4676 // COMPlus_LogEnable=1
4677 // COMPlus_LogToConsole=1
4678 // COMPlus_LogLevel=9
4679 // COMPlus_ManagedLogFacility=0x00001000
4680 [Conditional("_LOGGING")]
4681 internal static void LexTraceExit(string message, DS dps)
4684 if (!_tracingEnabled)
4686 BCLDebug.Trace("DATETIME", "[DATETIME] Lex return {0}, DS.{1}", message, dps);
4689 [Conditional("_LOGGING")]
4690 internal static void PTSTraceExit(DS dps, bool passed)
4693 if (!_tracingEnabled)
4695 BCLDebug.Trace("DATETIME", "[DATETIME] ProcessTerminalState {0} @ DS.{1}", passed ? "passed" : "failed", dps);
4698 [Conditional("_LOGGING")]
4699 internal static void TPTraceExit(string message, DS dps)
4702 if (!_tracingEnabled)
4704 BCLDebug.Trace("DATETIME", "[DATETIME] TryParse return {0}, DS.{1}", message, dps);
4707 [Conditional("_LOGGING")]
4708 internal static void DTFITrace(DateTimeFormatInfo dtfi)
4711 if (!_tracingEnabled)
4714 BCLDebug.Trace("DATETIME", "[DATETIME] DateTimeFormatInfo Properties");
4715 #if !FEATURE_COREFX_GLOBALIZATION
4716 BCLDebug.Trace("DATETIME", " NativeCalendarName {0}", Hex(dtfi.NativeCalendarName));
4718 BCLDebug.Trace("DATETIME", " AMDesignator {0}", Hex(dtfi.AMDesignator));
4719 BCLDebug.Trace("DATETIME", " PMDesignator {0}", Hex(dtfi.PMDesignator));
4720 BCLDebug.Trace("DATETIME", " TimeSeparator {0}", Hex(dtfi.TimeSeparator));
4721 BCLDebug.Trace("DATETIME", " AbbrvDayNames {0}", Hex(dtfi.AbbreviatedDayNames));
4722 BCLDebug.Trace("DATETIME", " ShortestDayNames {0}", Hex(dtfi.ShortestDayNames));
4723 BCLDebug.Trace("DATETIME", " DayNames {0}", Hex(dtfi.DayNames));
4724 BCLDebug.Trace("DATETIME", " AbbrvMonthNames {0}", Hex(dtfi.AbbreviatedMonthNames));
4725 BCLDebug.Trace("DATETIME", " MonthNames {0}", Hex(dtfi.MonthNames));
4726 BCLDebug.Trace("DATETIME", " AbbrvMonthGenNames {0}", Hex(dtfi.AbbreviatedMonthGenitiveNames));
4727 BCLDebug.Trace("DATETIME", " MonthGenNames {0}", Hex(dtfi.MonthGenitiveNames));
4731 // return a string in the form: "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
4732 internal static string Hex(string[] strs)
4734 if (strs == null || strs.Length == 0)
4735 return String.Empty;
4736 if (strs.Length == 1)
4737 return Hex(strs[0]);
4739 int curLineLength = 0;
4740 int maxLineLength = 55;
4741 int newLinePadding = 20;
4744 //invariant: strs.Length >= 2
4745 StringBuilder buffer = new StringBuilder();
4746 buffer.Append(Hex(strs[0]));
4747 curLineLength = buffer.Length;
4750 for (int i = 1; i < strs.Length - 1; i++)
4754 if (s.Length > maxLineLength || (curLineLength + s.Length + 2) > maxLineLength)
4757 buffer.Append(Environment.NewLine);
4758 buffer.Append(' ', newLinePadding);
4763 buffer.Append(", ");
4767 curLineLength += s.Length;
4771 s = Hex(strs[strs.Length - 1]);
4772 if (s.Length > maxLineLength || (curLineLength + s.Length + 6) > maxLineLength)
4774 buffer.Append(Environment.NewLine);
4775 buffer.Append(' ', newLinePadding);
4782 return buffer.ToString();
4784 // return a string in the form: "Sun"
4785 internal static string Hex(string str)
4787 StringBuilder buffer = new StringBuilder();
4788 buffer.Append("\"");
4789 for (int i = 0; i < str.Length; i++)
4791 if (str[i] <= '\x007f')
4792 buffer.Append(str[i]);
4794 buffer.Append("\\u" + ((int)str[i]).ToString("x4", CultureInfo.InvariantCulture));
4796 buffer.Append("\"");
4797 return buffer.ToString();
4799 // return an unicode escaped string form of char c
4800 internal static String Hex(char c)
4803 return c.ToString(CultureInfo.InvariantCulture);
4805 return "\\u" + ((int)c).ToString("x4", CultureInfo.InvariantCulture);
4808 internal static bool _tracingEnabled = BCLDebug.CheckEnabled("DATETIME");
4814 // This is a string parsing helper which wraps a String object.
4815 // It has a Index property which tracks
4816 // the current parsing pointer of the string.
4822 // Value propery: stores the real string to be parsed.
4824 internal String Value;
4827 // Index property: points to the character that we are currently parsing.
4831 // The length of Value string.
4834 // The current chracter to be looked at.
4835 internal char m_current;
4837 private CompareInfo m_info;
4838 // Flag to indicate if we encouter an digit, we should check for token or not.
4839 // In some cultures, such as mn-MN, it uses "\x0031\x00a0\x0434\x04af\x0433\x044d\x044d\x0440\x00a0\x0441\x0430\x0440" in month names.
4840 private bool m_checkDigitToken;
4842 internal __DTString(String str, DateTimeFormatInfo dtfi, bool checkDigitToken) : this(str, dtfi)
4844 m_checkDigitToken = checkDigitToken;
4847 internal __DTString(String str, DateTimeFormatInfo dtfi)
4856 m_info = dtfi.CompareInfo;
4857 m_checkDigitToken = ((dtfi.FormatFlags & DateTimeFormatFlags.UseDigitPrefixInTokens) != 0);
4861 m_info = CultureInfo.CurrentCulture.CompareInfo;
4862 m_checkDigitToken = false;
4866 internal CompareInfo CompareInfo
4868 get { return m_info; }
4872 // Advance the Index.
4873 // Return true if Index is NOT at the end of the string.
4876 // while (str.GetNext())
4878 // char ch = str.GetChar()
4880 internal bool GetNext()
4885 m_current = Value[Index];
4891 internal bool AtEnd()
4893 return Index < len ? false : true;
4896 internal bool Advance(int count)
4898 Debug.Assert(Index + count <= len, "__DTString::Advance: Index + count <= len");
4902 m_current = Value[Index];
4909 // Used by DateTime.Parse() to get the next token.
4910 internal void GetRegularToken(out TokenType tokenType, out int tokenValue, DateTimeFormatInfo dtfi)
4915 tokenType = TokenType.EndOfString;
4919 tokenType = TokenType.UnknownToken;
4922 if (DateTimeParse.IsDigit(m_current))
4925 tokenValue = m_current - '0';
4930 // Collect other digits.
4932 while (++Index < len)
4934 m_current = Value[Index];
4935 value = m_current - '0';
4936 if (value >= 0 && value <= 9)
4938 tokenValue = tokenValue * 10 + value;
4945 if (Index - start > DateTimeParse.MaxDateTimeNumberDigits)
4947 tokenType = TokenType.NumberToken;
4950 else if (Index - start < 3)
4952 tokenType = TokenType.NumberToken;
4956 // If there are more than 3 digits, assume that it's a year value.
4957 tokenType = TokenType.YearNumberToken;
4959 if (m_checkDigitToken)
4962 char saveCh = m_current;
4963 // Re-scan using the staring Index to see if this is a token.
4964 Index = start; // To include the first digit.
4965 m_current = Value[Index];
4968 // This DTFI has tokens starting with digits.
4969 // E.g. mn-MN has month name like "\x0031\x00a0\x0434\x04af\x0433\x044d\x044d\x0440\x00a0\x0441\x0430\x0440"
4970 if (dtfi.Tokenize(TokenType.RegularTokenMask, out tempType, out tempValue, ref this))
4972 tokenType = tempType;
4973 tokenValue = tempValue;
4974 // This is a token, so the Index has been advanced propertly in DTFI.Tokenizer().
4978 // Use the number token value.
4979 // Restore the index.
4985 else if (Char.IsWhiteSpace(m_current))
4987 // Just skip to the next character.
4988 while (++Index < len)
4990 m_current = Value[Index];
4991 if (!(Char.IsWhiteSpace(m_current)))
4996 // We have reached the end of string.
4997 tokenType = TokenType.EndOfString;
5001 dtfi.Tokenize(TokenType.RegularTokenMask, out tokenType, out tokenValue, ref this);
5005 internal TokenType GetSeparatorToken(DateTimeFormatInfo dtfi, out int indexBeforeSeparator, out char charBeforeSeparator)
5007 indexBeforeSeparator = Index;
5008 charBeforeSeparator = m_current;
5009 TokenType tokenType;
5010 if (!SkipWhiteSpaceCurrent())
5012 // Reach the end of the string.
5013 return (TokenType.SEP_End);
5015 if (!DateTimeParse.IsDigit(m_current))
5017 // Not a digit. Tokenize it.
5019 bool found = dtfi.Tokenize(TokenType.SeparatorTokenMask, out tokenType, out tokenValue, ref this);
5022 tokenType = TokenType.SEP_Space;
5027 // Do nothing here. If we see a number, it will not be a separator. There is no need wasting time trying to find the
5029 tokenType = TokenType.SEP_Space;
5034 internal bool MatchSpecifiedWord(String target)
5036 return MatchSpecifiedWord(target, target.Length + Index);
5039 internal bool MatchSpecifiedWord(String target, int endIndex)
5041 int count = endIndex - Index;
5043 if (count != target.Length)
5048 if (Index + count > len)
5053 return (m_info.Compare(Value, Index, count, target, 0, count, CompareOptions.IgnoreCase) == 0);
5056 private static Char[] WhiteSpaceChecks = new Char[] { ' ', '\u00A0' };
5058 internal bool MatchSpecifiedWords(String target, bool checkWordBoundary, ref int matchLength)
5060 int valueRemaining = Value.Length - Index;
5061 matchLength = target.Length;
5063 if (matchLength > valueRemaining || m_info.Compare(Value, Index, matchLength, target, 0, matchLength, CompareOptions.IgnoreCase) != 0)
5065 // Check word by word
5066 int targetPosition = 0; // Where we are in the target string
5067 int thisPosition = Index; // Where we are in this string
5068 int wsIndex = target.IndexOfAny(WhiteSpaceChecks, targetPosition);
5075 int segmentLength = wsIndex - targetPosition;
5076 if (thisPosition >= Value.Length - segmentLength)
5077 { // Subtraction to prevent overflow.
5080 if (segmentLength == 0)
5082 // If segmentLength == 0, it means that we have leading space in the target string.
5083 // In that case, skip the leading spaces in the target and this string.
5088 // Make sure we also have whitespace in the input string
5089 if (!Char.IsWhiteSpace(Value[thisPosition + segmentLength]))
5093 if (m_info.Compare(Value, thisPosition, segmentLength, target, targetPosition, segmentLength, CompareOptions.IgnoreCase) != 0)
5097 // Advance the input string
5098 thisPosition = thisPosition + segmentLength + 1;
5100 // Advance our target string
5101 targetPosition = wsIndex + 1;
5104 // Skip past multiple whitespace
5105 while (thisPosition < Value.Length && Char.IsWhiteSpace(Value[thisPosition]))
5110 } while ((wsIndex = target.IndexOfAny(WhiteSpaceChecks, targetPosition)) >= 0);
5111 // now check the last segment;
5112 if (targetPosition < target.Length)
5114 int segmentLength = target.Length - targetPosition;
5115 if (thisPosition > Value.Length - segmentLength)
5119 if (m_info.Compare(Value, thisPosition, segmentLength, target, targetPosition, segmentLength, CompareOptions.IgnoreCase) != 0)
5126 if (checkWordBoundary)
5128 int nextCharIndex = Index + matchLength;
5129 if (nextCharIndex < Value.Length)
5131 if (Char.IsLetter(Value[nextCharIndex]))
5141 // Check to see if the string starting from Index is a prefix of
5143 // If a match is found, true value is returned and Index is updated to the next character to be parsed.
5144 // Otherwise, Index is unchanged.
5146 internal bool Match(String str)
5153 if (str.Length > (Value.Length - Index))
5158 if (m_info.Compare(Value, Index, str.Length, str, 0, str.Length, CompareOptions.Ordinal) == 0)
5160 // Update the Index to the end of the matching string.
5161 // So the following GetNext()/Match() opeartion will get
5162 // the next character to be parsed.
5163 Index += (str.Length - 1);
5169 internal bool Match(char ch)
5175 if (Value[Index] == ch)
5185 // Actions: From the current position, try matching the longest word in the specified string array.
5186 // E.g. words[] = {"AB", "ABC", "ABCD"}, if the current position points to a substring like "ABC DEF",
5187 // MatchLongestWords(words, ref MaxMatchStrLen) will return 1 (the index), and maxMatchLen will be 3.
5189 // The index that contains the longest word to match
5191 // words The string array that contains words to search.
5192 // maxMatchStrLen [in/out] the initailized maximum length. This parameter can be used to
5193 // find the longest match in two string arrays.
5195 internal int MatchLongestWords(String[] words, ref int maxMatchStrLen)
5198 for (int i = 0; i < words.Length; i++)
5200 String word = words[i];
5201 int matchLength = word.Length;
5202 if (MatchSpecifiedWords(word, false, ref matchLength))
5204 if (matchLength > maxMatchStrLen)
5206 maxMatchStrLen = matchLength;
5216 // Get the number of repeat character after the current character.
5217 // For a string "hh:mm:ss" at Index of 3. GetRepeatCount() = 2, and Index
5218 // will point to the second ':'.
5220 internal int GetRepeatCount()
5222 char repeatChar = Value[Index];
5223 int pos = Index + 1;
5224 while ((pos < len) && (Value[pos] == repeatChar))
5228 int repeatCount = (pos - Index);
5229 // Update the Index to the end of the repeated characters.
5230 // So the following GetNext() opeartion will get
5231 // the next character to be parsed.
5233 return (repeatCount);
5236 // Return false when end of string is encountered or a non-digit character is found.
5237 internal bool GetNextDigit()
5243 return (DateTimeParse.IsDigit(Value[Index]));
5247 // Get the current character.
5249 internal char GetChar()
5251 Debug.Assert(Index >= 0 && Index < len, "Index >= 0 && Index < len");
5252 return (Value[Index]);
5256 // Convert the current character to a digit, and return it.
5258 internal int GetDigit()
5260 Debug.Assert(Index >= 0 && Index < len, "Index >= 0 && Index < len");
5261 Debug.Assert(DateTimeParse.IsDigit(Value[Index]), "IsDigit(Value[Index])");
5262 return (Value[Index] - '0');
5266 // Eat White Space ahead of the current position
5268 // Return false if end of string is encountered.
5270 internal void SkipWhiteSpaces()
5272 // Look ahead to see if the next character
5274 while (Index + 1 < len)
5276 char ch = Value[Index + 1];
5277 if (!Char.IsWhiteSpace(ch))
5287 // Skip white spaces from the current position
5289 // Return false if end of string is encountered.
5291 internal bool SkipWhiteSpaceCurrent()
5298 if (!Char.IsWhiteSpace(m_current))
5303 while (++Index < len)
5305 m_current = Value[Index];
5306 if (!Char.IsWhiteSpace(m_current))
5315 internal void TrimTail()
5318 while (i >= 0 && Char.IsWhiteSpace(Value[i]))
5322 Value = Value.Substring(0, i + 1);
5326 // Trim the trailing spaces within a quoted string.
5327 // Call this after TrimTail() is done.
5328 internal void RemoveTrailingInQuoteSpaces()
5336 // Check if the last character is a quote.
5337 if (ch == '\'' || ch == '\"')
5339 if (Char.IsWhiteSpace(Value[i - 1]))
5342 while (i >= 1 && Char.IsWhiteSpace(Value[i - 1]))
5346 Value = Value.Remove(i, Value.Length - 1 - i);
5352 // Trim the leading spaces within a quoted string.
5353 // Call this after the leading spaces before quoted string are trimmed.
5354 internal void RemoveLeadingInQuoteSpaces()
5362 // Check if the last character is a quote.
5363 if (ch == '\'' || ch == '\"')
5365 while ((i + 1) < len && Char.IsWhiteSpace(Value[i + 1]))
5371 Value = Value.Remove(1, i);
5377 internal DTSubString GetSubString()
5379 DTSubString sub = new DTSubString();
5382 while (Index + sub.length < len)
5384 DTSubStringType currentType;
5385 Char ch = Value[Index + sub.length];
5386 if (ch >= '0' && ch <= '9')
5388 currentType = DTSubStringType.Number;
5392 currentType = DTSubStringType.Other;
5395 if (sub.length == 0)
5397 sub.type = currentType;
5401 if (sub.type != currentType)
5407 if (currentType == DTSubStringType.Number)
5409 // Incorporate the number into the value
5410 // Limit the digits to prevent overflow
5411 if (sub.length > DateTimeParse.MaxDateTimeNumberDigits)
5413 sub.type = DTSubStringType.Invalid;
5416 int number = ch - '0';
5417 Debug.Assert(number >= 0 && number <= 9, "number >= 0 && number <= 9");
5418 sub.value = sub.value * 10 + number;
5422 // For non numbers, just return this length 1 token. This should be expanded
5423 // to more types of thing if this parsing approach is used for things other
5424 // than numbers and single characters
5428 if (sub.length == 0)
5430 sub.type = DTSubStringType.End;
5437 internal void ConsumeSubString(DTSubString sub)
5439 Debug.Assert(sub.index == Index, "sub.index == Index");
5440 Debug.Assert(sub.index + sub.length <= len, "sub.index + sub.length <= len");
5441 Index = sub.index + sub.length;
5444 m_current = Value[Index];
5449 internal enum DTSubStringType
5458 internal struct DTSubString
5461 internal Int32 index;
5462 internal Int32 length;
5463 internal DTSubStringType type;
5464 internal Int32 value;
5466 internal Char this[Int32 relativeIndex]
5470 return s[index + relativeIndex];
5476 // The buffer to store the parsing token.
5479 struct DateTimeToken
5481 internal DateTimeParse.DTT dtt; // Store the token
5482 internal TokenType suffix; // Store the CJK Year/Month/Day suffix (if any)
5483 internal int num; // Store the number that we are parsing (if any)
5487 // The buffer to store temporary parsing information.
5490 unsafe struct DateTimeRawInfo
5493 internal int numCount;
5496 internal int dayOfWeek;
5498 internal DateTimeParse.TM timeMark;
5499 internal double fraction;
5500 internal bool hasSameDateAndTimeSeparators;
5502 internal void Init(int* numberBuffer)
5508 timeMark = DateTimeParse.TM.NotSet;
5512 internal unsafe void AddNumber(int value)
5514 num[numCount++] = value;
5516 internal unsafe int GetNumber(int index)
5522 internal enum ParseFailureKind
5527 FormatWithParameter = 3,
5528 FormatBadDateTimeCalendar = 4, // FormatException when ArgumentOutOfRange is thrown by a Calendar.TryToDateTime().
5532 internal enum ParseFlags
5534 HaveYear = 0x00000001,
5535 HaveMonth = 0x00000002,
5536 HaveDay = 0x00000004,
5537 HaveHour = 0x00000008,
5538 HaveMinute = 0x00000010,
5539 HaveSecond = 0x00000020,
5540 HaveTime = 0x00000040,
5541 HaveDate = 0x00000080,
5542 TimeZoneUsed = 0x00000100,
5543 TimeZoneUtc = 0x00000200,
5544 ParsedMonthName = 0x00000400,
5545 CaptureOffset = 0x00000800,
5546 YearDefault = 0x00001000,
5547 Rfc1123Pattern = 0x00002000,
5548 UtcSortPattern = 0x00004000,
5552 // This will store the result of the parsing. And it will be eventually
5553 // used to construct a DateTime instance.
5556 struct DateTimeResult
5562 // Set time defualt to 00:00:00.
5565 internal int Minute;
5566 internal int Second;
5567 internal double fraction;
5571 internal ParseFlags flags;
5573 internal TimeSpan timeZoneOffset;
5575 internal Calendar calendar;
5577 internal DateTime parsedDate;
5579 internal ParseFailureKind failure;
5580 internal string failureMessageID;
5581 internal object failureMessageFormatArgument;
5582 internal string failureArgumentName;
5584 internal void Init()
5593 internal void SetDate(int year, int month, int day)
5599 internal void SetFailure(ParseFailureKind failure, string failureMessageID, object failureMessageFormatArgument)
5601 this.failure = failure;
5602 this.failureMessageID = failureMessageID;
5603 this.failureMessageFormatArgument = failureMessageFormatArgument;
5606 internal void SetFailure(ParseFailureKind failure, string failureMessageID, object failureMessageFormatArgument, string failureArgumentName)
5608 this.failure = failure;
5609 this.failureMessageID = failureMessageID;
5610 this.failureMessageFormatArgument = failureMessageFormatArgument;
5611 this.failureArgumentName = failureArgumentName;
5615 // This is the helper data structure used in ParseExact().
5616 internal struct ParsingInfo
5618 internal Calendar calendar;
5619 internal int dayOfWeek;
5620 internal DateTimeParse.TM timeMark;
5622 internal bool fUseHour12;
5623 internal bool fUseTwoDigitYear;
5624 internal bool fAllowInnerWhite;
5625 internal bool fAllowTrailingWhite;
5626 internal bool fCustomNumberParser;
5627 internal DateTimeParse.MatchNumberDelegate parseNumberDelegate;
5629 internal void Init()
5632 timeMark = DateTimeParse.TM.NotSet;
5637 // The type of token that will be returned by DateTimeFormatInfo.Tokenize().
5639 internal enum TokenType
5641 // The valid token should start from 1.
5643 // Regular tokens. The range is from 0x00 ~ 0xff.
5644 NumberToken = 1, // The number. E.g. "12"
5645 YearNumberToken = 2, // The number which is considered as year number, which has 3 or more digits. E.g. "2003"
5646 Am = 3, // AM timemark. E.g. "AM"
5647 Pm = 4, // PM timemark. E.g. "PM"
5648 MonthToken = 5, // A word (or words) that represents a month name. E.g. "March"
5649 EndOfString = 6, // End of string
5650 DayOfWeekToken = 7, // A word (or words) that represents a day of week name. E.g. "Monday" or "Mon"
5651 TimeZoneToken = 8, // A word that represents a timezone name. E.g. "GMT"
5652 EraToken = 9, // A word that represents a era name. E.g. "A.D."
5653 DateWordToken = 10, // A word that can appear in a DateTime string, but serves no parsing semantics. E.g. "de" in Spanish culture.
5654 UnknownToken = 11, // An unknown word, which signals an error in parsing.
5655 HebrewNumber = 12, // A number that is composed of Hebrew text. Hebrew calendar uses Hebrew digits for year values, month values, and day values.
5656 JapaneseEraToken = 13, // Era name for JapaneseCalendar
5657 TEraToken = 14, // Era name for TaiwanCalendar
5658 IgnorableSymbol = 15, // A separator like "," that is equivalent to whitespace
5661 // Separator tokens.
5662 SEP_Unk = 0x100, // Unknown separator.
5663 SEP_End = 0x200, // The end of the parsing string.
5664 SEP_Space = 0x300, // Whitespace (including comma).
5665 SEP_Am = 0x400, // AM timemark. E.g. "AM"
5666 SEP_Pm = 0x500, // PM timemark. E.g. "PM"
5667 SEP_Date = 0x600, // date separator. E.g. "/"
5668 SEP_Time = 0x700, // time separator. E.g. ":"
5669 SEP_YearSuff = 0x800, // Chinese/Japanese/Korean year suffix.
5670 SEP_MonthSuff = 0x900, // Chinese/Japanese/Korean month suffix.
5671 SEP_DaySuff = 0xa00, // Chinese/Japanese/Korean day suffix.
5672 SEP_HourSuff = 0xb00, // Chinese/Japanese/Korean hour suffix.
5673 SEP_MinuteSuff = 0xc00, // Chinese/Japanese/Korean minute suffix.
5674 SEP_SecondSuff = 0xd00, // Chinese/Japanese/Korean second suffix.
5675 SEP_LocalTimeMark = 0xe00, // 'T', used in ISO 8601 format.
5676 SEP_DateOrOffset = 0xf00, // '-' which could be a date separator or start of a time zone offset
5678 RegularTokenMask = 0x00ff,
5679 SeparatorTokenMask = 0xff00,