Java中的简单摩尔斯电码翻译

莫尔斯电码使用点和破折号序列对文本字符进行编码,以表示字母、数字和标点符号。Samuel Morse 和 Alfred Vail 在 1830 年代初开发了它用于电报用途。

在本教程中,我们将编写一个将英语翻译为摩尔斯电码的方法。然后,我们将编写执行相反操作的方法。

 什么是摩尔斯电码?
在莫尔斯电码中,每个字母都由短信号(点)和长信号(破折号)的独特组合表示,允许通过一系列开关信号进行通信。

按照通常的用法,我们用“.”来表示点。”并带有“ - ”破折号。这两个字符足以写出整个莫尔斯字母。

然而,我们还需要更多的东西来写句子。由于莫尔斯电码确实针对非书面通信,因此流程对于解密莫尔斯电码至关重要。因此,负责传输莫尔斯电文的操作员会在每个字母之间留下短暂的停顿。此外,他会在每个词之间留出更长的停顿。因此,不考虑这些暂停的表示将不允许解码。

常见的选择是留一个空格“ ”来表示每个单词之间的停顿。我们还将使用“ / ”来编码两个单词之间的空格字符。由于斜杠也是一个字符,因此它周围会出现空格,就像其他字符一样。

英语和莫尔斯电码之间的双向映射
为了轻松地从英语翻译成莫尔斯电码,或者反向翻译,我们希望在两个字母之间有一个双向映射。因此,我们将使用 Apache Commons Collection 的BidiMap数据结构。它是一个允许按键或按值访问的映射。这样,我们将把它用于两种翻译方法。

但是,如果我们只想以一种方式进行翻译,我们会直接使用Map。

首先,让我们在pom.xml中包含该库的最新版本:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.4</version>
</dependency>

我们现在可以创建映射并在静态块中初始化它:

public class MorseTranslator {
    
    private static final BidiMap<String, String> morseAlphabet = new DualHashBidiMap<>();
    
    static {
        morseAlphabet.put("A", ".-");
        morseAlphabet.put(
"B", "-...");
        morseAlphabet.put(
"C", "-.-.");
        morseAlphabet.put(
"D", "-..");
        morseAlphabet.put(
"E", ".");
        morseAlphabet.put(
"F", "..-.");
        morseAlphabet.put(
"G", "--.");
        morseAlphabet.put(
"H", "....");
        morseAlphabet.put(
"I", "..");
       
// etc
        morseAlphabet.put(
" ", "/");
    }
    
}

请注意,我们添加了空白字符的翻译。此外,我们限制使用字母、数字和标点符号。如果我们还想使用重音字符,则需要使用其他数据结构或做出选择,因为各种重音字符可以匹配相同的摩尔斯电码。例如,“ à ”和“ å ”都对应于摩尔斯电码中的“ .–.- ”。

将英语翻译成莫尔斯电码
首先,让我们编写一个将英语句子翻译成摩尔斯电码的方法。

 通用算法
我们的BidiMap仅包含大写字母,因为大写不会改变翻译。因此,我们将从单词大写开始。然后,我们将迭代这些字母并一一翻译它们:

static String englishToMorse(String english) {
    String upperCaseEnglish = english.toUpperCase();
    String[] morse = new String[upperCaseEnglish.length()];
    for (int index = 0; index < upperCaseEnglish.length(); index++) {
        String morseCharacter = morseAlphabet.get(String.valueOf(upperCaseEnglish.charAt(index)));
        morse[index] = morseCharacter;
    }
    return String.join(" ", morse);
}

将翻译存储到Morse String数组中很方便。该中间数组具有与输入中的字符数一样多的值。最后,我们使用String.join()方法连接所有条目,并使用空格作为分隔符。

我们现在可以测试我们的方法。由于我们想检查大小写并不重要,因此我们将编写一个参数化测试,其中包含期望相同输出的各种输入:

@ParameterizedTest
@ValueSource(strings = {"MORSE CODE!", "morse code!", "mOrSe cOdE!"})
void givenAValidEnglishWordWhateverTheCapitalization_whenEnglishToMorse_thenTranslatedToMorse(String english) {
    assertEquals(
"-- --- .-. ... . / -.-. --- -.. . -.-.-----.", MorseTranslator.englishToMorse(english));
}

此外,我们可以注意到两个单词之间的空格如预期一样翻译为“ / ”。

边缘情况
目前,我们的程序没有考虑潜在的格式错误的输入。但是,我们希望拒绝包含无效字符的句子。在这种情况下,我们将抛出IllegalArgumentException:

String morseCharacter = morseAlphabet.get(String.valueOf(upperCaseEnglish.charAt(index)));
if (morseCharacter == null) {
    throw new IllegalArgumentException("Character " + upperCaseEnglish.charAt(index) + " can't be translated to morse");
}
morse[index] = morseCharacter;

修改非常简单,因为如果字符无效,则它不会作为双向映射的键出现。因此,get()方法返回null。我们还可以在我们的方法之上添加空安全检查。简而言之,我们的最终方法如下:

static String englishToMorse(String english) {
    if (english == null) {
        return null;
    }
    String upperCaseEnglish = english.toUpperCase();
    String[] morse = new String[upperCaseEnglish.length()];
    for (int index = 0; index < upperCaseEnglish.length(); index++) {
        String morseCharacter = morseAlphabet.get(String.valueOf(upperCaseEnglish.charAt(index)));
        if (morseCharacter == null) {
            throw new IllegalArgumentException("Character " + upperCaseEnglish.charAt(index) + " can't be translated to morse");
        }
        morse[index] = morseCharacter;
    }
    return String.join(
" ", morse);
}

最后,我们可以添加一个带有不可翻译句子的单元测试:

@Test
void givenAnEnglishWordWithAnIllegalCharacter_whenEnglishToMorse_thenThrows() {
    String english = "~This sentence starts with an illegal character";
    assertThrows(IllegalArgumentException.class, () -> MorseTranslator.englishToMorse(english));

将摩尔斯电码翻译成英语
现在让我们编写相反的方法。在深入研究边缘情况之前,我们将再次关注大局。

通用算法
概念是相同的:对于每个摩尔斯字符,我们在BidiMap中找到英文翻译。getKey ()方法允许我们做到这一点。然后,我们需要迭代每个摩尔斯字符:

static String morseToEnglish(String morse) {
    String[] morseUnitCharacters = morse.split(" ");
    StringBuilder stringBuilder = new StringBuilder();
    for (int index = 0; index < morseUnitCharacters.length; index ++) {
        String englishCharacter = morseAlphabet.getKey(morseUnitCharacters[index]);
        stringBuilder.append(englishCharacter);
    }
    return stringBuilder.toString();
}

借助String.split()方法,我们隔离了每个莫尔斯电码字符。将每个英文翻译附加到StringBuilder是连接结果的最有效方法。

现在让我们验证我们的方法是否返回正确的结果:

@Test
void givenAValidMorseWord_whenMorseToEnglish_thenTranslatedToUpperCaseEnglish() {
    assertEquals("MORSE CODE!", MorseTranslator.morseToEnglish("-- --- .-. ... . / -.-. --- -.. . -.-.-----."));
}

最后,我们可以回忆一下,输出始终是大写字母。

边缘情况
此外,我们希望拒绝包含无效莫尔斯字符的输入。就像在englishToMorse()中一样,在这种情况下我们将抛出IllegalArgumentException。此外,我们还可以处理空输入的特定情况。在这里,由于split()方法的内部功能,我们还必须单独处理空输入。

回顾一下,让我们编写最终的方法:

static String morseToEnglish(String morse) {
    if (morse == null) {
        return null;
    }
    if (morse.isEmpty()) {
        return "";
    }
    String[] morseUnitCharacters = morse.split(
" ");
    StringBuilder stringBuilder = new StringBuilder();
    for (int index = 0; index < morseUnitCharacters.length; index ++) {
        String englishCharacter = morseAlphabet.getKey(morseUnitCharacters[index]);
        if (englishCharacter == null) {
            throw new IllegalArgumentException(
"Character " + morseUnitCharacters[index] + " is not a valid morse character");
        }
        stringBuilder.append(englishCharacter);
    }
    return stringBuilder.toString();
}

处理无效字符与前一种情况一样简单,因为如果莫尔斯电码与BidiMap的任何值都不匹配,则getKey()方法将返回null。

最后,我们还可以测试错误情况:

@Test
void givenAMorseWordWithAnIllegalCharacter_whenMorseToEnglish_thenThrows() {
    assertThrows(IllegalArgumentException.class, () -> MorseTranslator.morseToEnglish(".!!!!!!!"));
}

结论
在本文中,我们了解了摩尔斯电码并编写了一个简单的摩尔斯电码和英语之间的双向翻译器。大多数考虑因素都不是特定于莫尔斯电码的,因此我们可以使我们的代码更加通用,以处理可以定义与英语双向映射的任何语言。