Skip to content

Commit

Permalink
Add Wildcard Pattern Matching (#470)
Browse files Browse the repository at this point in the history
  • Loading branch information
RicardoRibeiroRodrigues authored Sep 3, 2024
1 parent 5c09c0e commit 6b37d04
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 0 deletions.
36 changes: 36 additions & 0 deletions Algorithms.Tests/Strings/PatternMatching/WildCardMatcherTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Algorithms.Strings.PatternMatching;
using NUnit.Framework;

namespace Algorithms.Tests.Strings.PatternMatching;

public static class WildCardMatcherTests
{
[TestCase("aab", "c*a*b", true)]
[TestCase("aaa", "aa", false)]
[TestCase("aaa", "a.a", true)]
[TestCase("aaab", "aa*", false)]
[TestCase("aaab", ".*", true)]
[TestCase("a", "bbbb", false)]
[TestCase("", "bbbb", false)]
[TestCase("a", "", false)]
[TestCase("", "", true)]
public static void MatchPattern(string inputString, string pattern, bool expected)
{
// Act
var result = WildCardMatcher.MatchPattern(inputString, pattern);

// Assert
Assert.That(result, Is.EqualTo(expected));
}

[Test]
public static void MatchPatternThrowsArgumentException()
{
// Arrange
var inputString = "abc";
var pattern = "*abc";

// Assert
Assert.Throws<System.ArgumentException>(() => WildCardMatcher.MatchPattern(inputString, pattern));
}
}
97 changes: 97 additions & 0 deletions Algorithms/Strings/PatternMatching/WildCardMatcher.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System;

namespace Algorithms.Strings.PatternMatching;

/// <summary>
/// Implentation of regular expression matching with support for '.' and '*'.
/// '.' Matches any single character.
/// '*' Matches zero or more of the preceding element.
/// The matching should cover the entire input string (not partial).
/// </summary>
public static class WildCardMatcher
{
/// <summary>
/// Using bottom-up dynamic programming for matching the input string with the pattern.
///
/// Time complexity: O(n*m), where n is the length of the input string and m is the length of the pattern.
///
/// Constrain: The pattern cannot start with '*'.
/// </summary>
/// <param name="inputString">The input string to match.</param>
/// <param name="pattern">The pattern to match.</param>
/// <returns>True if the input string matches the pattern, false otherwise.</returns>
/// <exception cref="ArgumentException">Thrown when the pattern starts with '*'.</exception>
public static bool MatchPattern(string inputString, string pattern)
{
if (pattern.Length > 0 && pattern[0] == '*')
{
throw new ArgumentException("Pattern cannot start with *");
}

var inputLength = inputString.Length + 1;
var patternLength = pattern.Length + 1;

// DP 2d matrix, where dp[i, j] is true if the first i characters in the input string match the first j characters in the pattern
// This DP is initialized to all falses, as it is the default value for a boolean.
var dp = new bool[inputLength, patternLength];

// Empty string and empty pattern are a match
dp[0, 0] = true;

// Since the empty string can only match a pattern that has a * in it, we need to initialize the first row of the DP matrix
for (var j = 1; j < patternLength; j++)
{
if (pattern[j - 1] == '*')
{
dp[0, j] = dp[0, j - 2];
}
}

// Now using bottom-up approach to find for all remaining lenghts of input and pattern
for (var i = 1; i < inputLength; i++)
{
for (var j = 1; j < patternLength; j++)
{
MatchRemainingLenghts(inputString, pattern, dp, i, j);
}
}

return dp[inputLength - 1, patternLength - 1];
}

// Helper method to match the remaining lengths of the input string and the pattern
// This method is called for all i and j where i > 0 and j > 0
private static void MatchRemainingLenghts(string inputString, string pattern, bool[,] dp, int i, int j)
{
// If the characters match or the pattern has a ., then the result is the same as the previous positions.
if (inputString[i - 1] == pattern[j - 1] || pattern[j - 1] == '.')
{
dp[i, j] = dp[i - 1, j - 1];
}
else if (pattern[j - 1] == '*')
{
MatchForZeroOrMore(inputString, pattern, dp, i, j);
}
else
{
// If the characters do not match, then the result is false, which is the default value.
}
}

// Helper method to match for the "*" pattern.
private static void MatchForZeroOrMore(string inputString, string pattern, bool[,] dp, int i, int j)
{
if (dp[i, j - 2])
{
dp[i, j] = true;
}
else if (inputString[i - 1] == pattern[j - 2] || pattern[j - 2] == '.')
{
dp[i, j] = dp[i - 1, j];
}
else
{
// Leave the default value of false
}
}
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ find more than one implementation for the same objective but using different alg
* [Rabin Karp](./Algorithms/Strings/PatternMatching/RabinKarp.cs)
* [Boyer Moore](./Algorithms/Strings/PatternMatching/BoyerMoore.cs)
* [Knuth–Morris–Pratt Search](./Algorithms/Strings/PatternMatching/KnuthMorrisPrattSearcher.cs)
* [WildCard Pattern Matching](./Algorithms/Strings/PatternMatching/WildCardMatcher.cs)
* [Z-block substring search](./Algorithms/Strings/PatternMatching/ZblockSubstringSearch.cs)
* [Longest Consecutive Character](./Algorithms/Strings/GeneralStringAlgorithms.cs)
* [Palindrome Checker](./Algorithms/Strings/Palindrome.cs)
Expand Down

0 comments on commit 6b37d04

Please sign in to comment.