From 3a34b199357e32e23f81fb75d5e9a2722145e34a Mon Sep 17 00:00:00 2001 From: Stefan Bodewig Date: Sun, 5 Jul 2026 21:03:05 +0200 Subject: [PATCH 1/3] add an optional CultureInfo argument to isDateTime placeholder see https://github.com/xmlunit/xmlunit/pull/335 --- RELEASE_NOTES.md | 5 +++ .../IsDateTimePlaceholderHandler.cs | 27 +++++++++++++--- .../IsDateTimePlaceholderHandlerTests.cs | 31 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ab843eb..4a7bc8e 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,11 @@ ## XMLUnit.NET 2.11.2 - /not released, yet/ +* `IsDateTimePlaceholderHandler` now supports an optional second argument specifying a + different CultureInfo than the invariant culture used by default as a BCP 47 language tag. + + based on PR [xmlunit/#335](https://github.com/xmlunit/xmlunit/pull/335) by [@jmestwa-coder](https://github.com/jmestwa-coder) + ## XMLUnit.NET 2.11.1 - /Released 2025-05-19/ * placeholders can now also be used inside of the local part of `xsi:type` attributes. diff --git a/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs b/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs index 3e8405c..5befda7 100644 --- a/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs +++ b/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs @@ -28,6 +28,12 @@ namespace Org.XmlUnit.Placeholder /// /// since 2.8.0 /// + /// + /// The first optional argument is the date/time pattern, the second optional + /// argument is the culture used to parse it, given as a culture name (for example + /// de or fr-FR). When no culture is given + /// is used. + /// /// public class IsDateTimePlaceholderHandler : IPlaceholderHandler { @@ -37,10 +43,23 @@ public class IsDateTimePlaceholderHandler : IPlaceholderHandler public string Keyword { get { return _keyword; } } /// + /// + /// + /// When contains one element, it is used as the date/time pattern + /// and is used for parsing. + /// + /// + /// When contains two elements, the first is used as the date/time + /// pattern and the second as the culture name for parsing. + /// + /// public ComparisonResult Evaluate(string testText, params string[] args) { - if (args != null && args.Length == 1) { - return CanParse(args[0], testText) + if (args != null && args.Length >= 1) { + var culture = args.Length >= 2 && !string.IsNullOrEmpty(args[1]) + ? new CultureInfo(args[1]) + : CultureInfo.InvariantCulture; + return CanParse(args[0], testText, culture) ? ComparisonResult.EQUAL : ComparisonResult.DIFFERENT; } @@ -51,9 +70,9 @@ public ComparisonResult Evaluate(string testText, params string[] args) : ComparisonResult.DIFFERENT; } - private bool CanParse(string pattern, string testText) { + private bool CanParse(string pattern, string testText, CultureInfo culture) { try { - var _ = DateTime.ParseExact(testText, pattern, CultureInfo.InvariantCulture); + var _ = DateTime.ParseExact(testText, pattern, culture); return true; } catch (FormatException) { return false; diff --git a/src/tests/net-placeholders/IsDateTimePlaceholderHandlerTests.cs b/src/tests/net-placeholders/IsDateTimePlaceholderHandlerTests.cs index 7a02145..f0b1a36 100644 --- a/src/tests/net-placeholders/IsDateTimePlaceholderHandlerTests.cs +++ b/src/tests/net-placeholders/IsDateTimePlaceholderHandlerTests.cs @@ -71,5 +71,36 @@ public void ShouldParseExplicitPattern() { placeholderHandler.Evaluate("abc", "dd MM yyyy HH:mm")); } + [Test] + public void ShouldParsePatternIndependentOfDefaultLocale() { + // Test that parsing works regardless of current culture + // This corresponds to the Java test shouldParsePatternIndependentOfDefaultLocale + var originalCulture = Thread.CurrentThread.CurrentCulture; + try { + Thread.CurrentThread.CurrentCulture = new CultureInfo("de-DE"); // German culture + // Should still parse with InvariantCulture (default for explicit patterns) + Assert.AreEqual(ComparisonResult.EQUAL, + placeholderHandler.Evaluate("24 June 2023", "dd MMMM yyyy")); + } finally { + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Test] + public void ShouldParsePatternWithExplicitLocale() { + // Test explicit locale specification - corresponds to Java test shouldParsePatternWithExplicitLocale + Assert.AreEqual(ComparisonResult.EQUAL, + placeholderHandler.Evaluate("24 Juni 2023", "dd MMMM yyyy", "de")); + Assert.AreEqual(ComparisonResult.DIFFERENT, + placeholderHandler.Evaluate("24 Juni 2023", "dd MMMM yyyy", "en")); + } + + [Test] + public void ShouldUseInvariantCultureWithTwoArgsWhenSecondIsEmpty() { + // When second argument is empty string, should fall back to InvariantCulture + Assert.AreEqual(ComparisonResult.EQUAL, + placeholderHandler.Evaluate("24 June 2023", "dd MMMM yyyy", "")); + } + } } From 9e03af7ff8c5779eb4f002cfdcf1b399e177e097 Mon Sep 17 00:00:00 2001 From: Stefan Bodewig Date: Sun, 5 Jul 2026 21:25:54 +0200 Subject: [PATCH 2/3] port ISO datetime patterns of Java version of `isDateTime` --- RELEASE_NOTES.md | 8 ++- .../IsDateTimePlaceholderHandler.cs | 49 +++++++++++++++++-- .../IsDateTimePlaceholderHandlerTests.cs | 20 ++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4a7bc8e..f881918 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -5,7 +5,13 @@ * `IsDateTimePlaceholderHandler` now supports an optional second argument specifying a different CultureInfo than the invariant culture used by default as a BCP 47 language tag. - based on PR [xmlunit/#335](https://github.com/xmlunit/xmlunit/pull/335) by [@jmestwa-coder](https://github.com/jmestwa-coder) + Also modified the logic when no argument is present to still try + parsing using the current culture and then trying a set of ISO + patterns using the invariant culture in turn - making `isDateTime` + match what the Java version does mor closely. + + PR [#54](https://github.com/xmlunit/xmlunit.net/pull/54) based on + PR [xmlunit/#335](https://github.com/xmlunit/xmlunit/pull/335) by [@jmestwa-coder](https://github.com/jmestwa-coder) ## XMLUnit.NET 2.11.1 - /Released 2025-05-19/ diff --git a/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs b/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs index 5befda7..aa6fb4b 100644 --- a/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs +++ b/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs @@ -39,6 +39,21 @@ public class IsDateTimePlaceholderHandler : IPlaceholderHandler { private const string _keyword = "isDateTime"; + private static readonly IReadOnlyList _isoPatterns = new List + { + "yyyy-MM-dd", + "yyyy-MM-ddTHH:mm", + "yyyy-MM-ddTHH:mm:ss", + "yyyy-MM-ddTHH:mm:ss.fff", + "yyyy-MM-ddTHH:mm:ssK", + "yyyy-MM-ddTHH:mm:ss.fffK", + "yyyy-MM-dd HH:mm", + "yyyy-MM-dd HH:mm:ss", + "yyyy-MM-dd HH:mm:ss.fff", + "yyyy-MM-dd HH:mm:ssK", + "yyyy-MM-dd HH:mm:ss.fffK" + }; + /// public string Keyword { get { return _keyword; } } @@ -52,6 +67,10 @@ public class IsDateTimePlaceholderHandler : IPlaceholderHandler /// When contains two elements, the first is used as the date/time /// pattern and the second as the culture name for parsing. /// + /// + /// When no arguments are provided, the method tries to parse the text using ISO patterns + /// with . + /// /// public ComparisonResult Evaluate(string testText, params string[] args) { @@ -63,13 +82,37 @@ public ComparisonResult Evaluate(string testText, params string[] args) ? ComparisonResult.EQUAL : ComparisonResult.DIFFERENT; } - DateTime _; - var result = DateTime.TryParse(testText, out _); - return result + return CanParse(testText) ? ComparisonResult.EQUAL : ComparisonResult.DIFFERENT; } + private bool CanParse(string testText) { + if (string.IsNullOrEmpty(testText)) { + return false; + } + // Try locale-aware short date and datetime formats with current culture + if (CanParseWithCurrentCulture(testText)) { + return true; + } + // Try all ISO patterns with InvariantCulture + foreach (var pattern in _isoPatterns) { + if (CanParse(pattern, testText, CultureInfo.InvariantCulture)) { + return true; + } + } + return false; + } + + private bool CanParseWithCurrentCulture(string testText) { + DateTime result; + // Try short date format + if (DateTime.TryParse(testText, out result)) { + return true; + } + return false; + } + private bool CanParse(string pattern, string testText, CultureInfo culture) { try { var _ = DateTime.ParseExact(testText, pattern, culture); diff --git a/src/tests/net-placeholders/IsDateTimePlaceholderHandlerTests.cs b/src/tests/net-placeholders/IsDateTimePlaceholderHandlerTests.cs index f0b1a36..dcf5df6 100644 --- a/src/tests/net-placeholders/IsDateTimePlaceholderHandlerTests.cs +++ b/src/tests/net-placeholders/IsDateTimePlaceholderHandlerTests.cs @@ -63,6 +63,26 @@ public void ShouldGetKeyword() Assert.AreEqual(expected, keyword); } + [Test] + public void ShouldAcceptABunchOfStrings() + { + // Test various date formats that should be accepted by ISO patterns + // Corresponds to Java test shouldAcceptABunchOfStrings + var testStrings = new string[] { + "2020-01-01", + "01/01/2020", + "01/01/2020", + "2020-01-01T15:00", + "2020-01-01 15:00:00Z", + "01/01/2020 15:00" + }; + + foreach (var testString in testStrings) + { + Assert.AreEqual(ComparisonResult.EQUAL, placeholderHandler.Evaluate(testString)); + } + } + [Test] public void ShouldParseExplicitPattern() { Assert.AreEqual(ComparisonResult.EQUAL, From 65ebdd32f5dc8d4e296f5f6c8aee20fd2beaef0b Mon Sep 17 00:00:00 2001 From: Stefan Bodewig Date: Sun, 5 Jul 2026 21:38:21 +0200 Subject: [PATCH 3/3] IReadOnylyList didn't exist in .NET Framework 3.5 --- src/main/net-placeholders/IsDateTimePlaceholderHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs b/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs index aa6fb4b..bd6d949 100644 --- a/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs +++ b/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs @@ -39,7 +39,7 @@ public class IsDateTimePlaceholderHandler : IPlaceholderHandler { private const string _keyword = "isDateTime"; - private static readonly IReadOnlyList _isoPatterns = new List + private static readonly IEnumerable _isoPatterns = new List { "yyyy-MM-dd", "yyyy-MM-ddTHH:mm",