I am working my way through some code challenges — partly to improve my problem solving, but also to improve my code quality.
I think my current (fully-functional) solution to a challenge is pretty solid, and fairly understandable. I would love to know, however, if there is anything in my style which could be improved.
Readers are encouraged to be opinionated, but to clearly-demarcate what is purely personal preference from what is a preference, but also strongly advised.
Good morning! Here's your coding interview problem for today.
This problem was asked by Facebook.
Implement regular expression matching with the following special characters:
.(period) which matches any single character
*(asterisk) which matches zero or more of the preceding elementThat is, implement a function that takes in a string and a valid regular expression and returns whether or not the string matches the regular expression.
For example, given the regular expression
"ra."and the string"ray", your function should return true. The same regular expression on the string"Raymond"should return false.Given the regular expression
".\*at"and the string"chat", your function should return true. The same regular expression on the string"chats"should return false.
# solution.py
from collections import deque
import logging
from typing import Deque, NamedTuple, Optional, Union
logging.basicConfig(level=logging.INFO,format="%(asctime)s - %(levelname)s - %(name)s - %(funcName)s - %(lineno)d : %(message)s")
class Attempt(NamedTuple):
matched_string : str
remaining_string : str
remaining_pattern : str
class DecisionPoint(NamedTuple):
greedy : Attempt
non_greedy : Attempt
def advance_attempt(attempt: Attempt, greedy = False) -> Attempt:
char = attempt.remaining_pattern[0]
if greedy and char!= '*':
raise RuntimeError("Can't perform a greedy advancement on a non wildcard char.")
new_matched_string = attempt.matched_string + char
if greedy:
new_remaining_pattern = attempt.remaining_pattern
else:
new_remaining_pattern = attempt.remaining_pattern[1:]
new_remaining_string = attempt.remaining_string[1:]
return Attempt(matched_string=new_matched_string,
remaining_pattern=new_remaining_pattern,
remaining_string=new_remaining_string)
def next_char_is_match(next_char : str, string : str) -> bool:
"""Returns true if the supplied next_char in the pattern matches the
head of the string. Next char is expected to be a single char or an
empty string."""
if next_char == '':
return False
if next_char == '*':
return True
if next_char == '.' and len(string) != 0:
return True
return next_char == string[0]
def take_next_greedy(attempt: Attempt) -> Attempt:
return advance_attempt(attempt=attempt,greedy=True)
def take_next_non_greedy(attempt : Attempt) -> Attempt:
return advance_attempt(attempt)
def take_next(attempt : Attempt) -> Optional[Union[DecisionPoint,Attempt]]:
"""Returns None if no next match is possible; otherwise, returns either
a single progression of the attempt — i.e., a single character moved into
the matched field from the remaining field — or, in the case of a wildcard,
both the greedy and non-greedy next possibilities."""
if (len(attempt.remaining_string) == 0 \
or len(attempt.remaining_pattern) == 0):
return None
next_char_to_match_in_pattern = attempt.remaining_pattern[0]
next_char_to_match_in_string = attempt.remaining_string[0]
if next_char_to_match_in_pattern == '*':
return DecisionPoint(greedy=take_next_greedy(attempt),
non_greedy=take_next_non_greedy(attempt))
if (next_char_to_match_in_pattern == next_char_to_match_in_string\
or next_char_to_match_in_pattern == '.'):
return advance_attempt(attempt)
def main_loop(queue : Deque[Attempt]) -> bool:
logging.debug(queue)
logging.info(len(queue))
attempt = queue.popleft()
logging.info(attempt)
if attempt.remaining_pattern == ''\
and attempt.remaining_string == '':
return True
next_step = take_next(attempt)
if next_step is None:
return False
elif isinstance(next_step,DecisionPoint):
queue.appendleft(next_step.non_greedy)
queue.appendleft(next_step.greedy)
return False
else:
queue.appendleft(next_step)
return False
def main(pattern : str, string : str) -> bool:
q = deque()
q.append(Attempt(matched_string='',
remaining_string=string,
remaining_pattern=pattern))
match_found = False
while len(q) != 0 and not match_found:
match_found = main_loop(q)
logging.info(match_found)
return match_found
if __name__ == '__main__':
main('.*at*.rq','chatsdafrzafafrq')
# tests.py
import unittest
from .solution import main
class BasicTests(unittest.TestCase):
def test_given_which_is_expected_to_match_matches(self):
computed_result = main('ra.','ray')
self.assertTrue(computed_result)
def test_given_which_is_expected_to_not_match_does_not_match(self):
computed_result = main('ra.','raymond')
self.assertFalse(computed_result)
def test_other_given_which_is_expected_to_match_matches(self):
computed_result = main('.*at','chat')
self.assertTrue(computed_result)
def test_complex_which_is_expected_to_match_matches(self):
computed_result = main('.*jk*.wee*.weq','jkhkh;llkjkljklkjljl;weeklsfdjdsfj;weq')
self.assertTrue(computed_result)
def test_complex_which_is_expected_not_to_match_does_not_match(self):
computed_result = main('.*jk*.wee*.weqr','jkhkh;llkjkljklkjljl;weeklsfdjdsfj;weqtjhuafjs')
self.assertFalse(computed_result)
if __name__ == '__main__':
unittest.main()