Skip to content

Commit 36f59e8

Browse files
pra2892tarekgh
andauthored
Fix "MMM.yy" date parsing in German culture. #114169 (#114194)
* Fix "MMM.yy" date parsing in German culture. #114169 * Fix "MMM.yy" date parsing in German culture. #114169 * Fix "MMM.yy" date parsing in German culture. #114169 v3 * Fix "MMM.yy" date parsing in German culture. #114169 [Testing] * Fix "MMM.yy" date parsing in German culture. #114169 [Testing] v1 * Fix the test --------- Co-authored-by: Tarek Mahmoud Sayed <[email protected]>
1 parent c67a8ce commit 36f59e8

File tree

2 files changed

+102
-9
lines changed

2 files changed

+102
-9
lines changed

src/libraries/System.Private.CoreLib/src/System/Globalization/DateTimeParse.cs

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3289,6 +3289,51 @@ private static bool ParseTimeZoneOffset(ref __DTString str, int len, scoped ref
32893289
return true;
32903290
}
32913291

3292+
/// Determines if a format string contains a day-of-month specifier ('d' or 'dd').
3293+
/// Properly handles quoted sections and escape characters.
3294+
private static bool FormatContainsDayOfMonthSpecifier(ReadOnlySpan<char> format)
3295+
{
3296+
if (format.IsEmpty)
3297+
{
3298+
return false;
3299+
}
3300+
bool inQuote = false;
3301+
for (int i = 0; i < format.Length; i++)
3302+
{
3303+
char ch = format[i];
3304+
// Skip the next character if it's escaped
3305+
if (ch == '\\' || ch == '%')
3306+
{
3307+
i++;
3308+
continue;
3309+
}
3310+
// Toggle quote state
3311+
if (ch == '\'' || ch == '"')
3312+
{
3313+
inQuote = !inQuote;
3314+
continue;
3315+
}
3316+
// Only check for 'd' when not in quotes
3317+
if (!inQuote && ch == 'd')
3318+
{
3319+
// Make sure it's a day-of-month specifier (d or dd)
3320+
// and not a day-of-week specifier (ddd or dddd)
3321+
int repeatCount = 1;
3322+
while (i + 1 < format.Length && format[i + 1] == 'd')
3323+
{
3324+
repeatCount++;
3325+
i++;
3326+
}
3327+
// Only day-of-month specifiers (d or dd) trigger genitive case
3328+
if (repeatCount <= 2)
3329+
{
3330+
return true;
3331+
}
3332+
}
3333+
}
3334+
return false;
3335+
}
3336+
32923337
/*=================================MatchAbbreviatedMonthName==================================
32933338
**Action: Parse the abbreviated month name from string starting at str.Index.
32943339
**Returns: A value from 1 to 12 for the first month to the twelfth month.
@@ -3297,7 +3342,7 @@ private static bool ParseTimeZoneOffset(ref __DTString str, int len, scoped ref
32973342
**Exceptions: FormatException if an abbreviated month name can not be found.
32983343
==============================================================================*/
32993344

3300-
private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormatInfo dtfi, scoped ref int result)
3345+
private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormatInfo dtfi, scoped ref int result, ReadOnlySpan<char> format)
33013346
{
33023347
int maxMatchStrLen = 0;
33033348
result = -1;
@@ -3357,12 +3402,11 @@ private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormat
33573402
}
33583403
}
33593404

3360-
// Search genitive form.
3361-
if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0)
3405+
// Search genitive form only if the format contains a day-of-month specifier
3406+
if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0 && FormatContainsDayOfMonthSpecifier(format))
33623407
{
33633408
int tempResult = str.MatchLongestWords(dtfi.InternalGetGenitiveMonthNames(abbreviated: true), ref maxMatchStrLen);
3364-
3365-
// We found a longer match in the genitive month name. Use this as the result.
3409+
// We found a longer match in the genitive month name. Use this as the result.
33663410
// tempResult + 1 should be the month value.
33673411
if (tempResult >= 0)
33683412
{
@@ -3399,7 +3443,7 @@ private static bool MatchAbbreviatedMonthName(ref __DTString str, DateTimeFormat
33993443
**Exceptions: FormatException if a month name can not be found.
34003444
==============================================================================*/
34013445

3402-
private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi, scoped ref int result)
3446+
private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi, scoped ref int result, ReadOnlySpan<char> format)
34033447
{
34043448
int maxMatchStrLen = 0;
34053449
result = -1;
@@ -3458,7 +3502,7 @@ private static bool MatchMonthName(ref __DTString str, DateTimeFormatInfo dtfi,
34583502
}
34593503

34603504
// Search genitive form.
3461-
if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0)
3505+
if ((dtfi.FormatFlags & DateTimeFormatFlags.UseGenitiveMonth) != 0 && FormatContainsDayOfMonthSpecifier(format))
34623506
{
34633507
int tempResult = str.MatchLongestWords(dtfi.InternalGetGenitiveMonthNames(abbreviated: false), ref maxMatchStrLen);
34643508
// We found a longer match in the genitive month name. Use this as the result.
@@ -4045,15 +4089,15 @@ private static bool ParseByFormat(
40454089
{
40464090
if (tokenLen == 3)
40474091
{
4048-
if (!MatchAbbreviatedMonthName(ref str, dtfi, ref tempMonth))
4092+
if (!MatchAbbreviatedMonthName(ref str, dtfi, ref tempMonth, format.Value))
40494093
{
40504094
result.SetBadDateTimeFailure();
40514095
return false;
40524096
}
40534097
}
40544098
else
40554099
{
4056-
if (!MatchMonthName(ref str, dtfi, ref tempMonth))
4100+
if (!MatchMonthName(ref str, dtfi, ref tempMonth, format.Value))
40574101
{
40584102
result.SetBadDateTimeFailure();
40594103
return false;

src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DateTimeTests.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,55 @@ namespace System.Tests
1414
{
1515
public class DateTimeTests
1616
{
17+
[Fact]
18+
public static void ParseExact_GenitiveMonthNames()
19+
{
20+
// Create a German culture with explicitly defined genitive month names
21+
var culture = new CultureInfo("de-DE");
22+
culture.DateTimeFormat.AbbreviatedMonthGenitiveNames = new[] { "Jan.", "Feb.", "März", "Apr.", "Mai", "Juni", "Juli", "Aug.", "Sept.", "Okt.", "Nov.", "Dez.", "" };
23+
culture.DateTimeFormat.MonthGenitiveNames = new[] { "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember.", "" };
24+
culture.DateTimeFormat.DayNames = new[] { "Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag" };
25+
culture.DateTimeFormat.DateSeparator = ".";
26+
// Regular month names (non-genitive)
27+
culture.DateTimeFormat.AbbreviatedMonthNames = new[] { "Jan", "Feb", "Mär", "Apr", "Mai", "Jun", "Jul", "Aug", "Sep", "Okt", "Nov", "Dez", "" };
28+
culture.DateTimeFormat.MonthNames = new[] { "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember", "" };
29+
// Test cases for abbreviated month names (MMM)
30+
// Case 1: Format without day specifier - should use regular month name
31+
DateTime result;
32+
bool success = DateTime.TryParseExact("Dez.20", "MMM.yy", culture, DateTimeStyles.None, out result);
33+
Assert.True(success);
34+
Assert.Equal(12, result.Month);
35+
Assert.Equal(2020, result.Year);
36+
// Case 2: Format with day-of-month specifier - should use genitive month name
37+
success = DateTime.TryParseExact("Dez.20 01", "MMM.yy dd", culture, DateTimeStyles.None, out result);
38+
Assert.False(success);
39+
// Case 3: Format with day-of-month specifier before month - should use genitive month name
40+
success = DateTime.TryParseExact("01 Dez.20", "d MMM.yy", culture, DateTimeStyles.None, out result);
41+
Assert.False(success);
42+
// Case 4: Format with day-of-week specifier - should use regular month name
43+
success = DateTime.TryParseExact("Dienstag Dez.20", "dddd MMM.yy", culture, DateTimeStyles.None, out result);
44+
Assert.True(success);
45+
Assert.Equal(12, result.Month);
46+
Assert.Equal(2020, result.Year);
47+
// Test cases for full month names (MMMM)
48+
// Case 5: Format without day specifier - should use regular month name
49+
success = DateTime.TryParseExact("Dezember.20", "MMMM.yy", culture, DateTimeStyles.None, out result);
50+
Assert.True(success);
51+
Assert.Equal(12, result.Month);
52+
Assert.Equal(2020, result.Year);
53+
// Case 6: Format with day-of-month specifier - should use genitive month name
54+
success = DateTime.TryParseExact("Dezember.20 01", "MMMM.yy dd", culture, DateTimeStyles.None, out result);
55+
Assert.False(success);
56+
// Case 7: Format with day-of-month specifier before month - should use genitive month name
57+
success = DateTime.TryParseExact("01 Dezember.20", "d MMMM.yy", culture, DateTimeStyles.None, out result);
58+
Assert.False(success);
59+
// Case 8: Format with day-of-week specifier - should use regular month name
60+
success = DateTime.TryParseExact("Dienstag Dezember.20", "dddd MMMM.yy", culture, DateTimeStyles.None, out result);
61+
Assert.True(success);
62+
Assert.Equal(12, result.Month);
63+
Assert.Equal(2020, result.Year);
64+
}
65+
1766
[Fact]
1867
public static void MaxValue()
1968
{

0 commit comments

Comments
 (0)