Right up front I should apologize to anyone who’s not a PowerShell user, because it’s just become clear to me that for the rest of the scripting games, I’m going to be posting my solutions to most, if not all, of the puzzles. I thought for sure that I wouldn’t have any reason to post solutions to the advanced scripting games puzzles, because in addition to the Scripting Guys solutions, we were going to have solutions from luminaries of the PowerShell scripting world. After looking at the first two of those solutions, however, one thing is perfectly clear: when under pressure to perform, or when attempting to turn a scripting problem into a scripting lesson … anyone can turn something simple into something really complicated.
So, I’ve decided to offer my solutions to these events. I’m not implying they will always (or ever) be the best possible solutions, but they’re alternatives you can learn from .
Advanced Event 1 was a fun little puzzle: given a spell-checker-style word list, you should prompt the user for their phone number, and then find a word in the dictionary which could be used to represent that number according to the standard telephone dialpad’s letter arrangement. You can find the solution proposed by the scripting guys as well as Richard Siddaway on the scripting games site, but I know that other bloggers have posted their solutions as well — /\/\o\/\/ (the PowerShell Guy)‘s solution looks a bit like Richard Siddaway’s first one actually in that they both create lookup hashtables in an attempt to speed things up.
Incidentally, if I were grading a PowerShell class, and you handed in the Scripting Guy’s solution — you would get a D — it works, most of the time, but apart from being hideously inefficient and showing a lack of knowledge of the language, it’s non-deterministic and may not find the answer at all, even if there is one!
Compared to any of the solutions above, mine is dead simple, and the reason is this: when I think about finding a word that matches a certain pattern … I think regular expressions. It is, by far the fastest of any of these solutions, even with mow and Richard’s lookup tables … and I’ll let you judge it’s readability:
param([string]$number=(Read-Host "Please enter your phone number:"))
## sanitize common phone number formats
## this wasn't necessary according to the rules ...
$number = ($number -replace "[-() \.]","")
## Regular expressions based on phone keypads which include q and z
$numPatterns = @("","","[abc]","[def]","[ghi]","[jkl]","[mno]","[pqrs]","[tuv]","[wxyz]")
## Convert the number into an array of characters ...
## which we convert into numbers
## which we use as an index into the $numPatterns variable
## to make sure we don't get words like "lipreading" for "reading" we also
## wrap the resulting expression in ^ and $
$pattern = "^" + [string]::join("",($number.ToCharArray() | % { $numPatterns[[int][string]$_] })) + "$"
## Finally, call the Select-String cmdlet
## this is THE FASTEST way to, you know, select strings ...
## but it returns an object, and we just want the "Line"
@(select-string $pattern C:\Scripts\WordList.txt)[0].Line
Incidentally, if you switch the last line to: select-string $pattern C:\Scripts\WordList.txt | % {$_.Line } it will output every possible match, instead of just the first one … and it works equally well for 10-digit phone numbers with area codes! In fact, if you take off the + "$" on the end of the pattern line, it will even find words that start with the digits in the phone number, which is usually good enough (in the USA, at least, you can dial extra digits after the phone number without any effect). Plus, that way you can look up what words might work for phone numbers starting with a specific exchange, etc.
That Select-String cmdlet is blazing fast.
To give you an idea … I tested with four phone numbers I generated from words starting with a,y,w, and r … and this script runs in about 1/3 of a second to find all four matching words. On the same box, using get-content and a pipeline involving -match as in Richard Siddaway’s solution takes about 27 and a half seconds, and mow’s lookup table solution takes about 18 seconds to build the table, and 2.43 seconds to do the lookups.
Incidentally, if that [string]::join call in the middle of my script makes you cringe, you could use this, instead:
param([string]$number=(Read-Host "Please enter your phone number without punctuation"))
$number = ($number -replace "[-() \.]","") ## sanitize numbers
$numPatterns = @("","","[abc]","[def]","[ghi]","[jkl]","[mno]","[prs]","[tuv]","[wxy]")
$pattern = "^"
$number.ToCharArray() | % { $pattern += $numPatterns[[int][string]$_] }
select-string $pattern C:\Scripts\WordList.txt | % { $_.Line }
We had pretty similar approaches, although the way i built my regex was pure ugliness. Your’s is much nicer, and definitely more powershelley.
Andy
http://www.get-powershell.com
Hi Joel,
Thanks for the articles – I’m picking up different tips from all over the place:-)
My solution is very similar to yours (http://chrisjwarwick.spaces.live.com) but I didn’t think about using [string]::join() – there’s always something to overlook!
Couple of comments for you: Select-string has a -list switch to make it return a single match; and, as someone pointed out to me, you don’t need the [int] in the [int][string] cast – although I haven’t thought about why that should be yet:-)
Thanks again
Chris