|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
////////////////////////////////////////////////////////////////////////////
//
// Purpose: Used by TimeSpan to parse a time interval string.
//
// Standard Format:
// -=-=-=-=-=-=-=-
// "c": Constant format. [-][d'.']hh':'mm':'ss['.'fffffff]
// Not culture sensitive. Default format (and null/empty format string) map to this format.
//
// "g": General format, short: [-][d':']h':'mm':'ss'.'FFFFFFF
// Only print what's needed. Localized (if you want Invariant, pass in Invariant).
// The fractional seconds separator is localized, equal to the culture's DecimalSeparator.
//
// "G": General format, long: [-]d':'hh':'mm':'ss'.'fffffff
// Always print days and 7 fractional digits. Localized (if you want Invariant, pass in Invariant).
// The fractional seconds separator is localized, equal to the culture's DecimalSeparator.
//
// * "TryParseTimeSpan" is the main method for Parse/TryParse
//
// - TimeSpanTokenizer.GetNextToken() is used to split the input string into number and literal tokens.
// - TimeSpanRawInfo.ProcessToken() adds the next token into the parsing intermediary state structure
// - ProcessTerminalState() uses the fully initialized TimeSpanRawInfo to find a legal parse match.
// The terminal states are attempted as follows:
// foreach (+InvariantPattern, -InvariantPattern, +LocalizedPattern, -LocalizedPattern) try
// 1 number => d
// 2 numbers => h:m
// 3 numbers => h:m:s | d.h:m | h:m:.f
// 4 numbers => h:m:s.f | d.h:m:s | d.h:m:.f
// 5 numbers => d.h:m:s.f
//
// Custom Format:
// -=-=-=-=-=-=-=
//
// * "TryParseExactTimeSpan" is the main method for ParseExact/TryParseExact methods
// * "TryParseExactMultipleTimeSpan" is the main method for ParseExact/TryparseExact
// methods that take a string[] of formats
//
// - For single-letter formats "TryParseTimeSpan" is called (see above)
// - For multi-letter formats "TryParseByFormat" is called
// - TryParseByFormat uses helper methods (ParseExactLiteral, ParseExactDigits, etc)
// which drive the underlying TimeSpanTokenizer. However, unlike standard formatting which
// operates on whole-tokens, ParseExact operates at the character-level. As such,
// TimeSpanTokenizer.NextChar and TimeSpanTokenizer.BackOne() are called directly.
//
////////////////////////////////////////////////////////////////////////////
using System.Buffers.Text;
using System.Diagnostics;
using System.Text;
namespace System.Globalization
{
internal static class TimeSpanParse
{
private const int MaxFractionDigits = 7;
private const int MaxDays = 10675199;
private const int MaxHours = 23;
private const int MaxMinutes = 59;
private const int MaxSeconds = 59;
private const int MaxFraction = 9999999;
[Flags]
private enum TimeSpanStandardStyles : byte
{
// Standard Format Styles
None = 0x00000000,
Invariant = 0x00000001, // Allow Invariant Culture
Localized = 0x00000002, // Allow Localized Culture
RequireFull = 0x00000004, // Require the input to be in DHMSF format
Any = Invariant | Localized,
}
// TimeSpan Token Types
private enum TTT : byte
{
None = 0, // None of the TimeSpanToken fields are set
End = 1, // '\0'
Num = 2, // Number
Sep = 3, // literal
NumOverflow = 4, // Number that overflowed
}
private ref struct TimeSpanToken
{
internal TTT _ttt;
internal int _num; // Store the number that we are parsing (if any)
internal int _zeroes; // Store the number of leading zeroes (if any)
internal ReadOnlySpan<char> _sep; // Store the literal that we are parsing (if any)
public TimeSpanToken(TTT type) : this(type, 0, 0, default) { }
public TimeSpanToken(int number) : this(TTT.Num, number, 0, default) { }
public TimeSpanToken(int number, int leadingZeroes) : this(TTT.Num, number, leadingZeroes, default) { }
public TimeSpanToken(TTT type, int number, int leadingZeroes, ReadOnlySpan<char> separator)
{
_ttt = type;
_num = number;
_zeroes = leadingZeroes;
_sep = separator;
}
public bool NormalizeAndValidateFraction()
{
Debug.Assert(_ttt == TTT.Num);
Debug.Assert(_num > -1);
if (_num == 0)
return true;
if (_zeroes == 0 && _num > MaxFraction)
return false;
int totalDigitsCount = FormattingHelpers.CountDigits((uint)_num) + _zeroes;
if (totalDigitsCount == MaxFractionDigits)
{
// Already normalized. no more action needed
// .9999999 normalize to 9,999,999 ticks
// .0000001 normalize to 1 ticks
return true;
}
if (totalDigitsCount < MaxFractionDigits)
{
// normalize the fraction to the 7-digits
// .999999 normalize to 9,999,990 ticks
// .99999 normalize to 9,999,900 ticks
// .000001 normalize to 10 ticks
// .1 normalize to 1,000,000 ticks
_num *= Pow10UpToMaxFractionDigits(MaxFractionDigits - totalDigitsCount);
return true;
}
// totalDigitsCount is greater then MaxFractionDigits, we'll need to do the rounding to 7-digits length
// .00000001 normalized to 0 ticks
// .00000005 normalized to 1 ticks
// .09999999 normalize to 1,000,000 ticks
// .099999999 normalize to 1,000,000 ticks
Debug.Assert(_zeroes > 0); // Already validated that in the condition _zeroes == 0 && _num > MaxFraction
if (_zeroes > MaxFractionDigits)
{
// If there are 8 leading zeroes, it rounds to zero
_num = 0;
return true;
}
Debug.Assert(totalDigitsCount - MaxFractionDigits <= MaxFractionDigits);
uint power = (uint)Pow10UpToMaxFractionDigits(totalDigitsCount - MaxFractionDigits);
// Unsigned integer division, rounding away from zero
_num = (int)(((uint)_num + power / 2) / power);
Debug.Assert(_num < MaxFraction);
return true;
}
}
private ref struct TimeSpanTokenizer
{
private readonly ReadOnlySpan<char> _value;
private int _pos;
internal TimeSpanTokenizer(ReadOnlySpan<char> input) : this(input, 0) { }
internal TimeSpanTokenizer(ReadOnlySpan<char> input, int startPosition)
{
_value = input;
_pos = startPosition;
}
/// <summary>Returns the next token in the input string</summary>
/// <remarks>Used by the parsing routines that operate on standard-formats.</remarks>
internal TimeSpanToken GetNextToken()
{
// Get the position of the next character to be processed. If there is no
// next character, we're at the end.
int startPos = _pos;
int pos = startPos;
ReadOnlySpan<char> value = _value;
Debug.Assert(pos > -1);
if ((uint)pos >= (uint)value.Length)
{
return new TimeSpanToken(TTT.End);
}
// Now retrieve that character. If it's a digit, we're processing a number.
int num = value[pos] - '0';
if ((uint)num <= 9)
{
int zeroes = 0;
if (num == 0)
{
// Read all leading zeroes.
zeroes = 1;
while (true)
{
int digit;
if ((uint)++pos >= (uint)value.Length || (uint)(digit = value[pos] - '0') > 9)
{
_pos = pos;
return new TimeSpanToken(TTT.Num, 0, zeroes, default);
}
if (digit == 0)
{
zeroes++;
continue;
}
num = digit;
break;
}
_pos = pos;
}
// Continue to read as long as we're reading digits.
while ((uint)++pos < (uint)value.Length)
{
int digit = value[pos] - '0';
if ((uint)digit > 9)
{
break;
}
num = num * 10 + digit;
if ((num & 0xF0000000) != 0) // Max limit we can support 268435455 which is FFFFFFF
{
_pos = pos;
return new TimeSpanToken(TTT.NumOverflow);
}
}
_pos = pos;
return new TimeSpanToken(TTT.Num, num, zeroes, default);
}
// Otherwise, we're processing a separator, and we've already processed the first
// character of it. Continue processing characters as long as they're not digits.
int length = 1;
while ((uint)++pos < (uint)value.Length && !char.IsAsciiDigit(value[pos]))
{
length++;
}
_pos = pos;
// Return the separator.
return new TimeSpanToken(TTT.Sep, 0, 0, _value.Slice(startPos, length));
}
internal bool EOL => _pos >= (_value.Length - 1);
internal void BackOne()
{
if (_pos > 0) --_pos;
}
internal char NextChar()
{
int pos = ++_pos;
return (uint)pos < (uint)_value.Length ?
_value[pos] :
(char)0;
}
}
/// <summary>Stores intermediary parsing state for the standard formats.</summary>
private ref struct TimeSpanRawInfo
{
internal TimeSpanFormat.FormatLiterals PositiveLocalized
{
get
{
if (!_posLocInit)
{
_posLoc = default;
_posLoc.Init(_fullPosPattern, false);
_posLocInit = true;
}
return _posLoc;
}
}
internal TimeSpanFormat.FormatLiterals NegativeLocalized
{
get
{
if (!_negLocInit)
{
_negLoc = default;
_negLoc.Init(_fullNegPattern, false);
_negLocInit = true;
}
return _negLoc;
}
}
internal bool FullAppCompatMatch(TimeSpanFormat.FormatLiterals pattern) =>
_sepCount == 5
&& _numCount == 4
&& _literals0.EqualsOrdinal(pattern.Start)
&& _literals1.EqualsOrdinal(pattern.DayHourSep)
&& _literals2.EqualsOrdinal(pattern.HourMinuteSep)
&& _literals3.EqualsOrdinal(pattern.AppCompatLiteral)
&& _literals4.EqualsOrdinal(pattern.End);
internal bool PartialAppCompatMatch(TimeSpanFormat.FormatLiterals pattern) =>
_sepCount == 4
&& _numCount == 3
&& _literals0.EqualsOrdinal(pattern.Start)
&& _literals1.EqualsOrdinal(pattern.HourMinuteSep)
&& _literals2.EqualsOrdinal(pattern.AppCompatLiteral)
&& _literals3.EqualsOrdinal(pattern.End);
/// <summary>DHMSF (all values matched)</summary>
internal bool FullMatch(TimeSpanFormat.FormatLiterals pattern) =>
_sepCount == MaxLiteralTokens
&& _numCount == MaxNumericTokens
&& _literals0.EqualsOrdinal(pattern.Start)
&& _literals1.EqualsOrdinal(pattern.DayHourSep)
&& _literals2.EqualsOrdinal(pattern.HourMinuteSep)
&& _literals3.EqualsOrdinal(pattern.MinuteSecondSep)
&& _literals4.EqualsOrdinal(pattern.SecondFractionSep)
&& _literals5.EqualsOrdinal(pattern.End);
/// <summary>D (no hours, minutes, seconds, or fractions)</summary>
internal bool FullDMatch(TimeSpanFormat.FormatLiterals pattern) =>
_sepCount == 2
&& _numCount == 1
&& _literals0.EqualsOrdinal(pattern.Start)
&& _literals1.EqualsOrdinal(pattern.End);
/// <summary>HM (no days, seconds, or fractions)</summary>
internal bool FullHMMatch(TimeSpanFormat.FormatLiterals pattern) =>
_sepCount == 3
&& _numCount == 2
&& _literals0.EqualsOrdinal(pattern.Start)
&& _literals1.EqualsOrdinal(pattern.HourMinuteSep)
&& _literals2.EqualsOrdinal(pattern.End);
/// <summary>DHM (no seconds or fraction)</summary>
internal bool FullDHMMatch(TimeSpanFormat.FormatLiterals pattern) =>
_sepCount == 4
&& _numCount == 3
&& _literals0.EqualsOrdinal(pattern.Start)
&& _literals1.EqualsOrdinal(pattern.DayHourSep)
&& _literals2.EqualsOrdinal(pattern.HourMinuteSep)
&& _literals3.EqualsOrdinal(pattern.End);
/// <summary>HMS (no days or fraction)</summary>
internal bool FullHMSMatch(TimeSpanFormat.FormatLiterals pattern) =>
_sepCount == 4
&& _numCount == 3
&& _literals0.EqualsOrdinal(pattern.Start)
&& _literals1.EqualsOrdinal(pattern.HourMinuteSep)
&& _literals2.EqualsOrdinal(pattern.MinuteSecondSep)
&& _literals3.EqualsOrdinal(pattern.End);
/// <summary>DHMS (no fraction)</summary>
internal bool FullDHMSMatch(TimeSpanFormat.FormatLiterals pattern) =>
_sepCount == 5
&& _numCount == 4
&& _literals0.EqualsOrdinal(pattern.Start)
&& _literals1.EqualsOrdinal(pattern.DayHourSep)
&& _literals2.EqualsOrdinal(pattern.HourMinuteSep)
&& _literals3.EqualsOrdinal(pattern.MinuteSecondSep)
&& _literals4.EqualsOrdinal(pattern.End);
/// <summary>HMSF (no days)</summary>
internal bool FullHMSFMatch(TimeSpanFormat.FormatLiterals pattern) =>
_sepCount == 5
&& _numCount == 4
&& _literals0.EqualsOrdinal(pattern.Start)
&& _literals1.EqualsOrdinal(pattern.HourMinuteSep)
&& _literals2.EqualsOrdinal(pattern.MinuteSecondSep)
&& _literals3.EqualsOrdinal(pattern.SecondFractionSep)
&& _literals4.EqualsOrdinal(pattern.End);
internal TTT _lastSeenTTT;
internal int _tokenCount;
internal int _sepCount;
internal int _numCount;
private TimeSpanFormat.FormatLiterals _posLoc;
private TimeSpanFormat.FormatLiterals _negLoc;
private bool _posLocInit;
private bool _negLocInit;
private string _fullPosPattern;
private string _fullNegPattern;
private const int MaxTokens = 11;
private const int MaxLiteralTokens = 6;
private const int MaxNumericTokens = 5;
internal TimeSpanToken _numbers0, _numbers1, _numbers2, _numbers3, _numbers4; // MaxNumericTokens = 5
internal ReadOnlySpan<char> _literals0, _literals1, _literals2, _literals3, _literals4, _literals5; // MaxLiteralTokens=6
internal void Init(DateTimeFormatInfo dtfi)
{
Debug.Assert(dtfi != null);
_lastSeenTTT = TTT.None;
_tokenCount = 0;
_sepCount = 0;
_numCount = 0;
_fullPosPattern = dtfi.FullTimeSpanPositivePattern;
_fullNegPattern = dtfi.FullTimeSpanNegativePattern;
_posLocInit = false;
_negLocInit = false;
}
internal bool ProcessToken(ref TimeSpanToken tok, ref TimeSpanResult result)
{
switch (tok._ttt)
{
case TTT.Num:
if ((_tokenCount == 0 && !AddSep(default, ref result)) || !AddNum(tok, ref result))
{
return false;
}
break;
case TTT.Sep:
if (!AddSep(tok._sep, ref result))
{
return false;
}
break;
case TTT.NumOverflow:
return result.SetOverflowFailure();
default:
// Some unknown token or a repeat token type in the input
return result.SetBadTimeSpanFailure();
}
_lastSeenTTT = tok._ttt;
Debug.Assert(_tokenCount == (_sepCount + _numCount), "tokenCount == (SepCount + NumCount)");
return true;
}
private bool AddSep(ReadOnlySpan<char> sep, ref TimeSpanResult result)
{
if (_sepCount >= MaxLiteralTokens || _tokenCount >= MaxTokens)
{
return result.SetBadTimeSpanFailure();
}
switch (_sepCount++)
{
case 0: _literals0 = sep; break;
case 1: _literals1 = sep; break;
case 2: _literals2 = sep; break;
case 3: _literals3 = sep; break;
case 4: _literals4 = sep; break;
default: _literals5 = sep; break;
}
_tokenCount++;
return true;
}
private bool AddNum(TimeSpanToken num, ref TimeSpanResult result)
{
if (_numCount >= MaxNumericTokens || _tokenCount >= MaxTokens)
{
return result.SetBadTimeSpanFailure();
}
switch (_numCount++)
{
case 0: _numbers0 = num; break;
case 1: _numbers1 = num; break;
case 2: _numbers2 = num; break;
case 3: _numbers3 = num; break;
default: _numbers4 = num; break;
}
_tokenCount++;
return true;
}
}
/// <summary>Store the result of the parsing.</summary>
private ref struct TimeSpanResult
{
internal TimeSpan parsedTimeSpan;
private readonly bool _throwOnFailure;
private readonly ReadOnlySpan<char> _originalTimeSpanString;
internal TimeSpanResult(bool throwOnFailure, ReadOnlySpan<char> originalTimeSpanString)
{
parsedTimeSpan = default;
_throwOnFailure = throwOnFailure;
_originalTimeSpanString = originalTimeSpanString;
}
internal bool SetNoFormatSpecifierFailure()
{
if (!_throwOnFailure)
{
return false;
}
throw new FormatException(SR.Format_NoFormatSpecifier);
}
internal bool SetBadQuoteFailure(char failingCharacter)
{
if (!_throwOnFailure)
{
return false;
}
throw new FormatException(SR.Format(SR.Format_BadQuote, failingCharacter));
}
internal bool SetInvalidStringFailure()
{
if (!_throwOnFailure)
{
return false;
}
throw new FormatException(SR.Format_InvalidString);
}
internal bool SetArgumentNullFailure(string argumentName)
{
if (_throwOnFailure)
{
Debug.Assert(argumentName != null);
ArgumentNullException.Throw(argumentName);
}
return false;
}
internal bool SetOverflowFailure()
{
if (!_throwOnFailure)
{
return false;
}
throw new OverflowException(SR.Format(SR.Overflow_TimeSpanElementTooLarge, new string(_originalTimeSpanString)));
}
internal bool SetBadTimeSpanFailure()
{
if (!_throwOnFailure)
{
return false;
}
throw new FormatException(SR.Format(SR.Format_BadTimeSpan, new string(_originalTimeSpanString)));
}
internal bool SetBadFormatSpecifierFailure(char? formatSpecifierCharacter = null)
{
if (!_throwOnFailure)
{
return false;
}
throw new FormatException(SR.Format(SR.Format_BadFormatSpecifier, formatSpecifierCharacter));
}
}
internal static int Pow10UpToMaxFractionDigits(int pow)
{
ReadOnlySpan<int> powersOfTen =
[
1,
10,
100,
1000,
10000,
100000,
1000000,
10000000,
];
Debug.Assert(powersOfTen.Length == MaxFractionDigits + 1);
return powersOfTen[pow];
}
private static bool TryTimeToTicks(bool positive, TimeSpanToken days, TimeSpanToken hours, TimeSpanToken minutes, TimeSpanToken seconds, TimeSpanToken fraction, out long result)
{
if (days._num > MaxDays ||
hours._num > MaxHours ||
minutes._num > MaxMinutes ||
seconds._num > MaxSeconds ||
!fraction.NormalizeAndValidateFraction())
{
result = 0;
return false;
}
const long MaxMilliSeconds = long.MaxValue / TimeSpan.TicksPerMillisecond;
const long MinMilliSeconds = long.MinValue / TimeSpan.TicksPerMillisecond;
long ticks = ((long)days._num * 3600 * 24 + (long)hours._num * 3600 + (long)minutes._num * 60 + seconds._num) * 1000;
if (ticks > MaxMilliSeconds || ticks < MinMilliSeconds)
{
result = 0;
return false;
}
result = ticks * TimeSpan.TicksPerMillisecond + fraction._num;
if (positive && result < 0)
{
result = 0;
return false;
}
return true;
}
internal static TimeSpan Parse(ReadOnlySpan<char> input, IFormatProvider? formatProvider)
{
var parseResult = new TimeSpanResult(throwOnFailure: true, originalTimeSpanString: input);
bool success = TryParseTimeSpan(input, TimeSpanStandardStyles.Any, formatProvider, ref parseResult);
Debug.Assert(success, "Should have thrown on failure");
return parseResult.parsedTimeSpan;
}
internal static bool TryParse(ReadOnlySpan<char> input, IFormatProvider? formatProvider, out TimeSpan result)
{
var parseResult = new TimeSpanResult(throwOnFailure: false, originalTimeSpanString: input);
if (TryParseTimeSpan(input, TimeSpanStandardStyles.Any, formatProvider, ref parseResult))
{
result = parseResult.parsedTimeSpan;
return true;
}
result = default;
return false;
}
internal static TimeSpan ParseExact(ReadOnlySpan<char> input, ReadOnlySpan<char> format, IFormatProvider? formatProvider, TimeSpanStyles styles)
{
var parseResult = new TimeSpanResult(throwOnFailure: true, originalTimeSpanString: input);
bool success = TryParseExactTimeSpan(input, format, formatProvider, styles, ref parseResult);
Debug.Assert(success, "Should have thrown on failure");
return parseResult.parsedTimeSpan;
}
internal static bool TryParseExact(ReadOnlySpan<char> input, ReadOnlySpan<char> format, IFormatProvider? formatProvider, TimeSpanStyles styles, out TimeSpan result)
{
var parseResult = new TimeSpanResult(throwOnFailure: false, originalTimeSpanString: input);
if (TryParseExactTimeSpan(input, format, formatProvider, styles, ref parseResult))
{
result = parseResult.parsedTimeSpan;
return true;
}
result = default;
return false;
}
internal static TimeSpan ParseExactMultiple(ReadOnlySpan<char> input, string?[]? formats, IFormatProvider? formatProvider, TimeSpanStyles styles)
{
var parseResult = new TimeSpanResult(throwOnFailure: true, originalTimeSpanString: input);
bool success = TryParseExactMultipleTimeSpan(input, formats, formatProvider, styles, ref parseResult);
Debug.Assert(success, "Should have thrown on failure");
return parseResult.parsedTimeSpan;
}
internal static bool TryParseExactMultiple(ReadOnlySpan<char> input, string?[]? formats, IFormatProvider? formatProvider, TimeSpanStyles styles, out TimeSpan result)
{
var parseResult = new TimeSpanResult(throwOnFailure: false, originalTimeSpanString: input);
if (TryParseExactMultipleTimeSpan(input, formats, formatProvider, styles, ref parseResult))
{
result = parseResult.parsedTimeSpan;
return true;
}
result = default;
return false;
}
/// <summary>Common private Parse method called by both Parse and TryParse.</summary>
private static bool TryParseTimeSpan(ReadOnlySpan<char> input, TimeSpanStandardStyles style, IFormatProvider? formatProvider, ref TimeSpanResult result)
{
input = input.Trim();
if (input.IsEmpty)
{
return result.SetBadTimeSpanFailure();
}
var tokenizer = new TimeSpanTokenizer(input);
TimeSpanRawInfo raw = default;
raw.Init(DateTimeFormatInfo.GetInstance(formatProvider));
TimeSpanToken tok = tokenizer.GetNextToken();
// The following loop will break out when we reach the end of the str or
// when we can determine that the input is invalid.
while (tok._ttt != TTT.End)
{
if (!raw.ProcessToken(ref tok, ref result))
{
return result.SetBadTimeSpanFailure();
}
tok = tokenizer.GetNextToken();
}
Debug.Assert(tokenizer.EOL);
if (!ProcessTerminalState(ref raw, style, ref result))
{
return result.SetBadTimeSpanFailure();
}
return true;
}
/// <summary>
/// Validate the terminal state of a standard format parse.
/// Sets result.parsedTimeSpan on success.
/// Calculates the resultant TimeSpan from the TimeSpanRawInfo.
/// </summary>
/// <remarks>
/// try => +InvariantPattern, -InvariantPattern, +LocalizedPattern, -LocalizedPattern
/// 1) Verify Start matches
/// 2) Verify End matches
/// 3) 1 number => d
/// 2 numbers => h:m
/// 3 numbers => h:m:s | d.h:m | h:m:.f
/// 4 numbers => h:m:s.f | d.h:m:s | d.h:m:.f
/// 5 numbers => d.h:m:s.f
/// </remarks>
private static bool ProcessTerminalState(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)
{
if (raw._lastSeenTTT == TTT.Num)
{
TimeSpanToken tok = default;
tok._ttt = TTT.Sep;
if (!raw.ProcessToken(ref tok, ref result))
{
return result.SetBadTimeSpanFailure();
}
}
return raw._numCount switch
{
1 => ProcessTerminal_D(ref raw, style, ref result),
2 => ProcessTerminal_HM(ref raw, style, ref result),
3 => ProcessTerminal_HM_S_D(ref raw, style, ref result),
4 => ProcessTerminal_HMS_F_D(ref raw, style, ref result),
5 => ProcessTerminal_DHMSF(ref raw, style, ref result),
_ => result.SetBadTimeSpanFailure(),
};
}
/// <summary>Validate the 5-number "Days.Hours:Minutes:Seconds.Fraction" terminal case.</summary>
private static bool ProcessTerminal_DHMSF(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)
{
if (raw._sepCount != 6)
{
return result.SetBadTimeSpanFailure();
}
Debug.Assert(raw._numCount == 5);
bool inv = (style & TimeSpanStandardStyles.Invariant) != 0;
bool loc = (style & TimeSpanStandardStyles.Localized) != 0;
bool positive = false;
bool match = false;
if (inv)
{
if (raw.FullMatch(TimeSpanFormat.PositiveInvariantFormatLiterals))
{
match = true;
positive = true;
}
if (!match && raw.FullMatch(TimeSpanFormat.NegativeInvariantFormatLiterals))
{
match = true;
positive = false;
}
}
if (loc)
{
if (!match && raw.FullMatch(raw.PositiveLocalized))
{
match = true;
positive = true;
}
if (!match && raw.FullMatch(raw.NegativeLocalized))
{
match = true;
positive = false;
}
}
if (match)
{
if (!TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, raw._numbers4, out long ticks))
{
return result.SetOverflowFailure();
}
if (!positive)
{
ticks = -ticks;
if (ticks > 0)
{
return result.SetOverflowFailure();
}
}
result.parsedTimeSpan = new TimeSpan(ticks);
return true;
}
return result.SetBadTimeSpanFailure();
}
/// <summary>
/// Validate the ambiguous 4-number "Hours:Minutes:Seconds.Fraction", "Days.Hours:Minutes:Seconds",
/// or "Days.Hours:Minutes:.Fraction" terminal case.
/// </summary>
private static bool ProcessTerminal_HMS_F_D(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)
{
if (raw._sepCount != 5 || (style & TimeSpanStandardStyles.RequireFull) != 0)
{
return result.SetBadTimeSpanFailure();
}
Debug.Assert(raw._numCount == 4);
bool inv = ((style & TimeSpanStandardStyles.Invariant) != 0);
bool loc = ((style & TimeSpanStandardStyles.Localized) != 0);
long ticks = 0;
bool positive = false, match = false, overflow = false;
var zero = new TimeSpanToken(0);
if (inv)
{
if (raw.FullHMSFMatch(TimeSpanFormat.PositiveInvariantFormatLiterals))
{
positive = true;
match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullDHMSMatch(TimeSpanFormat.PositiveInvariantFormatLiterals))
{
positive = true;
match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, zero, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullAppCompatMatch(TimeSpanFormat.PositiveInvariantFormatLiterals))
{
positive = true;
match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, raw._numbers3, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullHMSFMatch(TimeSpanFormat.NegativeInvariantFormatLiterals))
{
positive = false;
match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullDHMSMatch(TimeSpanFormat.NegativeInvariantFormatLiterals))
{
positive = false;
match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, zero, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullAppCompatMatch(TimeSpanFormat.NegativeInvariantFormatLiterals))
{
positive = false;
match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, raw._numbers3, out ticks);
overflow = overflow || !match;
}
}
if (loc)
{
if (!match && raw.FullHMSFMatch(raw.PositiveLocalized))
{
positive = true;
match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullDHMSMatch(raw.PositiveLocalized))
{
positive = true;
match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, zero, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullAppCompatMatch(raw.PositiveLocalized))
{
positive = true;
match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, raw._numbers3, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullHMSFMatch(raw.NegativeLocalized))
{
positive = false;
match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullDHMSMatch(raw.NegativeLocalized))
{
positive = false;
match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, raw._numbers3, zero, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullAppCompatMatch(raw.NegativeLocalized))
{
positive = false;
match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, raw._numbers3, out ticks);
overflow = overflow || !match;
}
}
if (match)
{
if (!positive)
{
ticks = -ticks;
if (ticks > 0)
{
return result.SetOverflowFailure();
}
}
result.parsedTimeSpan = new TimeSpan(ticks);
return true;
}
return overflow ?
result.SetOverflowFailure() : // we found at least one literal pattern match but the numbers just didn't fit
result.SetBadTimeSpanFailure(); // we couldn't find a thing
}
/// <summary>Validate the ambiguous 3-number "Hours:Minutes:Seconds", "Days.Hours:Minutes", or "Hours:Minutes:.Fraction" terminal case.</summary>
private static bool ProcessTerminal_HM_S_D(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)
{
if (raw._sepCount != 4 || (style & TimeSpanStandardStyles.RequireFull) != 0)
{
return result.SetBadTimeSpanFailure();
}
Debug.Assert(raw._numCount == 3);
bool inv = ((style & TimeSpanStandardStyles.Invariant) != 0);
bool loc = ((style & TimeSpanStandardStyles.Localized) != 0);
bool positive = false, match = false, overflow = false;
var zero = new TimeSpanToken(0);
long ticks = 0;
if (inv)
{
if (raw.FullHMSMatch(TimeSpanFormat.PositiveInvariantFormatLiterals))
{
positive = true;
match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, zero, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullDHMMatch(TimeSpanFormat.PositiveInvariantFormatLiterals))
{
positive = true;
match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, zero, out ticks);
overflow = overflow || !match;
}
if (!match && raw.PartialAppCompatMatch(TimeSpanFormat.PositiveInvariantFormatLiterals))
{
positive = true;
match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, zero, raw._numbers2, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullHMSMatch(TimeSpanFormat.NegativeInvariantFormatLiterals))
{
positive = false;
match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, zero, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullDHMMatch(TimeSpanFormat.NegativeInvariantFormatLiterals))
{
positive = false;
match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, zero, out ticks);
overflow = overflow || !match;
}
if (!match && raw.PartialAppCompatMatch(TimeSpanFormat.NegativeInvariantFormatLiterals))
{
positive = false;
match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, zero, raw._numbers2, out ticks);
overflow = overflow || !match;
}
}
if (loc)
{
if (!match && raw.FullHMSMatch(raw.PositiveLocalized))
{
positive = true;
match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, zero, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullDHMMatch(raw.PositiveLocalized))
{
positive = true;
match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, zero, out ticks);
overflow = overflow || !match;
}
if (!match && raw.PartialAppCompatMatch(raw.PositiveLocalized))
{
positive = true;
match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, zero, raw._numbers2, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullHMSMatch(raw.NegativeLocalized))
{
positive = false;
match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, raw._numbers2, zero, out ticks);
overflow = overflow || !match;
}
if (!match && raw.FullDHMMatch(raw.NegativeLocalized))
{
positive = false;
match = TryTimeToTicks(positive, raw._numbers0, raw._numbers1, raw._numbers2, zero, zero, out ticks);
overflow = overflow || !match;
}
if (!match && raw.PartialAppCompatMatch(raw.NegativeLocalized))
{
positive = false;
match = TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, zero, raw._numbers2, out ticks);
overflow = overflow || !match;
}
}
if (match)
{
if (!positive)
{
ticks = -ticks;
if (ticks > 0)
{
return result.SetOverflowFailure();
}
}
result.parsedTimeSpan = new TimeSpan(ticks);
return true;
}
return overflow ?
result.SetOverflowFailure() : // we found at least one literal pattern match but the numbers just didn't fit
result.SetBadTimeSpanFailure(); // we couldn't find a thing
}
/// <summary>Validate the 2-number "Hours:Minutes" terminal case.</summary>
private static bool ProcessTerminal_HM(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)
{
if (raw._sepCount != 3 || (style & TimeSpanStandardStyles.RequireFull) != 0)
{
return result.SetBadTimeSpanFailure();
}
Debug.Assert(raw._numCount == 2);
bool inv = ((style & TimeSpanStandardStyles.Invariant) != 0);
bool loc = ((style & TimeSpanStandardStyles.Localized) != 0);
bool positive = false, match = false;
if (inv)
{
if (raw.FullHMMatch(TimeSpanFormat.PositiveInvariantFormatLiterals))
{
match = true;
positive = true;
}
if (!match && raw.FullHMMatch(TimeSpanFormat.NegativeInvariantFormatLiterals))
{
match = true;
positive = false;
}
}
if (loc)
{
if (!match && raw.FullHMMatch(raw.PositiveLocalized))
{
match = true;
positive = true;
}
if (!match && raw.FullHMMatch(raw.NegativeLocalized))
{
match = true;
positive = false;
}
}
if (match)
{
var zero = new TimeSpanToken(0);
if (!TryTimeToTicks(positive, zero, raw._numbers0, raw._numbers1, zero, zero, out long ticks))
{
return result.SetOverflowFailure();
}
if (!positive)
{
ticks = -ticks;
if (ticks > 0)
{
return result.SetOverflowFailure();
}
}
result.parsedTimeSpan = new TimeSpan(ticks);
return true;
}
return result.SetBadTimeSpanFailure();
}
/// <summary>Validate the 1-number "Days" terminal case.</summary>
private static bool ProcessTerminal_D(ref TimeSpanRawInfo raw, TimeSpanStandardStyles style, ref TimeSpanResult result)
{
if (raw._sepCount != 2 || (style & TimeSpanStandardStyles.RequireFull) != 0)
{
return result.SetBadTimeSpanFailure();
}
Debug.Assert(raw._numCount == 1);
bool inv = ((style & TimeSpanStandardStyles.Invariant) != 0);
bool loc = ((style & TimeSpanStandardStyles.Localized) != 0);
bool positive = false, match = false;
if (inv)
{
if (raw.FullDMatch(TimeSpanFormat.PositiveInvariantFormatLiterals))
{
match = true;
positive = true;
}
if (!match && raw.FullDMatch(TimeSpanFormat.NegativeInvariantFormatLiterals))
{
match = true;
positive = false;
}
}
if (loc)
{
if (!match && raw.FullDMatch(raw.PositiveLocalized))
{
match = true;
positive = true;
}
if (!match && raw.FullDMatch(raw.NegativeLocalized))
{
match = true;
positive = false;
}
}
if (match)
{
var zero = new TimeSpanToken(0);
if (!TryTimeToTicks(positive, raw._numbers0, zero, zero, zero, zero, out long ticks))
{
return result.SetOverflowFailure();
}
if (!positive)
{
ticks = -ticks;
if (ticks > 0)
{
return result.SetOverflowFailure();
}
}
result.parsedTimeSpan = new TimeSpan(ticks);
return true;
}
return result.SetBadTimeSpanFailure();
}
/// <summary>Common private ParseExact method called by both ParseExact and TryParseExact.</summary>
private static bool TryParseExactTimeSpan(ReadOnlySpan<char> input, ReadOnlySpan<char> format, IFormatProvider? formatProvider, TimeSpanStyles styles, ref TimeSpanResult result)
{
if (format.Length == 0)
{
return result.SetBadFormatSpecifierFailure();
}
if (format.Length == 1)
{
return format[0] switch
{
'c' or 't' or 'T' => TryParseTimeSpanConstant(input, ref result), // fast path for legacy style TimeSpan formats.
'g' => TryParseTimeSpan(input, TimeSpanStandardStyles.Localized, formatProvider, ref result),
'G' => TryParseTimeSpan(input, TimeSpanStandardStyles.Localized | TimeSpanStandardStyles.RequireFull, formatProvider, ref result),
_ => result.SetBadFormatSpecifierFailure(format[0]),
};
}
return TryParseByFormat(input, format, styles, ref result);
}
/// <summary>Parse the TimeSpan instance using the specified format. Used by TryParseExactTimeSpan.</summary>
private static bool TryParseByFormat(ReadOnlySpan<char> input, ReadOnlySpan<char> format, TimeSpanStyles styles, ref TimeSpanResult result)
{
bool seenDD = false; // already processed days?
bool seenHH = false; // already processed hours?
bool seenMM = false; // already processed minutes?
bool seenSS = false; // already processed seconds?
bool seenFF = false; // already processed fraction?
int dd = 0; // parsed days
int hh = 0; // parsed hours
int mm = 0; // parsed minutes
int ss = 0; // parsed seconds
int leadingZeroes = 0; // number of leading zeroes in the parsed fraction
int ff = 0; // parsed fraction
int i = 0; // format string position
int tokenLen; // length of current format token, used to update index 'i'
var tokenizer = new TimeSpanTokenizer(input, -1);
while ((uint)i < (uint)format.Length)
{
char ch = format[i];
int nextFormatChar;
switch (ch)
{
case 'h':
tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
if (tokenLen > 2 || seenHH || !ParseExactDigits(ref tokenizer, tokenLen, out hh))
{
return result.SetInvalidStringFailure();
}
seenHH = true;
break;
case 'm':
tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
if (tokenLen > 2 || seenMM || !ParseExactDigits(ref tokenizer, tokenLen, out mm))
{
return result.SetInvalidStringFailure();
}
seenMM = true;
break;
case 's':
tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
if (tokenLen > 2 || seenSS || !ParseExactDigits(ref tokenizer, tokenLen, out ss))
{
return result.SetInvalidStringFailure();
}
seenSS = true;
break;
case 'f':
tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
if (tokenLen > DateTimeFormat.MaxSecondsFractionDigits || seenFF || !ParseExactDigits(ref tokenizer, tokenLen, tokenLen, out leadingZeroes, out ff))
{
return result.SetInvalidStringFailure();
}
seenFF = true;
break;
case 'F':
tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
if (tokenLen > DateTimeFormat.MaxSecondsFractionDigits || seenFF)
{
return result.SetInvalidStringFailure();
}
ParseExactDigits(ref tokenizer, tokenLen, tokenLen, out leadingZeroes, out ff);
seenFF = true;
break;
case 'd':
tokenLen = DateTimeFormat.ParseRepeatPattern(format, i, ch);
if (tokenLen > 8 || seenDD || !ParseExactDigits(ref tokenizer, (tokenLen < 2) ? 1 : tokenLen, (tokenLen < 2) ? 8 : tokenLen, out _, out dd))
{
return result.SetInvalidStringFailure();
}
seenDD = true;
break;
case '\'':
case '\"':
var enquotedString = new ValueStringBuilder(64);
if (!DateTimeParse.TryParseQuoteString(format, i, ref enquotedString, out tokenLen))
{
enquotedString.Dispose();
return result.SetBadQuoteFailure(ch);
}
if (!ParseExactLiteral(ref tokenizer, ref enquotedString))
{
enquotedString.Dispose();
return result.SetInvalidStringFailure();
}
enquotedString.Dispose();
break;
case '%':
// Optional format character.
// For example, format string "%d" will print day
// Most of the cases, "%" can be ignored.
nextFormatChar = DateTimeFormat.ParseNextChar(format, i);
// nextFormatChar will be -1 if we already reach the end of the format string.
// Besides, we will not allow "%%" appear in the pattern.
if (nextFormatChar >= 0 && nextFormatChar != '%')
{
tokenLen = 1; // skip the '%' and process the format character
break;
}
else
{
// This means that '%' is at the end of the format string or
// "%%" appears in the format string.
return result.SetInvalidStringFailure();
}
case '\\':
// Escaped character. Can be used to insert character into the format string.
// For example, "\d" will insert the character 'd' into the string.
//
nextFormatChar = DateTimeFormat.ParseNextChar(format, i);
if (nextFormatChar >= 0 && tokenizer.NextChar() == (char)nextFormatChar)
{
tokenLen = 2;
}
else
{
// This means that '\' is at the end of the format string or the literal match failed.
return result.SetInvalidStringFailure();
}
break;
default:
return result.SetInvalidStringFailure();
}
i += tokenLen;
}
if (!tokenizer.EOL)
{
// the custom format didn't consume the entire input
return result.SetBadTimeSpanFailure();
}
bool positive = (styles & TimeSpanStyles.AssumeNegative) == 0;
if (TryTimeToTicks(positive, new TimeSpanToken(dd),
new TimeSpanToken(hh),
new TimeSpanToken(mm),
new TimeSpanToken(ss),
new TimeSpanToken(ff, leadingZeroes),
out long ticks))
{
if (!positive)
{
ticks = -ticks;
}
result.parsedTimeSpan = new TimeSpan(ticks);
return true;
}
else
{
return result.SetOverflowFailure();
}
}
private static bool ParseExactDigits(ref TimeSpanTokenizer tokenizer, int minDigitLength, out int result)
{
int maxDigitLength = (minDigitLength == 1) ? 2 : minDigitLength;
return ParseExactDigits(ref tokenizer, minDigitLength, maxDigitLength, out _, out result);
}
private static bool ParseExactDigits(ref TimeSpanTokenizer tokenizer, int minDigitLength, int maxDigitLength, out int zeroes, out int result)
{
int tmpResult = 0, tmpZeroes = 0;
int tokenLength = 0;
while (tokenLength < maxDigitLength)
{
char ch = tokenizer.NextChar();
if (!char.IsAsciiDigit(ch))
{
tokenizer.BackOne();
break;
}
tmpResult = tmpResult * 10 + (ch - '0');
if (tmpResult == 0) tmpZeroes++;
tokenLength++;
}
zeroes = tmpZeroes;
result = tmpResult;
return tokenLength >= minDigitLength;
}
private static bool ParseExactLiteral(ref TimeSpanTokenizer tokenizer, ref ValueStringBuilder enquotedString)
{
ReadOnlySpan<char> span = enquotedString.AsSpan();
for (int i = 0; i < span.Length; i++)
{
if (span[i] != tokenizer.NextChar())
{
return false;
}
}
return true;
}
/// <summary>
/// Parses the "c" (constant) format. This code is 100% identical to the non-globalized v1.0-v3.5 TimeSpan.Parse() routine
/// and exists for performance/appcompat with legacy callers who cannot move onto the globalized Parse overloads.
/// </summary>
private static bool TryParseTimeSpanConstant(ReadOnlySpan<char> input, ref TimeSpanResult result) =>
default(StringParser).TryParse(input, ref result);
private ref struct StringParser
{
private ReadOnlySpan<char> _str;
private char _ch;
private int _pos;
internal void NextChar()
{
ReadOnlySpan<char> str = _str;
if (_pos < str.Length)
{
_pos++;
}
int pos = _pos;
_ch = (uint)pos < (uint)str.Length ?
str[pos] :
(char)0;
}
internal char NextNonDigit()
{
int i = _str.Slice(_pos).IndexOfAnyExceptInRange('0', '9');
return i < 0 ? (char)0 : _str[_pos + i];
}
internal bool TryParse(ReadOnlySpan<char> input, ref TimeSpanResult result)
{
result.parsedTimeSpan = default;
_str = input;
_pos = -1;
NextChar();
SkipBlanks();
bool negative = false;
if (_ch == '-')
{
negative = true;
NextChar();
}
long time;
if (NextNonDigit() == ':')
{
if (!ParseTime(out time, ref result))
{
return false;
}
}
else
{
if (!ParseInt((int)(0x7FFFFFFFFFFFFFFFL / TimeSpan.TicksPerDay), out int days, ref result))
{
return false;
}
time = days * TimeSpan.TicksPerDay;
if (_ch == '.')
{
NextChar();
if (!ParseTime(out long remainingTime, ref result))
{
return false;
}
time += remainingTime;
}
}
if (negative)
{
time = -time;
// Allow -0 as well
if (time > 0)
{
return result.SetOverflowFailure();
}
}
else
{
if (time < 0)
{
return result.SetOverflowFailure();
}
}
SkipBlanks();
if (_pos < _str.Length)
{
return result.SetBadTimeSpanFailure();
}
result.parsedTimeSpan = new TimeSpan(time);
return true;
}
internal bool ParseInt(int max, out int i, ref TimeSpanResult result)
{
i = 0;
int p = _pos;
while (char.IsAsciiDigit(_ch))
{
if ((i & 0xF0000000) != 0)
{
return result.SetOverflowFailure();
}
i = i * 10 + _ch - '0';
if (i < 0)
{
return result.SetOverflowFailure();
}
NextChar();
}
if (p == _pos)
{
return result.SetBadTimeSpanFailure();
}
if (i > max)
{
return result.SetOverflowFailure();
}
return true;
}
internal bool ParseTime(out long time, ref TimeSpanResult result)
{
time = 0;
if (!ParseInt(23, out int unit, ref result))
{
return false;
}
time = unit * TimeSpan.TicksPerHour;
if (_ch != ':')
{
return result.SetBadTimeSpanFailure();
}
NextChar();
if (!ParseInt(59, out unit, ref result))
{
return false;
}
time += unit * TimeSpan.TicksPerMinute;
if (_ch == ':')
{
NextChar();
// allow seconds with the leading zero
if (_ch != '.')
{
if (!ParseInt(59, out unit, ref result))
{
return false;
}
time += unit * TimeSpan.TicksPerSecond;
}
if (_ch == '.')
{
NextChar();
int f = (int)TimeSpan.TicksPerSecond;
while (f > 1 && char.IsAsciiDigit(_ch))
{
f /= 10;
time += (_ch - '0') * f;
NextChar();
}
}
}
return true;
}
internal void SkipBlanks()
{
while (_ch == ' ' || _ch == '\t') NextChar();
}
}
/// <summary>Common private ParseExactMultiple method called by both ParseExactMultiple and TryParseExactMultiple.</summary>
private static bool TryParseExactMultipleTimeSpan(ReadOnlySpan<char> input, string?[]? formats, IFormatProvider? formatProvider, TimeSpanStyles styles, ref TimeSpanResult result)
{
if (formats == null)
{
return result.SetArgumentNullFailure(nameof(formats));
}
if (input.Length == 0)
{
return result.SetBadTimeSpanFailure();
}
if (formats.Length == 0)
{
return result.SetNoFormatSpecifierFailure();
}
// Do a loop through the provided formats and see if we can parse successfully in
// one of the formats.
for (int i = 0; i < formats.Length; i++)
{
string? format = formats[i];
if (string.IsNullOrEmpty(format))
{
return result.SetBadFormatSpecifierFailure();
}
// Create a new non-throwing result each time to ensure the runs are independent.
TimeSpanResult innerResult = new TimeSpanResult(throwOnFailure: false, originalTimeSpanString: input);
if (TryParseExactTimeSpan(input, format, formatProvider, styles, ref innerResult))
{
result.parsedTimeSpan = innerResult.parsedTimeSpan;
return true;
}
}
return result.SetBadTimeSpanFailure();
}
}
}
|