loading...

Daily Challenge #217 - SMS w/ an Old Phone

thepracticaldev profile image dev.to staff ・3 min read

If you're old enough, you might remember owning a flip phone, and sending your first text message with excitement in your eyes. Maybe you still have one lying in a drawer somewhere.

Let's try to remember the good old days and what it was like to send text messages with these devices. A lot of them had different layouts, most notably for special characters and spaces, so for simplicity we'll be using a fictional model with 3x4 key layout shown below:

-------------------------
|   1   |   2   |   3   |  <-- hold a key to type a number
|  .,?! |  abc  |  def  |  <-- press a key to type a letter
-------------------------
|   4   |   5   |   6   |  <-- Top row
|  ghi  |  jkl  |  mno  |  <-- Bottom row
-------------------------
|   7   |   8   |   9   |
|  pqrs |  tuv  |  wxyz |
-------------------------
|   *   |   0   |   #   |  <-- hold for *, 0 or #
|  '-+= | space |  case |  <-- press # to switch between upper/lower case
-------------------------

You'll receive a message and your job is to figure out which keys you need to press to output the given message with the lowest number of clicks possible. Return the result as a string of key inputs from top row (refer to diagram above).

Output string contains inputs that are shown at the top row of a key's layout (0-9*#), To type letters, press a button repeatedly to cycle through the possible characters. Pressing is represented by key's top row element repeated n times, where n is the position of character on that particular key. Examples:
2 => 'a', 9999 => 'z', 111 => '?', *** => '+'

To type numbers 0-9 and special characters *# - hold that key. Holding is represented by a number, followed by a dash. Examples:
3- => '3', 5-5-5- => '555'

Initially the case is lowercase. To toggle between lowercase and uppercase letters, use the # symbol. Case switching should only be considered when next character is alphabetic (a-z). Examples:
#2#9999 => 'Az' (remember, it's a toggle)
27-#2255 => 'a7BK' (do not switch before '7')

If you have 2 or more characters in a sequence that reside on the same button (refer to layout, bottom row), you have to wait before pressing the same button again. Waiting is represented by adding a space between 2 (or more) such characters. Example:
44 444 44 444 => 'hihi'

No need to wait after holding any key, even if next character resides on same button (4-4 => '4g'), or if there's a case switch between 2 characters on same button (#5#55 => 'Jk').

All inputs will be valid strings and only consist of characters from the key layout table.

Example

To put it all together, let's go over an example. Say you want to type this message - 'Def Con 1!':

Switch to uppercase with # and press 3 (#3 => D) (input is now in uppercase mode)
Switch to lowercase and press 3 twice (#33 => e). (Note that there is no waiting because of case switching)
Next character f is on button 3 again, and has the same case (lowercase input and lowercase character), so you have to wait to type again (' 333' => f).
In a similar manner type the second word (space is located on number 0). 0#222#666 660 => ' Con '
Finish off by holding 1 as 1- and typing ! as 1111 ('1-1111' = 1!). Note that after holding a key you don't need to wait to press another key.

Result:
sendMessage("Def Con 1!") => "#3#33 3330#222#666 6601-1111"

Tests

sendMessage("hey")
sendMessage("one two three")
sendMessage("Hello World!")

Good luck!


This challenge comes from B1ts on CodeWars. Thank you to CodeWars, who has licensed redistribution of this challenge under the 2-Clause BSD License!

Want to propose a challenge idea for a future post? Email yo+challenge@dev.to with your suggestions!

Posted on by:

thepracticaldev profile

dev.to staff

@thepracticaldev

The hardworking team behind dev.to ❤️

Discussion

pic
Editor guide
 

Swift solution :

let keys: [String:String] = [
    "1":".,?!",
    "2":"abc",
    "3":"def",
    "4":"ghi",
    "5":"jkl",
    "6":"mno",
    "7":"pqrs",
    "8":"tuv",
    "9":"wxyz",
    "*":"'-+=",
    "0":" ",
    "#":""]

func sendMessage(message: String) -> String {
    var isUpperCaseMode: Bool = false
    var keyPressed: String = ""

    let fetchCharacter: (Character) -> String  = { char in

        guard (keys.index(forKey: .init(char)) == nil) else {
          return "\(char)-"
        }

        guard let key = (keys.first { $1.contains(char.isLetter ? .init(char.lowercased()) : char) }?.key),
            let buttonText = keys[key] else {
                return ""
        }

        var code: [String] = []

        if key == keyPressed && char.isLowercase != isUpperCaseMode{
            code.append("  ")
        }

        keyPressed = key

        guard !char.isWhitespace else {
            code.append(key)
            return code.joined()
        }

        let number = buttonText.distance(from: buttonText.startIndex, to: buttonText.firstIndex(of: .init(char.lowercased())) ?? buttonText.startIndex) + 1

        if char.isLowercase == isUpperCaseMode {
            code.append("#")
            isUpperCaseMode.toggle()
        }

        code.append(String(repeating: key, count: number))
        return code.joined()
    }

    return message.map(fetchCharacter).joined()
}

sendMessage(message:"Def Con 1!") // "#3#33 3330#222#666 6601-1111"
sendMessage(message:"hey") // "4433999"
sendMessage(message:"one two three") // "666 6633089666084477733 33"
sendMessage(message:"Hello World!") // "#44#33555 5556660#9#66677755531111"
 

C++ solution

#include <iostream>
#include <vector>
#include <unordered_map>
using namespace std;

// returns key bindings for characters as a unordered_map
// Example :
// = ----> ****
// l ----> 555
// - ----> **
// # ----> #-
// . ----> 1
// c ----> 222
// 4 ----> 4-
// etc.
unordered_map<char, string> keyBindings(){
    // all options are in this vector of string
    // last empty string is for character '#'
    vector<string> allOptions = {".,?!","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz","'-+="," ",""};

    // make the key bindings for the keys
    unordered_map<char, vector<char>> keyOptions;
    for(int i=0;i<9;i++){
        keyOptions['0'+i+1] = vector<char>(allOptions[i].begin(), allOptions[i].end());
    }
    keyOptions['*'] = vector<char>(allOptions[9].begin(), allOptions[9].end());
    keyOptions['0'] = vector<char>(allOptions[10].begin(), allOptions[10].end());
    keyOptions['#'] = vector<char>(allOptions[11].begin(), allOptions[11].end());

    // this unordered_map maps a character to its key-typing
    // like 'a' -> "2", 'b' -> "22" etc.
    unordered_map<char, string> keyTypings;
    for(auto option : keyOptions){
        for(int i=0;i<option.second.size();i++){
            keyTypings[option.second[i]] = string(i+1,option.first);
        }
        // Add options for numbers like '2' -> "2-", '4' -> "4-"
        keyTypings[option.first] = option.first;
        keyTypings[option.first] += "-";
    }
    return keyTypings;
}


string sendMessage(string message){
    // get the key bindings for characters
    unordered_map<char, string> kb = keyBindings();
    // whether the word is upper case or not, true for upper case
    bool capital = false;
    string res = ""; // answer string

    for(int i=0;i<message.length();i++){
        // if the letter is lower case and capital is true
        // or letter is upper case and capital is false
        // then add a "#" and inverse the capital true to false or false to true
        if((islower(message[i]) && capital) || (isupper(message[i]) && !capital)){
            res += "#";
            capital = !capital;
        }

        // if same character is repeating then add a space
        // like for the case "hi" -> "44 444"
        if(!res.empty() && res[res.length()-1] == kb[tolower(message[i])][0])
            res += " ";

        // add the keybinding for the character's lower case
        res += kb[tolower(message[i])];
    }
    return res;
}

// main function
int main(){
    cout << sendMessage("Def Con 1!") << "\n"; // output -> #3#33 3330#222#666 6601-1111
    cout << sendMessage("hey") << "\n"; // output -> 4433999
    cout << sendMessage("one two three") << "\n"; // output -> 666 6633089666084477733 33
    cout << sendMessage("Hello World!") << "\n"; // output -> #44#33555 5556660#9#66677755531111

    return 0;
}
 

Here is Python solution based on my C++ solution

# returns key bindings for characters as a dictionary
# Example :
# = ----> ****
# l ----> 555
# - ----> **
# # ----> #-
# . ----> 1
# c ----> 222
# 4 ----> 4-
# etc.
def keyBindings() -> dict:
    # all options are in this list
    # last empty string is for '#'
    allOptions = ['.,?!','abc','def','ghi','jkl','mno','pqrs','tuv','wxyz',"'-+=",' ','']

    # key bindings for different keys
    # like for '1' -> ['.', ',', '?', '!']
    # for '2' -> ['a', 'b', 'c'] etc.
    keyOptions = {}
    for i in range(9):
        keyOptions[str(i+1)] = list(allOptions[i])
    keyOptions['*'] = list(allOptions[9])
    keyOptions['0'] = list(allOptions[10])
    keyOptions['#'] = list(allOptions[11])

    # maps a character to its key-typing
    # like 'a' -> "2", 'b' -> "22" etc.
    keyTypings = {}
    for key, value in keyOptions.items():
        for i in range(len(value)):
            keyTypings[value[i]] = key*(i+1)
        # add options for numbers like '2' -> "2-", '4' -> "4-"
        keyTypings[key] = key+'-'

    return keyTypings

def sendMessage(message: str) -> str:
    kb = keyBindings() # get the key bindings for characters
    capital = False # whether the word is upper case or not, true for upper case
    res = "" # answer string

    for i in range(len(message)):
        # if the letter is lower case and capital is true
        # or letter is upper case and capital is false
        # then add a "#" and inverse the capital, true to false or false to true
        if((message[i].islower() and capital) or (message[i].isupper() and not capital)):
            res += '#'
            capital = not capital

        # if same character is repeating then add a space
        # like for the case "hi" -> "44 444"
        if(res != '' and (res[-1] == kb[message[i].lower()][0])):
            res += ' '

        # add the keybinging for the character's lower case
        res += kb[message[i].lower()]
    return res


print(sendMessage("Def Con 1!")) # output -> 3#33 3330#222#666 6601-1111
print(sendMessage("hey")) # output -> 4433999
print(sendMessage("one two three")) # output -> 666 6633089666084477733 33
print(sendMessage("Hello World!")) # output -> 44#33555 5556660#9#66677755531111

 

JS Solution

const sendMessage = (msg) => {
  const keys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '*', '0', '#']
  const letters = ['.,?!', 'abc', 'def', 'ghi', 'jkl', 'mno', 'pqrs', 'tuv', 'wxyz', '-+=']
  const letterKeyMap = {}
  letters.forEach((ls, i) => ls.split('').forEach((l, r) => letterKeyMap[l] = [i, r + 1]))
  const isLowerCase = (c) => c.toLowerCase() === c

  let caseUp = false
  let lastKey
  return msg.split('').map((c, i, a) => {
    let caseChange = ''
    let key = ''
    let extra = ''
    if (c === ' ') { // Space
      key = '0'
    } else if (keys.indexOf(c) >= 0) { // Number
      key = keys[keys.indexOf(c)]
      extra = `-`
    } else { // Letter
      if (caseUp && isLowerCase(c) || !caseUp && !isLowerCase(c)) {
        caseChange = '#'
        caseUp = !isLowerCase(c)
      }
      const [ki, repeat] = letterKeyMap[c.toLowerCase()]
      key = keys[ki]
      extra = key.repeat(repeat - 1)
    }
    const addSpace = key === lastKey && caseChange === '' ? ' ' : ''
    const res = `${addSpace}${caseChange}${key}${extra}`
    lastKey = res[res.length - 1]
    return res
  }).join('')
}

const test = (msg, expected) => {
  const result = sendMessage(msg)
  console.log(`${result.localeCompare(expected) === 0} ${msg} ${result} === ${expected}`)
}

test('Def Con 1!', '#3#33 3330#222#666 6601-1111')
test('hey', '4433999')
test('one two three', '666 6633089666084477733 33')
test('Hello World!', '#44#33555 5556660#9#66677755531111')
test('Hi dev.to!  Fun times.', '#44#44403 338881866611110 0#333#88660844463377771')
 

Ruby solution

KEYS = {
  "1"=>".,?!", 
  "2"=>"abc", 
  "3"=>"def", 
  "4"=>"ghi", 
  "5"=>"jkl", 
  "6"=>"mno", 
  "7"=>"pqrs", 
  "8"=>"tuv", 
  "9"=>"wxyz", 
  "*"=>"'-+=", 
  "0"=>" ",
  "#"=>''
  }

@chars = { }
KEYS.each_key{|key| @chars[key]=key+'-'}
KEYS.each{|key,str| str.each_char.with_index{|char,index| @chars[char]=key*(index+1) }}

def need_temp?(char1,char2)
   return false if (str= @chars[char1]).nil? || char1.between?('0','9') || char1 == '*'
   return true  if str.include?(char2)
   KEYS[str[0]].include?(char2)
end

def send_message(msg)
  former_char = nil
  upcase = false
  msg.each_char.map do |char|
    sharp=''
    temp=''
    if char.between?('A','Z')
       sharp = upcase ? '' : '#'
       upcase = true if upcase == false
       char.downcase!
    elsif char.between?('a','z')
       sharp = upcase ? '#' : ''  
       upcase = false if upcase
    end
    temp=' ' if sharp.empty?  && need_temp?(former_char,char)
    former_char = char
    sharp + temp + @chars[char]
  end
  .join
end
 

Brute force the key map!

const keymap = new Map(
  [
    ['0', '0-'],
    ['1', '1-'],
    ['2', '2-'],
    ['3', '3-'],
    ['4', '4-'],
    ['5', '5-'],
    ['6', '6-'],
    ['7', '7-'],
    ['8', '8-'],
    ['9', '9-'],
    ['a', '2'],
    ['b', '22'],
    ['c', '222'],
    ['d', '3'],
    ['e', '33'],
    ['f', '333'],
    ['g', '4'],
    ['h', '44'],
    ['i', '444'],
    ['j', '5'],
    ['k', '55'],
    ['l', '555'],
    ['m', '6'],
    ['n', '66'],
    ['o', '666'],
    ['p', '7'],
    ['q', '77'],
    ['r', '777'],
    ['s', '7777'],
    ['t', '8'],
    ['u', '88'],
    ['v', '888'],
    ['w', '9'],
    ['x', '99'],
    ['y', '999'],
    ['z', '9999'],
    [' ', '0'],
    ['.', '1'],
    [',', '11'],
    ['?', '111'],
    ['!', '1111'],
    ['*', '*-'],
    ['\'', '*'],
    ['-', '**'],
    ['+', '***'],
    ['=', '****'],
    ['#', '#-'],
  ]);

function sendMessage(msg)
{
  let keypresses = '';
  let isUppercaseMode = false;
  let lastKey = '--';
  for(let c of msg)
  {
    let currKey = keymap.get(c.toLowerCase());
    if(isUppercaseMode && c.toUpperCase() != c)
    {
      keypresses += '#';
      isUppercaseMode = false;
    }
    else if(!isUppercaseMode && c.toLowerCase() != c)
    {
      keypresses += '#';
      isUppercaseMode = true;
    }
    else if(lastKey[1] !== '-' && currKey[0] === lastKey[0])
    {
      keypresses += ' ';
    }

    lastKey = currKey;

    keypresses += currKey;
  }
  return keypresses;
}
 

JS Solution

const sendMessage = message => {
    const keys = ['0', '1', '2', '3', '4','5', '6', '7', '8', '9', 
        '*', '#'];

    const keyCharacterMap = [' ', '.,?!', 'abc', 'def', 'ghi' , 'jkl', 
        'mno', 'pqrs', 'tuv', 'wxyz', '\'-+='];

    let result = [];
    let caseToggle = false;
    let previousKey = null;
    let caseToggleKey = keys[11];

    for (const character of message.split('')) {
        if (keys.indexOf(character) > -1) {
            result.push(character + '-');
            previousKey = null
        } else {
            for (const characterSet of keyCharacterMap) {
                let index = characterSet.indexOf(character.toLowerCase());
                let key = keys[keyCharacterMap.indexOf(characterSet)];
                let isUpperCase = character !== character.toLowerCase();

                if (index > -1) {
                    let caseToggleResult = '';

                    if (
                        (isUpperCase && !caseToggle) 
                        || (!isUpperCase && caseToggle)
                    ) {
                        caseToggle = !caseToggle;
                        caseToggleResult = caseToggleKey;
                        previousKey = null;
                    } 

                    result.push((key == previousKey ? ' ' : '')
                        + caseToggleResult + key.repeat(index + 1));

                    previousKey = key;

                    break;
                }
            }
        }
    }

    return result.join('');
};

[
    "Def Con 1!",
    "hey",
    "one two three",
    "Hello World!",
].forEach(s => console.log(
    `"${s}" => "${sendMessage(s)}"`
));

output:

"Def Con 1!" => "#3#33 3330#222#666 6601-1111"
"hey" => "4433999"
"one two three" => "666 6633089666084477733 33"
"Hello World!" => "#44#33555 5556660#9#66677755531111"
 
def sendMessage(user_str):
    key_clicks = ""
    is_toggle_clicked = False
    user_str = list(user_str)
    key_map = {
        "1":{"key": "1","clicks": 1, "holds": 1},
        ".":{"key": "1","clicks": 1, "holds": 0},
        ",":{"key": "1","clicks": 2, "holds": 0},
        "?":{"key": "1","clicks": 3, "holds": 0},
        "!":{"key": "1","clicks": 4, "holds": 0},

        "2":{"key": "2","clicks": 1, "holds": 1},
        "a":{"key": "2","clicks": 1, "holds": 0},
        "b":{"key": "2","clicks": 2, "holds": 0},
        "c":{"key": "2","clicks": 3, "holds": 0},

        "3":{"key": "3","clicks": 1, "holds": 1},
        "d":{"key": "3","clicks": 1, "holds": 0},
        "e":{"key": "3","clicks": 2, "holds": 0},
        "f":{"key": "3","clicks": 3, "holds": 0},

        "4":{"key": "4","clicks": 1, "holds": 1},
        "g":{"key": "4","clicks": 1, "holds": 0},
        "h":{"key": "4","clicks": 2, "holds": 0},
        "i":{"key": "4","clicks": 3, "holds": 0},

        "5":{"key": "5","clicks": 1, "holds": 1},
        "j":{"key": "5","clicks": 1, "holds": 0},
        "k":{"key": "5","clicks": 2, "holds": 0},
        "l":{"key": "5","clicks": 3, "holds": 0},

        "6":{"key": "6","clicks": 1, "holds": 1},
        "m":{"key": "6","clicks": 1, "holds": 0},
        "n":{"key": "6","clicks": 2, "holds": 0},
        "o":{"key": "6","clicks": 3, "holds": 0},

        "7":{"key": "7","clicks": 1, "holds": 1},
        "p":{"key": "7","clicks": 1, "holds": 0},
        "q":{"key": "7","clicks": 2, "holds": 0},
        "r":{"key": "7","clicks": 3, "holds": 0},
        "s":{"key": "7","clicks": 4, "holds": 0},

        "8":{"key": "8","clicks": 1, "holds": 1},
        "t":{"key": "8","clicks": 1, "holds": 0},
        "u":{"key": "8","clicks": 2, "holds": 0},
        "v":{"key": "8","clicks": 3, "holds": 0},

        "9":{"key": "9","clicks": 1, "holds": 1},
        "w":{"key": "9","clicks": 1, "holds": 0},
        "x":{"key": "9","clicks": 2, "holds": 0},
        "y":{"key": "9","clicks": 3, "holds": 0},
        "z":{"key": "9","clicks": 4, "holds": 0},

        "*":{"key": "*","clicks": 1, "holds": 1},
        "'":{"key": "*","clicks": 1, "holds": 0},
        "-":{"key": "*","clicks": 2, "holds": 0},
        "+":{"key": "*","clicks": 3, "holds": 0},
        "=":{"key": "*","clicks": 4, "holds": 0},

        " ":{"key": "0","clicks": 1, "holds": 0},

    }

    for index, character in enumerate(user_str):

        if character.isupper() and not is_toggle_clicked:
            is_toggle_clicked = True
            key_clicks += "#"
            character = character.lower()
        elif is_toggle_clicked:
            is_toggle_clicked = False
            key_clicks += "#"

        if index != 0 and key_map[user_str[index-1].lower()]['key'] == key_map[character]['key'] and key_clicks[-1] != "#" and key_clicks[-1] != "-":
            key_clicks += " "

        key_clicks += (key_map[character]['key'] *key_map[character]['clicks']) + ("-" * key_map[character]['holds'])


    return key_clicks