Skip to content

Instantly share code, notes, and snippets.

@ladyrassilon
Created August 30, 2018 19:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ladyrassilon/187d32d306ca6439f7bf5ced85370053 to your computer and use it in GitHub Desktop.
Save ladyrassilon/187d32d306ca6439f7bf5ced85370053 to your computer and use it in GitHub Desktop.
# pylint: disable=R0903, C0111
from operator import itemgetter
from collections import Counter, defaultdict
import unittest
import logging
class WordNotFoundError(KeyError):
"""
Error raised when word not in anagram list
"""
class Anagrams:
"""
Class provides anagrams of a given word.
Keyword arguments:
strict_mode (boolean) -- enable error raising mode (default False)
Limitations - if a valid word exists, but is not in the list,
it will not be recognised as potential anagram.
This is for performance reasons, as well as unspecified
behaviour concerning unknown words.
This only uses threadsafe objects, so will be threadsafe.
"""
def __init__(self, strict_mode=False):
self.strict_mode = strict_mode
# Load in words, and strip whitespace characters
with open('words.txt') as words_file:
words = [word.strip().lower() for word in words_file.readlines()]
# Produce the maps of word to counters, and counters to words
input_word_map = {}
reverse_map = defaultdict(list)
counters = []
for word in words:
counter_list = list(Counter(word).items())
counter_list.sort(key=itemgetter(0))
counter_tuple = tuple(counter_list)
counters.append(counter_tuple)
input_word_map[word] = counter_tuple
reverse_map[counter_tuple].append(word)
# Connect up words to their anagrams
self.anagram_map = {}
for word, counter in input_word_map.items():
anagram_list = reverse_map[counter]
anagram_list.sort()
self.anagram_map[word] = anagram_list
def get_anagrams(self, word):
"""
Get the anagrams of a given word.
Keyword arguments
word - word you are trying to find anagrams for (case insensitive)
If strict mode is off (default), then if a word isn't found
it'll return a list with that word in, and raise a logging warning.
If strict mode is on, then if a word isn't found a WordNotFoundError
is raise.
"""
lower_word = word.lower()
if lower_word in self.anagram_map:
return self.anagram_map[lower_word]
if self.strict_mode:
raise WordNotFoundError("{} not found".format(lower_word))
else:
logging.warning(
"%s was not found in the list of words", lower_word)
return [lower_word]
class TestAnagrams(unittest.TestCase):
"""
Test Anagrams class functionality"
"""
def test_lower_case_anagrams(self):
"""
Ensure that default functionality, that the anagram is returned.
"""
anagrams = Anagrams()
self.assertEqual(anagrams.get_anagrams('plates'), [
'palest', 'pastel', 'petals', 'plates', 'staple'])
self.assertEqual(
anagrams.get_anagrams('eat'), ['ate', 'eat', 'tea'])
def test_mixed_case_anagrams(self):
"""
Check that mixed case words still work.
"""
anagrams = Anagrams()
self.assertEqual(anagrams.get_anagrams('peTAls'), [
'palest', 'pastel', 'petals', 'plates', 'staple'])
self.assertEqual(
anagrams.get_anagrams('EaT'), ['ate', 'eat', 'tea'])
def test_input_is_in_output(self):
"""
Check that the input word is found in the output list
"""
anagrams = Anagrams()
self.assertIn('petals', anagrams.get_anagrams('petals'))
self.assertIn('abide', anagrams.get_anagrams('abide'))
def test_strict_mode_input_not_in_word_list(self):
"""
Test that in strict mode, a word not in the list
raises a WordNotFoundError
"""
anagrams = Anagrams(strict_mode=True)
with self.assertRaises(WordNotFoundError):
anagrams.get_anagrams("cheeseburger")
def test_default_mode_input_not_in_word_list(self):
"""
Test that in default (non-strict mode) a warning is raised
and the word is returned as a single item in a list.
"""
anagrams = Anagrams()
with self.assertLogs(level="WARNING") as logging_capture:
self.assertEqual(
anagrams.get_anagrams('cheeseburger'),
['cheeseburger']
)
self.assertEqual(
logging_capture.output, [
'WARNING:root:cheeseburger was not found in the list of words']
)
if __name__ == '__main__':
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment