diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index ab843eb..f881918 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -2,6 +2,17 @@ ## 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. + + 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/ * 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..bd6d949 100644 --- a/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs +++ b/src/main/net-placeholders/IsDateTimePlaceholderHandler.cs @@ -28,32 +28,94 @@ 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 { private const string _keyword = "isDateTime"; + private static readonly IEnumerable _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; } } /// + /// + /// + /// 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. + /// + /// + /// When no arguments are provided, the method tries to parse the text using ISO patterns + /// with . + /// + /// 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; } - DateTime _; - var result = DateTime.TryParse(testText, out _); - return result + return CanParse(testText) ? ComparisonResult.EQUAL : ComparisonResult.DIFFERENT; } - private bool CanParse(string pattern, string testText) { + 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, 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..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, @@ -71,5 +91,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", "")); + } + } }