More corelib cleanup (dotnet/coreclr#26993)
[mono-project.git] / netcore / System.Private.CoreLib / shared / System / Globalization / DateTimeParse.cs
blob188c980cddd0818515b51d1ac870946cffe575e4
1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System.Diagnostics;
6 using System.Globalization;
7 using System.Runtime.CompilerServices;
8 using System.Text;
10 namespace System
12 internal static class DateTimeParse
14 internal const int MaxDateTimeNumberDigits = 8;
16 internal delegate bool MatchNumberDelegate(ref __DTString str, int digitLen, out int result);
18 internal static MatchNumberDelegate m_hebrewNumberParser = new MatchNumberDelegate(DateTimeParse.MatchHebrewDigits);
20 internal static DateTime ParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format, DateTimeFormatInfo dtfi, DateTimeStyles style)
22 DateTimeResult result = default; // The buffer to store the parsing result.
23 result.Init(s);
24 if (TryParseExact(s, format, dtfi, style, ref result))
26 return result.parsedDate;
28 else
30 throw GetDateTimeParseException(ref result);
34 internal static DateTime ParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format, DateTimeFormatInfo dtfi, DateTimeStyles style, out TimeSpan offset)
36 DateTimeResult result = default; // The buffer to store the parsing result.
37 result.Init(s);
38 result.flags |= ParseFlags.CaptureOffset;
39 if (TryParseExact(s, format, dtfi, style, ref result))
41 offset = result.timeZoneOffset;
42 return result.parsedDate;
44 else
46 throw GetDateTimeParseException(ref result);
50 internal static bool TryParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format, DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result)
52 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
53 resultData.Init(s);
55 if (TryParseExact(s, format, dtfi, style, ref resultData))
57 result = resultData.parsedDate;
58 return true;
61 result = DateTime.MinValue;
62 return false;
65 internal static bool TryParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format, DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result, out TimeSpan offset)
67 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
68 resultData.Init(s);
69 resultData.flags |= ParseFlags.CaptureOffset;
71 if (TryParseExact(s, format, dtfi, style, ref resultData))
73 result = resultData.parsedDate;
74 offset = resultData.timeZoneOffset;
75 return true;
78 result = DateTime.MinValue;
79 offset = TimeSpan.Zero;
80 return false;
83 internal static bool TryParseExact(ReadOnlySpan<char> s, ReadOnlySpan<char> format, DateTimeFormatInfo dtfi, DateTimeStyles style, ref DateTimeResult result)
85 if (s.Length == 0)
87 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDateTime));
88 return false;
91 if (format.Length == 0)
93 result.SetBadFormatSpecifierFailure();
94 return false;
97 Debug.Assert(dtfi != null, "dtfi == null");
99 return DoStrictParse(s, format, style, dtfi, ref result);
102 internal static DateTime ParseExactMultiple(ReadOnlySpan<char> s, string[] formats,
103 DateTimeFormatInfo dtfi, DateTimeStyles style)
105 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
106 result.Init(s);
107 if (TryParseExactMultiple(s, formats, dtfi, style, ref result))
109 return result.parsedDate;
111 else
113 throw GetDateTimeParseException(ref result);
117 internal static DateTime ParseExactMultiple(ReadOnlySpan<char> s, string[] formats,
118 DateTimeFormatInfo dtfi, DateTimeStyles style, out TimeSpan offset)
120 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
121 result.Init(s);
122 result.flags |= ParseFlags.CaptureOffset;
123 if (TryParseExactMultiple(s, formats, dtfi, style, ref result))
125 offset = result.timeZoneOffset;
126 return result.parsedDate;
128 else
130 throw GetDateTimeParseException(ref result);
134 internal static bool TryParseExactMultiple(ReadOnlySpan<char> s, string?[]? formats,
135 DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result, out TimeSpan offset)
137 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
138 resultData.Init(s);
139 resultData.flags |= ParseFlags.CaptureOffset;
141 if (TryParseExactMultiple(s, formats, dtfi, style, ref resultData))
143 result = resultData.parsedDate;
144 offset = resultData.timeZoneOffset;
145 return true;
148 result = DateTime.MinValue;
149 offset = TimeSpan.Zero;
150 return false;
153 internal static bool TryParseExactMultiple(ReadOnlySpan<char> s, string?[]? formats,
154 DateTimeFormatInfo dtfi, DateTimeStyles style, out DateTime result)
156 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
157 resultData.Init(s);
159 if (TryParseExactMultiple(s, formats, dtfi, style, ref resultData))
161 result = resultData.parsedDate;
162 return true;
165 result = DateTime.MinValue;
166 return false;
169 internal static bool TryParseExactMultiple(ReadOnlySpan<char> s, string?[]? formats,
170 DateTimeFormatInfo dtfi, DateTimeStyles style, ref DateTimeResult result)
172 if (formats == null)
174 result.SetFailure(ParseFailureKind.ArgumentNull, nameof(SR.ArgumentNull_String), null, nameof(formats));
175 return false;
178 if (s.Length == 0)
180 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDateTime));
181 return false;
184 if (formats.Length == 0)
186 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_NoFormatSpecifier));
187 return false;
190 Debug.Assert(dtfi != null, "dtfi == null");
193 // Do a loop through the provided formats and see if we can parse successfully in
194 // one of the formats.
196 for (int i = 0; i < formats.Length; i++)
198 if (formats[i] == null || formats[i]!.Length == 0) // TODO-NULLABLE: Indexer nullability tracked (https://github.com/dotnet/roslyn/issues/34644)
200 result.SetBadFormatSpecifierFailure();
201 return false;
203 // Create a new result each time to ensure the runs are independent. Carry through
204 // flags from the caller and return the result.
205 DateTimeResult innerResult = new DateTimeResult(); // The buffer to store the parsing result.
206 innerResult.Init(s);
207 innerResult.flags = result.flags;
208 if (TryParseExact(s, formats[i], dtfi, style, ref innerResult))
210 result.parsedDate = innerResult.parsedDate;
211 result.timeZoneOffset = innerResult.timeZoneOffset;
212 return true;
215 result.SetBadDateTimeFailure();
216 return false;
219 ////////////////////////////////////////////////////////////////////////////
220 // Date Token Types
222 // Following is the set of tokens that can be generated from a date
223 // string. Notice that the legal set of trailing separators have been
224 // folded in with the date number, and month name tokens. This set
225 // of tokens is chosen to reduce the number of date parse states.
227 ////////////////////////////////////////////////////////////////////////////
229 internal enum DTT : int
231 End = 0, // '\0'
232 NumEnd = 1, // Num[ ]*[\0]
233 NumAmpm = 2, // Num[ ]+AmPm
234 NumSpace = 3, // Num[ ]+^[Dsep|Tsep|'0\']
235 NumDatesep = 4, // Num[ ]*Dsep
236 NumTimesep = 5, // Num[ ]*Tsep
237 MonthEnd = 6, // Month[ ]*'\0'
238 MonthSpace = 7, // Month[ ]+^[Dsep|Tsep|'\0']
239 MonthDatesep = 8, // Month[ ]*Dsep
240 NumDatesuff = 9, // Month[ ]*DSuff
241 NumTimesuff = 10, // Month[ ]*TSuff
242 DayOfWeek = 11, // Day of week name
243 YearSpace = 12, // Year+^[Dsep|Tsep|'0\']
244 YearDateSep = 13, // Year+Dsep
245 YearEnd = 14, // Year+['\0']
246 TimeZone = 15, // timezone name
247 Era = 16, // era name
248 NumUTCTimeMark = 17, // Num + 'Z'
249 // When you add a new token which will be in the
250 // state table, add it after NumLocalTimeMark.
251 Unk = 18, // unknown
252 NumLocalTimeMark = 19, // Num + 'T'
253 Max = 20, // marker
256 internal enum TM
258 NotSet = -1,
259 AM = 0,
260 PM = 1,
263 ////////////////////////////////////////////////////////////////////////////
265 // DateTime parsing state enumeration (DS.*)
267 ////////////////////////////////////////////////////////////////////////////
269 internal enum DS
271 BEGIN = 0,
272 N = 1, // have one number
273 NN = 2, // have two numbers
275 // The following are known to be part of a date
277 D_Nd = 3, // date string: have number followed by date separator
278 D_NN = 4, // date string: have two numbers
279 D_NNd = 5, // date string: have two numbers followed by date separator
281 D_M = 6, // date string: have a month
282 D_MN = 7, // date string: have a month and a number
283 D_NM = 8, // date string: have a number and a month
284 D_MNd = 9, // date string: have a month and number followed by date separator
285 D_NDS = 10, // date string: have one number followed a date suffix.
287 D_Y = 11, // date string: have a year.
288 D_YN = 12, // date string: have a year and a number
289 D_YNd = 13, // date string: have a year and a number and a date separator
290 D_YM = 14, // date string: have a year and a month
291 D_YMd = 15, // date string: have a year and a month and a date separator
292 D_S = 16, // have numbers followed by a date suffix.
293 T_S = 17, // have numbers followed by a time suffix.
295 // The following are known to be part of a time
297 T_Nt = 18, // have num followed by time separator
298 T_NNt = 19, // have two numbers followed by time separator
300 ERROR = 20,
302 // The following are terminal states. These all have an action
303 // associated with them; and transition back to BEGIN.
305 DX_NN = 21, // day from two numbers
306 DX_NNN = 22, // day from three numbers
307 DX_MN = 23, // day from month and one number
308 DX_NM = 24, // day from month and one number
309 DX_MNN = 25, // day from month and two numbers
310 DX_DS = 26, // a set of date suffixed numbers.
311 DX_DSN = 27, // day from date suffixes and one number.
312 DX_NDS = 28, // day from one number and date suffixes .
313 DX_NNDS = 29, // day from one number and date suffixes .
315 DX_YNN = 30, // date string: have a year and two number
316 DX_YMN = 31, // date string: have a year, a month, and a number.
317 DX_YN = 32, // date string: have a year and one number
318 DX_YM = 33, // date string: have a year, a month.
319 TX_N = 34, // time from one number (must have ampm)
320 TX_NN = 35, // time from two numbers
321 TX_NNN = 36, // time from three numbers
322 TX_TS = 37, // a set of time suffixed numbers.
323 DX_NNY = 38,
326 ////////////////////////////////////////////////////////////////////////////
328 // NOTE: The following state machine table is dependent on the order of the
329 // DS and DTT enumerations.
331 // For each non terminal state, the following table defines the next state
332 // for each given date token type.
334 ////////////////////////////////////////////////////////////////////////////
336 // End NumEnd NumAmPm NumSpace NumDaySep NumTimesep MonthEnd MonthSpace MonthDSep NumDateSuff NumTimeSuff DayOfWeek YearSpace YearDateSep YearEnd TimeZone Era UTCTimeMark
337 private static readonly DS[][] dateParsingStates = {
338 // DS.BEGIN // DS.BEGIN
339 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 },
341 // DS.N // DS.N
342 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 },
344 // DS.NN // DS.NN
345 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 },
347 // DS.D_Nd // DS.D_Nd
348 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 },
350 // DS.D_NN // DS.D_NN
351 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 },
353 // DS.D_NNd // DS.D_NNd
354 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 },
356 // DS.D_M // DS.D_M
357 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 },
359 // DS.D_MN // DS.D_MN
360 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 },
362 // DS.D_NM // DS.D_NM
363 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 },
365 // DS.D_MNd // DS.D_MNd
366 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 },
368 // DS.D_NDS, // DS.D_NDS,
369 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 },
371 // DS.D_Y // DS.D_Y
372 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 },
374 // DS.D_YN // DS.D_YN
375 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 },
377 // DS.D_YNd // DS.D_YNd
378 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 },
380 // DS.D_YM // DS.D_YM
381 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 },
383 // DS.D_YMd // DS.D_YMd
384 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 },
386 // DS.D_S // DS.D_S
387 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 },
389 // DS.T_S // DS.T_S
390 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 },
392 // DS.T_Nt // DS.T_Nt
393 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 },
395 // DS.T_NNt // DS.T_NNt
396 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 },
398 // End NumEnd NumAmPm NumSpace NumDaySep NumTimesep MonthEnd MonthSpace MonthDSep NumDateSuff NumTimeSuff DayOfWeek YearSpace YearDateSep YearEnd TimeZone Era UTCMark
400 internal const string GMTName = "GMT";
401 internal const string ZuluName = "Z";
404 // Search from the index of str at str.Index to see if the target string exists in the str.
406 private static bool MatchWord(ref __DTString str, string target)
408 if (target.Length > (str.Value.Length - str.Index))
410 return false;
413 if (str.CompareInfo.Compare(str.Value.Slice(str.Index, target.Length), target, CompareOptions.IgnoreCase) != 0)
415 return false;
418 int nextCharIndex = str.Index + target.Length;
420 if (nextCharIndex < str.Value.Length)
422 char nextCh = str.Value[nextCharIndex];
423 if (char.IsLetter(nextCh))
425 return false;
428 str.Index = nextCharIndex;
429 if (str.Index < str.Length)
431 str.m_current = str.Value[str.Index];
434 return true;
438 // Check the word at the current index to see if it matches GMT name or Zulu name.
440 private static bool GetTimeZoneName(ref __DTString str)
442 if (MatchWord(ref str, GMTName))
444 return true;
447 if (MatchWord(ref str, ZuluName))
449 return true;
452 return false;
455 internal static bool IsDigit(char ch) => (uint)(ch - '0') <= 9;
457 /*=================================ParseFraction==========================
458 **Action: Starting at the str.Index, which should be a decimal symbol.
459 ** if the current character is a digit, parse the remaining
460 ** numbers as fraction. For example, if the sub-string starting at str.Index is "123", then
461 ** the method will return 0.123
462 **Returns: The fraction number.
463 **Arguments:
464 ** str the parsing string
465 **Exceptions:
466 ============================================================================*/
468 private static bool ParseFraction(ref __DTString str, out double result)
470 result = 0;
471 double decimalBase = 0.1;
472 int digits = 0;
473 char ch;
474 while (str.GetNext()
475 && IsDigit(ch = str.m_current))
477 result += (ch - '0') * decimalBase;
478 decimalBase *= 0.1;
479 digits++;
481 return digits > 0;
484 /*=================================ParseTimeZone==========================
485 **Action: Parse the timezone offset in the following format:
486 ** "+8", "+08", "+0800", "+0800"
487 ** This method is used by DateTime.Parse().
488 **Returns: The TimeZone offset.
489 **Arguments:
490 ** str the parsing string
491 **Exceptions:
492 ** FormatException if invalid timezone format is found.
493 ============================================================================*/
495 private static bool ParseTimeZone(ref __DTString str, ref TimeSpan result)
497 // The hour/minute offset for timezone.
498 int hourOffset;
499 int minuteOffset = 0;
501 // Consume the +/- character that has already been read
502 DTSubString sub = str.GetSubString();
503 if (sub.length != 1)
505 return false;
507 char offsetChar = sub[0];
508 if (offsetChar != '+' && offsetChar != '-')
510 return false;
512 str.ConsumeSubString(sub);
514 sub = str.GetSubString();
515 if (sub.type != DTSubStringType.Number)
517 return false;
519 int value = sub.value;
520 int length = sub.length;
521 if (length == 1 || length == 2)
523 // Parsing "+8" or "+08"
524 hourOffset = value;
525 str.ConsumeSubString(sub);
526 // See if we have minutes
527 sub = str.GetSubString();
528 if (sub.length == 1 && sub[0] == ':')
530 // Parsing "+8:00" or "+08:00"
531 str.ConsumeSubString(sub);
532 sub = str.GetSubString();
533 if (sub.type != DTSubStringType.Number || sub.length < 1 || sub.length > 2)
535 return false;
537 minuteOffset = sub.value;
538 str.ConsumeSubString(sub);
541 else if (length == 3 || length == 4)
543 // Parsing "+800" or "+0800"
544 hourOffset = value / 100;
545 minuteOffset = value % 100;
546 str.ConsumeSubString(sub);
548 else
550 // Wrong number of digits
551 return false;
553 Debug.Assert(hourOffset >= 0 && hourOffset <= 99, "hourOffset >= 0 && hourOffset <= 99");
554 Debug.Assert(minuteOffset >= 0 && minuteOffset <= 99, "minuteOffset >= 0 && minuteOffset <= 99");
555 if (minuteOffset < 0 || minuteOffset >= 60)
557 return false;
560 result = new TimeSpan(hourOffset, minuteOffset, 0);
561 if (offsetChar == '-')
563 result = result.Negate();
565 return true;
568 // This is the helper function to handle timezone in string in the format like +/-0800
569 private static bool HandleTimeZone(ref __DTString str, ref DateTimeResult result)
571 if (str.Index < str.Length - 1)
573 char nextCh = str.Value[str.Index];
574 // Skip whitespace, but don't update the index unless we find a time zone marker
575 int whitespaceCount = 0;
576 while (char.IsWhiteSpace(nextCh) && str.Index + whitespaceCount < str.Length - 1)
578 whitespaceCount++;
579 nextCh = str.Value[str.Index + whitespaceCount];
581 if (nextCh == '+' || nextCh == '-')
583 str.Index += whitespaceCount;
584 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
586 // Should not have two timezone offsets.
587 result.SetBadDateTimeFailure();
588 return false;
590 result.flags |= ParseFlags.TimeZoneUsed;
591 if (!ParseTimeZone(ref str, ref result.timeZoneOffset))
593 result.SetBadDateTimeFailure();
594 return false;
598 return true;
602 // This is the lexer. Check the character at the current index, and put the found token in dtok and
603 // some raw date/time information in raw.
605 private static bool Lex(DS dps, ref __DTString str, ref DateTimeToken dtok, ref DateTimeRawInfo raw, ref DateTimeResult result, ref DateTimeFormatInfo dtfi, DateTimeStyles styles)
607 TokenType tokenType;
608 int tokenValue;
609 int indexBeforeSeparator;
610 char charBeforeSeparator;
612 TokenType sep;
613 dtok.dtt = DTT.Unk; // Assume the token is unkown.
615 str.GetRegularToken(out tokenType, out tokenValue, dtfi);
617 #if _LOGGING
618 if (_tracingEnabled)
620 Trace($"Lex({Hex(str.Value)})\tpos:{str.Index}({Hex(str.m_current)}), {tokenType}, DS.{dps}");
622 #endif // _LOGGING
624 // Look at the regular token.
625 switch (tokenType)
627 case TokenType.NumberToken:
628 case TokenType.YearNumberToken:
629 if (raw.numCount == 3 || tokenValue == -1)
631 result.SetBadDateTimeFailure();
632 LexTraceExit("0010", dps);
633 return false;
636 // This is a digit.
638 // If the previous parsing state is DS.T_NNt (like 12:01), and we got another number,
639 // so we will have a terminal state DS.TX_NNN (like 12:01:02).
640 // If the previous parsing state is DS.T_Nt (like 12:), and we got another number,
641 // so we will have a terminal state DS.TX_NN (like 12:01).
643 // Look ahead to see if the following character is a decimal point or timezone offset.
644 // This enables us to parse time in the forms of:
645 // "11:22:33.1234" or "11:22:33-08".
646 if (dps == DS.T_NNt)
648 if (str.Index < str.Length - 1)
650 char nextCh = str.Value[str.Index];
651 if (nextCh == '.')
653 // While ParseFraction can fail, it just means that there were no digits after
654 // the dot. In this case ParseFraction just removes the dot. This is actually
655 // valid for cultures like Albanian, that join the time marker to the time with
656 // with a dot: e.g. "9:03.MD"
657 ParseFraction(ref str, out raw.fraction);
661 if (dps == DS.T_NNt || dps == DS.T_Nt)
663 if (str.Index < str.Length - 1)
665 if (!HandleTimeZone(ref str, ref result))
667 LexTraceExit("0020 (value like \"12:01\" or \"12:\" followed by a non-TZ number", dps);
668 return false;
673 dtok.num = tokenValue;
674 if (tokenType == TokenType.YearNumberToken)
676 if (raw.year == -1)
678 raw.year = tokenValue;
680 // If we have number which has 3 or more digits (like "001" or "0001"),
681 // we assume this number is a year. Save the current raw.numCount in
682 // raw.year.
684 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
686 case TokenType.SEP_End:
687 dtok.dtt = DTT.YearEnd;
688 break;
689 case TokenType.SEP_Am:
690 case TokenType.SEP_Pm:
691 if (raw.timeMark == TM.NotSet)
693 raw.timeMark = (sep == TokenType.SEP_Am ? TM.AM : TM.PM);
694 dtok.dtt = DTT.YearSpace;
696 else
698 result.SetBadDateTimeFailure();
699 LexTraceExit("0030 (TM.AM/TM.PM Happened more than 1x)", dps);
701 break;
702 case TokenType.SEP_Space:
703 dtok.dtt = DTT.YearSpace;
704 break;
705 case TokenType.SEP_Date:
706 dtok.dtt = DTT.YearDateSep;
707 break;
708 case TokenType.SEP_Time:
709 if (!raw.hasSameDateAndTimeSeparators)
711 result.SetBadDateTimeFailure();
712 LexTraceExit("0040 (Invalid separator after number)", dps);
713 return false;
716 // we have the date and time separators are same and getting a year number, then change the token to YearDateSep as
717 // we are sure we are not parsing time.
718 dtok.dtt = DTT.YearDateSep;
719 break;
720 case TokenType.SEP_DateOrOffset:
721 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
722 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
723 if ((dateParsingStates[(int)dps][(int)DTT.YearDateSep] == DS.ERROR)
724 && (dateParsingStates[(int)dps][(int)DTT.YearSpace] > DS.ERROR))
726 str.Index = indexBeforeSeparator;
727 str.m_current = charBeforeSeparator;
728 dtok.dtt = DTT.YearSpace;
730 else
732 dtok.dtt = DTT.YearDateSep;
734 break;
735 case TokenType.SEP_YearSuff:
736 case TokenType.SEP_MonthSuff:
737 case TokenType.SEP_DaySuff:
738 dtok.dtt = DTT.NumDatesuff;
739 dtok.suffix = sep;
740 break;
741 case TokenType.SEP_HourSuff:
742 case TokenType.SEP_MinuteSuff:
743 case TokenType.SEP_SecondSuff:
744 dtok.dtt = DTT.NumTimesuff;
745 dtok.suffix = sep;
746 break;
747 default:
748 // Invalid separator after number number.
749 result.SetBadDateTimeFailure();
750 LexTraceExit("0040 (Invalid separator after number)", dps);
751 return false;
754 // Found the token already. Return now.
756 LexTraceExit("0050 (success)", dps);
757 return true;
759 result.SetBadDateTimeFailure();
760 LexTraceExit("0060", dps);
761 return false;
763 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
766 // Note here we check if the numCount is less than three.
767 // When we have more than three numbers, it will be caught as error in the state machine.
769 case TokenType.SEP_End:
770 dtok.dtt = DTT.NumEnd;
771 raw.AddNumber(dtok.num);
772 break;
773 case TokenType.SEP_Am:
774 case TokenType.SEP_Pm:
775 if (raw.timeMark == TM.NotSet)
777 raw.timeMark = (sep == TokenType.SEP_Am ? TM.AM : TM.PM);
778 dtok.dtt = DTT.NumAmpm;
779 // Fix AM/PM parsing case, e.g. "1/10 5 AM"
780 if (dps == DS.D_NN)
782 if (!ProcessTerminalState(DS.DX_NN, ref str, ref result, ref styles, ref raw, dtfi))
784 return false;
788 raw.AddNumber(dtok.num);
790 else
792 result.SetBadDateTimeFailure();
793 break;
795 if (dps == DS.T_NNt || dps == DS.T_Nt)
797 if (!HandleTimeZone(ref str, ref result))
799 LexTraceExit("0070 (HandleTimeZone returned false)", dps);
800 return false;
803 break;
804 case TokenType.SEP_Space:
805 dtok.dtt = DTT.NumSpace;
806 raw.AddNumber(dtok.num);
807 break;
808 case TokenType.SEP_Date:
809 dtok.dtt = DTT.NumDatesep;
810 raw.AddNumber(dtok.num);
811 break;
812 case TokenType.SEP_DateOrOffset:
813 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
814 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
815 if ((dateParsingStates[(int)dps][(int)DTT.NumDatesep] == DS.ERROR)
816 && (dateParsingStates[(int)dps][(int)DTT.NumSpace] > DS.ERROR))
818 str.Index = indexBeforeSeparator;
819 str.m_current = charBeforeSeparator;
820 dtok.dtt = DTT.NumSpace;
822 else
824 dtok.dtt = DTT.NumDatesep;
826 raw.AddNumber(dtok.num);
827 break;
828 case TokenType.SEP_Time:
829 if (raw.hasSameDateAndTimeSeparators &&
830 (dps == DS.D_Y || dps == DS.D_YN || dps == DS.D_YNd || dps == DS.D_YM || dps == DS.D_YMd))
832 // we are parsing a date and we have the time separator same as date separator, so we mark the token as date separator
833 dtok.dtt = DTT.NumDatesep;
834 raw.AddNumber(dtok.num);
835 break;
837 dtok.dtt = DTT.NumTimesep;
838 raw.AddNumber(dtok.num);
839 break;
840 case TokenType.SEP_YearSuff:
843 dtok.num = dtfi.Calendar.ToFourDigitYear(tokenValue);
845 catch (ArgumentOutOfRangeException)
847 result.SetBadDateTimeFailure();
848 LexTraceExit("0075 (Calendar.ToFourDigitYear failed)", dps);
849 return false;
851 dtok.dtt = DTT.NumDatesuff;
852 dtok.suffix = sep;
853 break;
854 case TokenType.SEP_MonthSuff:
855 case TokenType.SEP_DaySuff:
856 dtok.dtt = DTT.NumDatesuff;
857 dtok.suffix = sep;
858 break;
859 case TokenType.SEP_HourSuff:
860 case TokenType.SEP_MinuteSuff:
861 case TokenType.SEP_SecondSuff:
862 dtok.dtt = DTT.NumTimesuff;
863 dtok.suffix = sep;
864 break;
865 case TokenType.SEP_LocalTimeMark:
866 dtok.dtt = DTT.NumLocalTimeMark;
867 raw.AddNumber(dtok.num);
868 break;
869 default:
870 // Invalid separator after number number.
871 result.SetBadDateTimeFailure();
872 LexTraceExit("0080", dps);
873 return false;
875 break;
876 case TokenType.HebrewNumber:
877 if (tokenValue >= 100)
879 // This is a year number
880 if (raw.year == -1)
882 raw.year = tokenValue;
884 // If we have number which has 3 or more digits (like "001" or "0001"),
885 // we assume this number is a year. Save the current raw.numCount in
886 // raw.year.
888 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
890 case TokenType.SEP_End:
891 dtok.dtt = DTT.YearEnd;
892 break;
893 case TokenType.SEP_Space:
894 dtok.dtt = DTT.YearSpace;
895 break;
896 case TokenType.SEP_DateOrOffset:
897 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
898 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
899 if (dateParsingStates[(int)dps][(int)DTT.YearSpace] > DS.ERROR)
901 str.Index = indexBeforeSeparator;
902 str.m_current = charBeforeSeparator;
903 dtok.dtt = DTT.YearSpace;
904 break;
906 goto default;
907 default:
908 // Invalid separator after number number.
909 result.SetBadDateTimeFailure();
910 LexTraceExit("0090", dps);
911 return false;
914 else
916 // Invalid separator after number number.
917 result.SetBadDateTimeFailure();
918 LexTraceExit("0100", dps);
919 return false;
922 else
924 // This is a day number
925 dtok.num = tokenValue;
926 raw.AddNumber(dtok.num);
928 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
931 // Note here we check if the numCount is less than three.
932 // When we have more than three numbers, it will be caught as error in the state machine.
934 case TokenType.SEP_End:
935 dtok.dtt = DTT.NumEnd;
936 break;
937 case TokenType.SEP_Space:
938 case TokenType.SEP_Date:
939 dtok.dtt = DTT.NumDatesep;
940 break;
941 case TokenType.SEP_DateOrOffset:
942 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
943 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
944 if ((dateParsingStates[(int)dps][(int)DTT.NumDatesep] == DS.ERROR)
945 && (dateParsingStates[(int)dps][(int)DTT.NumSpace] > DS.ERROR))
947 str.Index = indexBeforeSeparator;
948 str.m_current = charBeforeSeparator;
949 dtok.dtt = DTT.NumSpace;
951 else
953 dtok.dtt = DTT.NumDatesep;
955 break;
956 default:
957 // Invalid separator after number number.
958 result.SetBadDateTimeFailure();
959 LexTraceExit("0110", dps);
960 return false;
963 break;
964 case TokenType.DayOfWeekToken:
965 if (raw.dayOfWeek == -1)
968 // This is a day of week name.
970 raw.dayOfWeek = tokenValue;
971 dtok.dtt = DTT.DayOfWeek;
973 else
975 result.SetBadDateTimeFailure();
976 LexTraceExit("0120 (DayOfWeek seen more than 1x)", dps);
977 return false;
979 break;
980 case TokenType.MonthToken:
981 if (raw.month == -1)
984 // This is a month name
986 switch (sep = str.GetSeparatorToken(dtfi, out indexBeforeSeparator, out charBeforeSeparator))
988 case TokenType.SEP_End:
989 dtok.dtt = DTT.MonthEnd;
990 break;
991 case TokenType.SEP_Space:
992 dtok.dtt = DTT.MonthSpace;
993 break;
994 case TokenType.SEP_Date:
995 dtok.dtt = DTT.MonthDatesep;
996 break;
997 case TokenType.SEP_Time:
998 if (!raw.hasSameDateAndTimeSeparators)
1000 result.SetBadDateTimeFailure();
1001 LexTraceExit("0130 (Invalid separator after month name)", dps);
1002 return false;
1005 // we have the date and time separators are same and getting a Month name, then change the token to MonthDatesep as
1006 // we are sure we are not parsing time.
1007 dtok.dtt = DTT.MonthDatesep;
1008 break;
1009 case TokenType.SEP_DateOrOffset:
1010 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
1011 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
1012 if ((dateParsingStates[(int)dps][(int)DTT.MonthDatesep] == DS.ERROR)
1013 && (dateParsingStates[(int)dps][(int)DTT.MonthSpace] > DS.ERROR))
1015 str.Index = indexBeforeSeparator;
1016 str.m_current = charBeforeSeparator;
1017 dtok.dtt = DTT.MonthSpace;
1019 else
1021 dtok.dtt = DTT.MonthDatesep;
1023 break;
1024 default:
1025 // Invalid separator after month name
1026 result.SetBadDateTimeFailure();
1027 LexTraceExit("0130 (Invalid separator after month name)", dps);
1028 return false;
1030 raw.month = tokenValue;
1032 else
1034 result.SetBadDateTimeFailure();
1035 LexTraceExit("0140 (MonthToken seen more than 1x)", dps);
1036 return false;
1038 break;
1039 case TokenType.EraToken:
1040 if (result.era != -1)
1042 result.era = tokenValue;
1043 dtok.dtt = DTT.Era;
1045 else
1047 result.SetBadDateTimeFailure();
1048 LexTraceExit("0150 (EraToken seen when result.era already set)", dps);
1049 return false;
1051 break;
1052 case TokenType.JapaneseEraToken:
1053 // Special case for Japanese. We allow Japanese era name to be used even if the calendar is not Japanese Calendar.
1054 result.calendar = JapaneseCalendar.GetDefaultInstance();
1055 dtfi = DateTimeFormatInfo.GetJapaneseCalendarDTFI();
1056 if (result.era != -1)
1058 result.era = tokenValue;
1059 dtok.dtt = DTT.Era;
1061 else
1063 result.SetBadDateTimeFailure();
1064 LexTraceExit("0160 (JapaneseEraToken seen when result.era already set)", dps);
1065 return false;
1067 break;
1068 case TokenType.TEraToken:
1069 result.calendar = TaiwanCalendar.GetDefaultInstance();
1070 dtfi = DateTimeFormatInfo.GetTaiwanCalendarDTFI();
1071 if (result.era != -1)
1073 result.era = tokenValue;
1074 dtok.dtt = DTT.Era;
1076 else
1078 result.SetBadDateTimeFailure();
1079 LexTraceExit("0170 (TEraToken seen when result.era already set)", dps);
1080 return false;
1082 break;
1083 case TokenType.TimeZoneToken:
1085 // This is a timezone designator
1087 // NOTENOTE : for now, we only support "GMT" and "Z" (for Zulu time).
1089 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
1091 // Should not have two timezone offsets.
1092 result.SetBadDateTimeFailure();
1093 LexTraceExit("0180 (seen GMT or Z more than 1x)", dps);
1094 return false;
1096 dtok.dtt = DTT.TimeZone;
1097 result.flags |= ParseFlags.TimeZoneUsed;
1098 result.timeZoneOffset = new TimeSpan(0);
1099 result.flags |= ParseFlags.TimeZoneUtc;
1100 break;
1101 case TokenType.EndOfString:
1102 dtok.dtt = DTT.End;
1103 break;
1104 case TokenType.DateWordToken:
1105 case TokenType.IgnorableSymbol:
1106 // Date words and ignorable symbols can just be skipped over
1107 break;
1108 case TokenType.Am:
1109 case TokenType.Pm:
1110 if (raw.timeMark == TM.NotSet)
1112 raw.timeMark = (TM)tokenValue;
1114 else
1116 result.SetBadDateTimeFailure();
1117 LexTraceExit("0190 (AM/PM timeMark already set)", dps);
1118 return false;
1120 break;
1121 case TokenType.UnknownToken:
1122 if (char.IsLetter(str.m_current))
1124 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTimeAndParameter, nameof(SR.Format_UnknownDateTimeWord), str.Index);
1125 LexTraceExit("0200", dps);
1126 return false;
1129 if ((str.m_current == '-' || str.m_current == '+') && ((result.flags & ParseFlags.TimeZoneUsed) == 0))
1131 int originalIndex = str.Index;
1132 if (ParseTimeZone(ref str, ref result.timeZoneOffset))
1134 result.flags |= ParseFlags.TimeZoneUsed;
1135 LexTraceExit("0220 (success)", dps);
1136 return true;
1138 else
1140 // Time zone parse attempt failed. Fall through to punctuation handling.
1141 str.Index = originalIndex;
1145 // Visual Basic implements string to date conversions on top of DateTime.Parse:
1146 // CDate("#10/10/95#")
1148 if (VerifyValidPunctuation(ref str))
1150 LexTraceExit("0230 (success)", dps);
1151 return true;
1154 result.SetBadDateTimeFailure();
1155 LexTraceExit("0240", dps);
1156 return false;
1159 LexTraceExit("0250 (success)", dps);
1160 return true;
1163 private static bool VerifyValidPunctuation(ref __DTString str)
1165 // Compatability Behavior. Allow trailing nulls and surrounding hashes
1166 char ch = str.Value[str.Index];
1167 if (ch == '#')
1169 bool foundStart = false;
1170 bool foundEnd = false;
1171 for (int i = 0; i < str.Length; i++)
1173 ch = str.Value[i];
1174 if (ch == '#')
1176 if (foundStart)
1178 if (foundEnd)
1180 // Having more than two hashes is invalid
1181 return false;
1183 else
1185 foundEnd = true;
1188 else
1190 foundStart = true;
1193 else if (ch == '\0')
1195 // Allow nulls only at the end
1196 if (!foundEnd)
1198 return false;
1201 else if (!char.IsWhiteSpace(ch))
1203 // Anything other than whitespace outside hashes is invalid
1204 if (!foundStart || foundEnd)
1206 return false;
1210 if (!foundEnd)
1212 // The has was un-paired
1213 return false;
1215 // Valid Hash usage: eat the hash and continue.
1216 str.GetNext();
1217 return true;
1219 else if (ch == '\0')
1221 for (int i = str.Index; i < str.Length; i++)
1223 if (str.Value[i] != '\0')
1225 // Nulls are only valid if they are the only trailing character
1226 return false;
1229 // Move to the end of the string
1230 str.Index = str.Length;
1231 return true;
1233 return false;
1236 private const int ORDER_YMD = 0; // The order of date is Year/Month/Day.
1237 private const int ORDER_MDY = 1; // The order of date is Month/Day/Year.
1238 private const int ORDER_DMY = 2; // The order of date is Day/Month/Year.
1239 private const int ORDER_YDM = 3; // The order of date is Year/Day/Month
1240 private const int ORDER_YM = 4; // Year/Month order.
1241 private const int ORDER_MY = 5; // Month/Year order.
1242 private const int ORDER_MD = 6; // Month/Day order.
1243 private const int ORDER_DM = 7; // Day/Month order.
1246 // Decide the year/month/day order from the datePattern.
1248 // Return 0 for YMD, 1 for MDY, 2 for DMY, otherwise -1.
1250 private static bool GetYearMonthDayOrder(string datePattern, out int order)
1252 int yearOrder = -1;
1253 int monthOrder = -1;
1254 int dayOrder = -1;
1255 int orderCount = 0;
1257 bool inQuote = false;
1259 for (int i = 0; i < datePattern.Length && orderCount < 3; i++)
1261 char ch = datePattern[i];
1262 if (ch == '\\' || ch == '%')
1264 i++;
1265 continue; // Skip next character that is escaped by this backslash
1268 if (ch == '\'' || ch == '"')
1270 inQuote = !inQuote;
1273 if (!inQuote)
1275 if (ch == 'y')
1277 yearOrder = orderCount++;
1280 // Skip all year pattern charaters.
1282 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'y'; i++)
1284 // Do nothing here.
1287 else if (ch == 'M')
1289 monthOrder = orderCount++;
1291 // Skip all month pattern characters.
1293 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'M'; i++)
1295 // Do nothing here.
1298 else if (ch == 'd')
1300 int patternCount = 1;
1302 // Skip all day pattern characters.
1304 for (; i + 1 < datePattern.Length && datePattern[i + 1] == 'd'; i++)
1306 patternCount++;
1309 // Make sure this is not "ddd" or "dddd", which means day of week.
1311 if (patternCount <= 2)
1313 dayOrder = orderCount++;
1319 if (yearOrder == 0 && monthOrder == 1 && dayOrder == 2)
1321 order = ORDER_YMD;
1322 return true;
1324 if (monthOrder == 0 && dayOrder == 1 && yearOrder == 2)
1326 order = ORDER_MDY;
1327 return true;
1329 if (dayOrder == 0 && monthOrder == 1 && yearOrder == 2)
1331 order = ORDER_DMY;
1332 return true;
1334 if (yearOrder == 0 && dayOrder == 1 && monthOrder == 2)
1336 order = ORDER_YDM;
1337 return true;
1339 order = -1;
1340 return false;
1344 // Decide the year/month order from the pattern.
1346 // Return 0 for YM, 1 for MY, otherwise -1.
1348 private static bool GetYearMonthOrder(string pattern, out int order)
1350 int yearOrder = -1;
1351 int monthOrder = -1;
1352 int orderCount = 0;
1354 bool inQuote = false;
1355 for (int i = 0; i < pattern.Length && orderCount < 2; i++)
1357 char ch = pattern[i];
1358 if (ch == '\\' || ch == '%')
1360 i++;
1361 continue; // Skip next character that is escaped by this backslash
1364 if (ch == '\'' || ch == '"')
1366 inQuote = !inQuote;
1369 if (!inQuote)
1371 if (ch == 'y')
1373 yearOrder = orderCount++;
1376 // Skip all year pattern charaters.
1378 for (; i + 1 < pattern.Length && pattern[i + 1] == 'y'; i++)
1382 else if (ch == 'M')
1384 monthOrder = orderCount++;
1386 // Skip all month pattern characters.
1388 for (; i + 1 < pattern.Length && pattern[i + 1] == 'M'; i++)
1395 if (yearOrder == 0 && monthOrder == 1)
1397 order = ORDER_YM;
1398 return true;
1400 if (monthOrder == 0 && yearOrder == 1)
1402 order = ORDER_MY;
1403 return true;
1405 order = -1;
1406 return false;
1410 // Decide the month/day order from the pattern.
1412 // Return 0 for MD, 1 for DM, otherwise -1.
1414 private static bool GetMonthDayOrder(string pattern, out int order)
1416 int monthOrder = -1;
1417 int dayOrder = -1;
1418 int orderCount = 0;
1420 bool inQuote = false;
1421 for (int i = 0; i < pattern.Length && orderCount < 2; i++)
1423 char ch = pattern[i];
1424 if (ch == '\\' || ch == '%')
1426 i++;
1427 continue; // Skip next character that is escaped by this backslash
1430 if (ch == '\'' || ch == '"')
1432 inQuote = !inQuote;
1435 if (!inQuote)
1437 if (ch == 'd')
1439 int patternCount = 1;
1441 // Skip all day pattern charaters.
1443 for (; i + 1 < pattern.Length && pattern[i + 1] == 'd'; i++)
1445 patternCount++;
1449 // Make sure this is not "ddd" or "dddd", which means day of week.
1451 if (patternCount <= 2)
1453 dayOrder = orderCount++;
1456 else if (ch == 'M')
1458 monthOrder = orderCount++;
1460 // Skip all month pattern characters.
1462 for (; i + 1 < pattern.Length && pattern[i + 1] == 'M'; i++)
1469 if (monthOrder == 0 && dayOrder == 1)
1471 order = ORDER_MD;
1472 return true;
1474 if (dayOrder == 0 && monthOrder == 1)
1476 order = ORDER_DM;
1477 return true;
1479 order = -1;
1480 return false;
1484 // Adjust the two-digit year if necessary.
1486 private static bool TryAdjustYear(ref DateTimeResult result, int year, out int adjustedYear)
1488 if (year < 100)
1492 // the Calendar classes need some real work. Many of the calendars that throw
1493 // don't implement a fast/non-allocating (and non-throwing) IsValid{Year|Day|Month} method.
1494 // we are making a targeted try/catch fix in the in-place release but will revisit this code
1495 // in the next side-by-side release.
1496 year = result.calendar.ToFourDigitYear(year);
1498 catch (ArgumentOutOfRangeException)
1500 adjustedYear = -1;
1501 return false;
1504 adjustedYear = year;
1505 return true;
1508 private static bool SetDateYMD(ref DateTimeResult result, int year, int month, int day)
1510 // Note, longer term these checks should be done at the end of the parse. This current
1511 // way of checking creates order dependence with parsing the era name.
1512 if (result.calendar.IsValidDay(year, month, day, result.era))
1514 result.SetDate(year, month, day); // YMD
1515 return true;
1517 return false;
1520 private static bool SetDateMDY(ref DateTimeResult result, int month, int day, int year)
1522 return SetDateYMD(ref result, year, month, day);
1525 private static bool SetDateDMY(ref DateTimeResult result, int day, int month, int year)
1527 return SetDateYMD(ref result, year, month, day);
1530 private static bool SetDateYDM(ref DateTimeResult result, int year, int day, int month)
1532 return SetDateYMD(ref result, year, month, day);
1535 private static void GetDefaultYear(ref DateTimeResult result, ref DateTimeStyles styles)
1537 result.Year = result.calendar.GetYear(GetDateTimeNow(ref result, ref styles));
1538 result.flags |= ParseFlags.YearDefault;
1541 // Processing teriminal case: DS.DX_NN
1542 private static bool GetDayOfNN(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1544 if ((result.flags & ParseFlags.HaveDate) != 0)
1546 // Multiple dates in the input string
1547 result.SetBadDateTimeFailure();
1548 return false;
1551 int n1 = raw.GetNumber(0);
1552 int n2 = raw.GetNumber(1);
1554 GetDefaultYear(ref result, ref styles);
1556 int order;
1557 if (!GetMonthDayOrder(dtfi.MonthDayPattern, out order))
1559 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1560 return false;
1563 if (order == ORDER_MD)
1565 if (SetDateYMD(ref result, result.Year, n1, n2)) // MD
1567 result.flags |= ParseFlags.HaveDate;
1568 return true;
1571 else
1573 // ORDER_DM
1574 if (SetDateYMD(ref result, result.Year, n2, n1)) // DM
1576 result.flags |= ParseFlags.HaveDate;
1577 return true;
1580 result.SetBadDateTimeFailure();
1581 return false;
1584 // Processing teriminal case: DS.DX_NNN
1585 private static bool GetDayOfNNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1587 if ((result.flags & ParseFlags.HaveDate) != 0)
1589 // Multiple dates in the input string
1590 result.SetBadDateTimeFailure();
1591 return false;
1594 int n1 = raw.GetNumber(0);
1595 int n2 = raw.GetNumber(1);
1596 int n3 = raw.GetNumber(2);
1598 int order;
1599 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, out order))
1601 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1602 return false;
1604 int year;
1606 if (order == ORDER_YMD)
1608 if (TryAdjustYear(ref result, n1, out year) && SetDateYMD(ref result, year, n2, n3)) // YMD
1610 result.flags |= ParseFlags.HaveDate;
1611 return true;
1614 else if (order == ORDER_MDY)
1616 if (TryAdjustYear(ref result, n3, out year) && SetDateMDY(ref result, n1, n2, year)) // MDY
1618 result.flags |= ParseFlags.HaveDate;
1619 return true;
1622 else if (order == ORDER_DMY)
1624 if (TryAdjustYear(ref result, n3, out year) && SetDateDMY(ref result, n1, n2, year)) // DMY
1626 result.flags |= ParseFlags.HaveDate;
1627 return true;
1630 else if (order == ORDER_YDM)
1632 if (TryAdjustYear(ref result, n1, out year) && SetDateYDM(ref result, year, n2, n3)) // YDM
1634 result.flags |= ParseFlags.HaveDate;
1635 return true;
1638 result.SetBadDateTimeFailure();
1639 return false;
1642 private static bool GetDayOfMN(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1644 if ((result.flags & ParseFlags.HaveDate) != 0)
1646 // Multiple dates in the input string
1647 result.SetBadDateTimeFailure();
1648 return false;
1651 // The interpretation is based on the MonthDayPattern and YearMonthPattern
1653 // MonthDayPattern YearMonthPattern Interpretation
1654 // --------------- ---------------- ---------------
1655 // MMMM dd MMMM yyyy Day
1656 // MMMM dd yyyy MMMM Day
1657 // dd MMMM MMMM yyyy Year
1658 // dd MMMM yyyy MMMM Day
1660 // In the first and last cases, it could be either or neither, but a day is a better default interpretation
1661 // than a 2 digit year.
1663 int monthDayOrder;
1664 if (!GetMonthDayOrder(dtfi.MonthDayPattern, out monthDayOrder))
1666 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1667 return false;
1669 if (monthDayOrder == ORDER_DM)
1671 int yearMonthOrder;
1672 if (!GetYearMonthOrder(dtfi.YearMonthPattern, out yearMonthOrder))
1674 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.YearMonthPattern);
1675 return false;
1677 if (yearMonthOrder == ORDER_MY)
1679 int year;
1680 if (!TryAdjustYear(ref result, raw.GetNumber(0), out year) || !SetDateYMD(ref result, year, raw.month, 1))
1682 result.SetBadDateTimeFailure();
1683 return false;
1685 return true;
1689 GetDefaultYear(ref result, ref styles);
1690 if (!SetDateYMD(ref result, result.Year, raw.month, raw.GetNumber(0)))
1692 result.SetBadDateTimeFailure();
1693 return false;
1695 return true;
1698 ////////////////////////////////////////////////////////////////////////
1699 // Actions:
1700 // Deal with the terminal state for Hebrew Month/Day pattern
1702 ////////////////////////////////////////////////////////////////////////
1704 private static bool GetHebrewDayOfNM(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1706 int monthDayOrder;
1707 if (!GetMonthDayOrder(dtfi.MonthDayPattern, out monthDayOrder))
1709 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1710 return false;
1712 result.Month = raw.month;
1713 if (monthDayOrder == ORDER_DM || monthDayOrder == ORDER_MD)
1715 if (result.calendar.IsValidDay(result.Year, result.Month, raw.GetNumber(0), result.era))
1717 result.Day = raw.GetNumber(0);
1718 return true;
1721 result.SetBadDateTimeFailure();
1722 return false;
1725 private static bool GetDayOfNM(ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1727 if ((result.flags & ParseFlags.HaveDate) != 0)
1729 // Multiple dates in the input string
1730 result.SetBadDateTimeFailure();
1731 return false;
1734 // The interpretation is based on the MonthDayPattern and YearMonthPattern
1736 // MonthDayPattern YearMonthPattern Interpretation
1737 // --------------- ---------------- ---------------
1738 // MMMM dd MMMM yyyy Day
1739 // MMMM dd yyyy MMMM Year
1740 // dd MMMM MMMM yyyy Day
1741 // dd MMMM yyyy MMMM Day
1743 // In the first and last cases, it could be either or neither, but a day is a better default interpretation
1744 // than a 2 digit year.
1746 int monthDayOrder;
1747 if (!GetMonthDayOrder(dtfi.MonthDayPattern, out monthDayOrder))
1749 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.MonthDayPattern);
1750 return false;
1752 if (monthDayOrder == ORDER_MD)
1754 int yearMonthOrder;
1755 if (!GetYearMonthOrder(dtfi.YearMonthPattern, out yearMonthOrder))
1757 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.YearMonthPattern);
1758 return false;
1760 if (yearMonthOrder == ORDER_YM)
1762 int year;
1763 if (!TryAdjustYear(ref result, raw.GetNumber(0), out year) || !SetDateYMD(ref result, year, raw.month, 1))
1765 result.SetBadDateTimeFailure();
1766 return false;
1768 return true;
1772 GetDefaultYear(ref result, ref styles);
1773 if (!SetDateYMD(ref result, result.Year, raw.month, raw.GetNumber(0)))
1775 result.SetBadDateTimeFailure();
1776 return false;
1778 return true;
1781 private static bool GetDayOfMNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1783 if ((result.flags & ParseFlags.HaveDate) != 0)
1785 // Multiple dates in the input string
1786 result.SetBadDateTimeFailure();
1787 return false;
1790 int n1 = raw.GetNumber(0);
1791 int n2 = raw.GetNumber(1);
1793 int order;
1794 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, out order))
1796 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1797 return false;
1799 int year;
1801 if (order == ORDER_MDY)
1803 if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1805 result.SetDate(year, raw.month, n1); // MDY
1806 result.flags |= ParseFlags.HaveDate;
1807 return true;
1809 else if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1811 result.SetDate(year, raw.month, n2); // YMD
1812 result.flags |= ParseFlags.HaveDate;
1813 return true;
1816 else if (order == ORDER_YMD)
1818 if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1820 result.SetDate(year, raw.month, n2); // YMD
1821 result.flags |= ParseFlags.HaveDate;
1822 return true;
1824 else if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1826 result.SetDate(year, raw.month, n1); // DMY
1827 result.flags |= ParseFlags.HaveDate;
1828 return true;
1831 else if (order == ORDER_DMY)
1833 if (TryAdjustYear(ref result, n2, out year) && result.calendar.IsValidDay(year, raw.month, n1, result.era))
1835 result.SetDate(year, raw.month, n1); // DMY
1836 result.flags |= ParseFlags.HaveDate;
1837 return true;
1839 else if (TryAdjustYear(ref result, n1, out year) && result.calendar.IsValidDay(year, raw.month, n2, result.era))
1841 result.SetDate(year, raw.month, n2); // YMD
1842 result.flags |= ParseFlags.HaveDate;
1843 return true;
1847 result.SetBadDateTimeFailure();
1848 return false;
1851 private static bool GetDayOfYNN(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1853 if ((result.flags & ParseFlags.HaveDate) != 0)
1855 // Multiple dates in the input string
1856 result.SetBadDateTimeFailure();
1857 return false;
1860 int n1 = raw.GetNumber(0);
1861 int n2 = raw.GetNumber(1);
1862 string pattern = dtfi.ShortDatePattern;
1864 // For compatibility, don't throw if we can't determine the order, but default to YMD instead
1865 int order;
1866 if (GetYearMonthDayOrder(pattern, out order) && order == ORDER_YDM)
1868 if (SetDateYMD(ref result, raw.year, n2, n1))
1870 result.flags |= ParseFlags.HaveDate;
1871 return true; // Year + DM
1874 else
1876 if (SetDateYMD(ref result, raw.year, n1, n2))
1878 result.flags |= ParseFlags.HaveDate;
1879 return true; // Year + MD
1882 result.SetBadDateTimeFailure();
1883 return false;
1886 private static bool GetDayOfNNY(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
1888 if ((result.flags & ParseFlags.HaveDate) != 0)
1890 // Multiple dates in the input string
1891 result.SetBadDateTimeFailure();
1892 return false;
1895 int n1 = raw.GetNumber(0);
1896 int n2 = raw.GetNumber(1);
1898 int order;
1899 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, out order))
1901 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
1902 return false;
1905 if (order == ORDER_MDY || order == ORDER_YMD)
1907 if (SetDateYMD(ref result, raw.year, n1, n2))
1909 result.flags |= ParseFlags.HaveDate;
1910 return true; // MD + Year
1913 else
1915 if (SetDateYMD(ref result, raw.year, n2, n1))
1917 result.flags |= ParseFlags.HaveDate;
1918 return true; // DM + Year
1921 result.SetBadDateTimeFailure();
1922 return false;
1925 private static bool GetDayOfYMN(ref DateTimeResult result, ref DateTimeRawInfo raw)
1927 if ((result.flags & ParseFlags.HaveDate) != 0)
1929 // Multiple dates in the input string
1930 result.SetBadDateTimeFailure();
1931 return false;
1934 if (SetDateYMD(ref result, raw.year, raw.month, raw.GetNumber(0)))
1936 result.flags |= ParseFlags.HaveDate;
1937 return true;
1939 result.SetBadDateTimeFailure();
1940 return false;
1943 private static bool GetDayOfYN(ref DateTimeResult result, ref DateTimeRawInfo raw)
1945 if ((result.flags & ParseFlags.HaveDate) != 0)
1947 // Multiple dates in the input string
1948 result.SetBadDateTimeFailure();
1949 return false;
1952 if (SetDateYMD(ref result, raw.year, raw.GetNumber(0), 1))
1954 result.flags |= ParseFlags.HaveDate;
1955 return true;
1957 result.SetBadDateTimeFailure();
1958 return false;
1961 private static bool GetDayOfYM(ref DateTimeResult result, ref DateTimeRawInfo raw)
1963 if ((result.flags & ParseFlags.HaveDate) != 0)
1965 // Multiple dates in the input string
1966 result.SetBadDateTimeFailure();
1967 return false;
1970 if (SetDateYMD(ref result, raw.year, raw.month, 1))
1972 result.flags |= ParseFlags.HaveDate;
1973 return true;
1975 result.SetBadDateTimeFailure();
1976 return false;
1979 private static void AdjustTimeMark(DateTimeFormatInfo dtfi, ref DateTimeRawInfo raw)
1981 // Specail case for culture which uses AM as empty string.
1982 // E.g. af-ZA (0x0436)
1983 // S1159 \x0000
1984 // S2359 nm
1985 // In this case, if we are parsing a string like "2005/09/14 12:23", we will assume this is in AM.
1987 if (raw.timeMark == TM.NotSet)
1989 if (dtfi.AMDesignator != null && dtfi.PMDesignator != null)
1991 if (dtfi.AMDesignator.Length == 0 && dtfi.PMDesignator.Length != 0)
1993 raw.timeMark = TM.AM;
1995 if (dtfi.PMDesignator.Length == 0 && dtfi.AMDesignator.Length != 0)
1997 raw.timeMark = TM.PM;
2004 // Adjust hour according to the time mark.
2006 private static bool AdjustHour(ref int hour, TM timeMark)
2008 if (timeMark != TM.NotSet)
2010 if (timeMark == TM.AM)
2012 if (hour < 0 || hour > 12)
2014 return false;
2016 hour = (hour == 12) ? 0 : hour;
2018 else
2020 if (hour < 0 || hour > 23)
2022 return false;
2024 if (hour < 12)
2026 hour += 12;
2030 return true;
2033 private static bool GetTimeOfN(ref DateTimeResult result, ref DateTimeRawInfo raw)
2035 if ((result.flags & ParseFlags.HaveTime) != 0)
2037 // Multiple times in the input string
2038 result.SetBadDateTimeFailure();
2039 return false;
2042 // In this case, we need a time mark. Check if so.
2044 if (raw.timeMark == TM.NotSet)
2046 result.SetBadDateTimeFailure();
2047 return false;
2049 result.Hour = raw.GetNumber(0);
2050 result.flags |= ParseFlags.HaveTime;
2051 return true;
2054 private static bool GetTimeOfNN(ref DateTimeResult result, ref DateTimeRawInfo raw)
2056 Debug.Assert(raw.numCount >= 2, "raw.numCount >= 2");
2057 if ((result.flags & ParseFlags.HaveTime) != 0)
2059 // Multiple times in the input string
2060 result.SetBadDateTimeFailure();
2061 return false;
2064 result.Hour = raw.GetNumber(0);
2065 result.Minute = raw.GetNumber(1);
2066 result.flags |= ParseFlags.HaveTime;
2067 return true;
2070 private static bool GetTimeOfNNN(ref DateTimeResult result, ref DateTimeRawInfo raw)
2072 if ((result.flags & ParseFlags.HaveTime) != 0)
2074 // Multiple times in the input string
2075 result.SetBadDateTimeFailure();
2076 return false;
2078 Debug.Assert(raw.numCount >= 3, "raw.numCount >= 3");
2079 result.Hour = raw.GetNumber(0);
2080 result.Minute = raw.GetNumber(1);
2081 result.Second = raw.GetNumber(2);
2082 result.flags |= ParseFlags.HaveTime;
2083 return true;
2087 // Processing terminal state: A Date suffix followed by one number.
2089 private static bool GetDateOfDSN(ref DateTimeResult result, ref DateTimeRawInfo raw)
2091 if (raw.numCount != 1 || result.Day != -1)
2093 result.SetBadDateTimeFailure();
2094 return false;
2096 result.Day = raw.GetNumber(0);
2097 return true;
2100 private static bool GetDateOfNDS(ref DateTimeResult result, ref DateTimeRawInfo raw)
2102 if (result.Month == -1)
2104 // Should have a month suffix
2105 result.SetBadDateTimeFailure();
2106 return false;
2108 if (result.Year != -1)
2110 // Already has a year suffix
2111 result.SetBadDateTimeFailure();
2112 return false;
2114 if (!TryAdjustYear(ref result, raw.GetNumber(0), out result.Year))
2116 // the year value is out of range
2117 result.SetBadDateTimeFailure();
2118 return false;
2120 result.Day = 1;
2121 return true;
2124 private static bool GetDateOfNNDS(ref DateTimeResult result, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2126 // For partial CJK Dates, the only valid formats are with a specified year, followed by two numbers, which
2127 // will be the Month and Day, and with a specified Month, when the numbers are either the year and day or
2128 // day and year, depending on the short date pattern.
2130 if ((result.flags & ParseFlags.HaveYear) != 0)
2132 if (((result.flags & ParseFlags.HaveMonth) == 0) && ((result.flags & ParseFlags.HaveDay) == 0))
2134 if (TryAdjustYear(ref result, raw.year, out result.Year) && SetDateYMD(ref result, result.Year, raw.GetNumber(0), raw.GetNumber(1)))
2136 return true;
2140 else if ((result.flags & ParseFlags.HaveMonth) != 0)
2142 if (((result.flags & ParseFlags.HaveYear) == 0) && ((result.flags & ParseFlags.HaveDay) == 0))
2144 int order;
2145 if (!GetYearMonthDayOrder(dtfi.ShortDatePattern, out order))
2147 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDatePattern), dtfi.ShortDatePattern);
2148 return false;
2150 int year;
2151 if (order == ORDER_YMD)
2153 if (TryAdjustYear(ref result, raw.GetNumber(0), out year) && SetDateYMD(ref result, year, result.Month, raw.GetNumber(1)))
2155 return true;
2158 else
2160 if (TryAdjustYear(ref result, raw.GetNumber(1), out year) && SetDateYMD(ref result, year, result.Month, raw.GetNumber(0)))
2162 return true;
2167 result.SetBadDateTimeFailure();
2168 return false;
2172 // A date suffix is found, use this method to put the number into the result.
2174 private static bool ProcessDateTimeSuffix(ref DateTimeResult result, ref DateTimeRawInfo raw, ref DateTimeToken dtok)
2176 switch (dtok.suffix)
2178 case TokenType.SEP_YearSuff:
2179 if ((result.flags & ParseFlags.HaveYear) != 0)
2181 return false;
2183 result.flags |= ParseFlags.HaveYear;
2184 result.Year = raw.year = dtok.num;
2185 break;
2186 case TokenType.SEP_MonthSuff:
2187 if ((result.flags & ParseFlags.HaveMonth) != 0)
2189 return false;
2191 result.flags |= ParseFlags.HaveMonth;
2192 result.Month = raw.month = dtok.num;
2193 break;
2194 case TokenType.SEP_DaySuff:
2195 if ((result.flags & ParseFlags.HaveDay) != 0)
2197 return false;
2199 result.flags |= ParseFlags.HaveDay;
2200 result.Day = dtok.num;
2201 break;
2202 case TokenType.SEP_HourSuff:
2203 if ((result.flags & ParseFlags.HaveHour) != 0)
2205 return false;
2207 result.flags |= ParseFlags.HaveHour;
2208 result.Hour = dtok.num;
2209 break;
2210 case TokenType.SEP_MinuteSuff:
2211 if ((result.flags & ParseFlags.HaveMinute) != 0)
2213 return false;
2215 result.flags |= ParseFlags.HaveMinute;
2216 result.Minute = dtok.num;
2217 break;
2218 case TokenType.SEP_SecondSuff:
2219 if ((result.flags & ParseFlags.HaveSecond) != 0)
2221 return false;
2223 result.flags |= ParseFlags.HaveSecond;
2224 result.Second = dtok.num;
2225 break;
2227 return true;
2230 ////////////////////////////////////////////////////////////////////////
2232 // Actions:
2233 // This is used by DateTime.Parse().
2234 // Process the terminal state for the Hebrew calendar parsing.
2236 ////////////////////////////////////////////////////////////////////////
2238 internal static bool ProcessHebrewTerminalState(DS dps, ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2240 // The following are accepted terminal state for Hebrew date.
2241 switch (dps)
2243 case DS.DX_MNN:
2244 // Deal with the default long/short date format when the year number is ambigous (i.e. year < 100).
2245 raw.year = raw.GetNumber(1);
2246 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2248 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
2249 return false;
2251 if (!GetDayOfMNN(ref result, ref raw, dtfi))
2253 return false;
2255 break;
2256 case DS.DX_YMN:
2257 // Deal with the default long/short date format when the year number is NOT ambigous (i.e. year >= 100).
2258 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2260 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
2261 return false;
2263 if (!GetDayOfYMN(ref result, ref raw))
2265 return false;
2267 break;
2268 case DS.DX_NNY:
2269 // When formatting, we only format up to the hundred digit of the Hebrew year, although Hebrew year is now over 5000.
2270 // E.g. if the year is 5763, we only format as 763. so we do the reverse when parsing.
2271 if (raw.year < 1000)
2273 raw.year += 5000;
2275 if (!GetDayOfNNY(ref result, ref raw, dtfi))
2277 return false;
2279 if (!dtfi.YearMonthAdjustment(ref result.Year, ref raw.month, true))
2281 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
2282 return false;
2284 break;
2285 case DS.DX_NM:
2286 case DS.DX_MN:
2287 // Deal with Month/Day pattern.
2288 GetDefaultYear(ref result, ref styles);
2289 if (!dtfi.YearMonthAdjustment(ref result.Year, ref raw.month, true))
2291 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
2292 return false;
2294 if (!GetHebrewDayOfNM(ref result, ref raw, dtfi))
2296 return false;
2298 break;
2299 case DS.DX_YM:
2300 // Deal with Year/Month pattern.
2301 if (!dtfi.YearMonthAdjustment(ref raw.year, ref raw.month, true))
2303 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
2304 return false;
2306 if (!GetDayOfYM(ref result, ref raw))
2308 return false;
2310 break;
2311 case DS.TX_N:
2312 // Deal hour + AM/PM
2313 if (!GetTimeOfN(ref result, ref raw))
2315 return false;
2317 break;
2318 case DS.TX_NN:
2319 if (!GetTimeOfNN(ref result, ref raw))
2321 return false;
2323 break;
2324 case DS.TX_NNN:
2325 if (!GetTimeOfNNN(ref result, ref raw))
2327 return false;
2329 break;
2330 default:
2331 result.SetBadDateTimeFailure();
2332 return false;
2334 if (dps > DS.ERROR)
2337 // We have reached a terminal state. Reset the raw num count.
2339 raw.numCount = 0;
2341 return true;
2345 // A terminal state has been reached, call the appropriate function to fill in the parsing result.
2346 // Return true if the state is a terminal state.
2348 internal static bool ProcessTerminalState(DS dps, ref __DTString str, ref DateTimeResult result, ref DateTimeStyles styles, ref DateTimeRawInfo raw, DateTimeFormatInfo dtfi)
2350 bool passed = true;
2351 switch (dps)
2353 case DS.DX_NN:
2354 passed = GetDayOfNN(ref result, ref styles, ref raw, dtfi);
2355 break;
2356 case DS.DX_NNN:
2357 passed = GetDayOfNNN(ref result, ref raw, dtfi);
2358 break;
2359 case DS.DX_MN:
2360 passed = GetDayOfMN(ref result, ref styles, ref raw, dtfi);
2361 break;
2362 case DS.DX_NM:
2363 passed = GetDayOfNM(ref result, ref styles, ref raw, dtfi);
2364 break;
2365 case DS.DX_MNN:
2366 passed = GetDayOfMNN(ref result, ref raw, dtfi);
2367 break;
2368 case DS.DX_DS:
2369 // The result has got the correct value. No need to process.
2370 passed = true;
2371 break;
2372 case DS.DX_YNN:
2373 passed = GetDayOfYNN(ref result, ref raw, dtfi);
2374 break;
2375 case DS.DX_NNY:
2376 passed = GetDayOfNNY(ref result, ref raw, dtfi);
2377 break;
2378 case DS.DX_YMN:
2379 passed = GetDayOfYMN(ref result, ref raw);
2380 break;
2381 case DS.DX_YN:
2382 passed = GetDayOfYN(ref result, ref raw);
2383 break;
2384 case DS.DX_YM:
2385 passed = GetDayOfYM(ref result, ref raw);
2386 break;
2387 case DS.TX_N:
2388 passed = GetTimeOfN(ref result, ref raw);
2389 break;
2390 case DS.TX_NN:
2391 passed = GetTimeOfNN(ref result, ref raw);
2392 break;
2393 case DS.TX_NNN:
2394 passed = GetTimeOfNNN(ref result, ref raw);
2395 break;
2396 case DS.TX_TS:
2397 // The result has got the correct value. Nothing to do.
2398 passed = true;
2399 break;
2400 case DS.DX_DSN:
2401 passed = GetDateOfDSN(ref result, ref raw);
2402 break;
2403 case DS.DX_NDS:
2404 passed = GetDateOfNDS(ref result, ref raw);
2405 break;
2406 case DS.DX_NNDS:
2407 passed = GetDateOfNNDS(ref result, ref raw, dtfi);
2408 break;
2411 PTSTraceExit(dps, passed);
2412 if (!passed)
2414 return false;
2417 if (dps > DS.ERROR)
2420 // We have reached a terminal state. Reset the raw num count.
2422 raw.numCount = 0;
2424 return true;
2427 internal static DateTime Parse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles)
2429 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
2430 result.Init(s);
2431 if (TryParse(s, dtfi, styles, ref result))
2433 return result.parsedDate;
2435 else
2437 throw GetDateTimeParseException(ref result);
2441 internal static DateTime Parse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out TimeSpan offset)
2443 DateTimeResult result = new DateTimeResult(); // The buffer to store the parsing result.
2444 result.Init(s);
2445 result.flags |= ParseFlags.CaptureOffset;
2446 if (TryParse(s, dtfi, styles, ref result))
2448 offset = result.timeZoneOffset;
2449 return result.parsedDate;
2451 else
2453 throw GetDateTimeParseException(ref result);
2457 internal static bool TryParse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out DateTime result)
2459 DateTimeResult resultData = new DateTimeResult(); // The buffer to store the parsing result.
2460 resultData.Init(s);
2462 if (TryParse(s, dtfi, styles, ref resultData))
2464 result = resultData.parsedDate;
2465 return true;
2468 result = DateTime.MinValue;
2469 return false;
2472 internal static bool TryParse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, out DateTime result, out TimeSpan offset)
2474 DateTimeResult parseResult = new DateTimeResult(); // The buffer to store the parsing result.
2475 parseResult.Init(s);
2476 parseResult.flags |= ParseFlags.CaptureOffset;
2478 if (TryParse(s, dtfi, styles, ref parseResult))
2480 result = parseResult.parsedDate;
2481 offset = parseResult.timeZoneOffset;
2482 return true;
2485 result = DateTime.MinValue;
2486 offset = TimeSpan.Zero;
2487 return false;
2491 // This is the real method to do the parsing work.
2493 internal static bool TryParse(ReadOnlySpan<char> s, DateTimeFormatInfo dtfi, DateTimeStyles styles, ref DateTimeResult result)
2495 if (s.Length == 0)
2497 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadDateTime));
2498 return false;
2501 Debug.Assert(dtfi != null, "dtfi == null");
2503 #if _LOGGING
2504 DTFITrace(dtfi);
2505 #endif
2507 DateTime time;
2509 // First try the predefined format.
2512 DS dps = DS.BEGIN; // Date Parsing State.
2513 bool reachTerminalState = false;
2515 DateTimeToken dtok = new DateTimeToken(); // The buffer to store the parsing token.
2516 dtok.suffix = TokenType.SEP_Unk;
2517 DateTimeRawInfo raw = new DateTimeRawInfo(); // The buffer to store temporary parsing information.
2518 unsafe
2520 int* numberPointer = stackalloc int[3];
2521 raw.Init(numberPointer);
2523 raw.hasSameDateAndTimeSeparators = dtfi.DateSeparator.Equals(dtfi.TimeSeparator, StringComparison.Ordinal);
2525 result.calendar = dtfi.Calendar;
2526 result.era = Calendar.CurrentEra;
2529 // The string to be parsed. Use a __DTString wrapper so that we can trace the index which
2530 // indicates the begining of next token.
2532 __DTString str = new __DTString(s, dtfi);
2534 str.GetNext();
2537 // The following loop will break out when we reach the end of the str.
2542 // Call the lexer to get the next token.
2544 // If we find a era in Lex(), the era value will be in raw.era.
2545 if (!Lex(dps, ref str, ref dtok, ref raw, ref result, ref dtfi, styles))
2547 TPTraceExit("0000", dps);
2548 return false;
2552 // If the token is not unknown, process it.
2553 // Otherwise, just discard it.
2555 if (dtok.dtt != DTT.Unk)
2558 // Check if we got any CJK Date/Time suffix.
2559 // Since the Date/Time suffix tells us the number belongs to year/month/day/hour/minute/second,
2560 // store the number in the appropriate field in the result.
2562 if (dtok.suffix != TokenType.SEP_Unk)
2564 if (!ProcessDateTimeSuffix(ref result, ref raw, ref dtok))
2566 result.SetBadDateTimeFailure();
2567 TPTraceExit("0010", dps);
2568 return false;
2571 dtok.suffix = TokenType.SEP_Unk; // Reset suffix to SEP_Unk;
2574 if (dtok.dtt == DTT.NumLocalTimeMark)
2576 if (dps == DS.D_YNd || dps == DS.D_YN)
2578 // Consider this as ISO 8601 format:
2579 // "yyyy-MM-dd'T'HH:mm:ss" 1999-10-31T02:00:00
2580 TPTraceExit("0020", dps);
2581 return ParseISO8601(ref raw, ref str, styles, ref result);
2583 else
2585 result.SetBadDateTimeFailure();
2586 TPTraceExit("0030", dps);
2587 return false;
2591 if (raw.hasSameDateAndTimeSeparators)
2593 if (dtok.dtt == DTT.YearEnd || dtok.dtt == DTT.YearSpace || dtok.dtt == DTT.YearDateSep)
2595 // When time and date separators are same and we are hitting a year number while the first parsed part of the string was recognized
2596 // 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
2597 if (dps == DS.T_Nt)
2599 dps = DS.D_Nd;
2601 if (dps == DS.T_NNt)
2603 dps = DS.D_NNd;
2607 bool atEnd = str.AtEnd();
2608 if (dateParsingStates[(int)dps][(int)dtok.dtt] == DS.ERROR || atEnd)
2610 switch (dtok.dtt)
2612 // we have the case of Serbia have dates in forms 'd.M.yyyy.' so we can expect '.' after the date parts.
2613 // changing the token to end with space instead of Date Separator will avoid failing the parsing.
2615 case DTT.YearDateSep: dtok.dtt = atEnd ? DTT.YearEnd : DTT.YearSpace; break;
2616 case DTT.NumDatesep: dtok.dtt = atEnd ? DTT.NumEnd : DTT.NumSpace; break;
2617 case DTT.NumTimesep: dtok.dtt = atEnd ? DTT.NumEnd : DTT.NumSpace; break;
2618 case DTT.MonthDatesep: dtok.dtt = atEnd ? DTT.MonthEnd : DTT.MonthSpace; break;
2624 // Advance to the next state, and continue
2626 dps = dateParsingStates[(int)dps][(int)dtok.dtt];
2628 if (dps == DS.ERROR)
2630 result.SetBadDateTimeFailure();
2631 TPTraceExit("0040 (invalid state transition)", dps);
2632 return false;
2634 else if (dps > DS.ERROR)
2636 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseHebrewRule) != 0)
2638 if (!ProcessHebrewTerminalState(dps, ref result, ref styles, ref raw, dtfi))
2640 TPTraceExit("0050 (ProcessHebrewTerminalState)", dps);
2641 return false;
2644 else
2646 if (!ProcessTerminalState(dps, ref str, ref result, ref styles, ref raw, dtfi))
2648 TPTraceExit("0060 (ProcessTerminalState)", dps);
2649 return false;
2652 reachTerminalState = true;
2655 // If we have reached a terminal state, start over from DS.BEGIN again.
2656 // For example, when we parsed "1999-12-23 13:30", we will reach a terminal state at "1999-12-23",
2657 // and we start over so we can continue to parse "12:30".
2659 dps = DS.BEGIN;
2662 } while (dtok.dtt != DTT.End && dtok.dtt != DTT.NumEnd && dtok.dtt != DTT.MonthEnd);
2664 if (!reachTerminalState)
2666 result.SetBadDateTimeFailure();
2667 TPTraceExit("0070 (did not reach terminal state)", dps);
2668 return false;
2671 AdjustTimeMark(dtfi, ref raw);
2672 if (!AdjustHour(ref result.Hour, raw.timeMark))
2674 result.SetBadDateTimeFailure();
2675 TPTraceExit("0080 (AdjustHour)", dps);
2676 return false;
2679 // Check if the parsed string only contains hour/minute/second values.
2680 bool bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1);
2683 // Check if any year/month/day is missing in the parsing string.
2684 // If yes, get the default value from today's date.
2686 if (!CheckDefaultDateTime(ref result, ref result.calendar, styles))
2688 TPTraceExit("0090 (failed to fill in missing year/month/day defaults)", dps);
2689 return false;
2692 if (!result.calendar.TryToDateTime(result.Year, result.Month, result.Day,
2693 result.Hour, result.Minute, result.Second, 0, result.era, out time))
2695 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
2696 TPTraceExit("0100 (result.calendar.TryToDateTime)", dps);
2697 return false;
2700 if (raw.fraction > 0)
2702 if (!time.TryAddTicks((long)Math.Round(raw.fraction * Calendar.TicksPerSecond), out time))
2704 result.SetBadDateTimeFailure();
2705 TPTraceExit("0100 (time.TryAddTicks)", dps);
2706 return false;
2711 // We have to check day of week before we adjust to the time zone.
2712 // Otherwise, the value of day of week may change after adjusting to the time zone.
2714 if (raw.dayOfWeek != -1)
2717 // Check if day of week is correct.
2719 if (raw.dayOfWeek != (int)result.calendar.GetDayOfWeek(time))
2721 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_BadDayOfWeek));
2722 TPTraceExit("0110 (dayOfWeek check)", dps);
2723 return false;
2727 result.parsedDate = time;
2729 if (!DetermineTimeZoneAdjustments(ref result, styles, bTimeOnly))
2731 TPTraceExit("0120 (DetermineTimeZoneAdjustments)", dps);
2732 return false;
2734 TPTraceExit("0130 (success)", dps);
2735 return true;
2738 // Handles time zone adjustments and sets DateTimeKind values as required by the styles
2739 private static bool DetermineTimeZoneAdjustments(ref DateTimeResult result, DateTimeStyles styles, bool bTimeOnly)
2741 if ((result.flags & ParseFlags.CaptureOffset) != 0)
2743 // This is a DateTimeOffset parse, so the offset will actually be captured directly, and
2744 // no adjustment is required in most cases
2745 return DateTimeOffsetTimeZonePostProcessing(ref result, styles);
2747 else
2749 long offsetTicks = result.timeZoneOffset.Ticks;
2751 // the DateTime offset must be within +- 14:00 hours.
2752 if (offsetTicks < DateTimeOffset.MinOffset || offsetTicks > DateTimeOffset.MaxOffset)
2754 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_OffsetOutOfRange));
2755 return false;
2759 // The flags AssumeUniveral and AssumeLocal only apply when the input does not have a time zone
2760 if ((result.flags & ParseFlags.TimeZoneUsed) == 0)
2762 // If AssumeLocal or AssumeLocal is used, there will always be a kind specified. As in the
2763 // case when a time zone is present, it will default to being local unless AdjustToUniversal
2764 // is present. These comparisons determine whether setting the kind is sufficient, or if a
2765 // time zone adjustment is required. For consistentcy with the rest of parsing, it is desirable
2766 // to fall through to the Adjust methods below, so that there is consist handling of boundary
2767 // cases like wrapping around on time-only dates and temporarily allowing an adjusted date
2768 // to exceed DateTime.MaxValue
2769 if ((styles & DateTimeStyles.AssumeLocal) != 0)
2771 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2773 result.flags |= ParseFlags.TimeZoneUsed;
2774 result.timeZoneOffset = TimeZoneInfo.GetLocalUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime);
2776 else
2778 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Local);
2779 return true;
2782 else if ((styles & DateTimeStyles.AssumeUniversal) != 0)
2784 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2786 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Utc);
2787 return true;
2789 else
2791 result.flags |= ParseFlags.TimeZoneUsed;
2792 result.timeZoneOffset = TimeSpan.Zero;
2795 else
2797 // No time zone and no Assume flags, so DateTimeKind.Unspecified is fine
2798 Debug.Assert(result.parsedDate.Kind == DateTimeKind.Unspecified, "result.parsedDate.Kind == DateTimeKind.Unspecified");
2799 return true;
2803 if (((styles & DateTimeStyles.RoundtripKind) != 0) && ((result.flags & ParseFlags.TimeZoneUtc) != 0))
2805 result.parsedDate = DateTime.SpecifyKind(result.parsedDate, DateTimeKind.Utc);
2806 return true;
2809 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2811 return AdjustTimeZoneToUniversal(ref result);
2813 return AdjustTimeZoneToLocal(ref result, bTimeOnly);
2816 // Apply validation and adjustments specific to DateTimeOffset
2817 private static bool DateTimeOffsetTimeZonePostProcessing(ref DateTimeResult result, DateTimeStyles styles)
2819 // For DateTimeOffset, default to the Utc or Local offset when an offset was not specified by
2820 // the input string.
2821 if ((result.flags & ParseFlags.TimeZoneUsed) == 0)
2823 if ((styles & DateTimeStyles.AssumeUniversal) != 0)
2825 // AssumeUniversal causes the offset to default to zero (0)
2826 result.timeZoneOffset = TimeSpan.Zero;
2828 else
2830 // AssumeLocal causes the offset to default to Local. This flag is on by default for DateTimeOffset.
2831 result.timeZoneOffset = TimeZoneInfo.GetLocalUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime);
2835 long offsetTicks = result.timeZoneOffset.Ticks;
2837 // there should be no overflow, because the offset can be no more than -+100 hours and the date already
2838 // fits within a DateTime.
2839 long utcTicks = result.parsedDate.Ticks - offsetTicks;
2841 // For DateTimeOffset, both the parsed time and the corresponding UTC value must be within the boundaries
2842 // of a DateTime instance.
2843 if (utcTicks < DateTime.MinTicks || utcTicks > DateTime.MaxTicks)
2845 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_UTCOutOfRange));
2846 return false;
2849 // the offset must be within +- 14:00 hours.
2850 if (offsetTicks < DateTimeOffset.MinOffset || offsetTicks > DateTimeOffset.MaxOffset)
2852 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_OffsetOutOfRange));
2853 return false;
2856 // DateTimeOffset should still honor the AdjustToUniversal flag for consistency with DateTime. It means you
2857 // want to return an adjusted UTC value, so store the utcTicks in the DateTime and set the offset to zero
2858 if ((styles & DateTimeStyles.AdjustToUniversal) != 0)
2860 if (((result.flags & ParseFlags.TimeZoneUsed) == 0) && ((styles & DateTimeStyles.AssumeUniversal) == 0))
2862 // Handle the special case where the timeZoneOffset was defaulted to Local
2863 bool toUtcResult = AdjustTimeZoneToUniversal(ref result);
2864 result.timeZoneOffset = TimeSpan.Zero;
2865 return toUtcResult;
2868 // The constructor should always succeed because of the range check earlier in the function
2869 // Although it is UTC, internally DateTimeOffset does not use this flag
2870 result.parsedDate = new DateTime(utcTicks, DateTimeKind.Utc);
2871 result.timeZoneOffset = TimeSpan.Zero;
2874 return true;
2878 // Adjust the specified time to universal time based on the supplied timezone.
2879 // E.g. when parsing "2001/06/08 14:00-07:00",
2880 // the time is 2001/06/08 14:00, and timeZoneOffset = -07:00.
2881 // The result will be "2001/06/08 21:00"
2883 private static bool AdjustTimeZoneToUniversal(ref DateTimeResult result)
2885 long resultTicks = result.parsedDate.Ticks;
2886 resultTicks -= result.timeZoneOffset.Ticks;
2887 if (resultTicks < 0)
2889 resultTicks += Calendar.TicksPerDay;
2892 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2894 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_DateOutOfRange));
2895 return false;
2897 result.parsedDate = new DateTime(resultTicks, DateTimeKind.Utc);
2898 return true;
2902 // Adjust the specified time to universal time based on the supplied timezone,
2903 // and then convert to local time.
2904 // E.g. when parsing "2001/06/08 14:00-04:00", and local timezone is GMT-7.
2905 // the time is 2001/06/08 14:00, and timeZoneOffset = -05:00.
2906 // The result will be "2001/06/08 11:00"
2908 private static bool AdjustTimeZoneToLocal(ref DateTimeResult result, bool bTimeOnly)
2910 long resultTicks = result.parsedDate.Ticks;
2911 // Convert to local ticks
2912 TimeZoneInfo tz = TimeZoneInfo.Local;
2913 bool isAmbiguousLocalDst = false;
2914 if (resultTicks < Calendar.TicksPerDay)
2917 // This is time of day.
2920 // Adjust timezone.
2921 resultTicks -= result.timeZoneOffset.Ticks;
2922 // If the time is time of day, use the current timezone offset.
2923 resultTicks += tz.GetUtcOffset(bTimeOnly ? DateTime.Now : result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime).Ticks;
2925 if (resultTicks < 0)
2927 resultTicks += Calendar.TicksPerDay;
2930 else
2932 // Adjust timezone to GMT.
2933 resultTicks -= result.timeZoneOffset.Ticks;
2934 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2936 // If the result ticks is greater than DateTime.MaxValue, we can not create a DateTime from this ticks.
2937 // In this case, keep using the old code.
2938 resultTicks += tz.GetUtcOffset(result.parsedDate, TimeZoneInfoOptions.NoThrowOnInvalidTime).Ticks;
2940 else
2942 // Convert the GMT time to local time.
2943 DateTime utcDt = new DateTime(resultTicks, DateTimeKind.Utc);
2944 bool isDaylightSavings = false;
2945 resultTicks += TimeZoneInfo.GetUtcOffsetFromUtc(utcDt, TimeZoneInfo.Local, out isDaylightSavings, out isAmbiguousLocalDst).Ticks;
2948 if (resultTicks < DateTime.MinTicks || resultTicks > DateTime.MaxTicks)
2950 result.parsedDate = DateTime.MinValue;
2951 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_DateOutOfRange));
2952 return false;
2954 result.parsedDate = new DateTime(resultTicks, DateTimeKind.Local, isAmbiguousLocalDst);
2955 return true;
2959 // Parse the ISO8601 format string found during Parse();
2962 private static bool ParseISO8601(ref DateTimeRawInfo raw, ref __DTString str, DateTimeStyles styles, ref DateTimeResult result)
2964 if (raw.year < 0 || raw.GetNumber(0) < 0 || raw.GetNumber(1) < 0)
2967 str.Index--;
2968 int hour, minute;
2969 int second = 0;
2970 double partSecond = 0;
2972 str.SkipWhiteSpaces();
2973 if (!ParseDigits(ref str, 2, out hour))
2975 result.SetBadDateTimeFailure();
2976 return false;
2978 str.SkipWhiteSpaces();
2979 if (!str.Match(':'))
2981 result.SetBadDateTimeFailure();
2982 return false;
2984 str.SkipWhiteSpaces();
2985 if (!ParseDigits(ref str, 2, out minute))
2987 result.SetBadDateTimeFailure();
2988 return false;
2990 str.SkipWhiteSpaces();
2991 if (str.Match(':'))
2993 str.SkipWhiteSpaces();
2994 if (!ParseDigits(ref str, 2, out second))
2996 result.SetBadDateTimeFailure();
2997 return false;
2999 if (str.Match('.'))
3001 if (!ParseFraction(ref str, out partSecond))
3003 result.SetBadDateTimeFailure();
3004 return false;
3006 str.Index--;
3008 str.SkipWhiteSpaces();
3010 if (str.GetNext())
3012 char ch = str.GetChar();
3013 if (ch == '+' || ch == '-')
3015 result.flags |= ParseFlags.TimeZoneUsed;
3016 if (!ParseTimeZone(ref str, ref result.timeZoneOffset))
3018 result.SetBadDateTimeFailure();
3019 return false;
3022 else if (ch == 'Z' || ch == 'z')
3024 result.flags |= ParseFlags.TimeZoneUsed;
3025 result.timeZoneOffset = TimeSpan.Zero;
3026 result.flags |= ParseFlags.TimeZoneUtc;
3028 else
3030 str.Index--;
3032 str.SkipWhiteSpaces();
3033 if (str.Match('#'))
3035 if (!VerifyValidPunctuation(ref str))
3037 result.SetBadDateTimeFailure();
3038 return false;
3040 str.SkipWhiteSpaces();
3042 if (str.Match('\0'))
3044 if (!VerifyValidPunctuation(ref str))
3046 result.SetBadDateTimeFailure();
3047 return false;
3050 if (str.GetNext())
3052 // If this is true, there were non-white space characters remaining in the DateTime
3053 result.SetBadDateTimeFailure();
3054 return false;
3058 DateTime time;
3059 Calendar calendar = GregorianCalendar.GetDefaultInstance();
3060 if (!calendar.TryToDateTime(raw.year, raw.GetNumber(0), raw.GetNumber(1),
3061 hour, minute, second, 0, result.era, out time))
3063 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
3064 return false;
3067 if (!time.TryAddTicks((long)Math.Round(partSecond * Calendar.TicksPerSecond), out time))
3069 result.SetBadDateTimeFailure();
3070 return false;
3073 result.parsedDate = time;
3074 return DetermineTimeZoneAdjustments(ref result, styles, false);
3077 ////////////////////////////////////////////////////////////////////////
3079 // Actions:
3080 // Parse the current word as a Hebrew number.
3081 // This is used by DateTime.ParseExact().
3083 ////////////////////////////////////////////////////////////////////////
3085 internal static bool MatchHebrewDigits(ref __DTString str, int digitLen, out int number)
3087 number = 0;
3089 // Create a context object so that we can parse the Hebrew number text character by character.
3090 HebrewNumberParsingContext context = new HebrewNumberParsingContext(0);
3092 // Set this to ContinueParsing so that we will run the following while loop in the first time.
3093 HebrewNumberParsingState state = HebrewNumberParsingState.ContinueParsing;
3095 while (state == HebrewNumberParsingState.ContinueParsing && str.GetNext())
3097 state = HebrewNumber.ParseByChar(str.GetChar(), ref context);
3100 if (state == HebrewNumberParsingState.FoundEndOfHebrewNumber)
3102 // If we have reached a terminal state, update the result and returns.
3103 number = context.result;
3104 return true;
3107 // If we run out of the character before reaching FoundEndOfHebrewNumber, or
3108 // the state is InvalidHebrewNumber or ContinueParsing, we fail to match a Hebrew number.
3109 // Return an error.
3110 return false;
3113 /*=================================ParseDigits==================================
3114 **Action: Parse the number string in __DTString that are formatted using
3115 ** the following patterns:
3116 ** "0", "00", and "000..0"
3117 **Returns: the integer value
3118 **Arguments: str: a __DTString. The parsing will start from the
3119 ** next character after str.Index.
3120 **Exceptions: FormatException if error in parsing number.
3121 ==============================================================================*/
3123 internal static bool ParseDigits(ref __DTString str, int digitLen, out int result)
3125 if (digitLen == 1)
3127 // 1 really means 1 or 2 for this call
3128 return ParseDigits(ref str, 1, 2, out result);
3130 else
3132 return ParseDigits(ref str, digitLen, digitLen, out result);
3136 internal static bool ParseDigits(ref __DTString str, int minDigitLen, int maxDigitLen, out int result)
3138 Debug.Assert(minDigitLen > 0, "minDigitLen > 0");
3139 Debug.Assert(maxDigitLen < 9, "maxDigitLen < 9");
3140 Debug.Assert(minDigitLen <= maxDigitLen, "minDigitLen <= maxDigitLen");
3141 int localResult = 0;
3142 int startingIndex = str.Index;
3143 int tokenLength = 0;
3144 while (tokenLength < maxDigitLen)
3146 if (!str.GetNextDigit())
3148 str.Index--;
3149 break;
3151 localResult = localResult * 10 + str.GetDigit();
3152 tokenLength++;
3154 result = localResult;
3155 if (tokenLength < minDigitLen)
3157 str.Index = startingIndex;
3158 return false;
3160 return true;
3163 /*=================================ParseFractionExact==================================
3164 **Action: Parse the number string in __DTString that are formatted using
3165 ** the following patterns:
3166 ** "0", "00", and "000..0"
3167 **Returns: the fraction value
3168 **Arguments: str: a __DTString. The parsing will start from the
3169 ** next character after str.Index.
3170 **Exceptions: FormatException if error in parsing number.
3171 ==============================================================================*/
3173 private static bool ParseFractionExact(ref __DTString str, int maxDigitLen, ref double result)
3175 if (!str.GetNextDigit())
3177 str.Index--;
3178 return false;
3180 result = str.GetDigit();
3182 int digitLen = 1;
3183 for (; digitLen < maxDigitLen; digitLen++)
3185 if (!str.GetNextDigit())
3187 str.Index--;
3188 break;
3190 result = result * 10 + str.GetDigit();
3193 result /= TimeSpanParse.Pow10(digitLen);
3194 return digitLen == maxDigitLen;
3197 /*=================================ParseSign==================================
3198 **Action: Parse a positive or a negative sign.
3199 **Returns: true if postive sign. flase if negative sign.
3200 **Arguments: str: a __DTString. The parsing will start from the
3201 ** next character after str.Index.
3202 **Exceptions: FormatException if end of string is encountered or a sign
3203 ** symbol is not found.
3204 ==============================================================================*/
3206 private static bool ParseSign(ref __DTString str, ref bool result)
3208 if (!str.GetNext())
3210 // A sign symbol ('+' or '-') is expected. However, end of string is encountered.
3211 return false;
3213 char ch = str.GetChar();
3214 if (ch == '+')
3216 result = true;
3217 return true;
3219 else if (ch == '-')
3221 result = false;
3222 return true;
3224 // A sign symbol ('+' or '-') is expected.
3225 return false;
3228 /*=================================ParseTimeZoneOffset==================================
3229 **Action: Parse the string formatted using "z", "zz", "zzz" in DateTime.Format().
3230 **Returns: the TimeSpan for the parsed timezone offset.
3231 **Arguments: str: a __DTString. The parsing will start from the
3232 ** next character after str.Index.
3233 ** len: the repeated number of the "z"
3234 **Exceptions: FormatException if errors in parsing.
3235 ==============================================================================*/
3237 private static bool ParseTimeZoneOffset(ref __DTString str, int len, ref TimeSpan result)
3239 bool isPositive = true;
3240 int hourOffset;
3241 int minuteOffset = 0;
3243 switch (len)
3245 case 1:
3246 case 2:
3247 if (!ParseSign(ref str, ref isPositive))
3249 return false;
3251 if (!ParseDigits(ref str, len, out hourOffset))
3253 return false;
3255 break;
3256 default:
3257 if (!ParseSign(ref str, ref isPositive))
3259 return false;
3262 // Parsing 1 digit will actually parse 1 or 2.
3263 if (!ParseDigits(ref str, 1, out hourOffset))
3265 return false;
3267 // ':' is optional.
3268 if (str.Match(":"))
3270 // Found ':'
3271 if (!ParseDigits(ref str, 2, out minuteOffset))
3273 return false;
3276 else
3278 // Since we can not match ':', put the char back.
3279 str.Index--;
3280 if (!ParseDigits(ref str, 2, out minuteOffset))
3282 return false;
3285 break;
3287 if (minuteOffset < 0 || minuteOffset >= 60)
3289 return false;
3292 result = (new TimeSpan(hourOffset, minuteOffset, 0));
3293 if (!isPositive)
3295 result = result.Negate();
3297 return true;
3300 /*=================================MatchAbbreviatedMonthName==================================
3301 **Action: Parse the abbreviated month name from string starting at str.Index.
3302 **Returns: A value from 1 to 12 for the first month to the twelfth month.
3303 **Arguments: str: a __DTString. The parsing will start from the
3304 ** next character after str.Index.
3305 **Exceptions: FormatException if an abbreviated month name can not be found.
3306 ==============================================================================*/
3308 private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3310 int maxMatchStrLen = 0;
3311 result = -1;
3312 if (str.GetNext())
3315 // Scan the month names (note that some calendars has 13 months) and find
3316 // the matching month name which has the max string length.
3317 // We need to do this because some cultures (e.g. "cs-CZ") which have
3318 // abbreviated month names with the same prefix.
3320 int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3321 for (int i = 1; i <= monthsInYear; i++)
3323 string searchStr = dtfi.GetAbbreviatedMonthName(i);
3324 int matchStrLen = searchStr.Length;
3325 if (dtfi.HasSpacesInMonthNames
3326 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3327 : str.MatchSpecifiedWord(searchStr))
3329 if (matchStrLen > maxMatchStrLen)
3331 maxMatchStrLen = matchStrLen;
3332 result = i;
3337 // Search genitive form.
3338 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0)
3340 int tempResult = str.MatchLongestWords(dtfi.AbbreviatedMonthGenitiveNames, ref maxMatchStrLen);
3342 // We found a longer match in the genitive month name. Use this as the result.
3343 // tempResult + 1 should be the month value.
3344 if (tempResult >= 0)
3346 result = tempResult + 1;
3350 // Search leap year form.
3351 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0)
3353 int tempResult = str.MatchLongestWords(dtfi.InternalGetLeapYearMonthNames(), ref maxMatchStrLen);
3354 // We found a longer match in the leap year month name. Use this as the result.
3355 // The result from MatchLongestWords is 0 ~ length of word array.
3356 // So we increment the result by one to become the month value.
3357 if (tempResult >= 0)
3359 result = tempResult + 1;
3363 if (result > 0)
3365 str.Index += (maxMatchStrLen - 1);
3366 return true;
3368 return false;
3371 /*=================================MatchMonthName==================================
3372 **Action: Parse the month name from string starting at str.Index.
3373 **Returns: A value from 1 to 12 indicating the first month to the twelfth month.
3374 **Arguments: str: a __DTString. The parsing will start from the
3375 ** next character after str.Index.
3376 **Exceptions: FormatException if a month name can not be found.
3377 ==============================================================================*/
3379 private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3381 int maxMatchStrLen = 0;
3382 result = -1;
3383 if (str.GetNext())
3386 // Scan the month names (note that some calendars has 13 months) and find
3387 // the matching month name which has the max string length.
3388 // We need to do this because some cultures (e.g. "vi-VN") which have
3389 // month names with the same prefix.
3391 int monthsInYear = (dtfi.GetMonthName(13).Length == 0 ? 12 : 13);
3392 for (int i = 1; i <= monthsInYear; i++)
3394 string searchStr = dtfi.GetMonthName(i);
3395 int matchStrLen = searchStr.Length;
3396 if (dtfi.HasSpacesInMonthNames
3397 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3398 : str.MatchSpecifiedWord(searchStr))
3400 if (matchStrLen > maxMatchStrLen)
3402 maxMatchStrLen = matchStrLen;
3403 result = i;
3408 // Search genitive form.
3409 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0)
3411 int tempResult = str.MatchLongestWords(dtfi.MonthGenitiveNames, ref maxMatchStrLen);
3412 // We found a longer match in the genitive month name. Use this as the result.
3413 // The result from MatchLongestWords is 0 ~ length of word array.
3414 // So we increment the result by one to become the month value.
3415 if (tempResult >= 0)
3417 result = tempResult + 1;
3421 // Search leap year form.
3422 if ((dtfi.FormatFlags & DateTimeFormatFlags.UseLeapYearMonth) != 0)
3424 int tempResult = str.MatchLongestWords(dtfi.InternalGetLeapYearMonthNames(), ref maxMatchStrLen);
3425 // We found a longer match in the leap year month name. Use this as the result.
3426 // The result from MatchLongestWords is 0 ~ length of word array.
3427 // So we increment the result by one to become the month value.
3428 if (tempResult >= 0)
3430 result = tempResult + 1;
3435 if (result > 0)
3437 str.Index += (maxMatchStrLen - 1);
3438 return true;
3440 return false;
3443 /*=================================MatchAbbreviatedDayName==================================
3444 **Action: Parse the abbreviated day of week name from string starting at str.Index.
3445 **Returns: A value from 0 to 6 indicating Sunday to Saturday.
3446 **Arguments: str: a __DTString. The parsing will start from the
3447 ** next character after str.Index.
3448 **Exceptions: FormatException if a abbreviated day of week name can not be found.
3449 ==============================================================================*/
3451 private static bool MatchAbbreviatedDayName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3453 int maxMatchStrLen = 0;
3454 result = -1;
3455 if (str.GetNext())
3457 for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3459 string searchStr = dtfi.GetAbbreviatedDayName(i);
3460 int matchStrLen = searchStr.Length;
3461 if (dtfi.HasSpacesInDayNames
3462 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3463 : str.MatchSpecifiedWord(searchStr))
3465 if (matchStrLen > maxMatchStrLen)
3467 maxMatchStrLen = matchStrLen;
3468 result = (int)i;
3473 if (result >= 0)
3475 str.Index += maxMatchStrLen - 1;
3476 return true;
3478 return false;
3481 /*=================================MatchDayName==================================
3482 **Action: Parse the day of week name from string starting at str.Index.
3483 **Returns: A value from 0 to 6 indicating Sunday to Saturday.
3484 **Arguments: str: a __DTString. The parsing will start from the
3485 ** next character after str.Index.
3486 **Exceptions: FormatException if a day of week name can not be found.
3487 ==============================================================================*/
3489 private static bool MatchDayName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3491 // Turkish (tr-TR) got day names with the same prefix.
3492 int maxMatchStrLen = 0;
3493 result = -1;
3494 if (str.GetNext())
3496 for (DayOfWeek i = DayOfWeek.Sunday; i <= DayOfWeek.Saturday; i++)
3498 string searchStr = dtfi.GetDayName(i);
3499 int matchStrLen = searchStr.Length;
3500 if (dtfi.HasSpacesInDayNames
3501 ? str.MatchSpecifiedWords(searchStr, false, ref matchStrLen)
3502 : str.MatchSpecifiedWord(searchStr))
3504 if (matchStrLen > maxMatchStrLen)
3506 maxMatchStrLen = matchStrLen;
3507 result = (int)i;
3512 if (result >= 0)
3514 str.Index += maxMatchStrLen - 1;
3515 return true;
3517 return false;
3520 /*=================================MatchEraName==================================
3521 **Action: Parse era name from string starting at str.Index.
3522 **Returns: An era value.
3523 **Arguments: str: a __DTString. The parsing will start from the
3524 ** next character after str.Index.
3525 **Exceptions: FormatException if an era name can not be found.
3526 ==============================================================================*/
3528 private static bool MatchEraName(ref __DTString str, DateTimeFormatInfo dtfi, ref int result)
3530 if (str.GetNext())
3532 int[] eras = dtfi.Calendar.Eras;
3534 if (eras != null)
3536 for (int i = 0; i < eras.Length; i++)
3538 string searchStr = dtfi.GetEraName(eras[i]);
3539 if (str.MatchSpecifiedWord(searchStr))
3541 str.Index += (searchStr.Length - 1);
3542 result = eras[i];
3543 return true;
3545 searchStr = dtfi.GetAbbreviatedEraName(eras[i]);
3546 if (str.MatchSpecifiedWord(searchStr))
3548 str.Index += (searchStr.Length - 1);
3549 result = eras[i];
3550 return true;
3555 return false;
3558 /*=================================MatchTimeMark==================================
3559 **Action: Parse the time mark (AM/PM) from string starting at str.Index.
3560 **Returns: TM_AM or TM_PM.
3561 **Arguments: str: a __DTString. The parsing will start from the
3562 ** next character after str.Index.
3563 **Exceptions: FormatException if a time mark can not be found.
3564 ==============================================================================*/
3566 private static bool MatchTimeMark(ref __DTString str, DateTimeFormatInfo dtfi, ref TM result)
3568 result = TM.NotSet;
3569 // In some cultures have empty strings in AM/PM mark. E.g. af-ZA (0x0436), the AM mark is "", and PM mark is "nm".
3570 if (dtfi.AMDesignator.Length == 0)
3572 result = TM.AM;
3574 if (dtfi.PMDesignator.Length == 0)
3576 result = TM.PM;
3579 if (str.GetNext())
3581 string searchStr = dtfi.AMDesignator;
3582 if (searchStr.Length > 0)
3584 if (str.MatchSpecifiedWord(searchStr))
3586 // Found an AM timemark with length > 0.
3587 str.Index += (searchStr.Length - 1);
3588 result = TM.AM;
3589 return true;
3592 searchStr = dtfi.PMDesignator;
3593 if (searchStr.Length > 0)
3595 if (str.MatchSpecifiedWord(searchStr))
3597 // Found a PM timemark with length > 0.
3598 str.Index += (searchStr.Length - 1);
3599 result = TM.PM;
3600 return true;
3603 str.Index--; // Undo the GetNext call.
3605 if (result != TM.NotSet)
3607 // If one of the AM/PM marks is empty string, return the result.
3608 return true;
3610 return false;
3613 /*=================================MatchAbbreviatedTimeMark==================================
3614 **Action: Parse the abbreviated time mark (AM/PM) from string starting at str.Index.
3615 **Returns: TM_AM or TM_PM.
3616 **Arguments: str: a __DTString. The parsing will start from the
3617 ** next character after str.Index.
3618 **Exceptions: FormatException if a abbreviated time mark can not be found.
3619 ==============================================================================*/
3621 private static bool MatchAbbreviatedTimeMark(ref __DTString str, DateTimeFormatInfo dtfi, ref TM result)
3623 // NOTENOTE : the assumption here is that abbreviated time mark is the first
3624 // character of the AM/PM designator. If this invariant changes, we have to
3625 // change the code below.
3626 if (str.GetNext())
3628 string amDesignator = dtfi.AMDesignator;
3629 if (amDesignator.Length > 0 && str.GetChar() == amDesignator[0])
3631 result = TM.AM;
3632 return true;
3635 string pmDesignator = dtfi.PMDesignator;
3636 if (pmDesignator.Length > 0 && str.GetChar() == pmDesignator[0])
3638 result = TM.PM;
3639 return true;
3642 return false;
3645 /*=================================CheckNewValue==================================
3646 **Action: Check if currentValue is initialized. If not, return the newValue.
3647 ** If yes, check if the current value is equal to newValue. Return false
3648 ** if they are not equal. This is used to check the case like "d" and "dd" are both
3649 ** used to format a string.
3650 **Returns: the correct value for currentValue.
3651 **Arguments:
3652 **Exceptions:
3653 ==============================================================================*/
3655 private static bool CheckNewValue(ref int currentValue, int newValue, char patternChar, ref DateTimeResult result)
3657 if (currentValue == -1)
3659 currentValue = newValue;
3660 return true;
3662 else
3664 if (newValue != currentValue)
3666 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), patternChar);
3667 return false;
3670 return true;
3673 private static DateTime GetDateTimeNow(ref DateTimeResult result, ref DateTimeStyles styles)
3675 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3677 if ((result.flags & ParseFlags.TimeZoneUsed) != 0)
3679 // use the supplied offset to calculate 'Now'
3680 return new DateTime(DateTime.UtcNow.Ticks + result.timeZoneOffset.Ticks, DateTimeKind.Unspecified);
3682 else if ((styles & DateTimeStyles.AssumeUniversal) != 0)
3684 // assume the offset is Utc
3685 return DateTime.UtcNow;
3689 // assume the offset is Local
3690 return DateTime.Now;
3693 private static bool CheckDefaultDateTime(ref DateTimeResult result, ref Calendar cal, DateTimeStyles styles)
3695 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3697 // DateTimeOffset.Parse should allow dates without a year, but only if there is also no time zone marker;
3698 // e.g. "May 1 5pm" is OK, but "May 1 5pm -08:30" is not. This is somewhat pragmatic, since we would
3699 // have to rearchitect parsing completely to allow this one case to correctly handle things like leap
3700 // years and leap months. Is an extremely corner case, and DateTime is basically incorrect in that
3701 // case today.
3703 // values like "11:00Z" or "11:00 -3:00" are also acceptable
3705 // if ((month or day is set) and (year is not set and time zone is set))
3707 if (((result.Month != -1) || (result.Day != -1))
3708 && ((result.Year == -1 || ((result.flags & ParseFlags.YearDefault) != 0)) && (result.flags & ParseFlags.TimeZoneUsed) != 0))
3710 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_MissingIncompleteDate));
3711 return false;
3715 if ((result.Year == -1) || (result.Month == -1) || (result.Day == -1))
3718 The following table describes the behaviors of getting the default value
3719 when a certain year/month/day values are missing.
3721 An "X" means that the value exists. And "--" means that value is missing.
3723 Year Month Day => ResultYear ResultMonth ResultDay Note
3725 X X X Parsed year Parsed month Parsed day
3726 X X -- Parsed Year Parsed month First day If we have year and month, assume the first day of that month.
3727 X -- X Parsed year First month Parsed day If the month is missing, assume first month of that year.
3728 X -- -- Parsed year First month First day If we have only the year, assume the first day of that year.
3730 -- X X CurrentYear Parsed month Parsed day If the year is missing, assume the current year.
3731 -- X -- CurrentYear Parsed month First day If we have only a month value, assume the current year and current day.
3732 -- -- X CurrentYear First month Parsed day If we have only a day value, assume current year and first month.
3733 -- -- -- CurrentYear Current month Current day So this means that if the date string only contains time, you will get current date.
3737 DateTime now = GetDateTimeNow(ref result, ref styles);
3738 if (result.Month == -1 && result.Day == -1)
3740 if (result.Year == -1)
3742 if ((styles & DateTimeStyles.NoCurrentDateDefault) != 0)
3744 // If there is no year/month/day values, and NoCurrentDateDefault flag is used,
3745 // set the year/month/day value to the beginning year/month/day of DateTime().
3746 // Note we should be using Gregorian for the year/month/day.
3747 cal = GregorianCalendar.GetDefaultInstance();
3748 result.Year = result.Month = result.Day = 1;
3750 else
3752 // Year/Month/Day are all missing.
3753 result.Year = cal.GetYear(now);
3754 result.Month = cal.GetMonth(now);
3755 result.Day = cal.GetDayOfMonth(now);
3758 else
3760 // Month/Day are both missing.
3761 result.Month = 1;
3762 result.Day = 1;
3765 else
3767 if (result.Year == -1)
3769 result.Year = cal.GetYear(now);
3771 if (result.Month == -1)
3773 result.Month = 1;
3775 if (result.Day == -1)
3777 result.Day = 1;
3781 // Set Hour/Minute/Second to zero if these value are not in str.
3782 if (result.Hour == -1) result.Hour = 0;
3783 if (result.Minute == -1) result.Minute = 0;
3784 if (result.Second == -1) result.Second = 0;
3785 if (result.era == -1) result.era = Calendar.CurrentEra;
3786 return true;
3789 // Expand a pre-defined format string (like "D" for long date) to the real format that
3790 // we are going to use in the date time parsing.
3791 // This method also set the dtfi according/parseInfo to some special pre-defined
3792 // formats.
3794 private static string ExpandPredefinedFormat(ReadOnlySpan<char> format, ref DateTimeFormatInfo dtfi, ref ParsingInfo parseInfo, ref DateTimeResult result)
3797 // Check the format to see if we need to override the dtfi to be InvariantInfo,
3798 // and see if we need to set up the userUniversalTime flag.
3800 switch (format[0])
3802 case 's': // Sortable format (in local time)
3803 case 'o':
3804 case 'O': // Round Trip Format
3805 ConfigureFormatOS(ref dtfi, ref parseInfo);
3806 break;
3807 case 'r':
3808 case 'R': // RFC 1123 Standard. (in Universal time)
3809 ConfigureFormatR(ref dtfi, ref parseInfo, ref result);
3810 break;
3811 case 'u': // Universal time format in sortable format.
3812 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3813 dtfi = DateTimeFormatInfo.InvariantInfo;
3815 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3817 result.flags |= ParseFlags.UtcSortPattern;
3819 break;
3820 case 'U': // Universal time format with culture-dependent format.
3821 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3822 result.flags |= ParseFlags.TimeZoneUsed;
3823 result.timeZoneOffset = new TimeSpan(0);
3824 result.flags |= ParseFlags.TimeZoneUtc;
3825 if (dtfi.Calendar.GetType() != typeof(GregorianCalendar))
3827 dtfi = (DateTimeFormatInfo)dtfi.Clone();
3828 dtfi.Calendar = GregorianCalendar.GetDefaultInstance();
3830 break;
3834 // Expand the pre-defined format character to the real format from DateTimeFormatInfo.
3836 return DateTimeFormat.GetRealFormat(format, dtfi);
3839 [MethodImpl(MethodImplOptions.AggressiveInlining)]
3840 private static bool ParseJapaneseEraStart(ref __DTString str, DateTimeFormatInfo dtfi)
3842 // ParseJapaneseEraStart will be called when parsing the year number. We can have dates which not listing
3843 // the year as a number and listing it as JapaneseEraStart symbol (which means year 1).
3844 // This will be legitimate date to recognize.
3845 if (LocalAppContextSwitches.EnforceLegacyJapaneseDateParsing || dtfi.Calendar.ID != CalendarId.JAPAN || !str.GetNext())
3846 return false;
3848 if (str.m_current != DateTimeFormatInfo.JapaneseEraStart[0])
3850 str.Index--;
3851 return false;
3854 return true;
3857 private static void ConfigureFormatR(ref DateTimeFormatInfo dtfi, ref ParsingInfo parseInfo, ref DateTimeResult result)
3859 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3860 dtfi = DateTimeFormatInfo.InvariantInfo;
3861 if ((result.flags & ParseFlags.CaptureOffset) != 0)
3863 result.flags |= ParseFlags.Rfc1123Pattern;
3867 private static void ConfigureFormatOS(ref DateTimeFormatInfo dtfi, ref ParsingInfo parseInfo)
3869 parseInfo.calendar = GregorianCalendar.GetDefaultInstance();
3870 dtfi = DateTimeFormatInfo.InvariantInfo;
3873 // Given a specified format character, parse and update the parsing result.
3875 private static bool ParseByFormat(
3876 ref __DTString str,
3877 ref __DTString format,
3878 ref ParsingInfo parseInfo,
3879 DateTimeFormatInfo dtfi,
3880 ref DateTimeResult result)
3882 int tokenLen = 0;
3883 int tempYear = 0, tempMonth = 0, tempDay = 0, tempDayOfWeek = 0, tempHour = 0, tempMinute = 0, tempSecond = 0;
3884 double tempFraction = 0;
3885 TM tempTimeMark = 0;
3887 char ch = format.GetChar();
3889 switch (ch)
3891 case 'y':
3892 tokenLen = format.GetRepeatCount();
3893 bool parseResult;
3894 if (ParseJapaneseEraStart(ref str, dtfi))
3896 tempYear = 1;
3897 parseResult = true;
3899 else if (dtfi.HasForceTwoDigitYears)
3901 parseResult = ParseDigits(ref str, 1, 4, out tempYear);
3903 else
3905 if (tokenLen <= 2)
3907 parseInfo.fUseTwoDigitYear = true;
3909 parseResult = ParseDigits(ref str, tokenLen, out tempYear);
3911 if (!parseResult && parseInfo.fCustomNumberParser)
3913 parseResult = parseInfo.parseNumberDelegate(ref str, tokenLen, out tempYear);
3915 if (!parseResult)
3917 result.SetBadDateTimeFailure();
3918 return false;
3920 if (!CheckNewValue(ref result.Year, tempYear, ch, ref result))
3922 return false;
3924 break;
3925 case 'M':
3926 tokenLen = format.GetRepeatCount();
3927 if (tokenLen <= 2)
3929 if (!ParseDigits(ref str, tokenLen, out tempMonth))
3931 if (!parseInfo.fCustomNumberParser ||
3932 !parseInfo.parseNumberDelegate(ref str, tokenLen, out tempMonth))
3934 result.SetBadDateTimeFailure();
3935 return false;
3939 else
3941 if (tokenLen == 3)
3943 if (!MatchAbbreviatedMonthName(ref str, dtfi, ref tempMonth))
3945 result.SetBadDateTimeFailure();
3946 return false;
3949 else
3951 if (!MatchMonthName(ref str, dtfi, ref tempMonth))
3953 result.SetBadDateTimeFailure();
3954 return false;
3957 result.flags |= ParseFlags.ParsedMonthName;
3959 if (!CheckNewValue(ref result.Month, tempMonth, ch, ref result))
3961 return false;
3963 break;
3964 case 'd':
3965 // Day & Day of week
3966 tokenLen = format.GetRepeatCount();
3967 if (tokenLen <= 2)
3969 // "d" & "dd"
3971 if (!ParseDigits(ref str, tokenLen, out tempDay))
3973 if (!parseInfo.fCustomNumberParser ||
3974 !parseInfo.parseNumberDelegate(ref str, tokenLen, out tempDay))
3976 result.SetBadDateTimeFailure();
3977 return false;
3980 if (!CheckNewValue(ref result.Day, tempDay, ch, ref result))
3982 return false;
3985 else
3987 if (tokenLen == 3)
3989 // "ddd"
3990 if (!MatchAbbreviatedDayName(ref str, dtfi, ref tempDayOfWeek))
3992 result.SetBadDateTimeFailure();
3993 return false;
3996 else
3998 // "dddd*"
3999 if (!MatchDayName(ref str, dtfi, ref tempDayOfWeek))
4001 result.SetBadDateTimeFailure();
4002 return false;
4005 if (!CheckNewValue(ref parseInfo.dayOfWeek, tempDayOfWeek, ch, ref result))
4007 return false;
4010 break;
4011 case 'g':
4012 tokenLen = format.GetRepeatCount();
4013 // Put the era value in result.era.
4014 if (!MatchEraName(ref str, dtfi, ref result.era))
4016 result.SetBadDateTimeFailure();
4017 return false;
4019 break;
4020 case 'h':
4021 parseInfo.fUseHour12 = true;
4022 tokenLen = format.GetRepeatCount();
4023 if (!ParseDigits(ref str, tokenLen < 2 ? 1 : 2, out tempHour))
4025 result.SetBadDateTimeFailure();
4026 return false;
4028 if (!CheckNewValue(ref result.Hour, tempHour, ch, ref result))
4030 return false;
4032 break;
4033 case 'H':
4034 tokenLen = format.GetRepeatCount();
4035 if (!ParseDigits(ref str, tokenLen < 2 ? 1 : 2, out tempHour))
4037 result.SetBadDateTimeFailure();
4038 return false;
4040 if (!CheckNewValue(ref result.Hour, tempHour, ch, ref result))
4042 return false;
4044 break;
4045 case 'm':
4046 tokenLen = format.GetRepeatCount();
4047 if (!ParseDigits(ref str, tokenLen < 2 ? 1 : 2, out tempMinute))
4049 result.SetBadDateTimeFailure();
4050 return false;
4052 if (!CheckNewValue(ref result.Minute, tempMinute, ch, ref result))
4054 return false;
4056 break;
4057 case 's':
4058 tokenLen = format.GetRepeatCount();
4059 if (!ParseDigits(ref str, tokenLen < 2 ? 1 : 2, out tempSecond))
4061 result.SetBadDateTimeFailure();
4062 return false;
4064 if (!CheckNewValue(ref result.Second, tempSecond, ch, ref result))
4066 return false;
4068 break;
4069 case 'f':
4070 case 'F':
4071 tokenLen = format.GetRepeatCount();
4072 if (tokenLen <= DateTimeFormat.MaxSecondsFractionDigits)
4074 if (!ParseFractionExact(ref str, tokenLen, ref tempFraction))
4076 if (ch == 'f')
4078 result.SetBadDateTimeFailure();
4079 return false;
4082 if (result.fraction < 0)
4084 result.fraction = tempFraction;
4086 else
4088 if (tempFraction != result.fraction)
4090 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), ch);
4091 return false;
4095 else
4097 result.SetBadDateTimeFailure();
4098 return false;
4100 break;
4101 case 't':
4102 // AM/PM designator
4103 tokenLen = format.GetRepeatCount();
4104 if (tokenLen == 1)
4106 if (!MatchAbbreviatedTimeMark(ref str, dtfi, ref tempTimeMark))
4108 result.SetBadDateTimeFailure();
4109 return false;
4112 else
4114 if (!MatchTimeMark(ref str, dtfi, ref tempTimeMark))
4116 result.SetBadDateTimeFailure();
4117 return false;
4121 if (parseInfo.timeMark == TM.NotSet)
4123 parseInfo.timeMark = tempTimeMark;
4125 else
4127 if (parseInfo.timeMark != tempTimeMark)
4129 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), ch);
4130 return false;
4133 break;
4134 case 'z':
4135 // timezone offset
4136 tokenLen = format.GetRepeatCount();
4138 TimeSpan tempTimeZoneOffset = new TimeSpan(0);
4139 if (!ParseTimeZoneOffset(ref str, tokenLen, ref tempTimeZoneOffset))
4141 result.SetBadDateTimeFailure();
4142 return false;
4144 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && tempTimeZoneOffset != result.timeZoneOffset)
4146 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'z');
4147 return false;
4149 result.timeZoneOffset = tempTimeZoneOffset;
4150 result.flags |= ParseFlags.TimeZoneUsed;
4152 break;
4153 case 'Z':
4154 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && result.timeZoneOffset != TimeSpan.Zero)
4156 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'Z');
4157 return false;
4160 result.flags |= ParseFlags.TimeZoneUsed;
4161 result.timeZoneOffset = new TimeSpan(0);
4162 result.flags |= ParseFlags.TimeZoneUtc;
4164 // The updating of the indexes is to reflect that ParseExact MatchXXX methods assume that
4165 // they need to increment the index and Parse GetXXX do not. Since we are calling a Parse
4166 // method from inside ParseExact we need to adjust this. Long term, we should try to
4167 // eliminate this discrepancy.
4168 str.Index++;
4169 if (!GetTimeZoneName(ref str))
4171 result.SetBadDateTimeFailure();
4172 return false;
4174 str.Index--;
4175 break;
4176 case 'K':
4177 // This should parse either as a blank, the 'Z' character or a local offset like "-07:00"
4178 if (str.Match('Z'))
4180 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && result.timeZoneOffset != TimeSpan.Zero)
4182 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'K');
4183 return false;
4186 result.flags |= ParseFlags.TimeZoneUsed;
4187 result.timeZoneOffset = new TimeSpan(0);
4188 result.flags |= ParseFlags.TimeZoneUtc;
4190 else if (str.Match('+') || str.Match('-'))
4192 str.Index--; // Put the character back for the parser
4193 TimeSpan tempTimeZoneOffset = new TimeSpan(0);
4194 if (!ParseTimeZoneOffset(ref str, 3, ref tempTimeZoneOffset))
4196 result.SetBadDateTimeFailure();
4197 return false;
4199 if ((result.flags & ParseFlags.TimeZoneUsed) != 0 && tempTimeZoneOffset != result.timeZoneOffset)
4201 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_RepeatDateTimePattern), 'K');
4202 return false;
4204 result.timeZoneOffset = tempTimeZoneOffset;
4205 result.flags |= ParseFlags.TimeZoneUsed;
4207 // Otherwise it is unspecified and we consume no characters
4208 break;
4209 case ':':
4210 // 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
4211 // We have to exclude the case when the time separator is more than one character and starts with ':' something like "::" for instance.
4212 if (((dtfi.TimeSeparator.Length > 1 && dtfi.TimeSeparator[0] == ':') || !str.Match(':')) &&
4213 !str.Match(dtfi.TimeSeparator))
4215 // A time separator is expected.
4216 result.SetBadDateTimeFailure();
4217 return false;
4219 break;
4220 case '/':
4221 // 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
4222 // We have to exclude the case when the date separator is more than one character and starts with '/' something like "//" for instance.
4223 if (((dtfi.DateSeparator.Length > 1 && dtfi.DateSeparator[0] == '/') || !str.Match('/')) &&
4224 !str.Match(dtfi.DateSeparator))
4226 // A date separator is expected.
4227 result.SetBadDateTimeFailure();
4228 return false;
4230 break;
4231 case '\"':
4232 case '\'':
4233 StringBuilder enquotedString = StringBuilderCache.Acquire();
4234 // Use ParseQuoteString so that we can handle escape characters within the quoted string.
4235 if (!TryParseQuoteString(format.Value, format.Index, enquotedString, out tokenLen))
4237 result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadQuote), ch);
4238 StringBuilderCache.Release(enquotedString);
4239 return false;
4241 format.Index += tokenLen - 1;
4243 // Some cultures uses space in the quoted string. E.g. Spanish has long date format as:
4244 // "dddd, dd' de 'MMMM' de 'yyyy". When inner spaces flag is set, we should skip whitespaces if there is space
4245 // in the quoted string.
4246 string quotedStr = StringBuilderCache.GetStringAndRelease(enquotedString);
4248 for (int i = 0; i < quotedStr.Length; i++)
4250 if (quotedStr[i] == ' ' && parseInfo.fAllowInnerWhite)
4252 str.SkipWhiteSpaces();
4254 else if (!str.Match(quotedStr[i]))
4256 // Can not find the matching quoted string.
4257 result.SetBadDateTimeFailure();
4258 return false;
4262 // The "r" and "u" formats incorrectly quoted 'GMT' and 'Z', respectively. We cannot
4263 // correct this mistake for DateTime.ParseExact for compatibility reasons, but we can
4264 // fix it for DateTimeOffset.ParseExact as DateTimeOffset has not been publically released
4265 // with this issue.
4266 if ((result.flags & ParseFlags.CaptureOffset) != 0)
4268 if (((result.flags & ParseFlags.Rfc1123Pattern) != 0 && quotedStr == GMTName) ||
4269 ((result.flags & ParseFlags.UtcSortPattern) != 0 && quotedStr == ZuluName))
4271 result.flags |= ParseFlags.TimeZoneUsed;
4272 result.timeZoneOffset = TimeSpan.Zero;
4276 break;
4277 case '%':
4278 // Skip this so we can get to the next pattern character.
4279 // Used in case like "%d", "%y"
4281 // Make sure the next character is not a '%' again.
4282 if (format.Index >= format.Value.Length - 1 || format.Value[format.Index + 1] == '%')
4284 result.SetBadFormatSpecifierFailure(format.Value);
4285 return false;
4287 break;
4288 case '\\':
4289 // Escape character. For example, "\d".
4290 // Get the next character in format, and see if we can
4291 // find a match in str.
4292 if (format.GetNext())
4294 if (!str.Match(format.GetChar()))
4296 // Can not find a match for the escaped character.
4297 result.SetBadDateTimeFailure();
4298 return false;
4301 else
4303 result.SetBadFormatSpecifierFailure(format.Value);
4304 return false;
4306 break;
4307 case '.':
4308 if (!str.Match(ch))
4310 if (format.GetNext())
4312 // If we encounter the pattern ".F", and the dot is not present, it is an optional
4313 // second fraction and we can skip this format.
4314 if (format.Match('F'))
4316 format.GetRepeatCount();
4317 break;
4320 result.SetBadDateTimeFailure();
4321 return false;
4323 break;
4324 default:
4325 if (ch == ' ')
4327 if (parseInfo.fAllowInnerWhite)
4329 // Skip whitespaces if AllowInnerWhite.
4330 // Do nothing here.
4332 else
4334 if (!str.Match(ch))
4336 // If the space does not match, and trailing space is allowed, we do
4337 // one more step to see if the next format character can lead to
4338 // successful parsing.
4339 // This is used to deal with special case that a empty string can match
4340 // a specific pattern.
4341 // The example here is af-ZA, which has a time format like "hh:mm:ss tt". However,
4342 // its AM symbol is "" (empty string). If fAllowTrailingWhite is used, and time is in
4343 // the AM, we will trim the whitespaces at the end, which will lead to a failure
4344 // when we are trying to match the space before "tt".
4345 if (parseInfo.fAllowTrailingWhite)
4347 if (format.GetNext())
4349 if (ParseByFormat(ref str, ref format, ref parseInfo, dtfi, ref result))
4351 return true;
4355 result.SetBadDateTimeFailure();
4356 return false;
4358 // Found a macth.
4361 else
4363 if (format.MatchSpecifiedWord(GMTName))
4365 format.Index += (GMTName.Length - 1);
4366 // Found GMT string in format. This means the DateTime string
4367 // is in GMT timezone.
4368 result.flags |= ParseFlags.TimeZoneUsed;
4369 result.timeZoneOffset = TimeSpan.Zero;
4370 if (!str.Match(GMTName))
4372 result.SetBadDateTimeFailure();
4373 return false;
4376 else if (!str.Match(ch))
4378 // ch is expected.
4379 result.SetBadDateTimeFailure();
4380 return false;
4383 break;
4384 } // switch
4385 return true;
4389 // The pos should point to a quote character. This method will
4390 // get the string enclosed by the quote character.
4392 internal static bool TryParseQuoteString(ReadOnlySpan<char> format, int pos, StringBuilder result, out int returnValue)
4395 // NOTE : pos will be the index of the quote character in the 'format' string.
4397 returnValue = 0;
4398 int formatLen = format.Length;
4399 int beginPos = pos;
4400 char quoteChar = format[pos++]; // Get the character used to quote the following string.
4402 bool foundQuote = false;
4403 while (pos < formatLen)
4405 char ch = format[pos++];
4406 if (ch == quoteChar)
4408 foundQuote = true;
4409 break;
4411 else if (ch == '\\')
4413 // The following are used to support escaped character.
4414 // Escaped character is also supported in the quoted string.
4415 // Therefore, someone can use a format like "'minute:' mm\"" to display:
4416 // minute: 45"
4417 // because the second double quote is escaped.
4418 if (pos < formatLen)
4420 result.Append(format[pos++]);
4422 else
4425 // This means that '\' is at the end of the formatting string.
4427 return false;
4430 else
4432 result.Append(ch);
4436 if (!foundQuote)
4438 // Here we can't find the matching quote.
4439 return false;
4443 // Return the character count including the begin/end quote characters and enclosed string.
4445 returnValue = (pos - beginPos);
4446 return true;
4449 /*=================================DoStrictParse==================================
4450 **Action: Do DateTime parsing using the format in formatParam.
4451 **Returns: The parsed DateTime.
4452 **Arguments:
4453 **Exceptions:
4455 **Notes:
4456 ** When the following general formats are used, InvariantInfo is used in dtfi:
4457 ** 'r', 'R', 's'.
4458 ** When the following general formats are used, the time is assumed to be in Universal time.
4460 **Limitations:
4461 ** Only GregorianCalendar is supported for now.
4462 ** Only support GMT timezone.
4463 ==============================================================================*/
4465 private static bool DoStrictParse(
4466 ReadOnlySpan<char> s,
4467 ReadOnlySpan<char> formatParam,
4468 DateTimeStyles styles,
4469 DateTimeFormatInfo dtfi,
4470 ref DateTimeResult result)
4472 ParsingInfo parseInfo = new ParsingInfo();
4473 parseInfo.Init();
4475 parseInfo.calendar = dtfi.Calendar;
4476 parseInfo.fAllowInnerWhite = ((styles & DateTimeStyles.AllowInnerWhite) != 0);
4477 parseInfo.fAllowTrailingWhite = ((styles & DateTimeStyles.AllowTrailingWhite) != 0);
4479 if (formatParam.Length == 1)
4481 char formatParamChar = formatParam[0];
4483 // Fast-paths for common and important formats/configurations.
4484 if (styles == DateTimeStyles.None)
4486 switch (formatParamChar)
4488 case 'R':
4489 case 'r':
4490 ConfigureFormatR(ref dtfi, ref parseInfo, ref result);
4491 return ParseFormatR(s, ref parseInfo, ref result);
4493 case 'O':
4494 case 'o':
4495 ConfigureFormatOS(ref dtfi, ref parseInfo);
4496 return ParseFormatO(s, ref result);
4500 if (((result.flags & ParseFlags.CaptureOffset) != 0) && formatParamChar == 'U')
4502 // The 'U' format is not allowed for DateTimeOffset
4503 result.SetBadFormatSpecifierFailure(formatParam);
4504 return false;
4507 formatParam = ExpandPredefinedFormat(formatParam, ref dtfi, ref parseInfo, ref result);
4510 bool bTimeOnly = false;
4511 result.calendar = parseInfo.calendar;
4513 if (parseInfo.calendar.ID == CalendarId.HEBREW)
4515 parseInfo.parseNumberDelegate = m_hebrewNumberParser;
4516 parseInfo.fCustomNumberParser = true;
4519 // Reset these values to negative one so that we could throw exception
4520 // if we have parsed every item twice.
4521 result.Hour = result.Minute = result.Second = -1;
4523 __DTString format = new __DTString(formatParam, dtfi, false);
4524 __DTString str = new __DTString(s, dtfi, false);
4526 if (parseInfo.fAllowTrailingWhite)
4528 // Trim trailing spaces if AllowTrailingWhite.
4529 format.TrimTail();
4530 format.RemoveTrailingInQuoteSpaces();
4531 str.TrimTail();
4534 if ((styles & DateTimeStyles.AllowLeadingWhite) != 0)
4536 format.SkipWhiteSpaces();
4537 format.RemoveLeadingInQuoteSpaces();
4538 str.SkipWhiteSpaces();
4542 // Scan every character in format and match the pattern in str.
4544 while (format.GetNext())
4546 // We trim inner spaces here, so that we will not eat trailing spaces when
4547 // AllowTrailingWhite is not used.
4548 if (parseInfo.fAllowInnerWhite)
4550 str.SkipWhiteSpaces();
4552 if (!ParseByFormat(ref str, ref format, ref parseInfo, dtfi, ref result))
4554 return false;
4558 if (str.Index < str.Value.Length - 1)
4560 // There are still remaining character in str.
4561 result.SetBadDateTimeFailure();
4562 return false;
4565 if (parseInfo.fUseTwoDigitYear && ((dtfi.FormatFlags & DateTimeFormatFlags.UseHebrewRule) == 0))
4567 // A two digit year value is expected. Check if the parsed year value is valid.
4568 if (result.Year >= 100)
4570 result.SetBadDateTimeFailure();
4571 return false;
4575 result.Year = parseInfo.calendar.ToFourDigitYear(result.Year);
4577 catch (ArgumentOutOfRangeException)
4579 result.SetBadDateTimeFailure();
4580 return false;
4584 if (parseInfo.fUseHour12)
4586 if (parseInfo.timeMark == TM.NotSet)
4588 // hh is used, but no AM/PM designator is specified.
4589 // Assume the time is AM.
4590 // Don't throw exceptions in here becasue it is very confusing for the caller.
4591 // I always got confused myself when I use "hh:mm:ss" to parse a time string,
4592 // and ParseExact() throws on me (because I didn't use the 24-hour clock 'HH').
4593 parseInfo.timeMark = TM.AM;
4595 if (result.Hour > 12)
4597 // AM/PM is used, but the value for HH is too big.
4598 result.SetBadDateTimeFailure();
4599 return false;
4601 if (parseInfo.timeMark == TM.AM)
4603 if (result.Hour == 12)
4605 result.Hour = 0;
4608 else
4610 result.Hour = (result.Hour == 12) ? 12 : result.Hour + 12;
4613 else
4615 // Military (24-hour time) mode
4617 // AM cannot be set with a 24-hour time like 17:15.
4618 // PM cannot be set with a 24-hour time like 03:15.
4619 if ((parseInfo.timeMark == TM.AM && result.Hour >= 12)
4620 || (parseInfo.timeMark == TM.PM && result.Hour < 12))
4622 result.SetBadDateTimeFailure();
4623 return false;
4627 // Check if the parsed string only contains hour/minute/second values.
4628 bTimeOnly = (result.Year == -1 && result.Month == -1 && result.Day == -1);
4629 if (!CheckDefaultDateTime(ref result, ref parseInfo.calendar, styles))
4631 return false;
4634 if (!bTimeOnly && dtfi.HasYearMonthAdjustment)
4636 if (!dtfi.YearMonthAdjustment(ref result.Year, ref result.Month, (result.flags & ParseFlags.ParsedMonthName) != 0))
4638 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
4639 return false;
4642 if (!parseInfo.calendar.TryToDateTime(result.Year, result.Month, result.Day,
4643 result.Hour, result.Minute, result.Second, 0, result.era, out result.parsedDate))
4645 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
4646 return false;
4648 if (result.fraction > 0)
4650 if (!result.parsedDate.TryAddTicks((long)Math.Round(result.fraction * Calendar.TicksPerSecond), out result.parsedDate))
4652 result.SetBadDateTimeFailure();
4653 return false;
4658 // We have to check day of week before we adjust to the time zone.
4659 // It is because the value of day of week may change after adjusting
4660 // to the time zone.
4662 if (parseInfo.dayOfWeek != -1)
4665 // Check if day of week is correct.
4667 if (parseInfo.dayOfWeek != (int)parseInfo.calendar.GetDayOfWeek(result.parsedDate))
4669 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_BadDayOfWeek));
4670 return false;
4674 return DetermineTimeZoneAdjustments(ref result, styles, bTimeOnly);
4677 private static bool ParseFormatR(ReadOnlySpan<char> source, ref ParsingInfo parseInfo, ref DateTimeResult result)
4679 // Example:
4680 // Tue, 03 Jan 2017 08:08:05 GMT
4682 // The format is exactly 29 characters.
4683 if ((uint)source.Length != 29)
4685 result.SetBadDateTimeFailure();
4686 return false;
4689 // Parse the three-letter day of week. Any casing is valid.
4690 DayOfWeek dayOfWeek;
4692 uint dow0 = source[0], dow1 = source[1], dow2 = source[2], comma = source[3];
4694 if ((dow0 | dow1 | dow2 | comma) > 0x7F)
4696 result.SetBadDateTimeFailure();
4697 return false;
4700 uint dowString = (dow0 << 24) | (dow1 << 16) | (dow2 << 8) | comma | 0x20202000;
4701 switch (dowString)
4703 case 0x73756E2c /* 'sun,' */: dayOfWeek = DayOfWeek.Sunday; break;
4704 case 0x6d6f6e2c /* 'mon,' */: dayOfWeek = DayOfWeek.Monday; break;
4705 case 0x7475652c /* 'tue,' */: dayOfWeek = DayOfWeek.Tuesday; break;
4706 case 0x7765642c /* 'wed,' */: dayOfWeek = DayOfWeek.Wednesday; break;
4707 case 0x7468752c /* 'thu,' */: dayOfWeek = DayOfWeek.Thursday; break;
4708 case 0x6672692c /* 'fri,' */: dayOfWeek = DayOfWeek.Friday; break;
4709 case 0x7361742c /* 'sat,' */: dayOfWeek = DayOfWeek.Saturday; break;
4710 default:
4711 result.SetBadDateTimeFailure();
4712 return false;
4716 if (source[4] != ' ')
4718 result.SetBadDateTimeFailure();
4719 return false;
4722 // Parse the two digit day.
4723 int day;
4725 uint digit1 = (uint)(source[5] - '0'), digit2 = (uint)(source[6] - '0');
4727 if (digit1 > 9 || digit2 > 9)
4729 result.SetBadDateTimeFailure();
4730 return false;
4733 day = (int)(digit1 * 10 + digit2);
4736 if (source[7] != ' ')
4738 result.SetBadDateTimeFailure();
4739 return false;
4742 // Parse the three letter month (followed by a space). Any casing is valid.
4743 int month;
4745 uint m0 = source[8], m1 = source[9], m2 = source[10], space = source[11];
4747 if ((m0 | m1 | m2 | space) > 0x7F)
4749 result.SetBadDateTimeFailure();
4750 return false;
4753 switch ((m0 << 24) | (m1 << 16) | (m2 << 8) | space | 0x20202000)
4755 case 0x6a616e20: /* 'jan ' */ month = 1; break;
4756 case 0x66656220: /* 'feb ' */ month = 2; break;
4757 case 0x6d617220: /* 'mar ' */ month = 3; break;
4758 case 0x61707220: /* 'apr ' */ month = 4; break;
4759 case 0x6d617920: /* 'may ' */ month = 5; break;
4760 case 0x6a756e20: /* 'jun ' */ month = 6; break;
4761 case 0x6a756c20: /* 'jul ' */ month = 7; break;
4762 case 0x61756720: /* 'aug ' */ month = 8; break;
4763 case 0x73657020: /* 'sep ' */ month = 9; break;
4764 case 0x6f637420: /* 'oct ' */ month = 10; break;
4765 case 0x6e6f7620: /* 'nov ' */ month = 11; break;
4766 case 0x64656320: /* 'dec ' */ month = 12; break;
4767 default:
4768 result.SetBadDateTimeFailure();
4769 return false;
4773 // Parse the four-digit year.
4774 int year;
4776 uint y1 = (uint)(source[12] - '0'), y2 = (uint)(source[13] - '0'), y3 = (uint)(source[14] - '0'), y4 = (uint)(source[15] - '0');
4778 if (y1 > 9 || y2 > 9 || y3 > 9 || y4 > 9)
4780 result.SetBadDateTimeFailure();
4781 return false;
4784 year = (int)(y1 * 1000 + y2 * 100 + y3 * 10 + y4);
4787 if (source[16] != ' ')
4789 result.SetBadDateTimeFailure();
4790 return false;
4793 // Parse the two digit hour.
4794 int hour;
4796 uint h1 = (uint)(source[17] - '0'), h2 = (uint)(source[18] - '0');
4798 if (h1 > 9 || h2 > 9)
4800 result.SetBadDateTimeFailure();
4801 return false;
4804 hour = (int)(h1 * 10 + h2);
4807 if (source[19] != ':')
4809 result.SetBadDateTimeFailure();
4810 return false;
4813 // Parse the two-digit minute.
4814 int minute;
4816 uint m1 = (uint)(source[20] - '0');
4817 uint m2 = (uint)(source[21] - '0');
4819 if (m1 > 9 || m2 > 9)
4821 result.SetBadDateTimeFailure();
4822 return false;
4825 minute = (int)(m1 * 10 + m2);
4828 if (source[22] != ':')
4830 result.SetBadDateTimeFailure();
4831 return false;
4834 // Parse the two-digit second.
4835 int second;
4837 uint s1 = (uint)(source[23] - '0'), s2 = (uint)(source[24] - '0');
4839 if (s1 > 9 || s2 > 9)
4841 result.SetBadDateTimeFailure();
4842 return false;
4845 second = (int)(s1 * 10 + s2);
4848 // Parse " GMT". It must be upper case.
4849 if (source[25] != ' ' || source[26] != 'G' || source[27] != 'M' || source[28] != 'T')
4851 result.SetBadDateTimeFailure();
4852 return false;
4855 // Validate that the parsed date is valid according to the calendar.
4856 if (!parseInfo.calendar.TryToDateTime(year, month, day, hour, minute, second, 0, 0, out result.parsedDate))
4858 result.SetFailure(ParseFailureKind.FormatBadDateTimeCalendar, nameof(SR.Format_BadDateTimeCalendar));
4859 return false;
4862 // And validate that the parsed day of week matches what the calendar said it should be.
4863 if (dayOfWeek != result.parsedDate.DayOfWeek)
4865 result.SetFailure(ParseFailureKind.FormatWithOriginalDateTime, nameof(SR.Format_BadDayOfWeek));
4866 return false;
4869 return true;
4872 private static bool ParseFormatO(ReadOnlySpan<char> source, ref DateTimeResult result)
4874 // Examples:
4875 // 2017-06-12T05:30:45.7680000 (interpreted as local time wrt to current time zone)
4876 // 2017-06-12T05:30:45.7680000Z (Z is short for "+00:00" but also distinguishes DateTimeKind.Utc from DateTimeKind.Local)
4877 // 2017-06-12T05:30:45.7680000-7:00 (special-case of one-digit offset hour)
4878 // 2017-06-12T05:30:45.7680000-07:00
4880 if ((uint)source.Length < 27 ||
4881 source[4] != '-' ||
4882 source[7] != '-' ||
4883 source[10] != 'T' ||
4884 source[13] != ':' ||
4885 source[16] != ':' ||
4886 source[19] != '.')
4888 result.SetBadDateTimeFailure();
4889 return false;
4892 int year;
4894 uint y1 = (uint)(source[0] - '0'), y2 = (uint)(source[1] - '0'), y3 = (uint)(source[2] - '0'), y4 = (uint)(source[3] - '0');
4896 if (y1 > 9 || y2 > 9 || y3 > 9 || y4 > 9)
4898 result.SetBadDateTimeFailure();
4899 return false;
4902 year = (int)(y1 * 1000 + y2 * 100 + y3 * 10 + y4);
4905 int month;
4907 uint m1 = (uint)(source[5] - '0'), m2 = (uint)(source[6] - '0');
4909 if (m1 > 9 || m2 > 9)
4911 result.SetBadDateTimeFailure();
4912 return false;
4915 month = (int)(m1 * 10 + m2);
4918 int day;
4920 uint d1 = (uint)(source[8] - '0'), d2 = (uint)(source[9] - '0');
4922 if (d1 > 9 || d2 > 9)
4924 result.SetBadDateTimeFailure();
4925 return false;
4928 day = (int)(d1 * 10 + d2);
4931 int hour;
4933 uint h1 = (uint)(source[11] - '0'), h2 = (uint)(source[12] - '0');
4935 if (h1 > 9 || h2 > 9)
4937 result.SetBadDateTimeFailure();
4938 return false;
4941 hour = (int)(h1 * 10 + h2);
4944 int minute;
4946 uint m1 = (uint)(source[14] - '0'), m2 = (uint)(source[15] - '0');
4948 if (m1 > 9 || m2 > 9)
4950 result.SetBadDateTimeFailure();
4951 return false;
4954 minute = (int)(m1 * 10 + m2);
4957 int second;
4959 uint s1 = (uint)(source[17] - '0'), s2 = (uint)(source[18] - '0');
4961 if (s1 > 9 || s2 > 9)
4963 result.SetBadDateTimeFailure();
4964 return false;
4967 second = (int)(s1 * 10 + s2);
4970 double fraction;
4972 uint f1 = (uint)(source[20] - '0');
4973 uint f2 = (uint)(source[21] - '0');
4974 uint f3 = (uint)(source[22] - '0');
4975 uint f4 = (uint)(source[23] - '0');
4976 uint f5 = (uint)(source[24] - '0');
4977 uint f6 = (uint)(source[25] - '0');
4978 uint f7 = (uint)(source[26] - '0');
4980 if (f1 > 9 || f2 > 9 || f3 > 9 || f4 > 9 || f5 > 9 || f6 > 9 || f7 > 9)
4982 result.SetBadDateTimeFailure();
4983 return false;
4986 fraction = (f1 * 1000000 + f2 * 100000 + f3 * 10000 + f4 * 1000 + f5 * 100 + f6 * 10 + f7) / 10000000.0;
4989 if (!DateTime.TryCreate(year, month, day, hour, minute, second, 0, out DateTime dateTime))
4991 result.SetBadDateTimeFailure();
4992 return false;
4995 if (!dateTime.TryAddTicks((long)Math.Round(fraction * Calendar.TicksPerSecond), out result.parsedDate))
4997 result.SetBadDateTimeFailure();
4998 return false;
5001 if ((uint)source.Length > 27)
5003 char offsetChar = source[27];
5004 switch (offsetChar)
5006 case 'Z':
5007 if (source.Length != 28)
5009 result.SetBadDateTimeFailure();
5010 return false;
5012 result.flags |= ParseFlags.TimeZoneUsed | ParseFlags.TimeZoneUtc;
5013 break;
5015 case '+':
5016 case '-':
5017 int offsetHours, colonIndex;
5019 if ((uint)source.Length == 33)
5021 uint oh1 = (uint)(source[28] - '0'), oh2 = (uint)(source[29] - '0');
5023 if (oh1 > 9 || oh2 > 9)
5025 result.SetBadDateTimeFailure();
5026 return false;
5029 offsetHours = (int)(oh1 * 10 + oh2);
5030 colonIndex = 30;
5032 else if ((uint)source.Length == 32) // special-case allowed for compat: only one offset hour digit
5034 offsetHours = source[28] - '0';
5036 if ((uint)offsetHours > 9)
5038 result.SetBadDateTimeFailure();
5039 return false;
5042 colonIndex = 29;
5044 else
5046 result.SetBadDateTimeFailure();
5047 return false;
5050 if (source[colonIndex] != ':')
5052 result.SetBadDateTimeFailure();
5053 return false;
5056 int offsetMinutes;
5058 uint om1 = (uint)(source[colonIndex + 1] - '0'), om2 = (uint)(source[colonIndex + 2] - '0');
5060 if (om1 > 9 || om2 > 9)
5062 result.SetBadDateTimeFailure();
5063 return false;
5066 offsetMinutes = (int)(om1 * 10 + om2);
5069 result.flags |= ParseFlags.TimeZoneUsed;
5070 result.timeZoneOffset = new TimeSpan(offsetHours, offsetMinutes, 0);
5071 if (offsetChar == '-')
5073 result.timeZoneOffset = result.timeZoneOffset.Negate();
5075 break;
5077 default:
5078 result.SetBadDateTimeFailure();
5079 return false;
5083 return DetermineTimeZoneAdjustments(ref result, DateTimeStyles.None, bTimeOnly: false);
5086 private static Exception GetDateTimeParseException(ref DateTimeResult result)
5088 switch (result.failure)
5090 case ParseFailureKind.ArgumentNull:
5091 return new ArgumentNullException(result.failureArgumentName, SR.GetResourceString(result.failureMessageID));
5092 case ParseFailureKind.Format:
5093 return new FormatException(SR.GetResourceString(result.failureMessageID));
5094 case ParseFailureKind.FormatWithParameter:
5095 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID)!, result.failureMessageFormatArgument));
5096 case ParseFailureKind.FormatBadDateTimeCalendar:
5097 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID)!, new string(result.originalDateTimeString), result.calendar));
5098 case ParseFailureKind.FormatWithOriginalDateTime:
5099 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID)!, new string(result.originalDateTimeString)));
5100 case ParseFailureKind.FormatWithFormatSpecifier:
5101 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID)!, new string(result.failedFormatSpecifier)));
5102 case ParseFailureKind.FormatWithOriginalDateTimeAndParameter:
5103 return new FormatException(SR.Format(SR.GetResourceString(result.failureMessageID)!, new string(result.originalDateTimeString), result.failureMessageFormatArgument));
5104 default:
5105 Debug.Fail("Unknown DateTimeParseFailure: " + result.failure.ToString());
5106 return null!;
5110 [Conditional("_LOGGING")]
5111 private static void LexTraceExit(string message, DS dps)
5113 #if _LOGGING
5114 if (!_tracingEnabled)
5115 return;
5116 Trace($"Lex return {message}, DS.{dps}");
5117 #endif // _LOGGING
5119 [Conditional("_LOGGING")]
5120 private static void PTSTraceExit(DS dps, bool passed)
5122 #if _LOGGING
5123 if (!_tracingEnabled)
5124 return;
5125 Trace($"ProcessTerminalState {(passed ? "passed" : "failed")} @ DS.{dps}");
5126 #endif // _LOGGING
5128 [Conditional("_LOGGING")]
5129 private static void TPTraceExit(string message, DS dps)
5131 #if _LOGGING
5132 if (!_tracingEnabled)
5133 return;
5134 Trace($"TryParse return {message}, DS.{dps}");
5135 #endif // _LOGGING
5137 [Conditional("_LOGGING")]
5138 private static void DTFITrace(DateTimeFormatInfo dtfi)
5140 #if _LOGGING
5141 if (!_tracingEnabled)
5142 return;
5144 Trace("DateTimeFormatInfo Properties");
5145 Trace($" NativeCalendarName {Hex(dtfi.NativeCalendarName)}");
5146 Trace($" AMDesignator {Hex(dtfi.AMDesignator)}");
5147 Trace($" PMDesignator {Hex(dtfi.PMDesignator)}");
5148 Trace($" TimeSeparator {Hex(dtfi.TimeSeparator)}");
5149 Trace($" AbbrvDayNames {Hex(dtfi.AbbreviatedDayNames)}");
5150 Trace($" ShortestDayNames {Hex(dtfi.ShortestDayNames)}");
5151 Trace($" DayNames {Hex(dtfi.DayNames)}");
5152 Trace($" AbbrvMonthNames {Hex(dtfi.AbbreviatedMonthNames)}");
5153 Trace($" MonthNames {Hex(dtfi.MonthNames)}");
5154 Trace($" AbbrvMonthGenNames {Hex(dtfi.AbbreviatedMonthGenitiveNames)}");
5155 Trace($" MonthGenNames {Hex(dtfi.MonthGenitiveNames)}");
5156 #endif // _LOGGING
5158 #if _LOGGING
5159 // return a string in the form: "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
5160 private static string Hex(string[] strs)
5162 if (strs == null || strs.Length == 0)
5163 return string.Empty;
5164 if (strs.Length == 1)
5165 return Hex(strs[0]);
5167 int curLineLength = 0;
5168 const int MaxLineLength = 55;
5169 const int NewLinePadding = 20;
5171 // invariant: strs.Length >= 2
5172 StringBuilder buffer = new StringBuilder();
5173 buffer.Append(Hex(strs[0]));
5174 curLineLength = buffer.Length;
5175 string s;
5177 for (int i = 1; i < strs.Length - 1; i++)
5179 s = Hex(strs[i]);
5181 if (s.Length > MaxLineLength || (curLineLength + s.Length + 2) > MaxLineLength)
5183 buffer.Append(',');
5184 buffer.Append(Environment.NewLine);
5185 buffer.Append(' ', NewLinePadding);
5186 curLineLength = 0;
5188 else
5190 buffer.Append(", ");
5191 curLineLength += 2;
5193 buffer.Append(s);
5194 curLineLength += s.Length;
5197 buffer.Append(',');
5198 s = Hex(strs[strs.Length - 1]);
5199 if (s.Length > MaxLineLength || (curLineLength + s.Length + 6) > MaxLineLength)
5201 buffer.Append(Environment.NewLine);
5202 buffer.Append(' ', NewLinePadding);
5204 else
5206 buffer.Append(' ');
5208 buffer.Append(s);
5209 return buffer.ToString();
5211 // return a string in the form: "Sun"
5212 private static string Hex(string str) => Hex((ReadOnlySpan<char>)str);
5213 private static string Hex(ReadOnlySpan<char> str)
5215 StringBuilder buffer = new StringBuilder();
5216 buffer.Append("\"");
5217 for (int i = 0; i < str.Length; i++)
5219 if (str[i] <= '\x007f')
5220 buffer.Append(str[i]);
5221 else
5222 buffer.Append("\\u").Append(((int)str[i]).ToString("x4", CultureInfo.InvariantCulture));
5224 buffer.Append("\"");
5225 return buffer.ToString();
5227 // return an unicode escaped string form of char c
5228 private static string Hex(char c)
5230 if (c <= '\x007f')
5231 return c.ToString(CultureInfo.InvariantCulture);
5232 else
5233 return "\\u" + ((int)c).ToString("x4", CultureInfo.InvariantCulture);
5236 private static void Trace(string s)
5238 // Internal.Console.WriteLine(s);
5241 // for testing; do not make this readonly
5242 private static bool _tracingEnabled = false;
5243 #endif // _LOGGING
5247 // This is a string parsing helper which wraps a String object.
5248 // It has a Index property which tracks
5249 // the current parsing pointer of the string.
5251 internal ref struct __DTString
5254 // Value property: stores the real string to be parsed.
5256 internal ReadOnlySpan<char> Value;
5259 // Index property: points to the character that we are currently parsing.
5261 internal int Index;
5263 // The length of Value string.
5264 internal int Length => Value.Length;
5266 // The current character to be looked at.
5267 internal char m_current;
5269 private readonly CompareInfo m_info;
5270 // Flag to indicate if we encouter an digit, we should check for token or not.
5271 // In some cultures, such as mn-MN, it uses "\x0031\x00a0\x0434\x04af\x0433\x044d\x044d\x0440\x00a0\x0441\x0430\x0440" in month names.
5272 private readonly bool m_checkDigitToken;
5274 internal __DTString(ReadOnlySpan<char> str, DateTimeFormatInfo dtfi, bool checkDigitToken) : this(str, dtfi)
5276 m_checkDigitToken = checkDigitToken;
5279 internal __DTString(ReadOnlySpan<char> str, DateTimeFormatInfo dtfi)
5281 Debug.Assert(dtfi != null, "Expected non-null DateTimeFormatInfo");
5283 Index = -1;
5284 Value = str;
5286 m_current = '\0';
5287 m_info = dtfi.CompareInfo;
5288 m_checkDigitToken = ((dtfi.FormatFlags & DateTimeFormatFlags.UseDigitPrefixInTokens) != 0);
5291 internal CompareInfo CompareInfo => m_info;
5294 // Advance the Index.
5295 // Return true if Index is NOT at the end of the string.
5297 // Typical usage:
5298 // while (str.GetNext())
5299 // {
5300 // char ch = str.GetChar()
5301 // }
5302 internal bool GetNext()
5304 Index++;
5305 if (Index < Length)
5307 m_current = Value[Index];
5308 return true;
5310 return false;
5313 internal bool AtEnd()
5315 return Index < Length ? false : true;
5318 internal bool Advance(int count)
5320 Debug.Assert(Index + count <= Length, "__DTString::Advance: Index + count <= len");
5321 Index += count;
5322 if (Index < Length)
5324 m_current = Value[Index];
5325 return true;
5327 return false;
5330 // Used by DateTime.Parse() to get the next token.
5331 internal void GetRegularToken(out TokenType tokenType, out int tokenValue, DateTimeFormatInfo dtfi)
5333 tokenValue = 0;
5334 if (Index >= Length)
5336 tokenType = TokenType.EndOfString;
5337 return;
5340 Start:
5341 if (DateTimeParse.IsDigit(m_current))
5343 // This is a digit.
5344 tokenValue = m_current - '0';
5345 int value;
5346 int start = Index;
5349 // Collect other digits.
5351 while (++Index < Length)
5353 m_current = Value[Index];
5354 value = m_current - '0';
5355 if (value >= 0 && value <= 9)
5357 tokenValue = tokenValue * 10 + value;
5359 else
5361 break;
5364 if (Index - start > DateTimeParse.MaxDateTimeNumberDigits)
5366 tokenType = TokenType.NumberToken;
5367 tokenValue = -1;
5369 else if (Index - start < 3)
5371 tokenType = TokenType.NumberToken;
5373 else
5375 // If there are more than 3 digits, assume that it's a year value.
5376 tokenType = TokenType.YearNumberToken;
5378 if (m_checkDigitToken)
5380 int save = Index;
5381 char saveCh = m_current;
5382 // Re-scan using the staring Index to see if this is a token.
5383 Index = start; // To include the first digit.
5384 m_current = Value[Index];
5385 TokenType tempType;
5386 int tempValue;
5387 // This DTFI has tokens starting with digits.
5388 // E.g. mn-MN has month name like "\x0031\x00a0\x0434\x04af\x0433\x044d\x044d\x0440\x00a0\x0441\x0430\x0440"
5389 if (dtfi.Tokenize(TokenType.RegularTokenMask, out tempType, out tempValue, ref this))
5391 tokenType = tempType;
5392 tokenValue = tempValue;
5393 // This is a token, so the Index has been advanced propertly in DTFI.Tokenizer().
5395 else
5397 // Use the number token value.
5398 // Restore the index.
5399 Index = save;
5400 m_current = saveCh;
5404 else if (char.IsWhiteSpace(m_current))
5406 // Just skip to the next character.
5407 while (++Index < Length)
5409 m_current = Value[Index];
5410 if (!char.IsWhiteSpace(m_current))
5412 goto Start;
5415 // We have reached the end of string.
5416 tokenType = TokenType.EndOfString;
5418 else
5420 dtfi.Tokenize(TokenType.RegularTokenMask, out tokenType, out tokenValue, ref this);
5424 internal TokenType GetSeparatorToken(DateTimeFormatInfo dtfi, out int indexBeforeSeparator, out char charBeforeSeparator)
5426 indexBeforeSeparator = Index;
5427 charBeforeSeparator = m_current;
5428 TokenType tokenType;
5429 if (!SkipWhiteSpaceCurrent())
5431 // Reach the end of the string.
5432 return TokenType.SEP_End;
5434 if (!DateTimeParse.IsDigit(m_current))
5436 // Not a digit. Tokenize it.
5437 bool found = dtfi.Tokenize(TokenType.SeparatorTokenMask, out tokenType, out _, ref this);
5438 if (!found)
5440 tokenType = TokenType.SEP_Space;
5443 else
5445 // Do nothing here. If we see a number, it will not be a separator. There is no need wasting time trying to find the
5446 // separator token.
5447 tokenType = TokenType.SEP_Space;
5449 return tokenType;
5452 [MethodImpl(MethodImplOptions.AggressiveInlining)]
5453 internal bool MatchSpecifiedWord(string target) =>
5454 Index + target.Length <= Length &&
5455 m_info.Compare(Value.Slice(Index, target.Length), target, CompareOptions.IgnoreCase) == 0;
5457 private static readonly char[] WhiteSpaceChecks = new char[] { ' ', '\u00A0' };
5459 internal bool MatchSpecifiedWords(string target, bool checkWordBoundary, ref int matchLength)
5461 int valueRemaining = Value.Length - Index;
5462 matchLength = target.Length;
5464 if (matchLength > valueRemaining || m_info.Compare(Value.Slice(Index, matchLength), target, CompareOptions.IgnoreCase) != 0)
5466 // Check word by word
5467 int targetPosition = 0; // Where we are in the target string
5468 int thisPosition = Index; // Where we are in this string
5469 int wsIndex = target.IndexOfAny(WhiteSpaceChecks, targetPosition);
5470 if (wsIndex == -1)
5472 return false;
5476 int segmentLength = wsIndex - targetPosition;
5477 if (thisPosition >= Value.Length - segmentLength)
5478 { // Subtraction to prevent overflow.
5479 return false;
5481 if (segmentLength == 0)
5483 // If segmentLength == 0, it means that we have leading space in the target string.
5484 // In that case, skip the leading spaces in the target and this string.
5485 matchLength--;
5487 else
5489 // Make sure we also have whitespace in the input string
5490 if (!char.IsWhiteSpace(Value[thisPosition + segmentLength]))
5492 return false;
5494 if (m_info.CompareOptionIgnoreCase(Value.Slice(thisPosition, segmentLength), target.AsSpan(targetPosition, segmentLength)) != 0)
5496 return false;
5498 // Advance the input string
5499 thisPosition = thisPosition + segmentLength + 1;
5501 // Advance our target string
5502 targetPosition = wsIndex + 1;
5504 // Skip past multiple whitespace
5505 while (thisPosition < Value.Length && char.IsWhiteSpace(Value[thisPosition]))
5507 thisPosition++;
5508 matchLength++;
5510 } while ((wsIndex = target.IndexOfAny(WhiteSpaceChecks, targetPosition)) >= 0);
5511 // now check the last segment;
5512 if (targetPosition < target.Length)
5514 int segmentLength = target.Length - targetPosition;
5515 if (thisPosition > Value.Length - segmentLength)
5517 return false;
5519 if (m_info.CompareOptionIgnoreCase(Value.Slice(thisPosition, segmentLength), target.AsSpan(targetPosition, segmentLength)) != 0)
5521 return false;
5526 if (checkWordBoundary)
5528 int nextCharIndex = Index + matchLength;
5529 if (nextCharIndex < Value.Length)
5531 if (char.IsLetter(Value[nextCharIndex]))
5533 return false;
5537 return true;
5541 // Check to see if the string starting from Index is a prefix of
5542 // str.
5543 // If a match is found, true value is returned and Index is updated to the next character to be parsed.
5544 // Otherwise, Index is unchanged.
5546 internal bool Match(string str)
5548 if (++Index >= Length)
5550 return false;
5553 if (str.Length > (Value.Length - Index))
5555 return false;
5558 if (m_info.Compare(Value.Slice(Index, str.Length), str, CompareOptions.Ordinal) == 0)
5560 // Update the Index to the end of the matching string.
5561 // So the following GetNext()/Match() opeartion will get
5562 // the next character to be parsed.
5563 Index += (str.Length - 1);
5564 return true;
5566 return false;
5569 internal bool Match(char ch)
5571 if (++Index >= Length)
5573 return false;
5575 if (Value[Index] == ch)
5577 m_current = ch;
5578 return true;
5580 Index--;
5581 return false;
5585 // Actions: From the current position, try matching the longest word in the specified string array.
5586 // E.g. words[] = {"AB", "ABC", "ABCD"}, if the current position points to a substring like "ABC DEF",
5587 // MatchLongestWords(words, ref MaxMatchStrLen) will return 1 (the index), and maxMatchLen will be 3.
5588 // Returns:
5589 // The index that contains the longest word to match
5590 // Arguments:
5591 // words The string array that contains words to search.
5592 // maxMatchStrLen [in/out] the initialized maximum length. This parameter can be used to
5593 // find the longest match in two string arrays.
5595 internal int MatchLongestWords(string[] words, ref int maxMatchStrLen)
5597 int result = -1;
5598 for (int i = 0; i < words.Length; i++)
5600 string word = words[i];
5601 int matchLength = word.Length;
5602 if (MatchSpecifiedWords(word, false, ref matchLength))
5604 if (matchLength > maxMatchStrLen)
5606 maxMatchStrLen = matchLength;
5607 result = i;
5612 return result;
5616 // Get the number of repeat character after the current character.
5617 // For a string "hh:mm:ss" at Index of 3. GetRepeatCount() = 2, and Index
5618 // will point to the second ':'.
5620 internal int GetRepeatCount()
5622 char repeatChar = Value[Index];
5623 int pos = Index + 1;
5624 while ((pos < Length) && (Value[pos] == repeatChar))
5626 pos++;
5628 int repeatCount = (pos - Index);
5629 // Update the Index to the end of the repeated characters.
5630 // So the following GetNext() opeartion will get
5631 // the next character to be parsed.
5632 Index = pos - 1;
5633 return repeatCount;
5636 // Return false when end of string is encountered or a non-digit character is found.
5637 [MethodImpl(MethodImplOptions.AggressiveInlining)]
5638 internal bool GetNextDigit() =>
5639 ++Index < Length &&
5640 DateTimeParse.IsDigit(Value[Index]);
5643 // Get the current character.
5645 internal char GetChar()
5647 Debug.Assert(Index >= 0 && Index < Length, "Index >= 0 && Index < len");
5648 return Value[Index];
5652 // Convert the current character to a digit, and return it.
5654 internal int GetDigit()
5656 Debug.Assert(Index >= 0 && Index < Length, "Index >= 0 && Index < len");
5657 Debug.Assert(DateTimeParse.IsDigit(Value[Index]), "IsDigit(Value[Index])");
5658 return Value[Index] - '0';
5662 // Eat White Space ahead of the current position
5664 // Return false if end of string is encountered.
5666 internal void SkipWhiteSpaces()
5668 // Look ahead to see if the next character
5669 // is a whitespace.
5670 while (Index + 1 < Length)
5672 char ch = Value[Index + 1];
5673 if (!char.IsWhiteSpace(ch))
5675 return;
5677 Index++;
5679 return;
5683 // Skip white spaces from the current position
5685 // Return false if end of string is encountered.
5687 internal bool SkipWhiteSpaceCurrent()
5689 if (Index >= Length)
5691 return false;
5694 if (!char.IsWhiteSpace(m_current))
5696 return true;
5699 while (++Index < Length)
5701 m_current = Value[Index];
5702 if (!char.IsWhiteSpace(m_current))
5704 return true;
5706 // Nothing here.
5708 return false;
5711 internal void TrimTail()
5713 int i = Length - 1;
5714 while (i >= 0 && char.IsWhiteSpace(Value[i]))
5716 i--;
5718 Value = Value.Slice(0, i + 1);
5721 // Trim the trailing spaces within a quoted string.
5722 // Call this after TrimTail() is done.
5723 internal void RemoveTrailingInQuoteSpaces()
5725 int i = Length - 1;
5726 if (i <= 1)
5728 return;
5730 char ch = Value[i];
5731 // Check if the last character is a quote.
5732 if (ch == '\'' || ch == '\"')
5734 if (char.IsWhiteSpace(Value[i - 1]))
5736 i--;
5737 while (i >= 1 && char.IsWhiteSpace(Value[i - 1]))
5739 i--;
5741 Span<char> result = new char[i + 1];
5742 result[i] = ch;
5743 Value.Slice(0, i).CopyTo(result);
5744 Value = result;
5749 // Trim the leading spaces within a quoted string.
5750 // Call this after the leading spaces before quoted string are trimmed.
5751 internal void RemoveLeadingInQuoteSpaces()
5753 if (Length <= 2)
5755 return;
5757 int i = 0;
5758 char ch = Value[i];
5759 // Check if the last character is a quote.
5760 if (ch == '\'' || ch == '\"')
5762 while ((i + 1) < Length && char.IsWhiteSpace(Value[i + 1]))
5764 i++;
5766 if (i != 0)
5768 Span<char> result = new char[Value.Length - i];
5769 result[0] = ch;
5770 Value.Slice(i + 1).CopyTo(result.Slice(1));
5771 Value = result;
5776 internal DTSubString GetSubString()
5778 DTSubString sub = new DTSubString();
5779 sub.index = Index;
5780 sub.s = Value;
5781 while (Index + sub.length < Length)
5783 DTSubStringType currentType;
5784 char ch = Value[Index + sub.length];
5785 if (ch >= '0' && ch <= '9')
5787 currentType = DTSubStringType.Number;
5789 else
5791 currentType = DTSubStringType.Other;
5794 if (sub.length == 0)
5796 sub.type = currentType;
5798 else
5800 if (sub.type != currentType)
5802 break;
5805 sub.length++;
5806 if (currentType == DTSubStringType.Number)
5808 // Incorporate the number into the value
5809 // Limit the digits to prevent overflow
5810 if (sub.length > DateTimeParse.MaxDateTimeNumberDigits)
5812 sub.type = DTSubStringType.Invalid;
5813 return sub;
5815 int number = ch - '0';
5816 Debug.Assert(number >= 0 && number <= 9, "number >= 0 && number <= 9");
5817 sub.value = sub.value * 10 + number;
5819 else
5821 // For non numbers, just return this length 1 token. This should be expanded
5822 // to more types of thing if this parsing approach is used for things other
5823 // than numbers and single characters
5824 break;
5827 if (sub.length == 0)
5829 sub.type = DTSubStringType.End;
5830 return sub;
5833 return sub;
5836 internal void ConsumeSubString(DTSubString sub)
5838 Debug.Assert(sub.index == Index, "sub.index == Index");
5839 Debug.Assert(sub.index + sub.length <= Length, "sub.index + sub.length <= len");
5840 Index = sub.index + sub.length;
5841 if (Index < Length)
5843 m_current = Value[Index];
5848 internal enum DTSubStringType
5850 Unknown = 0,
5851 Invalid = 1,
5852 Number = 2,
5853 End = 3,
5854 Other = 4,
5857 internal ref struct DTSubString
5859 internal ReadOnlySpan<char> s;
5860 internal int index;
5861 internal int length;
5862 internal DTSubStringType type;
5863 internal int value;
5865 internal char this[int relativeIndex] => s[index + relativeIndex];
5869 // The buffer to store the parsing token.
5871 internal
5872 struct DateTimeToken
5874 internal DateTimeParse.DTT dtt; // Store the token
5875 internal TokenType suffix; // Store the CJK Year/Month/Day suffix (if any)
5876 internal int num; // Store the number that we are parsing (if any)
5880 // The buffer to store temporary parsing information.
5882 internal unsafe struct DateTimeRawInfo
5884 private int* num;
5885 internal int numCount;
5886 internal int month;
5887 internal int year;
5888 internal int dayOfWeek;
5889 internal int era;
5890 internal DateTimeParse.TM timeMark;
5891 internal double fraction;
5892 internal bool hasSameDateAndTimeSeparators;
5894 internal void Init(int* numberBuffer)
5896 month = -1;
5897 year = -1;
5898 dayOfWeek = -1;
5899 era = -1;
5900 timeMark = DateTimeParse.TM.NotSet;
5901 fraction = -1;
5902 num = numberBuffer;
5905 internal void AddNumber(int value)
5907 num[numCount++] = value;
5910 internal int GetNumber(int index)
5912 return num[index];
5916 internal enum ParseFailureKind
5918 None = 0,
5919 ArgumentNull = 1,
5920 Format = 2,
5921 FormatWithParameter = 3,
5922 FormatWithOriginalDateTime = 4,
5923 FormatWithFormatSpecifier = 5,
5924 FormatWithOriginalDateTimeAndParameter = 6,
5925 FormatBadDateTimeCalendar = 7, // FormatException when ArgumentOutOfRange is thrown by a Calendar.TryToDateTime().
5928 [Flags]
5929 internal enum ParseFlags
5931 HaveYear = 0x00000001,
5932 HaveMonth = 0x00000002,
5933 HaveDay = 0x00000004,
5934 HaveHour = 0x00000008,
5935 HaveMinute = 0x00000010,
5936 HaveSecond = 0x00000020,
5937 HaveTime = 0x00000040,
5938 HaveDate = 0x00000080,
5939 TimeZoneUsed = 0x00000100,
5940 TimeZoneUtc = 0x00000200,
5941 ParsedMonthName = 0x00000400,
5942 CaptureOffset = 0x00000800,
5943 YearDefault = 0x00001000,
5944 Rfc1123Pattern = 0x00002000,
5945 UtcSortPattern = 0x00004000,
5949 // This will store the result of the parsing. And it will be eventually
5950 // used to construct a DateTime instance.
5952 internal ref struct DateTimeResult
5954 internal int Year;
5955 internal int Month;
5956 internal int Day;
5958 // Set time default to 00:00:00.
5960 internal int Hour;
5961 internal int Minute;
5962 internal int Second;
5963 internal double fraction;
5965 internal int era;
5967 internal ParseFlags flags;
5969 internal TimeSpan timeZoneOffset;
5971 internal Calendar calendar;
5973 internal DateTime parsedDate;
5975 internal ParseFailureKind failure;
5976 internal string failureMessageID;
5977 internal object? failureMessageFormatArgument;
5978 internal string failureArgumentName;
5979 internal ReadOnlySpan<char> originalDateTimeString;
5980 internal ReadOnlySpan<char> failedFormatSpecifier;
5982 internal void Init(ReadOnlySpan<char> originalDateTimeString)
5984 this.originalDateTimeString = originalDateTimeString;
5985 Year = -1;
5986 Month = -1;
5987 Day = -1;
5988 fraction = -1;
5989 era = -1;
5992 internal void SetDate(int year, int month, int day)
5994 Year = year;
5995 Month = month;
5996 Day = day;
5999 internal void SetBadFormatSpecifierFailure()
6001 SetBadFormatSpecifierFailure(ReadOnlySpan<char>.Empty);
6004 internal void SetBadFormatSpecifierFailure(ReadOnlySpan<char> failedFormatSpecifier)
6006 this.failure = ParseFailureKind.FormatWithFormatSpecifier;
6007 this.failureMessageID = nameof(SR.Format_BadFormatSpecifier);
6008 this.failedFormatSpecifier = failedFormatSpecifier;
6011 internal void SetBadDateTimeFailure()
6013 this.failure = ParseFailureKind.FormatWithOriginalDateTime;
6014 this.failureMessageID = nameof(SR.Format_BadDateTime);
6015 this.failureMessageFormatArgument = null;
6018 internal void SetFailure(ParseFailureKind failure, string failureMessageID)
6020 this.failure = failure;
6021 this.failureMessageID = failureMessageID;
6022 this.failureMessageFormatArgument = null;
6025 internal void SetFailure(ParseFailureKind failure, string failureMessageID, object? failureMessageFormatArgument)
6027 this.failure = failure;
6028 this.failureMessageID = failureMessageID;
6029 this.failureMessageFormatArgument = failureMessageFormatArgument;
6032 internal void SetFailure(ParseFailureKind failure, string failureMessageID, object? failureMessageFormatArgument, string failureArgumentName)
6034 this.failure = failure;
6035 this.failureMessageID = failureMessageID;
6036 this.failureMessageFormatArgument = failureMessageFormatArgument;
6037 this.failureArgumentName = failureArgumentName;
6041 // This is the helper data structure used in ParseExact().
6042 internal struct ParsingInfo
6044 internal Calendar calendar;
6045 internal int dayOfWeek;
6046 internal DateTimeParse.TM timeMark;
6048 internal bool fUseHour12;
6049 internal bool fUseTwoDigitYear;
6050 internal bool fAllowInnerWhite;
6051 internal bool fAllowTrailingWhite;
6052 internal bool fCustomNumberParser;
6053 internal DateTimeParse.MatchNumberDelegate parseNumberDelegate;
6055 internal void Init()
6057 dayOfWeek = -1;
6058 timeMark = DateTimeParse.TM.NotSet;
6063 // The type of token that will be returned by DateTimeFormatInfo.Tokenize().
6065 internal enum TokenType
6067 // The valid token should start from 1.
6069 // Regular tokens. The range is from 0x00 ~ 0xff.
6070 NumberToken = 1, // The number. E.g. "12"
6071 YearNumberToken = 2, // The number which is considered as year number, which has 3 or more digits. E.g. "2003"
6072 Am = 3, // AM timemark. E.g. "AM"
6073 Pm = 4, // PM timemark. E.g. "PM"
6074 MonthToken = 5, // A word (or words) that represents a month name. E.g. "March"
6075 EndOfString = 6, // End of string
6076 DayOfWeekToken = 7, // A word (or words) that represents a day of week name. E.g. "Monday" or "Mon"
6077 TimeZoneToken = 8, // A word that represents a timezone name. E.g. "GMT"
6078 EraToken = 9, // A word that represents a era name. E.g. "A.D."
6079 DateWordToken = 10, // A word that can appear in a DateTime string, but serves no parsing semantics. E.g. "de" in Spanish culture.
6080 UnknownToken = 11, // An unknown word, which signals an error in parsing.
6081 HebrewNumber = 12, // A number that is composed of Hebrew text. Hebrew calendar uses Hebrew digits for year values, month values, and day values.
6082 JapaneseEraToken = 13, // Era name for JapaneseCalendar
6083 TEraToken = 14, // Era name for TaiwanCalendar
6084 IgnorableSymbol = 15, // A separator like "," that is equivalent to whitespace
6086 // Separator tokens.
6087 SEP_Unk = 0x100, // Unknown separator.
6088 SEP_End = 0x200, // The end of the parsing string.
6089 SEP_Space = 0x300, // Whitespace (including comma).
6090 SEP_Am = 0x400, // AM timemark. E.g. "AM"
6091 SEP_Pm = 0x500, // PM timemark. E.g. "PM"
6092 SEP_Date = 0x600, // date separator. E.g. "/"
6093 SEP_Time = 0x700, // time separator. E.g. ":"
6094 SEP_YearSuff = 0x800, // Chinese/Japanese/Korean year suffix.
6095 SEP_MonthSuff = 0x900, // Chinese/Japanese/Korean month suffix.
6096 SEP_DaySuff = 0xa00, // Chinese/Japanese/Korean day suffix.
6097 SEP_HourSuff = 0xb00, // Chinese/Japanese/Korean hour suffix.
6098 SEP_MinuteSuff = 0xc00, // Chinese/Japanese/Korean minute suffix.
6099 SEP_SecondSuff = 0xd00, // Chinese/Japanese/Korean second suffix.
6100 SEP_LocalTimeMark = 0xe00, // 'T', used in ISO 8601 format.
6101 SEP_DateOrOffset = 0xf00, // '-' which could be a date separator or start of a time zone offset
6103 RegularTokenMask = 0x00ff,
6104 SeparatorTokenMask = 0xff00,