8
\$\begingroup\$

As much as I hate it, sometimes providing passwords to people has to be done electronically. When I do that I try to remove any ambiguity that might be created by the font, user etc. and provide a phonetic translation of the string. For things like 0 and O or l and I which might not be obvious at first.

The function below can take a single string or be piped a string array. I could get crazy with this later to support other languages by making some nested arrays but for now it only supports English.

function Get-PhoneticString{
    param(
        [parameter(ValueFromPipeline=$true, Mandatory=$true)]
        [string]$StringToExplode,
        [string]$Delimiter = " - "
    )
    process{
        # Alphabetic conversions using military phonetics
        $phoneticAlphabet = @{
            "a" = "alfa"; "b" = "bravo"; "c" = "charlie"; "d" = "delta"; "e" = "echo";
            "f" = "foxtrot"; "g" = "golf"; "h" = "hotel"; "i" = "india"; "j" = "juliett";
            "k" = "kilo"; "l" = "lima"; "m" = "mike"; "n" = "november"; "o" = "oscar";
            "p" = "papa"; "q" = "quebec"; "r" = "romeo"; "s" = "sierra"; "t" = "tango";
            "u" = "uniform"; "v" = "victor"; "w" = "whiskey"; "x" = "x-ray"; "y" = "yankee";
            "z" = "zulu"
        }

        $numberAsString = @{
            "0"="zero"; "1"="one"; "2"="two"; "3"="three"; "4"="four"
            "5"="five"; "6"="six"; "7"="seven"; "8"="eight"; "9"="nine"
        }

        $symbolAsString = @{
            "!" = "Exclamation Mark"; '"' = "Double Quote"; "#" = "Number Sign"; "$" = "Dollar"; "%" = "Percent"; "&" = "Ampersand"
            "'" = "Single Quote"; "(" = "Left Parenthesis"; ")" = "Right Parenthesis"; "*" = "Asterisk"; "+" = "Plus"
            "," = "Comma"; "-" = "Minus"; "." = "Period"; "/" = "Slash"; ":" = "Colon"; ";" = "Semicolon"; "<" = "Less Than"
            "=" = "Equality Sign"; ">" = "Greater Than"; "?" = "Question Mark"; "@" = "At Sign"; "[" = "Left Square Bracket"
            "\" = "Backslash"; "]" = "Right Square Bracket"; "^" = "Caret"; "_" = "Underscore"; '`' = "Grave / Accent"
            "{" = "Left Curly Bracket"; "|" = "Vertical Bar"; "}" = "Right Curly Bracket"
        }

        # Convert this string into a character array. For each character find its string equivilent in one of the defined hash tables
        ($StringToExplode.ToCharArray() |  ForEach-Object{
            switch -Regex -CaseSensitive ($_){
                "[a-z]"{$phoneticAlphabet."$_"}
                "[A-Z]"{($phoneticAlphabet."$_").ToUpper()}
                "\d"{$numberAsString."$_"}
                "\s"{$_}# Return whitespace unaltered
                default{ $symbolAsString."$_"}
            }
        }) -join $Delimiter
    }

}

So a sample call and output would look like this

"K0D3R3v13w" | Get-PhoneticString
KILO - zero - DELTA - three - ROMEO - three - victor - one - three - whiskey

It only supports the basic ASCII printable character set. Any uppercase alphabetical characters will have all uppercase strings returned. If the character is not alphanumeric it will be assumed that it is a symbol in the set. If that character is not covered at all then it will show as empty in the resultant string and means something needs to be updated to account for it. Since I am using this for passwords that should cover near all supported characters.

\$\endgroup\$
  • \$\begingroup\$ Your users might appreciate xkcd style passwords. They are easier to remember and would avoid this problem. \$\endgroup\$ – NonlinearFruit Feb 27 '17 at 3:07
3
\$\begingroup\$

Your code looks pretty good to me. I applaud your using datastructures to hold the mappings. A lot of developers would have used a humongous switch statement there, but I think as a general rule, datastructures are to be preferred over code as they makes things clearer and more flexible.

It's a good programming principle to break things down into smaller functions. That makes the code easier to read and maintain. It makes your code self-documenting, which means you don't need to rely on comments so much.

We can put the inner logic of your blob of code into its own function:

function wordFromChar($char)
{
    switch -Regex -CaseSensitive ($char){
        "[a-z]"{$phoneticAlphabet."$char"}
        "[A-Z]"{($phoneticAlphabet."$char").ToUpper()}
        "\d"{$numberAsString."$char"}
        "\s"{$char}# Return whitespace unaltered
        default{ $symbolAsString."$char"}
    }
}

And then call it like this:

$words = $StringToExplode.ToCharArray() |  ForEach-Object {
    wordFromChar $_
}

$words -join $Delimiter

We can improve the code in the wordFromChar function. The regular expressions such as [a-z] are repeating the information that is already contained in the hash table. That violates the DRY Principle. ("Don't repeat yourself.") This is an important principle which you can look up.

I'd be inclined to put all the hash tables into a single hash table. I don't see why there should be separate tables. Having separate tables just means we have more stuff to deal with but without any real benefits. Assuming that we make a single hash table called $charMap, we could rewrite the function like this:

function wordFromChar($char)
{
    if ($charMap.Contains($char))
    {
        $result = $charMap[$char]
    }
    else 
    {
        throw "Can't handle '$char'"    
    }

    if (isUppercase $char)
    {
        $result = $result.ToUpper()
    }

    $result
}

(If there was a good reason to keep the separate hash tables, we could certainly do that. We could just put them all in an array. That is, we would have an array of hash tables. That would make the lookup logic a little more complicated, but only by a little.)

isUppercase is a new function that could look like this:

function isUppercaseChar($char)
{
    $char -cmatch "^[A-Z]$"
}

-cmatch is a case-sensitive regular expression match.

I haven't tested any of the code above, by the way.

\$\endgroup\$
  • 1
    \$\begingroup\$ $char -cmatch "^[A-Z]$" looks like an unnecessary invocation of a big regex engine, it could be ([char]'A' -lt $char) -and ($char -lt [char]'Z'), or $char -in [char]'A'..[char]'Z' \$\endgroup\$ – TessellatingHeckler Mar 2 '17 at 8:58
  • 1
    \$\begingroup\$ @TessellatingHeckler, what does "unnecessary" mean? Do you mean that the PowerShell interpreter will have to do extra work? Who cares? What possible real world difference would that make? You should optimize for human beings, not for computers. In this particular case, the way I wrote it was optimized for clarity. Clarity helps guard against bugs. Your first expression has a bugs in it. Both '-lt' should be -le. \$\endgroup\$ – Dangph Mar 3 '17 at 4:19
  • \$\begingroup\$ @TessellatingHeckler, actually neither of your expressions work in any case. \$\endgroup\$ – Dangph Mar 3 '17 at 4:33
  • \$\begingroup\$ Yeah they both work - i.imgur.com/0ZVrdB2.png - but you're right about the boundary bug and that is a great reason it's a dumb idea. You should optimize for human beings, not for computers. - I agree .. and I started by looking for an equivalent of Python's x.isupper() but that doesn't exist. It seems to me from answering PowerShell questions that a lot of PowerShell users aren't comfortable with regex, and I am would still probably miss the 'c' in cmatch on first reading. Since it does nothing greatly regexy, it seems a lot to introduce an entire other language to check case. \$\endgroup\$ – TessellatingHeckler Mar 3 '17 at 5:40
  • 1
    \$\begingroup\$ You have to test. These give wrong results: $char = 'a'; $char -eq ([string]$char).ToUpper() and $char = 'A'; $char -in [char]'A'..[char]'Z' . One problem with your ToUpper logic is for example that '!' will be counted as uppercase, which isn't what is wanted for this NATO password program. I originally had it like this: $char -cne $char.ToLower(), which works, but the negative logic is not very clear. (Don't forget, BTW, that PowerShell thinks that 'A' -eq 'a' is true. String comparisons are case insensitive unless you use -ceq and so on.) \$\endgroup\$ – Dangph Mar 3 '17 at 7:08
2
\$\begingroup\$

I rather like the idea of making it an array where each position is the ASCII Character code, so $Lookup[97] = 'alpha', and so on, but I tried writing it and it looks more confusing than sensible. The main things I'm thinking about with your code is:

  • the regex lookup is a bit messy, identifying whitespace and 'default' and doing the same thing for both
  • having to handle upper/lowercase in the lookup because you haven't coded it into the table.
  • presentation of the lookup tables isn't very clear

My approach uses a single lookup table, grouped into columns, with single quoted strings (because even though they take up the same character width I think it's less visually busy), and then adds the words in from their first character in upper and lower case, so the lookup code can be simpler.

function Get-PhoneticString{
    param(
        [parameter(ValueFromPipeline=$true, Mandatory=$true)]
        [string]$StringToExplode,
        [string]$Delimiter = " - "
    )
    process{

        $PhoneticLookup = @{
          '0' = 'Zero'; '1' = 'One'; '2' = 'Two'; '3' = 'Three'; '4'='Four'; '5'='Five';'6'='Six';'7'='Seven';'8'='Eight';'9'='Nine';

          '"' = 'Double Quote'; '$' = 'Dollar'     ; '%' = 'Percent' ; '&' = 'Ampersand'    ; '!' = 'Exclamation Mark';
          "'" = 'Single Quote'; '*' = 'Asterisk'   ; '+' = 'Plus'    ; ',' = 'Comma'        ; '?' = 'Question Mark'   ;
          ';' = 'Semicolon'   ; '.' = 'Period'     ; ':' = 'Colon'   ; '=' = 'Equality Sign'; '@' = 'At Sign'         ; 
          '-' = 'Minus'       ; '#' = 'Number Sign'; '/' = 'Slash'   ; '_' = 'Underscore'   ; '<' = 'Less Than'       ;
          '\' = 'Backslash'   ; '^' = 'Caret'      ; '`' = 'Backtick'; '|' = 'Vertical Bar' ; '>' = 'Greater Than'    ; 
          '[' = 'Left Square Bracket' ; '{' = 'Left Curly Bracket'   ; '(' = 'Left Parenthesis' ;
          ']' = 'Right Square Bracket'; '}' = 'Right Curly Bracket'  ; ')' = 'Right Parenthesis';
        }

        # Add the words by first character, in both upper and lower case.
        @( 
            'ALPHA',    'BRAVO', 'CHARLIE', 'DELTA',  'ECHO',  'FOXTROT', 'GOLF',  'HOTEL',   'INDIA',  'JULIET',  'KILO',  'LIMA',   'MIKE',
            'NOVEMBER', 'OSCAR', 'PAPA',    'QUEBEC', 'ROMEO', 'SIERRA',  'TANGO', 'UNIFORM', 'VICTOR', 'WHISKEY', 'X-RAY', 'YANKEE', 'ZULU'
        ) | ForEach-Object {
            $PhoneticLookup[$_[0]] = $_
            $PhoneticLookup[$_.ToLower()[0]] = $_.ToLower()
        }

        # Lookup
        $Words = foreach ($Char in [string[]]$StringToExplode.ToCharArray()) {
            if ($PhoneticLookup.ContainsKey($Char)) { $PhoneticLookup[$Char] } else { $Char }
        }
        $Words -join $Delimiter
    }
}
\$\endgroup\$
  • \$\begingroup\$ This was just something quick I threw together. Never thought of putting them together at first. The tables declarations were a sore point for me and I figured someone might say something about it. One thing that I notice in my code that is in yours as well: $PhoneticLookup should be in a begin block for when this gets called via the pipeline with multiple strings yes? \$\endgroup\$ – Matt Mar 2 '17 at 13:20
  • \$\begingroup\$ $PhoneticLookup should be in a begin block for when this gets called via the pipeline with multiple strings yes? - seems like a good idea to me. \$\endgroup\$ – TessellatingHeckler Mar 2 '17 at 23:34
  • \$\begingroup\$ What were you doing to build that formatted table of values? I would guess that you didn't manually align all of that. \$\endgroup\$ – Matt Oct 10 '17 at 13:27
  • 1
    \$\begingroup\$ This appears to be failing when using numbers in the input string e.g $PhoneticLookup.ContainsKey([char]"4") is false so the literal is returned. Trying to think of the simplest way to address this \$\endgroup\$ – Matt Oct 10 '17 at 13:36
  • 1
    \$\begingroup\$ So it was not doing the symbol lookups properly for me either. I changed the lines that created the alphabet keys to make string based keys. Then I made the loop do lookups with strings. The function works for me now the way I believe it should. if ($PhoneticLookup.ContainsKey("$Char")) { $PhoneticLookup["$Char"] } else { $Char } \$\endgroup\$ – Matt Oct 10 '17 at 13:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.