Recently as part of a job interview I was given the following coding task:
Attached is a gzipped text file of about 100,000 English words. Write a Python, C or C++ program that finds all the anagrams of the word "empires" that exist in the words file (just a reminder: an anagram is the result of rearranging the letters of a word to form a new word).
Your program should ideally take only a few seconds at most to run on modern hardware. The only inputs to your program should be the "empires" string and the words file. Send back your Python source files. Your solution should reflect the quality of work you would deliver to a customer, so keep in mind things like unit/acceptance tests, comments, PEP8, etc.
I submitted two files; one with the code, and one with tests (code below). I didn't pass on to the next stage of the interview, but can't figure out what, if anything, is wrong with my code. Any critiques would be appreciated.
anagrams.py:
#!/usr/bin/env python
"""Find anagrams in a file.
Given a term and a filename, find the anagrams of the term within the file.
An anagram must use each of the letters in the term exactly once.
"""
import sys
def usage():
"""Print a usage message."""
print "Usage:"
print "anagrams.py <term> <wordfile>"
def count_letters(word):
"""Count the occurrances of each letter in a word."""
counts = {}
for letter in word:
if letter in counts.keys():
counts[letter] += 1
else:
counts[letter] = 1
return counts
def main(argv=None):
"""Handle arguments and execute."""
if argv is None:
argv = sys.argv
if len(argv) != 3:
usage()
return 2
term = argv[1]
wordfilename = argv[2]
try:
grams = find_anagrams(term, wordfilename)
report_grams(grams, term, wordfilename)
return 0
except IOError:
print "Unable to open %s" % wordfilename
return 2
def find_anagrams(term, wordfilename):
"""Actually find the anagrams.
We use three tests to identify anagrams:
First, we check that the number of letters is the same. This will eliminate
the bulk of the words in the list, which is good for efficiency.
Second, for each word that has the right overall number of letters, we
check that the set of letters is correct. If there are any repeated letters,
this will result in some false positives.
The third test is to actually count the occurrances of each letter in
the word, and check it against the correct collection of letter counts.
For the word list and term supplied for this task, there isn't a huge
difference in computational load between tests two and three, and we could
eliminate test two without problems, but as the size of the list and
the size of the word to be anagrammed both get larger, the second test
will offer some speedup.
"""
termset = set(term)
termcounts = count_letters(term)
grams = []
with open(wordfilename, 'r') as wordfile:
for word in wordfile:
word = word.rstrip()
if len(word) == len(term):
# quickest way; gets some false positives
if set(word) == termset:
# eliminate false positives with letter counts
if count_letters(word) == termcounts:
grams.append(word)
return grams
def report_grams(grams, term, wordfilename):
"""Output the results."""
print "There are %d anagrams of %s in %s:" % (len(grams),
term, wordfilename)
for gram in grams:
print gram
if __name__ == '__main__':
sys.exit(main())
anagram_tests.py:
#!/usr/bin/env python
"""Testing for the anagrams module."""
import unittest
import os
import anagrams
class TestAnagrams(unittest.TestCase):
"""Test cases for anagrams modoule."""
def setUp(self):
"""Set up for anagram tests.
Create a file with some anagrams and some edge cases. Specifically, we
want some words that use the same letters but in different quantities,
and words that are subsets or supersets of the test word.
"""
self.testword = 'all'
self.testlist = 'testwords.txt'
self.actual_anagrams = ['all', 'lal', 'all']
wordlist = ['all', # the word itself
'ball', # superset of the word
'aal', # same set of letters, different counts
'ale', # extra letter
'lal', # an anagram
'lala', # too long
'al', # too short
'aaa', # subset of letters
'all'] # the word itself again
wordlistfile = open(self.testlist, "w")
wordlistfile.write("\n".join(wordlist))
wordlistfile.close()
def tearDown(self):
"""Clean up.
Delete the file created in the setup phase.
"""
os.remove(self.testlist)
def test_count_letters(self):
"""Make sure we're counting letters the right way."""
self.assertEquals(anagrams.count_letters(self.testword), {'a':1, 'l':2})
def test_find_anagrams(self):
"""Test that we're getting the right anagrams from a sample list."""
analist = anagrams.find_anagrams(self.testword, self.testlist)
self.assertEquals(self.actual_anagrams, analist)
if __name__ == '__main__':
unittest.main()
setUp
andtearDown
are run before and after every test. You would either want to turn those into@classmethod
s namedsetUpClass
andtearDownClass
that get run before and after all tests, or I think is a better solution, just have the relevant test cases created inside the test methods themselves. \$\endgroup\$if len(word) == len(term): if set(word) == termset: if count_letters(word) == termcounts:
, you can writeif len(word) == len(term) and set(word) == termset and count_letters(word) == termcounts:
. This won't fit on one line, so you can nest one if-statement or use a backslash. \$\endgroup\$