1 // Licensed to the .NET Foundation under one or more agreements.
2 // The .NET Foundation licenses this file to you under the MIT license.
3 // See the LICENSE file in the project root for more information.
5 using System
.Diagnostics
;
6 using System
.Globalization
;
7 using System
.Runtime
.CompilerServices
;
12 internal static class DateTimeParse
14 internal const 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.
24 if (TryParseExact(s
, format
, dtfi
, style
, ref result
))
26 return result
.parsedDate
;
30 throw GetDateTimeParseException(ref result
);
34 internal static DateTime
ParseExact(ReadOnlySpan
<char> s
, ReadOnlySpan
<char> format
, DateTimeFormatInfo dtfi
, DateTimeStyles style
, out TimeSpan offset
)
36 DateTimeResult result
= default; // The buffer to store the parsing result.
38 result
.flags
|= ParseFlags
.CaptureOffset
;
39 if (TryParseExact(s
, format
, dtfi
, style
, ref result
))
41 offset
= result
.timeZoneOffset
;
42 return result
.parsedDate
;
46 throw GetDateTimeParseException(ref result
);
50 internal static bool TryParseExact(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.
55 if (TryParseExact(s
, format
, dtfi
, style
, ref resultData
))
57 result
= resultData
.parsedDate
;
61 result
= DateTime
.MinValue
;
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.
69 resultData
.flags
|= ParseFlags
.CaptureOffset
;
71 if (TryParseExact(s
, format
, dtfi
, style
, ref resultData
))
73 result
= resultData
.parsedDate
;
74 offset
= resultData
.timeZoneOffset
;
78 result
= DateTime
.MinValue
;
79 offset
= TimeSpan
.Zero
;
83 internal static bool TryParseExact(ReadOnlySpan
<char> s
, ReadOnlySpan
<char> format
, DateTimeFormatInfo dtfi
, DateTimeStyles style
, ref DateTimeResult result
)
87 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDateTime
));
91 if (format
.Length
== 0)
93 result
.SetBadFormatSpecifierFailure();
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.
107 if (TryParseExactMultiple(s
, formats
, dtfi
, style
, ref result
))
109 return result
.parsedDate
;
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.
122 result
.flags
|= ParseFlags
.CaptureOffset
;
123 if (TryParseExactMultiple(s
, formats
, dtfi
, style
, ref result
))
125 offset
= result
.timeZoneOffset
;
126 return result
.parsedDate
;
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.
139 resultData
.flags
|= ParseFlags
.CaptureOffset
;
141 if (TryParseExactMultiple(s
, formats
, dtfi
, style
, ref resultData
))
143 result
= resultData
.parsedDate
;
144 offset
= resultData
.timeZoneOffset
;
148 result
= DateTime
.MinValue
;
149 offset
= TimeSpan
.Zero
;
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.
159 if (TryParseExactMultiple(s
, formats
, dtfi
, style
, ref resultData
))
161 result
= resultData
.parsedDate
;
165 result
= DateTime
.MinValue
;
169 internal static bool TryParseExactMultiple(ReadOnlySpan
<char> s
, string?[]? formats
,
170 DateTimeFormatInfo dtfi
, DateTimeStyles style
, ref DateTimeResult result
)
174 result
.SetFailure(ParseFailureKind
.ArgumentNull
, nameof(SR
.ArgumentNull_String
), null, nameof(formats
));
180 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDateTime
));
184 if (formats
.Length
== 0)
186 result
.SetFailure(ParseFailureKind
.Format
, nameof(SR
.Format_NoFormatSpecifier
));
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();
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.
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
;
215 result
.SetBadDateTimeFailure();
219 ////////////////////////////////////////////////////////////////////////////
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
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.
252 NumLocalTimeMark
= 19, // Num + 'T'
263 ////////////////////////////////////////////////////////////////////////////
265 // DateTime parsing state enumeration (DS.*)
267 ////////////////////////////////////////////////////////////////////////////
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
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.
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 }
,
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 }
,
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 }
,
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 }
,
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 }
,
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 }
,
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
))
413 if (str
.CompareInfo
.Compare(str
.Value
.Slice(str
.Index
, target
.Length
), target
, CompareOptions
.IgnoreCase
) != 0)
418 int nextCharIndex
= str
.Index
+ target
.Length
;
420 if (nextCharIndex
< str
.Value
.Length
)
422 char nextCh
= str
.Value
[nextCharIndex
];
423 if (char.IsLetter(nextCh
))
428 str
.Index
= nextCharIndex
;
429 if (str
.Index
< str
.Length
)
431 str
.m_current
= str
.Value
[str
.Index
];
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
))
447 if (MatchWord(ref str
, ZuluName
))
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.
464 ** str the parsing string
466 ============================================================================*/
468 private static bool ParseFraction(ref __DTString str
, out double result
)
471 double decimalBase
= 0.1;
475 && IsDigit(ch
= str
.m_current
))
477 result
+= (ch
- '0') * decimalBase
;
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.
490 ** str the parsing string
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.
499 int minuteOffset
= 0;
501 // Consume the +/- character that has already been read
502 DTSubString sub
= str
.GetSubString();
507 char offsetChar
= sub
[0];
508 if (offsetChar
!= '+' && offsetChar
!= '-')
512 str
.ConsumeSubString(sub
);
514 sub
= str
.GetSubString();
515 if (sub
.type
!= DTSubStringType
.Number
)
519 int value = sub
.value;
520 int length
= sub
.length
;
521 if (length
== 1 || length
== 2)
523 // Parsing "+8" or "+08"
525 str
.ConsumeSubString(sub
);
526 // See if we have minutes
527 sub
= str
.GetSubString();
528 if (sub
.length
== 1 && sub
[0] == ':')
530 // Parsing "+8:00" or "+08:00"
531 str
.ConsumeSubString(sub
);
532 sub
= str
.GetSubString();
533 if (sub
.type
!= DTSubStringType
.Number
|| sub
.length
< 1 || sub
.length
> 2)
537 minuteOffset
= sub
.value;
538 str
.ConsumeSubString(sub
);
541 else if (length
== 3 || length
== 4)
543 // Parsing "+800" or "+0800"
544 hourOffset
= value / 100;
545 minuteOffset
= value % 100;
546 str
.ConsumeSubString(sub
);
550 // Wrong number of digits
553 Debug
.Assert(hourOffset
>= 0 && hourOffset
<= 99, "hourOffset >= 0 && hourOffset <= 99");
554 Debug
.Assert(minuteOffset
>= 0 && minuteOffset
<= 99, "minuteOffset >= 0 && minuteOffset <= 99");
555 if (minuteOffset
< 0 || minuteOffset
>= 60)
560 result
= new TimeSpan(hourOffset
, minuteOffset
, 0);
561 if (offsetChar
== '-')
563 result
= result
.Negate();
568 // This is the helper function to handle timezone in string in the format like +/-0800
569 private static bool HandleTimeZone(ref __DTString str
, ref DateTimeResult result
)
571 if (str
.Index
< str
.Length
- 1)
573 char nextCh
= str
.Value
[str
.Index
];
574 // Skip whitespace, but don't update the index unless we find a time zone marker
575 int whitespaceCount
= 0;
576 while (char.IsWhiteSpace(nextCh
) && str
.Index
+ whitespaceCount
< str
.Length
- 1)
579 nextCh
= str
.Value
[str
.Index
+ whitespaceCount
];
581 if (nextCh
== '+' || nextCh
== '-')
583 str
.Index
+= whitespaceCount
;
584 if ((result
.flags
& ParseFlags
.TimeZoneUsed
) != 0)
586 // Should not have two timezone offsets.
587 result
.SetBadDateTimeFailure();
590 result
.flags
|= ParseFlags
.TimeZoneUsed
;
591 if (!ParseTimeZone(ref str
, ref result
.timeZoneOffset
))
593 result
.SetBadDateTimeFailure();
602 // This is the lexer. Check the character at the current index, and put the found token in dtok and
603 // some raw date/time information in raw.
605 private static bool Lex(DS dps
, ref __DTString str
, ref DateTimeToken dtok
, ref DateTimeRawInfo raw
, ref DateTimeResult result
, ref DateTimeFormatInfo dtfi
, DateTimeStyles styles
)
609 int indexBeforeSeparator
;
610 char charBeforeSeparator
;
613 dtok
.dtt
= DTT
.Unk
; // Assume the token is unkown.
615 str
.GetRegularToken(out tokenType
, out tokenValue
, dtfi
);
620 Trace($"Lex({Hex(str.Value)})\tpos:{str.Index}({Hex(str.m_current)}), {tokenType}, DS.{dps}");
624 // Look at the regular token.
627 case TokenType
.NumberToken
:
628 case TokenType
.YearNumberToken
:
629 if (raw
.numCount
== 3 || tokenValue
== -1)
631 result
.SetBadDateTimeFailure();
632 LexTraceExit("0010", dps
);
638 // If the previous parsing state is DS.T_NNt (like 12:01), and we got another number,
639 // so we will have a terminal state DS.TX_NNN (like 12:01:02).
640 // If the previous parsing state is DS.T_Nt (like 12:), and we got another number,
641 // so we will have a terminal state DS.TX_NN (like 12:01).
643 // Look ahead to see if the following character is a decimal point or timezone offset.
644 // This enables us to parse time in the forms of:
645 // "11:22:33.1234" or "11:22:33-08".
648 if (str
.Index
< str
.Length
- 1)
650 char nextCh
= str
.Value
[str
.Index
];
653 // While ParseFraction can fail, it just means that there were no digits after
654 // the dot. In this case ParseFraction just removes the dot. This is actually
655 // valid for cultures like Albanian, that join the time marker to the time with
656 // with a dot: e.g. "9:03.MD"
657 ParseFraction(ref str
, out raw
.fraction
);
661 if (dps
== DS
.T_NNt
|| dps
== DS
.T_Nt
)
663 if (str
.Index
< str
.Length
- 1)
665 if (!HandleTimeZone(ref str
, ref result
))
667 LexTraceExit("0020 (value like \"12:01\" or \"12:\" followed by a non-TZ number", dps
);
673 dtok
.num
= tokenValue
;
674 if (tokenType
== TokenType
.YearNumberToken
)
678 raw
.year
= tokenValue
;
680 // If we have number which has 3 or more digits (like "001" or "0001"),
681 // we assume this number is a year. Save the current raw.numCount in
684 switch (sep
= str
.GetSeparatorToken(dtfi
, out indexBeforeSeparator
, out charBeforeSeparator
))
686 case TokenType
.SEP_End
:
687 dtok
.dtt
= DTT
.YearEnd
;
689 case TokenType
.SEP_Am
:
690 case TokenType
.SEP_Pm
:
691 if (raw
.timeMark
== TM
.NotSet
)
693 raw
.timeMark
= (sep
== TokenType
.SEP_Am
? TM
.AM
: TM
.PM
);
694 dtok
.dtt
= DTT
.YearSpace
;
698 result
.SetBadDateTimeFailure();
699 LexTraceExit("0030 (TM.AM/TM.PM Happened more than 1x)", dps
);
702 case TokenType
.SEP_Space
:
703 dtok
.dtt
= DTT
.YearSpace
;
705 case TokenType
.SEP_Date
:
706 dtok
.dtt
= DTT
.YearDateSep
;
708 case TokenType
.SEP_Time
:
709 if (!raw
.hasSameDateAndTimeSeparators
)
711 result
.SetBadDateTimeFailure();
712 LexTraceExit("0040 (Invalid separator after number)", dps
);
716 // we have the date and time separators are same and getting a year number, then change the token to YearDateSep as
717 // we are sure we are not parsing time.
718 dtok
.dtt
= DTT
.YearDateSep
;
720 case TokenType
.SEP_DateOrOffset
:
721 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
722 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
723 if ((dateParsingStates
[(int)dps
][(int)DTT
.YearDateSep
] == DS
.ERROR
)
724 && (dateParsingStates
[(int)dps
][(int)DTT
.YearSpace
] > DS
.ERROR
))
726 str
.Index
= indexBeforeSeparator
;
727 str
.m_current
= charBeforeSeparator
;
728 dtok
.dtt
= DTT
.YearSpace
;
732 dtok
.dtt
= DTT
.YearDateSep
;
735 case TokenType
.SEP_YearSuff
:
736 case TokenType
.SEP_MonthSuff
:
737 case TokenType
.SEP_DaySuff
:
738 dtok
.dtt
= DTT
.NumDatesuff
;
741 case TokenType
.SEP_HourSuff
:
742 case TokenType
.SEP_MinuteSuff
:
743 case TokenType
.SEP_SecondSuff
:
744 dtok
.dtt
= DTT
.NumTimesuff
;
748 // Invalid separator after number number.
749 result
.SetBadDateTimeFailure();
750 LexTraceExit("0040 (Invalid separator after number)", dps
);
754 // Found the token already. Return now.
756 LexTraceExit("0050 (success)", dps
);
759 result
.SetBadDateTimeFailure();
760 LexTraceExit("0060", dps
);
763 switch (sep
= str
.GetSeparatorToken(dtfi
, out indexBeforeSeparator
, out charBeforeSeparator
))
766 // Note here we check if the numCount is less than three.
767 // When we have more than three numbers, it will be caught as error in the state machine.
769 case TokenType
.SEP_End
:
770 dtok
.dtt
= DTT
.NumEnd
;
771 raw
.AddNumber(dtok
.num
);
773 case TokenType
.SEP_Am
:
774 case TokenType
.SEP_Pm
:
775 if (raw
.timeMark
== TM
.NotSet
)
777 raw
.timeMark
= (sep
== TokenType
.SEP_Am
? TM
.AM
: TM
.PM
);
778 dtok
.dtt
= DTT
.NumAmpm
;
779 // Fix AM/PM parsing case, e.g. "1/10 5 AM"
782 if (!ProcessTerminalState(DS
.DX_NN
, ref str
, ref result
, ref styles
, ref raw
, dtfi
))
788 raw
.AddNumber(dtok
.num
);
792 result
.SetBadDateTimeFailure();
795 if (dps
== DS
.T_NNt
|| dps
== DS
.T_Nt
)
797 if (!HandleTimeZone(ref str
, ref result
))
799 LexTraceExit("0070 (HandleTimeZone returned false)", dps
);
804 case TokenType
.SEP_Space
:
805 dtok
.dtt
= DTT
.NumSpace
;
806 raw
.AddNumber(dtok
.num
);
808 case TokenType
.SEP_Date
:
809 dtok
.dtt
= DTT
.NumDatesep
;
810 raw
.AddNumber(dtok
.num
);
812 case TokenType
.SEP_DateOrOffset
:
813 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
814 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
815 if ((dateParsingStates
[(int)dps
][(int)DTT
.NumDatesep
] == DS
.ERROR
)
816 && (dateParsingStates
[(int)dps
][(int)DTT
.NumSpace
] > DS
.ERROR
))
818 str
.Index
= indexBeforeSeparator
;
819 str
.m_current
= charBeforeSeparator
;
820 dtok
.dtt
= DTT
.NumSpace
;
824 dtok
.dtt
= DTT
.NumDatesep
;
826 raw
.AddNumber(dtok
.num
);
828 case TokenType
.SEP_Time
:
829 if (raw
.hasSameDateAndTimeSeparators
&&
830 (dps
== DS
.D_Y
|| dps
== DS
.D_YN
|| dps
== DS
.D_YNd
|| dps
== DS
.D_YM
|| dps
== DS
.D_YMd
))
832 // we are parsing a date and we have the time separator same as date separator, so we mark the token as date separator
833 dtok
.dtt
= DTT
.NumDatesep
;
834 raw
.AddNumber(dtok
.num
);
837 dtok
.dtt
= DTT
.NumTimesep
;
838 raw
.AddNumber(dtok
.num
);
840 case TokenType
.SEP_YearSuff
:
843 dtok
.num
= dtfi
.Calendar
.ToFourDigitYear(tokenValue
);
845 catch (ArgumentOutOfRangeException
)
847 result
.SetBadDateTimeFailure();
848 LexTraceExit("0075 (Calendar.ToFourDigitYear failed)", dps
);
851 dtok
.dtt
= DTT
.NumDatesuff
;
854 case TokenType
.SEP_MonthSuff
:
855 case TokenType
.SEP_DaySuff
:
856 dtok
.dtt
= DTT
.NumDatesuff
;
859 case TokenType
.SEP_HourSuff
:
860 case TokenType
.SEP_MinuteSuff
:
861 case TokenType
.SEP_SecondSuff
:
862 dtok
.dtt
= DTT
.NumTimesuff
;
865 case TokenType
.SEP_LocalTimeMark
:
866 dtok
.dtt
= DTT
.NumLocalTimeMark
;
867 raw
.AddNumber(dtok
.num
);
870 // Invalid separator after number number.
871 result
.SetBadDateTimeFailure();
872 LexTraceExit("0080", dps
);
876 case TokenType
.HebrewNumber
:
877 if (tokenValue
>= 100)
879 // This is a year number
882 raw
.year
= tokenValue
;
884 // If we have number which has 3 or more digits (like "001" or "0001"),
885 // we assume this number is a year. Save the current raw.numCount in
888 switch (sep
= str
.GetSeparatorToken(dtfi
, out indexBeforeSeparator
, out charBeforeSeparator
))
890 case TokenType
.SEP_End
:
891 dtok
.dtt
= DTT
.YearEnd
;
893 case TokenType
.SEP_Space
:
894 dtok
.dtt
= DTT
.YearSpace
;
896 case TokenType
.SEP_DateOrOffset
:
897 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
898 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
899 if (dateParsingStates
[(int)dps
][(int)DTT
.YearSpace
] > DS
.ERROR
)
901 str
.Index
= indexBeforeSeparator
;
902 str
.m_current
= charBeforeSeparator
;
903 dtok
.dtt
= DTT
.YearSpace
;
908 // Invalid separator after number number.
909 result
.SetBadDateTimeFailure();
910 LexTraceExit("0090", dps
);
916 // Invalid separator after number number.
917 result
.SetBadDateTimeFailure();
918 LexTraceExit("0100", dps
);
924 // This is a day number
925 dtok
.num
= tokenValue
;
926 raw
.AddNumber(dtok
.num
);
928 switch (sep
= str
.GetSeparatorToken(dtfi
, out indexBeforeSeparator
, out charBeforeSeparator
))
931 // Note here we check if the numCount is less than three.
932 // When we have more than three numbers, it will be caught as error in the state machine.
934 case TokenType
.SEP_End
:
935 dtok
.dtt
= DTT
.NumEnd
;
937 case TokenType
.SEP_Space
:
938 case TokenType
.SEP_Date
:
939 dtok
.dtt
= DTT
.NumDatesep
;
941 case TokenType
.SEP_DateOrOffset
:
942 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
943 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
944 if ((dateParsingStates
[(int)dps
][(int)DTT
.NumDatesep
] == DS
.ERROR
)
945 && (dateParsingStates
[(int)dps
][(int)DTT
.NumSpace
] > DS
.ERROR
))
947 str
.Index
= indexBeforeSeparator
;
948 str
.m_current
= charBeforeSeparator
;
949 dtok
.dtt
= DTT
.NumSpace
;
953 dtok
.dtt
= DTT
.NumDatesep
;
957 // Invalid separator after number number.
958 result
.SetBadDateTimeFailure();
959 LexTraceExit("0110", dps
);
964 case TokenType
.DayOfWeekToken
:
965 if (raw
.dayOfWeek
== -1)
968 // This is a day of week name.
970 raw
.dayOfWeek
= tokenValue
;
971 dtok
.dtt
= DTT
.DayOfWeek
;
975 result
.SetBadDateTimeFailure();
976 LexTraceExit("0120 (DayOfWeek seen more than 1x)", dps
);
980 case TokenType
.MonthToken
:
984 // This is a month name
986 switch (sep
= str
.GetSeparatorToken(dtfi
, out indexBeforeSeparator
, out charBeforeSeparator
))
988 case TokenType
.SEP_End
:
989 dtok
.dtt
= DTT
.MonthEnd
;
991 case TokenType
.SEP_Space
:
992 dtok
.dtt
= DTT
.MonthSpace
;
994 case TokenType
.SEP_Date
:
995 dtok
.dtt
= DTT
.MonthDatesep
;
997 case TokenType
.SEP_Time
:
998 if (!raw
.hasSameDateAndTimeSeparators
)
1000 result
.SetBadDateTimeFailure();
1001 LexTraceExit("0130 (Invalid separator after month name)", dps
);
1005 // we have the date and time separators are same and getting a Month name, then change the token to MonthDatesep as
1006 // we are sure we are not parsing time.
1007 dtok
.dtt
= DTT
.MonthDatesep
;
1009 case TokenType
.SEP_DateOrOffset
:
1010 // The separator is either a date separator or the start of a time zone offset. If the token will complete the date then
1011 // process just the number and roll back the index so that the outer loop can attempt to parse the time zone offset.
1012 if ((dateParsingStates
[(int)dps
][(int)DTT
.MonthDatesep
] == DS
.ERROR
)
1013 && (dateParsingStates
[(int)dps
][(int)DTT
.MonthSpace
] > DS
.ERROR
))
1015 str
.Index
= indexBeforeSeparator
;
1016 str
.m_current
= charBeforeSeparator
;
1017 dtok
.dtt
= DTT
.MonthSpace
;
1021 dtok
.dtt
= DTT
.MonthDatesep
;
1025 // Invalid separator after month name
1026 result
.SetBadDateTimeFailure();
1027 LexTraceExit("0130 (Invalid separator after month name)", dps
);
1030 raw
.month
= tokenValue
;
1034 result
.SetBadDateTimeFailure();
1035 LexTraceExit("0140 (MonthToken seen more than 1x)", dps
);
1039 case TokenType
.EraToken
:
1040 if (result
.era
!= -1)
1042 result
.era
= tokenValue
;
1047 result
.SetBadDateTimeFailure();
1048 LexTraceExit("0150 (EraToken seen when result.era already set)", dps
);
1052 case TokenType
.JapaneseEraToken
:
1053 // Special case for Japanese. We allow Japanese era name to be used even if the calendar is not Japanese Calendar.
1054 result
.calendar
= JapaneseCalendar
.GetDefaultInstance();
1055 dtfi
= DateTimeFormatInfo
.GetJapaneseCalendarDTFI();
1056 if (result
.era
!= -1)
1058 result
.era
= tokenValue
;
1063 result
.SetBadDateTimeFailure();
1064 LexTraceExit("0160 (JapaneseEraToken seen when result.era already set)", dps
);
1068 case TokenType
.TEraToken
:
1069 result
.calendar
= TaiwanCalendar
.GetDefaultInstance();
1070 dtfi
= DateTimeFormatInfo
.GetTaiwanCalendarDTFI();
1071 if (result
.era
!= -1)
1073 result
.era
= tokenValue
;
1078 result
.SetBadDateTimeFailure();
1079 LexTraceExit("0170 (TEraToken seen when result.era already set)", dps
);
1083 case TokenType
.TimeZoneToken
:
1085 // This is a timezone designator
1087 // NOTENOTE : for now, we only support "GMT" and "Z" (for Zulu time).
1089 if ((result
.flags
& ParseFlags
.TimeZoneUsed
) != 0)
1091 // Should not have two timezone offsets.
1092 result
.SetBadDateTimeFailure();
1093 LexTraceExit("0180 (seen GMT or Z more than 1x)", dps
);
1096 dtok
.dtt
= DTT
.TimeZone
;
1097 result
.flags
|= ParseFlags
.TimeZoneUsed
;
1098 result
.timeZoneOffset
= new TimeSpan(0);
1099 result
.flags
|= ParseFlags
.TimeZoneUtc
;
1101 case TokenType
.EndOfString
:
1104 case TokenType
.DateWordToken
:
1105 case TokenType
.IgnorableSymbol
:
1106 // Date words and ignorable symbols can just be skipped over
1110 if (raw
.timeMark
== TM
.NotSet
)
1112 raw
.timeMark
= (TM
)tokenValue
;
1116 result
.SetBadDateTimeFailure();
1117 LexTraceExit("0190 (AM/PM timeMark already set)", dps
);
1121 case TokenType
.UnknownToken
:
1122 if (char.IsLetter(str
.m_current
))
1124 result
.SetFailure(ParseFailureKind
.FormatWithOriginalDateTimeAndParameter
, nameof(SR
.Format_UnknownDateTimeWord
), str
.Index
);
1125 LexTraceExit("0200", dps
);
1129 if ((str
.m_current
== '-' || str
.m_current
== '+') && ((result
.flags
& ParseFlags
.TimeZoneUsed
) == 0))
1131 int originalIndex
= str
.Index
;
1132 if (ParseTimeZone(ref str
, ref result
.timeZoneOffset
))
1134 result
.flags
|= ParseFlags
.TimeZoneUsed
;
1135 LexTraceExit("0220 (success)", dps
);
1140 // Time zone parse attempt failed. Fall through to punctuation handling.
1141 str
.Index
= originalIndex
;
1145 // Visual Basic implements string to date conversions on top of DateTime.Parse:
1146 // CDate("#10/10/95#")
1148 if (VerifyValidPunctuation(ref str
))
1150 LexTraceExit("0230 (success)", dps
);
1154 result
.SetBadDateTimeFailure();
1155 LexTraceExit("0240", dps
);
1159 LexTraceExit("0250 (success)", dps
);
1163 private static bool VerifyValidPunctuation(ref __DTString str
)
1165 // Compatability Behavior. Allow trailing nulls and surrounding hashes
1166 char ch
= str
.Value
[str
.Index
];
1169 bool foundStart
= false;
1170 bool foundEnd
= false;
1171 for (int i
= 0; i
< str
.Length
; i
++)
1180 // Having more than two hashes is invalid
1193 else if (ch
== '\0')
1195 // Allow nulls only at the end
1201 else if (!char.IsWhiteSpace(ch
))
1203 // Anything other than whitespace outside hashes is invalid
1204 if (!foundStart
|| foundEnd
)
1212 // The has was un-paired
1215 // Valid Hash usage: eat the hash and continue.
1219 else if (ch
== '\0')
1221 for (int i
= str
.Index
; i
< str
.Length
; i
++)
1223 if (str
.Value
[i
] != '\0')
1225 // Nulls are only valid if they are the only trailing character
1229 // Move to the end of the string
1230 str
.Index
= str
.Length
;
1236 private const int ORDER_YMD
= 0; // The order of date is Year/Month/Day.
1237 private const int ORDER_MDY
= 1; // The order of date is Month/Day/Year.
1238 private const int ORDER_DMY
= 2; // The order of date is Day/Month/Year.
1239 private const int ORDER_YDM
= 3; // The order of date is Year/Day/Month
1240 private const int ORDER_YM
= 4; // Year/Month order.
1241 private const int ORDER_MY
= 5; // Month/Year order.
1242 private const int ORDER_MD
= 6; // Month/Day order.
1243 private const int ORDER_DM
= 7; // Day/Month order.
1246 // Decide the year/month/day order from the datePattern.
1248 // Return 0 for YMD, 1 for MDY, 2 for DMY, otherwise -1.
1250 private static bool GetYearMonthDayOrder(string datePattern
, out int order
)
1253 int monthOrder
= -1;
1257 bool inQuote
= false;
1259 for (int i
= 0; i
< datePattern
.Length
&& orderCount
< 3; i
++)
1261 char ch
= datePattern
[i
];
1262 if (ch
== '\\' || ch
== '%')
1265 continue; // Skip next character that is escaped by this backslash
1268 if (ch
== '\'' || ch
== '"')
1277 yearOrder
= orderCount
++;
1280 // Skip all year pattern charaters.
1282 for (; i
+ 1 < datePattern
.Length
&& datePattern
[i
+ 1] == 'y'; i
++)
1289 monthOrder
= orderCount
++;
1291 // Skip all month pattern characters.
1293 for (; i
+ 1 < datePattern
.Length
&& datePattern
[i
+ 1] == 'M'; i
++)
1300 int patternCount
= 1;
1302 // Skip all day pattern characters.
1304 for (; i
+ 1 < datePattern
.Length
&& datePattern
[i
+ 1] == 'd'; i
++)
1309 // Make sure this is not "ddd" or "dddd", which means day of week.
1311 if (patternCount
<= 2)
1313 dayOrder
= orderCount
++;
1319 if (yearOrder
== 0 && monthOrder
== 1 && dayOrder
== 2)
1324 if (monthOrder
== 0 && dayOrder
== 1 && yearOrder
== 2)
1329 if (dayOrder
== 0 && monthOrder
== 1 && yearOrder
== 2)
1334 if (yearOrder
== 0 && dayOrder
== 1 && monthOrder
== 2)
1344 // Decide the year/month order from the pattern.
1346 // Return 0 for YM, 1 for MY, otherwise -1.
1348 private static bool GetYearMonthOrder(string pattern
, out int order
)
1351 int monthOrder
= -1;
1354 bool inQuote
= false;
1355 for (int i
= 0; i
< pattern
.Length
&& orderCount
< 2; i
++)
1357 char ch
= pattern
[i
];
1358 if (ch
== '\\' || ch
== '%')
1361 continue; // Skip next character that is escaped by this backslash
1364 if (ch
== '\'' || ch
== '"')
1373 yearOrder
= orderCount
++;
1376 // Skip all year pattern charaters.
1378 for (; i
+ 1 < pattern
.Length
&& pattern
[i
+ 1] == 'y'; i
++)
1384 monthOrder
= orderCount
++;
1386 // Skip all month pattern characters.
1388 for (; i
+ 1 < pattern
.Length
&& pattern
[i
+ 1] == 'M'; i
++)
1395 if (yearOrder
== 0 && monthOrder
== 1)
1400 if (monthOrder
== 0 && yearOrder
== 1)
1410 // Decide the month/day order from the pattern.
1412 // Return 0 for MD, 1 for DM, otherwise -1.
1414 private static bool GetMonthDayOrder(string pattern
, out int order
)
1416 int monthOrder
= -1;
1420 bool inQuote
= false;
1421 for (int i
= 0; i
< pattern
.Length
&& orderCount
< 2; i
++)
1423 char ch
= pattern
[i
];
1424 if (ch
== '\\' || ch
== '%')
1427 continue; // Skip next character that is escaped by this backslash
1430 if (ch
== '\'' || ch
== '"')
1439 int patternCount
= 1;
1441 // Skip all day pattern charaters.
1443 for (; i
+ 1 < pattern
.Length
&& pattern
[i
+ 1] == 'd'; i
++)
1449 // Make sure this is not "ddd" or "dddd", which means day of week.
1451 if (patternCount
<= 2)
1453 dayOrder
= orderCount
++;
1458 monthOrder
= orderCount
++;
1460 // Skip all month pattern characters.
1462 for (; i
+ 1 < pattern
.Length
&& pattern
[i
+ 1] == 'M'; i
++)
1469 if (monthOrder
== 0 && dayOrder
== 1)
1474 if (dayOrder
== 0 && monthOrder
== 1)
1484 // Adjust the two-digit year if necessary.
1486 private static bool TryAdjustYear(ref DateTimeResult result
, int year
, out int adjustedYear
)
1492 // the Calendar classes need some real work. Many of the calendars that throw
1493 // don't implement a fast/non-allocating (and non-throwing) IsValid{Year|Day|Month} method.
1494 // we are making a targeted try/catch fix in the in-place release but will revisit this code
1495 // in the next side-by-side release.
1496 year
= result
.calendar
.ToFourDigitYear(year
);
1498 catch (ArgumentOutOfRangeException
)
1504 adjustedYear
= year
;
1508 private static bool SetDateYMD(ref DateTimeResult result
, int year
, int month
, int day
)
1510 // Note, longer term these checks should be done at the end of the parse. This current
1511 // way of checking creates order dependence with parsing the era name.
1512 if (result
.calendar
.IsValidDay(year
, month
, day
, result
.era
))
1514 result
.SetDate(year
, month
, day
); // YMD
1520 private static bool SetDateMDY(ref DateTimeResult result
, int month
, int day
, int year
)
1522 return SetDateYMD(ref result
, year
, month
, day
);
1525 private static bool SetDateDMY(ref DateTimeResult result
, int day
, int month
, int year
)
1527 return SetDateYMD(ref result
, year
, month
, day
);
1530 private static bool SetDateYDM(ref DateTimeResult result
, int year
, int day
, int month
)
1532 return SetDateYMD(ref result
, year
, month
, day
);
1535 private static void GetDefaultYear(ref DateTimeResult result
, ref DateTimeStyles styles
)
1537 result
.Year
= result
.calendar
.GetYear(GetDateTimeNow(ref result
, ref styles
));
1538 result
.flags
|= ParseFlags
.YearDefault
;
1541 // Processing teriminal case: DS.DX_NN
1542 private static 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();
1551 int n1
= raw
.GetNumber(0);
1552 int n2
= raw
.GetNumber(1);
1554 GetDefaultYear(ref result
, ref styles
);
1557 if (!GetMonthDayOrder(dtfi
.MonthDayPattern
, out order
))
1559 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDatePattern
), dtfi
.MonthDayPattern
);
1563 if (order
== ORDER_MD
)
1565 if (SetDateYMD(ref result
, result
.Year
, n1
, n2
)) // MD
1567 result
.flags
|= ParseFlags
.HaveDate
;
1574 if (SetDateYMD(ref result
, result
.Year
, n2
, n1
)) // DM
1576 result
.flags
|= ParseFlags
.HaveDate
;
1580 result
.SetBadDateTimeFailure();
1584 // Processing teriminal case: DS.DX_NNN
1585 private static 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();
1594 int n1
= raw
.GetNumber(0);
1595 int n2
= raw
.GetNumber(1);
1596 int n3
= raw
.GetNumber(2);
1599 if (!GetYearMonthDayOrder(dtfi
.ShortDatePattern
, out order
))
1601 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDatePattern
), dtfi
.ShortDatePattern
);
1606 if (order
== ORDER_YMD
)
1608 if (TryAdjustYear(ref result
, n1
, out year
) && SetDateYMD(ref result
, year
, n2
, n3
)) // YMD
1610 result
.flags
|= ParseFlags
.HaveDate
;
1614 else if (order
== ORDER_MDY
)
1616 if (TryAdjustYear(ref result
, n3
, out year
) && SetDateMDY(ref result
, n1
, n2
, year
)) // MDY
1618 result
.flags
|= ParseFlags
.HaveDate
;
1622 else if (order
== ORDER_DMY
)
1624 if (TryAdjustYear(ref result
, n3
, out year
) && SetDateDMY(ref result
, n1
, n2
, year
)) // DMY
1626 result
.flags
|= ParseFlags
.HaveDate
;
1630 else if (order
== ORDER_YDM
)
1632 if (TryAdjustYear(ref result
, n1
, out year
) && SetDateYDM(ref result
, year
, n2
, n3
)) // YDM
1634 result
.flags
|= ParseFlags
.HaveDate
;
1638 result
.SetBadDateTimeFailure();
1642 private static 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();
1651 // The interpretation is based on the MonthDayPattern and YearMonthPattern
1653 // MonthDayPattern YearMonthPattern Interpretation
1654 // --------------- ---------------- ---------------
1655 // MMMM dd MMMM yyyy Day
1656 // MMMM dd yyyy MMMM Day
1657 // dd MMMM MMMM yyyy Year
1658 // dd MMMM yyyy MMMM Day
1660 // In the first and last cases, it could be either or neither, but a day is a better default interpretation
1661 // than a 2 digit year.
1664 if (!GetMonthDayOrder(dtfi
.MonthDayPattern
, out monthDayOrder
))
1666 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDatePattern
), dtfi
.MonthDayPattern
);
1669 if (monthDayOrder
== ORDER_DM
)
1672 if (!GetYearMonthOrder(dtfi
.YearMonthPattern
, out yearMonthOrder
))
1674 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDatePattern
), dtfi
.YearMonthPattern
);
1677 if (yearMonthOrder
== ORDER_MY
)
1680 if (!TryAdjustYear(ref result
, raw
.GetNumber(0), out year
) || !SetDateYMD(ref result
, year
, raw
.month
, 1))
1682 result
.SetBadDateTimeFailure();
1689 GetDefaultYear(ref result
, ref styles
);
1690 if (!SetDateYMD(ref result
, result
.Year
, raw
.month
, raw
.GetNumber(0)))
1692 result
.SetBadDateTimeFailure();
1698 ////////////////////////////////////////////////////////////////////////
1700 // Deal with the terminal state for Hebrew Month/Day pattern
1702 ////////////////////////////////////////////////////////////////////////
1704 private static bool GetHebrewDayOfNM(ref DateTimeResult result
, ref DateTimeRawInfo raw
, DateTimeFormatInfo dtfi
)
1707 if (!GetMonthDayOrder(dtfi
.MonthDayPattern
, out monthDayOrder
))
1709 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDatePattern
), dtfi
.MonthDayPattern
);
1712 result
.Month
= raw
.month
;
1713 if (monthDayOrder
== ORDER_DM
|| monthDayOrder
== ORDER_MD
)
1715 if (result
.calendar
.IsValidDay(result
.Year
, result
.Month
, raw
.GetNumber(0), result
.era
))
1717 result
.Day
= raw
.GetNumber(0);
1721 result
.SetBadDateTimeFailure();
1725 private static 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();
1734 // The interpretation is based on the MonthDayPattern and YearMonthPattern
1736 // MonthDayPattern YearMonthPattern Interpretation
1737 // --------------- ---------------- ---------------
1738 // MMMM dd MMMM yyyy Day
1739 // MMMM dd yyyy MMMM Year
1740 // dd MMMM MMMM yyyy Day
1741 // dd MMMM yyyy MMMM Day
1743 // In the first and last cases, it could be either or neither, but a day is a better default interpretation
1744 // than a 2 digit year.
1747 if (!GetMonthDayOrder(dtfi
.MonthDayPattern
, out monthDayOrder
))
1749 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDatePattern
), dtfi
.MonthDayPattern
);
1752 if (monthDayOrder
== ORDER_MD
)
1755 if (!GetYearMonthOrder(dtfi
.YearMonthPattern
, out yearMonthOrder
))
1757 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDatePattern
), dtfi
.YearMonthPattern
);
1760 if (yearMonthOrder
== ORDER_YM
)
1763 if (!TryAdjustYear(ref result
, raw
.GetNumber(0), out year
) || !SetDateYMD(ref result
, year
, raw
.month
, 1))
1765 result
.SetBadDateTimeFailure();
1772 GetDefaultYear(ref result
, ref styles
);
1773 if (!SetDateYMD(ref result
, result
.Year
, raw
.month
, raw
.GetNumber(0)))
1775 result
.SetBadDateTimeFailure();
1781 private static 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();
1790 int n1
= raw
.GetNumber(0);
1791 int n2
= raw
.GetNumber(1);
1794 if (!GetYearMonthDayOrder(dtfi
.ShortDatePattern
, out order
))
1796 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDatePattern
), dtfi
.ShortDatePattern
);
1801 if (order
== ORDER_MDY
)
1803 if (TryAdjustYear(ref result
, n2
, out year
) && result
.calendar
.IsValidDay(year
, raw
.month
, n1
, result
.era
))
1805 result
.SetDate(year
, raw
.month
, n1
); // MDY
1806 result
.flags
|= ParseFlags
.HaveDate
;
1809 else if (TryAdjustYear(ref result
, n1
, out year
) && result
.calendar
.IsValidDay(year
, raw
.month
, n2
, result
.era
))
1811 result
.SetDate(year
, raw
.month
, n2
); // YMD
1812 result
.flags
|= ParseFlags
.HaveDate
;
1816 else if (order
== ORDER_YMD
)
1818 if (TryAdjustYear(ref result
, n1
, out year
) && result
.calendar
.IsValidDay(year
, raw
.month
, n2
, result
.era
))
1820 result
.SetDate(year
, raw
.month
, n2
); // YMD
1821 result
.flags
|= ParseFlags
.HaveDate
;
1824 else if (TryAdjustYear(ref result
, n2
, out year
) && result
.calendar
.IsValidDay(year
, raw
.month
, n1
, result
.era
))
1826 result
.SetDate(year
, raw
.month
, n1
); // DMY
1827 result
.flags
|= ParseFlags
.HaveDate
;
1831 else if (order
== ORDER_DMY
)
1833 if (TryAdjustYear(ref result
, n2
, out year
) && result
.calendar
.IsValidDay(year
, raw
.month
, n1
, result
.era
))
1835 result
.SetDate(year
, raw
.month
, n1
); // DMY
1836 result
.flags
|= ParseFlags
.HaveDate
;
1839 else if (TryAdjustYear(ref result
, n1
, out year
) && result
.calendar
.IsValidDay(year
, raw
.month
, n2
, result
.era
))
1841 result
.SetDate(year
, raw
.month
, n2
); // YMD
1842 result
.flags
|= ParseFlags
.HaveDate
;
1847 result
.SetBadDateTimeFailure();
1851 private static 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();
1860 int n1
= raw
.GetNumber(0);
1861 int n2
= raw
.GetNumber(1);
1862 string pattern
= dtfi
.ShortDatePattern
;
1864 // For compatibility, don't throw if we can't determine the order, but default to YMD instead
1866 if (GetYearMonthDayOrder(pattern
, out order
) && order
== ORDER_YDM
)
1868 if (SetDateYMD(ref result
, raw
.year
, n2
, n1
))
1870 result
.flags
|= ParseFlags
.HaveDate
;
1871 return true; // Year + DM
1876 if (SetDateYMD(ref result
, raw
.year
, n1
, n2
))
1878 result
.flags
|= ParseFlags
.HaveDate
;
1879 return true; // Year + MD
1882 result
.SetBadDateTimeFailure();
1886 private static 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();
1895 int n1
= raw
.GetNumber(0);
1896 int n2
= raw
.GetNumber(1);
1899 if (!GetYearMonthDayOrder(dtfi
.ShortDatePattern
, out order
))
1901 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDatePattern
), dtfi
.ShortDatePattern
);
1905 if (order
== ORDER_MDY
|| order
== ORDER_YMD
)
1907 if (SetDateYMD(ref result
, raw
.year
, n1
, n2
))
1909 result
.flags
|= ParseFlags
.HaveDate
;
1910 return true; // MD + Year
1915 if (SetDateYMD(ref result
, raw
.year
, n2
, n1
))
1917 result
.flags
|= ParseFlags
.HaveDate
;
1918 return true; // DM + Year
1921 result
.SetBadDateTimeFailure();
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();
1934 if (SetDateYMD(ref result
, raw
.year
, raw
.month
, raw
.GetNumber(0)))
1936 result
.flags
|= ParseFlags
.HaveDate
;
1939 result
.SetBadDateTimeFailure();
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();
1952 if (SetDateYMD(ref result
, raw
.year
, raw
.GetNumber(0), 1))
1954 result
.flags
|= ParseFlags
.HaveDate
;
1957 result
.SetBadDateTimeFailure();
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();
1970 if (SetDateYMD(ref result
, raw
.year
, raw
.month
, 1))
1972 result
.flags
|= ParseFlags
.HaveDate
;
1975 result
.SetBadDateTimeFailure();
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)
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)
2016 hour
= (hour
== 12) ? 0 : hour
;
2020 if (hour
< 0 || hour
> 23)
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();
2042 // In this case, we need a time mark. Check if so.
2044 if (raw
.timeMark
== TM
.NotSet
)
2046 result
.SetBadDateTimeFailure();
2049 result
.Hour
= raw
.GetNumber(0);
2050 result
.flags
|= ParseFlags
.HaveTime
;
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();
2064 result
.Hour
= raw
.GetNumber(0);
2065 result
.Minute
= raw
.GetNumber(1);
2066 result
.flags
|= ParseFlags
.HaveTime
;
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();
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
;
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();
2096 result
.Day
= raw
.GetNumber(0);
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();
2108 if (result
.Year
!= -1)
2110 // Already has a year suffix
2111 result
.SetBadDateTimeFailure();
2114 if (!TryAdjustYear(ref result
, raw
.GetNumber(0), out result
.Year
))
2116 // the year value is out of range
2117 result
.SetBadDateTimeFailure();
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)))
2140 else if ((result
.flags
& ParseFlags
.HaveMonth
) != 0)
2142 if (((result
.flags
& ParseFlags
.HaveYear
) == 0) && ((result
.flags
& ParseFlags
.HaveDay
) == 0))
2145 if (!GetYearMonthDayOrder(dtfi
.ShortDatePattern
, out order
))
2147 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDatePattern
), dtfi
.ShortDatePattern
);
2151 if (order
== ORDER_YMD
)
2153 if (TryAdjustYear(ref result
, raw
.GetNumber(0), out year
) && SetDateYMD(ref result
, year
, result
.Month
, raw
.GetNumber(1)))
2160 if (TryAdjustYear(ref result
, raw
.GetNumber(1), out year
) && SetDateYMD(ref result
, year
, result
.Month
, raw
.GetNumber(0)))
2167 result
.SetBadDateTimeFailure();
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)
2183 result
.flags
|= ParseFlags
.HaveYear
;
2184 result
.Year
= raw
.year
= dtok
.num
;
2186 case TokenType
.SEP_MonthSuff
:
2187 if ((result
.flags
& ParseFlags
.HaveMonth
) != 0)
2191 result
.flags
|= ParseFlags
.HaveMonth
;
2192 result
.Month
= raw
.month
= dtok
.num
;
2194 case TokenType
.SEP_DaySuff
:
2195 if ((result
.flags
& ParseFlags
.HaveDay
) != 0)
2199 result
.flags
|= ParseFlags
.HaveDay
;
2200 result
.Day
= dtok
.num
;
2202 case TokenType
.SEP_HourSuff
:
2203 if ((result
.flags
& ParseFlags
.HaveHour
) != 0)
2207 result
.flags
|= ParseFlags
.HaveHour
;
2208 result
.Hour
= dtok
.num
;
2210 case TokenType
.SEP_MinuteSuff
:
2211 if ((result
.flags
& ParseFlags
.HaveMinute
) != 0)
2215 result
.flags
|= ParseFlags
.HaveMinute
;
2216 result
.Minute
= dtok
.num
;
2218 case TokenType
.SEP_SecondSuff
:
2219 if ((result
.flags
& ParseFlags
.HaveSecond
) != 0)
2223 result
.flags
|= ParseFlags
.HaveSecond
;
2224 result
.Second
= dtok
.num
;
2230 ////////////////////////////////////////////////////////////////////////
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.
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
));
2251 if (!GetDayOfMNN(ref result
, ref raw
, dtfi
))
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
));
2263 if (!GetDayOfYMN(ref result
, ref raw
))
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)
2275 if (!GetDayOfNNY(ref result
, ref raw
, dtfi
))
2279 if (!dtfi
.YearMonthAdjustment(ref result
.Year
, ref raw
.month
, true))
2281 result
.SetFailure(ParseFailureKind
.FormatBadDateTimeCalendar
, nameof(SR
.Format_BadDateTimeCalendar
));
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
));
2294 if (!GetHebrewDayOfNM(ref result
, ref raw
, dtfi
))
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
));
2306 if (!GetDayOfYM(ref result
, ref raw
))
2312 // Deal hour + AM/PM
2313 if (!GetTimeOfN(ref result
, ref raw
))
2319 if (!GetTimeOfNN(ref result
, ref raw
))
2325 if (!GetTimeOfNNN(ref result
, ref raw
))
2331 result
.SetBadDateTimeFailure();
2337 // We have reached a terminal state. Reset the raw num count.
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
)
2354 passed
= GetDayOfNN(ref result
, ref styles
, ref raw
, dtfi
);
2357 passed
= GetDayOfNNN(ref result
, ref raw
, dtfi
);
2360 passed
= GetDayOfMN(ref result
, ref styles
, ref raw
, dtfi
);
2363 passed
= GetDayOfNM(ref result
, ref styles
, ref raw
, dtfi
);
2366 passed
= GetDayOfMNN(ref result
, ref raw
, dtfi
);
2369 // The result has got the correct value. No need to process.
2373 passed
= GetDayOfYNN(ref result
, ref raw
, dtfi
);
2376 passed
= GetDayOfNNY(ref result
, ref raw
, dtfi
);
2379 passed
= GetDayOfYMN(ref result
, ref raw
);
2382 passed
= GetDayOfYN(ref result
, ref raw
);
2385 passed
= GetDayOfYM(ref result
, ref raw
);
2388 passed
= GetTimeOfN(ref result
, ref raw
);
2391 passed
= GetTimeOfNN(ref result
, ref raw
);
2394 passed
= GetTimeOfNNN(ref result
, ref raw
);
2397 // The result has got the correct value. Nothing to do.
2401 passed
= GetDateOfDSN(ref result
, ref raw
);
2404 passed
= GetDateOfNDS(ref result
, ref raw
);
2407 passed
= GetDateOfNNDS(ref result
, ref raw
, dtfi
);
2411 PTSTraceExit(dps
, passed
);
2420 // We have reached a terminal state. Reset the raw num count.
2427 internal static DateTime
Parse(ReadOnlySpan
<char> s
, DateTimeFormatInfo dtfi
, DateTimeStyles styles
)
2429 DateTimeResult result
= new DateTimeResult(); // The buffer to store the parsing result.
2431 if (TryParse(s
, dtfi
, styles
, ref result
))
2433 return result
.parsedDate
;
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.
2445 result
.flags
|= ParseFlags
.CaptureOffset
;
2446 if (TryParse(s
, dtfi
, styles
, ref result
))
2448 offset
= result
.timeZoneOffset
;
2449 return result
.parsedDate
;
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.
2462 if (TryParse(s
, dtfi
, styles
, ref resultData
))
2464 result
= resultData
.parsedDate
;
2468 result
= DateTime
.MinValue
;
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
;
2485 result
= DateTime
.MinValue
;
2486 offset
= TimeSpan
.Zero
;
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
)
2497 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_BadDateTime
));
2501 Debug
.Assert(dtfi
!= null, "dtfi == null");
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.
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
);
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
);
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
);
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
);
2585 result
.SetBadDateTimeFailure();
2586 TPTraceExit("0030", dps
);
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
2601 if (dps
== DS
.T_NNt
)
2607 bool atEnd
= str
.AtEnd();
2608 if (dateParsingStates
[(int)dps
][(int)dtok
.dtt
] == DS
.ERROR
|| atEnd
)
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
);
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
);
2646 if (!ProcessTerminalState(dps
, ref str
, ref result
, ref styles
, ref raw
, dtfi
))
2648 TPTraceExit("0060 (ProcessTerminalState)", dps
);
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".
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
);
2671 AdjustTimeMark(dtfi
, ref raw
);
2672 if (!AdjustHour(ref result
.Hour
, raw
.timeMark
))
2674 result
.SetBadDateTimeFailure();
2675 TPTraceExit("0080 (AdjustHour)", dps
);
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
);
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
);
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
);
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
);
2727 result
.parsedDate
= time
;
2729 if (!DetermineTimeZoneAdjustments(ref result
, styles
, bTimeOnly
))
2731 TPTraceExit("0120 (DetermineTimeZoneAdjustments)", dps
);
2734 TPTraceExit("0130 (success)", dps
);
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
);
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
));
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
);
2778 result
.parsedDate
= DateTime
.SpecifyKind(result
.parsedDate
, DateTimeKind
.Local
);
2782 else if ((styles
& DateTimeStyles
.AssumeUniversal
) != 0)
2784 if ((styles
& DateTimeStyles
.AdjustToUniversal
) != 0)
2786 result
.parsedDate
= DateTime
.SpecifyKind(result
.parsedDate
, DateTimeKind
.Utc
);
2791 result
.flags
|= ParseFlags
.TimeZoneUsed
;
2792 result
.timeZoneOffset
= TimeSpan
.Zero
;
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");
2803 if (((styles
& DateTimeStyles
.RoundtripKind
) != 0) && ((result
.flags
& ParseFlags
.TimeZoneUtc
) != 0))
2805 result
.parsedDate
= DateTime
.SpecifyKind(result
.parsedDate
, DateTimeKind
.Utc
);
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
;
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
));
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
));
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
;
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
;
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
));
2897 result
.parsedDate
= new DateTime(resultTicks
, DateTimeKind
.Utc
);
2902 // Adjust the specified time to universal time based on the supplied timezone,
2903 // and then convert to local time.
2904 // E.g. when parsing "2001/06/08 14:00-04:00", and local timezone is GMT-7.
2905 // the time is 2001/06/08 14:00, and timeZoneOffset = -05:00.
2906 // The result will be "2001/06/08 11:00"
2908 private static 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.
2921 resultTicks
-= result
.timeZoneOffset
.Ticks
;
2922 // If the time is time of day, use the current timezone offset.
2923 resultTicks
+= tz
.GetUtcOffset(bTimeOnly
? DateTime
.Now
: result
.parsedDate
, TimeZoneInfoOptions
.NoThrowOnInvalidTime
).Ticks
;
2925 if (resultTicks
< 0)
2927 resultTicks
+= Calendar
.TicksPerDay
;
2932 // Adjust timezone to GMT.
2933 resultTicks
-= result
.timeZoneOffset
.Ticks
;
2934 if (resultTicks
< DateTime
.MinTicks
|| resultTicks
> DateTime
.MaxTicks
)
2936 // If the result ticks is greater than DateTime.MaxValue, we can not create a DateTime from this ticks.
2937 // In this case, keep using the old code.
2938 resultTicks
+= tz
.GetUtcOffset(result
.parsedDate
, TimeZoneInfoOptions
.NoThrowOnInvalidTime
).Ticks
;
2942 // Convert the GMT time to local time.
2943 DateTime utcDt
= new DateTime(resultTicks
, DateTimeKind
.Utc
);
2944 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
));
2954 result
.parsedDate
= new DateTime(resultTicks
, DateTimeKind
.Local
, isAmbiguousLocalDst
);
2959 // Parse the ISO8601 format string found during Parse();
2962 private static bool ParseISO8601(ref DateTimeRawInfo raw
, ref __DTString str
, DateTimeStyles styles
, ref DateTimeResult result
)
2964 if (raw
.year
< 0 || raw
.GetNumber(0) < 0 || raw
.GetNumber(1) < 0)
2970 double partSecond
= 0;
2972 str
.SkipWhiteSpaces();
2973 if (!ParseDigits(ref str
, 2, out hour
))
2975 result
.SetBadDateTimeFailure();
2978 str
.SkipWhiteSpaces();
2979 if (!str
.Match(':'))
2981 result
.SetBadDateTimeFailure();
2984 str
.SkipWhiteSpaces();
2985 if (!ParseDigits(ref str
, 2, out minute
))
2987 result
.SetBadDateTimeFailure();
2990 str
.SkipWhiteSpaces();
2993 str
.SkipWhiteSpaces();
2994 if (!ParseDigits(ref str
, 2, out second
))
2996 result
.SetBadDateTimeFailure();
3001 if (!ParseFraction(ref str
, out partSecond
))
3003 result
.SetBadDateTimeFailure();
3008 str
.SkipWhiteSpaces();
3012 char ch
= str
.GetChar();
3013 if (ch
== '+' || ch
== '-')
3015 result
.flags
|= ParseFlags
.TimeZoneUsed
;
3016 if (!ParseTimeZone(ref str
, ref result
.timeZoneOffset
))
3018 result
.SetBadDateTimeFailure();
3022 else if (ch
== 'Z' || ch
== 'z')
3024 result
.flags
|= ParseFlags
.TimeZoneUsed
;
3025 result
.timeZoneOffset
= TimeSpan
.Zero
;
3026 result
.flags
|= ParseFlags
.TimeZoneUtc
;
3032 str
.SkipWhiteSpaces();
3035 if (!VerifyValidPunctuation(ref str
))
3037 result
.SetBadDateTimeFailure();
3040 str
.SkipWhiteSpaces();
3042 if (str
.Match('\0'))
3044 if (!VerifyValidPunctuation(ref str
))
3046 result
.SetBadDateTimeFailure();
3052 // If this is true, there were non-white space characters remaining in the DateTime
3053 result
.SetBadDateTimeFailure();
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
));
3067 if (!time
.TryAddTicks((long)Math
.Round(partSecond
* Calendar
.TicksPerSecond
), out time
))
3069 result
.SetBadDateTimeFailure();
3073 result
.parsedDate
= time
;
3074 return DetermineTimeZoneAdjustments(ref result
, styles
, false);
3077 ////////////////////////////////////////////////////////////////////////
3080 // Parse the current word as a Hebrew number.
3081 // This is used by DateTime.ParseExact().
3083 ////////////////////////////////////////////////////////////////////////
3085 internal static bool MatchHebrewDigits(ref __DTString str
, int digitLen
, out int number
)
3089 // Create a context object so that we can parse the Hebrew number text character by character.
3090 HebrewNumberParsingContext context
= new HebrewNumberParsingContext(0);
3092 // Set this to ContinueParsing so that we will run the following while loop in the first time.
3093 HebrewNumberParsingState state
= HebrewNumberParsingState
.ContinueParsing
;
3095 while (state
== HebrewNumberParsingState
.ContinueParsing
&& str
.GetNext())
3097 state
= HebrewNumber
.ParseByChar(str
.GetChar(), ref context
);
3100 if (state
== HebrewNumberParsingState
.FoundEndOfHebrewNumber
)
3102 // If we have reached a terminal state, update the result and returns.
3103 number
= context
.result
;
3107 // If we run out of the character before reaching FoundEndOfHebrewNumber, or
3108 // the state is InvalidHebrewNumber or ContinueParsing, we fail to match a Hebrew number.
3113 /*=================================ParseDigits==================================
3114 **Action: Parse the number string in __DTString that are formatted using
3115 ** the following patterns:
3116 ** "0", "00", and "000..0"
3117 **Returns: the integer value
3118 **Arguments: str: a __DTString. The parsing will start from the
3119 ** next character after str.Index.
3120 **Exceptions: FormatException if error in parsing number.
3121 ==============================================================================*/
3123 internal static bool ParseDigits(ref __DTString str
, int digitLen
, out int result
)
3127 // 1 really means 1 or 2 for this call
3128 return ParseDigits(ref str
, 1, 2, out result
);
3132 return ParseDigits(ref str
, digitLen
, digitLen
, out result
);
3136 internal static bool ParseDigits(ref __DTString str
, int minDigitLen
, int maxDigitLen
, out int result
)
3138 Debug
.Assert(minDigitLen
> 0, "minDigitLen > 0");
3139 Debug
.Assert(maxDigitLen
< 9, "maxDigitLen < 9");
3140 Debug
.Assert(minDigitLen
<= maxDigitLen
, "minDigitLen <= maxDigitLen");
3141 int localResult
= 0;
3142 int startingIndex
= str
.Index
;
3143 int tokenLength
= 0;
3144 while (tokenLength
< maxDigitLen
)
3146 if (!str
.GetNextDigit())
3151 localResult
= localResult
* 10 + str
.GetDigit();
3154 result
= localResult
;
3155 if (tokenLength
< minDigitLen
)
3157 str
.Index
= startingIndex
;
3163 /*=================================ParseFractionExact==================================
3164 **Action: Parse the number string in __DTString that are formatted using
3165 ** the following patterns:
3166 ** "0", "00", and "000..0"
3167 **Returns: the fraction value
3168 **Arguments: str: a __DTString. The parsing will start from the
3169 ** next character after str.Index.
3170 **Exceptions: FormatException if error in parsing number.
3171 ==============================================================================*/
3173 private static bool ParseFractionExact(ref __DTString str
, int maxDigitLen
, ref double result
)
3175 if (!str
.GetNextDigit())
3180 result
= str
.GetDigit();
3183 for (; digitLen
< maxDigitLen
; digitLen
++)
3185 if (!str
.GetNextDigit())
3190 result
= result
* 10 + str
.GetDigit();
3193 result
/= TimeSpanParse
.Pow10(digitLen
);
3194 return digitLen
== maxDigitLen
;
3197 /*=================================ParseSign==================================
3198 **Action: Parse a positive or a negative sign.
3199 **Returns: true if postive sign. flase if negative sign.
3200 **Arguments: str: a __DTString. The parsing will start from the
3201 ** next character after str.Index.
3202 **Exceptions: FormatException if end of string is encountered or a sign
3203 ** symbol is not found.
3204 ==============================================================================*/
3206 private static bool ParseSign(ref __DTString str
, ref bool result
)
3210 // A sign symbol ('+' or '-') is expected. However, end of string is encountered.
3213 char ch
= str
.GetChar();
3224 // A sign symbol ('+' or '-') is expected.
3228 /*=================================ParseTimeZoneOffset==================================
3229 **Action: Parse the string formatted using "z", "zz", "zzz" in DateTime.Format().
3230 **Returns: the TimeSpan for the parsed timezone offset.
3231 **Arguments: str: a __DTString. The parsing will start from the
3232 ** next character after str.Index.
3233 ** len: the repeated number of the "z"
3234 **Exceptions: FormatException if errors in parsing.
3235 ==============================================================================*/
3237 private static bool ParseTimeZoneOffset(ref __DTString str
, int len
, ref TimeSpan result
)
3239 bool isPositive
= true;
3241 int minuteOffset
= 0;
3247 if (!ParseSign(ref str
, ref isPositive
))
3251 if (!ParseDigits(ref str
, len
, out hourOffset
))
3257 if (!ParseSign(ref str
, ref isPositive
))
3262 // Parsing 1 digit will actually parse 1 or 2.
3263 if (!ParseDigits(ref str
, 1, out hourOffset
))
3271 if (!ParseDigits(ref str
, 2, out minuteOffset
))
3278 // Since we can not match ':', put the char back.
3280 if (!ParseDigits(ref str
, 2, out minuteOffset
))
3287 if (minuteOffset
< 0 || minuteOffset
>= 60)
3292 result
= (new TimeSpan(hourOffset
, minuteOffset
, 0));
3295 result
= result
.Negate();
3300 /*=================================MatchAbbreviatedMonthName==================================
3301 **Action: Parse the abbreviated month name from string starting at str.Index.
3302 **Returns: A value from 1 to 12 for the first month to the 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;
3315 // Scan the month names (note that some calendars has 13 months) and find
3316 // the matching month name which has the max string length.
3317 // We need to do this because some cultures (e.g. "cs-CZ") which have
3318 // abbreviated month names with the same prefix.
3320 int monthsInYear
= (dtfi
.GetMonthName(13).Length
== 0 ? 12 : 13);
3321 for (int i
= 1; i
<= monthsInYear
; i
++)
3323 string searchStr
= dtfi
.GetAbbreviatedMonthName(i
);
3324 int matchStrLen
= searchStr
.Length
;
3325 if (dtfi
.HasSpacesInMonthNames
3326 ? str
.MatchSpecifiedWords(searchStr
, false, ref matchStrLen
)
3327 : str
.MatchSpecifiedWord(searchStr
))
3329 if (matchStrLen
> maxMatchStrLen
)
3331 maxMatchStrLen
= matchStrLen
;
3337 // Search 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;
3365 str
.Index
+= (maxMatchStrLen
- 1);
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;
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
;
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;
3437 str
.Index
+= (maxMatchStrLen
- 1);
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;
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
;
3475 str
.Index
+= maxMatchStrLen
- 1;
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;
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
;
3514 str
.Index
+= maxMatchStrLen
- 1;
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
)
3532 int[] eras
= dtfi
.Calendar
.Eras
;
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);
3545 searchStr
= dtfi
.GetAbbreviatedEraName(eras
[i
]);
3546 if (str
.MatchSpecifiedWord(searchStr
))
3548 str
.Index
+= (searchStr
.Length
- 1);
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
)
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)
3574 if (dtfi
.PMDesignator
.Length
== 0)
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);
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);
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.
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.
3628 string amDesignator
= dtfi
.AMDesignator
;
3629 if (amDesignator
.Length
> 0 && str
.GetChar() == amDesignator
[0])
3635 string pmDesignator
= dtfi
.PMDesignator
;
3636 if (pmDesignator
.Length
> 0 && str
.GetChar() == pmDesignator
[0])
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.
3653 ==============================================================================*/
3655 private static bool CheckNewValue(ref int currentValue
, int newValue
, char patternChar
, ref DateTimeResult result
)
3657 if (currentValue
== -1)
3659 currentValue
= newValue
;
3664 if (newValue
!= currentValue
)
3666 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_RepeatDateTimePattern
), patternChar
);
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
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
));
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;
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
);
3760 // Month/Day are both missing.
3767 if (result
.Year
== -1)
3769 result
.Year
= cal
.GetYear(now
);
3771 if (result
.Month
== -1)
3775 if (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
;
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
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.
3802 case 's': // Sortable format (in local time)
3804 case 'O': // Round Trip Format
3805 ConfigureFormatOS(ref dtfi
, ref parseInfo
);
3808 case 'R': // RFC 1123 Standard. (in Universal time)
3809 ConfigureFormatR(ref dtfi
, ref parseInfo
, ref result
);
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
;
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();
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())
3848 if (str
.m_current
!= DateTimeFormatInfo
.JapaneseEraStart
[0])
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(
3877 ref __DTString format
,
3878 ref ParsingInfo parseInfo
,
3879 DateTimeFormatInfo dtfi
,
3880 ref DateTimeResult result
)
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();
3892 tokenLen
= format
.GetRepeatCount();
3894 if (ParseJapaneseEraStart(ref str
, dtfi
))
3899 else if (dtfi
.HasForceTwoDigitYears
)
3901 parseResult
= ParseDigits(ref str
, 1, 4, out tempYear
);
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
);
3917 result
.SetBadDateTimeFailure();
3920 if (!CheckNewValue(ref result
.Year
, tempYear
, ch
, ref result
))
3926 tokenLen
= format
.GetRepeatCount();
3929 if (!ParseDigits(ref str
, tokenLen
, out tempMonth
))
3931 if (!parseInfo
.fCustomNumberParser
||
3932 !parseInfo
.parseNumberDelegate(ref str
, tokenLen
, out tempMonth
))
3934 result
.SetBadDateTimeFailure();
3943 if (!MatchAbbreviatedMonthName(ref str
, dtfi
, ref tempMonth
))
3945 result
.SetBadDateTimeFailure();
3951 if (!MatchMonthName(ref str
, dtfi
, ref tempMonth
))
3953 result
.SetBadDateTimeFailure();
3957 result
.flags
|= ParseFlags
.ParsedMonthName
;
3959 if (!CheckNewValue(ref result
.Month
, tempMonth
, ch
, ref result
))
3965 // Day & Day of week
3966 tokenLen
= format
.GetRepeatCount();
3971 if (!ParseDigits(ref str
, tokenLen
, out tempDay
))
3973 if (!parseInfo
.fCustomNumberParser
||
3974 !parseInfo
.parseNumberDelegate(ref str
, tokenLen
, out tempDay
))
3976 result
.SetBadDateTimeFailure();
3980 if (!CheckNewValue(ref result
.Day
, tempDay
, ch
, ref result
))
3990 if (!MatchAbbreviatedDayName(ref str
, dtfi
, ref tempDayOfWeek
))
3992 result
.SetBadDateTimeFailure();
3999 if (!MatchDayName(ref str
, dtfi
, ref tempDayOfWeek
))
4001 result
.SetBadDateTimeFailure();
4005 if (!CheckNewValue(ref parseInfo
.dayOfWeek
, tempDayOfWeek
, ch
, ref result
))
4012 tokenLen
= format
.GetRepeatCount();
4013 // Put the era value in result.era.
4014 if (!MatchEraName(ref str
, dtfi
, ref result
.era
))
4016 result
.SetBadDateTimeFailure();
4021 parseInfo
.fUseHour12
= true;
4022 tokenLen
= format
.GetRepeatCount();
4023 if (!ParseDigits(ref str
, tokenLen
< 2 ? 1 : 2, out tempHour
))
4025 result
.SetBadDateTimeFailure();
4028 if (!CheckNewValue(ref result
.Hour
, tempHour
, ch
, ref result
))
4034 tokenLen
= format
.GetRepeatCount();
4035 if (!ParseDigits(ref str
, tokenLen
< 2 ? 1 : 2, out tempHour
))
4037 result
.SetBadDateTimeFailure();
4040 if (!CheckNewValue(ref result
.Hour
, tempHour
, ch
, ref result
))
4046 tokenLen
= format
.GetRepeatCount();
4047 if (!ParseDigits(ref str
, tokenLen
< 2 ? 1 : 2, out tempMinute
))
4049 result
.SetBadDateTimeFailure();
4052 if (!CheckNewValue(ref result
.Minute
, tempMinute
, ch
, ref result
))
4058 tokenLen
= format
.GetRepeatCount();
4059 if (!ParseDigits(ref str
, tokenLen
< 2 ? 1 : 2, out tempSecond
))
4061 result
.SetBadDateTimeFailure();
4064 if (!CheckNewValue(ref result
.Second
, tempSecond
, ch
, ref result
))
4071 tokenLen
= format
.GetRepeatCount();
4072 if (tokenLen
<= DateTimeFormat
.MaxSecondsFractionDigits
)
4074 if (!ParseFractionExact(ref str
, tokenLen
, ref tempFraction
))
4078 result
.SetBadDateTimeFailure();
4082 if (result
.fraction
< 0)
4084 result
.fraction
= tempFraction
;
4088 if (tempFraction
!= result
.fraction
)
4090 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_RepeatDateTimePattern
), ch
);
4097 result
.SetBadDateTimeFailure();
4103 tokenLen
= format
.GetRepeatCount();
4106 if (!MatchAbbreviatedTimeMark(ref str
, dtfi
, ref tempTimeMark
))
4108 result
.SetBadDateTimeFailure();
4114 if (!MatchTimeMark(ref str
, dtfi
, ref tempTimeMark
))
4116 result
.SetBadDateTimeFailure();
4121 if (parseInfo
.timeMark
== TM
.NotSet
)
4123 parseInfo
.timeMark
= tempTimeMark
;
4127 if (parseInfo
.timeMark
!= tempTimeMark
)
4129 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_RepeatDateTimePattern
), ch
);
4136 tokenLen
= format
.GetRepeatCount();
4138 TimeSpan tempTimeZoneOffset
= new TimeSpan(0);
4139 if (!ParseTimeZoneOffset(ref str
, tokenLen
, ref tempTimeZoneOffset
))
4141 result
.SetBadDateTimeFailure();
4144 if ((result
.flags
& ParseFlags
.TimeZoneUsed
) != 0 && tempTimeZoneOffset
!= result
.timeZoneOffset
)
4146 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_RepeatDateTimePattern
), 'z');
4149 result
.timeZoneOffset
= tempTimeZoneOffset
;
4150 result
.flags
|= ParseFlags
.TimeZoneUsed
;
4154 if ((result
.flags
& ParseFlags
.TimeZoneUsed
) != 0 && result
.timeZoneOffset
!= TimeSpan
.Zero
)
4156 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_RepeatDateTimePattern
), 'Z');
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.
4169 if (!GetTimeZoneName(ref str
))
4171 result
.SetBadDateTimeFailure();
4177 // This should parse either as a blank, the 'Z' character or a local offset like "-07:00"
4180 if ((result
.flags
& ParseFlags
.TimeZoneUsed
) != 0 && result
.timeZoneOffset
!= TimeSpan
.Zero
)
4182 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_RepeatDateTimePattern
), 'K');
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();
4199 if ((result
.flags
& ParseFlags
.TimeZoneUsed
) != 0 && tempTimeZoneOffset
!= result
.timeZoneOffset
)
4201 result
.SetFailure(ParseFailureKind
.FormatWithParameter
, nameof(SR
.Format_RepeatDateTimePattern
), 'K');
4204 result
.timeZoneOffset
= tempTimeZoneOffset
;
4205 result
.flags
|= ParseFlags
.TimeZoneUsed
;
4207 // Otherwise it is unspecified and we consume no characters
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();
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();
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
);
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();
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
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
;
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
);
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();
4303 result
.SetBadFormatSpecifierFailure(format
.Value
);
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();
4320 result
.SetBadDateTimeFailure();
4327 if (parseInfo
.fAllowInnerWhite
)
4329 // Skip whitespaces if AllowInnerWhite.
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
))
4355 result
.SetBadDateTimeFailure();
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();
4376 else if (!str
.Match(ch
))
4379 result
.SetBadDateTimeFailure();
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.
4398 int formatLen
= format
.Length
;
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
)
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:
4417 // because the second double quote is escaped.
4418 if (pos
< formatLen
)
4420 result
.Append(format
[pos
++]);
4425 // This means that '\' is at the end of the formatting string.
4438 // Here we can't find the matching quote.
4443 // Return the character count including the begin/end quote characters and enclosed string.
4445 returnValue
= (pos
- beginPos
);
4449 /*=================================DoStrictParse==================================
4450 **Action: Do DateTime parsing using the format in formatParam.
4451 **Returns: The parsed DateTime.
4456 ** When the following general formats are used, InvariantInfo is used in dtfi:
4458 ** When the following general formats are used, the time is assumed to be in Universal time.
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();
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
)
4490 ConfigureFormatR(ref dtfi
, ref parseInfo
, ref result
);
4491 return ParseFormatR(s
, ref parseInfo
, ref result
);
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
);
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.
4530 format
.RemoveTrailingInQuoteSpaces();
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
))
4558 if (str
.Index
< str
.Value
.Length
- 1)
4560 // There are still remaining character in str.
4561 result
.SetBadDateTimeFailure();
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();
4575 result
.Year
= parseInfo
.calendar
.ToFourDigitYear(result
.Year
);
4577 catch (ArgumentOutOfRangeException
)
4579 result
.SetBadDateTimeFailure();
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();
4601 if (parseInfo
.timeMark
== TM
.AM
)
4603 if (result
.Hour
== 12)
4610 result
.Hour
= (result
.Hour
== 12) ? 12 : result
.Hour
+ 12;
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();
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
))
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
));
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
));
4648 if (result
.fraction
> 0)
4650 if (!result
.parsedDate
.TryAddTicks((long)Math
.Round(result
.fraction
* Calendar
.TicksPerSecond
), out result
.parsedDate
))
4652 result
.SetBadDateTimeFailure();
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
));
4674 return DetermineTimeZoneAdjustments(ref result
, styles
, bTimeOnly
);
4677 private static bool ParseFormatR(ReadOnlySpan
<char> source
, ref ParsingInfo parseInfo
, ref DateTimeResult result
)
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();
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();
4700 uint dowString
= (dow0
<< 24) | (dow1
<< 16) | (dow2
<< 8) | comma
| 0x20202000;
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;
4711 result
.SetBadDateTimeFailure();
4716 if (source
[4] != ' ')
4718 result
.SetBadDateTimeFailure();
4722 // Parse the two digit day.
4725 uint digit1
= (uint)(source
[5] - '0'), digit2
= (uint)(source
[6] - '0');
4727 if (digit1
> 9 || digit2
> 9)
4729 result
.SetBadDateTimeFailure();
4733 day
= (int)(digit1
* 10 + digit2
);
4736 if (source
[7] != ' ')
4738 result
.SetBadDateTimeFailure();
4742 // Parse the three letter month (followed by a space). Any casing is valid.
4745 uint m0
= source
[8], m1
= source
[9], m2
= source
[10], space
= source
[11];
4747 if ((m0
| m1
| m2
| space
) > 0x7F)
4749 result
.SetBadDateTimeFailure();
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;
4768 result
.SetBadDateTimeFailure();
4773 // Parse the four-digit 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();
4784 year
= (int)(y1
* 1000 + y2
* 100 + y3
* 10 + y4
);
4787 if (source
[16] != ' ')
4789 result
.SetBadDateTimeFailure();
4793 // Parse the two digit hour.
4796 uint h1
= (uint)(source
[17] - '0'), h2
= (uint)(source
[18] - '0');
4798 if (h1
> 9 || h2
> 9)
4800 result
.SetBadDateTimeFailure();
4804 hour
= (int)(h1
* 10 + h2
);
4807 if (source
[19] != ':')
4809 result
.SetBadDateTimeFailure();
4813 // Parse the two-digit minute.
4816 uint m1
= (uint)(source
[20] - '0');
4817 uint m2
= (uint)(source
[21] - '0');
4819 if (m1
> 9 || m2
> 9)
4821 result
.SetBadDateTimeFailure();
4825 minute
= (int)(m1
* 10 + m2
);
4828 if (source
[22] != ':')
4830 result
.SetBadDateTimeFailure();
4834 // Parse the two-digit second.
4837 uint s1
= (uint)(source
[23] - '0'), s2
= (uint)(source
[24] - '0');
4839 if (s1
> 9 || s2
> 9)
4841 result
.SetBadDateTimeFailure();
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();
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
));
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
));
4872 private static bool ParseFormatO(ReadOnlySpan
<char> source
, ref DateTimeResult result
)
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 ||
4883 source
[10] != 'T' ||
4884 source
[13] != ':' ||
4885 source
[16] != ':' ||
4888 result
.SetBadDateTimeFailure();
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();
4902 year
= (int)(y1
* 1000 + y2
* 100 + y3
* 10 + y4
);
4907 uint m1
= (uint)(source
[5] - '0'), m2
= (uint)(source
[6] - '0');
4909 if (m1
> 9 || m2
> 9)
4911 result
.SetBadDateTimeFailure();
4915 month
= (int)(m1
* 10 + m2
);
4920 uint d1
= (uint)(source
[8] - '0'), d2
= (uint)(source
[9] - '0');
4922 if (d1
> 9 || d2
> 9)
4924 result
.SetBadDateTimeFailure();
4928 day
= (int)(d1
* 10 + d2
);
4933 uint h1
= (uint)(source
[11] - '0'), h2
= (uint)(source
[12] - '0');
4935 if (h1
> 9 || h2
> 9)
4937 result
.SetBadDateTimeFailure();
4941 hour
= (int)(h1
* 10 + h2
);
4946 uint m1
= (uint)(source
[14] - '0'), m2
= (uint)(source
[15] - '0');
4948 if (m1
> 9 || m2
> 9)
4950 result
.SetBadDateTimeFailure();
4954 minute
= (int)(m1
* 10 + m2
);
4959 uint s1
= (uint)(source
[17] - '0'), s2
= (uint)(source
[18] - '0');
4961 if (s1
> 9 || s2
> 9)
4963 result
.SetBadDateTimeFailure();
4967 second
= (int)(s1
* 10 + s2
);
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();
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();
4995 if (!dateTime
.TryAddTicks((long)Math
.Round(fraction
* Calendar
.TicksPerSecond
), out result
.parsedDate
))
4997 result
.SetBadDateTimeFailure();
5001 if ((uint)source
.Length
> 27)
5003 char offsetChar
= source
[27];
5007 if (source
.Length
!= 28)
5009 result
.SetBadDateTimeFailure();
5012 result
.flags
|= ParseFlags
.TimeZoneUsed
| ParseFlags
.TimeZoneUtc
;
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();
5029 offsetHours
= (int)(oh1
* 10 + oh2
);
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();
5046 result
.SetBadDateTimeFailure();
5050 if (source
[colonIndex
] != ':')
5052 result
.SetBadDateTimeFailure();
5058 uint om1
= (uint)(source
[colonIndex
+ 1] - '0'), om2
= (uint)(source
[colonIndex
+ 2] - '0');
5060 if (om1
> 9 || om2
> 9)
5062 result
.SetBadDateTimeFailure();
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();
5078 result
.SetBadDateTimeFailure();
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
));
5105 Debug
.Fail("Unknown DateTimeParseFailure: " + result
.failure
.ToString());
5110 [Conditional("_LOGGING")]
5111 private static void LexTraceExit(string message
, DS dps
)
5114 if (!_tracingEnabled
)
5116 Trace($"Lex return {message}, DS.{dps}");
5119 [Conditional("_LOGGING")]
5120 private static void PTSTraceExit(DS dps
, bool passed
)
5123 if (!_tracingEnabled
)
5125 Trace($"ProcessTerminalState {(passed ? "passed" : "failed")} @ DS.{dps}");
5128 [Conditional("_LOGGING")]
5129 private static void TPTraceExit(string message
, DS dps
)
5132 if (!_tracingEnabled
)
5134 Trace($"TryParse return {message}, DS.{dps}");
5137 [Conditional("_LOGGING")]
5138 private static void DTFITrace(DateTimeFormatInfo dtfi
)
5141 if (!_tracingEnabled
)
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)}");
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
;
5177 for (int i
= 1; i
< strs
.Length
- 1; i
++)
5181 if (s
.Length
> MaxLineLength
|| (curLineLength
+ s
.Length
+ 2) > MaxLineLength
)
5184 buffer
.Append(Environment
.NewLine
);
5185 buffer
.Append(' ', NewLinePadding
);
5190 buffer
.Append(", ");
5194 curLineLength
+= s
.Length
;
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
);
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
]);
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
)
5231 return c
.ToString(CultureInfo
.InvariantCulture
);
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;
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.
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");
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.
5298 // while (str.GetNext())
5300 // char ch = str.GetChar()
5302 internal bool GetNext()
5307 m_current
= Value
[Index
];
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");
5324 m_current
= Value
[Index
];
5330 // Used by DateTime.Parse() to get the next token.
5331 internal void GetRegularToken(out TokenType tokenType
, out int tokenValue
, DateTimeFormatInfo dtfi
)
5334 if (Index
>= Length
)
5336 tokenType
= TokenType
.EndOfString
;
5341 if (DateTimeParse
.IsDigit(m_current
))
5344 tokenValue
= m_current
- '0';
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;
5364 if (Index
- start
> DateTimeParse
.MaxDateTimeNumberDigits
)
5366 tokenType
= TokenType
.NumberToken
;
5369 else if (Index
- start
< 3)
5371 tokenType
= TokenType
.NumberToken
;
5375 // If there are more than 3 digits, assume that it's a year value.
5376 tokenType
= TokenType
.YearNumberToken
;
5378 if (m_checkDigitToken
)
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
];
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().
5397 // Use the number token value.
5398 // Restore the index.
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
))
5415 // We have reached the end of string.
5416 tokenType
= TokenType
.EndOfString
;
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);
5440 tokenType
= TokenType
.SEP_Space
;
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
5447 tokenType
= TokenType
.SEP_Space
;
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
);
5476 int segmentLength
= wsIndex
- targetPosition
;
5477 if (thisPosition
>= Value
.Length
- segmentLength
)
5478 { // Subtraction to prevent overflow.
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.
5489 // Make sure we also have whitespace in the input string
5490 if (!char.IsWhiteSpace(Value
[thisPosition
+ segmentLength
]))
5494 if (m_info
.CompareOptionIgnoreCase(Value
.Slice(thisPosition
, segmentLength
), target
.AsSpan(targetPosition
, segmentLength
)) != 0)
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
]))
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
)
5519 if (m_info
.CompareOptionIgnoreCase(Value
.Slice(thisPosition
, segmentLength
), target
.AsSpan(targetPosition
, segmentLength
)) != 0)
5526 if (checkWordBoundary
)
5528 int nextCharIndex
= Index
+ matchLength
;
5529 if (nextCharIndex
< Value
.Length
)
5531 if (char.IsLetter(Value
[nextCharIndex
]))
5541 // Check to see if the string starting from Index is a prefix of
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
)
5553 if (str
.Length
> (Value
.Length
- Index
))
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);
5569 internal bool Match(char ch
)
5571 if (++Index
>= Length
)
5575 if (Value
[Index
] == ch
)
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.
5589 // The index that contains the longest word to match
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
)
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
;
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
))
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.
5636 // Return false when end of string is encountered or a non-digit character is found.
5637 [MethodImpl(MethodImplOptions
.AggressiveInlining
)]
5638 internal bool GetNextDigit() =>
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
5670 while (Index
+ 1 < Length
)
5672 char ch
= Value
[Index
+ 1];
5673 if (!char.IsWhiteSpace(ch
))
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
)
5694 if (!char.IsWhiteSpace(m_current
))
5699 while (++Index
< Length
)
5701 m_current
= Value
[Index
];
5702 if (!char.IsWhiteSpace(m_current
))
5711 internal void TrimTail()
5714 while (i
>= 0 && char.IsWhiteSpace(Value
[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()
5731 // Check if the last character is a quote.
5732 if (ch
== '\'' || ch
== '\"')
5734 if (char.IsWhiteSpace(Value
[i
- 1]))
5737 while (i
>= 1 && char.IsWhiteSpace(Value
[i
- 1]))
5741 Span
<char> result
= new char[i
+ 1];
5743 Value
.Slice(0, i
).CopyTo(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()
5759 // Check if the last character is a quote.
5760 if (ch
== '\'' || ch
== '\"')
5762 while ((i
+ 1) < Length
&& char.IsWhiteSpace(Value
[i
+ 1]))
5768 Span
<char> result
= new char[Value
.Length
- i
];
5770 Value
.Slice(i
+ 1).CopyTo(result
.Slice(1));
5776 internal DTSubString
GetSubString()
5778 DTSubString sub
= new DTSubString();
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
;
5791 currentType
= DTSubStringType
.Other
;
5794 if (sub
.length
== 0)
5796 sub
.type
= currentType
;
5800 if (sub
.type
!= currentType
)
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
;
5815 int number
= ch
- '0';
5816 Debug
.Assert(number
>= 0 && number
<= 9, "number >= 0 && number <= 9");
5817 sub
.value = sub
.value * 10 + number
;
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
5827 if (sub
.length
== 0)
5829 sub
.type
= DTSubStringType
.End
;
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
;
5843 m_current
= Value
[Index
];
5848 internal enum DTSubStringType
5857 internal ref struct DTSubString
5859 internal ReadOnlySpan
<char> s
;
5861 internal int length
;
5862 internal DTSubStringType type
;
5865 internal char this[int relativeIndex
] => s
[index
+ relativeIndex
];
5869 // The buffer to store the parsing token.
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
5885 internal int numCount
;
5888 internal int dayOfWeek
;
5890 internal DateTimeParse
.TM timeMark
;
5891 internal double fraction
;
5892 internal bool hasSameDateAndTimeSeparators
;
5894 internal void Init(int* numberBuffer
)
5900 timeMark
= DateTimeParse
.TM
.NotSet
;
5905 internal void AddNumber(int value)
5907 num
[numCount
++] = value;
5910 internal int GetNumber(int index
)
5916 internal enum ParseFailureKind
5921 FormatWithParameter
= 3,
5922 FormatWithOriginalDateTime
= 4,
5923 FormatWithFormatSpecifier
= 5,
5924 FormatWithOriginalDateTimeAndParameter
= 6,
5925 FormatBadDateTimeCalendar
= 7, // FormatException when ArgumentOutOfRange is thrown by a Calendar.TryToDateTime().
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
5958 // Set time default to 00:00:00.
5961 internal int Minute
;
5962 internal int Second
;
5963 internal double fraction
;
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
;
5992 internal void SetDate(int year
, int month
, int 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()
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,