Linq
Calculator Letters Puzzle – Solved!
Or, Fun with Lambdas and Set Functions!
The challenge:
This three-part challenge comes from listener Lou Gottlieb.
If you punch 0-1-4-0 into a calculator, and turn it upside-down, you get the state OHIO.
What numbers can you punch in a calculator, and turn upside-down, to get a state capital,
a country and a country’s capital?
Puzzle Location:
http://www.npr.org/2014/09/14/348220755/the-puzzle-and-the-pea
The Solution Revealed!
- State Capitals
- World Capitals
- Countries
I’ve included those data files in my code download; if you run my code, note that their path will be different on your PC than mine.
Next, we’ll make a list of the available letters, which I have listed in the image below:
Once we have the data, it is basically a matter of determining whether one string set is entirely included within another string set. In other words, we will ask, for a number of solution candidates, whether it is a proper subset of the set of calculator letters.
To determine whether we have a subset, it is fun to use the Linq built-in set operators. In particular, we can use the method ‘Except‘. I also could have used the HashSet functionality, which actually has a method named ‘IsProperSubSet’, but HashSet is not as convenient as Linq and I just wanted to crank-out some code!
Using Linq, we can determine that one string is a proper subset of another if, when we use Except on them, the result is the empty set, i.e. a list with no members. Examples:
- “atlanta”.Except(“beignlos”) will return a list containing (‘a’, ‘t’), i.e. not a subset.
- “cheyenne”.Except(“beignlos”) will return a list containing (‘c’, ‘h’, ‘y’). Also not a subset.
- “boise”.Except(“beignlos”) returns the empty set, i.e. it is the answer!
Note: “beignlos” is a string containing all the calculator letters. The true benefit of the ‘Except’ method is apparent below:
Here’s a Super-Cool Sample of the .NET Set-Operator ‘Except’
var leftoverLetters = "boise".Except("beignlos"); if (leftoverLetters.Count() == 0) Console.WriteLine("Ding ding ding - we have a winner!");
Conclusion: ‘boise’ is a subset of ‘beignlos’ –> you can spell ‘Boise’ using upside down calculator letters, and the Except function tells us that.
Use Set-Operators at Work, Too!
At work, I have recently used the ‘Except’ operator to find orders that were lost. Something along these lines: ‘var missing = outboundOrders.Except(inBoundOrders);‘
My guess is you can use the Except operator to identify a lot of other things!.
Using Lambdas as ‘First Class’ Objects
First, a little bit of terminiology: “First class objects” are objects that you can use like any other entity. Such uses include passing them as parameters. When you pass a Lambda expression as a parameter, you are taking advantage of the fact that Lambdas are first class objects.
Why did I tell you that? Because my code benefited from it in this fun little app. In particular, I have these needs:
- I need to perform the same work on 3 different files (state capitals, world capitals, countries)
- Each file has a different layout
- I would like to write a single method to do the same work for each file
- To do so, I could write 3 different lambdas; each knowing how to extract the candidate from a line in a file
- And then pass each lambda as an input parameter it to my method
- Benefit: I write one method, which uses a lambda to extract the candidate, instead of 3 methods, each with their own
techinque to extract the candidates
Here’s the Code
private void btnSolve_Click(object sender, RoutedEventArgs e) { //Make a definition list of the calculator letters. //This maps letters to the numbers they represent in a calculator. //Each letter is in lower case to make matching easier //Note: a 'tuple' is a type of anonymous class List<Tuple<char, int>> calculatorLetterMap = new List<Tuple<char, int>>() { Tuple.Create('b', 8), Tuple.Create('e', 3), Tuple.Create('i', 1), Tuple.Create('g', 6), Tuple.Create('n', 4), Tuple.Create('l', 7), Tuple.Create('o', 0), Tuple.Create('s', 5) }; //The next 3 lines process state capitals: Func<string, string> extractStatCap = (aLine => { //Look for hyphen, take the rest of the line, trimmed, in lowercase return aLine.Substring(aLine.IndexOf('-') + 2) .TrimEnd() .ToLower(); }); List<string> stateCapHits = GetFileMatchesAgainstLetterSet(STATE_CAPITALS_FILE, calculatorLetterMap, extractStatCap); DisplayHits(stateCapHits, "State Capitals", calculatorLetterMap, txtResults); //Now world capitals: //The function is a bit more complicated because some names have spaces, such as 'Abu Dhabi' Func<string, string> extractPlaceFunc = (aLine => { //Look for a tilde and take everything prior to it, removeing non-letters return aLine.Substring(0, aLine.IndexOf('~')) .Where(c => char.IsLetter(c)) .Aggregate("", (p,c) => p + c); }); List<string> worldCapHits = GetFileMatchesAgainstLetterSet(WORLD_CAPITALS_FILE, calculatorLetterMap, extractPlaceFunc); DisplayHits(worldCapHits, "World Capitals", calculatorLetterMap, txtResults); //Finally, country names: Func<string, string> extractCountryFunc = (aLine => aLine); List<string> countryHits = GetFileMatchesAgainstLetterSet(COUNTRIES_FILE, calculatorLetterMap, extractCountryFunc); DisplayHits(countryHits, "Countries", calculatorLetterMap, txtResults); } /// <summary> /// Loops through a file and returns a list of every entry which /// can be composed solely with 'calculator letters' /// </summary> /// <param name="fName">The file containing things that might match</param> /// <param name="calculatorLetterMap">List of letters derived from upside-down /// calculator numbers</param> /// <param name="extractPlaceFunc">Function to extract the item of interest /// from each line in the file</param> /// <returns></returns> private static List<string> GetFileMatchesAgainstLetterSet(string fName, List<Tuple<char, int>> calculatorLetterMap, Func<string, string> extractPlaceFunc) { List<string> result = new List<string>(); using (StreamReader sr = File.OpenText(fName)) { while (sr.Peek() != -1) { //Execute the Lambda provided by our caller: string candidate = extractPlaceFunc(sr.ReadLine()); var leftOverLetters = candidate .ToLower() .Except(calculatorLetterMap.Select(m => m.Item1)); //If there are no leftover letters, we have a winner! if (leftOverLetters.Count() == 0) { string propperCase = char.ToUpper(candidate[0]) + candidate.Substring(1); result.Add(propperCase); } } } return result; } /// <summary> /// Displays every hit in the list with its equivalent in 'calculator letters' /// </summary> /// <param name="hitList">List if items to dispaly</param> /// <param name="caption">Caption, such as 'State Capitals'</param> /// <param name="calculatorLetterMap">List of letter/number pairs for translating</param> /// <param name="txtResult">Textbox to hold the result</param> private static void DisplayHits(List<string> hitList, string caption, List<Tuple<char, int>> calculatorLetterMap, TextBox txtResult) { txtResult.Text += caption + "\n"; foreach (string aHit in hitList) { string number = MapLettersToCalculatorNumbers(aHit, calculatorLetterMap); txtResult.Text += string.Format("\t{0} = {1}\n", number, aHit); } } /// <summary> /// Translates the puzzle answer ('candidate') back into 'Calculator letters' /// </summary> /// <param name="candidate">Such as 'boise' or 'benin' or 'lisbon'</param> /// <param name="calculatorLetterMap">List of letter/number pairs</param> /// <returns>A string composed of numerals</returns> private static string MapLettersToCalculatorNumbers(string candidate, List<Tuple<char, int>> calculatorLetterMap) { string result = (from c in candidate join m in calculatorLetterMap on c equals m.Item1 select m.Item2 ).Aggregate("", (p, c) => p + c); return result; }
Download the code here: Puzzle Code
ILookup: A Hidden, Powerful Feature of Linq
The ILookup Interface Just Made My Life Easier!
It was a classic case of something that sounded easy to the users, but which was, in reality, somewhat tricky. All right, I admit it, at first I thought would be easy too!
Problem: our ERP sometimes generates two line items for the same product. Actually, it wasn’t a problem until we got a new eCommerce partner and they griped about multiple line items for the same thing. “Your’e making us work too hard here, our warehouse people are getting frustrated when they have to go back to the bin they just visited to pickup something they could have grabbed the first time!”.
Naturally, our business folks wanted to please, so I got my marching orders to “Just consolidate the two line items into one and sum the quantity. It’ll be easy!”. Famous last words.
My first approach was too complicated: use a Linq Groupby clause to grab all the duplicates (along with quantities), then iterate all the line items and, for each item, if my list of duplicates contained the current Product ID, write the first line item with the grand total and suppress all subsequent items. Too complicated, I hope you’re still reading!
Here’s what I ended-up doing with ILookup:
int prodIndex = 0; //Build the lookup //Each entry will have the ProductId for the key //The value is the list of all line items sharing that key! ILookup<string, LineItem> distinctProductIds = ((IEnumerable)orderHeader.DetailList) .ToLookup(i => i.ProductID); result = new ShippingProduct[distinctProductIds.Count()]; //Loop through the distinct ProductIDs: foreach (var anOrderItem in distinctProductIds) { //Grab the first product that might be duplicated LineItem firstInItemGoup = distinctProductIds[anOrderItem.Key].First(); ShippingProduct curProd = new ShippingProduct(); curProd.SKU = anOrderItem.Key; curProd.Name = firstInItemGoup.Description; //Here is where the order gets consolidated curProd.Quantity = distinctProductIds[anOrderItem.Key].Sum(i => i.Quantity).ToString(); //... Copy all other remaining fields result[prodIndex++] = curProd; }
Receiving Order Confirmations
OK, that wasn’t really tricky, but receiving the order confirmations from our eCommerce partner was. Fortunately, I used similar code for that, and that made life easier. One reason for the trickiness: our eCommerce partner doesn’t send the Product ID back to us, just the line item IDs. Also tricky because I had to credit the original order line items with the correct quantity.
ILookup<string, LineItem> ProductIDLookup = detailList.ToLookup(i => i.ProductID); //Loop through each line item from partner and match, on LineItem ID (LineSeq == ItemNumber) to find a line item in Traverse that matches theirs foreach (OrdersItem orderLineItem in egOrder.Items) { decimal partnerQty = 0; decimal.TryParse(orderLineItem.Shipped, out partnerQty); if (partnerQty > 0) { LineItem curOrderDetail = detailList.FirstOrDefault(dtl => dtl.LineSeq.Value.ToString() == orderLineItem.ItemNumber); if (curOrderDetail != null) { string ProductID = curOrderDetail.ProductID; decimal lineItemTotQty = ProductIDLookup[ProductID].Sum(i => i.Quantity); if (lineItemTotQty !=partnerQty) { string msg = string.Format("Quantity shipped by does not match "); throw new ApplicationException(msg); } //There should only be one line item for this ProductID (product id) //BUT, if there are duplicates, we will handle them all, so long as we do not exceed the quantity actually shipped by partner foreach (LineItem erpLineItem in ProductIDLookup[ProductID].OrderByDescending(l => l.UnitPrice)) { decimal qtyToCredit = erpLineItem.Quantity; ReconcileOrder(erpLineItem, qtyToCredit, erpLineItem.TransId); partnerQty -= qtyToCredit; if (partnerQty <= 0) break; } } } }
Bottom Line
Any time you need to group members of your list on some field, whether it is Product ID, customer ID, zip code, or any other field, and then do something with each little group, the ILookup interface makes your life easier. For starts, building the list is painless. I’m surprised I never knew about it before!
NPR Puzzle Solved: June 29, 2014
The Challenge:
This week’s challenge comes from Steve Baggish of Arlington, Mass., the father of the 11-year-old boy who created the challenge two weeks ago. Name a boy’s name and a girl’s name, each in four letters. The names start with the same letter of the alphabet. The boy’s name contains the letter R. Drop that R from the boy’s name and insert it into the girl’s name. Phonetically, the result will be a familiar two-word phrase for something no one wants to have. What is it?
Challenge URL
Link to the Sunday Puzzle
Overview
This was kind of interesting: even though my code won’t yield an exact solution, I was still able to produce a short list of potential solutions and then manually pick the best answer from that list. Main problem: the English language does not have simple pronunciation rules! I am using the Soundex algorithm to compare the pronunciation of phrases against words I construct (from boy/girl names), and the Soundex algorithm is only an approximation. Among its many drawbacks, Soundex doesn’t account for differences in vowel pronunciation. Creating your own replacement for Soundex is a lot of work, email me if you have a better way!
The soundex algorithm is a very useful algortithm; if you read this article, you will learn one good way to use it. You can download implementations of the algorithm from many places; I think I got my version from the Code Project. I have a link below to its description on Wikipedia.
You can also learn from my examples using Linq and .NET Dictionaries, both very useful tools to master!
Solution
Algorithms I Used
Soundex Algorithm for Mapping Pronunciation
The Soundex function takes a string for input and generates a number that corresponds to the pronunciation. Words that sound alike have similar soundex values. For example,
- Soundex(“Robert”) == R163
- Soundex(“Rupert”) == R163
- Soundex(“Rubin”) == R150
- Since Robert sounds a lot like Rubert, both names have the same soundex value.
- As you can see, the soundex algorithm is just an approximate way to match similar words; according to soundex, “bated” sounds the same as as “bad” (because they both have the same soundex value of B300)
In my solution (explained below), I manipulate boy/girl names to construct candidate solutions. If my two candidate names have the same soundex values as a common phrase, then I know that boy and girl might be the answer I seek.
You could use the Soundex algorithm for a customer service app, when the customer talks with marbles in their mouth and your users need to quickly find possible spellings for what customer is telling them!
Levenshtein Distance for Measuring String Similarity
The Levenshtein Distance indicates the similarity of two strings. The modified algorithm I used, computes a distance between 0 and 100; when the distance is 100, the two strings have nothing in common, when their distance is 0, they are identical. The lower the number, the more similar. For example,
- LevenshteinDistance(“kitten”, “kitting”) == 28
- LevenshteinDistance(“kitten”, “sitting”) == 42
- ImprovedLevenshteinDistance(“kitten”, “asdf”) == 100
Thanks to Sten Hjelmqvist for his improved implementation of the Levenshtein altorithm, which you can download here: Code Project Article by Sten.
Why do I need the Levenshtein distance? Because the soundex algorithm is an imperfect mapping. For example, “Bated breath” and “bad breath” have the same soundex scores. But, by using Levenshtein, whenever to phrases have the same soundex score, I can pick the phrase which is closest to the original words. In this case, “BadBreth” is closer to “bad breath” than “bated breath”, because the the Levenshtein distance is smaller.
Sten uses Levenshtein distance to judge how much web pages have changed; apparently he crawls a lot of sites and needs to know who has been updated a lot.
Data
I got a list of boy and girl names from government web site a few years ago and now I can’t find the link. It might have been from the census bureau. If anyone wants a copy of my name files, leave a comment with your email and I can send them.
I got the list of phrases from various web sites and built my own list, using regular expressions and screen scraping techniques that I have explained in previous articles. Again, if anyone wants a copy of my list, leave a comment with your email.
My Approach
- Build a dictionary ‘phraseSoundexPairs’ containing:
- For dictionary value, use Phrases . Per puzzle instructions, only use 2-word phrases where each word has length == 4
- For the dictionary entry keys, use the Soundex values of the two words
- For example, the dictionary entry for “bad breath” will receive a key of “B300B630”, where B300 is the soundex of “bad” and B630 is the soundex of “breath”.
- Why did I construct my key this way? Because I can easily determine if any a candidate boy/girl matches (based on soundex) the pronunciation of any phrase in the dictionary. If the candidate soundex does not match any dictionary keys, then I know it is not a solution.
- Find all the boy names with length 4, make a list
- Find all the girl names with length 4, make a list
- Using Linq, join (match) the two lists on their first letters, rejecting any boy names that don’t contain the letter ‘r’
- Now, iterate the result of the join above
- For each match, remove the r from the boy and insert it into the girl’s name (there are 5 possible positions to insert it)
- For example, manipulate “brad” –> “bad”, “beth” –> “rbeth”, “breth”, “berth”, “betrh”, “bethr”
- Compute the soundex for the manipulated boy/girl name and check if it is in the dictionary. If so, we have a possible solution!
-
- Soundex(“bad”) == B300
- Soundex(“breth”) == B630
- Candidate key –> “B300B630”
- My dictionary has a matching key!
- phraseSoundexPairs[“B300B630”] == “bad breath”, telling us we have the solution!
- Since my dictionary actually has several entries with similar soundex values, use the Levenshtein distance to find the best among those similar entries.
- For example, “bated breath” and “bad breath” both have the same soundex scores, but the Levenshtein distance is better for “bad breath”
The Top-Level Code
Take a look at the top-level code that solves the puzzle and writes the answer to a textbox named ‘tbAnswer’:
private void btnSolve_Click(object sender, RoutedEventArgs e) { tbAnswer.Clear(); //Load the boy and girl names into the list, restricting them to length 4: List<string> boyList = LoadNameFile( DATA_FOLDER + BOY_FILE, NAME_LENGTH); List<string> girlList = LoadNameFile( DATA_FOLDER + GIRL_FILE, NAME_LENGTH); //Build a dictionary containing value= phrases, key=soundex scores Dictionary<string, string> phraseSoundexPairs = BuildSoundex_PhraseDictionary(); //Join the two lists on their first letters: var namePairs = from b in boyList join g in girlList on b.Substring(0, 1) equals g.Substring(0, 1) where b.Contains('r') && b.Length == 4 && g.Length == 4 select new { Boy = b, Girl = g }; //Iterate the joined result and construct candidate soundex values: foreach (var aPair in namePairs) { string boyName = aPair.Boy.Replace("r", ""); string boySoundex = PuzzleHelper.Soundex(boyName); //There are 5 ways to construct each girl name, //because there are 5 places to insert the 'r' for (int p = 0; p < 5; p++) { string girlName = aPair.Girl.Insert(p, "r"); string girlSoundex = PuzzleHelper.Soundex(girlName); //Check if we have a dictionary entry composed //of both soundex values if (phraseSoundexPairs.ContainsKey(boySoundex + girlSoundex)) { //Use Levenstein distance to find the best match //if multiple are present: string closestPhrase = ClosestMatchFromCSVEntries( phraseSoundexPairs[boySoundex + girlSoundex], boyName + girlName); tbAnswer.Text += string.Format ("Phrase: {0}/Boy:{1} Girl: {2} (Manipulated boy: {3}, Manipulated girl:{4})\n", closestPhrase, aPair.Boy, aPair.Girl, boyName, girlName); break; } } } }
Finding the Closest Match using Levenshtein Distance
This method takes an entry from the dictionary and splits it on commas. (Because when I have multiple phrases with the same soundex value, I append the new phrase using a comma to separate them.) Using Linq, I make a list of anonymous type containing the distance and the phrase, then choosing the first entry from that list, which has the lowest distance.
private string ClosestMatchFromCSVEntries(string csvPhrases, string combinedName) { var distList = from p in csvPhrases.Split(',') select new { dist = PuzzleHelper.ImprovedLevenshteinDistance(combinedName, p), Phrase = p }; return (from p in distList orderby p.dist select p ).First().Phrase; }
Notes: Loading the Name Lists and Building the Dictionary
I don’t bother to explain function “LoadNameFile”; you should find it pretty easy to load the boy/girl names from file and construct a list; I only used simple C# IO methods. For examples, please refer to some of my previous puzzle solutions. Constructing the dictionary is a little bit tricker, only because of how I handle duplicate keys. It is easier just to display the code:
private static Dictionary<string, string> BuildSoundex_PhraseDictionary() { Dictionary<string, string> result = new Dictionary<string,string>(); using (StreamReader sr = File.OpenText(DATA_FOLDER + PHRASE_FILE)) { while (sr.Peek() != -1) { string aLine = sr.ReadLine().ToLower().Trim(); //Look for phrases containing exactly 1 space, //ie. 2-word phrases if (aLine.Where(c => c == ' ').Count() == 1) { int p = aLine.IndexOf(' '); string word1 = aLine.Substring(0, p); string word2 = aLine.Substring(p + 1); if (PuzzleHelper.AllLettersAreAlpha(word1 + word2)) { string soundex = PuzzleHelper.Soundex(word1) + PuzzleHelper.Soundex(word2); //The only tricky part: if two phrases have the same //soundex, then append the second phrase after a comma: if (result.ContainsKey(soundex)) { result[soundex] += ", " + aLine; } else { result.Add(soundex, aLine); } } } } } return result; }
Alternative Solution
I am not totally happy with this approach, due to difficulties with pronouncing English words, as discussed above. I considered using my list of common words mapped to their phonemes, which I downloaded from Carnegie Mellon University. (A phoneme is a way to pronounce a single syllable; the list of Phonemes for English consists of every possible sound we use in our common language.) That approach would have had the advantage of mapping words onto a value one-to-one. That would eliminate any duplicates from my list. Unfortunately, that list does not have any mappings for imaginary words like ‘breth’, so I can’t use it.
Summary
I hope you enjoyed this solution; Soundex has a number of uses, as does the Levenshtein distance. If you can use either of these in your code, leave a comment to that effect! I also hope you enjoyed my simple example of using Linq to join the list of boys and girls, I found that particular piece of code rather pleasing.
Solved the NPR Sunday Puzzle from June 1, 2014
The Challenge
This challenge comes from listener Dan Pitt of Palo Alto, Calif. Take the name of a well-known American businessman — first and last names. Put the last name first. Insert an M between the two names. The result names a food item. What is it?
Challenge URL: http://www.npr.org/2014/06/01/317697851/getting-to-know-the-in-crowd
Facing the Challenge
Writing the code to solving this one was simple; the real challenge was getting the right data to solve! For some reason, businessmen don’t get much attention on Wikipedia or other sites. Wikipedia has lists of chefs, dentists, copyrighters, etc. but no list of businessmen! I was able to find a list of Entrepreneurs and CEOs, but those are not quite the same thing. Fortunately, the two lists were sufficient to solve the puzzle.
Likewise, finding the foods was not too easy, mainly because I found a huge list at USDA, but said list turned-out to be a list of processed foods that did not include the answer. Fortunately, I was able to solve using the next place I looked for foods, namely, a list of fruits from Wikipedia. I guess USDA doesn’t need to list fruits on their web site.
Nonetheless, it was quite satisfying to solve the puzzle!
Code to Solve the Puzzle, Given that the Data is Available
Let’s jump right into the guts of the issue, solving the puzzle. First, here is the code to test if a given name is a solution.
//This method will return true if fullName can be manipulated to match a key
//in the food dictionary, and will set the output variable 'answer'
public bool NameIsASolution(string fullName,
Dictionary<string, string> foodDic, out string answer) {
answer = string.Empty;
//Only look at names that have at least one space, i.e. a first and last name
if (fullName.Contains(' ')) {
//Get the first and last name, using the
//positions of the first and last space in the name
string firstName = fullName.Substring(0, fullName.IndexOf(' '));
int p = fullName.LastIndexOf(' ');
string lastName = fullName.Substring(p + 1, fullName.Length - p - 1);
//Reverse the last and first names, while
//inserting the letter 'm'
string candidate = lastName + 'm' + firstName;
//The dictionary uses hashing technology to efficiently
//tell us if the candidate is a key in it:
if (foodDic.ContainsKey(candidate)) {
answer = foodDic[candidate];
}
return foodDic.ContainsKey(candidate);
} else {
return false;
}
}
Note:
- We identify the first name (if present) using IndexOf(‘ ‘)
- In other words, find the first space in the name
- We identify the last name using LastIndexof(‘ ‘)
- Which won’t work if a name ends with “Jr.”, but we don’t care for this puzzle
- The dictionary has already been constructed to strip-out spaces and punctionation in the key
- But each value contains the original, unmanipulated fruit name
The code above gets called for every entry in two files:
- BUSINESSMAN_FILE
- CEO_FILE
Why two files? Because I elected to save the businessmen in a different format, including their country and area of specialization. So processing each line in the file requires slightly more work, to extract just the name from each line. In contrast, the food and CEO files list a simple name on each line.
Here is the code that fires when user clicks the button to solve the puzzle:
private void btnSolve_Click(object sender, RoutedEventArgs e) { //The original food name is the value, the key is the food in lower case with no spaces or punctuation Dictionary<string, string> foodDic = new Dictionary<string, string>(); using (StreamReader sr = File.OpenText(FOOD_FILE)) { while (sr.Peek() != -1) { string aLine = sr.ReadLine(); //Use Linq to get only letters (the Where-clause) and then reassemble //them from char to string (the Aggregate clause) string aKey = aLine .ToLower() .Where(c => c >= 'a' && c <= 'z') .Aggregate("", (p, c) => p + c); if (!foodDic.ContainsKey(aKey)) { foodDic.Add(aKey, aLine); } } } //Process each line in the businessman file string answer = string.Empty; using (StreamReader sr = File.OpenText(BUSINESSMAN_FILE)) { while (sr.Peek() != -1) { string aLine = sr.ReadLine(); //The full name is all the text preceding the first tab: string fullName = aLine.Substring(0, aLine.IndexOf('\t')); if (NameIsASolution(fullName.ToLower(), foodDic, out answer)) { txtAnswer.Text += answer + "<-->" + fullName + "\n"; stbMessage.Text = "Solved!"; } } } //Now process each line in the CEO file: using (StreamReader sr = File.OpenText(CEO_FILE)) { while (sr.Peek() != -1) { string aLine = sr.ReadLine(); if (NameIsASolution(aLine.ToLower(), foodDic, out answer)) { txtAnswer.Text += answer + "<-->" + aLine + "\n"; stbMessage.Text = "Solved!"; } } } }
As you can see, the code above
- Opens the food file and builds a dictionary with the contents
- Opens the businessmen file, finds the name on each line, and tests it to see if it is a solution
- Does the same thing for the CEO file
Getting the Data
If you have read my blog before, you know that I generally adopt the same approach:
- Go to a URL with a .NET WebClient
- Download the html via the OpenRead method
- Identify the data using a regular expression
- Save the regular expression matches to a file
You can read one of my other blog entries to get the code for that technique; today I will just list the regular expression that I used for each URL.
URL | Regex |
---|---|
http://en.wikipedia.org/ wiki/List_of_chief_executive_officers | <li><a #Literal [^>]+ #Anything but > > #Literal ([^<]+) #Capture group, anything but < </a> #Literal ( #Start capture group \s* #Whitespace \( #Escaped parenthesis [^)]+ #Anything but ) \), #Escaped parenthesis, comma )? #End capture group, optional ([^<]+)? #Start capture group, anything but <, optional </li> #Literal |
http://en.wikipedia.org/ wiki/ List_of_fruits |
<span #Literal \s+ #Whitespace id=” #Literal ([^”]+) #Capture group, anything but quote ” #Literal |
http://en.wikipedia.org/wiki/List_of_chief_executive_officers | <tr> #Literal \r?\n? #Optional carriage return linefeed <td><a #Literal [^>]+ #Anything but > > #Literal .*? #Anything, lazy match </a></td> #Literal match \r?\n? #Optional carriage return linefeed <td><a #Literal [^>]+ #Anything but > > #Literal ([^<]+) #Capture Group, anything but < </a></td> #Literal match |
http://ndb.nal.usda.gov/ndb/foods | <a #Literal \s+ #Whitespace href=”/ndb/foods/show #Literal [^>]+ #Anything but > > #Literal ([^<]+) #Capture group, anything but < </a> #Literal \r?\n? #Optional carriage return linefeed \s+ #Whitespace </td> #Literal |
Summary
Use regular expressions to capture the data from web sites. Use string manipulation to reverse the businessman name and inject the letter ‘M’. Build a dictionary of foods using the food name as the key. For each manipulated businessman name, test if the dictionary contains that value as a key; if so, we have a winner!
Puzzle Victory – Word Synonym Pair/Remove Double-S
The challenge:
What word, containing two consecutive S’s, becomes its own synonym if you drop those S’s?
Link to the challenge: http://www.npr.org/2014/01/26/266210037/take-synonyms-for-a-spin-or-pirouette
Fun With Linq!
Since I had a nice synonym list already, I was able to dive into this one and come up with an elegant solution using a couple lines of Linq (Language INtegrated Query). “Linq” is Microsoft’s built-in query language that allows you process all kinds of lists and a lot more.
Sorry, faithful readers, I don’t remember exactly where I got my synonym list, but you can get a similar list here, even it is formatted differently than mine.
Algorithm
The following code snippet analyzes a single line from my file and puts the result into the variable ‘answer’ if a solution is found. It does almost all the work, yet takes only six lines. A detailed explanation follows, but if you examine it closely, you should be able to figure-out most of it:
var answer = from word1 in aLine.Split('|') let wp = new { Original = word1, WordNoSS = word1.Replace("ss", "") } join word2 in aLine.Split('|') on wp.WordNoSS equals word2 where word1.Contains("ss") && !distinctAnswers.Contains(word1) select wp;
Detailed Explanation
- Open the synonym file fore reading, using a stream reader named ‘sr’
- Each line contains synonyms, separated by a pipe character, like this:
- bloom|blossom|flower|flourish|coloration
- Within a Linq query, split every line from the file on the | character,
- Into an array,
- Name each array entry ‘word1’
- Use the Linq ‘where’ operator to select only entries in the array containing ‘ss’
- Join the first Linq query against another query
- Which we build by again splitting the line, referring to its entries as ‘word2’
- If the join operation has any match whatsoever,
- Then we have a potential solution
- But reject any answer we have already discovered, by comparing current against previous entries from a list called ‘distinctAnswers’
- If our potential answer is not in the list, display the answer, and add it to the distinctAnswers list
Here’s the entire code listing
using System; using System.Collections.Generic; //For my list (of word pairs) using System.Windows; using System.IO; //To open synonyms file using System.Windows.Input; //To display hourglass while waiting using System.Linq; namespace Jan26_2014 { /// <summary>Code to solve the puzzleThe buttonEvent args(); /// <summary>Code to solve the puzzle</summary> /// <remarks> /// Solution loads a list of synonyms from a file /// Each line has several synonyms on it, separated by | /// Use split the line into an array of words and use linq on it. /// Linq provides an elegant solution that is also pretty efficient. /// </remarks> public partial class MainWindow : Window { private const string SYNONYM_FILE = @"C:\Users\Randy\Documents\Puzzles\Data\Synonyms.txt"; /// <summary>Default constructor</summary> public MainWindow() { InitializeComponent(); } /// <summary> /// Fires when user clicks the Solve button /// </summary> /// <param name="sender">The button</param> /// <param name="e">Event args</param> private void btnSolve_Click(object sender, RoutedEventArgs e) { Mouse.OverrideCursor = Cursors.Wait; txtAnswer.Clear(); //Open the file for reading using (StreamReader sr = File.OpenText(SYNONYM_FILE)) { //Each line in the file contains words separted by | //There is an issue with the file (for our purposes) //For example, both these lines are in the file: // - bloom|blossom|flower|flourish|coloration // - blossom|bloom|flower|flourish //Use the list of distinct answers to deal with that issue and //avoid reporting the same answer twice: List<string> distinctAnswers = new List<string>(); //Keep reading while there is more to be read while (sr.Peek() != -1) { string aLine = sr.ReadLine(); //Use Linq to join words lacking 'ss' against the same words in the the line var answer = from word1 in aLine.Split('|') //Hold word1 in anonymous class/1)original value 2)word lacking 'ss' let wp = new { Original = word1, WordNoSS = word1.Replace("ss", "") } //Word2 values also come from the words in the line join word2 in aLine.Split('|') on wp.WordNoSS equals word2 //Only use word1 if it has ss and we haven't found it already where word1.Contains("ss") && !distinctAnswers.Contains(word1) select wp; //'answer' is an IEnumerable (a kind of list) containing entries //of an anonymous type; each entry has properties 'Original' and 'WordNoSS' //If the list has any entry, each will be a valid answer pair foreach (var wp in answer) { txtAnswer.Text += wp.Original + " - " + wp.WordNoSS + "\n"; distinctAnswers.Add(wp.Original); } } } MessageBox.Show("Done Solving", "Mission Accomplished", MessageBoxButton.OK, MessageBoxImage.Information); Mouse.OverrideCursor = null; } } }
Now, here’s the XAML, you should be able to run it except for the background, which you can either comment-out, or else download yourself from here.
A couple comments:
- Since the solution was related to flowers, I decided to use a field of flowers for the background
- And make the answer textbox partly transparent using an opacity setting of .5
- The title bar uses a drop shadow effect for a little pizzaz
- Like previous puzzle solutions, I elected to draw my own icon instead of linking to a file
<Window x:Class="Jan26_2014.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Synonym Pair with Double-S" Height="350" Width="710" WindowStartupLocation="CenterScreen" > <Grid> <!-- The Title with a drop shadow effect --> <TextBlock FontSize="36" Grid.ColumnSpan="3" Text="Two Synonyms Spelled Same Except for SS" Foreground="DarkRed" HorizontalAlignment="Center"> <TextBlock.Effect> <DropShadowEffect BlurRadius="5" Color="LightGray" Opacity=".8" /> </TextBlock.Effect> </TextBlock> <!-- The Challenge textblock --> <TextBlock Grid.Row="1" Grid.ColumnSpan="5" TextWrapping="Wrap" FontSize="16" Margin="3"> <Bold>Challenge:</Bold> What word, containing two consecutive S's, becomes its own synonym if you drop those S's? </TextBlock> <!-- The 3rd row has a label/textbox for the answer, plus the Solve button--> <Label Grid.Row="2" Content="Answer(s):" Target="{Binding ElementName=txtAnswer}" VerticalAlignment="Top" FontSize="16" FontWeight="Bold" /> <TextBox Grid.Row="2" Grid.Column="1" Name="txtAnswer" TextWrapping="Wrap" AcceptsReturn="True" Opacity=".5" FontWeight="Black" FontSize="16" /> <Button Grid.Row="2" Grid.Column="2" Name="btnSolve" Content="_Solve" Height="30" FontSize="16" FontWeight="Black" VerticalAlignment="Top" Margin="3" Click="btnSolve_Click" /> <!-- Comment-out the next 3 lines if you don't have a background file with that name --> <Grid.Background> <ImageBrush ImageSource="/Jan26_2014;component/Images/Flowers.jpg" Opacity=".6" /> </Grid.Background> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto" /> <ColumnDefinition /> <ColumnDefinition Width="auto" /> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="auto" /> <RowDefinition Height="auto" /> <RowDefinition /> </Grid.RowDefinitions> </Grid> <!-- I elected to use a icon drawn in XAML, a flower was too hard --> <Window.Icon> <DrawingImage> <DrawingImage.Drawing> <GeometryDrawing Brush="Gold"> <GeometryDrawing.Pen> <Pen Brush="Yellow" Thickness="1" /> </GeometryDrawing.Pen> <GeometryDrawing.Geometry> <EllipseGeometry RadiusX="25" RadiusY="25" /> </GeometryDrawing.Geometry> </GeometryDrawing> </DrawingImage.Drawing> </DrawingImage> </Window.Icon> </Window>
That’s all, good luck!
NPR Dancing Puzzle, December 1st, 2013
Challenge: Name a dance. Change one of the letters to a U. The resulting letters can be rearranged to name an event at which this dance is done. What is it?
Although similar to the last several NPR puzzles, this one was a bit tough! Not for coding reasons, but because I couldn’t find a list of social events. I had to go to online thesauri repeatedly until I found the correct event name.
The heart of the solution is to
- Build a dictionary of events, again using the sorted event name as the key and the original event name as the value
- Iterate the dance list
- For each dance name, form a candidate by
- Iterating the letters
- Replacing each letter with u
- Sorting the resulting string to form a candidate
- And checking whether that candidate is a key in our event name dictionary
If it seems like I am going a bit fast, please take a look a this previous post, where I used the same anagram dictionary technique to find other anagrams .
I created two extension methods, ‘Left‘ and ‘Right‘, to make my sample a bit easier to read:
- MyString.Left(i) will return all the characters left of position i
- MyString.Right(i) will return all the character to the right of position i
For example,
- “hula”.Left(1) returns “h”.
- “hula”.Right(1) returns “la”
I’ll show the (very simple) code for these two extension methods shortly, in the mean time, here’s the heart of the solution. This code snippet assumes
- That you have a file containing dance names named “DANCE_FILE”
- Which you have cleaned-up to exclude non-alpha characters like blanks and hyphens
- And that you have an event name dictionary “eventDic” similar to what I described above
//Open the dance file and process each dance name (one per line)
using (StreamReader sr = File.OpenText(DANCE_FILE)) {
while (sr.Peek() != -1) {
string aDance = sr.ReadLine().Trim().ToLower();
//Iterate the dance name chracters, injecting u at every
//position, then sorting the result, to form a candidate:
for (int i = 0; i < danceName.Length -1; i++) {
//Combine the left side + u + right side and sort the result:
string candidate = (danceName.Left(i) + "u" + danceName.Right(i))
.Select(c => c)
.OrderBy(c => c)
.Aggregate("", (t, c) => t + c);
//If the candidate's sorted letters match the sorted letters of
//an event name, that means they are anagrams!
if (eventDic.ContainsKey(candidate)) {
txtAnswer.Text += string.Format("Dance name: '{0}'; "
+ "Event name: '{1}'\n",
aDance, eventDic[candidate]);
}
}
}
}
If you’re gagging on the funky syntax (“.Select”, “.Aggregate”, etc.), be advised that I skimmed over this because I explained it in a previous post and didn’t want to bore repeat visitors. These are Lambda Expressions, and I explain their usage in depth in this post, except that there I used Lambda Expressions instead of Linq syntax.
I promised to show my extension class, so here it is:
public static class StringExtensions {
public static string Left(this string str, int rightPos) {
if (rightPos <= 0)
return "";
else
return str.Substring(0, rightPos);
}
public static string Right(this string str, int rightPos) {
if (rightPos < str.Length - 1)
return str.Substring(rightPos + 1);
else
return "";
}
}
There is nothing in here too difficult; the point is that .NET allows you to write your own extension methods which behave as if they were part of the .NET string class. A simple class like this can help make your code more readable. Note the class is static, and also note the use of ‘this string str‘ as a parameter. That’s all it takes to make an extension method!
I used one more interesting bit of coding while getting a list of dance names. If you’ve read my previous puzzle articles, you know that I use regular expressions to get lists from web pages, frequently from Wikipedia. In this case, I got my list of dances from here. If you go to that link, you will see that the main list is preceded by a bunch of other links, which happen to match the same pattern as the dance names! In order to deal with this, I modified my regular expression to exclude any hits whose ‘payload’ starts with ‘List’. To do so, I used the Regex feature ‘Negative Lookahead‘, which was fairly new to me.
Here’s my documented regex to capture dance names from that page (negative lookahead highlighted):
<li><a #Literal match
\s+ #Whitespace, one or more
href=\"/wiki/ #Literal match
[^>]+ #Anything except >, one or more
> #Literal match
( #Start of our group
(?!List) #Exclude matches starting with List
[^<]+ #Anything except <
) #end of group
</a> #Literal match
In case you are totally baffled,
- The regex will take raw HTML as input
- That is, the HTML from Wikipedia
- Which looks like this:
<li><a href="/wiki/Hula" title="Hula">Hula</a></li>
- This regular expression contains a capture group (starts on line 6)
- Basically, it looks for
- “<li><a” (line 1)
- followed by space (line 2)
- followed by href=\”/wiki/ (line 3)
- followed by anything except a ‘>’ (this is a “negated character class“) (line 4)
- Followed by > (line 5)
- The parenthesis on line 6 starts a capture group
- The negative lookahead (line 7) rejects any text starting with ‘article’
- On line 8 we specify the text we will capture, namely: anything except <
- Line 9 we close the capture group with a close-parenthesis
- And specify that it must be followed by </a> (line 10)
I have listed the entire code below (138 lines, it took way longer for me to explain it here!)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Text.RegularExpressions;
using System.Net; //For WebClient
using System.IO;
namespace Dec_1_2013 {
/// <summary>
/// Solves the puzzle by responding to each button click form the main page
/// </summary>
/// <remarks>
/// Has code to fetch dance lists from two web pages and
/// then use those dance lists to /// solve the puzzle
/// </remarks>
public partial class MainWindow : Window {
private const string DANCE_FILE = "DanceList.txt";
public MainWindow() {
InitializeComponent();
}
/// <summary>
/// Fires when user clicks the button to get the dance list from the web
/// </summary>
/// <param name="sender">The button</param>
/// <param name="e">Event args</param>
/// <remarks>
/// Goes the the URL in the textbox, gets the page content, extracts matches
/// with the regex, saves to file
/// </remarks>
private void btnGetDances_Click(object sender, RoutedEventArgs e) {
string pattern = @"
<li><a #Literal match
\s+ #Whitespace, one or more
href=\""/wiki/ #Literal match
[^>]+ #Anything except >, one or more
> #Literal match
( #Start of our group
(?!List) #Exclude matches starting with List
[^<]+ #Anything except <
) #end of group
</a> #Literal match
";
Regex reDance = new Regex(pattern
, RegexOptions.IgnorePatternWhitespace | RegexOptions.Multiline);
//Go to the web page, get the text, extract
//all the matches using the regex, save to file
using (WebClient wc = new WebClient()) {
using (StreamReader sr = new StreamReader(wc.OpenRead(txtDanceURL.Text))) {
string allText = sr.ReadToEnd();
//There exist a number of false matches after the last dance on
//the page (marked by the text 'see also'), find the position now
int stopPos = allText.IndexOf("See also");
using (StreamWriter sw = File.CreateText(DANCE_FILE)) {
Match m = reDance.Match(allText);
//keep looping so long as we have more matches and have
//not exceeded the last dance position
while (m.Success && m.Index < stopPos) {
sw.WriteLine(m.Groups[1].Value);
m = m.NextMatch();
}
}
}
}
MessageBox.Show("Done", "Dances Captured");
}
/// <summary>
/// Fired when user clicks the button to solve the puzzle
/// </summary>
/// <param name="sender">The button</param>
/// <param name="e">Event args</param>
/// <remarks>
/// Load the dance list from file into a dictionary
/// - Each dictionary key is the sorted dance name
/// - Each dictionary value is the UNsorted dance name, the original value
///
/// Loop through every social event in the text box
/// - Try replacing each letter in the event name with 'u'
/// - Sort the result and check for a match in the dictionary
/// - If we have a hit, display in the textbox
/// </remarks>
private void btnSolve_Click(object sender, RoutedEventArgs e) {
txtAnswer.Clear();
//Snag the events from the textbox, put them into an array after splitting on comma char
string[] events = txtEventNames.Text.ToLower().Split(',');
Dictionary<string, string> eventDic = new Dictionary<string, string>();
foreach (string evt in events) {
//Remove non-alpha letters, sort, recombine into a new string
string sortedEvent = evt.Where(ev => ev>= 'a' && ev <= 'z')
.OrderBy(ev => ev)
.Aggregate("", (t, c) => t + c);
//store in the dictionary
if (eventDic.ContainsKey(sortedEvent))
eventDic[sortedEvent] += ", " + evt;
else
eventDic.Add(sortedEvent, evt.Trim());
}
//Now open the dance file and process each dance name (one per line)
using (StreamReader sr = File.OpenText(DANCE_FILE)) {
while (sr.Peek() != -1) {
string aDance = sr.ReadLine().Trim().ToLower();
//Remove non-alpha characters
string danceName = (aDance.Where(d => d >= 'a' && d <= 'z'))
.Aggregate("", (t, c) => t + c);
//Iterate the dance name chracters, injecting u at every
//position, to form a candidate
for (int i = 0; i < danceName.Length -1; i++) {
//Combine the left side + u + right side and sort the result:
string candidate = (danceName.Left(i) + "u" + danceName.Right(i))
.Select(c => c)
.OrderBy(c => c)
.Aggregate("", (t, c) => t + c);
//If the candidate's sorted letters match the sorted letters of
//an event name, that means they are anagrams!
if (eventDic.ContainsKey(candidate)) {
txtAnswer.Text += string.Format("Dance name: '{0}'; "
+ "Event name: '{1}'\n",
aDance, eventDic[candidate]);
}
}
}
}
MessageBox.Show("Done", "Matching Complete", MessageBoxButton.OK,
MessageBoxImage.Exclamation);
}
}
}
And here is the XAML:
<Window x:Class="Dec_1_2013.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStartupLocation="CenterScreen"
Title="Dec 1st, 2013" Height="460" Width="555">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition/>
<ColumnDefinition Width="auto" />
</Grid.ColumnDefinitions>
<Border Grid.ColumnSpan="3">
<Border.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="DarkRed" Offset="0" />
<GradientStop Color="Crimson" Offset="1" />
</LinearGradientBrush>
</Border.Background>
<TextBlock HorizontalAlignment="Center" FontSize="36"
Foreground="AntiqueWhite"
Text="Dance Name to Event Name">
<TextBlock.Effect>
<DropShadowEffect BlurRadius="4" Color="LightSalmon"
Opacity="0.5" />
</TextBlock.Effect>
</TextBlock>
</Border>
<TextBlock Grid.Row="1" Grid.ColumnSpan="3" TextWrapping="Wrap"
FontSize="14">
<Bold>Next week's challenge: </Bold>
Name a dance. Change one of the letters to a U. The
resulting letters can be rearranged to name an event at
which this dance is done. What is it?
</TextBlock>
<Label Grid.Row="2" Content="Dance _URL:"
Target="{Binding ElementName=txtDanceURL}" />
<TextBox Grid.Row="2" Grid.Column="1" x:Name="txtDanceURL"
Text="http://en.wikipedia.org/wiki/List_of_dances" />
<Button Grid.Row="2" Grid.Column="2" x:Name="btnGetDances"
Content="Get _Dance List" Height="30"
HorizontalAlignment="Right" Margin="3" Click="btnGetDances_Click" />
<Label Grid.Row="3" Content="_Events:"
Target="{Binding ElementName=txtEventNames}" />
<TextBox Grid.Row="3" Grid.Column="1" x:Name="txtEventNames"
AcceptsReturn="True" TextWrapping="WrapWithOverflow"
VerticalAlignment="Stretch">
social function, fundraiser, function, debutante ball,
photo opportunity, jubilation, funeral, groundbreaking,
graduation, sacramental union, communion, masquerade, ritual,
inauguration, presidential inauguration, papal inauguration,
audience, changing of the guard, courtesy call,
investiture, yule, house party, circumstance, nuptials,
induction, Maundy, bunfight, musical, burlesque, puppet play,
tournament, occurrence, masquerade ball, masque, masquerade party,
nauch, nautch, nautch dance, duet, pas de deux, pas de quatre,
ritual dance, ritual dancing, nude dancing, beaux arts ball,
buffet, housewarming party, housewarming, costume party,
hurricane party, June Event, Open house, houseparty,
Symposium, Musical Occasion, Cultural festivals, Burning Man,
Hula Festival, Saturnalia, Fat Tuesday, blowout, Ludi Saeculares,
secular games, perambulation, church festival, luau, slumber party,
reunion, banquet, musical soiree, soiree musicale
</TextBox>
<Button Grid.Row="4" Grid.Column="2" Content="_Solve" Height="30"
Margin="3" HorizontalAlignment="Right"
VerticalAlignment="Top" Click="btnSolve_Click" x:Name="btnSolve"
Width="{Binding ElementName=btnGetDances, Path=ActualWidth}" />
<Label Grid.Row="4" Content="_Answer:"
Target="{Binding ElementName=txtAnswer}" />
<TextBox Grid.Row="4" Grid.Column="1" x:Name="txtAnswer" />
</Grid>
</Window>
NPR Puzzle Challenge – Solved in Code
I enjoy listening to the NPR Sunday Puzzle (shouting-out the answers as I listen!), and it is even more fun to solve in code. Here’s my solution to the latest challenge, written in C# as a WPF application. You may enjoy it if you like code, want to learn new coding techniques, or just like puzzles
Laying-out the Puzzle Challenge
Next week’s challenge: This week’s challenge comes from listener Steve Baggish of Arlington, Mass. Think of a word meaning “quarrel” in which several of the letters appear more than once. Remove exactly two occurrences of every repeated letter, and the remaining letters can be rearranged to spell a new word meaning “quarrel.” What are the two words?
General Approach
This puzzle is basically an anagram combined with string manipulation. The input is pretty easy, you can get synonyms for ‘quarrel’ from Google and a couple other places.
Checking for Anagrams in Code
There is an easy way to solve anagrams and a hard way. The hard way is probably the first way that comes to mind for most of us, namely
- Start with word 1, generate all possible combinations of the letters in the word
- Do the same thing for word 2
- Compare every combination from step 1 against step 2
Not only is that hard, but it is computationally expensive (generating word combinations is an O(n-squared) operation, not that we care for such a short-list).
The easy way is:
- Sort the letters in word 1
- Sort the letters in word 2
- Compare the result, if they are the same, word1 is an anagram of word2!
Solving Anagrams for Groups of Words
For this puzzle we need to determine if a any two of a number of words are anagrams. We can make it easy by modifying the approach above, instead of sorting each word just before comparing it to another word,
- Sort each word once
- Store the result in a dictionary
- Using the sorted letters as the entry’s key
- And the original word as the entry’s value
Then, if I have an anagram candidate, I can look-it up in the dictionary, if the key is present, I know I have two matched-anagrams and can inspect the value to see what the other word is.
Sample Code Illustrates An Anagram Dictionary
//Declare a dictionary object that uses strings for keys and strings for values: Dictionary<string, string> anagramDic = new Dictionary<string, string>(); //add an entry into the dictionary using the sorted letters (act) as the key and the original word as the value anagramDic.Add("act", "cat"); //we have another word that might be an anagram, 'tac' //we can check whether the word exists by sorting its letters to get 'act' and then checking whether it //already exists in the dictionary if (anagramDic.ContainsKey("act")) { //We can fetch the value using the key as an indexer string anagramPair = anagramDic["act"]; Console.WriteLine(anagramPair + " is an anagram of tac"); }
Side Note About Dictionaries
I love 🙂 dictionaries because they use hashing technology. That makes them ultra-efficient, and as you can see above, they are also quite easy to use.
One More Coding Technique
The code below uses a bit of Linq usage that is explained in the comments, but I will discuss it here for emphasis. The lines of code take a string variable named ‘aWord’ and sort the letters, storing the result into a new string called ‘sorted’:
string sorted = (from l in aWord where l >= 'a' && l <= 'z' orderby l select l ).Aggregate("", (r, b) => r + b);
- ‘aWord’ is a string such as ‘argument’, we select chars from it when we use the syntax “from l in aWord’
- ‘l’ is an anonymous variable that has type char (.NET knows this so we do not need to explicitly declare it)
- ‘orderby l’ has the effect of sorting the letters in aWord
- Into an IEnumerable<char>, basically as list of chars
- Since we don’t want a list, we use the ‘Aggregate‘ operator to concatenate the results into a string (because the + operator creates strings from chars)
- The aggregate method used above takes two inputs:
- the seed (in my casen an empty string “”)
- And an anonymous function to perform the aggregation, namely “(r, b) => r + b)”
- The anonymous function takes two anonymous inputs, the current aggregated value (represented by r)
- and the current value being included into the aggregate (represented by b)
- The operation to be performed is r + b, in other words, take the current aggregate value r and append the next value, b
- The aggregate method used above takes two inputs:
Code to Solve the Puzzle
I solved the puzzle in about 50 lines of code, then I added another 55 lines of documentation to it just to explain what is going on.
The full code is listed below, it uses a simple form with
- A TextBox ‘txtSynonyms’ containing synonyms to the word ‘quarrel’
- A button labeled ‘Solve’ whose click-event does all the work
- Another Textbox ‘txtAnswer’ to display the result it
using System; using System.Collections.Generic; using System.Linq; using System.Windows; using System.Windows.Controls; namespace Nov17_2013 { /// <summary>Solves the puzzle using the synonyms in the textbox, /// displaying the answer in the second textbox</summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } /// <summary> /// Fires when user clicks the button to solve the puzzle /// </summary> /// <param name="sender">The button</param> /// <param name="e">Event args</param> /// <remarks> /// This solution relies on a simple technique to determine if two words are anagrams: /// - Sort the letters of both words /// - If their results are the same, they obviously are anagrams /// /// Actually, I take it a bit further by sorting all the candidate words and storing them /// in a dictionary. The dictionary allows us to store a 'payload' (value) for each key /// we create. I use the sorted letters as the key and the original word as /// the value, like this: /// /// Key Value /// --- ----- /// aegmnrtu argument /// adeeegimnrst disagreement /// fghit fight /// achls clash /// defu feud /// addegiimnnnrsstu misunderstanding /// /// After constructing the dictionary I identify repeated letters in all the candidates, /// remove two of each, and check to see if the resulting word exists in the dictionary. /// If it does, the dictionary value is the anagram we seek! /// </remarks> private void btnSolve_Click(object sender, RoutedEventArgs e) { txtAnswer.Clear(); Dictionary<string, string> sortedLettersDic = new Dictionary<string, string>(); //The textbox contains words separated by commas, when I split them //I get an array with one entry per word string[] synonyms = txtSynonyms.Text.Split(','); //Process each synonym and put it into the dictionary. foreach (string aWord in synonyms) { //The "where-clause" cleans-up the candidate, removing blanks and hyphens //"orderby" sorts the letters //"Aggregate" concatenates the sorted chars into a string string sorted = (from l in aWord where l >= 'a' && l <= 'z' orderby l select l ).Aggregate("", (r, b) => r + b); if (!sortedLettersDic.ContainsKey(sorted)) sortedLettersDic.Add(sorted, aWord); } //Now examine every dictionary entry, first building a list of repeated letters, //then using it to construct a potential anagram by removing two //of each repeated letter from the original foreach (KeyValuePair<string, string> kvp in sortedLettersDic) { List<char> repeatedLetters = new List<char>(); for (int ndx = 1; ndx < kvp.Key.Length; ndx++) { //examine the letter immediately prior to current letter if (kvp.Key[ndx-1] == kvp.Key[ndx] && !repeatedLetters.Contains(kvp.Key[ndx])) { repeatedLetters.Add(kvp.Key[ndx]); } } if (repeatedLetters.Count > 0) { //It is easier to remove letters from a List than a string, //to put the letters into one: //Also it is more efficient too, since strings are imutable in .NET List<char> ltrs = new List<char>(kvp.Key); foreach (char c in repeatedLetters) { //remove exactly two of each repeated letter ltrs.Remove(c); ltrs.Remove(c); } //construct a new string by aggregating the list; use empth string for seed, //and the inputs to the lambda expression are the running total (r) //and the current value (c): string candidate = ltrs.Aggregate("", (r, c) => r + c); //for example, if our candidate originally was 'misunderstanding', we now //will have manipulated it as follows: //step 1, sort: addegiimnnnrsstu //step2, remove 2 repeated letters: aegmnrtu //next step - is 'aegmnrtu' a key in the dictionary? //Finally! If our candidate is in the dictionary, it means we have //constructed anagram of on of the other synonyms and solved the puzzle if (sortedLettersDic.ContainsKey(candidate)) { txtAnswer.Text += sortedLettersDic[candidate] + " - " + kvp.Value + " "; } } } } } }
In case you’re curious, I have also saved the XAML markup below so you can run the app. You don’t need any new references; I built it with VS 2010 but it should work in VS 2008, 2012 and 2013.
<Window x:Class="Nov17_2013.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" WindowStartupLocation="CenterScreen" FocusManager.FocusedElement="{Binding ElementName=btnSolve}" Title="Puzzle Nov 17, 2013" Height="430" Width="525"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="auto" /> <RowDefinition Height="auto" /> <RowDefinition Height="auto" /> <RowDefinition /> <RowDefinition Height="auto" /> </Grid.RowDefinitions> <Grid.ColumnDefinitions> <ColumnDefinition Width="auto" /> <ColumnDefinition /> <ColumnDefinition Width="auto" /> </Grid.ColumnDefinitions> <Border Grid.ColumnSpan="3" > <Border.Background> <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0"> <GradientStop Color="DarkRed" Offset="0" /> <GradientStop Color="Crimson" Offset="1" /> </LinearGradientBrush> </Border.Background> <TextBlock HorizontalAlignment="Center" FontSize="36" Text="Quarrel Manipulation Puzzle" Foreground="Ivory"> <TextBlock.Effect> <DropShadowEffect Color="MistyRose" BlurRadius="5" Opacity="0.35" /> </TextBlock.Effect> </TextBlock> </Border> <!-- Row 1 --> <TextBlock Grid.Row="1" Grid.ColumnSpan="3" FontSize="14" TextWrapping="Wrap" Margin="3" > This week's challenge comes from listener Steve Baggish of Arlington, Mass. Think of a word meaning "quarrel" in which several of the letters appear more than once. Remove exactly two occurrences of every repeated letter, and the remaining letters can be rearranged to spell a new word meaning "quarrel." <LineBreak /> <LineBreak /> What are the two words? </TextBlock> <!-- Row 2 --> <Label Grid.Row="2" Content="Puzzle _URL:" VerticalAlignment="Center" FontWeight="SemiBold" Target="{Binding ElementName=tblPuzleUrl}" /> <TextBlock Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2" x:Name="tblPuzleUrl" VerticalAlignment="Center" Text="http://www.npr.org/2013/11/17/245761408/more-fun-than-a-dead-rose" /> <!-- Row 3 --> <Label Grid.Row="3" Content="Quarrel Synonyms:" FontWeight="SemiBold" Target="{Binding ElementName=txtSynonyms}" /> <TextBox Grid.Row="3" Grid.Column="1" x:Name="txtSynonyms" TextWrapping="Wrap" > argument, disagreement, fight, clash, feud, contretemps, war of words, shouting match, informaltiff, blowup, altercation, bickering, brawl, controversy, difference, difference of opinion, discord, dispute, dissension, disturbance, shitstorm, falling-out, fracas, misunderstanding, row, ruckus, run-in, spat, squabble, strife, struggle, tiff, tumult, vendetta, wrangle, affray, beef, breach, broil, combat, commotion, complaint, contention, difficulty, disapproval, disputation, dissidence, dust, fisticuffs, fray, fuss, hassle, objection, rhubarb, scrap, set-to, battle royal, brannigan </TextBox> <Button Grid.Row="3" Grid.Column="2" Content="_Solve" x:Name="btnSolve" VerticalAlignment="Top" Height="30" Margin="3" Click="btnSolve_Click" /> <!-- Row 4 --> <Label Grid.Row="4" Content="_Answer:" FontWeight="SemiBold" Target="{Binding ElementName=txtAnswer}" /> <TextBox Grid.Row="4" Grid.Column="1" x:Name="txtAnswer" /> </Grid> </Window>
- ← Previous
- 1
- 2