#! /usr/bin/env python """Give a string-oriented API to the generic "diff" module. The "diff" module is very powerful but practically useless on its own. The "search" and "empty_master" functions below resolve this problem.""" ################################################################################ __author__ = 'Stephen "Zero" Chappell ' __date__ = '11 February 2010' __version__ = '$Revision: 3 $' ################################################################################ import diff ################################################################################ # Matching Sensitivity - OFF CASE_AND_PUNCTUATION = False ################################################################################ def connect_tree(tree): """Takes the master and finds out what part of the slave matches it. The tree from "diff.search" may contain several different routes for finding matches. This function takes the best one, gets the master match, and fills in the prefix and suffix with the best choices.""" match = tree.nodes[tree.index.index(tree.value)] node = match.a if match.prefix.value: node.prefix = connect_tree(match.prefix) if match.suffix.value: node.suffix = connect_tree(match.suffix) return node def flatten_tree(node): """Flattens a tree from "connect_tree" for linear iteration. The root node created after connecting a tree must be traversed from beginning to end in a linear fashion. This function flattens the tree to make that possible. Further processing is done by other functions.""" array = [0] _flatten(node, array) return array def _flatten(node, array): """Recursively traverse and flatten the given tree. This is a helper function that takes "node" and sequentially processes its prefix, root, and suffix. The results are appended to the array.""" if isinstance(node.prefix, diff.Slice): _flatten(node.prefix, array) else: array.append(node.prefix) array[0] += 1 array.append((array[0], node.root)) if isinstance(node.suffix, diff.Slice): _flatten(node.suffix, array) else: array.append(node.suffix) default = lambda words: ' '.join('_' * len(word) for word in words) ################################################################################ # Note: search, build_answer, & empty_master documentation is copied! # ------^^^^^^--^^^^^^^^^^^^----^^^^^^^^^^^^------------------------- if CASE_AND_PUNCTUATION: def search(master, slave): """Search for differences in the master and slave strings. The strings are translated into key and data, and their difference is calculated. An answer is composed after further processing and returned with the number of right words and total number of words.""" key = tuple(master.split()) data = tuple(slave.split()) tree = diff.search(key, data) if tree.value: node = connect_tree(tree) array = flatten_tree(node) answer = build_answer(array) else: answer = default(key) return tree.value, len(key), answer def build_answer(array): """Take in flattened / serialized data and generate a hint. This implementation returns a string useful for Verse objects. Incorrect or missed words get printed up as empty blank lines.""" cache = [] for chunk in array: if chunk and isinstance(chunk, tuple): if isinstance(chunk[0], int): for word in chunk[1]: cache.append(word) else: for word in chunk: cache.append('_' * len(word)) return ' '.join(cache) def empty_master(master): """Compute the represenation of a master without a slave.""" return default(master.split()) ################################################################################ else: def search(master, slave): """Search for differences in the master and slave strings. The strings are translated into key and data, and their difference is calculated. An answer is composed after further processing and returned with the number of right words and total number of words.""" words = master.split() key = simplify(words) assert len(words) == len(key), 'Cannot Simplify Words' data = simplify(slave.split()) tree = diff.search(key, data) if tree.value: node = connect_tree(tree) array = flatten_tree(node) pairs = flatten_list(array) answer = build_answer(words, pairs) else: answer = default(key) return tree.value, len(key), answer def simplify(words): """Remove non-alphabetic characters from an array of words.""" letter = lambda s: ''.join(filter(lambda s: 'a' <= s <= 'z', s)) return tuple(filter(bool, map(letter, map(str.lower, words)))) def flatten_list(array): """Build (flag, load) pairs for the "build_answer" function.""" pairs = [] for chunk in array: if chunk and isinstance(chunk, tuple): if isinstance(chunk[0], int): for word in chunk[1]: pairs.append((True, word)) else: for word in chunk: pairs.append((False, word)) return pairs def build_answer(words, pairs): """Take in flattened / serialized data and generate a hint. This implementation returns a string useful for Verse objects. Incorrect or missed words get tranformed into underscore lines.""" cache = [] for word, (flag, load) in zip(words, pairs): cache.append(word if flag else '_' * len(load)) return ' '.join(cache) def empty_master(master): """Compute the represenation of a master without a slave.""" return default(simplify(master.split()))