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.
4 
5 ////////////////////////////////////////////////////////////////////////////
6 //
7 //  Purpose:  Used by TimeSpan to parse a time interval string.
8 //
9 //  Standard Format:
10 //  -=-=-=-=-=-=-=-
11 //  "c":  Constant format.  [-][d'.']hh':'mm':'ss['.'fffffff]
12 //  Not culture sensitive.  Default format (and null/empty format string) map to this format.
13 //
14 //  "g":  General format, short:  [-][d':']h':'mm':'ss'.'FFFFFFF
15 //  Only print what's needed.  Localized (if you want Invariant, pass in Invariant).
16 //  The fractional seconds separator is localized, equal to the culture's DecimalSeparator.
17 //
18 //  "G":  General format, long:  [-]d':'hh':'mm':'ss'.'fffffff
19 //  Always print days and 7 fractional digits.  Localized (if you want Invariant, pass in Invariant).
20 //  The fractional seconds separator is localized, equal to the culture's DecimalSeparator.
21 //
22 //  * "TryParseTimeSpan" is the main method for Parse/TryParse
23 //
24 //  - TimeSpanTokenizer.GetNextToken() is used to split the input string into number and literal tokens.
25 //  - TimeSpanRawInfo.ProcessToken() adds the next token into the parsing intermediary state structure
26 //  - ProcessTerminalState() uses the fully initialized TimeSpanRawInfo to find a legal parse match.
27 //    The terminal states are attempted as follows:
28 //    foreach (+InvariantPattern, -InvariantPattern, +LocalizedPattern, -LocalizedPattern) try
29 //       1 number  => d
30 //       2 numbers => h:m
31 //       3 numbers => h:m:s     | d.h:m   | h:m:.f
32 //       4 numbers => h:m:s.f   | d.h:m:s | d.h:m:.f
33 //       5 numbers => d.h:m:s.f
34 //
35 // Custom Format:
36 // -=-=-=-=-=-=-=
37 //
38 // * "TryParseExactTimeSpan" is the main method for ParseExact/TryParseExact methods
39 // * "TryParseExactMultipleTimeSpan" is the main method for ParseExact/TryparseExact
40 //    methods that take a string[] of formats
41 //
42 // - For single-letter formats "TryParseTimeSpan" is called (see above)
43 // - For multi-letter formats "TryParseByFormat" is called
44 // - TryParseByFormat uses helper methods (ParseExactLiteral, ParseExactDigits, etc)
45 //   which drive the underlying TimeSpanTokenizer.  However, unlike standard formatting which
46 //   operates on whole-tokens, ParseExact operates at the character-level.  As such,
47 //   TimeSpanTokenizer.NextChar and TimeSpanTokenizer.BackOne() are called directly.
48 //
49 ////////////////////////////////////////////////////////////////////////////
50 
51 using System.Diagnostics;
52 using System.Text;
53 
54 namespace System.Globalization
55 {
56     internal static class TimeSpanParse
57     {
58         private const int MaxFractionDigits = 7;
59         private const int MaxDays = 10675199;
60         private const int MaxHours = 23;
61         private const int MaxMinutes = 59;
62         private const int MaxSeconds = 59;
63         private const int MaxFraction = 9999999;
64 
65         private enum ParseFailureKind : byte
66         {
67             None = 0,
68             ArgumentNull = 1,
69             Format = 2,
70             FormatWithParameter = 3,
71             Overflow = 4,
72         }
73 
74         [Flags]
75         private enum TimeSpanStandardStyles : byte
76         {
77             // Standard Format Styles
78             None = 0x00000000,
79             Invariant = 0x00000001, //Allow Invariant Culture
80             Localized = 0x00000002, //Allow Localized Culture
81             RequireFull = 0x00000004, //Require the input to be in DHMSF format
82             Any = Invariant | Localized,
83         }
84 
85         // TimeSpan Token Types
86         private enum TTT : byte
87         {
88             None = 0,         // None of the TimeSpanToken fields are set
89             End = 1,          // '\0'
90             Num = 2,          // Number
91             Sep = 3,          // literal
92             NumOverflow = 4,  // Number that overflowed
93         }
94 
95         private ref struct TimeSpanToken
96         {
97             internal TTT _ttt;
98             internal int _num;                // Store the number that we are parsing (if any)
99             internal int _zeroes;             // Store the number of leading zeroes (if any)
100             internal ReadOnlySpan<char> _sep; // Store the literal that we are parsing (if any)
101 
TimeSpanTokenSystem.Globalization.TimeSpanParse.TimeSpanToken102             public TimeSpanToken(TTT type) : this(type, 0, 0, default(ReadOnlySpan<char>)) { }
103 
TimeSpanTokenSystem.Globalization.TimeSpanParse.TimeSpanToken104             public TimeSpanToken(int number) : this(TTT.Num, number, 0, default(ReadOnlySpan<char>)) { }
105 
TimeSpanTokenSystem.Globalization.TimeSpanParse.TimeSpanToken106             public TimeSpanToken(int number, int leadingZeroes) : this(TTT.Num, number, leadingZeroes, default(ReadOnlySpan<char>)) { }
107 
TimeSpanTokenSystem.Globalization.TimeSpanParse.TimeSpanToken108             public TimeSpanToken(TTT type, int number, int leadingZeroes, ReadOnlySpan<char> separator)
109             {
110                 _ttt = type;
111                 _num = number;
112                 _zeroes = leadingZeroes;
113                 _sep = separator;
114             }
115 
IsInvalidFractionSystem.Globalization.TimeSpanParse.TimeSpanToken116             public bool IsInvalidFraction()
117             {
118                 Debug.Assert(_ttt == TTT.Num);
119                 Debug.Assert(_num > -1);
120 
121                 if (_num > MaxFraction || _zeroes > MaxFractionDigits)
122                     return true;
123 
124                 if (_num == 0 || _zeroes == 0)
125                     return false;
126 
127                 // num > 0 && zeroes > 0 && num <= maxValue && zeroes <= maxPrecision
128                 return _num >= MaxFraction / Pow10(_zeroes - 1);
129             }
130         }
131 
132         private ref struct TimeSpanTokenizer
133         {
134             private ReadOnlySpan<char> _value;
135             private int _pos;
136 
TimeSpanTokenizerSystem.Globalization.TimeSpanParse.TimeSpanTokenizer137             internal TimeSpanTokenizer(ReadOnlySpan<char> input) : this(input, 0) { }
138 
TimeSpanTokenizerSystem.Globalization.TimeSpanParse.TimeSpanTokenizer139             internal TimeSpanTokenizer(ReadOnlySpan<char> input, int startPosition)
140             {
141                 _value = input;
142                 _pos = startPosition;
143             }
144 
145             /// <summary>Returns the next token in the input string</summary>
146             /// <remarks>Used by the parsing routines that operate on standard-formats.</remarks>
GetNextTokenSystem.Globalization.TimeSpanParse.TimeSpanTokenizer147             internal TimeSpanToken GetNextToken()
148             {
149                 // Get the position of the next character to be processed.  If there is no
150                 // next character, we're at the end.
151                 int pos = _pos;
152                 Debug.Assert(pos > -1);
153                 if (pos >= _value.Length)
154                 {
155                     return new TimeSpanToken(TTT.End);
156                 }
157 
158                 // Now retrieve that character. If it's a digit, we're processing a number.
159                 int num = _value[pos] - '0';
160                 if ((uint)num <= 9)
161                 {
162                     int zeroes = 0;
163                     if (num == 0)
164                     {
165                         // Read all leading zeroes.
166                         zeroes = 1;
167                         while (true)
168                         {
169                             int digit;
170                             if (++_pos >= _value.Length || (uint)(digit = _value[_pos] - '0') > 9)
171                             {
172                                 return new TimeSpanToken(TTT.Num, 0, zeroes, default(ReadOnlySpan<char>));
173                             }
174 
175                             if (digit == 0)
176                             {
177                                 zeroes++;
178                                 continue;
179                             }
180 
181                             num = digit;
182                             break;
183                         }
184                     }
185 
186                     // Continue to read as long as we're reading digits.
187                     while (++_pos < _value.Length)
188                     {
189                         int digit = _value[_pos] - '0';
190                         if ((uint)digit > 9)
191                         {
192                             break;
193                         }
194 
195                         num = num * 10 + digit;
196                         if ((num & 0xF0000000) != 0)
197                         {
198                             return new TimeSpanToken(TTT.NumOverflow);
199                         }
200                     }
201 
202                     return new TimeSpanToken(TTT.Num, num, zeroes, default(ReadOnlySpan<char>));
203                 }
204 
205                 // Otherwise, we're processing a separator, and we've already processed the first
206                 // character of it.  Continue processing characters as long as they're not digits.
207                 int length = 1;
208                 while (true)
209                 {
210                     if (++_pos >= _value.Length || (uint)(_value[_pos] - '0') <= 9)
211                     {
212                         break;
213                     }
214                     length++;
215                 }
216 
217                 // Return the separator.
218                 return new TimeSpanToken(TTT.Sep, 0, 0, _value.Slice(pos, length));
219             }
220 
221             internal bool EOL => _pos >= (_value.Length - 1);
222 
BackOneSystem.Globalization.TimeSpanParse.TimeSpanTokenizer223             internal void BackOne()
224             {
225                 if (_pos > 0) --_pos;
226             }
227 
228             internal char NextChar
229             {
230                 get
231                 {
232                     int pos = ++_pos;
233                     return (uint)pos < (uint)_value.Length ?
234                         _value[pos] :
235                         (char)0;
236                 }
237             }
238         }
239 
240         /// <summary>Stores intermediary parsing state for the standard formats.</summary>
241         private ref struct TimeSpanRawInfo
242         {
243             internal TimeSpanFormat.FormatLiterals PositiveInvariant => TimeSpanFormat.PositiveInvariantFormatLiterals;
244             internal TimeSpanFormat.FormatLiterals NegativeInvariant => TimeSpanFormat.NegativeInvariantFormatLiterals;
245 
246             internal TimeSpanFormat.FormatLiterals PositiveLocalized
247             {
248                 get
249                 {
250                     if (!_posLocInit)
251                     {
252                         _posLoc = new TimeSpanFormat.FormatLiterals();
253                         _posLoc.Init(_fullPosPattern, false);
254                         _posLocInit = true;
255                     }
256                     return _posLoc;
257                 }
258             }
259 
260             internal TimeSpanFormat.FormatLiterals NegativeLocalized
261             {
262                 get
263                 {
264                     if (!_negLocInit)
265                     {
266                         _negLoc = new TimeSpanFormat.FormatLiterals();
267                         _negLoc.Init(_fullNegPattern, false);
268                         _negLocInit = true;
269                     }
270                     return _negLoc;
271                 }
272             }
273 
274             internal bool FullAppCompatMatch(TimeSpanFormat.FormatLiterals pattern) =>
275                 _sepCount == 5
276                 && _numCount == 4
277                 && StringSpanHelpers.Equals(_literals0, pattern.Start)
278                 && StringSpanHelpers.Equals(_literals1, pattern.DayHourSep)
279                 && StringSpanHelpers.Equals(_literals2, pattern.HourMinuteSep)
280                 && StringSpanHelpers.Equals(_literals3, pattern.AppCompatLiteral)
281                 && StringSpanHelpers.Equals(_literals4, pattern.End);
282 
283             internal bool PartialAppCompatMatch(TimeSpanFormat.FormatLiterals pattern) =>
284                 _sepCount == 4
285                 && _numCount == 3
286                 && StringSpanHelpers.Equals(_literals0, pattern.Start)
287                 && StringSpanHelpers.Equals(_literals1, pattern.HourMinuteSep)
288                 && StringSpanHelpers.Equals(_literals2, pattern.AppCompatLiteral)
289                 && StringSpanHelpers.Equals(_literals3, pattern.End);
290 
291             /// <summary>DHMSF (all values matched)</summary>
292             internal bool FullMatch(TimeSpanFormat.FormatLiterals pattern) =>
293                 _sepCount == MaxLiteralTokens
294                 && _numCount == MaxNumericTokens
295                 && StringSpanHelpers.Equals(_literals0, pattern.Start)
296                 && StringSpanHelpers.Equals(_literals1, pattern.DayHourSep)
297                 && StringSpanHelpers.Equals(_literals2, pattern.HourMinuteSep)
298                 && StringSpanHelpers.Equals(_literals3, pattern.MinuteSecondSep)
299                 && StringSpanHelpers.Equals(_literals4, pattern.SecondFractionSep)
300                 && StringSpanHelpers.Equals(_literals5, pattern.End);
301 
302             /// <summary>D (no hours, minutes, seconds, or fractions)</summary>
303             internal bool FullDMatch(TimeSpanFormat.FormatLiterals pattern) =>
304                 _sepCount == 2
305                 && _numCount == 1
306                 && StringSpanHelpers.Equals(_literals0, pattern.Start)
307                 && StringSpanHelpers.Equals(_literals1, pattern.End);
308 
309             /// <summary>HM (no days, seconds, or fractions)</summary>
310             internal bool FullHMMatch(TimeSpanFormat.FormatLiterals pattern) =>
311                 _sepCount == 3
312                 && _numCount == 2
313                 && StringSpanHelpers.Equals(_literals0, pattern.Start)
314                 && StringSpanHelpers.Equals(_literals1, pattern.HourMinuteSep)
315                 && StringSpanHelpers.Equals(_literals2, pattern.End);
316 
317             /// <summary>DHM (no seconds or fraction)</summary>
318             internal bool FullDHMMatch(TimeSpanFormat.FormatLiterals pattern) =>
319                 _sepCount == 4
320                 && _numCount == 3
321                 && StringSpanHelpers.Equals(_literals0, pattern.Start)
322                 && StringSpanHelpers.Equals(_literals1, pattern.DayHourSep)
323                 && StringSpanHelpers.Equals(_literals2, pattern.HourMinuteSep)
324                 && StringSpanHelpers.Equals(_literals3, pattern.End);
325 
326             /// <summary>HMS (no days or fraction)</summary>
327             internal bool FullHMSMatch(TimeSpanFormat.FormatLiterals pattern) =>
328                 _sepCount == 4
329                 && _numCount == 3
330                 && StringSpanHelpers.Equals(_literals0, pattern.Start)
331                 && StringSpanHelpers.Equals(_literals1, pattern.HourMinuteSep)
332                 && StringSpanHelpers.Equals(_literals2, pattern.MinuteSecondSep)
333                 && StringSpanHelpers.Equals(_literals3, pattern.End);
334 
335             /// <summary>DHMS (no fraction)</summary>
336             internal bool FullDHMSMatch(TimeSpanFormat.FormatLiterals pattern) =>
337                 _sepCount == 5
338                 && _numCount == 4
339                 && StringSpanHelpers.Equals(_literals0, pattern.Start)
340                 && StringSpanHelpers.Equals(_literals1, pattern.DayHourSep)
341                 && StringSpanHelpers.Equals(_literals2, pattern.HourMinuteSep)
342                 && StringSpanHelpers.Equals(_literals3, pattern.MinuteSecondSep)
343                 && StringSpanHelpers.Equals(_literals4, pattern.End);
344 
345             /// <summary>HMSF (no days)</summary>
346             internal bool FullHMSFMatch(TimeSpanFormat.FormatLiterals pattern) =>
347                 _sepCount == 5
348                 && _numCount == 4
349                 && StringSpanHelpers.Equals(_literals0, pattern.Start)
350                 && StringSpanHelpers.Equals(_literals1, pattern.HourMinuteSep)
351                 && StringSpanHelpers.Equals(_literals2, pattern.MinuteSecondSep)
352                 && StringSpanHelpers.Equals(_literals3, pattern.SecondFractionSep)
353                 && StringSpanHelpers.Equals(_literals4, pattern.End);
354 
355             internal TTT _lastSeenTTT;
356             internal int _tokenCount;
357             internal int _sepCount;
358             internal int _numCount;
359 
360             private TimeSpanFormat.FormatLiterals _posLoc;
361             private TimeSpanFormat.FormatLiterals _negLoc;
362             private bool _posLocInit;
363             private bool _negLocInit;
364             private string _fullPosPattern;
365             private string _fullNegPattern;
366 
367             private const int MaxTokens = 11;
368             private const int MaxLiteralTokens = 6;
369             private const int MaxNumericTokens = 5;
370 
371             internal TimeSpanToken _numbers0, _numbers1, _numbers2, _numbers3, _numbers4; // MaxNumbericTokens = 5
372             internal ReadOnlySpan<char> _literals0, _literals1, _literals2, _literals3, _literals4, _literals5; // MaxLiteralTokens=6
373 
InitSystem.Globalization.TimeSpanParse.TimeSpanRawInfo374             internal void Init(DateTimeFormatInfo dtfi)
375             {
376                 Debug.Assert(dtfi != null);
377 
378                 _lastSeenTTT = TTT.None;
379                 _tokenCount = 0;
380                 _sepCount = 0;
381                 _numCount = 0;
382 
383                 _fullPosPattern = dtfi.FullTimeSpanPositivePattern;
384                 _fullNegPattern = dtfi.FullTimeSpanNegativePattern;
385                 _posLocInit = false;
386                 _negLocInit = false;
387             }
388 
ProcessTokenSystem.Globalization.TimeSpanParse.TimeSpanRawInfo389             internal bool ProcessToken(ref TimeSpanToken tok, ref TimeSpanResult result)
390             {
391                 switch (tok._ttt)
392                 {
393                     case TTT.Num:
394                         if ((_tokenCount == 0 && !AddSep(default(ReadOnlySpan<char>), ref result)) || !AddNum(tok, ref result))
395                         {
396                             return false;
397                         }
398                         break;
399 
400                     case TTT.Sep:
401                         if (!AddSep(tok._sep, ref result))
402                         {
403                             return false;
404                         }
405                         break;
406 
407                     case TTT.NumOverflow:
408                         return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
409 
410                     default:
411                         // Some unknown token or a repeat token type in the input
412                         return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
413                 }
414 
415                 _lastSeenTTT = tok._ttt;
416                 Debug.Assert(_tokenCount == (_sepCount + _numCount), "tokenCount == (SepCount + NumCount)");
417                 return true;
418             }
419 
AddSepSystem.Globalization.TimeSpanParse.TimeSpanRawInfo420             private bool AddSep(ReadOnlySpan<char> sep, ref TimeSpanResult result)
421             {
422                 if (_sepCount >= MaxLiteralTokens || _tokenCount >= MaxTokens)
423                 {
424                     return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
425                 }
426 
427                 switch (_sepCount++)
428                 {
429                     case 0: _literals0 = sep; break;
430                     case 1: _literals1 = sep; break;
431                     case 2: _literals2 = sep; break;
432                     case 3: _literals3 = sep; break;
433                     case 4: _literals4 = sep; break;
434                     default: _literals5 = sep; break;
435                 }
436 
437                 _tokenCount++;
438                 return true;
439             }
AddNumSystem.Globalization.TimeSpanParse.TimeSpanRawInfo440             private bool AddNum(TimeSpanToken num, ref TimeSpanResult result)
441             {
442                 if (_numCount >= MaxNumericTokens || _tokenCount >= MaxTokens)
443                 {
444                     return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
445                 }
446 
447                 switch (_numCount++)
448                 {
449                     case 0: _numbers0 = num; break;
450                     case 1: _numbers1 = num; break;
451                     case 2: _numbers2 = num; break;
452                     case 3: _numbers3 = num; break;
453                     default: _numbers4 = num; break;
454                 }
455 
456                 _tokenCount++;
457                 return true;
458             }
459         }
460 
461         /// <summary>Store the result of the parsing.</summary>
462         private struct TimeSpanResult
463         {
464             internal TimeSpan parsedTimeSpan;
465             private readonly bool _throwOnFailure;
466 
TimeSpanResultSystem.Globalization.TimeSpanParse.TimeSpanResult467             internal TimeSpanResult(bool throwOnFailure)
468             {
469                 parsedTimeSpan = default(TimeSpan);
470                 _throwOnFailure = throwOnFailure;
471             }
472 
SetFailureSystem.Globalization.TimeSpanParse.TimeSpanResult473             internal bool SetFailure(ParseFailureKind kind, string resourceKey, object messageArgument = null, string argumentName = null)
474             {
475                 if (!_throwOnFailure)
476                 {
477                     return false;
478                 }
479 
480                 string message = SR.GetResourceString(resourceKey);
481                 switch (kind)
482                 {
483                     case ParseFailureKind.ArgumentNull:
484                         Debug.Assert(argumentName != null);
485                         throw new ArgumentNullException(argumentName, message);
486 
487                     case ParseFailureKind.FormatWithParameter:
488                         throw new FormatException(SR.Format(message, messageArgument));
489 
490                     case ParseFailureKind.Overflow:
491                         throw new OverflowException(message);
492 
493                     default:
494                         Debug.Assert(kind == ParseFailureKind.Format, $"Unexpected failure {kind}");
495                         throw new FormatException(message);
496                 }
497             }
498         }
499 
Pow10(int pow)500         internal static long Pow10(int pow)
501         {
502             switch (pow)
503             {
504                 case 0:  return 1;
505                 case 1:  return 10;
506                 case 2:  return 100;
507                 case 3:  return 1000;
508                 case 4:  return 10000;
509                 case 5:  return 100000;
510                 case 6:  return 1000000;
511                 case 7:  return 10000000;
512                 default: return (long)Math.Pow(10, pow);
513             }
514         }
515 
TryTimeToTicks(bool positive, TimeSpanToken days, TimeSpanToken hours, TimeSpanToken minutes, TimeSpanToken seconds, TimeSpanToken fraction, out long result)516         private static bool TryTimeToTicks(bool positive, TimeSpanToken days, TimeSpanToken hours, TimeSpanToken minutes, TimeSpanToken seconds, TimeSpanToken fraction, out long result)
517         {
518             if (days._num > MaxDays ||
519                 hours._num > MaxHours ||
520                 minutes._num > MaxMinutes ||
521                 seconds._num > MaxSeconds ||
522                 fraction.IsInvalidFraction())
523             {
524                 result = 0;
525                 return false;
526             }
527 
528             long ticks = ((long)days._num * 3600 * 24 + (long)hours._num * 3600 + (long)minutes._num * 60 + seconds._num) * 1000;
529             if (ticks > InternalGlobalizationHelper.MaxMilliSeconds || ticks < InternalGlobalizationHelper.MinMilliSeconds)
530             {
531                 result = 0;
532                 return false;
533             }
534 
535             // Normalize the fraction component
536             //
537             // string representation => (zeroes,num) => resultant fraction ticks
538             // ---------------------    ------------    ------------------------
539             // ".9999999"            => (0,9999999)  => 9,999,999 ticks (same as constant maxFraction)
540             // ".1"                  => (0,1)        => 1,000,000 ticks
541             // ".01"                 => (1,1)        =>   100,000 ticks
542             // ".001"                => (2,1)        =>    10,000 ticks
543             long f = fraction._num;
544             if (f != 0)
545             {
546                 long lowerLimit = InternalGlobalizationHelper.TicksPerTenthSecond;
547                 if (fraction._zeroes > 0)
548                 {
549                     long divisor = Pow10(fraction._zeroes);
550                     lowerLimit = lowerLimit / divisor;
551                 }
552 
553                 while (f < lowerLimit)
554                 {
555                     f *= 10;
556                 }
557             }
558 
559             result = ticks * TimeSpan.TicksPerMillisecond + f;
560             if (positive && result < 0)
561             {
562                 result = 0;
563                 return false;
564             }
565 
566             return true;
567         }
568 
Parse(ReadOnlySpan<char> input, IFormatProvider formatProvider)569         internal static TimeSpan Parse(ReadOnlySpan<char> input, IFormatProvider formatProvider)
570         {
571             var parseResult = new TimeSpanResult(throwOnFailure: true);
572             bool success = TryParseTimeSpan(input, TimeSpanStandardStyles.Any, formatProvider, ref parseResult);
573             Debug.Assert(success, "Should have thrown on failure");
574             return parseResult.parsedTimeSpan;
575         }
576 
TryParse(ReadOnlySpan<char> input, IFormatProvider formatProvider, out TimeSpan result)577         internal static bool TryParse(ReadOnlySpan<char> input, IFormatProvider formatProvider, out TimeSpan result)
578         {
579             var parseResult = new TimeSpanResult(throwOnFailure: false);
580 
581             if (TryParseTimeSpan(input, TimeSpanStandardStyles.Any, formatProvider, ref parseResult))
582             {
583                 result = parseResult.parsedTimeSpan;
584                 return true;
585             }
586 
587             result = default(TimeSpan);
588             return false;
589         }
590 
ParseExact(ReadOnlySpan<char> input, ReadOnlySpan<char> format, IFormatProvider formatProvider, TimeSpanStyles styles)591         internal static TimeSpan ParseExact(ReadOnlySpan<char> input, ReadOnlySpan<char> format, IFormatProvider formatProvider, TimeSpanStyles styles)
592         {
593             var parseResult = new TimeSpanResult(throwOnFailure: true);
594             bool success = TryParseExactTimeSpan(input, format, formatProvider, styles, ref parseResult);
595             Debug.Assert(success, "Should have thrown on failure");
596             return parseResult.parsedTimeSpan;
597         }
598 
TryParseExact(ReadOnlySpan<char> input, ReadOnlySpan<char> format, IFormatProvider formatProvider, TimeSpanStyles styles, out TimeSpan result)599         internal static bool TryParseExact(ReadOnlySpan<char> input, ReadOnlySpan<char> format, IFormatProvider formatProvider, TimeSpanStyles styles, out TimeSpan result)
600         {
601             var parseResult = new TimeSpanResult(throwOnFailure: false);
602 
603             if (TryParseExactTimeSpan(input, format, formatProvider, styles, ref parseResult))
604             {
605                 result = parseResult.parsedTimeSpan;
606                 return true;
607             }
608 
609             result = default(TimeSpan);
610             return false;
611         }
612 
ParseExactMultiple(ReadOnlySpan<char> input, string[] formats, IFormatProvider formatProvider, TimeSpanStyles styles)613         internal static TimeSpan ParseExactMultiple(ReadOnlySpan<char> input, string[] formats, IFormatProvider formatProvider, TimeSpanStyles styles)
614         {
615             var parseResult = new TimeSpanResult(throwOnFailure: true);
616             bool success = TryParseExactMultipleTimeSpan(input, formats, formatProvider, styles, ref parseResult);
617             Debug.Assert(success, "Should have thrown on failure");
618             return parseResult.parsedTimeSpan;
619         }
620 
TryParseExactMultiple(ReadOnlySpan<char> input, string[] formats, IFormatProvider formatProvider, TimeSpanStyles styles, out TimeSpan result)621         internal static bool TryParseExactMultiple(ReadOnlySpan<char> input, string[] formats, IFormatProvider formatProvider, TimeSpanStyles styles, out TimeSpan result)
622         {
623             var parseResult = new TimeSpanResult(throwOnFailure: false);
624 
625             if (TryParseExactMultipleTimeSpan(input, formats, formatProvider, styles, ref parseResult))
626             {
627                 result = parseResult.parsedTimeSpan;
628                 return true;
629             }
630 
631             result = default(TimeSpan);
632             return false;
633         }
634 
635         /// <summary>Common private Parse method called by both Parse and TryParse.</summary>
TryParseTimeSpan(ReadOnlySpan<char> input, TimeSpanStandardStyles style, IFormatProvider formatProvider, ref TimeSpanResult result)636         private static bool TryParseTimeSpan(ReadOnlySpan<char> input, TimeSpanStandardStyles style, IFormatProvider formatProvider, ref TimeSpanResult result)
637         {
638             input = input.Trim();
639             if (input.IsEmpty)
640             {
641                 return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
642             }
643 
644             var tokenizer = new TimeSpanTokenizer(input);
645 
646             var raw = new TimeSpanRawInfo();
647             raw.Init(DateTimeFormatInfo.GetInstance(formatProvider));
648 
649             TimeSpanToken tok = tokenizer.GetNextToken();
650 
651             // The following loop will break out when we reach the end of the str or
652             // when we can determine that the input is invalid.
653             while (tok._ttt != TTT.End)
654             {
655                 if (!raw.ProcessToken(ref tok, ref result))
656                 {
657                     return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
658                 }
659                 tok = tokenizer.GetNextToken();
660             }
661             Debug.Assert(tokenizer.EOL);
662 
663             if (!ProcessTerminalState(ref raw, style, ref result))
664             {
665                 return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
666             }
667 
668             return true;
669         }
670 
671         /// <summary>
672         /// Validate the terminal state of a standard format parse.
673         /// Sets result.parsedTimeSpan on success.
674         /// Calculates the resultant TimeSpan from the TimeSpanRawInfo.
675         /// </summary>
676         /// <remarks>
677         /// try => +InvariantPattern, -InvariantPattern, +LocalizedPattern, -LocalizedPattern
678         /// 1) Verify Start matches
679         /// 2) Verify End matches
680         /// 3) 1 number  => d
681         ///    2 numbers => h:m
682         ///    3 numbers => h:m:s | d.h:m | h:m:.f
683         ///    4 numbers => h:m:s.f | d.h:m:s | d.h:m:.f
684         ///    5 numbers => d.h:m:s.f
685         /// </remarks>
ProcessTerminalState(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)686         private static bool ProcessTerminalState(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)
687         {
688             if (raw._lastSeenTTT == TTT.Num)
689             {
690                 TimeSpanToken tok = new TimeSpanToken();
691                 tok._ttt = TTT.Sep;
692                 if (!raw.ProcessToken(ref tok, ref result))
693                 {
694                     return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
695                 }
696             }
697 
698             switch (raw._numCount)
699             {
700                 case 1: return ProcessTerminal_D(ref raw, style, ref result);
701                 case 2: return ProcessTerminal_HM(ref raw, style, ref result);
702                 case 3: return ProcessTerminal_HM_S_D(ref raw, style, ref result);
703                 case 4: return ProcessTerminal_HMS_F_D(ref raw, style, ref result);
704                 case 5: return ProcessTerminal_DHMSF(ref raw, style, ref result);
705                 default: return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
706             }
707         }
708 
709         /// <summary>Validate the 5-number "Days.Hours:Minutes:Seconds.Fraction" terminal case.</summary>
ProcessTerminal_DHMSF(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)710         private static bool ProcessTerminal_DHMSF(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)
711         {
712             if (raw._sepCount != 6)
713             {
714                 return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
715             }
716             Debug.Assert(raw._numCount == 5);
717 
718             bool inv = (style & TimeSpanStandardStyles.Invariant) != 0;
719             bool loc = (style & TimeSpanStandardStyles.Localized) != 0;
720             bool positive = false;
721             bool match = false;
722 
723             if (inv)
724             {
725                 if (raw.FullMatch(raw.PositiveInvariant))
726                 {
727                     match = true;
728                     positive = true;
729                 }
730                 if (!match && raw.FullMatch(raw.NegativeInvariant))
731                 {
732                     match = true;
733                     positive = false;
734                 }
735             }
736 
737             if (loc)
738             {
739                 if (!match && raw.FullMatch(raw.PositiveLocalized))
740                 {
741                     match = true;
742                     positive = true;
743                 }
744                 if (!match && raw.FullMatch(raw.NegativeLocalized))
745                 {
746                     match = true;
747                     positive = false;
748                 }
749             }
750 
751             if (match)
752             {
753                 long ticks;
754 
755                 if (!TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, raw._numbers4, out ticks))
756                 {
757                     return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
758                 }
759 
760                 if (!positive)
761                 {
762                     ticks = -ticks;
763                     if (ticks > 0)
764                     {
765                         return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
766                     }
767                 }
768 
769                 result.parsedTimeSpan._ticks = ticks;
770                 return true;
771             }
772 
773             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
774         }
775 
776 
777         /// <summary>
778         /// Validate the ambiguous 4-number "Hours:Minutes:Seconds.Fraction", "Days.Hours:Minutes:Seconds",
779         /// or "Days.Hours:Minutes:.Fraction" terminal case.
780         /// </summary>
ProcessTerminal_HMS_F_D(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)781         private static bool ProcessTerminal_HMS_F_D(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)
782         {
783             if (raw._sepCount != 5 || (style & TimeSpanStandardStyles.RequireFull) != 0)
784             {
785                 return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
786             }
787             Debug.Assert(raw._numCount == 4);
788 
789             bool inv = ((style & TimeSpanStandardStyles.Invariant) != 0);
790             bool loc = ((style & TimeSpanStandardStyles.Localized) != 0);
791 
792             long ticks = 0;
793             bool positive = false, match = false, overflow = false;
794             var zero = new TimeSpanToken(0);
795 
796             if (inv)
797             {
798                 if (raw.FullHMSFMatch(raw.PositiveInvariant))
799                 {
800                     positive = true;
801                     match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, out ticks);
802                     overflow = overflow || !match;
803                 }
804 
805                 if (!match && raw.FullDHMSMatch(raw.PositiveInvariant))
806                 {
807                     positive = true;
808                     match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, zero, out ticks);
809                     overflow = overflow || !match;
810                 }
811 
812                 if (!match && raw.FullAppCompatMatch(raw.PositiveInvariant))
813                 {
814                     positive = true;
815                     match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, raw._numbers3, out ticks);
816                     overflow = overflow || !match;
817                 }
818 
819                 if (!match && raw.FullHMSFMatch(raw.NegativeInvariant))
820                 {
821                     positive = false;
822                     match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, out ticks);
823                     overflow = overflow || !match;
824                 }
825 
826                 if (!match && raw.FullDHMSMatch(raw.NegativeInvariant))
827                 {
828                     positive = false;
829                     match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, zero, out ticks);
830                     overflow = overflow || !match;
831                 }
832 
833                 if (!match && raw.FullAppCompatMatch(raw.NegativeInvariant))
834                 {
835                     positive = false;
836                     match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, raw._numbers3, out ticks);
837                     overflow = overflow || !match;
838                 }
839             }
840 
841             if (loc)
842             {
843                 if (!match && raw.FullHMSFMatch(raw.PositiveLocalized))
844                 {
845                     positive = true;
846                     match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, out ticks);
847                     overflow = overflow || !match;
848                 }
849 
850                 if (!match && raw.FullDHMSMatch(raw.PositiveLocalized))
851                 {
852                     positive = true;
853                     match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, zero, out ticks);
854                     overflow = overflow || !match;
855                 }
856 
857                 if (!match && raw.FullAppCompatMatch(raw.PositiveLocalized))
858                 {
859                     positive = true;
860                     match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, raw._numbers3, out ticks);
861                     overflow = overflow || !match;
862                 }
863 
864                 if (!match && raw.FullHMSFMatch(raw.NegativeLocalized))
865                 {
866                     positive = false;
867                     match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, out ticks);
868                     overflow = overflow || !match;
869                 }
870 
871                 if (!match && raw.FullDHMSMatch(raw.NegativeLocalized))
872                 {
873                     positive = false;
874                     match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, zero, out ticks);
875                     overflow = overflow || !match;
876                 }
877 
878                 if (!match && raw.FullAppCompatMatch(raw.NegativeLocalized))
879                 {
880                     positive = false;
881                     match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, raw._numbers3, out ticks);
882                     overflow = overflow || !match;
883                 }
884             }
885 
886             if (match)
887             {
888                 if (!positive)
889                 {
890                     ticks = -ticks;
891                     if (ticks > 0)
892                     {
893                         return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
894                     }
895                 }
896 
897                 result.parsedTimeSpan._ticks = ticks;
898                 return true;
899             }
900 
901             return overflow ?
902                 result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge)) : // we found at least one literal pattern match but the numbers just didn't fit
903                 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan)); // we couldn't find a thing
904         }
905 
906         /// <summary>Validate the ambiguous 3-number "Hours:Minutes:Seconds", "Days.Hours:Minutes", or "Hours:Minutes:.Fraction" terminal case.</summary>
ProcessTerminal_HM_S_D(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)907         private static bool ProcessTerminal_HM_S_D(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)
908         {
909             if (raw._sepCount != 4 || (style & TimeSpanStandardStyles.RequireFull) != 0)
910             {
911                 return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
912             }
913             Debug.Assert(raw._numCount == 3);
914 
915             bool inv = ((style & TimeSpanStandardStyles.Invariant) != 0);
916             bool loc = ((style & TimeSpanStandardStyles.Localized) != 0);
917 
918             bool positive = false, match = false, overflow = false;
919             var zero = new TimeSpanToken(0);
920             long ticks = 0;
921 
922             if (inv)
923             {
924                 if (raw.FullHMSMatch(raw.PositiveInvariant))
925                 {
926                     positive = true;
927                     match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, zero, out ticks);
928                     overflow = overflow || !match;
929                 }
930 
931                 if (!match && raw.FullDHMMatch(raw.PositiveInvariant))
932                 {
933                     positive = true;
934                     match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, zero, out ticks);
935                     overflow = overflow || !match;
936                 }
937 
938                 if (!match && raw.PartialAppCompatMatch(raw.PositiveInvariant))
939                 {
940                     positive = true;
941                     match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, zero, raw._numbers2, out ticks);
942                     overflow = overflow || !match;
943                 }
944 
945                 if (!match && raw.FullHMSMatch(raw.NegativeInvariant))
946                 {
947                     positive = false;
948                     match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, zero, out ticks);
949                     overflow = overflow || !match;
950                 }
951 
952                 if (!match && raw.FullDHMMatch(raw.NegativeInvariant))
953                 {
954                     positive = false;
955                     match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, zero, out ticks);
956                     overflow = overflow || !match;
957                 }
958 
959                 if (!match && raw.PartialAppCompatMatch(raw.NegativeInvariant))
960                 {
961                     positive = false;
962                     match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, zero, raw._numbers2, out ticks);
963                     overflow = overflow || !match;
964                 }
965             }
966 
967             if (loc)
968             {
969                 if (!match && raw.FullHMSMatch(raw.PositiveLocalized))
970                 {
971                     positive = true;
972                     match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, zero, out ticks);
973                     overflow = overflow || !match;
974                 }
975 
976                 if (!match && raw.FullDHMMatch(raw.PositiveLocalized))
977                 {
978                     positive = true;
979                     match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, zero, out ticks);
980                     overflow = overflow || !match;
981                 }
982 
983                 if (!match && raw.PartialAppCompatMatch(raw.PositiveLocalized))
984                 {
985                     positive = true;
986                     match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, zero, raw._numbers2, out ticks);
987                     overflow = overflow || !match;
988                 }
989 
990                 if (!match && raw.FullHMSMatch(raw.NegativeLocalized))
991                 {
992                     positive = false;
993                     match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, zero, out ticks);
994                     overflow = overflow || !match;
995                 }
996 
997                 if (!match && raw.FullDHMMatch(raw.NegativeLocalized))
998                 {
999                     positive = false;
1000                     match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, zero, out ticks);
1001                     overflow = overflow || !match;
1002                 }
1003 
1004                 if (!match && raw.PartialAppCompatMatch(raw.NegativeLocalized))
1005                 {
1006                     positive = false;
1007                     match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, zero, raw._numbers2, out ticks);
1008                     overflow = overflow || !match;
1009                 }
1010             }
1011 
1012             if (match)
1013             {
1014                 if (!positive)
1015                 {
1016                     ticks = -ticks;
1017                     if (ticks > 0)
1018                     {
1019                         return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
1020                     }
1021                 }
1022 
1023                 result.parsedTimeSpan._ticks = ticks;
1024                 return true;
1025             }
1026 
1027             return overflow ?
1028                 result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge)) : // we found at least one literal pattern match but the numbers just didn't fit
1029                 result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan)); // we couldn't find a thing
1030         }
1031 
1032         /// <summary>Validate the 2-number "Hours:Minutes" terminal case.</summary>
ProcessTerminal_HM(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)1033         private static bool ProcessTerminal_HM(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)
1034         {
1035             if (raw._sepCount != 3 || (style & TimeSpanStandardStyles.RequireFull) != 0)
1036             {
1037                 return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
1038             }
1039             Debug.Assert(raw._numCount == 2);
1040 
1041             bool inv = ((style & TimeSpanStandardStyles.Invariant) != 0);
1042             bool loc = ((style & TimeSpanStandardStyles.Localized) != 0);
1043 
1044             bool positive = false, match = false;
1045 
1046             if (inv)
1047             {
1048                 if (raw.FullHMMatch(raw.PositiveInvariant))
1049                 {
1050                     match = true;
1051                     positive = true;
1052                 }
1053 
1054                 if (!match && raw.FullHMMatch(raw.NegativeInvariant))
1055                 {
1056                     match = true;
1057                     positive = false;
1058                 }
1059             }
1060 
1061             if (loc)
1062             {
1063                 if (!match && raw.FullHMMatch(raw.PositiveLocalized))
1064                 {
1065                     match = true;
1066                     positive = true;
1067                 }
1068 
1069                 if (!match && raw.FullHMMatch(raw.NegativeLocalized))
1070                 {
1071                     match = true;
1072                     positive = false;
1073                 }
1074             }
1075 
1076             if (match)
1077             {
1078                 long ticks = 0;
1079                 var zero = new TimeSpanToken(0);
1080 
1081                 if (!TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, zero, zero, out ticks))
1082                 {
1083                     return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
1084                 }
1085 
1086                 if (!positive)
1087                 {
1088                     ticks = -ticks;
1089                     if (ticks > 0)
1090                     {
1091                         return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
1092                     }
1093                 }
1094 
1095                 result.parsedTimeSpan._ticks = ticks;
1096                 return true;
1097             }
1098 
1099             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
1100         }
1101 
1102         /// <summary>Validate the 1-number "Days" terminal case.</summary>
ProcessTerminal_D(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)1103         private static bool ProcessTerminal_D(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)
1104         {
1105             if (raw._sepCount != 2 || (style & TimeSpanStandardStyles.RequireFull) != 0)
1106             {
1107                 return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
1108             }
1109             Debug.Assert(raw._numCount == 1);
1110 
1111             bool inv = ((style & TimeSpanStandardStyles.Invariant) != 0);
1112             bool loc = ((style & TimeSpanStandardStyles.Localized) != 0);
1113 
1114             bool positive = false, match = false;
1115 
1116             if (inv)
1117             {
1118                 if (raw.FullDMatch(raw.PositiveInvariant))
1119                 {
1120                     match = true;
1121                     positive = true;
1122                 }
1123 
1124                 if (!match && raw.FullDMatch(raw.NegativeInvariant))
1125                 {
1126                     match = true;
1127                     positive = false;
1128                 }
1129             }
1130 
1131             if (loc)
1132             {
1133                 if (!match && raw.FullDMatch(raw.PositiveLocalized))
1134                 {
1135                     match = true;
1136                     positive = true;
1137                 }
1138 
1139                 if (!match && raw.FullDMatch(raw.NegativeLocalized))
1140                 {
1141                     match = true;
1142                     positive = false;
1143                 }
1144             }
1145 
1146             if (match)
1147             {
1148                 long ticks = 0;
1149                 var zero = new TimeSpanToken(0);
1150 
1151                 if (!TryTimeToTicks(positive, raw._numbers0, zero, zero, zero, zero, out ticks))
1152                 {
1153                     return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
1154                 }
1155 
1156                 if (!positive)
1157                 {
1158                     ticks = -ticks;
1159                     if (ticks > 0)
1160                     {
1161                         return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
1162                     }
1163                 }
1164 
1165                 result.parsedTimeSpan._ticks = ticks;
1166                 return true;
1167             }
1168 
1169             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
1170         }
1171 
1172         /// <summary>Common private ParseExact method called by both ParseExact and TryParseExact.</summary>
TryParseExactTimeSpan(ReadOnlySpan<char> input, ReadOnlySpan<char> format, IFormatProvider formatProvider, TimeSpanStyles styles, ref TimeSpanResult result)1173         private static bool TryParseExactTimeSpan(ReadOnlySpan<char> input, ReadOnlySpan<char> format, IFormatProvider formatProvider, TimeSpanStyles styles, ref TimeSpanResult result)
1174         {
1175             if (format.Length == 0)
1176             {
1177                 return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier));
1178             }
1179 
1180             if (format.Length == 1)
1181             {
1182                 switch (format[0])
1183                 {
1184                     case 'c':
1185                     case 't':
1186                     case 'T':
1187                         return TryParseTimeSpanConstant(input, ref result); // fast path for legacy style TimeSpan formats.
1188 
1189                     case 'g':
1190                         return TryParseTimeSpan(input, TimeSpanStandardStyles.Localized, formatProvider, ref result);
1191 
1192                     case 'G':
1193                         return TryParseTimeSpan(input, TimeSpanStandardStyles.Localized | TimeSpanStandardStyles.RequireFull, formatProvider, ref result);
1194 
1195                     default:
1196                         return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier));
1197                 }
1198             }
1199 
1200             return TryParseByFormat(input, format, styles, ref result);
1201         }
1202 
1203         /// <summary>Parse the TimeSpan instance using the specified format.  Used by TryParseExactTimeSpan.</summary>
TryParseByFormat(ReadOnlySpan<char> input, ReadOnlySpan<char> format, TimeSpanStyles styles, ref TimeSpanResult result)1204         private static bool TryParseByFormat(ReadOnlySpan<char> input, ReadOnlySpan<char> format, TimeSpanStyles styles, ref TimeSpanResult result)
1205         {
1206             bool seenDD = false;      // already processed days?
1207             bool seenHH = false;      // already processed hours?
1208             bool seenMM = false;      // already processed minutes?
1209             bool seenSS = false;      // already processed seconds?
1210             bool seenFF = false;      // already processed fraction?
1211 
1212             int dd = 0;               // parsed days
1213             int hh = 0;               // parsed hours
1214             int mm = 0;               // parsed minutes
1215             int ss = 0;               // parsed seconds
1216             int leadingZeroes = 0;    // number of leading zeroes in the parsed fraction
1217             int ff = 0;               // parsed fraction
1218             int i = 0;                // format string position
1219             int tokenLen = 0;         // length of current format token, used to update index 'i'
1220 
1221             var tokenizer = new TimeSpanTokenizer(input, -1);
1222 
1223             while (i < format.Length)
1224             {
1225                 char ch = format[i];
1226                 int nextFormatChar;
1227                 switch (ch)
1228                 {
1229                     case 'h':
1230                         tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
1231                         if (tokenLen > 2 || seenHH || !ParseExactDigits(ref tokenizer, tokenLen, out hh))
1232                         {
1233                             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_InvalidString));
1234                         }
1235                         seenHH = true;
1236                         break;
1237 
1238                     case 'm':
1239                         tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
1240                         if (tokenLen > 2 || seenMM || !ParseExactDigits(ref tokenizer, tokenLen, out mm))
1241                         {
1242                             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_InvalidString));
1243                         }
1244                         seenMM = true;
1245                         break;
1246 
1247                     case 's':
1248                         tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
1249                         if (tokenLen > 2 || seenSS || !ParseExactDigits(ref tokenizer, tokenLen, out ss))
1250                         {
1251                             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_InvalidString));
1252                         }
1253                         seenSS = true;
1254                         break;
1255 
1256                     case 'f':
1257                         tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
1258                         if (tokenLen > DateTimeFormat.MaxSecondsFractionDigits || seenFF || !ParseExactDigits(ref tokenizer, tokenLen, tokenLen, out leadingZeroes, out ff))
1259                         {
1260                             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_InvalidString));
1261                         }
1262                         seenFF = true;
1263                         break;
1264 
1265                     case 'F':
1266                         tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
1267                         if (tokenLen > DateTimeFormat.MaxSecondsFractionDigits || seenFF)
1268                         {
1269                             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_InvalidString));
1270                         }
1271                         ParseExactDigits(ref tokenizer, tokenLen, tokenLen, out leadingZeroes, out ff);
1272                         seenFF = true;
1273                         break;
1274 
1275                     case 'd':
1276                         tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
1277                         int tmp = 0;
1278                         if (tokenLen > 8 || seenDD || !ParseExactDigits(ref tokenizer, (tokenLen < 2) ? 1 : tokenLen, (tokenLen < 2) ? 8 : tokenLen, out tmp, out dd))
1279                         {
1280                             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_InvalidString));
1281                         }
1282                         seenDD = true;
1283                         break;
1284 
1285                     case '\'':
1286                     case '\"':
1287                         StringBuilder enquotedString = StringBuilderCache.Acquire();
1288                         if (!DateTimeParse.TryParseQuoteString(format, i, enquotedString, out tokenLen))
1289                         {
1290                             StringBuilderCache.Release(enquotedString);
1291                             return result.SetFailure(ParseFailureKind.FormatWithParameter, nameof(SR.Format_BadQuote), ch);
1292                         }
1293                         if (!ParseExactLiteral(ref tokenizer, enquotedString))
1294                         {
1295                             StringBuilderCache.Release(enquotedString);
1296                             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_InvalidString));
1297                         }
1298                         StringBuilderCache.Release(enquotedString);
1299                         break;
1300 
1301                     case '%':
1302                         // Optional format character.
1303                         // For example, format string "%d" will print day
1304                         // Most of the cases, "%" can be ignored.
1305                         nextFormatChar = DateTimeFormat.ParseNextChar(format, i);
1306 
1307                         // nextFormatChar will be -1 if we already reach the end of the format string.
1308                         // Besides, we will not allow "%%" appear in the pattern.
1309                         if (nextFormatChar >= 0 && nextFormatChar != '%')
1310                         {
1311                             tokenLen = 1; // skip the '%' and process the format character
1312                             break;
1313                         }
1314                         else
1315                         {
1316                             // This means that '%' is at the end of the format string or
1317                             // "%%" appears in the format string.
1318                             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_InvalidString));
1319                         }
1320 
1321                     case '\\':
1322                         // Escaped character.  Can be used to insert character into the format string.
1323                         // For example, "\d" will insert the character 'd' into the string.
1324                         //
1325                         nextFormatChar = DateTimeFormat.ParseNextChar(format, i);
1326                         if (nextFormatChar >= 0 && tokenizer.NextChar == (char)nextFormatChar)
1327                         {
1328                             tokenLen = 2;
1329                         }
1330                         else
1331                         {
1332                             // This means that '\' is at the end of the format string or the literal match failed.
1333                             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_InvalidString));
1334                         }
1335                         break;
1336 
1337                     default:
1338                         return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_InvalidString));
1339                 }
1340 
1341                 i += tokenLen;
1342             }
1343 
1344 
1345             if (!tokenizer.EOL)
1346             {
1347                 // the custom format didn't consume the entire input
1348                 return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
1349             }
1350 
1351             bool positive = (styles & TimeSpanStyles.AssumeNegative) == 0;
1352             if (TryTimeToTicks(positive, new TimeSpanToken(dd),
1353                                          new TimeSpanToken(hh),
1354                                          new TimeSpanToken(mm),
1355                                          new TimeSpanToken(ss),
1356                                          new TimeSpanToken(ff, leadingZeroes),
1357                                          out long ticks))
1358             {
1359                 if (!positive)
1360                 {
1361                     ticks = -ticks;
1362                 }
1363 
1364                 result.parsedTimeSpan._ticks = ticks;
1365                 return true;
1366             }
1367             else
1368             {
1369                 return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
1370             }
1371         }
1372 
ParseExactDigits(ref TimeSpanTokenizer tokenizer, int minDigitLength, out int result)1373         private static bool ParseExactDigits(ref TimeSpanTokenizer tokenizer, int minDigitLength, out int result)
1374         {
1375             result = 0;
1376             int zeroes = 0;
1377             int maxDigitLength = (minDigitLength == 1) ? 2 : minDigitLength;
1378             return ParseExactDigits(ref tokenizer, minDigitLength, maxDigitLength, out zeroes, out result);
1379         }
1380 
ParseExactDigits(ref TimeSpanTokenizer tokenizer, int minDigitLength, int maxDigitLength, out int zeroes, out int result)1381         private static bool ParseExactDigits(ref TimeSpanTokenizer tokenizer, int minDigitLength, int maxDigitLength, out int zeroes, out int result)
1382         {
1383             int tmpResult = 0, tmpZeroes = 0;
1384 
1385             int tokenLength = 0;
1386             while (tokenLength < maxDigitLength)
1387             {
1388                 char ch = tokenizer.NextChar;
1389                 if (ch < '0' || ch > '9')
1390                 {
1391                     tokenizer.BackOne();
1392                     break;
1393                 }
1394 
1395                 tmpResult = tmpResult * 10 + (ch - '0');
1396                 if (tmpResult == 0) tmpZeroes++;
1397                 tokenLength++;
1398             }
1399 
1400             zeroes = tmpZeroes;
1401             result = tmpResult;
1402             return tokenLength >= minDigitLength;
1403         }
1404 
ParseExactLiteral(ref TimeSpanTokenizer tokenizer, StringBuilder enquotedString)1405         private static bool ParseExactLiteral(ref TimeSpanTokenizer tokenizer, StringBuilder enquotedString)
1406         {
1407             for (int i = 0; i < enquotedString.Length; i++)
1408             {
1409                 if (enquotedString[i] != tokenizer.NextChar)
1410                 {
1411                     return false;
1412                 }
1413             }
1414 
1415             return true;
1416         }
1417 
1418         /// <summary>
1419         /// Parses the "c" (constant) format.  This code is 100% identical to the non-globalized v1.0-v3.5 TimeSpan.Parse() routine
1420         /// and exists for performance/appcompat with legacy callers who cannot move onto the globalized Parse overloads.
1421         /// </summary>
TryParseTimeSpanConstant(ReadOnlySpan<char> input, ref TimeSpanResult result)1422         private static bool TryParseTimeSpanConstant(ReadOnlySpan<char> input, ref TimeSpanResult result) =>
1423             new StringParser().TryParse(input, ref result);
1424 
1425         private ref struct StringParser
1426         {
1427             private ReadOnlySpan<char> _str;
1428             private char _ch;
1429             private int _pos;
1430             private int _len;
1431 
NextCharSystem.Globalization.TimeSpanParse.StringParser1432             internal void NextChar()
1433             {
1434                 if (_pos < _len)
1435                 {
1436                     _pos++;
1437                 }
1438 
1439                 _ch = _pos < _len ?
1440                     _str[_pos] :
1441                     (char)0;
1442             }
1443 
NextNonDigitSystem.Globalization.TimeSpanParse.StringParser1444             internal char NextNonDigit()
1445             {
1446                 int i = _pos;
1447                 while (i < _len)
1448                 {
1449                     char ch = _str[i];
1450                     if (ch < '0' || ch > '9') return ch;
1451                     i++;
1452                 }
1453 
1454                 return (char)0;
1455             }
1456 
TryParseSystem.Globalization.TimeSpanParse.StringParser1457             internal bool TryParse(ReadOnlySpan<char> input, ref TimeSpanResult result)
1458             {
1459                 result.parsedTimeSpan._ticks = 0;
1460 
1461                 _str = input;
1462                 _len = input.Length;
1463                 _pos = -1;
1464                 NextChar();
1465                 SkipBlanks();
1466 
1467                 bool negative = false;
1468                 if (_ch == '-')
1469                 {
1470                     negative = true;
1471                     NextChar();
1472                 }
1473 
1474                 long time;
1475                 if (NextNonDigit() == ':')
1476                 {
1477                     if (!ParseTime(out time, ref result))
1478                     {
1479                         return false;
1480                     };
1481                 }
1482                 else
1483                 {
1484                     int days;
1485                     if (!ParseInt((int)(0x7FFFFFFFFFFFFFFFL / TimeSpan.TicksPerDay), out days, ref result))
1486                     {
1487                         return false;
1488                     }
1489 
1490                     time = days * TimeSpan.TicksPerDay;
1491 
1492                     if (_ch == '.')
1493                     {
1494                         NextChar();
1495                         long remainingTime;
1496                         if (!ParseTime(out remainingTime, ref result))
1497                         {
1498                             return false;
1499                         };
1500                         time += remainingTime;
1501                     }
1502                 }
1503 
1504                 if (negative)
1505                 {
1506                     time = -time;
1507                     // Allow -0 as well
1508                     if (time > 0)
1509                     {
1510                         return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
1511                     }
1512                 }
1513                 else
1514                 {
1515                     if (time < 0)
1516                     {
1517                         return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
1518                     }
1519                 }
1520 
1521                 SkipBlanks();
1522 
1523                 if (_pos < _len)
1524                 {
1525                     return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
1526                 }
1527 
1528                 result.parsedTimeSpan._ticks = time;
1529                 return true;
1530             }
1531 
ParseIntSystem.Globalization.TimeSpanParse.StringParser1532             internal bool ParseInt(int max, out int i, ref TimeSpanResult result)
1533             {
1534                 i = 0;
1535                 int p = _pos;
1536                 while (_ch >= '0' && _ch <= '9')
1537                 {
1538                     if ((i & 0xF0000000) != 0)
1539                     {
1540                         return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
1541                     }
1542 
1543                     i = i * 10 + _ch - '0';
1544                     if (i < 0)
1545                     {
1546                         return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
1547                     }
1548 
1549                     NextChar();
1550                 }
1551 
1552                 if (p == _pos)
1553                 {
1554                     return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
1555                 }
1556 
1557                 if (i > max)
1558                 {
1559                     return result.SetFailure(ParseFailureKind.Overflow, nameof(SR.Overflow_TimeSpanElementTooLarge));
1560                 }
1561 
1562                 return true;
1563             }
1564 
ParseTimeSystem.Globalization.TimeSpanParse.StringParser1565             internal bool ParseTime(out long time, ref TimeSpanResult result)
1566             {
1567                 time = 0;
1568                 int unit;
1569 
1570                 if (!ParseInt(23, out unit, ref result))
1571                 {
1572                     return false;
1573                 }
1574 
1575                 time = unit * TimeSpan.TicksPerHour;
1576                 if (_ch != ':')
1577                 {
1578                     return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
1579                 }
1580 
1581                 NextChar();
1582 
1583                 if (!ParseInt(59, out unit, ref result))
1584                 {
1585                     return false;
1586                 }
1587 
1588                 time += unit * TimeSpan.TicksPerMinute;
1589 
1590                 if (_ch == ':')
1591                 {
1592                     NextChar();
1593 
1594                     // allow seconds with the leading zero
1595                     if (_ch != '.')
1596                     {
1597                         if (!ParseInt(59, out unit, ref result))
1598                         {
1599                             return false;
1600                         }
1601                         time += unit * TimeSpan.TicksPerSecond;
1602                     }
1603 
1604                     if (_ch == '.')
1605                     {
1606                         NextChar();
1607                         int f = (int)TimeSpan.TicksPerSecond;
1608                         while (f > 1 && _ch >= '0' && _ch <= '9')
1609                         {
1610                             f /= 10;
1611                             time += (_ch - '0') * f;
1612                             NextChar();
1613                         }
1614                     }
1615                 }
1616 
1617                 return true;
1618             }
1619 
SkipBlanksSystem.Globalization.TimeSpanParse.StringParser1620             internal void SkipBlanks()
1621             {
1622                 while (_ch == ' ' || _ch == '\t') NextChar();
1623             }
1624         }
1625 
1626         /// <summary>Common private ParseExactMultiple method called by both ParseExactMultiple and TryParseExactMultiple.</summary>
TryParseExactMultipleTimeSpan(ReadOnlySpan<char> input, string[] formats, IFormatProvider formatProvider, TimeSpanStyles styles, ref TimeSpanResult result)1627         private static bool TryParseExactMultipleTimeSpan(ReadOnlySpan<char> input, string[] formats, IFormatProvider formatProvider, TimeSpanStyles styles, ref TimeSpanResult result)
1628         {
1629             if (formats == null)
1630             {
1631                 return result.SetFailure(ParseFailureKind.ArgumentNull, nameof(SR.ArgumentNull_String), argumentName: nameof(formats));
1632             }
1633 
1634             if (input.Length == 0)
1635             {
1636                 return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
1637             }
1638 
1639             if (formats.Length == 0)
1640             {
1641                 return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier));
1642             }
1643 
1644             // Do a loop through the provided formats and see if we can parse succesfully in
1645             // one of the formats.
1646             for (int i = 0; i < formats.Length; i++)
1647             {
1648                 if (formats[i] == null || formats[i].Length == 0)
1649                 {
1650                     return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadFormatSpecifier));
1651                 }
1652 
1653                 // Create a new non-throwing result each time to ensure the runs are independent.
1654                 TimeSpanResult innerResult = new TimeSpanResult(throwOnFailure: false);
1655 
1656                 if (TryParseExactTimeSpan(input, formats[i], formatProvider, styles, ref innerResult))
1657                 {
1658                     result.parsedTimeSpan = innerResult.parsedTimeSpan;
1659                     return true;
1660                 }
1661             }
1662 
1663             return result.SetFailure(ParseFailureKind.Format, nameof(SR.Format_BadTimeSpan));
1664         }
1665     }
1666 }
1667