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