Blame | Last modification | View Log | RSS feed
#!/usr/bin/env python# Copyright: Ira W. Snyder# Start Date: 2005-11-05# End Date: 2005-11-05# License: Public Domain## Changelog Follows:# - 2005-11-05# - Ported over the necessary parts of the automaton class from Project #1.# This is now called DFA.# - Added the functions read_dfa_table() and find_end_of_occurrence() to DFA.# - Ported over the necessary parts of the nfa class from Project #2.# This is now called NFA.# - Added the function from_pattern_to_dfa() to NFA.# - Created the StringChecker class.## Check for <Python-2.3 compatibility (boolean values)try:True, Falseexcept NameError:(True, False) = (1, 0)import string, sysclass DFA:"""Implements a Deterministic Finite-State Automaton"""def __init__(self):"""Constructor"""self.clear()self.verbose = Falsedef clear(self):"""Clear all of the important private variables"""self.automaton_entered = Falseself.num_states = 0self.num_letters = 0self.final_states = []self.trans_function = []def check_state(self, s):"""Checks the string representing a state to see if it's a valid state.Returns True if it is valid, False otherwise"""retVal = Truetry:state = int(s)except (TypeError, ValueError):retVal = Falsestate = 99999999if state >= self.num_states:retVal = Falsereturn retValdef get_letter_num(self, s):"""Get the number value representing the character s"""s = s.lower()for i in range(26):if s == string.letters[i]:return ireturn -1def next_state(self, cur_state, letter):"""Return the next state (as an integer) given the current state, andthe next letter of input"""letter_num = self.get_letter_num(letter)# There is a bad letter in the inputif letter_num == -1 or letter_num >= self.num_letters:raise ValueErrornext_state = self.trans_function[cur_state][letter_num]if self.verbose:print 'state %d letter %s -> state %d' % (cur_state, letter, next_state)return next_statedef find_end_of_occurrence(self, input_string):"""Find and return the position of the end of the first occurrence of thepattern in input_string. Return -1 if there is no occurrence"""cur_state = 0 # initial state is always q0try:for i in range(len(input_string)):c = input_string[i]cur_state = self.next_state(cur_state, c)if self.is_final_state(cur_state):return iexcept ValueError:print 'There was a bad letter in the input, stopping here!'return -1 # not founddef is_final_state(self, s):"""Returns True if s is a final state, False otherwise"""retVal = Truetry:self.final_states.index(s)except ValueError:retVal = Falsereturn retValdef read_dfa_table(self, dfa_table):"""Take a dfa table, as it is represented by the NFA class,and set ourselves up as a DFA using that information."""self.clear()for entry in dfa_table:# Find the number of letters in the tableself.num_letters = len(entry) - 3# Find the number of states in the tableself.num_states += 1# Find all of the final states in the tableif entry[0]:self.final_states.append(entry[1])# The transition function (as we need it) is stored from# position 3 to the end of every entry in the tableself.trans_function.append(entry[3:])# We've fully entered the automaton nowself.automaton_entered = Trueclass NFA:"""Implements a Non-Deterministic Finite-State Automaton"""def __init__(self):"""Constructor"""self.__clear()self.verbose = Falsedef __clear(self):"""Clear all instance variables of the NFA class"""self.trans_func = {}self.num_states = 0self.final_states = []self.initial_state = 0self.possible_letters = []self.dfa_table = []def __state_in_range(self, state):"""Check if a given state is in the acceptable range"""# Check to make sure this can be converted to an int.# Return False if the conversion fails.try:temp = int(state)except (TypeError, ValueError):return False# Make sure this is a state in the rangeif temp >= 0 and temp < self.num_states:return True# We weren't in the correct range, so return Falsereturn Falsedef __next_states(self, state, letter):"""Return the next states for the key (state, letter)"""try:next = self.trans_func[(state, letter)]except KeyError:# Create a new empty list for this state if it doesn't exist yetnext = self.trans_func[(state, letter)] = []# Make sure that we can go to ourselves by the empty letterif letter == '^' and state not in next:next.append(state)return nextdef __E(self, state, letter='^', storage=None, visited=None):"""Calculate E(state) and return it. This handles circular E()calculations."""# Work around weird mutable default argument stuffif storage is None:storage = []# Work around weird mutable default argument stuffif visited is None:visited = []# Find all of the direct next states that we can get to by# the empty string, and append anything that is not already therefor i in self.__next_states(state, letter):if i not in storage:storage.append(i)# Visit everything in storage that has not been visited alreadyfor i in storage:if i not in visited:visited.append(i)temp = self.__E(i, letter, storage, visited)# Avoid duplicating things in storagefor j in temp:if j not in storage:storage.append(j)return storagedef __unique_sort(self, li):"""Make sure everything in a list is unique, and then sort it"""newlist = []for i in li:if i not in newlist:newlist.append(i)newlist.sort()return newlistdef __is_final_state(self, state):"""Check if a state is a final state."""if state in self.final_states:return Truereturn Falsedef __is_list_final(self, states):"""Check if at least one state in the list "states" is final"""for s in states:if self.__is_final_state(s):return Truereturn Falsedef __get_temp_record(self, num_letters):"""Create a record (for the DFA table) that has the proper numberof slots"""blank = Nonereturn [blank for i in range(num_letters + 3)]def __get_possible_letters(self):"""Create a list of all the possible letters for the NFA,and store it in possible_letters"""# Get the list of all letterspossible_letters = []for (state, letter) in self.trans_func.keys():if letter not in possible_letters and letter != '^':possible_letters.append(letter)possible_letters = self.__unique_sort(possible_letters)self.possible_letters = possible_lettersdef __generate_dfa_table(self):"""Generate a table for the DFA representation of the NFA entered earlier"""# Get all the possible lettersself.__get_possible_letters()# Prime the dfa table with the first stateself.make_basic_record(self.__E(0))# Loop until we resolve every letter in the tablewhile self.find_unresolved_letter() != (-1, -1):(record, letter_pos) = self.find_unresolved_letter()self.resolve_letter(record, letter_pos)def resolve_letter(self, record, letter_pos):"""Resolve a letter in the table, either adding a new entry if therequired state doesn't already exist, or putting a link toan existing state"""# Get the NFA equivalent multi-statefromstates = self.dfa_table[record][2]tostates = []# Find all the states we can go to from our multi-state.# It is _EXTREMELY_ important that you call E(s) for every state that# you can get to, otherwise you miss certain situations.for from_s in fromstates:for next_s in self.__next_states(from_s, self.possible_letters[letter_pos-3]):tostates.extend(self.__E(next_s))tostates = self.__unique_sort(tostates)# Make the letter we are trying to resolve point to a record in the table.# make_basic_record() will return the correct record, even if it needs to# create a new one.self.dfa_table[record][letter_pos] = self.make_basic_record(tostates)def find_unresolved_letter(self):"""Returns an index pair of an unresolved letter that exists in the dfa_table.If there are no more letters, return (-1, -1)"""for i in range(len(self.dfa_table)):for j in range(len(self.possible_letters)):if self.dfa_table[i][j+3] == None:return (i, j+3)return (-1, -1)def make_basic_record(self, from_states):"""Create a record, if necessary, for the states "from_states."If a corresponding state already exists, return it's index. A new statewill have everything but the letters filled in."""if self.dfa_state_exists(from_states) == -1: #doesn't existtemp_record = self.__get_temp_record(len(self.possible_letters))recordnum = len(self.dfa_table)temp_record[1] = recordnumtemp_record[2] = self.__unique_sort(from_states)temp_record[0] = self.__is_list_final(from_states)self.dfa_table.append(temp_record)# always return a re-call of the function. This will not fail, since a new# record is added above if a record does not already exist.return self.dfa_state_exists(from_states)def dfa_state_exists(self, from_states):"""Find out if this state already exists in the dfa table.If it does exist, return it's dfa state name.If it does not exist, return -1"""sorted_states = self.__unique_sort(from_states)for i in range(len(self.dfa_table)):if self.dfa_table[i][2] == sorted_states:return self.dfa_table[i][1]return -1def from_pattern_to_dfa(self, possible_letters, pattern):"""Convert a pattern to a DFA. This is done using an intermediateNFA representation of the form (all_letters)* pattern (all_letters)*"""self.__clear()# Find the number of states we'll needself.num_states = len(pattern) + 1# The last state is the only final state, alwaysself.final_states = [self.num_states - 1]# Get all of the possible lettersself.possible_letters = possible_letters# Create the initial statefor l in self.possible_letters:if l not in self.__next_states(0, l):self.__next_states(0, l).append(0)# Create the final statefor l in self.possible_letters:if l not in self.__next_states(self.num_states - 1, l):self.__next_states(self.num_states - 1, l).append(self.num_states - 1)# Create each state in betweenfor s in range(len(pattern)):self.__next_states(s, pattern[s]).append(s+1)# Make sure that q0 is the initial stateself.initial_state = 0# Generate and return the tableself.__generate_dfa_table()return self.dfa_tableclass StringChecker:"""An object that will check a string against a simple pattern,and will find the first occurrence of the pattern."""def __init__(self):"""Constructor"""self.__clear()def __clear(self):"""Clear all of the private variables"""self.DFA = DFA()self.NFA = NFA()self.pattern = ''self.test_str = ''self.possible_letters = ''def __input_last_letter(self):"""Get the last possible letter from the user"""done = Falsewhile not done:print 'Please input the last possible's = raw_input('letter that will be in your input or pattern: ')if len(s) != 1 and s not in string.letters:print 'Incorrect input, try again'continuedone = Truefor i in range(len(string.letters)):if string.letters[i] == s:self.possible_letters = string.letters[:i+1]breakdef __input_pattern(self):"""Get the pattern from the user"""done = Falsewhile not done:s = raw_input('Please input a pattern to find: ')if not self.__valid_string(s):continuedone = Trueself.pattern = sdef __input_test_str(self):"""Get the string in which to find the pattern, from the user"""done = Falsewhile not done:s = raw_input('Please input the string to test against: ')if not self.__valid_string(s):continuedone = Trueself.test_str = sdef __valid_string(self, str):"""Make sure all letters in str are within the valid range"""for c in str:if c not in self.possible_letters:print 'Bad letter (%s) in the string, try again' % (c, )return Falsereturn Truedef __print_occurrence(self):"""Find and print the (non)occurrence of the pattern in the test string"""end_occurrence = self.DFA.find_end_of_occurrence(self.test_str)if end_occurrence == -1:print 'There was no occurrence of "%s" in the string "%s"' % (self.pattern, self.test_str)else:print 'There was an occurrence of "%s" in the string "%s",' % (self.pattern, self.test_str)print 'which starts at position %s' % (end_occurrence - len(self.pattern) + 1, )def main_menu(self):"""Run the StringChecker, giving the user possible things to do"""done = Falsepattern_entered = Falsewhile not done:print 'Menu:'print '========================================'print '1. Enter a pattern'print '2. Test string'print '3. Quit's = raw_input('Choice >>> ')if s == '1':self.__clear()self.__input_last_letter()self.__input_pattern()dfa_table = self.NFA.from_pattern_to_dfa(self.possible_letters, self.pattern)self.DFA.read_dfa_table(dfa_table)pattern_entered = Trueelif s == '2':if pattern_entered:self.__input_test_str()self.__print_occurrence()else:print 'Enter a pattern first'elif s == '3':done = Trueelse:print 'Bad Selection'if __name__ == '__main__':checker = StringChecker()checker.main_menu()