jumble_example.py

#!/usr/bin/python
#
# Jumble example:
#	This is an example of using strings, lists and dictionaries
#	in implementing a simple puzzle solver.
#
#	Jumble is a game found in the San Francisco Chronicle.
#	The player is given letters from a word in random order
#	and the goal is to find the original word.
#
#	There are at least a couple different ways of solving
#	this problem.  For example, we can compute all permutations
#	of the given letters and check whether each one is a
#	real word.  Another is to start with a list of real
#	words and check which ones have the same letter composition
#	(fingerprint) as the given set of letters.
#
#	We are implementing the latter solution.  In our program
#	words are Python strings.  Fingerprints are Python dictionaries
#	whose keys are letters and whose values are the number
#	of occurrences of those letters (in short, a histogram).
#	The list of real words is kept in a Python dictionary
#	whose keys are the lengths of words and whose values are
#	Python lists of words of those lengths.  (The choice to
#	use word length as the key is for easy elimination of
#	words that cannot possibly match the given input.)
#
#	An interesting question is "which approach will be faster?"
#	Consider five-letter words (the statistics are similar for
#	six-letter words).  There are about 3,000 to 4,000 "real"
#	words, depending on your dictionary.  Is it faster to compute
#	the 120 (5*4*3*2*1) permutations of letters and check them
#	against a Python dictionary; or is it faster to compute
#	the fingerprints for all the "real" words and compare them
#	against the fingerprint of the given letters?
#

# Python 2/3 compatibility
from __future__ import print_function
try:
    read_input = raw_input
except NameError:
    read_input = input

#
# Main program
#
def main():
	"""Main program"""
	# Load a dictionary of words then prompt users
	# for letters that they want to "unjumble"
	load('words')	# or get a larger distionary at http://thinkpython.com/code/words.txt
	while True:
		line = read_input('Letters: ')
		characters = line.strip()
		if not characters:
			break	# blank line means quit program
		print(jumble(characters))

def jumble(w):
	"""Find a word in the dictionary with the same letters as 'w'"""
	matches = []
	n = length(w)
	fp = fingerprint(w)
	for w in wordsOfLength(n):
		if fingerprint(w) == fp:	# note that this compares two dictionaries!
			matches.append(w)
	return matches

#
# Functions for handling a dictionary of words
#
words = {}	# key is number of characters, value is list of words with that many chars

def wordsOfLength(l):
	"""Return a list wof words of requested length 'l'"""
	return words.get(l, [])

def load(filename):
	"""Load dictionary of words from a file"""
	# Only keep words of length 5 and 6 because the
	# SF Chronicle jumble only uses words of those lengths
	with open(filename) as f:
		for line in f:
			w = line.strip()
			n = length(w)
			# Only keep words of length 5 and 6 because the
			# SF Chronicle jumble only uses words of those lengths
			if n != 5 and n != 6:
				continue
			try:
				words[n].append(w)	# append another word to the list
			except KeyError:
				words[n] = [w]	# first time through, create a list with just that word
	# print('words: ', words)

#
# Functions for handling words
#
def length(word):
	"""Return number of alphabetic characters in 'word'"""
	count = 0
	for c in word:
		if c.isalpha():
			count = count + 1
	return count

def fingerprint(word):
	"""Return fingerprint (character histogram) of 'word'"""
	fp = {}
	for c in word:
		if not c.isalpha():
			continue
		c = c.lower()
		try:
			fp[c] = fp[c] + 1
		except KeyError:
			fp[c] = 1
	return fp

#
# If invoked directly (i.e., not via "import"), run the program
#
# Some words to try: lebjmu, siruv, soome, zugea, retine, timer, silly, python
#
if __name__ == '__main__':
	main()