
Windows PowerShell solution to Event 1 in the 2008 Winter Scripting Games.
We don’t know if anybody had any fun writing a script for this event, but the Scripting Guys actually had fun testing the scripts for this event. That’s because there were two basic approaches to tackling this problem. Given a phone number and a corresponding set of letters, some people (including the Scripting Guys) chose to randomly generate a string value, check to see if that was a word, and then – if it wasn’t – randomly generate another string value and try again. Other people were far more methodical; they would generate a string value such as ADGJMPT and check to see if that was a word. If it wasn’t, they would then change a single character (e.g., BDGJMPT) and try again.
So which approach was the best? Well, that’s hard to say. In general, the random approach seemed to be a bit faster; in some cases, it seemed to be considerably faster. However, that probably depends on the telephone number and on the number of words that can be generated from that number. If there’s only one word that could possibly be generated from a set of letters it might take awhile to randomly hit upon the correct string; in fact, random numbers being what they are, you might never hit upon the correct string. (It’s highly unlikely, but it is possible to generate the random number 7 every time you try to generate a random number between 1 and 1 million.) Overall the methodical approach is slower, but sooner or later it will generate the magic string.
Like we said, it was interesting to see the different approaches that people took. And yes, you’ll get a chance to see these approaches, too. We fully intend to publish a selected set of solutions sent in by Scripting Games competitors, solutions that demonstrate different ways to solve the same problem. Those probably won’t be posted until the Games are over; we don’t have a lot of spare time at the moment. But when they do get published we think you’ll find them as interesting as we did.
In the meantime, let’s take a look at the solution the Scripting Guys came up with:
$a = New-Object Random
$dictionary = @{}
$Dictionary.Add("2", "ABC")
$Dictionary.Add("3", "DEF")
$Dictionary.Add("4", "GHI")
$Dictionary.Add("5", "JKL")
$Dictionary.Add("6", "MNO")
$Dictionary.Add("7", "PRS")
$Dictionary.Add("8", "TUV")
$Dictionary.Add("9", "WXY")
$arrWordList = Get-Content C:\Scripts\WordList.txt
$strWordList = [string]::join(" ", $arrWordList)
$phoneNumber = Read-Host "Please enter the phone number"
do
{
$strCharacters = ""
for ($i = 0; $i -lt 7; $i++)
{
$intValue = $phoneNumber.substring($i, 1)
$letters = $dictionary.Get_Item($intValue)
$b = $a.next(0,3)
$strCharacters = $strCharacters + $letters.substring($b, 1)
}
$strCharacters = " " + $strCharacters + " "
$match = $strWordList -match $strCharacters
if ($match -ne $False)
{$a = $strCharacters.trim(); $a; break}
}
while ($x -ne 1000)
To tell you the truth, this script turned out to be a quite a bit shorter than we expected it to be; that’s because it turned out to be a little bit easier to write than we expected it to be. How much easier? Let’s take a look.
To begin with, we use the New-Object cmdlet to create an instance of the System.Random class; this is a .NET Framework class that enables us to generate random numbers. Why do we even need to generate random numbers? Well, we don’t. However, as the event instructions told us, each number in a phone number (except 1 and 0) has three letters associated with it; for example, the number 2 is associated with the letters A, B, and C. What we’re going to do is randomly select one of those letters when we try to create a word using the digits in the phone number. Needless to say, we can’t randomly select a letter unless we have a way to generate random numbers. That’s where System.Random comes in.
After creating an instance of the System.Random class we use this line of code to create an empty hash table (roughly akin to the Dictionary object in VBScript):
$dictionary = @{}
We then use this block of code to load the numbers 2-9 – and the letters associated with them – into the hash table:
$Dictionary.Add("2", "ABC")
$Dictionary.Add("3", "DEF")
$Dictionary.Add("4", "GHI")
$Dictionary.Add("5", "JKL")
$Dictionary.Add("6", "MNO")
$Dictionary.Add("7", "PRS")
$Dictionary.Add("8", "TUV")
$Dictionary.Add("9", "WXY")
What’s the point of all that? Well, as you’ll see in a minute, it’s very easy to look up items in a hash table; if your phone number has a 3 in it, a single line of code enables us to query the hash table and return the three letters associated with the number 3 (D, E, and F).
And yes, that is very useful, especially in this script.
That brings us to these two lines of code:
$arrWordList = Get-Content C:\Scripts\WordList.txt
$strWordList = [string]::join(" ", $arrWordList)
In the first line, we’re using the Get-Content cmdlet to read the contents of the file C:\Scripts\WordList.txt, our official list of acceptable words. By default, these words will be put into an array similar to this one:
abalone abandon abandoned abandonment abase abasement abash
On the one hand, that’s fine; on the other hand, having the words in an array complicates our ability to determine what is – and is not – a word. For example, the following string of seven characters is not a word:
ndonmen
However, if we search for this string value we’ll be told that it is a word; that’s because that string appears in the middle of the word abandonment:
abandonment
Now, we could put together a more-complicated regular expression to ensure that we’re searching for complete words. Alternatively, we could – and did – use the join method to turn out array into a single string value, with each word (each array item) separated by a blank space:
abalone abandon abandoned abandonment abase abasement abash
Now our search is much easier: if a string value isn’t enclosed by blank spaces then it isn’t a complete word.
At this point the fun really begins. For starters, we use the Read-Host cmdlet to prompt the user to enter a phone number, storing that value in a variable named $phoneNumber:
$phoneNumber = Read-Host "Please enter the phone number"
We then set up a do loop designed to run as long as the variable $x is not equal to 1000 (while ($x -ne 1000)). Because we never set $x to anything, let alone 1000, that means this loop will run from now until the end of time.
Or at least until we specifically tell it to end.
Inside the loop, we set the value of a “holder” variable named $strCharacters to an empty string:
$strCharacters = ""
We then set up a for loop that runs from 0 to 6 ($i –lt 7):
for ($i = 0; $i -lt 7; $i++)
Why 0 to 6? Well, inside this loop we’re going to grab the digits in the phone number, one-by-one. In Windows PowerShell, the first character in a string (or number) is in position 0; thus the first number in our phone number is character 0. That means that the last character will be the total number of characters in the string (7) minus 1. In other words, 6.
Inside this interior loop we execute the following block of code:
$intValue = $phoneNumber.substring($i, 1) $letters = $dictionary.Get_Item($intValue) $b = $a.next(0,3) $strCharacters = $strCharacters + $letters.substring($b, 1)
In the first line, we’re using the substring method to retrieve a single character from the phone number. Which character? Well, the first time through the loop $i is equal to 0; hence we’ll be grabbing character 0, the first digit in the phone number. The second time through the loop $i will be equal to 1; that means that we’ll grab character 1, the second digit in the phone number. Once we have the actual digit (for example, a 9), we then use the Get_Item method to query our hash table and retrieve the letters associated with that digit:
$letters = $dictionary.Get_Item($intValue)
If the first digit in our phone number really is 9 then that means that the variable $letters will be equal to this:
WXY
Of course, we don’t want all three of those letters; we want only one. Because of that, we use the System.Random class and the next method to generate a random number between 0 and 2:
$b = $a.next(0,3)
Note. Yes, we know: that is a little confusing, isn’t it? As it turns out, the System.Random class is a bit of an odd duck. The minimum value passed to the next method (0) will always be included the random number range; however, the maximum value (3) will never be included in the range. Instead, the maximum number in the method call must always be one more than the highest number in your random number range. We want to choose values between 0 and 2, corresponding to character positions 0, 1, and 2 in our three-letter string. To choose a number between 0 and 2 we have to specify 0 and 3 in our method call. |
Once we have a random number we use the substring method to grab that character from our three-letter string, then append that character to the variable $strCharacters. We then repeat this process for all the numbers in the phone number. Suppose our phone number was 9737853. When all is said and done, $strCharacters will be a string value featuring one of the letters associated with each digit in the phone number. For example:
9 | 7 | 3 | 7 | 8 | 5 | 3 |
Y | R | F | P | T | J | E |
So how can we determine whether YRFPTJE is an actual word? Well, to begin with, we use this line of code to add a blank space to the beginning and the end of the string:
$strCharacters = " " + $strCharacters + " "
We then use this line of code to search our word list for that value:
$match = $strWordList -match $strCharacters
If $match returns False ($False in PowerShell’s vernacular) then that means the value is not a real word. (Or, at the very least, that it’s not in our official word list.) If $match doesn’t return False that means $strCharacters was found in the word list. In that case, we execute this little block of code:
{$a = $strCharacters.trim(); $a; break}
Here we’re doing three things:
| • | We’re using the trim method to remove the blank spaces from the beginning and end of $strCharacters. |
| • | We’re echoing back the value of $strCharacters (minus the blank spaces). |
| • | We’re using the break command to exit the loop and bring the script to a close. |
The net result? Onscreen we should see a word that can be built using the digits in the phone number 9737853:
WRESTLE
Like we said, quite a bit easier than we expected it to be.