Convert a String to Morse Code in C#

10 minutes

Table of Contents

Introduction

Converting text to Morse code is one of those simple exercises that sharpens your thinking around dictionaries, string manipulation, and control flow. It also gives you a taste of how encoding systems work behind the scenes.

In this article, we will walk through how to create a Morse Code converter from scratch in C#, exploring the encoding logic, and showing how even a lightweight exercise like this can deepen your understanding of the C# language. Above all, it is fun!


Requirements

As mentioned in Parsing Integers Manually article, having strong requirements defined up front can save you time and effort later, and avoid costly refactoring. Therefore we need to come up with the requirements for converting C# strings to and from Morse code, whilst accounting for edge cases and quirks.

Morse Code dictionary

Before we start, we need to know how to represent text as Morse Code, and also what characters are supported by the morse code alphabet. Below is a dictionary of symbols that make up each valid alphabetical letter and number in Morse Code.

String symbol Morse Code symbol
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--..
0-----
1.----
2..---
3...--
4....-
5.....
6-....
7--...
8---..
9----.

Converting String to Morse Code requirements

Below we have listed the requirements for converting a C# string to Morse code.

  • Only characters from A-Z, a-z, or 0-9 are valid as input
  • Space between words shall consist of a single space character
  • Extra whitespace before or after words is not valid
  • Empty input is not valid
  • Null input is not valid

Testing

Now that we have the requirements for valid input for converting a C# String to Morse Code, we can define our test cases below.

Valid C# String to Morse Code test cases

Below are the test cases which will be used to determine whether a C# string input is valid Morse Code.

Test case Test input Expected output
Single letter A .-
Multiple letters with spaces A B C D .- / -... / -.-. / -..
Common phrase HELLO WORLD .... . .-.. .-.. --- / .-- --- .-. .-.. -..
Extended sentence THERE IS NO SPOON - .... . .-. . / .. ... / -. --- / ... .--. --- --- -.
Sentence with duplicate words (one at end) Dan da Dan -.. .- -. / -.. .- / -.. .- -.

Invalid C# String to Morse Code test cases

Below are the test cases which will be used to determine whether a C# String input is invalid Morse Code.

Test case Test input Expected output
Null input null Message cannot be null
Empty input "" Message cannot be empty
Extra whitespace between words There is      no spoon Message contains extra whitespace between words
Invalid characters !
Whitespace only      Message cannot be whitespace
Whitespace in prefix  .... Message cannot start or end with spaces
Whitespace in suffix ....  Message cannot start or end with spaces
Whitespace on both sides  ....  Message cannot start or end with spaces

Converting the test cases to C#

String to Morse Code C# test cases

public sealed class StringToMorseCodeConverterTests
{
    [Theory]
    [MemberData(nameof(StringToMorseCodeTestCases.ValidMessages), MemberType = typeof(StringToMorseCodeTestCases))]
    public void For_StringToMorseCodeConverter_When_TryConvert_With_Valid_MorseCode_Then_Return_Expected_Message(string message, string expectedMessage)
    {
        StringToMorseCodeConverterResult.Success expectedResult = new(expectedMessage);
        StringToMorseCodeConverterResult actualResult = StringToMorseCodeConverter.TryConvert(message);
        Assert.Equal(expectedResult, actualResult);
    }

    [Theory]
    [MemberData(nameof(StringToMorseCodeTestCases.InvalidMessages), MemberType = typeof(StringToMorseCodeTestCases))]
    public void For_StringToMorseCodeConverter_When_TryConvert_With_Invalid_MorseCode_Then_Return_Error_Message(string? message, string expectedErrorMessage)
    {
        StringToMorseCodeConverterResult.Failure expectedResult = new(expectedErrorMessage);
        StringToMorseCodeConverterResult actualResult = StringToMorseCodeConverter.TryConvert(message);
        Assert.Equal(expectedResult, actualResult);
    }
    
    private static class StringToMorseCodeTestCases
    {
        public static readonly TheoryData<string, string> ValidMessages = new()
        {
            { "A", ".-" },
            { "A B C D", ".- / -... / -.-. / -.." },
            { "HELLO WORLD", ".... . .-.. .-.. --- / .-- --- .-. .-.. -.." },
            { "THERE IS NO SPOON", "- .... . .-. . / .. ... / -. --- / ... .--. --- --- -." },
            { "Dan da Dan", "-.. .- -. / -.. .- / -.. .- -." }
        };

        public static readonly TheoryData<string?, string> InvalidMessages = new()
        {
            { null, IsNull },
            { InvalidMessage.ExtraWhitespaceInBetweenWords, ExtraWhitespaceBetweenWords },
            { InvalidMessage.Empty, IsEmpty },
            { InvalidMessage.InvalidCharacters, InvalidCharacters },
            { InvalidMessage.WhitespaceOnly, IsWhitespace },
            { InvalidMessage.WhitespacePrefix, StartOrEndingSpaces },
            { InvalidMessage.WhitespaceSuffix, StartOrEndingSpaces },
            { InvalidMessage.WhitespaceBothSides, StartOrEndingSpaces }
        };
    }
}

Implementing the C# String to Morse Code converter

Below we will break down the implementation of the C# String to Morse Code converter. You can see the full source code of the implementation later on as well.

Defining the map between C# characters and Morse Code strings

First we define the map between an alphabetical character or number, and the representative Morse Code string. We can do this via C# Dictionary collection.

private static readonly Dictionary<char, string> _alphabet = new()
{
    ['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'] = "--..",
    ['0'] = "-----",
    ['1'] = ".----",
    ['2'] = "..---",
    ['3'] = "...--",
    ['4'] = "....-",
    ['5'] = ".....",
    ['6'] = "-....",
    ['7'] = "--...",
    ['8'] = "---..",
    ['9'] = "----."
};

The key is of the type char, and the value is of the type string.

Define character and word constants

Next we will define some constants we can use later on in our converter function.

private const char _morseCodeLetterSeparator = ' ';
private const string _morseCodeWordSeparator = " / ";
private const string _wordSeparator = " ";
  • _morseCodeLetterSeparator is used to denote the character separating Morse Code letters
  • _morseCodeWordSeparator is used to denote the characters separating Morse Code words
  • _wordSeparator is used to denote the character separating the words in the input

Defining the convert function and validating input

Now we will define our converter function and validate to initially weed out bad input.

public static StringToMorseCodeConverterResult TryConvert(string? message)
{
    if (message is null)
    {
        return new StringToMorseCodeConverterResult.Failure(IsNull);
    }

    if (message == string.Empty)
    {
        return new StringToMorseCodeConverterResult.Failure(IsEmpty);
    }

    if (string.IsNullOrWhiteSpace(message))
    {
        return new StringToMorseCodeConverterResult.Failure(IsWhitespace);
    }

    if (message.Trim().Length != message.Length)
    {
        return new StringToMorseCodeConverterResult.Failure(StartOrEndingSpaces);
    }

As our function can fail to convert, we define our TryConvert method with a string parameter for the input message, and a return type of StringToMorseCodeConverterResult. This return type can either be a success or failure type. Success will contain the converted message, and a failure will contain an error message.

In the intial body of the method we are checking for common bad input, such as if it was null, empty, or whitespace. The final boolean expression message.Trim().Length != message.Length enforces our requirement that any input must not have any whitespace preceding or following the message by trimming the message and then checking its initial length against it's trimmed length. If there is a disparity, it will indicate there was whitespace preceding or following the message.

Checking for invalid spaces between words

We then move on to checking for invalid spaces between words in our input message.

string[] words = message.Split(_wordSeparator);
if (words.Any(string.IsNullOrWhiteSpace))
{
    return new StringToMorseCodeConverterResult.Failure(ExtraWhitespaceBetweenWords);
}

Breaking up the message into words is easy using the string Split method. This will give us an array of strings delimited by our _wordSeparator we defined earlier. What is helpful is that if there is any extra spaces between words, we will have elements of that string array which are just a space character. We utilise that quirk in the check below the split using the LINQ Any method, along with string.IsNullOrWhiteSpace. If there any matches, we can then return an error.

Building the Morse Code from the input message

Now that we completed the intial bad input checks, we will build our Morse Code from our input message.

StringBuilder result = new();

for (int index = 0; index < words.Length; index++)
{
    string word = words[index];
    IList<string> morseCodeWord = new List<string>();

    foreach (char letter in word)
    {
        char normalisedLetter = char.ToUpperInvariant(letter);
        if (!_alphabet.TryGetValue(normalisedLetter, out var morseCodeCharacter))
        {
            return new StringToMorseCodeConverterResult.Failure(InvalidCharacters);
        }

        morseCodeWord.Add(morseCodeCharacter);
    }

    result.Append(string.Join(_morseCodeLetterSeparator, morseCodeWord));

    if (index < words.Length - 1)
    {
        result.Append(_morseCodeWordSeparator);
    }
}

Firstly we declare a result variable, of the StringBuilder type. A string builder is preferred over plain string concatenation in C# when you are performing a lot of string manipulations, especially in loops, because strings are immutable. Every + or string.Join creates a new string in memory, which adds overhead. StringBuilder avoids that by keeping a mutable buffer, so it is faster and uses less memory.

foreach (string word in words)
{
    IList<string> morseCodeWord = new List<string>();

Next we begin to loop through each word in our input message, and for each iteration of the loop we define a string list morseCodeWord to store the individual sequence of strings that make up our Morse Code word.

foreach (char letter in word)
{
    char normalisedLetter = char.ToUpperInvariant(letter);
    if (!_alphabet.TryGetValue(normalisedLetter, out var morseCodeCharacter))
    {
        return new StringToMorseCodeConverterResult.Failure(InvalidCharacters);
    }

    morseCodeWord.Add(morseCodeCharacter);
}

Then we loop through each character in the input word, and to prevent lowercase letters from not matching our valid character alphabet we defined earlier, we convert the character to uppercase, followed by then checking if the character exists in the alphabet we defined earlier. If there is no match for the character within our alphabet, we return an error and exit the method.

If the character is in the alphabet via a true result from the dictionary TryGetValue method, then we add the corresponding Morse Code character from the alphabet dictionary (stored in the morseCodeCharacter variable) to the morseCodeWord list.

Scoping rules around out variables

if (!_alphabet.TryGetValue(normalisedLetter, out var morseCodeCharacter))
{
    return new StringToMorseCodeConverterResult.Failure(InvalidCharacters);
}

morseCodeWord.Add(morseCodeCharacter);

The morseCodeCharacter variable might look like it is not in scope, because it is defined inside the if statement. You would logically then think it was only in the scope of the if statement itself. This is not the case however due to the scoping rules around out variables inside if expressions. They do no restrict the out variable's scope to just the method call. Instead, it allows you to declare and assign the variable as part of the method call, but the variable stays in scope for the entire surrounding block, not just inside the if statement.

result.Append(string.Join(_morseCodeLetterSeparator, morseCodeWord));

if (index < words.Length - 1)
{
    result.Append(_morseCodeWordSeparator);
}

In the final part of the loop for processing the words for the input message is to append all of the Morse Code characters that make up the word that we have in the morseCodeWord list, joined using the Morse Code letter separator we defined earlier in the _morseCodeLetterSeparator variable, and then append to the string builder result variable. This is accomplished by using the handy string.Join method.

We then have one last check to see if the word we are currently scrutinising is the last word in our message, and when it is not, we add a Morse Code word separator to the result string builder variable. That way, we have our Morse Code words separated correctly. This is accomplished by checking the index variable of our for loop to determine it is the last one. We do this by subtracting one from the length of the words list. That is necessary because C# is zero-based, therefore the length will always be one ahead.

Putting the result together

At this point we have exited both the letter and character loops and all is left to do is to return the converted Morse Code string.

return new StringToMorseCodeConverterResult.Success(result.ToString());

For string builders it is easy to get a resulting string as we can just call its ToString method. We are doing this, along with returning a new success result.


Summary

Phew! That was a ride. We hope you have enjoyed implementing a Morse code converter in C#. This fun exercise can be used to reinforce practical knowledge of encoding, validation, data structures, and clean coding.

Here are some key learning outcomes are summarised below:

  • Control flow: Managed conversion logic with clear branching and error handling
  • Dictionaries: Practiced Dictionary for efficient lookups
  • Encoding basics: Learned character-to-symbol mapping via Morse code
  • Performance: Used StringBuilder for efficient string construction
  • String handling: Used Split, Trim, and ToUpperInvariant effectively
  • Validation: Applied thorough checks for input correctness

Thank you for visiting!


Full implementation code

Below you can find the full implementation of the C# String to Morse Code converter.

internal static class StringToMorseCodeConverter
{
    private static readonly Dictionary<char, string> _alphabet = new()
    {
        ['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'] = "--..",
        ['0'] = "-----",
        ['1'] = ".----",
        ['2'] = "..---",
        ['3'] = "...--",
        ['4'] = "....-",
        ['5'] = ".....",
        ['6'] = "-....",
        ['7'] = "--...",
        ['8'] = "---..",
        ['9'] = "----."
    };
    
    private const char _morseCodeLetterSeparator = ' ';
    private const string _morseCodeWordSeparator = " / ";
    private const string _wordSeparator = " ";

    public static StringToMorseCodeConverterResult TryConvert(string? message)
    {
        if (message is null)
        {
            return new StringToMorseCodeConverterResult.Failure(IsNull);
        }
        
        if (message == string.Empty)
        {
            return new StringToMorseCodeConverterResult.Failure(IsEmpty);
        }
        
        if (string.IsNullOrWhiteSpace(message))
        {
            return new StringToMorseCodeConverterResult.Failure(IsWhitespace);
        }

        if (message.Trim().Length != message.Length)
        {
            return new StringToMorseCodeConverterResult.Failure(StartOrEndingSpaces);
        }
        
        string[] words = message.Split(_wordSeparator);
        if (words.Any(string.IsNullOrWhiteSpace))
        {
            return new StringToMorseCodeConverterResult.Failure(ExtraWhitespaceBetweenWords);
        }
        
        StringBuilder result = new();

        for (int index = 0; index < words.Length; index++)
        {
            string word = words[index];
            IList<string> morseCodeWord = new List<string>();

            foreach (char letter in word)
            {
                char normalisedLetter = char.ToUpperInvariant(letter);
                if (!_alphabet.TryGetValue(normalisedLetter, out var morseCodeCharacter))
                {
                    return new StringToMorseCodeConverterResult.Failure(InvalidCharacters);
                }

                morseCodeWord.Add(morseCodeCharacter);
            }

            result.Append(string.Join(_morseCodeLetterSeparator, morseCodeWord));

            if (index < words.Length - 1)
            {
                result.Append(_morseCodeWordSeparator);
            }
        }

        return new StringToMorseCodeConverterResult.Success(result.ToString());
    }
}

Part of the Fun Exercises series
Filed under Extras, and tagged under Characters , Strings

View the source code for this article on GitHub