Dyota's blog

Keypad

IT troubles

I had to call the IT service desk this morning to resolve an issue with my work laptop. Our IT department has this automated voice assistant that answers when you call. It asked me to punch in my username using the keypad.

That got me thinking: how many other people have a username that also match the number combination that I'm punching in?

Keypad combinations

Let's say that your username was gideonj. On a keypad, that would be 4433665. What if there are other people that have that number combination as a result of their username, like gedfool, gifdonk, or hidemok?

Recursive functions

I started thinking about List.Generate or List.Accumulate, but I couldn't imagine in my head how to execute that.

I had an inkling that I will need to use recursive functions somehow in this. I had never created anything with recursive functions before so I had to look it up. In Power Query, functions can call themselves by prefixing the function call with "@".

For example,

generateFull = (i as number, inputLists as list) as list =>
    ...
    generateNewList(
        @generateFull(i - 1, inputLists),
        inputLists{i - 1}
    )

I found it difficult to fully comprehend the effects of recursion inside my head. For this, I didn't just "wing it" and start writing code. Usually I could do that, because of the previews that Power Query shows at every step. Because this was going to be recursive, I had to know exactly that the function was doing at every step.

That's why I did it first on paper, and to start small.

Full code

I couldn't fully answer my question because I didn't find a resource with everybody's username, but I got most of the way there in that I was able to generate all of the possible letter combinations, given a set of numbers punched in.

For convenience, the input is actually an alphabetic string (i.e., this script will accept gideonj and not 4433665.

Here it is:

Power Query

let
    input = 
        let 
            alphaInput = Excel.CurrentWorkbook(){[Name="input"]}[Content]{0}[input],

            alphaInputArray = Text.ToList(alphaInput),
            convertToKeypadNumbers = List.Transform(
                alphaInputArray,
                each 
                    let
                        thisLetter = _
                    in
                        Table.SelectRows(
                            keypad, 
                            each List.Contains([letters], thisLetter)
                        ){0}[number]
            )
        in
            convertToKeypadNumbers,
    
    keypad = Table.FromColumns(
        {
            { 2, 3, 4, 5, 6, 7, 8, 9 },
            {
                {"a", "b", "c"},
                {"d", "e", "f"},
                {"g", "h", "i"},
                {"j", "k", "l"},
                {"m", "n", "o"},
                {"p", "q", "r", "s"},
                {"t", "u", "v"},
                {"w", "x", "y", "z"}
            }
        }, 
        {
            "number", 
            "letters"
        }
    ),
    
    combinations = List.Transform(
        input, 
        each keypad{[number = _]}[letters]
    ),

    combine = (thisLetter as text, followingLetters as list) => 
        List.Transform(
            followingLetters,
            each thisLetter & _
        ),

    generateNewList = (firstLetters as list, nextLetters as list) =>
        List.Combine(
            List.Transform(
                firstLetters,
                each combine(_, nextLetters)
            )
        ),

    generateFull = (i as number, inputLists as list) as list =>
        if ( i < 3 ) 
            then generateNewList(
                inputLists{0}, 
                inputLists{1}
            )
            else generateNewList(
                @generateFull(i - 1, inputLists),
                inputLists{i - 1}
            ),

    allCombinations = generateFull(List.Count(combinations), combinations)
        
in
    allCombinations

Extra

Same thing, but in different languages.

JavaScript

const input = 'hello';

const keypad = [
    { number: 2, letters: ["a", "b", "c"] },
    { number: 3, letters: ["d", "e", "f"] },
    { number: 4, letters: ["g", "h", "i"] },
    { number: 5, letters: ["j", "k", "l"] },
    { number: 6, letters: ["m", "n", "o"] },
    { number: 7, letters: ["p", "q", "r", "s"] },
    { number: 8, letters: ["t", "u", "v"] },
    { number: 9, letters: ["w", "x", "y", "z"] }
]

const inputList = input
    .split('')
    .map((_) => {
        const thisLetter = _
        return keypad.filter((_) => {
            return _.letters.includes(thisLetter)
        })[0]
    })
console.log(inputList);

const combine = (thisLetter, followingLetters) =>  followingLetters.map( _ => thisLetter + _ );

const generateNewList = (firstLetters, nextLetters) => [].concat.apply([], firstLetters.map( _ => combine(_, nextLetters) ));

const generateFull = (i, inputList) => (i < 3) ? 
        generateNewList(inputList[0].letters, inputList[1].letters) :
        generateNewList(
            generateFull((i-1), inputList),
            inputList[i-1].letters
        )

console.log(generateFull(inputList.length, inputList));

PowerShell

using namespace System.Collections;

$input = Read-Host "Enter a set of characters: "

$keypad = @(
    [PSCustomObject] @{ number = 2; letters = @("a", "b", "c") },
    [PSCustomObject] @{ number = 3; letters = @("d", "e", "f") },
    [PSCustomObject] @{ number = 4; letters = @("g", "h", "i") },
    [PSCustomObject] @{ number = 5; letters = @("j", "k", "l") },
    [PSCustomObject] @{ number = 6; letters = @("m", "n", "o") },
    [PSCustomObject] @{ number = 7; letters = @("p", "q", "r", "s") },
    [PSCustomObject] @{ number = 8; letters = @("t", "u", "v") },
    [PSCustomObject] @{ number = 9; letters = @("w", "x", "y", "z") }
)

$inputList = $($input -split '') |
    Where { return $_ -ne '' } |
    ForEach({ 
        $thisLetter = $_
        $keypad |
            Where {
                $_.letters -contains $thisLetter
            } |
            Select-Object letters
    }) 

function CombineWith-NextLetter ( [string] $thisLetter, [array] $followingLetters ) { 
    return $followingLetters.ForEach({
        $thisLetter + $_
    })
}

function Generate-NewList( [array] $firstLetters, [array] $nextLetters ) {
    return $firstLetters.ForEach({
        CombineWith-NextLetter $_ $nextLetters
    })
}

function Generate-FullResults( [int] $i, [array] $inputList ) {
    return $(if ( $i -lt 3) {
        Generate-NewList $inputList[0].letters $inputList[1].letters
    } else {
        Generate-NewList $(Generate-FullResults $($i - 1) $inputList) $inputList[$i - 1].letters
    })
}

Generate-FullResults $inputList.Count $inputList

C#

using System;
using System.Collections.Generic;
using System.Linq;

namespace KeypadDictionary
{
    internal class Program
    {
        static void Main(string[] args)
        {
            // Opening
            Console.WriteLine("Enter a set of characters: ");
            string input = Console.ReadLine();

            // Set up keypad
            Dictionary<int, string[]> keypad = new Dictionary<int, string[]>() {
                { 2, new string[3] { "a", "b", "c" }},
                { 3, new string[3] { "d", "e", "f" }},
                { 4, new string[3] { "g", "h", "i" }},
                { 5, new string[3] { "j", "k", "l" }},
                { 6, new string[3] { "m", "n", "o" }},
                { 7, new string[4] { "p", "q", "r", "s" }},
                { 8, new string[3] { "t", "u", "v" }},
                { 9, new string[4] { "w", "x", "y", "z" }}
            };

            // Set up input array
            char[] chars = input.ToCharArray();

            string[][] inputList = Array.ConvertAll(
                chars,
                _ =>
                    keypad
                        .AsQueryable()
                        .Where(key =>
                            Array.Exists(key.Value,e =>
                                e == _.ToString()
                            )
                        )
                        .Select(_ => _.Value)
                        .First()
            ).ToArray();

            // Print out results
            foreach (string result in GenerateFull(inputList.Length, inputList))
                Console.WriteLine(result);
        }

        static string[] Combine(string thisLetter, string[] followingLetters)
        {
            return Array.ConvertAll(followingLetters, _ => thisLetter + _);
        }

        public static string[] ConcatArrays(params string[][] p)
        {
            // reference: https://stackoverflow.com/questions/47321325/c-sharp-a-better-way-to-copy-merge-multiple-arrays-into-one-array
            var position = 0;
            var outputArray = new string[p.Sum(a => a.Length)];
            foreach (var curr in p)
            {
                Array.Copy(curr, 0, outputArray, position, curr.Length);
                position += curr.Length;
            }
            return outputArray;
        }

        static string[] GenerateNewList(string[] firstLetters, string[] nextLetters)
        {
            string[][] combinedArrays = Array.ConvertAll(firstLetters, _ => Combine(_, nextLetters)).ToArray();
            return ConcatArrays(combinedArrays);
        }

        static string[] GenerateFull(int i, string[][] inputList)
        {
            if (i < 3)
                return GenerateNewList(inputList[0], inputList[1]);
            else
                return GenerateNewList(GenerateFull(i - 1, inputList), inputList[i - 1]);
        }
    }

}

#csharp #javascript #powerquery #powershell