Preface
I was trying to review this question on the same topic, but in the end many points I wanted to make were excellently explained by @ferada so I felt that posting my code and explaining the concepts around such changes would just be repetition.
Instead I am questioning whether my approach is over-engineered considering the simplicity of this task.
Specification
I consider two words to rhyme if they share a certain number of phonemes at the end.
The division of the words into phonemes is already given from input and must only be read into program data.
Data is fetched from the internet on the first run and stored in a local file for successive runs to save internet traffic and execution time.
Code
import doctest
import os
import re
import requests
from collections import namedtuple
URL = "http://svn.code.sf.net/p/cmusphinx/code/trunk/cmudict/cmudict-0.7b"
LIBRARY = "library.txt"
PHONEMES_TO_MATCH = 3
ReadingData = namedtuple("ReadingData", ["word", "phonemes"])
def first_and_others(xs):
"""
>>> first_and_others([1, 2, 3])
(1, [2, 3])
"""
return xs[0], xs[1:]
def format_data(raw_data):
'''
Transforms a list of lines of the format
WORD [SPACE] PHONEME1 [SPACE] PHONEME2 [SPACE] PHONEME3 ...
into a list of `ReadingData` classes for easier further processing.
>>> format_data(["ZURCHER Z ER1 K ER0", "ZUREK Z UH1 R EH0 K"])
[ReadingData(word='ZURCHER', phonemes=['Z', 'ER1', 'K', 'ER0']), ReadingData(word='ZUREK', phonemes=['Z', 'UH1', 'R', 'EH0', 'K'])]
'''
return [ReadingData(*first_and_others(x.strip().replace('\n','').split()))\
for x in raw_data \
if not x.startswith(';;;')]
def write_url_to_filename(url, filename):
response = requests.get(URL, stream=True)
if not response.ok:
raise Exception("Error writing {} to {}".format(url, filename))
with open(filename, 'wb+') as f:
for block in response.iter_content(4096):
f.write(block)
def find(predicate, iterable):
"""
>>> find(lambda x: x % 2 == 0, [1, 2, 3, 4])
2
"""
return next(i for i in iterable if predicate(i))
def last_items_match(a, b, how_many):
"""
>>> last_items_match([1, 2, 3], [7, 2, 3], 2)
True
"""
return a[-how_many:] == b[-how_many:]
def find_rhymes(word, data, how_many_phonemes=2):
"""
Finds all the words in `data` that rhyme with `word`, that is,
all the words that end in the same `how_many_phonemes` phonemes.
`word` itself is always included as each word rhymes with itself
(except when it has less than `how_many_phonemes` phonemes).
>>> find_rhymes("example", [ReadingData('EXAMPLE', ['IH0', 'G', 'Z', 'AE1', 'M', 'P', 'AH0', 'L']),
... ReadingData('AMPAL', ['AE1', 'M', 'P', 'AH0', 'L']),
... ReadingData('HAPPINESS', ['HH', 'AE1', 'P', 'IY0', 'N', 'AH0', 'S'])])
[ReadingData(word='EXAMPLE', phonemes=['IH0', 'G', 'Z', 'AE1', 'M', 'P', 'AH0', 'L']), ReadingData(word='AMPAL', phonemes=['AE1', 'M', 'P', 'AH0', 'L'])]
"""
this_phonemes = find(lambda line: line.word == word.upper(), data).phonemes
return [candidate for candidate in data \
if last_items_match(candidate.phonemes, this_phonemes, how_many_phonemes)]
def main():
if not os.path.isfile(LIBRARY):
write_url_to_filename(URL, LIBRARY)
with open(LIBRARY) as f:
data = format_data(f.readlines())
print(find_rhymes("FORCE", data, PHONEMES_TO_MATCH)[:20])
if __name__ == "__main__":
doctest.testmod()
main()