Martin
Martin

Reputation: 57

Translate morse code string which has single spaces between letters and three spaces between words

I'm writing a function that decodes a morse code message to plain text. The problem I'm running into, is that it doesn't add spaces where needed. Keep in mind that every morse code character/letter, is separated with a blank space and every full word, is separated by 3 spaces. I want the function to add a single blank space to the plain text when it detects 3 blank spaces in a row in the morse code. I can't figure it out so I've come here.

Here is the function as it is now.

public function decode($morseCode)
{
    // 1 space between characters, 3 spaces between words

    // split the string
    $morseArray = preg_split('/\s+/', $morseCode, -1);

    $plainText = '';
    $blankSpaceCount = 0;
    // set each string as a morse code character
    foreach ($morseArray as $morseChar) 
    {
        if ($morseChar != ' ')
        {
            // check if the morsecode character is in the array
            if(isset($this->plainTextsByMorseCode[$morseChar]))
            {
                // if it is, convert it to the corresponding plain text
                $plainText .= $this->plainTextsByMorseCode[$morseChar];
                $blankSpaceCount = 0;
            }
        }           
        else {
            $blankSpaceCount++;
        
            if ($blankSpaceCount === 3) 
            {
                $plainText .= ' ';  // Append a single blank space
                $blankSpaceCount = 0;
            }
        }
    }           

    return trim($plainText);
}

If it helps, the phrase I'm trying to decode is as follows:

-.-- --- ..-   ... .... .- .-.. .-..   -. --- -   .--. .- ... ...

It says "YOU SHALL NOT PASS" and you can clearly see the triple blank spaces between the words and the singular blank space between the letters.

Upvotes: 1

Views: 319

Answers (5)

Ulrich Eckhardt
Ulrich Eckhardt

Reputation: 17415

No need to go the regex way, just define a substitution. Every valid morse code sequence, with an optional trailing space, gets replaced by the according Latin letter. Note that the trailing space must be exactly zero or one space, zero spaces after the last letter. A triple space separating two morse letters will then become a single space in the output. For simplicity, you can pad the input string with a space at the end.

function morse_decode(string $morse): string
{
    if (!$morse) {
        return '';
    }

    $padded = $morse . ' ';
    // https://www.php.net/manual/en/function.str-replace
    $res = str_replace(
        [
            '--- ',
            '..- ',
            '-.-- ',
            // ...
            '  '
        ], [
            'O',
            'U',
            'Y',
            // ...
            ' '
        ],
        $padded
    );

    // https://www.php.net/manual/en/function.strspn.php
    if (strspn($res, ' ABCDEFGHIJKLMNOPQRSTUVWXYZ') !== strlen($res)) {
        throw new DomainException('input is not valid morse code');
    }

    return $res;
}

Upvotes: 1

mickmackusa
mickmackusa

Reputation: 47903

Don't bother creating any temporary arrays and doing conditional iterations and other such convolutions.

You are translating a regularly formatted string into another string by leveraging a lookup array -- this is an ideal scenario for preg_replace_callback() and/or strtr().

If this were my project, I'd probably declare $plainTextsByMorseCode as a class-level constant instead of a class variable.

Code: (Demo)

class MyMorse
{
    private array $plainTextsByMorseCode = [
        '.-' => 'A',
        '....' => 'H',
        '.-..' => 'L',
        '-.' => 'N',
        '---' => 'O',
        '.--.' => 'P',
        '...' => 'S',
        '-' => 'T',
        '..-' => 'U',
        '-.--' => 'Y',
    ];

    public function decode(string $morseCode): string
    {
        return preg_replace_callback(
                   '/ ?(\S+)| {3}/',
                   fn($m) => ctype_space($m[0])
                       ? ' ' // reduce 3 spaces to 1
                       : $this->plainTextsByMorseCode[$m[1]], // translate
                   $morseCode
               );
    }
}

Or the decode() method could be done in two steps (translate, then handle spacing) if you find that clearer. (Demo)

public function decode(string $morseCode): string
{
    return strtr(
        preg_replace_callback('/[.-]+/', fn($m) => $this->plainTextsByMorseCode[$m[0]], $morseCode),
        ['   ' => ' ', ' ' => '']
    );
}

In fact, this translation task can be completely handled by strtr(). (Demo)

public function decode(string $morseCode): string
{
    return strtr($morseCode, $this->plainTextsByMorseCode + ['   ' => ' ', ' ' => '']);
}

To call:

$obj = new MyMorse();
var_export(
    $obj->decode('-.-- --- ..-   ... .... .- .-.. .-..   -. --- -   .--. .- ... ...')
);

Output (with var_export() to show no lingering/unwanted spaces):

'YOU SHALL NOT PASS'

The regex pattern matches an optional space before a sequence of non-whitespace characters and captures the visible characters; or it matches 3 consecutive spaces. In the callback function, if the match is only spaces, then return a single space (word separator); otherwise translate the sequence, omit the leading space, and return the letter.

Here is the inverse operation to encode plain text as morse code

Upvotes: 1

gview
gview

Reputation: 15361

As @Thefourthbird has pointed out, you are splitting into an array using spaces (regardless of the number) and then you are trying to count spaces again after that. It's irrelevant because you already parsed the words into an array of letters.

This simple test illustrates the mistake in your logic and the proper regex to break your cipher into an array of words.

<?php

$morseCode = '-.-- --- ..-   ... .... .- .-.. .-..   -. --- -   .--. .- ... ...';

$morseArray = preg_split('/\s{3}/', $morseCode, -1);

var_dump($morseArray);

The output is:

array(4) {
  [0]=>
  string(12) "-.-- --- ..-"
  [1]=>
  string(21) "... .... .- .-.. .-.."
  [2]=>
  string(8) "-. --- -"
  [3]=>
  string(15) ".--. .- ... ..."
}

Once you have the array of words, you are only left to convert to letters, and add a space back for all but the final word in the word array.

Upvotes: 0

The fourth bird
The fourth bird

Reputation: 163362

After the splitting, this part is always true if ($morseChar != ' ') so you will not reach the else clause.

Here is another take on it, mapping the morse to the plain text characters exploding first on 3 spaces and then on a single space.

public function decode($morseCode)
{
    $decodedWords = [];

    foreach (explode('   ', $morseCode) as $morseWord) {
        $decodedWords[] = implode('', array_map(
            fn($morse) => $this->plainTextsByMorseCode[$morse] ?? '',
            explode(' ', $morseWord)
        ));
    }

    return implode(' ', $decodedWords);
}

The output will be

YOU SHALL NOT PASS

Upvotes: 0

Michel
Michel

Reputation: 4157

$morsecode='your morse code';

//empty array for decode words
$decoded_words=[];

//split the code in words on 3 spaces
$morse_words=explode('   ',$morsecode); //3 spaces

//loop the words
foreach($morse_words as $word){
    //empty string for decoded characters
    $plain_word='';
    
    //split the word into characters on 1 space
    $morse_chars=explode(' ',$word); //1 space
    
    //loop the characters, decode and add to the plain word string
    foreach($morse_chars as $char){
        $plain_word.=decode_morse_character($char);
        }
    //Add decoded word to the words array
    $decoded_words[]=$plain_word;
    
    }

//implode the decoded_words array with a space between every word
$decoded=implode(' ',$decoded_words);

Upvotes: 0

Related Questions