Codewars Lösung | Texting with an old-school mobile phone


coden
Codewars. Achieve mastery through challenge.
Daniel Kaser|2. September 2024
6 min.

Inhalt

  1. Die Fakten
  2. Beschreibung
  3. Lösung
    1. Pseudo-Code
    2. Code
  4. Feedback

Die Fakten:

Plattform:codewars.com
Name:Texting with an old-school mobile phone
Level:6 kyu
Sprache:TypeScript

Beschreibung:

If you're old enough, you might remember buying your first mobile phone, one of the old ones with no touchscreen, 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 such 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
-------------------------

The task

You got your thumb ready to go, so 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).

Take your time to study the rules below.

How it works

Result

Output string contains inputs that are shown at the top row of a key's layout. (0-9*#)

Typing letters

To type letters, press a button repeatedly to cycle through the possible characters (bottom row of a key's layout). 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 => '?', *** => '+'

Typing numbers

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'

Uppercase / Lowercase

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')

Waiting for next character

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'

Exceptions: 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').

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:

`send_message("Def Con 1!") => "#3#33 3330#222#666 6601-1111"`
`sendMessage("Def Con 1!") => "#3#33 3330#222#666 6601-1111" `
`Kata.SendMessage("Def Con 1!") => "#3#33 3330#222#666 6601-1111"`
`send_message("Def Con 1!", "#3#33 3330#222#666 6601-1111").`
SendMessage("Def Con 1!") => "#3#33 3330#222#666 6601-1111"
***Note for CFML: be careful with '#'. To output it, you should use '##'.***

More examples are provided in sample test suite.

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

Good luck!

Also check out other cool katas that inspired this one:

Quelle: codewars.com

Lösung

Pseudo-Code

Wie immer gibt's reichlich Varianten, hier ist eine meiner.

Erst die Lösungsschritte in Pseudo-Code. Los geht’s:

Lösungsschritte
Schritt 1

Zuerst brauchen wir ein paar Variablen. Eine für’s gesamte Keypad, eine um den aktuellen Klein-/Großschreibmodus festzuhalten und eine um uns den letzten gedrückten Button zu merken.

Schritt 2

Dann loopen wir durch den Input-String.

Schritt 3

Wir suchen das jeweils aktuelle Zeichen in unserem Keypad, darum gehen wir mit einem weiteren Loop durch jeden Button im Keypad.

Schritt 4

Und prüfen, ob unser aktuelles Zeichen auf dem aktuellen Button ist.

Schritt 5

Wenn ja, speichern wir uns den Index, den das Zeichen auf dem Button hat.

Schritt 6

Wenn der aktuelle Button der gleiche ist, wie der letzte, fügen wir schon mal ein Leerzeichen zum aktuellen Button-Zwischenergebnis hinzu.

Schritt 7

Wenn sich das aktuelle Zeichen in der oberen Reihe des Buttons befindet (also z.B. eine Zahl ist), setzen wir den letzten Button zurück und fügen den aktuellen Button, gefolgt von einem - zum aktuellen Button-Zwischenergebnis hinzu. In diesem Fall können wir das Zwischenergebnis speichern und zum nächsten Element springen.

Schritt 8

Wenn das aktuelle Zeichen ein Kleinbuchstabe ist UND wir uns im Großbuchstaben-Modus befinden ODER ein Großbuchstabe ist UND wir uns NICHT im Großbuchstabenmodus befinden, switchen wir den Großbuchstabenmodus ins Gegenteil und fügen ein # zum aktuellen Button-Zwischenergebnis hinzu.

Schritt 9

Am Ende des inneren Loops hängen wir noch den Button so oft ans aktuelle Button-Zwischenergebnis an, wie der Index des aktuellen Zeichens auf dem Button (also bei einem h, mit Index 2 auf dem Button 4: 2x4 → 44).

Schritt 10

Nicht vergessen den letzten Button für die nächste Runde auf den aktuellen zu setzen.

Schritt 11

Nach dem inneren Loop speichern wir das aktuelle Button-Zwischenergebnis.

Schritt 12

Nach dem äußeren Loop geben wir das Endergebnis als String zurück.

Puh, ganz schön komplex... Aber ich hab dich gewarnt!

Code

Geil. Übersetzen wir unseren Pseudo-Code in TypeScript:

Lösungsschritte
Der Lesbarkeit halber erstelle ich mir 2 Hilfsfunktionen:
function isLowerCaseLetter(char: string): boolean {
  return /[a-z]/.test(char);
}

function isUpperCaseLetter(char: string): boolean {
  return /[A-Z]/.test(char);
}

Dann geht’s los mit der Hauptfunktion!

Die erste Zeile meiner Hauptfunktion:
export function sendMessage(message: string): string {
Dann erstellen wir unsere Variablen:
const keypad = [
  "1.,?!",
  "2abc",
  "3def",
  "4ghi",
  "5jkl",
  "6mno",
  "7pqrs",
  "8tuv",
  "9wxyz",
  "*'-+=",
  "0 ",
  "#",
];

let isUpperCaseMode = false;
let lastButton = "";
Dann loopen wir durch den Input-String:
  return [...message]
    .map((char) => {
      let currResult = "";

Dafür wandeln wir den String als Erstes in ein Array um. Da wir für jedes Zeichen im Input-String etwas zurück haben wollen, bietet sich hier ein .map() an.

Und da das Array, das wir danach erhalten (fast) schon unserem Endergebnis entspricht, können wir unser return-Statement schon hier platzieren.

Außerdem brauchen wir noch eine Variable um unsere Zwischenergebnisse für das jeweils aktuelle Zeichen zu speichern.

Dann der (innere) Loop durch die Keypad-Buttons:
      for (const button of keypad) {

Hier entscheide ich mich für einen for...of-Loop, damit wir ihn ggf. vorzeitig beenden können.

Wir checken jeden Button, ob er unser aktuelles Zeichen enthält:
        if (button.includes(char.toLowerCase())) {
Wenn ja, speichern wir uns den Index des Zeichens im Button in einer Variablen:
const currButton = button[0];
const keyIndex = button.indexOf(char.toLowerCase());
const isTopRow = !keyIndex;

Außerdem erstelle ich mir ein paar weitere (optionale) Variablen zur besseren Lesbarkeit.

Sollte der keyIndex 0 sein, ist unser gesuchtes Zeichen das erste auf dem Button. Also das Zeichen in der oberen Reihe. Da 0 falsy ist, gibt !keyIndex true zurück. In allen anderen Fällen false.

Dann prüfen wir, ob der aktuelle Button der selbe ist wie der letzte:
if (currButton === lastButton) currResult += " ";

Wenn ja, fügen wir das Leerzeichen zu unserem aktuellen Zwischenergebnis hinzu.

Dann prüfen wir, ob sich unser Zeichen in der oberen Reihe befindet (also z.B. eine Zahl ist):
if (isTopRow) {
  lastButton = "";
  return currResult + currButton + "-";
}

Wenn ja, können wir den inneren Loop beenden und für das aktuelle Zeichen im Input-String unser bisheriges Zwischenergebnis plus den aktuellen Button und ein - zurückgeben.

Da es laut Regel für das Halten eines Buttons der letzte Button keine Rolle spielt, setzen wir ihn vorher noch zurück.

Jetzt können wir prüfen, ob das aktuelle Zeichen ein Kleinbuchstabe ist UND der UpperCaseMode gerade aktiv ist ODER ob es ein Großbuchstabe ist UND der UpperCaseMode gerade NICHT aktiv ist:
          if (
            (isLowerCaseLetter(char) && isUpperCaseMode) ||
            (isUpperCaseLetter(char) && !isUpperCaseMode)
          ) {
Wenn eines von beiden zutrifft, müssen wir den aktuellen UpperCaseMode in sein Gegenteil switchen:
            isUpperCaseMode = !isUpperCaseMode;
            currResult = "#";
          }

Außerdem ersetzen wir dann unser bisheriges Zwischenergebnis mit einem #.

Dann ergänzen wir den aktuellen Button in entsprechender Anzahl zum Zwischenergebnis:
currResult += currButton.repeat(keyIndex);

Die Anzahl der Wiederholungen ergibt sich aus der Position, also dem Index, auf dem Button.

Am Ende des inneren Loops nicht vergessen, den letzten Button zu aktualisieren:
          lastButton = currButton;
        }
      }
Am Ende des äußeren Loops noch das aktuelle Zwischenergnis zurückgeben:
      return currResult;
    })
Und zum Schluss noch das Endergebnis-Array in einen String umwandeln:
    .join("");
}

Zurückgegeben haben wir das Ganze schon oben mit dem return-Statement vor dem .map()-Loop.

Voilá! 💪

Wie gesagt, dat Ding war ziemlich komplex und eher etwas anspruchsvoller als ein Level 6. Aber: Nur die Harten komm’ in’ Garten!

Fragen?

Komplettlösung
export function sendMessage(message: string): string {
  const keypad = [
    "1.,?!",
    "2abc",
    "3def",
    "4ghi",
    "5jkl",
    "6mno",
    "7pqrs",
    "8tuv",
    "9wxyz",
    "*'-+=",
    "0 ",
    "#",
  ];

  let isUpperCaseMode = false;
  let lastButton = "";

  return [...message]
    .map((char) => {
      let currResult = "";

      for (const button of keypad) {
        if (button.includes(char.toLowerCase())) {
          const currButton = button[0];
          const keyIndex = button.indexOf(char.toLowerCase());
          const isTopRow = !keyIndex;

          if (currButton === lastButton) currResult += " ";

          if (isTopRow) {
            lastButton = "";
            return currResult + currButton + "-";
          }

          if (
            (isLowerCaseLetter(char) && isUpperCaseMode) ||
            (isUpperCaseLetter(char) && !isUpperCaseMode)
          ) {
            isUpperCaseMode = !isUpperCaseMode;
            currResult = "#";
          }

          currResult += currButton.repeat(keyIndex);
          lastButton = currButton;
        }
      }

      return currResult;
    })
    .join("");
}

function isLowerCaseLetter(char: string): boolean {
  return /[a-z]/.test(char);
}

function isUpperCaseLetter(char: string): boolean {
  return /[A-Z]/.test(char);
}

Feedback

Schreib mir!