Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -3289,6 +3289,51 @@ private static bool ParseTimeZoneOffset(ref __DTString str, int len, scoped ref
return true;
}

/// Determines if a format string contains a day-of-month specifier ('d' or 'dd').
/// Properly handles quoted sections and escape characters.
private static bool FormatContainsDayOfMonthSpecifier(ReadOnlySpan<char> format)
{
if (format.IsEmpty)
{
return false;
}
bool inQuote = false;
for (int i = 0; i < format.Length; i++)
{
char ch = format[i];
// Skip the next character if it's escaped
if (ch == '\\' || ch == '%')
{
i++;
continue;
}
// Toggle quote state
if (ch == '\'' || ch == '"')
{
inQuote = !inQuote;
continue;
}
// Only check for 'd' when not in quotes
if (!inQuote && ch == 'd')
{
// Make sure it's a day-of-month specifier (d or dd)
// and not a day-of-week specifier (ddd or dddd)
int repeatCount = 1;
while (i + 1 < format.Length && format[i + 1] == 'd')
{
repeatCount++;
i++;
}
// Only day-of-month specifiers (d or dd) trigger genitive case
if (repeatCount <= 2)
{
return true;
}
}
}
return false;
}

/*=================================MatchAbbreviatedMonthName==================================
**Action: Parse the abbreviated month name from string starting at str.Index.
**Returns: A value from 1 to 12 for the first month to the twelfth month.
Expand All @@ -3297,7 +3342,7 @@ private static bool ParseTimeZoneOffset(ref __DTString str, int len, scoped ref
**Exceptions: FormatException if an abbreviated month name can not be found.
==============================================================================*/

private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormatInfo dtfi, scoped ref int result)
private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormatInfo dtfi, scoped ref int result, ReadOnlySpan<char> format)
{
int maxMatchStrLen = 0;
result = -1;
Expand Down Expand Up @@ -3357,12 +3402,11 @@ private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormat
}
}

// Search genitive form.
if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0)
// Search genitive form only if the format contains a day-of-month specifier
if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0 && FormatContainsDayOfMonthSpecifier(format))
{
int tempResult = str.MatchLongestWords(dtfi.InternalGetGenitiveMonthNames(abbreviated: true), ref maxMatchStrLen);

// We found a longer match in the genitive month name. Use this as the result.
// We found a longer match in the genitive month name. Use this as the result.
// tempResult + 1 should be the month value.
if (tempResult >= 0)
{
Expand Down Expand Up @@ -3399,7 +3443,7 @@ private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormat
**Exceptions: FormatException if a month name can not be found.
==============================================================================*/

private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi, scoped ref int result)
private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi, scoped ref int result, ReadOnlySpan<char> format)
{
int maxMatchStrLen = 0;
result = -1;
Expand Down Expand Up @@ -3458,7 +3502,7 @@ private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi,
}

// Search genitive form.
if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0)
if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0 && FormatContainsDayOfMonthSpecifier(format))
{
int tempResult = str.MatchLongestWords(dtfi.InternalGetGenitiveMonthNames(abbreviated: false), ref maxMatchStrLen);
// We found a longer match in the genitive month name. Use this as the result.
Expand Down Expand Up @@ -4045,15 +4089,15 @@ private static bool ParseByFormat(
{
if (tokenLen == 3)
{
if (!MatchAbbreviatedMonthName(ref str, dtfi, ref tempMonth))
if (!MatchAbbreviatedMonthName(ref str, dtfi, ref tempMonth, format.Value))
{
result.SetBadDateTimeFailure();
return false;
}
}
else
{
if (!MatchMonthName(ref str, dtfi, ref tempMonth))
if (!MatchMonthName(ref str, dtfi, ref tempMonth, format.Value))
{
result.SetBadDateTimeFailure();
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,55 @@ namespace System.Tests
{
public class DateTimeTests
{
[Fact]
public static void ParseExact_GenitiveMonthNames()
{
// Create a German culture with explicitly defined genitive month names
var culture = new CultureInfo("de-DE");
culture.DateTimeFormat.AbbreviatedMonthGenitiveNames = new[] { "Jan.", "Feb.", "März", "Apr.", "Mai", "Juni", "Juli", "Aug.", "Sept.", "Okt.", "Nov.", "Dez.", "" };
culture.DateTimeFormat.MonthGenitiveNames = new[] { "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember.", "" };
culture.DateTimeFormat.DayNames = new[] { "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" };
culture.DateTimeFormat.DateSeparator = ".";
// Regular month names (non-genitive)
culture.DateTimeFormat.AbbreviatedMonthNames = new[] { "Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez", "" };
culture.DateTimeFormat.MonthNames = new[] { "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember", "" };
// Test cases for abbreviated month names (MMM)
// Case 1: Format without day specifier - should use regular month name
DateTime result;
bool success = DateTime.TryParseExact("Dez.20", "MMM.yy", culture, DateTimeStyles.None, out result);
Assert.True(success);
Assert.Equal(12, result.Month);
Assert.Equal(2020, result.Year);
// Case 2: Format with day-of-month specifier - should use genitive month name
success = DateTime.TryParseExact("Dez.20 01", "MMM.yy dd", culture, DateTimeStyles.None, out result);
Assert.False(success);
// Case 3: Format with day-of-month specifier before month - should use genitive month name
success = DateTime.TryParseExact("01 Dez.20", "d MMM.yy", culture, DateTimeStyles.None, out result);
Assert.False(success);
// Case 4: Format with day-of-week specifier - should use regular month name
success = DateTime.TryParseExact("Dienstag Dez.20", "dddd MMM.yy", culture, DateTimeStyles.None, out result);
Assert.True(success);
Assert.Equal(12, result.Month);
Assert.Equal(2020, result.Year);
// Test cases for full month names (MMMM)
// Case 5: Format without day specifier - should use regular month name
success = DateTime.TryParseExact("Dezember.20", "MMMM.yy", culture, DateTimeStyles.None, out result);
Assert.True(success);
Assert.Equal(12, result.Month);
Assert.Equal(2020, result.Year);
// Case 6: Format with day-of-month specifier - should use genitive month name
success = DateTime.TryParseExact("Dezember.20 01", "MMMM.yy dd", culture, DateTimeStyles.None, out result);
Assert.False(success);
// Case 7: Format with day-of-month specifier before month - should use genitive month name
success = DateTime.TryParseExact("01 Dezember.20", "d MMMM.yy", culture, DateTimeStyles.None, out result);
Assert.False(success);
// Case 8: Format with day-of-week specifier - should use regular month name
success = DateTime.TryParseExact("Dienstag Dezember.20", "dddd MMMM.yy", culture, DateTimeStyles.None, out result);
Assert.True(success);
Assert.Equal(12, result.Month);
Assert.Equal(2020, result.Year);
}

[Fact]
public static void MaxValue()
{
Expand Down
Loading