A few weeks ago, the Iron Scripter challenge was to write code to convert a short string into its numeric valuesusing the alphabet as it is laid out on a telephone. A number of solutions have already been shared in the comments on the original post. There are certainly a number of ways to meet the challenge. Here's what I worked out.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
Convert Text to Number
I started with a simple hashtable.
$phone = @{
2 = 'abc'
3 = 'def'
4 = 'ghi'
5 = 'jkl'
6 = 'mno'
7 = 'pqrs'
8 = 'tuv'
9 = 'wxyz'
}
The keys, coincidentally named, correspond to the telephone keys on my phone. To convert 'help' means finding the value and its key.
The Name property shows me the key. I can repeat this process for the other letters, join the keys and get my result: 4357. Here's the function I'm using to make this easier and meet some of the challenge's other requirements.
Function Convert-TextToNumber {
[cmdletbinding()]
[Outputtype("int32")]
Param(
[Parameter(Position = 0, Mandatory, ValueFromPipeline)]
[ValidatePattern("^[A-Za-z]{1,5}$")]
[string]$Plaintext,
[Parameter(HelpMessage = "A custom dictionary file which is updated everytime a word is converted")]
[string]$Dictionary = "mywords.txt"
)
Begin {
Write-Verbose "[BEGIN ] Starting: $($MyInvocation.Mycommand)"
#define a simple hashtable
$phone = @{
2 = 'abc'
3 = 'def'
4 = 'ghi'
5 = 'jkl'
6 = 'mno'
7 = 'pqrs'
8 = 'tuv'
9 = 'wxyz'
}
#initialize a dictionary list
$dict = [System.Collections.Generic.List[string]]::new()
if (Test-Path -path $Dictionary) {
Write-Verbose "[BEGIN ] Loading user dictionary from $Dictionary"
(Get-Content -path $Dictionary).foreach({$dict.add($_)})
$originalCount = $dict.Count
Write-Verbose "[BEGIN ] Loaded $originalCount items"
}
} #begin
Process {
Write-Verbose "[PROCESS] Converting: $Plaintext"
#add plaintext to dictionary if missing
if ($dict -notcontains $Plaintext) {
Write-Verbose "[PROCESS] Adding to user dictionary"
$dict.Add($plaintext)
}
#this is a technically a one-line expression
#get the matching key for each value and join together
($plaintext.ToCharArray()).ForEach({
$val = $_
($phone.GetEnumerator().Where({$_.value -match $val}).name)}) -join ""
} #process
End {
#commit dictionary to file if new words were added
if ($dict.count -gt $originalCount) {
Write-Verbose "[END ] Ending: Saving updated dictionary $Dictionary"
$dict | Out-File -FilePath $Dictionary -Encoding ascii -Force
}
Write-Verbose "[END ] Ending: $($MyInvocation.Mycommand)"
} #end
}
The function accepts pipelined input of words that are no more than 5 characters long AND can only be alphabet characters, either upper or lower case. That's what I'm doing with the ValidatePattern regular expression.
I am also planning ahead and keeping a text file of all converted words. If the plaintext doesn't exist in the file, it gets added. If the number of items in the dictionary list changes, I update the file in the End block. Note that I'm using a generic list object and not an array.
$dict = [System.Collections.Generic.List[string]]::new()
This type of object performs much better than an array. The difference is insignificant in this function, but I am moving more to this model instead of traditional arrays.
The conversion is done in the Process block with this one-line expression.
($plaintext.ToCharArray()).ForEach({
$val = $_
($phone.GetEnumerator().Where({$_.value -match $val}).name)}) -join ""
Here's a taste of the function in action.
Convert Number to Text
Converting back is only a little more complex. I can use the same hashtable. This time, I need to get the value associated with each key. I can then build a regular expression pattern to search through my dictionary file. Let me showyou the Verbose output first to illustrate.
This word file had everyting I had converted. Here's the same thing using a larger word file.
Remember the phrase I converted earlier?
It would take a little work but I could probably figure this out. Certainly, a higher quality word file helps. Which is why I started keeping track of what I converted.
Here's the complete function.
Function Convert-NumberToText {
[cmdletbinding()]
Param(
[Parameter(Position = 0, Mandatory, ValueFromPipeline)]
[ValidateRange(2, 99999)]
[int]$NumericValue,
[Parameter(Mandatory,HelpMessage= "Specify the path to the word dictionary file.")]
[ValidateScript({Test-Path $_})]
[string]$Dictionary
)
Begin {
Write-Verbose "[BEGIN ] Starting: $($MyInvocation.Mycommand)"
$phone = @{
2 = 'abc'
3 = 'def'
4 = 'ghi'
5 = 'jkl'
6 = 'mno'
7 = 'pqrs'
8 = 'tuv'
9 = 'wxyz'
}
Write-Verbose "[BEGIN ] Loading a word dictionary from $dictionary"
$words = Get-Content -path $Dictionary
Write-Verbose "[BEGIN ] ...$($words.count) available words."
#define a regex to divide the numeric value
[regex]$rx = "\d{1}"
} #begin
Process {
Write-Verbose "[PROCESS] Converting: $NumericValue"
$Values = ($rx.Matches($NumericValue).value).ForEach({ $val = $_ ; ($phone.GetEnumerator() | Where-Object {$_.name -match $val}).value})
Write-Verbose "[PROCESS] Converting: Possible values: $($Values -join ',')"
#build a regex pattern with a word boundary
$pattern = "\b"
foreach ($value in $values ) {
$pattern += "[$value]{1}"
}
$patternb += "\b"
Write-Verbose "[PROCESS] Using pattern: $pattern"
#output will be lower case
[System.Text.RegularExpressions.Regex]::Matches($words, $pattern, "IgnoreCase") |
Group-Object -Property Value |
Select-Object -ExpandProperty Name |
Sort-Object |
Foreach-Object {$_.tolower()}
} #process
End {
Write-Verbose "[END ] Ending: $($MyInvocation.Mycommand)"
} #end
}
And if you want to try my larger word file, download it here.
Learn More
There were a number of terrific learning opportunities in this challenge. If you find yourself wanting or needing to learn more, I encourage you to check out the PowerShell content on Pluralsight.com. I even have a course on PowerShell and Regular Expressions. And to really take your scripting to the next level, grab a copy of The PowerShell Scripting and Toolmaking book.