Puzzle Solving!

Sunday Puzzle for April 10, 2022

Posted on Updated on

Add Letters to a Word Twice to Get New Words with/without Silent ‘L’

This week’s challenge comes from listener Ari Ofsevit, of Boston. Think of a 5-letter word with an “L” that is pronounced. Add a letter at the start to get a 6-letter word in which the “L” is silent. Then add a new letter in the fifth position to get a 7-letter word in which the “L” is pronounced again. What words are these?

Link to the challenge on the NPR website
Steps to solve the puzzle. We start with one word, add a letter, test it for a silent L. If the new word qualifies, add another letter and test it for a pronounced L. If the 3rd word qualifies, we have solved the puzzle!

Discussion

That’s a fun little puzzle, and one I suspect is a lot easier if you can write code. I love saying “Older Solder Soldier“! The puzzle can be solved if you have a list of English words and their phonemes. A phoneme is the smallest phonetic unit in a language that is capable of conveying a distinction in meaning, as the m of mat and the b of bat in English.

I use the phoneme list to determine if the ‘L’ is silent. For example, consider the following 3 words and their phonemes:

  • OLDER OW1 L D ER0
  • SOLDER S AA1 D ER0
  • SOLDIER S OW1 L JH ER0

Note that the phoneme list for ‘older‘ contains an ‘L’, so it is pronounced. But the phoneme list for ‘solder‘ does not contain an ‘L’, so it is silent. Again, ‘soldier‘ contains an ‘L’, so it is pronounced.

Programming Techniques to Solve

  • File handling
  • String manipulation
  • Binary search
  • Classes
  • Simple ranges
  • General logic and looping
An Older Solder Soldier Screen Shot. How’s that for alliteration? Say it 5 times fast!

Code Synopsis

  • Build a list of words and their associated phonemes
  • Iterate the list, seeking 5-letter words containing an L
    • For each such list entry, add a letter at the beginning of the word, for example:
      • ‘s’ + older → solder
    • If the result is a bona fide word with a silent ‘L’,
      • Then add another letter at position 5, for example:
        • ‘sold’ + i + ‘er’ → soldier
      • If the result is a bona fide word word a pronounced ‘L’, print the result
Stick figure illustration showing a phoneme mapped to a word

You can download a list of phonemes from Github

Code: A Class to Hold a Word/Phoneme Pair

Constructing a WordPhoneme from a line in the file. The first token is the word, the 3rd through the end becomes the list ‘Phonemes’

I want a list of words and their associated phonemes. My list will hold members of a class I call ‘WordPhoneme‘, defined below:

class WordPhoneme:
    Word = ""
    Phonemes = []

    # Constructor takes a line that looks like the following:
    # SOLDER  S AA1 D ER0
    # I.e. the word followed by 2 spaces, then a list of phonemes
    def __init__(self, aLine):
        # Following the example above, tokens will look like the following:
        #  tokens = ['SOLDER', '', 'S', 'AA1', 'D', 'ER0']
        tokens = aLine.rsplit(" ")
        self.Word = tokens[0]
        self.Phonemes = []
        # Now assign the values to our new phoneme list, 
        # starting after the empty string:
        for p in tokens[2:]:
            self.Phonemes.append(p.strip())

The class above has two properties: Word and Phonemes, i.e. a string and a list. The first two lines above define those two properties. The next lines define the constructor, which is responsible for initializing the class.

The constructor takes a parameter ‘aLine‘ which represents a line of code from the file. All we have to do is split that line using a built-in function called ‘rsplit‘ that gives us a list with one entry for each distinct string of characters in the line.

  • rsplit needs a delimiter, such as a space, comma, slash, etc. I specify a space because that is what separates the tokens in the file
  • The result is a list, as noted in my code comment above
  • We grab the first entry (tokens[0]) and shove it into our Word property
  • And then grab the remainder of the tokens for our Phonemes list
    • Examine the following line of code:
      • for p in tokens[2:]:
    • Remember that the first entry in token list has index 0, so we can reference it with tokens[0]
    • But we already grabbed the first token (the word), and the next token is a space, so we want to skip it too
    • That tells us we need to we start at index 2 and take the remainder of the tokens. Using range indexing, we could have (for example) specified [2:3] to get the tokens at index 2 and 3, but my code doesn’t specify the end index, we instead get the remainder of the line using “[2:]” for the index.
    • Refer to this article explaining range indexing in python
I made this screen shot by creating a breakpoint. When the debugger paused on it, I hovered my mouse over the variable ‘addMe’ (a WordPhoneme instance) to cause the value to be displayed as shown above

Now, let’s open the file of phonemes and make a list of WordPhoneme pairs.

Code: Build the List of WordPhoneme Pairs by Reading the File

fName = "C:\\Users\\Randy\\Documents\\Puzzles\\Data\\cmudict.0.7a"
fHandle = open(fName, 'rt')
phonemeList = []
for aLine in fHandle:
    if aLine[0].isalpha():
        # Invoke the constructor using the line we just read
        addMe = WordPhoneme(aLine)
        phonemeList.append(addMe)

Observe the file name in variable ‘fName‘; I use double slashes ‘\\’ instead of slashes. This is necessary because a single slash is a special character that normally modifies the following character, but a double slash is interpreted as just a slash. A single slash starts an escape character sequence. Note that my file is text, but for some reason doesn’t have a file extension “.txt”.

  • The open statement takes a string parameter ‘rt’, meaning open it for Read, Text.
  • The following line checks if the line we read starts with a letter:
  • I need to do this because the top of the file contains documentation and those lines start with something other than a letter, typically a semicolon
  • The following line invokes the class constructor, discussed above:
    • addMe = WordPhoneme(aLine)

Great news! After executing this code, we now have a list of WordPhoneme pairs. Next, we define a binary search method.

Binary Search

Binary search is an extremely efficient way to search a list. That is important when searching a big list. Since I discussed this code before (NPR Puzzle February 5, 2022), I will only list the code and not discuss the algorithm.

Depiction of Binary Search. Start in the middle of the list by setting the index accordingly. If the item is less than the middle entry, set the index to the middle of the left half. But if the item is greater the middle entry, set the index to the middle of the right side. Repeat until found or there is nothing left to search.
def binary_search(lst, aWord):  
    low = 0  
    high = len(lst) - 1  
    mid = 0  
  
    while low <= high:  
        # Compute index 'mid' for our next guess, the middle of high and low
        mid = (high + low) >> 1
  
        # Check if aWord is present at mid
        if lst[mid].Word < aWord:  
            low = mid + 1  
  
        # If aWord is greater, so search to the right of mid
        elif lst[mid].Word > aWord:  
            high = mid - 1  
  
        # aWord is smaller, so search to the left of mid
        else:  
            return mid  
  
    # element not found, return -1
    return -1

Now that we have a method to perform a binary search, we will use it to search our list of WordPhoneme pairs. We need to search because we are creating strings that might or might not be legitimate words; if the search comes up dry, then we know the string is not a word.

Code to Solve!

So far, we have built a list of WordPhonemes and defined a Binary Search method. Now we use them to solve.

# Now that we have built the word-phoneme list, loop through it
for wp in phonemeList:
    # Check if the word is 5 letters long and the L is pronounced
    if len(wp.Word) == 5 and 'L' in wp.Phonemes:
        # In ASCII, 'A' is represented by 65 and 'Z' is represented by 90
        for i in range(65, 90):
            ltr = chr(i)
            # Build a potential new word by adding the letters
            # A-Z to the word in question. Each loop pass adds a successive letter
            maybeWord = ltr + wp.Word
            # Check if we created a bona fide word:
            ndx = binary_search(phonemeList, maybeWord)
            # If we found its index, it is in the list and indeed a word. 
            # Proceed if so and also the L is pronounced, i.e. in the phoneme list
            if ndx >= 0 and not 'L' in phonemeList[ndx].Phonemes:
                # Loop through the alphabet again
                for i2 in range(65, 90):
                    ltr = chr(i2)
                    # Construct a potential word by taking the first 4 letters 
                    # plus the letter and the remainder of the word
                    candidate = maybeWord[0:4] + ltr + maybeWord[4:]
                    # Check if candidate is a word:
                    ndx = binary_search(phonemeList, candidate)
                    if ndx >= 0 and 'L' in phonemeList[ndx].Phonemes:
                        # Print our result
                        print(wp.Word, maybeWord, candidate)

Analysis

Below I list some interesting lines of code from above and discuss what they do.

  • if len(wp.Word) == 5 and 'L' in wp.Phonemes:
    • The puzzle says we start with a 5-letter word, so first we check the length of the Word property of the WordPhoneme
    • If the letter ‘L’ is present in the phoneme list for the WordPhoneme, that means the L is not silent
  • for i in range(65, 90):
    • This code says start a loop where variable i holds values starting at 65 and goes up to 90
    • This is because the ASCII code represents letters as numbers ‘behind the scenes’, so we can easily convert the number to a character. Here’s some examples of the ASCII representation of letters:
      • ‘A’ = 65
      • ‘B’ = 66
      • ‘C’ = 67
      • ‘Z’ = 90
  • ltr = chr(i)
    • This line of code makes a character from a number. For example, it converts 65 to ‘A’.
  • maybeWord = ltr + wp.Word
    • Here I construct a string by prepending a letter to the Word in the WordPhoneme, for example:
      • ‘A’ + ‘OLDER’ = ‘AOLDER’
      • ‘B’ + ‘OLDER’ = ‘BOLDER’
      • ‘S’ + ‘OLDER’ = ‘SOLDER’
  • ndx = binary_search(phonemeList, maybeWord)
    • Search the list for maybeWord
  • if ndx >= 0 and not 'L' in phonemeList[ndx].Phonemes:
    • ndx is the index, in our list, where we found the word we searched for
    • If the word is not in our list, ndx has value -1
    • For example, ‘OLDER’ is located at index 85,826, because there are 85,825 entries before it
    • Note the 2nd part if the if statement:
      • and not 'L' in phonemeList[ndx].Phonemes:
    • In other words, the ‘L’ is silent, because it is not present in the phoneme list
  • candidate = maybeWord[0:4] + ltr + maybeWord[4:]
    • Just to review, at this point in code, we have started with a 5-letter word with a pronounced ‘L’, added a letter to the start of the word, tested the result to see if it is a word and also a word with a silent ‘L’. Now we construct a 3rd string
    • My 3rd string (candidate) takes the first 4 letters of maybeWord, adds the loop variable ltr, then finally adds the remainder of maybeWord
  • ndx = binary_search(phonemeList, candidate)
    • Check if candidate is a bona fide word by searching our list
  • if ndx >= 0 and 'L' in phonemeList[ndx].Phonemes:
    • This code checks that the candidate is present in the list and has a pronounced ‘L’. If candidate passes that test, we have a winner! Ding! Ding! Ding!

Code Summary

My program is 42 lines of code and 42 lines of comments or blank lines. That’s pretty short! Of course, you need a special file of phonemes to solve it, so thank you to the folks at Carnegie Mellon University who created and released this file.

The code works because we can create strings that might be words and search our list to see if they are found in that list, and also because we can determine if the ‘L’ is silent by whether an ‘L’ is present in the phoneme list for the word we are working with.

Get the Code

You can download the code here, from my Dropbox account. Note that you will need to create a free account to do so.

Sunday Puzzle for March 13, 2022

Posted on

Manipulate Words from a Common Phrase Containing ‘in the’

This week’s challenge comes from Tyler Hinman, of San Francisco. He’s the reigning champion of the American Crossword Puzzle Tournament, which is coming up again on April 1-3. Think of two four-letter words that complete the phrase “_ in the _.” Move the first letter of the second word to the start of the first word. You’ll get two synonyms. What are they?

Link to the challenge
Take a common phrase containing ‘in the’, extract two words and manipulate them to find two synonyms

Discussion

The code to solve the puzzle was easy. Getting the list of phrases was tough! Eventually, I built up a list of over 10,000 common phrases and used it to solve the puzzle. Once again, I elected to use python.

Skills and Techniques to Solve

  • File handling
  • String manipulation

Again, the code is quite short, just 57 lines, including 16 comment lines

Screen shot showing my solution

Code Discussion

For simplicity, I will show the key code, saving 3 things for later:

  1. Building a list of English words called ‘wordLst’
  2. Defining a method ‘GetPriorWord’
  3. Defining another method ‘GetNextWord’

Here is the important, key word, skipping the three items above:

fName = 'C:\\Users\\Randy\\Documents\\Puzzles\\Data\\CommonPhrases.txt'
fHandle = open(fName, 'rt', encoding="utf8")
# Read each line in the file (about 10,000) and process lines containing ' in the '
for aLine in fHandle:
    p = aLine.find(' in the ')
    if p > 0:
        #Remove trailing linefeed
        fixed = aLine.strip()
        previous = GetPriorWord(fixed, p)
        if len(previous) == 4:
            next = GetNextWord(fixed, p)
            if len(next) == 4:
                #At this point, we know both words have length 4, 
                #now manipulate them according to the instructions:
                candidate1 = next[0] + previous
                candidate2 = next[1:]

                #if both candidates are valid words, print the result:
                if candidate1 in wordLst and candidate2 in wordLst:
                    print(fixed, ' → ', candidate1, candidate2)

First, we open the file with this code:

fHandle = open(fName, 'rt', encoding="utf8").

  • ‘rt’ means ‘read’ ‘text’
  • encoding=”utf8″ means expect and handle non-standard letters, like à la carte

Next, we loop through the lines in the file, we look for a line containing ‘in the’

p = aLine.find(' in the ')

variable ‘p’ is the position where we find that substring; if not found, p will be -1. Only about 20 phrases qualify. Next, we get the words immediately before and immediately after the position where we found ‘in the’:

        previous = GetPriorWord(fixed, p)
        if len(previous) == 4:
            next = GetNextWord(fixed, p)
            if len(next) == 4:

As discussed above, I will explain ‘GetPriorWord’ and ‘GetNextWord’ below. For now, the code above creates two variables holding the words we want, named ‘previous’ and ‘next’. If both have length 4, we proceed:

                #At this point, we know both words have length 4, 
                #now manipulate them according to the instructions:
                candidate1 = next[0] + previous
                candidate2 = next[1:]

                #if both candidates are valid words, print the result:
                if candidate1 in wordLst and candidate2 in wordLst:
                    print(fixed, ' → ', candidate1, candidate2)

‘candidate1’ is built by concatenating the first letter of next with previous. ‘candidate2’ is every letter following the first. If both are in my list of words ‘wordLst’, then we have a solution. Note that only one phrase matches this, so I didn’t write code to determine whether they are synonyms.

The Three Parts I Skipped Over

Here’s how I built wordLst:

# First load a list of English words
fName = "C:\\Users\\Randy\\Documents\\Puzzles\\Data\\2of12inf.txt"
fHandle = open(fName, 'rt')
wordLst = []
for aWord in fHandle:
    wordLst.append(aWord.strip())

Here are the definitions of my two methods ‘GetPriorWord’ and ‘GetNextWord’:

#When given a position p in a string 'aLine', finds the word preceeding it
def GetPriorWord(aLine, p):
    i = p - 1
    while i >= 0 and aLine[i] != ' ':
        i -= 1
    
    result = ''
    if i < p:
        result = aLine[i + 1:p - i - 1]
    return result

#When given a position p in string 'aLine, finds the word succeeding it
def GetNextWord(aLine, p):
    i = p + 8
    while i < len(aLine) and aLine[i].isalpha():
        i += 1
    
    result = ''
    if i > p + 8:
        result = aLine[p + 8:i]
    return result

Download My Code

You can download my solution, plus the phrase file, from my Dropbox account. You will need to create a free account with Dropbox to do so.

Sunday Puzzle March 6, 2022

Posted on

Find a Word That Ought to Start with a ‘Q’

This week’s challenge comes from listener Ward Hartenstein, of Rochester, N.Y. Words starting with a “kw-” sound usually start with the letters QU-, as in question, or “KW-,” as in Kwanzaa. What common, uncapitalized English word starting with a “kw-” sound contains none of the letters Q, U, K, or W?

Link to the challenge
Find a word that should start with a ‘Q’ but doesn’t

Discussion

This is a simple puzzle if you have a list of words and their associated phonemes! You can download one here, though I have to admit I downloaded mine a very long time ago. Phonemes are a “unit of sound”, i.e. a standardized way of representing the sounds that make up a word.

The entries in the word/phoneme list look like these three samples:

  • KWANZA K W AA1 N Z AH0
  • QUICK K W IH1 K
  • CHOIR K W AY1 ER0

Note that each word is followed by two spaces and then the phonemes that represent how that word is pronounced in North America. The main thing we care about is whether the word is followed by a K, and a W.

Skills and Techniques to Solve

Again, I elected to solve with Python, and the specific programming skills include:

  • String processing
  • File handling

The program is extremely short.

Screen shot showing my solution

Here’s the Code

Since the program is so short, I’ll paste the whole 18 lines here:

def HasNoBadLetters(word):
    result = True
    badLetters = ['Q', 'U', 'K', 'W', '(']
    for l in word:
        if l in badLetters:
            result = False
            break
    return result

fHandle = open('C:\\Users\\Randy\\Documents\\Puzzles\\Data\\cmudict.0.7a', 'rt')
for aLine in fHandle:
    if (aLine.startswith('Q') or aLine.startswith('KW')):
        continue
    elif aLine[0:1].isalpha():
        # The line in the file looks like: "CHOIR  K W AY1 ER0"
        # where the word 'choir' is followed by 2 spaces and then the phoneme list
        p = aLine.find("  K W")
        if p >= 0 and HasNoBadLetters(aLine[0:p]):
            print(aLine[0:p])

Code Discussion

My code listing starts by defining a short method called ‘HasNoBadLetters‘. As per the instructions, I use this method to exclude any word that has bad letters in it, namely, ‘Q’, ‘U‘, ‘K‘, ‘W’ and ‘(‘. If the input parameter, named ‘word‘, has none of these letters, we return True for our result. Otherwise, we return False. Note that I invoke this method in the second-to-last line above.

Note: Puzzle Master Will said to exclude 4 specific letters, but I exclude one more: the parenthesis. The reason for this is that some words in the file have secondary pronunciations, like the following:

  • COOPERATE(2) K W AA1 P ER0 EY2 T

Apparently some weirdos pronounce it like ‘quooperate’!

Next, we open the file for reading as text (passing parameter ‘rt‘ to the open method). We loop through the lines in the file, reading each line into variable ‘aLine‘.

Next, we skip any words that start with Q or K, and also require that the first letter be alphabetic.

    if (aLine.startswith('Q') or aLine.startswith('KW')):
        continue

The ‘continue‘ keyword effectively means “go to the next line in our loop”.

The following line of code makes sure the first letter is not punctuation, numeric, spaces, or anything else:

elif aLine[0:1].isalpha():

Why check this? The reason for this is that the top of the phoneme file has a bunch of notes and explanation, while the remainder of the file is the word listing. All the notes/explanation lines start with a semicolon, parenthesis, or other punctuation.

This is why I check whether the line starts with a letter, i.e. “isalpha()”

Finally, having determined that the line we read from the file is a word followed by its phonemes, we test to see if it is pronounced like a word starting with “QU”, which means we look for 2 spaces followed by K W. The following three lines of code does that:

        p = aLine.find("  K W")
        if p >= 0 and HasNoBadLetters(aLine[0:p]):
            print(aLine[0:p])

The ‘find‘ method searches for a sub-string inside another string and returns the position where it was found; if not found, it returns -1.

Since the the word itself ends where “ K W” begins, we can extract it by using the notation aLine[0:p]; in other words, grab the text starting at position 0 and ending before position p.

As a side note, I didn’t realize “croissant” was pronounced with a KW sound, maybe I’m just a philistine?

No Download this Time – Too Short

If you want my code, just cut-and-paste it from the 18-line listing above. And run it yourself with the phoneme file which you can download from my link above.

NR Puzzle Feb 25, 2022

Posted on Updated on

This week’s challenge comes from listener Alan Hochbaum, of Duluth, Ga. Name a famous actor — first and last names. Remove the last letter of each name. You’ll be left with an animal and an adjective that describes that animal, respectively. Who is the actor?

Link to Puzzle at NPR
Take a list of actors, remove a letter from first and last names, and get an animal and an adjective.

Another easy puzzle, and again I elected to solve it using Python. Skills and techniques include:

  • File processing
  • String manipulation
  • Simple Regular Expressions
  • Binary search
  • Loops and basic logic
Screenshot from Visual Studio Code Shows the Terminal Print-out of my 3 Candidate Solutions

Comments

I’m pretty confident in my code, and I have a lists of 16,000 actors, 5,000 adjectives, and 2,000 animals. But, I don’t have all the actors, animals and adjectives! So there could be other solutions that I missed, even though my code works satisfactorily. “Redd Foxx” is not a well known actor these days. Sorry if either is your favorite actor, but I’ve never heard of my other two actors, Cody Longo and Sean Whalen. So, come Sunday, I would not be surprised if Will says some other actor is the solution. But, as you can see, given my data, these three are legitimate solutions.

Redd Foxx in the Breakthrough Television Comedy “Sanford and Son”

Strategy

  • Load the animal file into a list. Note my file is alphabetically sorted
  • Load the adjective file into another list
  • Open the actor file and process each entry
    • Split the name into separate words
      • Using a regular expression expressing to control the separator character
  • Take the first word – call it the first name,
    • Take the last word – call it the actor’s last name
      • Avoid any suffix, such as ‘Jr.’, ‘Sr.’, etc.
    • Use a binary search to quickly search the animal and adjective lists
    • If we get a hit for first and last names, print a candidate solution

Here’s The Code

Step 1: Load the two Files

import re
# Open and load the animals file; note it is presorted
fName = "C:\\Users\\Randy\\Documents\\Puzzles\\Data\\CommonAnimals.txt"
animals = []
# Note: file is already sorted
fHandle = open(fName, 'rt')
for aLine in fHandle:
    animals.append(aLine.strip())

# Open and load the adjectives file, also presorted
adjectives = []
# This file is sorted too
fName = "C:\\Users\\Randy\\Documents\\Puzzles\\Data\\Adjectives.txt"
fHandle = open(fName, 'rt')
for aLine in fHandle:
    adjectives.append(aLine.strip())

A couple notes:

  • I use a double backslash \\ in the file path because a single backslash is used to designate special characters
  • I invoke the ‘strip’ method primarily to remove the linefeed

Step 2: Define the Binary Search Method

def binary_search(list1, n):  
    low = 0  
    high = len(list1) - 1  
    mid = 0  
  
    while low <= high:  
        # Compute index for our next guess, the middle of high and low
        mid = (high + low) >> 1
  
        # Check if n is present at mid
        if list1[mid] < n:  
            low = mid + 1  
  
        # If n is greater, compare to the right of mid
        elif list1[mid] > n:  
            high = mid - 1  
  
        # If n is smaller, compared to the left of mid
        else:  
            return mid  
  
    # element not found, return -1
    return -1

Notes

  • Binary search is extremely fast (it runs in O (log(n)) time). Meaning that the search time is proportionate to the log of the list size. For example, if the list has a million entries, binary search would find an entry in an average 6 operations (because Log(1000000) = 6). Note that it works if the list is sorted.
  • low, mid and high are indexes to the list. ‘mid‘ is the middle of the current search subsection.
  • We examine the element at position mid
    • If it matches our target ‘n’, we found it
    • But if it is smaller, recompute high (which then causes mid to be recomputed)
    • Alternatively, if it is higher, recompute low (which also causes mid to be recomputed next guess)
  • Bit-shift for even more speed: the following is the faster version of division by 2:
    • mid = (high + low) >> 1
  • It does this by shifting the bits to the right one position. I do this for a minor increase in speed; note that division is by far the slowest arithmetic operator

Step 3: Process the Actor File

# Use this list to make sure we get the correct surname
suffixes = ['Jr', 'Sr', 'II', 'III']
fName = "C:\\Users\\Randy\\Documents\\Puzzles\\Data\\Actors\\Actors.txt"
# The file has some non-standard letters, so open with utf-8
fHandle = open(fName, 'rt', encoding="utf8")
for aLine in fHandle:
    # By splitting the name, we can get each name part as 
    # a separate entry in the list 'tokens':
    tokens = re.split(r'[ ,.]', aLine.strip())
    first = tokens[0][:-1]
    last = ''
    lastIndex = len(tokens) - 1
    if tokens[lastIndex] in suffixes and lastIndex >= 2:
        last = tokens[lastIndex -1][:-1]
    elif lastIndex >= 1:
        last = tokens[lastIndex][:-1]

    # At this point, the first and last names have been stripped of
    # their last characters. Some names might not work 
    # because they have length 1,
    # such as the actor 'X Brands' or 'A.J. Trauth', 
    # so skip over empty first/last names
    if last != '' and first != '':
        # see if first name starts corresponds to an animal
        aniIndex = binary_search(animals, first)
        # -1 means no match; so now let's see 
        # if first name works with adjectives:
        if aniIndex == -1:
            adjIndex = binary_search(adjectives, first.lower())

            if adjIndex != -1:
                aniIndex = binary_search(animals, last)
                if aniIndex != -1:
                    print(aLine.strip(), '-->',  adjectives[adjIndex], animals[aniIndex].lower())
        else:
            # First name is an animal; try last name for adjectives
            adjIndex = binary_search(adjectives, last.lower())
            if adjIndex >= 0:
                print(aLine.strip(), '-->', adjectives[adjIndex], animals[aniIndex].lower())

Comments

  • I use a regular expression to split the actor name into a list of separate strings
    • tokens = re.split(r'[ ,.]', aLine.strip())
  • For example, “William Collier, Jr.”
    • Becomes [“William”, “Collier”, “Jr”], i.e. a list of 3 strings
  • The separator characters (delimiters) are space, comma and period, specified here:
    • r'[ ,.]'
  • The first name is at position 0 of the list, the first element
  • The following line of code creates a variable ‘first’ by removing the last character from the first element:
  • Usually, the last string in my list is the actor’s surname, but sometimes they have a suffix like “Sr.”
  • The following code tests if the last string is such a suffix:
    • if tokens[lastIndex] in suffixes and lastIndex >= 2:
  • After assigning the first and last name, we use those variables to perform a binary search
  • If first is an adjective and last is an animal, print a solution
  • Or, if last is an adjective and first is an animal, print

Download the Code

You can download my code and data files here. Note that you will be required to create a free DropBox account to do so.

NPR Sunday Puzzle – Language on 3 Adjacent Phone Keys – Solved in Python

Posted on Updated on

Link to the NPR puzzle.

This week’s challenge: What language in seven letters can be spelled using the letters on three consecutive keys on a telephone? It’s a language you would probably recognize, but not one that many people can speak.

This is an easy and fun puzzle that only requires basic tools to solve. I elected to solve the puzzle using Python. Some of the language features used include:

  • String handling
    • String length
    • strip method
    • casefold method
  • Dictionaries
  • Looping
  • Basic logic and arithmetic operations
  • File reading

Basic Approach

  1. Build a dictionary linking alphabetic letters to keyboard numbers
  2. Open the file containing language names
  3. Loop through each line in the file
  4. Use the strip method to make sure the language name has no trailing blanks or newlines
  5. If the language name has length other than 7, go to the next line
  6. Create two integer variables to reflect the highest and lowest key used by the language, ‘minKey‘ and ‘maxKey
    • Consider how to type the language ‘Klingon’ on your keypad:
    • The highest key is 6, the lowest key is 4. Because K and I are on key 4, while O and N are on key 6
    • We know that Klingon is a valid solution because it uses keys 4, 5, 6, which form a set of 3 adjacent keys, as required by the puzzle
  7. Now, loop through each letter in the language name
  8. Use our dictionary to look up the key for each letter
  9. If the current key is lower than the lowest so far, update minKey
  10. Likewise, update maxKey if the current key is higher than the previous max
  11. If the difference between minKey and maxKey is greater than 2, move to the next line
  12. Finally, after processing every letter in the language, if we haven’t disqualified it, print the language name to the console

Now, let’s look at each of those steps along with the code to implement it. We’ll start with building the dictionary.

Code to Build the Dictionary

import string
# Build a dictionary whose key is the letter and whose value is the keypad key
# For example, keyDic['a'] = 2 because letter 'a' is on key 2
# Another example, keyDic['s'] = 7 because letter 'S' is on key 7
keyDic = {}
key = 0
for lc in string.ascii_lowercase:
    # insert an entry into the dictionary, note that // performs integer division
    keyDic[lc] = 2 + (key // 3)

    # There are letters (PQRS) on key 7 and also 4 on key 9 (WXYZ)
    if lc != 'r' and lc != 'y':
        key += 1

Interpretation: Build an empty dictionary with this line: “keyDic = {}”. The loop iterates the range consisting of the lower case ascii letters, ‘a’ through ‘z’. We are able to do this by importing string (note the first line of code). Note that integer division ignores fractional values, so 3.333 becomes 3.

We add new entries to the dictionary with this statement: “keyDic[lc] = 2 + (key // 3)”

The dictionary looks like this, note the 4 entries sharing value 4 (PQRS) and the 4 entries sharing value 9 (WXYZ):

{'a': 2,
'b': 2,
'c': 2,
'd': 3,
'e': 3,
'f': 3,
'g': 4,
'h': 4,
'i': 4,
'j': 5,
'k': 5,
'l': 5,
'm': 6,
'n': 6,
'o': 6,
'p': 7,
'q': 7,
'r': 7,
's': 7,
't': 8 ,
'u': 8,
'v': 8,
'w': 9,
'x': 9,
'y': 9,
'z': 9 }

Interpretation: look at the first line. The key is ‘a’, its associated value is 2, because a is on the keboard pad for 2.

Code to Open the Language File and Loop Through Each Line

    # This file contains 472 languages from Wikipedia, including constructed languages:
    fPath = "C:\\Users\\Randy\\Documents\\LearnPython\\LanguagList.txt"
    fHandle = open(fPath, "r", encoding='utf-8')

    # Loop handles one language each pass
    for aLine in fHandle:

Interpretation: the file name is in a variable ‘fPath’. We need double slashes (i.e. ‘\\’) to separate the path because, normally, \ is uses as a prefix for characters that interfere with the quotation marks. Note that I have to specify the file encoding, i.e. utf-8, because some language names have special characters, such as the languages ‘Brežice’ or ‘Kēlen’.

Code to Clean the Line of code

    # Loop handles one language each pass
    for aLine in fHandle:
        # Need to remove the newline character
        cleanLine = aLine.strip()

Check the length of the language

    # Puzzle requirements specifies the language has length seven:
    if len(cleanLine) == 7:

Interpretation: we’re in a loop, we execute the following lines only if the length is seven

Declare and Initialize Variables minKey and maxKey

        #To initialize the minKey/maxKey variables, we use the 1st character
        ltr1 = cleanLine[0].casefold()
        if ltr1 in keyDic:
            # these two variables reflect the highest and lowest keys on the keypad
            # as used by the language name
            minKey = keyDic[ltr1]
            maxKey = keyDic[ltr1]

Interpretation: get the first letter in the language, i.e. ‘cleanLine[0]’. Like every other letter in the language name, we check if it exists in the dictionary before accessing the dictionary value. The ‘casefold’ method converts it to lower case. minKey and maxKey share the same value at first; that will change shortly.

Loop Through the Letters in the Language

            # loop through the letters in the language, after converting to
            # lower case, and skipping the first character, which we already looked at
            for c in cleanLine.casefold()[1:]:
                if c in keyDic:
                    # look up the key for the language letter:
                    key = keyDic[c]
                    if key < minKey:
                        minKey = key
                    if key > maxKey:
                        maxKey = key

Interpretation: we already looked at the first letter and we won’t reprocess it. So we slice the word using indexing, i.e. [1:]. (Look at the end of the first line of code in the sample above). This indexing means ‘give me the slice starting at position 1, through the end of the word’. We check if the letter is in the dictionary and update minKey or maxKey as necessary.

If the difference between minKey and maxKey is greater than 2, move to the next line

                    # The puzzle says the language can be spelled by 3 consecutive keys
                    if maxKey - minKey > 2:
                        # Since the diffenece is more than 2, it doesn't qualify
                        # for example, if maxKey = 4 and minKey = 2, then it qualifies
                        # but when maxKey = 5 and minKey = 2, it does not
                        qualifies = False
                        break
                else:
                    # the letter is not on the keypad, probably it is 
                    # a non-standard letter like â, å, -, etc
                    qualifies = False
                    break

Print the Language Name if it Hasn’t Been Disqualified

            if qualifies:
                print(cleanLine)

That’s it! The code loops around after this last statement and processes the next language name. It runs in a couple milliseconds and we get the output shown below:

Terminal Output Showing 2 Language Solutions: Klingon and Kikingo

Get the Code

I uploaded my code (just 67 short lines) to my DropBox account, along with my language file (472 language names from Wikipedia). You can download the code here: get the code here. Note: you will need your own DropBox account (free).

NPR Sunday Puzzle, World Capital to Farm Animal

Posted on

WPF Screen Shot
Screen shot showing the solution

Synopsis

What makes this puzzle challenging is the lack of a good list of “informal” farm animals. There are lists of farm animal names, they don’t work. Also, the solution, “moocow”, requires a really big dictionary, though my PC has plenty of power to solve the puzzle in a few seconds. However, it is nice to have a progress bar, so my code does so.

Approach

I don’t have a list of informal farm animal names; maybe I can find an ordinary word which contains a farm animal inside it. Check every capital which can be morphed into other words, and then check to see if any of those words contain a farm animal name.

Specifics

  1. Build a dictionary of every word, the dictionary key will be the capital with the 3rd letter chopped out, each entry’s payload (value) will be the original word(s)
  2. For example, “moscow” → [“mocow”, “moscow”], i.e. the key is the chopped word and the value is the original word
  3. Sometimes the same dictionary key corresponds to multiple words, for example, Accra, acara and Acura all turn into ‘acra’ when you chop-out  the 3rd letter
  4. Besides building the dictionary, build a list of every common farm animal, this will be pretty short
  5. Next, process the file containing capital names.
  6. For every capital, chop-out the 3rd letter and look-up the corresponding words in the dictionary
  7. At this point, we need to check every farm animal and see if any of them are a substring of any of the words the capital can morph into
  8. If so, display it as part of the solution

Additional Techniques

I used the IProgress construct, along with asynchronous invocation of my solving method to allow the UI to remain responsive, and update my progress bar, while the code runs.

The Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.IO;
using System.Collections.ObjectModel;

namespace Dec30_2018 {
  /// <summary>
  /// Holds the controls and code to solve the puzzle
  /// </summary>
  public partial class MainWindow : Window {
    /// <summary>Bound to the solution grid</summary>
    private ObservableCollection<Tuple<string, string>> _SolutionList;

    public MainWindow() {
      InitializeComponent();
    }

    /// <summary>
    /// Fires when user clicks the solve button
    /// </summary>
    /// <param name="sender">The buton</param>
    /// <param name="e">Event args</param>
    private async void BtnSolve_Click(object sender, RoutedEventArgs e) {
      Mouse.OverrideCursor = Cursors.Wait;
      btnSolve.IsEnabled = false;
      _SolutionList = new ObservableCollection<Tuple<string, string>>();
      grdSolution.ItemsSource = _SolutionList;

      //This code fires when the SolvePuzzle method reports progress:
      var progress = new Progress<Tuple<int, string, string>>(args => {
        prg.Value = args.Item1;
        if (!string.IsNullOrWhiteSpace(args.Item2))
          _SolutionList.Add(Tuple.Create(args.Item2, args.Item3));
      });

      //Run the solution on a different thread so the UI will be interactive
      await Task.Run(() => SolvePuzzle(progress));

      btnSolve.IsEnabled = true;
      Mouse.OverrideCursor = null;
    }

    /// <summary>
    /// Solves the puzzle using a big word list, a list of captials, and a list of farm animals
    /// </summary>
    /// <param name="rptProg">Basically a method to invoke when we want to report progress</param>
    private void SolvePuzzle(IProgress<Tuple<int, string, string>> rptProg) {
      string wordFile = @"C:\Users\User\Documents\Puzzles\Data\NPLCombinedWordList.txt";

      //Key to the dictionary will be the word with its 3rd letter cut-out, the payload will
      //be a comma-separated list of words that correspond to it.
      //For example, for Moscow, the key will be 'mocow' and the value will be 'Moscow'
      //For Accra, the key will be 'acra' and the value will be 'acara,accra, acura'
      var wordDic = new Dictionary<string, string>();
      var fSize = (new FileInfo(wordFile)).Length;

      int processedBytes = 0;
      int lastPct = 0;
      using (StreamReader sr = File.OpenText(wordFile)) {
        while (sr.Peek() != -1) {
          string aWord = sr.ReadLine();
          if (aWord.Length > 3) {
            string key = (aWord.Substring(0, 2) + aWord.Substring(3)).ToLower();
            if (wordDic.TryGetValue(key, out string fullWords))
              wordDic[key] += "," + aWord;
            else
              wordDic.Add(key, aWord);
          }

          //Calculate the percentage of the file processd and report progress if it increase by 1 point
          processedBytes += aWord.Length + 2;
          var pct = (int)(100 * ((double)processedBytes / fSize));
          if (pct != lastPct) {
            rptProg.Report(Tuple.Create(pct, "", ""));
            lastPct = pct;
          }
        }
      }

      //Build a list of farm animals:
      var farmAnimalList = new List<string>();
      string farmAnimalFile = @"C:\Users\User\Documents\Puzzles\Data\FarmAnimals.txt";
      using (StreamReader sr = File.OpenText(farmAnimalFile)) {
        while (sr.Peek() != -1) {
          farmAnimalList.Add(sr.ReadLine());
        }
      }

      //Process the list of capitals and, for each entry, check to see if it forms a solution
      string capitalFile = @"C:\Users\User\Documents\Puzzles\Data\WorldCapitals.txt";
      using (StreamReader sr = File.OpenText(capitalFile)) {
        while (sr.Peek() != -1) {
          string aLine = sr.ReadLine();
          //A typical line looks like 'Moscow~Russia' or 'London~Great Britian'
          int p = aLine.IndexOf('~');
          string capital = aLine.Substring(0, p);
          if (capital.Length > 3) {
            //Chop-out the 3rd letter and get the words that correspond to it in the word dictionary:
            string key = (capital.Substring(0, 2) + capital.Substring(3)).ToLower();
            if (wordDic.TryGetValue(key, out string morphedWords)) {
              //Build an array for all the words which share the same key:
              var splitWords = morphedWords.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
              //Perform a Cartesian product so we get every combination from the farm animal list and the words
              //corresponding to the key:
              var joinList = splitWords.Join(farmAnimalList, 
                               s => "", f => "", 
                               (s, f) => new { Word = s, Animal = f });

              //Examine every combo in the cartesian product:
              foreach (var candidate in joinList) {
                //If the animal is a substring of the word, then we have a solution,
                //such as "moocow".IndexOf("cow") = 3, because cow is a substring of moocow at position 3
                if (candidate.Word.IndexOf(candidate.Animal) >= 0
                  && capital.ToLower() != candidate.Word) {

                  //Report progress with the solution pair, which will disply it in the grid:
                  rptProg.Report(Tuple.Create(100, capital, candidate.Word));
                }
              }
            }
          }
        }
      }
    }
  }
}

Get the Code

Download the code here, from my dropbox account. Note: they will ask you to create a free account to perform the download.

Puzzle: Chicagoan Name to Actor Name

Posted on

Solution
Screen Shot Showing the Solution

Synopsis

Another easy one! The appropriate name lists are available on Wikipedia, and the main coding issue is knowing how to check whether two names are anagrams. (Easy! Sort both candidates –> anagrams are the same after sorting . E.g. ‘caponi  -> acinop, also pacino -> acinop.)

Getting the Names

I checked Wikipedia, they had a nice list of about 500 famous people from Chicago. I looked through the names and immediately suspected Al Capone, since he was the most famous of the few whose names ended with ‘e’. The hard part would be finding an actor to match.

Happily, I already had a set of files for actor’s names from a previous puzzle, also harvested from Wikipedia.

I elected to grab the name of every Chicagoan and save them in a file, who knows, I might need them again. This time, I manually cut-and pasted the data, cleaned it up, and saved to a file. I’ll include the file in the code download, listed below.

CaponeAndPacino.png
Capone’s mugshot and Pacino from one of his many roles

Algorithm Outline

  1. Read the Chicagoan name file, keep the few whose name ends in ‘e’
  2. Read the actor name files, store the results in a dictionary
    1. Use the sorted last name as the dictionary key
    2. Use the first/last names as the value for each entry
    3. For convenience, store them as a Tuple<string, string>
    4. ActorsDictionary
    5. Note that a ‘Tuple’ is simply a means of holding two (or more) things in a single object, in this case, my tuple is composed of Item1 = first name and Item2 = last name.
  3. For each Chicagoan, replace the last ‘e’ with an i (capone –> caponi)
  4. Sort the revised name (caponi -> acionp)
  5. Use the result to check if there is an entry in the dictionary with matching  that key
    1. Because dictionaries use hashing, this check is super efficient.
  6. If the dictionary contains such a key, retrieve the value and check if the first name matches the candidate from Chicago.
  7. If so, we found the solution!

Techniques Used

Selected Code Highlights

Here’s the code from the button click event method:

private void btnSolve_Click(object sender, RoutedEventArgs e) {
  Mouse.OverrideCursor = Cursors.Wait;
  string chicogoansFile = @"C:\Users\User\Documents\Puzzles\Data\Chicogoans.csv";
  var chicogoans = GetChicogoansEndingWithE(chicogoansFile);

  string actorFolder = @"C:\Users\User\Documents\Puzzles\Data\Actors";
  var actorsDic = GetActors(actorFolder);

  foreach(var chicagoPerson in chicogoans) {
    //each entry is a Tuple of two strings, the last name is item2
    //Sort swap the e for an i and sort
    var sortedName = (chicagoPerson.Item2.Substring(0, chicagoPerson.Item2.Length - 1) + "i")
             .ToLower()
             .OrderBy(c => c)
             .Aggregate("", (p, c) => p + c);
    //Is there an actor whose name is an anagram of 'sortedName'?
    if (actorsDic.ContainsKey(sortedName)) {
      //OK, now check if the Actor's first name is the same:
      if (actorsDic[sortedName].Item1 == chicagoPerson.Item1) {
        string firstName = actorsDic[sortedName].Item1;
        string lastName = actorsDic[sortedName].Item2;
        //Build a string to display in the UI: 
        string candidate = $"{chicagoPerson.Item1} {chicagoPerson.Item2}" +
                   "-->" +
                   "{firstName} {lastName}\n";
        txtSolution.Text += candidate;
      }
    }
  }
  Mouse.OverrideCursor = null;
}

Notes

  • GetChicogoansEndingWithE” is a method I wrote to build a list of people from Chicago. Download my code (below) for details
  • GetActors” is another method I wrote that builds a dictionary of actors, using their sorted last name as the key
  • I sort the last name using a lambda expression
    • The following fragment replaces the ‘e’ with ‘i’ in the Chicagoan’s last name:
      • (chicagoPerson.Item2.Substring(0, chicagoPerson.Item2.Length – 1) + “i”)
    • “.ToLower()” converts the result from above to lower case, i.e. ‘Caponi’ -> ‘caponi’
    • The following fragment sorts the letters in the result from above:
      • .OrderBy(c => c)
      • Note that this code treats a string as a list of char elements
      • In the syntax above, ‘c’ represents the thing to order by, namely, each char
    • The result from above is another list of char
    • The following fragment converts the list back to a string:
      • .Aggregate(“”, (p, c) => p + c)
      • Where “” is the seed to use for agregation
      • (p,c) represents the arguments used for the aggregation function, ‘p’ is result from the previous aggregation operation, ‘c’ is the current value
      • p + c is the aggregation operation, i.e. take the previous and add current to it

Download the Code!

I uploaded my code to my DropBox site, you can download it here. Note that they will ask you to create a free account in order to download.

 

Puzzle: Singer Name to Group Type

Posted on

The Challenge

This challenge comes from listener Patrick Berry of Jasper, Ala. Name a famous singer — 3 letters in the first name, 5 letters in the last. Drop the middle letter of the last name and rearrange the result to name a variety of singing group. What is it?

The Solution

Dec31_2017_Solution
Screen-Shot Showing the Puzzle Solution

Synopsis

Fun, easy! I was fortunate to have a file lying around with singer names, and it was easy enough to go to Wikipedia to grab a list of musical group types.

Also: woo-woo! I Almost solved it without a single bug! I was able to fix my solitary bug during a debug session, so I can honestly say it ran the first time.

Techniques Used

The Code

string singerFile = @"Singers.txt";
string groupTypeFile = "MusicalGroupTypeList.txt";

//Make a function to sort the letters in a string, converting 
//to lower case, and removing all but letters
Func<string, string> sortLetters = (g) => g
                            .Where(c => char.IsLetter(c))
                            .Select(c => char.ToLower(c))
                            .OrderBy(c => c)
                            .Aggregate("", (p, c) => p + c);

//The dictionary key is the sorted letters; the 
//value is the original string
var groupTypeDic = new Dictionary<string, string>();

//Open the file of group types; use it to build the dictionary
using (StreamReader sr = File.OpenText(groupTypeFile)) {
    while (sr.Peek() != -1) {
        string grpType = sr.ReadLine();

        string key = sortLetters(grpType);
        if (!groupTypeDic.ContainsKey(key)) {
            groupTypeDic.Add(key, grpType);
        }
    }
}

//Now read all the singers from the file
using (StreamReader sr = File.OpenText(singerFile)) {
    while (sr.Peek() != -1) {
        string singerName = sr.ReadLine();
        string[] tokens = singerName.Split(' ');
        int lastIndex = tokens.Length - 1;
        if (tokens.Length > 1 
            && tokens[0].Length == 3 
            && tokens[lastIndex].Length == 5) {
            //Combine the first name and /1st 2 letters of last name, 
            //plus the last 2 letters of last name
            string remove3rdLetterLastName = tokens[0]
                                       + tokens[lastIndex].Substring(0, 2)
                                       + tokens[lastIndex].Substring(3);
            string sorted = sortLetters(remove3rdLetterLastName);
            //Does the dictionary contain the sorted letters?
            if (groupTypeDic.ContainsKey(sorted)) {
               //Yipee! Found a solution
               txtSolution.Text = $"{singerName} → {groupTypeDic[sorted]}";
            }
        }
    }
}

 

Explanation

Remember, if two words are anagrams, then their sorted letters are the same.

  • “Bob Dylan” → “bobdyan” →”abbdnoy”
  • “Boy Band” → “boyband” → “abbdnoy”

Encapsulate that Functionality in an Anonymous Function named ‘sortLetters’

Func<string, string> sortLetters = (g) => g
      .Where(c => char.IsLetter(c))
      .Select(c => char.ToLower(c))
      .OrderBy(c => c)
      .Aggregate("", (p, c) => p + c);

Here I create a function variable named ‘sortLetters’ that has one input parameter (string) and returns another string.

Inside the function, the input parameter (‘g’) is treated like a list of char, because I invoke Linq methods on it.

  • The ‘where-clause’ removes any spaces or other funky chars
  • The ‘select-clause’ converts ‘BobDylan’ → ‘bobdylan’
  • The ‘OrderBy’ clause converts ‘bobdylan’ to ‘abbdnoy’
  • The ‘Aggregate’ clause converts the result back to a string

Read all the Group Types, Hold Each in Variable ‘grpType’

using (StreamReader sr = File.OpenText(groupTypeFile)) {
 while (sr.Peek() != -1) {
 string grpType = sr.ReadLine();

Store the Group Type in a Dictionary

string key = sortLetters(grpType); 
if (!groupTypeDic.ContainsKey(key)) { 
    groupTypeDic.Add(key, grpType); 
}

Dictionary
Screen-Shot from debug session helps visualize the dictionary of group types

Read the Singers File, Store Each in Variable ‘singerName’

using (StreamReader sr = File.OpenText(singerFile)) { 
    while (sr.Peek() != -1) { 
        string singerName = sr.ReadLine();

Split the Singer Name into Parts, Proceed if Lengths are 3/5

string[] tokens = singerName.Split(' '); 
int lastIndex = tokens.Length - 1; 
if (tokens.Length > 1 
    && tokens[0].Length == 3 
    && tokens[lastIndex].Length == 5) {
  • The ‘split’ function returns an array of string

Split Results
Screen-Shot from debugger session shows how ‘Ziggy Marley’ → an array of length = 2

Remove 3rd Letter From Singer’s Name, Sort the Letters

 string remove3rdLetterLastName = tokens[0] 
                                  + tokens[lastIndex].Substring(0, 2) 
                                  + tokens[lastIndex].Substring(3); 
string sorted = sortLetters(remove3rdLetterLastName);

Here we concatenate the first name with the first 2 letters of last name and then the last 2 letters of the last name. Re-use the anonymous function to sort the letters.

If the Sorted Letters Match a Dictionary Entry, Add the Result to the TextBox

if (groupTypeDic.ContainsKey(sorted)) { 
    //Yipee! Found a solution 
    txtSolution.Text = $"{singerName} → {groupTypeDic[sorted]}"; 
}

Get the Code

You can download my code here. Note that you will need to create a (free) dropbox account to access the link.

Puzzle: US City With Only 3 Letters

Posted on

The Sunday Puzzle Challenge:

The name of what well-known U.S. city, in 10 letters, contains only three different letters of the alphabet? Puzzle URL

Synopsis

Fun and Simple! You can get the census place name file from the US Census Bureau.

CityPuzzleSolution
Screen Shot Shows the Solution

Techniques Used

Code to Solve the Puzzle

private void btnSolve_Click(object sender, RoutedEventArgs e) {
  string cityFile = 
	@"C:\Users\User\Documents\Puzzles\Data\AmericanPlaces2k.txt";

  using(StreamReader sr = File.OpenText(cityFile)) {
    while(sr.Peek() != -1) {
      string aLine = sr.ReadLine();
      //File is fixed-width, city starts in column 9
      string cityName = aLine.Substring(9, 30)
			     .TrimEnd();

      //Place names end with town/city/CDP; remove it now:
      if (cityName.EndsWith(" town"))
        cityName = cityName.Substring(0, cityName.Length - 5);
      else if (cityName.EndsWith(" CDP"))
        cityName = cityName.Substring(0, cityName.Length - 4);
      else if (cityName.EndsWith(" city"))
        cityName = cityName.Substring(0, cityName.Length - 5);

      //Remove spaces and other non-letters:
      var letters = cityName.ToLower()
                  .Where(c => char.IsLetter(c));

      //Check if the city has 10 letters and a distinct count of 3
      if (letters.Count() == 10) {
        int letterCount = letters.Distinct().Count();
        if (letterCount == 3) {
          txtSolution.Text += $"{cityName}\n";
        }
      }
    }
  }
}

Any Tricky Parts?

When you use Linq  on a string variable, it treats your variable as an array of char. That is how I can, for example, execute this line of code:

var letters = cityName.ToLower().Where(c => char.IsLetter(c));

In the code above, ‘c‘, refers to a char in the city name. Note that the result above, ‘letters‘, has data type ‘IEnumerble<char>’. I.e., everything is a char when you use Linq on a string.

Download the Code

You can download the code from my DropBox account. To do so, you will need to create a free account. If you run the downloaded code, you will need to adjust the path to the city name file. I included that file in the download.

Sunday Puzzle for Oct 1st, 2017

Posted on

Flan.pngI sure hope I found the solution Will was looking for! I’ve never heard of ‘flan’ until now. I know Will creates crossword puzzles for a living, so maybe flan is a common word to him. Or, maybe, as a New Yorker, he eats fancy desserts all the time.

According to Google, the dish pictured here is a ‘flan’. Sorry, it looks like cheese cake to me! I guess I’m not a gourmand.

Screen Shot_Oct1_2017.png
Screen shot showing the challenge and it’s solution

Struggle Notes

If you look at the buttons on my screen shot, you might guess I tried pretty hard to get alternative word lists. (I wasn’t too happy with ‘flan’.)

  • The button ‘Try All Words” uses a huge dictionary of English words (forget mere food words, try anything!), so if the solution used common words, like ‘corn’, that would have solved it
  • The button “Get Name Brand Foods” downloads foods that might not be in my dictionary of English words.
    • Since neither of those two methods found a different solution, I’m sticking with flan/gumbo.

Downloading Food Names

Gumbo.png
Mmm, gumbo!

Note my other button, ”Get Food Names”.

 

  • This button goes to Wikipedia’s page https://en.wikipedia.org/wiki/Lists_of_foods, which is a list of links…
  • Follows each link, and grabs the food names from that page
  • The result is 347 files of various food names, such as ‘List_of_pies,_tarts_and_flans’.
  • If you’re a web guru, you might guess that takes a while to run. And you’d be correct; it took about 15 minutes to download them all.

Techniques Used

How To Shift Letters Using Linq

string shifted = aWord.Select(c => c == 'z' ? 'a' : (char)(c + 1))
                      .Aggregate("", (p, c) => p + c);

This code shifts each letter by one, as Will describes it in the challenge. For example, ‘flan’ becomes ‘gmbo’.
Notes:

  1. My variable ‘aWord’ might contain, for example, the word ‘flan’
  2. Linq Select treats aWord as a list of letters (i.e. list of char data type)
    1. The code examines each letter, c
    2. I use the ternary operator on each letter. ‘Ternary Operator” is a short-hand if-statement
    3. It takes the form condition ? first_expression : second_expression;
    4. If the letter c is ‘z’ (my condition)
      1. Return an ‘a’ (my first expression)
      2. Otherwise, add one to it and convert it back to char (2nd expression)
  3. The aggregate operator converts the list of char back into a string
  4. If  ‘aWord’ contained “flan”, shifted would be assigned “gmbo”

How to Solve the Puzzle

The method below uses the code from above to shift a 4-letter word. My method needs a list of 4-letter foods and of 5-letter foods, passed as parameters

private void SearchListsForSolution(List<string> fiveLetterFoodList, 
                  List<string> fourLetterFoodList) {
  List<string> solutionList = new List<string>();
  foreach (string aWord in fourLetterFoodList) {
    if (!solutionList.Contains(aWord)) {
      string shifted = aWord.Select(c => c == 'z' ? 'a' : (char)(c + 1))
                  .Aggregate("", (p, c) => p + c);

      //Loop injects 'u' at 3 different positions to build candidate
      //So we try 'gumbo', 'gmubo' and 'gmbuo'
      for (int i = 1; i <= 3; i++) {
        string candidate = shifted.Substring(0, i) 
                           + "u" + shifted.Substring(i);

        //fiveLetterFoodList is a sorted list of foods
        int p = fiveLetterFoodList.BinarySearch(candidate);
        //if p is greater than zero, we found candidate in the list
        if (p >= 0) {
          //Add the 4-letter word, shifted version 
          //and 5-letter word to textbox
          tbSolution.Text += 
             $"{aWord } → {shifted} + u → {fiveLetterFoodList[p]}\n";
          solutionList.Add(aWord);
        }
      }
    }
  }
}

Notes

  • There are only 3 places to inject a u into a 4-letter word
  • My code tries all 3 places and checks to see if the result is a word
  • By using Binary Search against the 5-letter word list
  • The BinarySearch method returns an index > 0 if it finds the candidate
  • So when that happens, I update my TextBox ‘tbSolution’ to include what I just found
  • The list ‘SolutionList’ helps me avoid entering the same solution twice; I need it because ‘flan’ was present in multiple files

Removing Diacritics

Just in case, I tried fixing words like ‘Ragú‘, converting it to ‘Ragu‘ by removing the accent over the letter u. (It didn’t help.) It was pretty easy; I just copied the code from here: “Stripping is an Interesting Job…

That code looked like this:

static string RemoveDiacritics(string text) 
{
    var normalizedString = text.Normalize(NormalizationForm.FormD);
    var stringBuilder = new StringBuilder();

    foreach (var c in normalizedString)
    {
        var unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
        if (unicodeCategory != UnicodeCategory.NonSpacingMark)
        {
            stringBuilder.Append(c);
        }
    }

    return stringBuilder.ToString().Normalize(NormalizationForm.FormC);
}

Get the Code!

As usual, you can download my code from my DropBox account here. As usual, they will only let you download if you create a free account with them. Note: my zip file contains all the food lists, located in my bin folder.