diff --git a/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs b/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs index f24aa06296c2a4..1a38a7619b2b73 100644 --- a/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs +++ b/src/libraries/System.Private.CoreLib/src/System/DateOnly.cs @@ -286,6 +286,11 @@ public int CompareTo(object? value) /// An object that is equivalent to the date contained in s, as specified by provider and styles. public static DateOnly Parse(ReadOnlySpan s, IFormatProvider? provider = default, DateTimeStyles style = DateTimeStyles.None) { + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) + { + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + } + ParseFailureKind result = TryParseInternal(s, provider, style, out DateOnly dateOnly); if (result != ParseFailureKind.None) { @@ -309,6 +314,11 @@ public static DateOnly Parse(ReadOnlySpan s, IFormatProvider? provider = d /// An object that is equivalent to the date contained in s, as specified by format, provider, and style. public static DateOnly ParseExact(ReadOnlySpan s, [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] ReadOnlySpan format, IFormatProvider? provider = default, DateTimeStyles style = DateTimeStyles.None) { + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) + { + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + } + ParseFailureKind result = TryParseExactInternal(s, format, provider, style, out DateOnly dateOnly); if (result != ParseFailureKind.None) @@ -339,6 +349,25 @@ public static DateOnly ParseExact(ReadOnlySpan s, [StringSyntax(StringSynt /// An object that is equivalent to the date contained in s, as specified by format, provider, and style. public static DateOnly ParseExact(ReadOnlySpan s, [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] string[] formats, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) { + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) + { + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + } + + if (formats == null) + { + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + } + + // Validate formats array for null/empty entries upfront + for (int i = 0; i < formats.Length; i++) + { + if (string.IsNullOrEmpty(formats[i])) + { + throw new FormatException(SR.Argument_BadFormatSpecifier); + } + } + ParseFailureKind result = TryParseExactInternal(s, formats, provider, style, out DateOnly dateOnly); if (result != ParseFailureKind.None) { @@ -433,16 +462,19 @@ public static DateOnly ParseExact(string s, [StringSyntax(StringSyntaxAttribute. /// A bitwise combination of enumeration values that indicates the permitted format of s. A typical value to specify is None. /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is empty string, or does not contain a valid string representation of a date. This parameter is passed uninitialized. /// true if the s parameter was converted successfully; otherwise, false. - public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) => TryParseInternal(s, provider, style, out result) == ParseFailureKind.None; - - private static ParseFailureKind TryParseInternal(ReadOnlySpan s, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) { if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) { - result = default; - return ParseFailureKind.Argument_InvalidDateStyles; + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); } + ParseFailureKind failureKind = TryParseInternal(s, provider, style, out result); + return failureKind == ParseFailureKind.None; + } + + private static ParseFailureKind TryParseInternal(ReadOnlySpan s, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) + { DateTimeResult dtResult = default; dtResult.Init(s); @@ -482,16 +514,26 @@ private static ParseFailureKind TryParseInternal(ReadOnlySpan s, IFormatPr /// A bitwise combination of one or more enumeration values that indicate the permitted format of s. /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s is empty string, or does not contain a date that correspond to the pattern specified in format. This parameter is passed uninitialized. /// true if s was converted successfully; otherwise, false. - public static bool TryParseExact(ReadOnlySpan s, [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] ReadOnlySpan format, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) => - TryParseExactInternal(s, format, provider, style, out result) == ParseFailureKind.None; - private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, ReadOnlySpan format, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) + public static bool TryParseExact(ReadOnlySpan s, [StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] ReadOnlySpan format, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) { if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) { - result = default; - return ParseFailureKind.Argument_InvalidDateStyles; + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); } + ParseFailureKind failureKind = TryParseExactInternal(s, format, provider, style, out result); + if (failureKind != ParseFailureKind.None) + { + if (failureKind == ParseFailureKind.Argument_BadFormatSpecifier) + { + throw new FormatException(SR.Argument_BadFormatSpecifier); + } + return false; + } + return true; + } + private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, ReadOnlySpan format, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) + { if (format.Length == 1) { switch (format[0] | 0x20) @@ -514,7 +556,9 @@ private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, Read if (!DateTimeParse.TryParseExact(s, format, DateTimeFormatInfo.GetInstance(provider), style, ref dtResult)) { result = default; - return ParseFailureKind.Format_BadDateOnly; + return dtResult.failure == ParseFailureKind.Argument_BadFormatSpecifier + ? ParseFailureKind.Argument_BadFormatSpecifier + : ParseFailureKind.Format_BadDateOnly; } if ((dtResult.flags & ParseFlagsDateMask) != 0) @@ -546,30 +590,40 @@ private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, Read /// A bitwise combination of enumeration values that defines how to interpret the parsed date. A typical value to specify is None. /// When this method returns, contains the DateOnly value equivalent to the date contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is Empty, or does not contain a valid string representation of a date. This parameter is passed uninitialized. /// true if the s parameter was converted successfully; otherwise, false. - public static bool TryParseExact(ReadOnlySpan s, [NotNullWhen(true), StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] string?[]? formats, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) => - TryParseExactInternal(s, formats, provider, style, out result) == ParseFailureKind.None; - - private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, string?[]? formats, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) + public static bool TryParseExact(ReadOnlySpan s, [NotNullWhen(true), StringSyntax(StringSyntaxAttribute.DateOnlyFormat)] string?[]? formats, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) { - if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0 || formats == null) + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) { - result = default; - return ParseFailureKind.Argument_InvalidDateStyles; + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); } - DateTimeFormatInfo dtfi = DateTimeFormatInfo.GetInstance(provider); + if (formats == null) + { + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + } + // Validate formats array for null/empty entries upfront for (int i = 0; i < formats.Length; i++) { - DateTimeFormatInfo dtfiToUse = dtfi; - string? format = formats[i]; - if (string.IsNullOrEmpty(format)) + if (string.IsNullOrEmpty(formats[i])) { - result = default; - return ParseFailureKind.Argument_BadFormatSpecifier; + throw new FormatException(SR.Argument_BadFormatSpecifier); } + } + + return TryParseExactInternal(s, formats, provider, style, out result) == ParseFailureKind.None; + } + + private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, string?[]? formats, IFormatProvider? provider, DateTimeStyles style, out DateOnly result) + { + DateTimeFormatInfo dtfi = DateTimeFormatInfo.GetInstance(provider); + + for (int i = 0; i < formats!.Length; i++) + { + DateTimeFormatInfo dtfiToUse = dtfi; + string? format = formats[i]; - if (format.Length == 1) + if (format!.Length == 1) { switch (format[0] | 0x20) { @@ -594,6 +648,12 @@ private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, stri result = new DateOnly(DayNumberFromDateTime(dtResult.parsedDate)); return ParseFailureKind.None; } + + if (dtResult.failure == ParseFailureKind.Argument_BadFormatSpecifier) + { + result = default; + return ParseFailureKind.Argument_BadFormatSpecifier; + } } result = default; diff --git a/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs b/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs index 72e15d2faada6a..329270849f3149 100644 --- a/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs +++ b/src/libraries/System.Private.CoreLib/src/System/TimeOnly.cs @@ -455,6 +455,11 @@ public override int GetHashCode() /// public static TimeOnly Parse(ReadOnlySpan s, IFormatProvider? provider = default, DateTimeStyles style = DateTimeStyles.None) { + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) + { + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + } + ParseFailureKind result = TryParseInternal(s, provider, style, out TimeOnly timeOnly); if (result != ParseFailureKind.None) { @@ -478,6 +483,11 @@ public static TimeOnly Parse(ReadOnlySpan s, IFormatProvider? provider = d /// An object that is equivalent to the time contained in s, as specified by format, provider, and style. public static TimeOnly ParseExact(ReadOnlySpan s, [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] ReadOnlySpan format, IFormatProvider? provider = default, DateTimeStyles style = DateTimeStyles.None) { + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) + { + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + } + ParseFailureKind result = TryParseExactInternal(s, format, provider, style, out TimeOnly timeOnly); if (result != ParseFailureKind.None) { @@ -507,6 +517,25 @@ public static TimeOnly ParseExact(ReadOnlySpan s, [StringSyntax(StringSynt /// An object that is equivalent to the time contained in s, as specified by format, provider, and style. public static TimeOnly ParseExact(ReadOnlySpan s, [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] string[] formats, IFormatProvider? provider, DateTimeStyles style = DateTimeStyles.None) { + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) + { + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + } + + if (formats == null) + { + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + } + + // Validate formats array for null/empty entries upfront + for (int i = 0; i < formats.Length; i++) + { + if (string.IsNullOrEmpty(formats[i])) + { + throw new FormatException(SR.Argument_BadFormatSpecifier); + } + } + ParseFailureKind result = TryParseExactInternal(s, formats, provider, style, out TimeOnly timeOnly); if (result != ParseFailureKind.None) { @@ -602,16 +631,18 @@ public static TimeOnly ParseExact(string s, [StringSyntax(StringSyntaxAttribute. /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is empty string, or does not contain a valid string representation of a date. This parameter is passed uninitialized. /// true if the s parameter was converted successfully; otherwise, false. /// - public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) => - TryParseInternal(s, provider, style, out result) == ParseFailureKind.None; - private static ParseFailureKind TryParseInternal(ReadOnlySpan s, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) + public static bool TryParse(ReadOnlySpan s, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) { if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) { - result = default; - return ParseFailureKind.Argument_InvalidDateStyles; + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); } + ParseFailureKind failureKind = TryParseInternal(s, provider, style, out result); + return failureKind == ParseFailureKind.None; + } + private static ParseFailureKind TryParseInternal(ReadOnlySpan s, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) + { DateTimeResult dtResult = default; dtResult.Init(s); @@ -652,17 +683,27 @@ private static ParseFailureKind TryParseInternal(ReadOnlySpan s, IFormatPr /// A bitwise combination of one or more enumeration values that indicate the permitted format of s. /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s is empty string, or does not contain a time that correspond to the pattern specified in format. This parameter is passed uninitialized. /// true if s was converted successfully; otherwise, false. - public static bool TryParseExact(ReadOnlySpan s, [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] ReadOnlySpan format, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) => - TryParseExactInternal(s, format, provider, style, out result) == ParseFailureKind.None; - - private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, ReadOnlySpan format, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) + public static bool TryParseExact(ReadOnlySpan s, [StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] ReadOnlySpan format, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) { if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) { - result = default; - return ParseFailureKind.Argument_InvalidDateStyles; + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + } + + ParseFailureKind failureKind = TryParseExactInternal(s, format, provider, style, out result); + if (failureKind != ParseFailureKind.None) + { + if (failureKind == ParseFailureKind.Argument_BadFormatSpecifier) + { + throw new FormatException(SR.Argument_BadFormatSpecifier); + } + return false; } + return true; + } + private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, ReadOnlySpan format, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) + { if (format.Length == 1) { switch (format[0] | 0x20) @@ -685,7 +726,9 @@ private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, Read if (!DateTimeParse.TryParseExact(s, format, DateTimeFormatInfo.GetInstance(provider), style, ref dtResult)) { result = default; - return ParseFailureKind.Format_BadTimeOnly; + return dtResult.failure == ParseFailureKind.Argument_BadFormatSpecifier + ? ParseFailureKind.Argument_BadFormatSpecifier + : ParseFailureKind.Format_BadTimeOnly; } if ((dtResult.flags & ParseFlagsTimeMask) != 0) @@ -716,30 +759,40 @@ private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, Read /// A bitwise combination of enumeration values that defines how to interpret the parsed time. A typical value to specify is None. /// When this method returns, contains the TimeOnly value equivalent to the time contained in s, if the conversion succeeded, or MinValue if the conversion failed. The conversion fails if the s parameter is Empty, or does not contain a valid string representation of a time. This parameter is passed uninitialized. /// true if the s parameter was converted successfully; otherwise, false. - public static bool TryParseExact(ReadOnlySpan s, [NotNullWhen(true), StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] string?[]? formats, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) => - TryParseExactInternal(s, formats, provider, style, out result) == ParseFailureKind.None; - - private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, string?[]? formats, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) + public static bool TryParseExact(ReadOnlySpan s, [NotNullWhen(true), StringSyntax(StringSyntaxAttribute.TimeOnlyFormat)] string?[]? formats, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) { - if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0 || formats == null) + if ((style & ~DateTimeStyles.AllowWhiteSpaces) != 0) { - result = default; - return ParseFailureKind.Argument_InvalidDateStyles; + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); } - DateTimeFormatInfo dtfi = DateTimeFormatInfo.GetInstance(provider); + if (formats == null) + { + throw new ArgumentException(SR.Argument_InvalidDateStyles, nameof(style)); + } + // Validate formats array for null/empty entries upfront for (int i = 0; i < formats.Length; i++) { - DateTimeFormatInfo dtfiToUse = dtfi; - string? format = formats[i]; - if (string.IsNullOrEmpty(format)) + if (string.IsNullOrEmpty(formats[i])) { - result = default; - return ParseFailureKind.Argument_BadFormatSpecifier; + throw new FormatException(SR.Argument_BadFormatSpecifier); } + } + + return TryParseExactInternal(s, formats, provider, style, out result) == ParseFailureKind.None; + } + + private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, string?[]? formats, IFormatProvider? provider, DateTimeStyles style, out TimeOnly result) + { + DateTimeFormatInfo dtfi = DateTimeFormatInfo.GetInstance(provider); + + for (int i = 0; i < formats!.Length; i++) + { + DateTimeFormatInfo dtfiToUse = dtfi; + string? format = formats[i]; - if (format.Length == 1) + if (format!.Length == 1) { switch (format[0] | 0x20) { @@ -764,6 +817,12 @@ private static ParseFailureKind TryParseExactInternal(ReadOnlySpan s, stri result = FromDateTime(dtResult.parsedDate); return ParseFailureKind.None; } + + if (dtResult.failure == ParseFailureKind.Argument_BadFormatSpecifier) + { + result = default; + return ParseFailureKind.Argument_BadFormatSpecifier; + } } result = default; diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateOnlyTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateOnlyTests.cs index d32f4c391fc8e9..4a52dd2bbc3869 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateOnlyTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateOnlyTests.cs @@ -349,14 +349,14 @@ public static void BasicFormatParseTest() Assert.Equal(dateOnly, parsedDateOnly); Assert.Equal(dateOnly, parsedDateOnly1); - Assert.False(DateOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out parsedDateOnly1)); - Assert.Throws(() => DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)); - Assert.False(DateOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out parsedDateOnly1)); - Assert.Throws(() => DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal)); - Assert.False(DateOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out parsedDateOnly1)); - Assert.Throws(() => DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)); - Assert.False(DateOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out parsedDateOnly1)); - Assert.Throws(() => DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault)); + AssertExtensions.Throws("style", () => DateOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out parsedDateOnly1)); + AssertExtensions.Throws("style", () => DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)); + AssertExtensions.Throws("style", () => DateOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out parsedDateOnly1)); + AssertExtensions.Throws("style", () => DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal)); + AssertExtensions.Throws("style", () => DateOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out parsedDateOnly1)); + AssertExtensions.Throws("style", () => DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)); + AssertExtensions.Throws("style", () => DateOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out parsedDateOnly1)); + AssertExtensions.Throws("style", () => DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault)); s = " " + s + " "; parsedDateOnly = DateOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AllowWhiteSpaces); @@ -554,5 +554,57 @@ public static void TryFormatTest() Assert.Throws(() => $"{dateOnly:hh-ss}"); } } + + [Fact] + public static void TryParseExact_InvalidDateTimeStyles_ThrowsArgumentException() + { + // Test that TryParseExact throws ArgumentException for invalid DateTimeStyles + string validInput = "2064-07-01"; + string format = "yyyy-MM-dd"; + DateOnly result; + + // Test ReadOnlySpan overload with single format + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput.AsSpan(), format.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result)); + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput.AsSpan(), format.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out result)); + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput.AsSpan(), format.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result)); + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput.AsSpan(), format.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out result)); + + // Test string overload with single format + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result)); + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out result)); + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput, format, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result)); + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput, format, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out result)); + + // Test ReadOnlySpan overload with multiple formats + string[] formats = new[] { "yyyy-MM-dd", "MM/dd/yyyy" }; + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput.AsSpan(), formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result)); + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput.AsSpan(), formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out result)); + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput.AsSpan(), formats, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result)); + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput.AsSpan(), formats, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out result)); + + // Test string overload with multiple formats + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result)); + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out result)); + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput, formats, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result)); + AssertExtensions.Throws("style", () => DateOnly.TryParseExact(validInput, formats, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out result)); + } + + [Fact] + public static void TryParseExact_InvalidFormatSpecifier_ThrowsFormatException() + { + // Test that TryParseExact throws FormatException for null/empty format specifiers + string validInput = "2064-07-01"; + DateOnly result; + + // Test with array containing null format as first element + string?[] formatsWithNull = new string?[] { null, "yyyy-MM-dd", "MM/dd/yyyy" }; + Assert.Throws(() => DateOnly.TryParseExact(validInput.AsSpan(), formatsWithNull, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)); + Assert.Throws(() => DateOnly.TryParseExact(validInput, formatsWithNull, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)); + + // Test with array containing empty format as first element + string[] formatsWithEmpty = new[] { "", "yyyy-MM-dd", "MM/dd/yyyy" }; + Assert.Throws(() => DateOnly.TryParseExact(validInput.AsSpan(), formatsWithEmpty, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)); + Assert.Throws(() => DateOnly.TryParseExact(validInput, formatsWithEmpty, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)); + } } } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeOnlyTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeOnlyTests.cs index 75f14b5ab4f6bd..d98c055b54eef4 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeOnlyTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/TimeOnlyTests.cs @@ -388,13 +388,13 @@ public static void BasicFormatParseTest() Assert.True(TimeOnly.TryParse(s.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.None, out parsedTimeOnly1)); Assert.Equal(parsedTimeOnly, parsedTimeOnly1); - Assert.False(TimeOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out parsedTimeOnly1)); + AssertExtensions.Throws("style", () => TimeOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out parsedTimeOnly1)); AssertExtensions.Throws("style", () => TimeOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)); - Assert.False(TimeOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out parsedTimeOnly1)); + AssertExtensions.Throws("style", () => TimeOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out parsedTimeOnly1)); AssertExtensions.Throws("style", () => TimeOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal)); - Assert.False(TimeOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out parsedTimeOnly1)); + AssertExtensions.Throws("style", () => TimeOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out parsedTimeOnly1)); AssertExtensions.Throws("style", () => TimeOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)); - Assert.False(TimeOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out parsedTimeOnly1)); + AssertExtensions.Throws("style", () => TimeOnly.TryParse(s, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out parsedTimeOnly1)); AssertExtensions.Throws("style", () => TimeOnly.Parse(s, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault)); s = " " + s + " "; @@ -620,5 +620,57 @@ public static void TryFormatTest() Assert.Throws(() => $"{timeOnly:dd-yyyy}"); } } + + [Fact] + public static void TryParseExact_InvalidDateTimeStyles_ThrowsArgumentException() + { + // Test that TryParseExact throws ArgumentException for invalid DateTimeStyles + string validInput = "14:30:00"; + string format = "HH:mm:ss"; + TimeOnly result; + + // Test ReadOnlySpan overload with single format + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput.AsSpan(), format.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result)); + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput.AsSpan(), format.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out result)); + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput.AsSpan(), format.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result)); + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput.AsSpan(), format.AsSpan(), CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out result)); + + // Test string overload with single format + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result)); + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out result)); + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput, format, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result)); + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput, format, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out result)); + + // Test ReadOnlySpan overload with multiple formats + string[] formats = new[] { "HH:mm:ss", "hh:mm:ss tt" }; + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput.AsSpan(), formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result)); + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput.AsSpan(), formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out result)); + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput.AsSpan(), formats, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result)); + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput.AsSpan(), formats, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out result)); + + // Test string overload with multiple formats + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result)); + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput, formats, CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out result)); + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput, formats, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal, out result)); + AssertExtensions.Throws("style", () => TimeOnly.TryParseExact(validInput, formats, CultureInfo.InvariantCulture, DateTimeStyles.NoCurrentDateDefault, out result)); + } + + [Fact] + public static void TryParseExact_InvalidFormatSpecifier_ThrowsFormatException() + { + // Test that TryParseExact throws FormatException for null/empty format specifiers + string validInput = "14:30:00"; + TimeOnly result; + + // Test with array containing null format as first element + string?[] formatsWithNull = new string?[] { null, "HH:mm:ss", "hh:mm:ss tt" }; + Assert.Throws(() => TimeOnly.TryParseExact(validInput.AsSpan(), formatsWithNull, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)); + Assert.Throws(() => TimeOnly.TryParseExact(validInput, formatsWithNull, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)); + + // Test with array containing empty format as first element + string[] formatsWithEmpty = new[] { "", "HH:mm:ss", "hh:mm:ss tt" }; + Assert.Throws(() => TimeOnly.TryParseExact(validInput.AsSpan(), formatsWithEmpty, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)); + Assert.Throws(() => TimeOnly.TryParseExact(validInput, formatsWithEmpty, CultureInfo.InvariantCulture, DateTimeStyles.None, out result)); + } } }